package wechat import ( "context" "io" "os" "server/utility/ecode" "sync" "time" "github.com/go-resty/resty/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/google/uuid" ) type weChatClient struct { AppId string AppSecret string TicketExpire int Token string accessToken string expiresIn int lastUpdated time.Time mu sync.RWMutex } var ( instance *weChatClient once sync.Once ) func GetWeChatClient() *weChatClient { return instance } func init() { once.Do(func() { ctx := context.Background() instance = &weChatClient{ AppId: g.Config().MustGet(ctx, "wechat.appId").String(), AppSecret: g.Config().MustGet(ctx, "wechat.appSecret").String(), TicketExpire: g.Config().MustGet(ctx, "wechat.ticketExpire").Int(), Token: g.Config().MustGet(ctx, "wechat.token").String(), } go instance.autoRefreshToken(ctx) }) } func (c *weChatClient) getAccessToken() error { result := struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` }{} ctx := context.Background() resp, err := resty.New().R(). SetQueryParams(g.MapStrStr{ "grant_type": "client_credential", "appid": c.AppId, "secret": c.AppSecret, }). SetResult(&result). Get("https://api.weixin.qq.com/cgi-bin/token") if err != nil { glog.Errorf(ctx, "发起 get access_token 请求出现异常: %+v", err) return ecode.Fail.Sub("发起 get access_token 请求出现异常") } if resp.StatusCode() != 200 { glog.Errorf(ctx, "获取微信 access_token 响应异常: %+v", resp.Status()) return ecode.Fail.Sub("获取微信 access_token 失败") } c.mu.Lock() defer c.mu.Unlock() c.accessToken = result.AccessToken c.expiresIn = result.ExpiresIn c.lastUpdated = time.Now() glog.Infof(ctx, "获取微信 access_token 成功: %+v", c.accessToken) return nil } func (c *weChatClient) autoRefreshToken(ctx context.Context) { for { // 初次获取 if err := c.getAccessToken(); err != nil { time.Sleep(1 * time.Minute) continue } // 等待 expiresIn - 300 秒(即提前 5 分钟刷新) c.mu.RLock() expiresIn := c.expiresIn c.mu.RUnlock() refreshAfter := time.Duration(expiresIn-300) * time.Second time.Sleep(refreshAfter) } } func (c *weChatClient) GetTicket(sceneId string) (string, error) { body := map[string]interface{}{ "expire_seconds": c.TicketExpire, "action_name": "QR_STR_SCENE", "action_info": map[string]interface{}{ "scene": map[string]string{ "scene_str": sceneId, }, }, } result := struct { Ticket string `json:"ticket"` ExpireSeconds int `json:"expire_seconds"` Url string `json:"url"` }{} resp, err := resty.New().R(). SetQueryParams(g.MapStrStr{"access_token": c.accessToken}). SetBody(body). SetResult(&result). Post("https://api.weixin.qq.com/cgi-bin/qrcode/create") if err != nil { glog.Errorf(context.Background(), "发起 get ticket 请求出现异常: %+v", err) return "", ecode.Fail.Sub("发起 get ticket 请求出现异常") } if resp.StatusCode() != 200 { glog.Errorf(context.Background(), "获取微信 ticket 响应异常: %+v", resp.Status()) return "", ecode.Fail.Sub("获取微信 ticket 失败") } glog.Infof(context.Background(), "获取微信 ticket 成功: %+v", result.Ticket) return result.Ticket, nil } func (c *weChatClient) GetQrCode(ticket string) (imagePath string, err error) { qrCodeUrl := "https://mp.weixin.qq.com/cgi-bin/showqrcode" resp, err := resty.New().R(). SetDoNotParseResponse(true). SetQueryParams(g.MapStrStr{"ticket": ticket}). Get(qrCodeUrl) if err != nil { glog.Errorf(context.Background(), "获取二维码图片请求异常: %+v", err) return "", ecode.Fail.Sub("获取二维码图片请求失败") } if resp.StatusCode() != 200 { glog.Errorf(context.Background(), "获取二维码图片响应异常: %+v", resp.Status()) return "", ecode.Fail.Sub("获取二维码图片失败") } imagePath = uuid.New().String() + ".jpg" data, readErr := io.ReadAll(resp.RawBody()) if readErr != nil { glog.Errorf(context.Background(), "读取二维码图片失败: %+v", readErr) return "", ecode.Fail.Sub("读取二维码图片失败") } defer resp.RawBody().Close() writeErr := os.WriteFile(imagePath, data, 0644) if writeErr != nil { glog.Errorf(context.Background(), "保存二维码图片失败: %+v", writeErr) return "", ecode.Fail.Sub("保存二维码图片失败") } glog.Infof(context.Background(), "二维码图片保存成功: %s", imagePath) return imagePath, nil } func (c *weChatClient) GetToken() string { return c.Token } func (c *weChatClient) GetUserUnionId(ctx context.Context, openid string) (unionId string, err error) { // TODO 获取唯一UnionId //result := struct { // UnionId string `json:"unionid"` //}{} // //resp, err := resty.New().R(). // SetQueryParams(g.MapStrStr{"access_token": c.accessToken}).SetQueryParam("openid", openid). // SetResult(&result). // Get("https://api.weixin.qq.com/cgi-bin/user/info") // //if err != nil { // glog.Errorf(ctx, "发起 get ticket 请求出现异常: %+v", err) // return "", ecode.Fail.Sub("发起 get ticket 请求出现异常") //} //if resp.StatusCode() != 200 { // glog.Errorf(ctx, "获取微信 ticket 响应异常: %+v", resp.Status()) // return "", ecode.Fail.Sub("获取微信 ticket 失败") //} return openid, nil }