Files
arenax-server/utility/wechat/wechat.go
2025-06-23 16:07:45 +08:00

274 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package wechat
import (
"context"
"github.com/gogf/gf/v2/encoding/gjson"
"io"
"os"
"server/internal/consts"
"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 (c *weChatClient) loadCachedToken(ctx context.Context) bool {
val, err := g.Redis().Get(ctx, consts.WechatAccessTokenKey)
if err != nil || val.IsEmpty() {
return false
}
var data struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
}
if err := gjson.DecodeTo(val.String(), &data); err != nil {
return false
}
// 如果当前时间超过过期时间 - 5分钟则视为即将过期
if time.Now().Unix() >= data.ExpiresAt-300 {
return false
}
c.mu.Lock()
c.accessToken = data.Token
c.expiresIn = int(data.ExpiresAt - time.Now().Unix())
c.lastUpdated = time.Now()
c.mu.Unlock()
glog.Infof(ctx, "[loadCachedToken] 成功加载 Redis 中的 access_token: %s", data.Token)
return true
}
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 {
ctx := context.Background()
result := struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"` // 一般为 7200 秒
}{}
url := "https://api.weixin.qq.com/cgi-bin/token"
resp, err := resty.New().R().
SetQueryParams(g.MapStrStr{
"grant_type": "client_credential",
"appid": c.AppId,
"secret": c.AppSecret,
}).
SetResult(&result).
Get(url)
if err != nil {
glog.Errorf(ctx, "[getAccessToken] 请求异常, URL: %s, err: %+v", url, err)
return ecode.Fail.Sub("发起 get access_token 请求出现异常")
}
if resp.IsError() {
glog.Errorf(ctx, "[getAccessToken] 响应错误, 状态: %s, 内容: %s", resp.Status(), resp.String())
return ecode.Fail.Sub("获取 access_token 失败")
}
// 写入内存
c.mu.Lock()
c.accessToken = result.AccessToken
c.expiresIn = result.ExpiresIn
c.lastUpdated = time.Now()
c.mu.Unlock()
// 写入 Redis
expiresAt := time.Now().Unix() + int64(result.ExpiresIn)
cacheData := g.Map{
"token": result.AccessToken,
"expires_at": expiresAt,
}
jsonStr, _ := gjson.Encode(cacheData)
ttl := result.ExpiresIn - 300
if ttl < 60 {
ttl = 60
}
err = g.Redis().SetEX(ctx, consts.WechatAccessTokenKey, jsonStr, int64(ttl))
if err != nil {
glog.Warningf(ctx, "[getAccessToken] Redis 缓存失败: %+v", err)
} else {
glog.Infof(ctx, "[getAccessToken] 成功写入 Redisexpires_in: %ds", result.ExpiresIn)
}
glog.Infof(ctx, "[getAccessToken] 成功获取 access_token: %sexpires_in: %ds", result.AccessToken, result.ExpiresIn)
return nil
}
func (c *weChatClient) autoRefreshToken(ctx context.Context) {
if c.loadCachedToken(ctx) {
glog.Infof(ctx, "[autoRefreshToken] 成功加载缓存 token")
} else {
if err := c.getAccessToken(); err != nil {
glog.Errorf(ctx, "[autoRefreshToken] 初次获取 token 失败: %+v", err)
time.Sleep(1 * time.Minute)
}
}
for {
c.mu.RLock()
expiresIn := c.expiresIn
c.mu.RUnlock()
refreshAfter := time.Duration(expiresIn-300) * time.Second
if refreshAfter <= 0 {
refreshAfter = 5 * time.Minute
}
time.Sleep(refreshAfter)
if err := c.getAccessToken(); err != nil {
glog.Errorf(ctx, "[autoRefreshToken] 刷新 token 失败: %+v", err)
time.Sleep(1 * time.Minute)
}
}
}
func (c *weChatClient) GetTicket(sceneId string) (string, error) {
ctx := context.Background()
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 {
Errcode int `json:"errcode"`
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(ctx, "[GetTicket] 请求异常: %+v", err)
return "", ecode.Fail.Sub("发起 get ticket 请求出现异常")
}
if resp.IsError() {
glog.Errorf(ctx, "[GetTicket] 响应错误,状态: %s内容: %s", resp.Status(), resp.String())
return "", ecode.Fail.Sub("获取微信 ticket 失败")
}
if result.Errcode == 40001 {
if err := c.getAccessToken(); err != nil {
glog.Errorf(ctx, "[GetTicket] 刷新 token 失败: %+v", err)
return "", ecode.Fail.Sub("刷新 access_token 失败")
}
return c.GetTicket(sceneId)
}
glog.Infof(ctx, "[GetTicket] 成功获取 ticket: %s, 过期时间: %ds", result.Ticket, result.ExpireSeconds)
return result.Ticket, nil
}
func (c *weChatClient) GetQrCode(ticket string) (string, error) {
ctx := context.Background()
url := "https://mp.weixin.qq.com/cgi-bin/showqrcode"
resp, err := resty.New().R().
SetDoNotParseResponse(true).
SetQueryParams(g.MapStrStr{"ticket": ticket}).
Get(url)
if err != nil {
glog.Errorf(ctx, "[GetQrCode] 请求失败: %+v", err)
return "", ecode.Fail.Sub("获取二维码图片请求失败")
}
if resp.IsError() {
glog.Errorf(ctx, "[GetQrCode] 响应异常, 状态: %s", resp.Status())
return "", ecode.Fail.Sub("获取二维码图片失败")
}
imagePath := uuid.New().String() + ".jpg"
data, readErr := io.ReadAll(resp.RawBody())
defer resp.RawBody().Close()
if readErr != nil {
glog.Errorf(ctx, "[GetQrCode] 读取图片失败: %+v", readErr)
return "", ecode.Fail.Sub("读取二维码图片失败")
}
writeErr := os.WriteFile(imagePath, data, 0644)
if writeErr != nil {
glog.Errorf(ctx, "[GetQrCode] 保存图片失败: %+v", writeErr)
return "", ecode.Fail.Sub("保存二维码图片失败")
}
glog.Infof(ctx, "[GetQrCode] 二维码保存成功: %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
}