From 03c7939a81d86d5234ad47517ca0ee292781d3ab Mon Sep 17 00:00:00 2001 From: denghui <1016848185@qq.com> Date: Mon, 23 Jun 2025 11:42:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BE=AE=E4=BF=A1=E6=89=AB?= =?UTF-8?q?=E7=A0=81=E7=99=BB=E5=BD=95=20access=5Ftoken=20=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 10 +- go.sum | 4 + internal/consts/redis.go | 4 + .../storeDesktopSetting.go | 16 ++ internal/model/gamelife.go | 64 +++++++- internal/model/storeDesktopSetting.go | 6 +- internal/packed/packed.go | 9 +- manifest/config/config.yaml | 8 + utility/gamelife/gamelife.go | 21 +-- utility/mqtt/emqx/emqx.go | 102 +++++++++++++ utility/mqtt/mqtt.go | 23 +++ utility/myCasbin/casbin.go | 1 + utility/wechat/wechat.go | 138 +++++++++++++----- 13 files changed, 346 insertions(+), 60 deletions(-) create mode 100644 utility/mqtt/emqx/emqx.go create mode 100644 utility/mqtt/mqtt.go diff --git a/go.mod b/go.mod index 15eaa67..1b40967 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,15 @@ module server go 1.24.2 require ( + github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11 github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.3 + github.com/alibabacloud-go/tea v1.2.2 + github.com/alibabacloud-go/tea-utils/v2 v2.0.6 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible + github.com/aliyun/credentials-go v1.4.6 github.com/bwmarrin/snowflake v0.3.0 github.com/casbin/casbin/v2 v2.105.0 + github.com/eclipse/paho.mqtt.golang v1.5.0 github.com/go-resty/resty/v2 v2.16.5 github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.0 github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.0 @@ -20,14 +25,10 @@ require ( require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect - github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect github.com/alibabacloud-go/openapi-util v0.1.1 // indirect - github.com/alibabacloud-go/tea v1.2.2 // indirect - github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // indirect github.com/alibabacloud-go/tea-xml v1.1.3 // indirect - github.com/aliyun/credentials-go v1.4.6 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/casbin/govaluate v1.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -57,6 +58,7 @@ require ( go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.14.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.6.0 // indirect diff --git a/go.sum b/go.sum index 00f042b..d9696e3 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= +github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -244,6 +246,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/consts/redis.go b/internal/consts/redis.go index c566880..8f9d00c 100644 --- a/internal/consts/redis.go +++ b/internal/consts/redis.go @@ -15,3 +15,7 @@ const ( WeChatLoginCache = "wx:login:cache:%s" WeChatLoginLimit = "wx:login:limit:%s" ) + +const ( + WechatAccessTokenKey = "wechat:access_token" +) diff --git a/internal/logic/storeDesktopSetting/storeDesktopSetting.go b/internal/logic/storeDesktopSetting/storeDesktopSetting.go index 65df2a6..db4e4c6 100644 --- a/internal/logic/storeDesktopSetting/storeDesktopSetting.go +++ b/internal/logic/storeDesktopSetting/storeDesktopSetting.go @@ -2,11 +2,14 @@ package storeDesktopSetting import ( "context" + "encoding/json" + "fmt" "github.com/gogf/gf/v2/errors/gerror" "server/internal/dao" "server/internal/model" "server/internal/model/do" "server/internal/service" + "server/utility/mqtt" ) type sStoreDesktopSetting struct { @@ -60,6 +63,19 @@ func (s *sStoreDesktopSetting) Save(ctx context.Context, in model.SaveDesktopSet if err != nil { return nil, err } + client, b := mqtt.GetClient("emqx") + if !b { + return nil, gerror.New("获取MQTT客户端失败") + } + marshal, err := json.Marshal(in) + if err != nil { + return nil, err + } + err = client.Publish(fmt.Sprintf("/desktop/%d", in.StoreId), marshal) + if err != nil { + return nil, err + } + return &model.SaveDesktopSettingOut{ Success: true, }, nil diff --git a/internal/model/gamelife.go b/internal/model/gamelife.go index 568c916..7e134cf 100644 --- a/internal/model/gamelife.go +++ b/internal/model/gamelife.go @@ -131,7 +131,13 @@ type GiftResponse struct { } type gameRoleInfo struct { - // FIXME + RoleIdx string `json:"roleIdx"` + RoleBaseInfo roleBaseInfo `json:"RoleBaseInfo"` +} + +type roleBaseInfo struct { + Gid int `json:"gid"` + RoleIdx string `json:"roleidx"` } type userAddress struct { @@ -139,13 +145,24 @@ type userAddress struct { } type waterExtraInfo struct { - // FIXME + EchangeWaterLog echangeWaterLog `json:"echange_water_log"` + PremiumPrivInfo premiumPrivInfo `json:"premium_priv_info"` + SendOrderQuantity int `json:"send_order_quantity"` } -type goodsType struct { - // FIXME +type echangeWaterLog struct { + SrcChannel string `json:"src_channel"` + SrcModule string `json:"src_module"` + Source string `json:"source"` + CreateBrandId string `json:"create_brand_id"` } +type premiumPrivInfo struct { + CustomInfo string `json:"custom_info"` // 字符串 JSON,可视需要二次解析 +} + +type goodsType int64 + type goodsDisplayType struct { // FIXME } @@ -153,9 +170,44 @@ type goodsDisplayType struct { type merchantInfo struct { // FIXME } - +type olCouponCfg struct { + CouponReq couponReq `json:"coupon_req"` +} +type couponReq struct { + ReqContent reqContent `json:"reqContent"` +} +type reqContent struct { + CouponContent couponContent `json:"couponContent"` +} +type couponContent struct { + CouponNum string `json:"couponNum"` +} +type goodsBrandInfo struct { + BrandName string `json:"brand_name"` + JumpStatus int `json:"jump_status"` + BrandId string `json:"brand_id"` + BrandLogo string `json:"brand_logo"` +} +type expireConfig struct { + ExpireDays int `json:"expire_days"` +} +type privCoupon struct { + AppId string `json:"appid"` + PrizeChannelId string `json:"prize_channel_id"` + PrizeId string `json:"prize_id"` + PrizeType int `json:"prize_type"` + Num int `json:"num"` + Url string `json:"url"` +} type goodsExtraInfo struct { - // FIXME + OlCouponCfg olCouponCfg `json:"olcoupon_cfg"` + GoodsBrandInfo goodsBrandInfo `json:"goods_brand_info"` + ExpireType int `json:"expire_type"` + ExpireConfig expireConfig `json:"expire_config"` + MprocJmpCfg map[string]any `json:"mproc_jmp_cfg"` // 空对象可用 map[string]any 表示 + ApplyGoods string `json:"apply_goods"` + NewlyBuilt bool `json:"newly_built"` + PrivCoupon privCoupon `json:"priv_coupon"` } type water struct { diff --git a/internal/model/storeDesktopSetting.go b/internal/model/storeDesktopSetting.go index 2eae48b..2560219 100644 --- a/internal/model/storeDesktopSetting.go +++ b/internal/model/storeDesktopSetting.go @@ -19,9 +19,9 @@ type StoreGetDesktopSettingOut struct { } type SaveDesktopSettingIn struct { - StoreId int64 - TopComponentVisible int // 1显示,2 隐藏 - RightComponentVisible int // 1显示,2 隐藏 + StoreId int64 `json:"storeId"` + TopComponentVisible int `json:"topComponentVisible"` // 1显示,2 隐藏 + RightComponentVisible int `json:"rightComponentVisible"` // 1显示,2 隐藏 } type SaveDesktopSettingOut struct { Success bool diff --git a/internal/packed/packed.go b/internal/packed/packed.go index 84d479a..a09b24c 100644 --- a/internal/packed/packed.go +++ b/internal/packed/packed.go @@ -3,11 +3,12 @@ package packed import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2" - _ "server/utility/gamelife" + //_ "server/utility/gamelife" + //_ "server/utility/mqtt/emqx" _ "server/utility/myCasbin" - _ "server/utility/oss/aliyun" - _ "server/utility/rsa" - _ "server/utility/sms/aliyun" + //_ "server/utility/oss/aliyun" + //_ "server/utility/rsa" + //_ "server/utility/sms/aliyun" _ "server/utility/snowid" _ "server/utility/wechat" ) diff --git a/manifest/config/config.yaml b/manifest/config/config.yaml index 1b57930..9c9a211 100644 --- a/manifest/config/config.yaml +++ b/manifest/config/config.yaml @@ -48,3 +48,11 @@ gamelife: secret: "LqPQ2gbF" mode: "test" rsaKey: "./manifest/config/全游-测试环境密钥.txt" + +mqtt: + emqx: + host: "127.0.0.1" + clientId: "server" + port: 1883 + username: "areanx_server" + password: "areanx_server" diff --git a/utility/gamelife/gamelife.go b/utility/gamelife/gamelife.go index 2c50aa3..63eb7a2 100644 --- a/utility/gamelife/gamelife.go +++ b/utility/gamelife/gamelife.go @@ -178,7 +178,7 @@ func (s *gamelifeClient) GetUrl(ctx context.Context, popenID, appName, nickname if !isBound { rootURL = s.unBoundURLMap[s.config.Mode] } - cache, err := s.ensureUserCacheWithParams(ctx, popenID, appName, nickname, bindType) + cache, err := s.ensureUserCacheWithParams(ctx, popenID, appName, nickname, bindType, isBound) if err != nil { return "", err } @@ -319,7 +319,7 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } - cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType, true) in.TaskParam.BrandId = s.config.BrandID var result model.GameTaskResponse resp, err := client.R(). @@ -332,11 +332,11 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar } return &result, nil case consts.QueryUserRoleList: - value, err := dao.Games.Ctx(ctx).Where(do.Games{GameId: in.QueryUserGoodsDetailParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() + value, err := dao.Games.Ctx(ctx).Where(do.Games{GameId: in.UserRoleParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } - cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType, true) var result model.UserRoleListResponse resp, err := client.R(). SetContext(ctx). @@ -346,13 +346,14 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } + return &result.RoleList, nil case consts.GetGift: value, err := dao.Games.Ctx(ctx).Where(do.Games{GameId: in.QueryUserGoodsDetailParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } - cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType, true) var result model.GiftResponse resp, err := client.R(). SetContext(ctx). @@ -368,7 +369,7 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } - cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType, true) var result model.GoodsResponse resp, err := client.R(). SetContext(ctx). @@ -384,7 +385,7 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } - cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType, true) var result model.ExchangeGoodsResponse resp, err := client.R(). SetContext(ctx). @@ -400,7 +401,7 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } - cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType, true) if err != nil { return nil, err } @@ -421,7 +422,7 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar // ensureUserCacheWithParams ensures user cache exists and contains a valid Params string. // It handles cache retrieval, fallback refresh, Params generation, and cache update. -func (s *gamelifeClient) ensureUserCacheWithParams(ctx context.Context, popenId, appname, nickname string, bindType int) (*model.UserGamelifeCache, error) { +func (s *gamelifeClient) ensureUserCacheWithParams(ctx context.Context, popenId, appname, nickname string, bindType int, isBound bool) (*model.UserGamelifeCache, error) { cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenId) cacheData, err := g.Redis().Get(ctx, cacheKey) if err != nil { @@ -441,7 +442,7 @@ func (s *gamelifeClient) ensureUserCacheWithParams(ctx context.Context, popenId, } if cache.Params == "" { - cache.Params, err = s.buildQueryParams(ctx, popenId, cache, appname, nickname, bindType, true) + cache.Params, err = s.buildQueryParams(ctx, popenId, cache, appname, nickname, bindType, isBound) if err != nil { return nil, err } diff --git a/utility/mqtt/emqx/emqx.go b/utility/mqtt/emqx/emqx.go new file mode 100644 index 0000000..b7b9de0 --- /dev/null +++ b/utility/mqtt/emqx/emqx.go @@ -0,0 +1,102 @@ +package emqx + +import ( + "context" + "fmt" + "sync" + "time" + + mqttlib "github.com/eclipse/paho.mqtt.golang" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/glog" + "server/utility/mqtt" +) + +type emqxClient struct { + client mqttlib.Client + once sync.Once +} + +// New 创建并连接 EMQX 客户端实例,连接成功才返回 +func New(broker, clientID, username, password string) mqtt.MqttClient { + opts := mqttlib.NewClientOptions(). + AddBroker(broker). + SetClientID(clientID). + SetUsername(username). + SetPassword(password). + SetAutoReconnect(true). + SetConnectTimeout(5 * time.Second) + + c := &emqxClient{ + client: mqttlib.NewClient(opts), + } + + // 立即连接 + err := c.connect() + if err != nil { + glog.Errorf(context.Background(), "连接EMQX失败: %v", err) + panic(fmt.Sprintf("连接EMQX失败: %v", err)) + } + glog.Infof(context.Background(), "EMQX客户端连接成功") + return c +} + +// connect 内部只调用一次连接 +func (e *emqxClient) connect() error { + var err error + e.once.Do(func() { + token := e.client.Connect() + if token.Wait() && token.Error() != nil { + err = fmt.Errorf("EMQX连接失败: %w", token.Error()) + glog.Errorf(context.Background(), err.Error()) + return + } + glog.Infof(context.Background(), "EMQX连接成功") + }) + return err +} + +// Publish 实现接口 Publish +func (e *emqxClient) Publish(topic string, payload []byte) error { + token := e.client.Publish(topic, 0, false, payload) + token.Wait() + err := token.Error() + if err != nil { + glog.Errorf(context.Background(), "发布消息失败,topic=%s,错误:%v", topic, err) + } else { + glog.Infof(context.Background(), "成功发布消息,topic=%s,消息大小=%d字节", topic, len(payload)) + } + return err +} + +// Subscribe 实现接口 Subscribe +func (e *emqxClient) Subscribe(topic string, handler func(topic string, payload []byte)) error { + token := e.client.Subscribe(topic, 0, func(client mqttlib.Client, msg mqttlib.Message) { + handler(msg.Topic(), msg.Payload()) + }) + token.Wait() + err := token.Error() + if err != nil { + glog.Errorf(context.Background(), "订阅失败,topic=%s,错误:%v", topic, err) + } else { + glog.Infof(context.Background(), "成功订阅主题,topic=%s", topic) + } + return err +} + +// init 注册emqx客户端 +func init() { + ctx := context.Background() + cfg := g.Config() + host := cfg.MustGet(ctx, "mqtt.emqx.host").String() + port := cfg.MustGet(ctx, "mqtt.emqx.port").Int() + username := cfg.MustGet(ctx, "mqtt.emqx.username").String() + password := cfg.MustGet(ctx, "mqtt.emqx.password").String() + clientId := cfg.MustGet(ctx, "mqtt.emqx.clientId").String() + + broker := fmt.Sprintf("tcp://%s:%d", host, port) + client := New(broker, clientId, username, password) + + mqtt.Register("emqx", client) + glog.Infof(ctx, "EMQX客户端注册完成,broker=%s, clientID=%s", broker, clientId) +} diff --git a/utility/mqtt/mqtt.go b/utility/mqtt/mqtt.go new file mode 100644 index 0000000..2e30eb8 --- /dev/null +++ b/utility/mqtt/mqtt.go @@ -0,0 +1,23 @@ +package mqtt + +// MqttClient 定义了通用 MQTT 客户端接口 +type MqttClient interface { + // Publish 发布消息到指定 topic + Publish(topic string, payload []byte) error + // Subscribe 订阅指定 topic,接收回调 + Subscribe(topic string, handler func(topic string, payload []byte)) error +} + +// 全局注册表 +var clients = make(map[string]MqttClient) + +// Register 注册一个 MQTT 客户端实现 +func Register(name string, client MqttClient) { + clients[name] = client +} + +// GetClient 获取已注册的客户端 +func GetClient(name string) (MqttClient, bool) { + client, ok := clients[name] + return client, ok +} diff --git a/utility/myCasbin/casbin.go b/utility/myCasbin/casbin.go index dce07ca..ec7d70e 100644 --- a/utility/myCasbin/casbin.go +++ b/utility/myCasbin/casbin.go @@ -43,6 +43,7 @@ func init() { // 任务 enforcer.AddPolicy("guest", "/x/task/ranking", "GET", "获取排行榜") enforcer.AddPolicy("guest", "/x/task/getNonLoginTaskList", "GET", "未登录获取任务列表") + enforcer.AddPolicy("guest", "/x/reward/callback", "POST", "") // 游戏列表 enforcer.AddPolicy("guest", "/x/game", "GET", "获取游戏列表") diff --git a/utility/wechat/wechat.go b/utility/wechat/wechat.go index 6c8b2dd..17cd834 100644 --- a/utility/wechat/wechat.go +++ b/utility/wechat/wechat.go @@ -2,8 +2,10 @@ package wechat import ( "context" + "github.com/gogf/gf/v2/encoding/gjson" "io" "os" + "server/internal/consts" "server/utility/ecode" "sync" "time" @@ -30,6 +32,35 @@ var ( 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 } @@ -48,11 +79,15 @@ func init() { } func (c *weChatClient) getAccessToken() error { + ctx := context.Background() + result := struct { AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` + ExpiresIn int `json:"expires_in"` // 一般为 7200 秒 }{} - ctx := context.Background() + + url := "https://api.weixin.qq.com/cgi-bin/token" + resp, err := resty.New().R(). SetQueryParams(g.MapStrStr{ "grant_type": "client_credential", @@ -60,44 +95,79 @@ func (c *weChatClient) getAccessToken() error { "secret": c.AppSecret, }). SetResult(&result). - Get("https://api.weixin.qq.com/cgi-bin/token") + Get(url) if err != nil { - glog.Errorf(ctx, "发起 get access_token 请求出现异常: %+v", err) + glog.Errorf(ctx, "[getAccessToken] 请求异常, URL: %s, err: %+v", url, 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 失败") + if resp.IsError() { + glog.Errorf(ctx, "[getAccessToken] 响应错误, 状态: %s, 内容: %s", resp.Status(), resp.String()) + 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) + 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) { - for { - // 初次获取 - if err := c.getAccessToken(); err != nil { - time.Sleep(1 * time.Minute) - continue - } - // 等待 expiresIn - 300 秒(即提前 5 分钟刷新) +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", @@ -120,49 +190,51 @@ func (c *weChatClient) GetTicket(sceneId string) (string, error) { Post("https://api.weixin.qq.com/cgi-bin/qrcode/create") if err != nil { - glog.Errorf(context.Background(), "发起 get ticket 请求出现异常: %+v", err) + glog.Errorf(ctx, "[GetTicket] 请求异常: %+v", err) return "", ecode.Fail.Sub("发起 get ticket 请求出现异常") } - if resp.StatusCode() != 200 { - glog.Errorf(context.Background(), "获取微信 ticket 响应异常: %+v", resp.Status()) + if resp.IsError() { + glog.Errorf(ctx, "[GetTicket] 响应错误,状态: %s,内容: %s", resp.Status(), resp.String()) return "", ecode.Fail.Sub("获取微信 ticket 失败") } - glog.Infof(context.Background(), "获取微信 ticket 成功: %+v", result.Ticket) + glog.Infof(ctx, "[GetTicket] 成功获取 ticket: %s, 过期时间: %ds", result.Ticket, result.ExpireSeconds) return result.Ticket, nil } -func (c *weChatClient) GetQrCode(ticket string) (imagePath string, err error) { - qrCodeUrl := "https://mp.weixin.qq.com/cgi-bin/showqrcode" +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(qrCodeUrl) + Get(url) if err != nil { - glog.Errorf(context.Background(), "获取二维码图片请求异常: %+v", err) + glog.Errorf(ctx, "[GetQrCode] 请求失败: %+v", err) return "", ecode.Fail.Sub("获取二维码图片请求失败") } - if resp.StatusCode() != 200 { - glog.Errorf(context.Background(), "获取二维码图片响应异常: %+v", resp.Status()) + if resp.IsError() { + glog.Errorf(ctx, "[GetQrCode] 响应异常, 状态: %s", resp.Status()) return "", ecode.Fail.Sub("获取二维码图片失败") } - imagePath = uuid.New().String() + ".jpg" + imagePath := uuid.New().String() + ".jpg" data, readErr := io.ReadAll(resp.RawBody()) + defer resp.RawBody().Close() + if readErr != nil { - glog.Errorf(context.Background(), "读取二维码图片失败: %+v", readErr) + glog.Errorf(ctx, "[GetQrCode] 读取图片失败: %+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) + glog.Errorf(ctx, "[GetQrCode] 保存图片失败: %+v", writeErr) return "", ecode.Fail.Sub("保存二维码图片失败") } - glog.Infof(context.Background(), "二维码图片保存成功: %s", imagePath) + glog.Infof(ctx, "[GetQrCode] 二维码保存成功: %s", imagePath) return imagePath, nil }