From 9d41358f835d0d1af7d55c8bbac3de3d97dfd6a9 Mon Sep 17 00:00:00 2001 From: denghui <1016848185@qq.com> Date: Thu, 19 Jun 2025 15:09:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=B8=B8=E6=88=8F=E4=BA=BA?= =?UTF-8?q?=E7=94=9F=E5=B0=81=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/task/v1/task.go | 3 +- .../task/task_v1_get_login_task_list.go | 1 + internal/logic/task/task.go | 2 +- internal/model/gamelife.go | 1 + internal/model/reward.go | 2 + internal/model/task.go | 13 +- utility/gamelife/gamelife.go | 522 +++++++++--------- 7 files changed, 281 insertions(+), 263 deletions(-) diff --git a/api/task/v1/task.go b/api/task/v1/task.go index 682bef5..94d7a52 100644 --- a/api/task/v1/task.go +++ b/api/task/v1/task.go @@ -41,7 +41,8 @@ type GetLoginTaskListReq struct { Pageidx string `json:"pageidx" dc:"分页索引"` Gid int `json:"gid" v:"required#游戏唯一id不能为空" dc:"游戏唯一id"` //BrandId string `json:"brandId" dc:"品牌id(可选)"` - PopenId string `json:"popenId" v:"required#popenId不能为空" dc:"用户详情接口返回的 wxPopenId 或者是 qqPopenId"` + PopenId string `json:"popenId" v:"required#popenId不能为空" dc:"用户详情接口返回的 wxPopenId 或者是 qqPopenId"` + BindType int `json:"bindType" v:"required#不能为空" dc:"绑定类型 1: QQ 2:微信"` } type GetLoginTaskListRes struct { diff --git a/internal/controller/task/task_v1_get_login_task_list.go b/internal/controller/task/task_v1_get_login_task_list.go index b081200..ffbf13c 100644 --- a/internal/controller/task/task_v1_get_login_task_list.go +++ b/internal/controller/task/task_v1_get_login_task_list.go @@ -15,6 +15,7 @@ func (c *ControllerV1) GetLoginTaskList(ctx context.Context, req *v1.GetLoginTas PopenId: req.PopenId, Num: req.Num, Pageidx: req.Pageidx, + BindType: req.BindType, }) if err != nil { diff --git a/internal/logic/task/task.go b/internal/logic/task/task.go index de45380..e2fd8ef 100644 --- a/internal/logic/task/task.go +++ b/internal/logic/task/task.go @@ -188,7 +188,7 @@ func (s *sTask) GetNonLoginTaskList(ctx context.Context, in *model.GetTaskListIn func (s *sTask) GetLoginTaskList(ctx context.Context, in *model.GetTaskListIn) (out *model.GetTaskListOut, err error) { // 调用外部接口 - activity, err := gamelife.GetGamelifeClient(ctx).RequestActivity(ctx, &model.QQNetbarActivityIn{ServiceName: consts.GetTaskList, PopenId: in.PopenId, TaskParam: model.TaskParam{Gid: in.Gid, NetBarAccount: in.NetBarAccount, Num: in.Num, Pageidx: in.Pageidx}}) + activity, err := gamelife.GetGamelifeClient(ctx).RequestActivity(ctx, &model.QQNetbarActivityIn{ServiceName: consts.GetTaskList, PopenId: in.PopenId, BindType: in.BindType, TaskParam: model.TaskParam{Gid: in.Gid, NetBarAccount: in.NetBarAccount, Num: in.Num, Pageidx: in.Pageidx}}) if err != nil { return nil, err } diff --git a/internal/model/gamelife.go b/internal/model/gamelife.go index 3335da7..076499b 100644 --- a/internal/model/gamelife.go +++ b/internal/model/gamelife.go @@ -62,4 +62,5 @@ type QQNetbarActivityIn struct { ServiceName string // 服务名称 TaskParam TaskParam // 参数体 PopenId string + BindType int } diff --git a/internal/model/reward.go b/internal/model/reward.go index 7b26d58..819340e 100644 --- a/internal/model/reward.go +++ b/internal/model/reward.go @@ -17,6 +17,8 @@ type Reward struct { StoreId int64 `json:"storeId" dc:"门店ID,系统内置奖励为NULL" orm:"store_id"` Value int64 `json:"value" dc:"奖励值(如积分数额、优惠金额)" orm:"value"` Status int `json:"status" dc:"状态:1=正常,0=禁用" orm:"status"` + TotalNum int64 `json:"totalNum" dc:"奖励数量" orm:"total_num"` + UseNum int64 `json:"useNum" dc:"已使用数量" orm:"use_num"` CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间" orm:"created_at"` UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间" orm:"updated_at"` DeletedAt *gtime.Time `json:"deletedAt" dc:"软删除时间戳" orm:"deleted_at"` diff --git a/internal/model/task.go b/internal/model/task.go index b4fb511..db0bd86 100644 --- a/internal/model/task.go +++ b/internal/model/task.go @@ -32,12 +32,13 @@ type GetTaskListIn struct { NetBarAccount string `json:"netbar_account"` //网关账号 //Page int `json:"page"` // 分页索引 //Size int `json:"size"` // 分页大小 - Pageidx string `json:"pageidx"` // 分页索引 - Num int `json:"num"` // - Gid int `json:"gid"` // 游戏唯一id - Source string `json:"source"` // 不能为空 - BrandId string `json:"brand_id"` // 品牌id(可选) - PopenId string `json:"POpenId"` + Pageidx string `json:"pageidx"` // 分页索引 + Num int `json:"num"` // + Gid int `json:"gid"` // 游戏唯一id + Source string `json:"source"` // 不能为空 + BrandId string `json:"brand_id"` // 品牌id(可选) + PopenId string `json:"POpenId"` + BindType int `json:"bindType"` // 1:QQ 2:微信 } type GetNonLoginTaskListOut struct { diff --git a/utility/gamelife/gamelife.go b/utility/gamelife/gamelife.go index bb16635..9a6874f 100644 --- a/utility/gamelife/gamelife.go +++ b/utility/gamelife/gamelife.go @@ -5,12 +5,16 @@ import ( "encoding/json" "fmt" "net/url" + "server/internal/dao" + "server/internal/model/do" + "sync" + "time" + "server/internal/consts" "server/internal/model" "server/utility/ecode" "server/utility/encrypt" "server/utility/rsa" - "time" "github.com/go-resty/resty/v2" "github.com/gogf/gf/v2/frame/g" @@ -19,341 +23,349 @@ import ( "github.com/gogf/gf/v2/util/grand" ) +// Config holds gamelife client configuration. +type Config struct { + PlatID string + BrandID string + Secret string + Mode string +} + +// gamelifeClient manages interactions with the GameLife system. type gamelifeClient struct { - PlatId string `json:"platId" ` - BrandId string `json:"brand_id"` - Secret string `json:"secret"` - Mode string `json:"mode" ` - keyivUrlMap map[string]string `json:"-"` // 存储获取用户 aes key 和 iv 的 url - boundUrlMap map[string]string `json:"-"` // 存储用户绑定状态的 url - unBoundUrlMap map[string]string `json:"-"` // 存储用户解绑状态的 url - getBoundUrl map[string]string `json:"-"` // 存储用户绑定状态的 url - taskUrlMap map[string]string `json:"-"` + config Config + keyIVURLMap map[string]string + boundURLMap map[string]string + unBoundURLMap map[string]string + getBoundURL map[string]string + taskURLMap map[string]string +} + +// URLMaps defines the URL mappings for different environments. +var URLMaps = struct { + KeyIV map[string]string + Bound map[string]string + UnBound map[string]string + GetBound map[string]string + Task map[string]string +}{ + KeyIV: map[string]string{ + "test": "https://api-test.nes.smoba.qq.com/pvpesport.sgamenes.commcgi.commcgi/GetExplatSecret", + "prod": "https://api.nes.smoba.qq.com/pvpesport.sgamenes.commcgi.commcgi/GetExplatSecret", + }, + Bound: map[string]string{ + "test": "https://h5-test.cafe.qq.com/pmd-mobile.cafe.bind-account.pc.feature.bind-account/#/index", + "prod": "https://h5.cafe.qq.com/pmd-mobile.cafe.bind-account.pc/#/index", + }, + UnBound: map[string]string{ + "test": "https://h5-test.cafe.qq.com/pmd-mobile.cafe.bind-account.pc.feature.bind-account/#/bind-manage", + "prod": "https://h5.cafe.qq.com/pmd-mobile.cafe.bind-account.pc/#/bind-manage", + }, + GetBound: map[string]string{ + "test": "https://api-test.cafe.qq.com/tipmp.user.authinfo_cgi.authinfo_cgi/GetPlatUserInfo", + "prod": "https://api.cafe.qq.com/tipmp.user.authinfo_cgi.authinfo_cgi/GetPlatUserInfo", + }, + Task: map[string]string{ + "test": "https://api-test.cafe.qq.com/netbar.cafe.open_api.open_api/", + "prod": "https://api.cafe.qq.com/netbar.cafe.open_api.open_api/", + }, } var ( instance *gamelifeClient + once sync.Once ) -func newgamelifeClient(ctx context.Context) *gamelifeClient { - instance = &gamelifeClient{ - PlatId: g.Config().MustGet(ctx, "gamelife.platId").String(), - BrandId: g.Config().MustGet(ctx, "gamelife.brandId").String(), +// GetGamelifeClient returns the singleton gamelifeClient instance, initializing it if necessary. +func GetGamelifeClient(ctx context.Context) *gamelifeClient { + once.Do(func() { + instance = newGamelifeClient(ctx) + }) + return instance +} + +// newGamelifeClient initializes a new gamelifeClient with configuration and URL mappings. +func newGamelifeClient(ctx context.Context) *gamelifeClient { + cfg := Config{ + PlatID: g.Config().MustGet(ctx, "gamelife.platId").String(), + BrandID: g.Config().MustGet(ctx, "gamelife.brandId").String(), Secret: g.Config().MustGet(ctx, "gamelife.secret").String(), Mode: g.Config().MustGet(ctx, "gamelife.mode").String(), - keyivUrlMap: map[string]string{ - "test": "https://api-test.nes.smoba.qq.com/pvpesport.sgamenes.commcgi.commcgi/GetExplatSecret", - "prod": "https://api.nes.smoba.qq.com/pvpesport.sgamenes.commcgi.commcgi/GetExplatSecret", - }, - boundUrlMap: map[string]string{ - "test": "https://h5-test.cafe.qq.com/pmd-mobile.cafe.bind-account.pc.feature.bind-account/#/index", - "prod": "https://h5.cafe.qq.com/pmd-mobile.cafe.bind-account.pc/#/index", - }, - unBoundUrlMap: map[string]string{ - "test": "https://h5-test.cafe.qq.com/pmd-mobile.cafe.bind-account.pc.feature.bind-account/#/bind-manage", - "prod": "https://h5.cafe.qq.com/pmd-mobile.cafe.bind-account.pc/#/bind-manage", - }, - getBoundUrl: map[string]string{ - "test": "https://api-test.cafe.qq.com/tipmp.user.authinfo_cgi.authinfo_cgi/GetPlatUserInfo", - "prod": "https://api.cafe.qq.com/tipmp.user.authinfo_cgi.authinfo_cgi/GetPlatUserInfo", - }, - taskUrlMap: map[string]string{ - "test": "https://api-test.cafe.qq.com/netbar.cafe.open_api.open_api/", - "prod": "https://api.cafe.qq.com/netbar.cafe.open_api.open_api/", - }, } - glog.Infof(ctx, "初始化 gamelifeClient 成功") - return instance + client := &gamelifeClient{ + config: cfg, + keyIVURLMap: URLMaps.KeyIV, + boundURLMap: URLMaps.Bound, + unBoundURLMap: URLMaps.UnBound, + getBoundURL: URLMaps.GetBound, + taskURLMap: URLMaps.Task, + } + glog.Info(ctx, "Initialized gamelifeClient successfully", map[string]interface{}{ + "plat_id": cfg.PlatID, + "mode": cfg.Mode, + }) + return client } -func init() { - ctx := context.Background() - newgamelifeClient(ctx) -} - -func GetGamelifeClient(ctx context.Context) *gamelifeClient { - if instance == nil { - instance = newgamelifeClient(ctx) +// GetUserKeyIV retrieves or refreshes the AES key, IV, and token for a user, storing them in cache. +func (s *gamelifeClient) GetUserKeyIV(ctx context.Context, popenID string) (model.UserGamelifeCache, error) { + oriData := map[string]string{ + "PlatId": s.config.PlatID, + "PopenId": popenID, } - return instance -} - -func (s *gamelifeClient) GetUserKeyIV(ctx context.Context, popenId string) (cache model.UserGamelifeCache, err error) { - // 创建加密原数据 - oriData := g.MapStrStr{ - "PlatId": s.PlatId, - "PopenId": popenId, - } - marshal, err := json.Marshal(oriData) + marshaled, err := json.Marshal(oriData) if err != nil { - err = ecode.Fail.Sub("序列化 json 数据出现异常") - return + return model.UserGamelifeCache{}, ecode.Fail.Sub("序列化 json 数据出现异常") } - key, err := rsa.GetRsaClient().EncryptWithRsaPublicKey(marshal) - if err != nil { - err = ecode.Fail.Sub("序列化 json 数据出现异常") - return + encrypted, err := rsa.GetRsaClient().EncryptWithRsaPublicKey(marshaled) + if err != nil { + return model.UserGamelifeCache{}, ecode.Fail.Sub("序列化 json 数据出现异常") } - // base64编码 - base64Encode := encrypt.Base64Encode(key) - // 向游戏人生发送请求 + type httpResult struct { Secret string `json:"secret"` - Key string `json:"key"` // 该用户的token,后续的账号绑定、登录态携带都需要此参数。 + Key string `json:"key"` } var result httpResult - resp, err := resty.New().R().SetBody(map[string]string{ - "plat_id": s.PlatId, - "key": base64Encode, - }).SetResult(&result).Post(s.keyivUrlMap[s.Mode]) + resp, err := resty.New().R(). + SetBody(map[string]string{ + "plat_id": s.config.PlatID, + "key": encrypt.Base64Encode(encrypted), + }). + SetResult(&result). + Post(s.keyIVURLMap[s.config.Mode]) + if err != nil || resp.StatusCode() != 200 { + return model.UserGamelifeCache{}, ecode.Fail.Sub("获取用户信息失败") + } + + decoded, err := encrypt.Base64Decode(result.Secret) if err != nil { - err = ecode.Fail.Sub("获取用户信息失败") - return - - } - if resp.StatusCode() != 200 { - err = ecode.Fail.Sub("获取用户信息失败") - return - + return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败") } - decode, err := encrypt.Base64Decode(result.Secret) + plain, err := rsa.GetRsaClient().DecryptWithRsaPrivateKey(decoded) if err != nil { - err = ecode.Fail.Sub("解密用户信息失败") - return - + return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败") } - plain, err := rsa.GetRsaClient().DecryptWithRsaPrivateKey(decode) - if err != nil { - err = ecode.Fail.Sub("解密用户信息失败") - return - - } - aesResult := struct { + var aesResult struct { Key string `json:"key"` IV string `json:"iv"` - }{} - if err = json.Unmarshal(plain, &aesResult); err != nil { - err = ecode.Fail.Sub("解密用户信息失败") - return + } + if err := json.Unmarshal(plain, &aesResult); err != nil { + return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败") } - gamelifeCache := model.UserGamelifeCache{Aes: aesResult.Key, IV: aesResult.IV, Token: result.Key} - // 将用户的 aeskey 和 iv 存储到缓存当中,用于后续请求数据加密, 固定时间 2 小时同时不同用户加上一个随机时间 - if err = g.Redis().SetEX(ctx, fmt.Sprintf(consts.GameLifeUserKey, popenId), gamelifeCache, int64(consts.GameLifeUserExpire+grand.Intn(1000))); err != nil { - err = ecode.Fail.Sub("设置用户信息失败") - return + cache := model.UserGamelifeCache{ + Aes: aesResult.Key, + IV: aesResult.IV, + Token: result.Key, } - return + cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenID) + if err := g.Redis().SetEX(ctx, cacheKey, cache, int64(consts.GameLifeUserExpire+grand.Intn(1000))); err != nil { + return model.UserGamelifeCache{}, ecode.Fail.Sub("设置用户信息失败") + } + + return cache, nil } -// GetUrl 为 GameLife 系统生成绑定或解绑定的 URL。 -// 它从缓存中获取用户加密密钥(若缓存过期则重新获取),加密用户数据, -// 并根据操作类型(绑定或解绑)构造包含查询参数的 URL。 -// -// 参数: -// - ctx: 用于控制请求生命周期和取消的上下文。 -// - popenid: 用户的唯一标识符。 -// - appname: 应用程序名称。 -// - nickname: 用户昵称,仅在解绑操作时需要(绑定操作时忽略)。 -// - bindType: 绑定类型(1 表示 QQ,其他值表示微信)。 -// - isBound: 布尔值,指示操作类型(true 表示绑定,false 表示解绑)。 -// -// 返回值: -// - string: 构造的绑定或解绑操作的 URL。 -// - error: 如果任何步骤(缓存获取、密钥获取、JSON 序列化、加密或 URL 构造)失败,则返回错误。 -// -// 错误: -// - 如果发生以下情况,将返回带有具体错误信息的错误: -// - 无法解析基础 URL。 -// - 无法从缓存或通过 GetUserKeyIV 获取用户信息。 -// - JSON 序列化或反序列化失败。 -// - AES 加密或 Base64 编码失败。 -func (s *gamelifeClient) GetUrl(ctx context.Context, popenid, appname, nickname string, bindType int, isBound bool) (string, error) { - // 从缓存中获取用户的 aes 和 iv - rooturl := s.boundUrlMap[s.Mode] +// GetUrl generates a URL for binding or unbinding a user in the GameLife system. +// It retrieves or refreshes user encryption keys from cache and constructs a URL +// with query parameters using buildQueryParams. +func (s *gamelifeClient) GetUrl(ctx context.Context, popenID, appName, nickname string, bindType int, isBound bool) (string, error) { + rootURL := s.boundURLMap[s.config.Mode] if !isBound { - rooturl = s.unBoundUrlMap[s.Mode] + rootURL = s.unBoundURLMap[s.config.Mode] } - gamelifeCacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenid) - cacheData, err := g.Redis().Get(ctx, gamelifeCacheKey) + cache, err := s.getOrRefreshCache(ctx, popenID) if err != nil { - return "", ecode.Fail.Sub("从缓存中获取用户信息失败") + return "", err } - var gamelifeCache model.UserGamelifeCache - if cacheData.IsEmpty() { - // 如果缓存不存在或已过期,重新调用 GetUserKeyIV - data, err := s.GetUserKeyIV(ctx, popenid) - if err != nil { - return "", ecode.Fail.Sub("获取用户信息失败") - } - gamelifeCache = model.UserGamelifeCache{Aes: data.Aes, IV: data.IV, Token: data.Token} - } else { - // 缓存存在,直接解析 - if err = json.Unmarshal(cacheData.Bytes(), &gamelifeCache); err != nil { - return "", ecode.Fail.Sub("解析用户信息失败") - } + cache.Params, err = s.buildQueryParams(ctx, popenID, cache, appName, nickname, bindType, isBound) + if err != nil { + return "", err } - // 序列化原始数据 - oriData := g.Map{ - "PopenId": popenid, + cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenID) + ttl, err := g.Redis().TTL(ctx, cacheKey) + if err != nil { + return "", ecode.Fail.Sub("获取缓存过期时间失败") + } + if err := g.Redis().SetEX(ctx, cacheKey, cache, ttl); err != nil { + return "", ecode.Fail.Sub("更新缓存失败") + } + + return fmt.Sprintf("%s?%s", rootURL, cache.Params), nil +} + +// GetBound retrieves the binding status of a user from the GameLife system. +// It uses buildQueryParams to construct the encrypted user data for the POST body. +func (s *gamelifeClient) GetBound(ctx context.Context, popenID string) (*model.UserBoundResult, error) { + cache, err := s.getOrRefreshCache(ctx, popenID) + if err != nil { + return nil, err + } + + // Use default values for appName, nickname, bindType, and isBound + platUserStr, err := s.buildQueryParams(ctx, popenID, cache, s.config.PlatID, "", 1, true) + if err != nil { + return nil, err + } + + // Parse platUserStr to extract Token and PlatUserInfoStr + queryParams, err := url.ParseQuery(platUserStr) + if err != nil { + return nil, ecode.Fail.Sub("解析查询参数失败") + } + extplatDataStr := queryParams.Get("extplat_data") + if extplatDataStr == "" { + return nil, ecode.Fail.Sub("查询参数中缺少 extplat_data") + } + var extplatData map[string]string + if err := json.Unmarshal([]byte(extplatDataStr), &extplatData); err != nil { + return nil, ecode.Fail.Sub("解析 extplat_data 失败") + } + + postBody := map[string]string{ + "plat_id": s.config.PlatID, + "plat_user_info": popenID, + "plat_user_str": gconv.String(extplatData), + } + var result model.UserBoundResult + resp, err := resty.New().R(). + SetBody(postBody). + SetResult(&result). + Post(s.getBoundURL[s.config.Mode]) + if err != nil || resp.StatusCode() != 200 { + return nil, ecode.Fail.Sub("向游戏人生获取绑定信息出现异常") + } + + glog.Info(ctx, "Fetched user binding info", map[string]interface{}{ + "popen_id": popenID, + "result": result, + }) + return &result, nil +} + +// buildQueryParams constructs URL query parameters for GameLife requests using cached AES, IV, and token. +// It encrypts user data and encodes it into a URL query string. +func (s *gamelifeClient) buildQueryParams(ctx context.Context, popenID string, cache model.UserGamelifeCache, appName, nickname string, bindType int, isBound bool) (string, error) { + oriData := map[string]interface{}{ + "PopenId": popenID, "TimeStamp": time.Now().Unix(), } - marshal, err := json.Marshal(oriData) + marshaled, err := json.Marshal(oriData) if err != nil { return "", ecode.Fail.Sub("序列化用户信息失败") } - // 加密用户信息 - aesEncrypt, err := encrypt.AesEncryptCBCPKCS5(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.IV)) + encrypted, err := encrypt.AesEncryptCBCPKCS5(marshaled, []byte(cache.Aes), []byte(cache.IV)) if err != nil { return "", ecode.Fail.Sub("加密用户信息失败") } - platUserInfoStr := encrypt.Base64Encode(aesEncrypt) - explatData := g.MapStrStr{"Token": gamelifeCache.Token, "PlatUserInfoStr": platUserInfoStr} - queryParams := url.Values{} - queryParams.Add("extplat_plat", s.PlatId) - queryParams.Add("app_name", appname) + platUserInfoStr := encrypt.Base64Encode(encrypted) - if bindType == 1 { - queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeQQ) - } else { - queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeWX) + queryParams := url.Values{ + "extplat_plat": {s.config.PlatID}, + "app_name": {appName}, + "bind_type": {consts.GamelifeExtplatBoundTypeQQ}, + "extplat_type": {consts.GamelifeExtplatType}, + "mini_program_band": {consts.GamelifeMiniProgramBand}, + "extplat_extra": {consts.GamelifeExtplatExtraPc}, + "extplat_data": {gconv.String(map[string]string{"Token": cache.Token, "PlatUserInfoStr": platUserInfoStr})}, + } + if bindType != 1 { + queryParams.Set("bind_type", consts.GamelifeExtplatBoundTypeWX) } - - queryParams.Add("extplat_type", consts.GamelifeExtplatType) - queryParams.Add("mini_program_band", consts.GamelifeMiniProgramBand) - queryParams.Add("extplat_extra", consts.GamelifeExtplatExtraPc) - queryParams.Add("extplat_data", gconv.String(explatData)) - - // 解绑时加 nickname if !isBound { queryParams.Add("nickname", nickname) } - // 将请求参数更新到缓存中 - gamelifeCache.Params = queryParams.Encode() - // 获取原缓存值、过期时间 - ttl, err := g.Redis().TTL(ctx, gamelifeCacheKey) - if err != nil { - return "", ecode.Fail.Sub("获取缓存过期时间失败") - } - // 更新数据 - if err = g.Redis().SetEX(ctx, gamelifeCacheKey, gamelifeCache, ttl); err != nil { - return "", ecode.Fail.Sub("更新缓存失败") - } - // 拼接最终 URL - url := fmt.Sprintf("%s?%s", rooturl, queryParams.Encode()) - return url, nil -} - -// GetBound 获取用户绑定情况 -func (s *gamelifeClient) GetBound(ctx context.Context, popenid string) (*model.UserBoundResult, error) { - // 获取基础 URL - rooturl := s.getBoundUrl[s.Mode] - - // 获取缓存 - cacheData, err := g.Redis().Get(ctx, fmt.Sprintf(consts.GameLifeUserKey, popenid)) - if err != nil { - return nil, ecode.Fail.Sub("从缓存中获取用户信息失败") - } - - var gamelifeCache model.UserGamelifeCache - if cacheData.IsEmpty() { - // 如果缓存不存在或已过期,重新调用 GetUserKeyIV - data, err := s.GetUserKeyIV(ctx, popenid) - if err != nil { - return nil, ecode.Fail.Sub("获取用户信息失败") - } - gamelifeCache = model.UserGamelifeCache{Aes: data.Aes, IV: data.IV, Token: data.Token} - } else { - if err = json.Unmarshal(cacheData.Bytes(), &gamelifeCache); err != nil { - return nil, ecode.Fail.Sub("解析用户信息失败") - } - } - - // 加密原始数据 - oriData := g.Map{ - "PopenId": popenid, - "TimeStamp": time.Now().Unix(), - } - marshal, err := json.Marshal(oriData) - if err != nil { - return nil, ecode.Fail.Sub("序列化用户信息失败") - } - aesEncrypt, err := encrypt.AesEncryptCBCPKCS5(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.IV)) - if err != nil { - return nil, ecode.Fail.Sub("加密用户信息失败") - } - platUserInfoStr := encrypt.Base64Encode(aesEncrypt) - - postBody := g.MapStrStr{ - "plat_id": s.PlatId, - "plat_user_info": popenid, - "plat_user_str": gconv.String(g.MapStrStr{"Token": gamelifeCache.Token, "PlatUserInfoStr": platUserInfoStr}), - } - var result model.UserBoundResult - resp, err := resty.New().R().SetBody(postBody).SetResult(&result).Post(rooturl) - if err != nil { - return nil, ecode.Fail.Sub("向游戏人生获取绑定信息出现异常") - } - if resp.StatusCode() != 200 { - return nil, ecode.Fail.Sub("向游戏人生获取绑定信息失败") - } - glog.Infof(ctx, "获取用户游戏人生绑定信息: %v", result) - - return &result, nil + + return queryParams.Encode(), nil } +// RequestActivity handles activity requests for the GameLife system based on the service name. +// It uses the cached UserGamelifeCache.Params for authenticated task list requests, reconstructing +// Params if empty and updating the cache. func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbarActivityIn) (interface{}, error) { client := resty.New() + taskURL := s.taskURLMap[s.config.Mode] + switch in.ServiceName { case consts.GetNonLoginTaskList: - result := model.GameTaskResponse{} - in.TaskParam.Source = s.PlatId - in.TaskParam.BrandId = s.BrandId + in.TaskParam.Source = s.config.PlatID + in.TaskParam.BrandId = s.config.BrandID + var result model.GameTaskResponse resp, err := client.R(). SetContext(ctx). SetBody(in.TaskParam). SetResult(&result). - Post(s.taskUrlMap[s.Mode] + consts.GetNonLoginTaskList) - if err != nil { + Post(taskURL + consts.GetNonLoginTaskList) + if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } - - if resp.IsError() { - return nil, ecode.Fail.Sub("请求失败") - } return &result, nil + case consts.GetTaskList: - result := model.GameTaskResponse{} - in.TaskParam.BrandId = s.BrandId - get, err := g.Redis().Get(ctx, fmt.Sprintf(consts.GameLifeUserKey, in.PopenId)) + cache, err := s.getOrRefreshCache(ctx, in.PopenId) if err != nil { - return nil, ecode.Fail.Sub("从缓存中获取用户信息失败") + return nil, err } - var gamelifeCache model.UserGamelifeCache - if err = json.Unmarshal(get.Bytes(), &gamelifeCache); err != nil { - return nil, ecode.Fail.Sub("解析用户信息失败") + cacheKey := fmt.Sprintf(consts.GameLifeUserKey, in.PopenId) + if cache.Params == "" { + // Reconstruct Params with default values + value, err := dao.Games.Ctx(ctx).Where(do.Games{GameId: in.TaskParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() + if err != nil { + return nil, ecode.Fail.Sub("获取游戏名称失败") + } + + cache.Params, err = s.buildQueryParams(ctx, in.PopenId, cache, value.String(), "", in.BindType, true) + if err != nil { + return nil, err + } + // Update cache with new Params + ttl, err := g.Redis().TTL(ctx, cacheKey) + if err != nil { + return nil, ecode.Fail.Sub("获取缓存过期时间失败") + } + if err := g.Redis().SetEX(ctx, cacheKey, cache, ttl); err != nil { + return nil, ecode.Fail.Sub("更新缓存失败") + } } + in.TaskParam.BrandId = s.config.BrandID + var result model.GameTaskResponse resp, err := client.R(). SetContext(ctx). SetBody(in.TaskParam). SetResult(&result). - Post(fmt.Sprintf("%s%s?%s", s.taskUrlMap[s.Mode], consts.GetTaskList, gamelifeCache.Params)) - if err != nil { + Post(fmt.Sprintf("%s%s?%s", taskURL, consts.GetTaskList, cache.Params)) + if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } - if resp.IsError() { - return nil, ecode.Fail.Sub("请求失败") - } return &result, nil default: - return nil, ecode.Fail.Sub("不支持的任务") + return nil, ecode.Fail.Sub(fmt.Sprintf("不支持的任务: %s", in.ServiceName)) } } + +// getOrRefreshCache retrieves user cache or refreshes it if expired. +func (s *gamelifeClient) getOrRefreshCache(ctx context.Context, popenID string) (model.UserGamelifeCache, error) { + cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenID) + cacheData, err := g.Redis().Get(ctx, cacheKey) + if err != nil { + return model.UserGamelifeCache{}, ecode.Fail.Sub("从缓存中获取用户信息失败") + } + + var cache model.UserGamelifeCache + if cacheData.IsEmpty() { + return s.GetUserKeyIV(ctx, popenID) + } + if err := json.Unmarshal(cacheData.Bytes(), &cache); err != nil { + return model.UserGamelifeCache{}, ecode.Fail.Sub("解析用户信息失败") + } + return cache, nil +}