package gamelife import ( "context" "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" "github.com/go-resty/resty/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" "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 { config Config keyIVURLMap map[string]string boundURLMap map[string]string unBoundURLMap map[string]string getBoundURL map[string]string taskURLMap map[string]string packageURLMap 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 Package 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/", }, Package: map[string]string{ "test": "https://test.igame.qq.com/tip/ingame-page/feature/add-game-life/igame-game-life-web/#/index/goods", "prod": "https://igame.qq.com/tip/ingame-page/igame-game-life-web/index.html#/index/goods", }, } var ( instance *gamelifeClient once sync.Once ) // 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(), } client := &gamelifeClient{ config: cfg, keyIVURLMap: URLMaps.KeyIV, boundURLMap: URLMaps.Bound, unBoundURLMap: URLMaps.UnBound, getBoundURL: URLMaps.GetBound, taskURLMap: URLMaps.Task, packageURLMap: URLMaps.Package, } glog.Info(ctx, "Initialized gamelifeClient successfully", map[string]interface{}{ "plat_id": cfg.PlatID, "mode": cfg.Mode, }) return client } // 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, } marshaled, err := json.Marshal(oriData) if err != nil { return nil, ecode.Fail.Sub("序列化 json 数据出现异常") } encrypted, err := rsa.GetRsaClient().EncryptWithRsaPublicKey(marshaled) if err != nil { return nil, ecode.Fail.Sub("序列化 json 数据出现异常") } type httpResult struct { Secret string `json:"secret"` Key string `json:"key"` } var result httpResult 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 nil, ecode.Fail.Sub("获取用户信息失败") } decoded, err := encrypt.Base64Decode(result.Secret) if err != nil { return nil, ecode.Fail.Sub("解密用户信息失败") } plain, err := rsa.GetRsaClient().DecryptWithRsaPrivateKey(decoded) if err != nil { return nil, ecode.Fail.Sub("解密用户信息失败") } var aesResult struct { Key string `json:"key"` IV string `json:"iv"` } if err := json.Unmarshal(plain, &aesResult); err != nil { return nil, ecode.Fail.Sub("解密用户信息失败") } cache := &model.UserGamelifeCache{ Aes: aesResult.Key, IV: aesResult.IV, Token: result.Key, } cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenID) if err := g.Redis().SetEX(ctx, cacheKey, cache, int64(consts.GameLifeUserExpire-grand.Intn(1000))); err != nil { return nil, ecode.Fail.Sub("设置用户信息失败") } return cache, nil } // 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.config.Mode] } cache, err := s.ensureUserCache(ctx, popenID) if err != nil { return "", err } params, err := s.buildQueryParams(ctx, popenID, cache, appName, nickname, bindType, isBound) if err != nil { return "", err } return fmt.Sprintf("%s?%s", rootURL, params), nil } func (s *gamelifeClient) GetGamelifePackageUrl(ctx context.Context, popenID, gameCode string, gameId, bindType int) (string, error) { // 从配置中获取 package URL 前缀 packageURL := s.packageURLMap[s.config.Mode] // 获取用户缓存 cache, err := s.ensureUserCache(ctx, popenID) if err != nil { return "", err } params, err := s.buildQueryParams(ctx, popenID, cache, gameCode, "", bindType, true) if err != nil { return "", err } if gameId != 0 { params = fmt.Sprintf("%s&gid=%d", params, gameId) } return fmt.Sprintf("%s?%s", packageURL, params), nil } // 通用缓存获取函数,只负责获取/刷新 aes、iv、token func (s *gamelifeClient) ensureUserCacheGeneral( 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 nil, ecode.Fail.Sub("从缓存中获取用户信息失败") } var cache *model.UserGamelifeCache if cacheData.IsEmpty() { cache, err = s.GetUserKeyIV(ctx, popenId) if err != nil { return nil, err } } else { if err := json.Unmarshal(cacheData.Bytes(), &cache); err != nil { return nil, ecode.Fail.Sub("解析用户信息失败") } } return cache, nil } // 只获取基本缓存 func (s *gamelifeClient) ensureUserCache(ctx context.Context, popenID string) (*model.UserGamelifeCache, error) { if popenID == "" || popenID == "undefined" { return nil, ecode.Params.Sub("popenID 不能为空或 undefined") } return s.ensureUserCacheGeneral(ctx, popenID) } // 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.ensureUserCache(ctx, popenID) if err != nil { return nil, err } platUserStr, err := s.buildQueryParams(ctx, popenID, cache, "", "", 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(), } marshaled, err := json.Marshal(oriData) if err != nil { return "", ecode.Fail.Sub("序列化用户信息失败") } encrypted, err := encrypt.AesEncryptCBCPKCS5(marshaled, []byte(cache.Aes), []byte(cache.IV)) if err != nil { return "", ecode.Fail.Sub("加密用户信息失败") } platUserInfoStr := encrypt.Base64Encode(encrypted) 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) } if !isBound { queryParams.Add("nickname", nickname) } 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: 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(taskURL + consts.GetNonLoginTaskList) if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } return &result, nil case consts.GetTaskList: 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, err := s.ensureUserCache(ctx, in.PopenId) if err != nil { return nil, err } params, err := s.buildQueryParams(ctx, in.PopenId, cache, value.String(), in.NickName, in.BindType, true) if err != nil { return nil, err } 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", taskURL, consts.GetTaskList, params)) if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } return &result, nil case consts.QueryUserRoleList: 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.ensureUserCache(ctx, in.PopenId) if err != nil { return nil, err } params, err := s.buildQueryParams(ctx, in.PopenId, cache, value.String(), in.NickName, in.BindType, true) if err != nil { return nil, err } var result model.UserRoleListResponse resp, err := client.R(). SetContext(ctx). SetBody(in.UserRoleParam). SetResult(&result). Post(fmt.Sprintf("%s%s?%s", taskURL, consts.QueryUserRoleList, params)) 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.GiftParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } cache, err := s.ensureUserCache(ctx, in.PopenId) if err != nil { return nil, err } params, err := s.buildQueryParams(ctx, in.PopenId, cache, value.String(), in.NickName, in.BindType, true) if err != nil { return nil, err } var result *model.GiftResponse resp, err := client.R(). SetContext(ctx). SetBody(in.GiftParam). SetResult(&result). Post(fmt.Sprintf("%s%s?%s", taskURL, consts.GetGift, params)) if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } if result == nil { return nil, ecode.Fail.Sub("请求结果为空") } return result, nil case consts.QueryUserGoodsList: value, err := dao.Games.Ctx(ctx).Where(do.Games{GameId: in.GoodsParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } cache, err := s.ensureUserCache(ctx, in.PopenId) if err != nil { return nil, err } params, err := s.buildQueryParams(ctx, in.PopenId, cache, value.String(), in.NickName, in.BindType, true) if err != nil { return nil, err } var result model.GoodsResponse resp, err := client.R(). SetContext(ctx). SetBody(in.GoodsParam). SetResult(&result). Post(fmt.Sprintf("%s%s?%s", taskURL, consts.QueryUserGoodsList, params)) if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } return &result, nil case consts.ExchangeGoods: value, err := dao.Games.Ctx(ctx).Where(do.Games{GameId: in.ExchangeGoodsParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() if err != nil { return nil, ecode.Fail.Sub("获取游戏编码失败") } cache, err := s.ensureUserCache(ctx, in.PopenId) if err != nil { return nil, err } params, err := s.buildQueryParams(ctx, in.PopenId, cache, value.String(), in.NickName, in.BindType, true) if err != nil { return nil, err } var result model.ExchangeGoodsResponse resp, err := client.R(). SetContext(ctx). SetBody(in.ExchangeGoodsParam). SetResult(&result). Post(fmt.Sprintf("%s%s?%s", taskURL, consts.ExchangeGoods, params)) if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } return &result, nil case consts.QueryUserGoodsDetail: 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.ensureUserCache(ctx, in.PopenId) if err != nil { return nil, err } params, err := s.buildQueryParams(ctx, in.PopenId, cache, value.String(), in.NickName, in.BindType, true) if err != nil { return nil, err } var result model.QueryUserGoodsDetailResponse resp, err := client.R(). SetContext(ctx). SetBody(in.QueryUserGoodsDetailParam). SetResult(&result). Post(fmt.Sprintf("%s%s?%s", taskURL, consts.QueryUserGoodsDetail, params)) if err != nil || resp.IsError() { return nil, ecode.Fail.Sub("请求出现异常") } return &result, nil default: return nil, ecode.Fail.Sub(fmt.Sprintf("不支持的任务: %s", in.ServiceName)) } } func (s *gamelifeClient) GetSecret() string { return s.config.Secret }