调整游戏人生封装

This commit is contained in:
2025-06-19 15:09:36 +08:00
parent b6c84d9a03
commit 9d41358f83
7 changed files with 281 additions and 263 deletions

View File

@ -41,7 +41,8 @@ type GetLoginTaskListReq struct {
Pageidx string `json:"pageidx" dc:"分页索引"` Pageidx string `json:"pageidx" dc:"分页索引"`
Gid int `json:"gid" v:"required#游戏唯一id不能为空" dc:"游戏唯一id"` Gid int `json:"gid" v:"required#游戏唯一id不能为空" dc:"游戏唯一id"`
//BrandId string `json:"brandId" 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 { type GetLoginTaskListRes struct {

View File

@ -15,6 +15,7 @@ func (c *ControllerV1) GetLoginTaskList(ctx context.Context, req *v1.GetLoginTas
PopenId: req.PopenId, PopenId: req.PopenId,
Num: req.Num, Num: req.Num,
Pageidx: req.Pageidx, Pageidx: req.Pageidx,
BindType: req.BindType,
}) })
if err != nil { if err != nil {

View File

@ -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) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -62,4 +62,5 @@ type QQNetbarActivityIn struct {
ServiceName string // 服务名称 ServiceName string // 服务名称
TaskParam TaskParam // 参数体 TaskParam TaskParam // 参数体
PopenId string PopenId string
BindType int
} }

View File

@ -17,6 +17,8 @@ type Reward struct {
StoreId int64 `json:"storeId" dc:"门店ID系统内置奖励为NULL" orm:"store_id"` StoreId int64 `json:"storeId" dc:"门店ID系统内置奖励为NULL" orm:"store_id"`
Value int64 `json:"value" dc:"奖励值(如积分数额、优惠金额)" orm:"value"` Value int64 `json:"value" dc:"奖励值(如积分数额、优惠金额)" orm:"value"`
Status int `json:"status" dc:"状态1=正常0=禁用" orm:"status"` 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"` CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间" orm:"created_at"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间" orm:"updated_at"` UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间" orm:"updated_at"`
DeletedAt *gtime.Time `json:"deletedAt" dc:"软删除时间戳" orm:"deleted_at"` DeletedAt *gtime.Time `json:"deletedAt" dc:"软删除时间戳" orm:"deleted_at"`

View File

@ -32,12 +32,13 @@ type GetTaskListIn struct {
NetBarAccount string `json:"netbar_account"` //网关账号 NetBarAccount string `json:"netbar_account"` //网关账号
//Page int `json:"page"` // 分页索引 //Page int `json:"page"` // 分页索引
//Size int `json:"size"` // 分页大小 //Size int `json:"size"` // 分页大小
Pageidx string `json:"pageidx"` // 分页索引 Pageidx string `json:"pageidx"` // 分页索引
Num int `json:"num"` // Num int `json:"num"` //
Gid int `json:"gid"` // 游戏唯一id Gid int `json:"gid"` // 游戏唯一id
Source string `json:"source"` // 不能为空 Source string `json:"source"` // 不能为空
BrandId string `json:"brand_id"` // 品牌id(可选) BrandId string `json:"brand_id"` // 品牌id(可选)
PopenId string `json:"POpenId"` PopenId string `json:"POpenId"`
BindType int `json:"bindType"` // 1:QQ 2:微信
} }
type GetNonLoginTaskListOut struct { type GetNonLoginTaskListOut struct {

View File

@ -5,12 +5,16 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url" "net/url"
"server/internal/dao"
"server/internal/model/do"
"sync"
"time"
"server/internal/consts" "server/internal/consts"
"server/internal/model" "server/internal/model"
"server/utility/ecode" "server/utility/ecode"
"server/utility/encrypt" "server/utility/encrypt"
"server/utility/rsa" "server/utility/rsa"
"time"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
@ -19,341 +23,349 @@ import (
"github.com/gogf/gf/v2/util/grand" "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 { type gamelifeClient struct {
PlatId string `json:"platId" ` config Config
BrandId string `json:"brand_id"` keyIVURLMap map[string]string
Secret string `json:"secret"` boundURLMap map[string]string
Mode string `json:"mode" ` unBoundURLMap map[string]string
keyivUrlMap map[string]string `json:"-"` // 存储获取用户 aes key 和 iv 的 url getBoundURL map[string]string
boundUrlMap map[string]string `json:"-"` // 存储用户绑定状态的 url taskURLMap map[string]string
unBoundUrlMap map[string]string `json:"-"` // 存储用户解绑状态的 url }
getBoundUrl map[string]string `json:"-"` // 存储用户绑定状态的 url
taskUrlMap map[string]string `json:"-"` // 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 ( var (
instance *gamelifeClient instance *gamelifeClient
once sync.Once
) )
func newgamelifeClient(ctx context.Context) *gamelifeClient { // GetGamelifeClient returns the singleton gamelifeClient instance, initializing it if necessary.
instance = &gamelifeClient{ func GetGamelifeClient(ctx context.Context) *gamelifeClient {
PlatId: g.Config().MustGet(ctx, "gamelife.platId").String(), once.Do(func() {
BrandId: g.Config().MustGet(ctx, "gamelife.brandId").String(), 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(), Secret: g.Config().MustGet(ctx, "gamelife.secret").String(),
Mode: g.Config().MustGet(ctx, "gamelife.mode").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 成功") client := &gamelifeClient{
return instance 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() { // GetUserKeyIV retrieves or refreshes the AES key, IV, and token for a user, storing them in cache.
ctx := context.Background() func (s *gamelifeClient) GetUserKeyIV(ctx context.Context, popenID string) (model.UserGamelifeCache, error) {
newgamelifeClient(ctx) oriData := map[string]string{
} "PlatId": s.config.PlatID,
"PopenId": popenID,
func GetGamelifeClient(ctx context.Context) *gamelifeClient {
if instance == nil {
instance = newgamelifeClient(ctx)
} }
return instance marshaled, err := json.Marshal(oriData)
}
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)
if err != nil { if err != nil {
err = ecode.Fail.Sub("序列化 json 数据出现异常") return model.UserGamelifeCache{}, ecode.Fail.Sub("序列化 json 数据出现异常")
return
} }
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 { type httpResult struct {
Secret string `json:"secret"` Secret string `json:"secret"`
Key string `json:"key"` // 该用户的token后续的账号绑定、登录态携带都需要此参数。 Key string `json:"key"`
} }
var result httpResult var result httpResult
resp, err := resty.New().R().SetBody(map[string]string{ resp, err := resty.New().R().
"plat_id": s.PlatId, SetBody(map[string]string{
"key": base64Encode, "plat_id": s.config.PlatID,
}).SetResult(&result).Post(s.keyivUrlMap[s.Mode]) "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 { if err != nil {
err = ecode.Fail.Sub("获取用户信息失败") return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败")
return
}
if resp.StatusCode() != 200 {
err = ecode.Fail.Sub("获取用户信息失败")
return
} }
decode, err := encrypt.Base64Decode(result.Secret) plain, err := rsa.GetRsaClient().DecryptWithRsaPrivateKey(decoded)
if err != nil { if err != nil {
err = ecode.Fail.Sub("解密用户信息失败") return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败")
return
} }
plain, err := rsa.GetRsaClient().DecryptWithRsaPrivateKey(decode) var aesResult struct {
if err != nil {
err = ecode.Fail.Sub("解密用户信息失败")
return
}
aesResult := struct {
Key string `json:"key"` Key string `json:"key"`
IV string `json:"iv"` IV string `json:"iv"`
}{} }
if err = json.Unmarshal(plain, &aesResult); err != nil { if err := json.Unmarshal(plain, &aesResult); err != nil {
err = ecode.Fail.Sub("解密用户信息失败") return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败")
return
} }
gamelifeCache := model.UserGamelifeCache{Aes: aesResult.Key, IV: aesResult.IV, Token: result.Key} cache := model.UserGamelifeCache{
// 将用户的 aeskey 和 iv 存储到缓存当中,用于后续请求数据加密, 固定时间 2 小时同时不同用户加上一个随机时间 Aes: aesResult.Key,
if err = g.Redis().SetEX(ctx, fmt.Sprintf(consts.GameLifeUserKey, popenId), gamelifeCache, int64(consts.GameLifeUserExpire+grand.Intn(1000))); err != nil { IV: aesResult.IV,
err = ecode.Fail.Sub("设置用户信息失败") Token: result.Key,
return
} }
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。 // 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
// 并根据操作类型(绑定或解绑)构造包含查询参数的 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]
// - 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]
if !isBound { if !isBound {
rooturl = s.unBoundUrlMap[s.Mode] rootURL = s.unBoundURLMap[s.config.Mode]
} }
gamelifeCacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenid) cache, err := s.getOrRefreshCache(ctx, popenID)
cacheData, err := g.Redis().Get(ctx, gamelifeCacheKey)
if err != nil { if err != nil {
return "", ecode.Fail.Sub("从缓存中获取用户信息失败") return "", err
} }
var gamelifeCache model.UserGamelifeCache cache.Params, err = s.buildQueryParams(ctx, popenID, cache, appName, nickname, bindType, isBound)
if cacheData.IsEmpty() { if err != nil {
// 如果缓存不存在或已过期,重新调用 GetUserKeyIV return "", err
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("解析用户信息失败")
}
} }
// 序列化原始数据 cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenID)
oriData := g.Map{ ttl, err := g.Redis().TTL(ctx, cacheKey)
"PopenId": popenid, 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(), "TimeStamp": time.Now().Unix(),
} }
marshal, err := json.Marshal(oriData) marshaled, err := json.Marshal(oriData)
if err != nil { if err != nil {
return "", ecode.Fail.Sub("序列化用户信息失败") return "", ecode.Fail.Sub("序列化用户信息失败")
} }
// 加密用户信息 encrypted, err := encrypt.AesEncryptCBCPKCS5(marshaled, []byte(cache.Aes), []byte(cache.IV))
aesEncrypt, err := encrypt.AesEncryptCBCPKCS5(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.IV))
if err != nil { if err != nil {
return "", ecode.Fail.Sub("加密用户信息失败") return "", ecode.Fail.Sub("加密用户信息失败")
} }
platUserInfoStr := encrypt.Base64Encode(aesEncrypt) platUserInfoStr := encrypt.Base64Encode(encrypted)
explatData := g.MapStrStr{"Token": gamelifeCache.Token, "PlatUserInfoStr": platUserInfoStr}
queryParams := url.Values{}
queryParams.Add("extplat_plat", s.PlatId)
queryParams.Add("app_name", appname)
if bindType == 1 { queryParams := url.Values{
queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeQQ) "extplat_plat": {s.config.PlatID},
} else { "app_name": {appName},
queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeWX) "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 { if !isBound {
queryParams.Add("nickname", nickname) queryParams.Add("nickname", nickname)
} }
// 将请求参数更新到缓存中
gamelifeCache.Params = queryParams.Encode() return queryParams.Encode(), nil
// 获取原缓存值、过期时间
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
} }
// 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) { func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbarActivityIn) (interface{}, error) {
client := resty.New() client := resty.New()
taskURL := s.taskURLMap[s.config.Mode]
switch in.ServiceName { switch in.ServiceName {
case consts.GetNonLoginTaskList: case consts.GetNonLoginTaskList:
result := model.GameTaskResponse{} in.TaskParam.Source = s.config.PlatID
in.TaskParam.Source = s.PlatId in.TaskParam.BrandId = s.config.BrandID
in.TaskParam.BrandId = s.BrandId var result model.GameTaskResponse
resp, err := client.R(). resp, err := client.R().
SetContext(ctx). SetContext(ctx).
SetBody(in.TaskParam). SetBody(in.TaskParam).
SetResult(&result). SetResult(&result).
Post(s.taskUrlMap[s.Mode] + consts.GetNonLoginTaskList) Post(taskURL + consts.GetNonLoginTaskList)
if err != nil { if err != nil || resp.IsError() {
return nil, ecode.Fail.Sub("请求出现异常") return nil, ecode.Fail.Sub("请求出现异常")
} }
if resp.IsError() {
return nil, ecode.Fail.Sub("请求失败")
}
return &result, nil return &result, nil
case consts.GetTaskList: case consts.GetTaskList:
result := model.GameTaskResponse{} cache, err := s.getOrRefreshCache(ctx, in.PopenId)
in.TaskParam.BrandId = s.BrandId
get, err := g.Redis().Get(ctx, fmt.Sprintf(consts.GameLifeUserKey, in.PopenId))
if err != nil { if err != nil {
return nil, ecode.Fail.Sub("从缓存中获取用户信息失败") return nil, err
} }
var gamelifeCache model.UserGamelifeCache cacheKey := fmt.Sprintf(consts.GameLifeUserKey, in.PopenId)
if err = json.Unmarshal(get.Bytes(), &gamelifeCache); err != nil { if cache.Params == "" {
return nil, ecode.Fail.Sub("解析用户信息失败") // 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(). resp, err := client.R().
SetContext(ctx). SetContext(ctx).
SetBody(in.TaskParam). SetBody(in.TaskParam).
SetResult(&result). SetResult(&result).
Post(fmt.Sprintf("%s%s?%s", s.taskUrlMap[s.Mode], consts.GetTaskList, gamelifeCache.Params)) Post(fmt.Sprintf("%s%s?%s", taskURL, consts.GetTaskList, cache.Params))
if err != nil { if err != nil || resp.IsError() {
return nil, ecode.Fail.Sub("请求出现异常") return nil, ecode.Fail.Sub("请求出现异常")
} }
if resp.IsError() {
return nil, ecode.Fail.Sub("请求失败")
}
return &result, nil return &result, nil
default: 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
}