504 lines
16 KiB
Go
504 lines
16 KiB
Go
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))
|
|
}
|
|
}
|