267 lines
7.0 KiB
Go
267 lines
7.0 KiB
Go
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] 成功写入 Redis,expires_in: %ds", result.ExpiresIn)
|
||
}
|
||
|
||
glog.Infof(ctx, "[getAccessToken] 成功获取 access_token: %s,expires_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 {
|
||
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 失败")
|
||
}
|
||
|
||
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
|
||
}
|