package gamelife import ( "context" "encoding/json" "fmt" "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" "net/url" "server/internal/consts" "server/internal/model" "server/utility/ecode" "server/utility/encrypt" "server/utility/rsa" "time" ) type gamelifeClient struct { PlatId string `json:"platId" ` 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 } var ( instance *gamelifeClient ) func newgamelifeClient(ctx context.Context) *gamelifeClient { instance = &gamelifeClient{ PlatId: g.Config().MustGet(ctx, "gamelife.platId").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.authinfoo_cgi.authinfo_cgi/GetPlatUserInfo", "prod": "https://api.cafe.qq.com/tipmp.user.authinfoo_cgi.authinfo_cgi/GetPlatUserInfo", }, } 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) // 向游戏人生发送请求 result := struct { secret string `json:"secret"` key string `json:"key"` // 该用户的token,后续的账号绑定、登录态携带都需要此参数。 }{} 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] } baseUrl, err := url.Parse(rooturl) if err != nil { return "", ecode.Fail.Sub("解析基础 URL 失败") } cacheData, err := g.Redis().Get(ctx, fmt.Sprintf(consts.GameLifeUserKey, popenid)) 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.AesEncrypt(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 := baseUrl.Query() queryParams.Add("app_name", appname) queryParams.Add("mini_program_band", consts.GamelifeMiniProgramBand) queryParams.Add("extplat_plat", s.PlatId) queryParams.Add("extplat_type", consts.GamelifeExtplatType) queryParams.Add("extplat_extra", consts.GamelifeExtplatExtraPc) queryParams.Add("extplat_data", url.QueryEscape(gconv.String(explatData))) // 根据 isBound 设置 bind_type 和 nickname if bindType == 1 { queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeQQ) } else { queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeWX) } // 仅在解绑时添加 nickname if !isBound { queryParams.Add("nickname", nickname) } baseUrl.RawQuery = queryParams.Encode() return baseUrl.String(), nil } // GetBound 获取用户绑定情况 func (s *gamelifeClient) GetBound(ctx context.Context, popenid string) (string, error) { baseUrl, err := url.Parse(s.getBoundUrl[s.Mode]) if err != nil { return "", ecode.Fail.Sub("解析基础 URL 失败") } cacheData, err := g.Redis().Get(ctx, fmt.Sprintf(consts.GameLifeUserKey, popenid)) 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.AesEncrypt(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 := baseUrl.Query() queryParams.Add("plat_id", s.PlatId) queryParams.Add("plat_user_info", popenid) queryParams.Add("plat_user_str", url.QueryEscape(gconv.String(explatData))) baseUrl.RawQuery = queryParams.Encode() return baseUrl.String(), nil }