Files
arenax-server/utility/gamelife/gamelife.go
2025-06-19 09:40:11 +08:00

339 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package gamelife
import (
"context"
"encoding/json"
"fmt"
"net/url"
"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"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/grand"
)
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:"-"`
}
var (
instance *gamelifeClient
)
func newgamelifeClient(ctx context.Context) *gamelifeClient {
instance = &gamelifeClient{
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
}
func init() {
ctx := context.Background()
newgamelifeClient(ctx)
}
func GetGamelifeClient(ctx context.Context) *gamelifeClient {
if instance == nil {
instance = newgamelifeClient(ctx)
}
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)
if err != nil {
err = ecode.Fail.Sub("序列化 json 数据出现异常")
return
}
key, err := rsa.GetRsaClient().EncryptWithRsaPublicKey(marshal)
if err != nil {
err = ecode.Fail.Sub("序列化 json 数据出现异常")
return
}
// base64编码
base64Encode := encrypt.Base64Encode(key)
// 向游戏人生发送请求
type httpResult struct {
Secret string `json:"secret"`
Key string `json:"key"` // 该用户的token后续的账号绑定、登录态携带都需要此参数。
}
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])
if err != nil {
err = ecode.Fail.Sub("获取用户信息失败")
return
}
if resp.StatusCode() != 200 {
err = ecode.Fail.Sub("获取用户信息失败")
return
}
decode, err := encrypt.Base64Decode(result.Secret)
if err != nil {
err = ecode.Fail.Sub("解密用户信息失败")
return
}
plain, err := rsa.GetRsaClient().DecryptWithRsaPrivateKey(decode)
if err != nil {
err = ecode.Fail.Sub("解密用户信息失败")
return
}
aesResult := struct {
Key string `json:"key"`
IV string `json:"iv"`
}{}
if err = json.Unmarshal(plain, &aesResult); err != nil {
err = ecode.Fail.Sub("解密用户信息失败")
return
}
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
}
return
}
// 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]
if !isBound {
rooturl = s.unBoundUrlMap[s.Mode]
}
gamelifeCacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenid)
cacheData, err := g.Redis().Get(ctx, gamelifeCacheKey)
if err != nil {
return "", ecode.Fail.Sub("从缓存中获取用户信息失败")
}
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("解析用户信息失败")
}
}
// 序列化原始数据
oriData := g.Map{
"PopenId": popenid,
"TimeStamp": time.Now().Unix(),
}
marshal, err := json.Marshal(oriData)
if err != nil {
return "", ecode.Fail.Sub("序列化用户信息失败")
}
// 加密用户信息
aesEncrypt, err := encrypt.AesEncryptCBCPKCS5(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.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)
if bindType == 1 {
queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeQQ)
} else {
queryParams.Add("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
}
func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbarActivityIn) (interface{}, error) {
client := resty.New()
switch in.ServiceName {
case consts.GetNonLoginTaskList:
result := model.GameTaskResponse{}
in.TaskParam.Source = s.PlatId
in.TaskParam.BrandId = s.BrandId
resp, err := client.R().
SetContext(ctx).
SetBody(in.TaskParam).
SetResult(&result).
Post(s.taskUrlMap[s.Mode] + consts.GetNonLoginTaskList)
if err != nil {
return nil, ecode.Fail.Sub("请求出现异常")
}
if resp.IsError() {
return nil, ecode.Fail.Sub("请求失败")
}
return &result, nil
case consts.GetTaskList:
return nil, nil
default:
return nil, ecode.Fail.Sub("不支持的任务")
}
}