diff --git a/api/user/v1/user.go b/api/user/v1/user.go index 857d65f..cd07f1c 100644 --- a/api/user/v1/user.go +++ b/api/user/v1/user.go @@ -63,7 +63,11 @@ type GetUserBoundInfoReq struct { PopenId string `json:"popenId" v:"required#popenId不能为空" dc:"用户详情接口返回的 wxPopenId 或者是 qqPopenId"` } type GetUserBoundInfoRes struct { - IsBound bool `json:"isBound" dc:"是否已绑定"` + IsBound bool `json:"isBound" dc:"是否已绑定"` + Nick string `json:"nick" dc:"昵称"` + AppNames []string `json:"appNames" dc:"已绑定游戏名称"` + Utype int8 `json:"utype" dc:"用户类型,1: qq,2: 微信"` + Ctime uint32 `json:"ctime" dc:"绑定时间"` } type GetBoundUrlReq struct { diff --git a/internal/controller/user/user_v1_get_user_bound_info.go b/internal/controller/user/user_v1_get_user_bound_info.go index f9e3c56..d4b2523 100644 --- a/internal/controller/user/user_v1_get_user_bound_info.go +++ b/internal/controller/user/user_v1_get_user_bound_info.go @@ -13,5 +13,5 @@ func (c *ControllerV1) GetUserBoundInfo(ctx context.Context, req *v1.GetUserBoun if err != nil { return nil, err } - return &v1.GetUserBoundInfoRes{IsBound: out.IsBound}, nil + return &v1.GetUserBoundInfoRes{IsBound: out.IsBound, Nick: out.Nick, AppNames: out.AppNames, Utype: out.Utype, Ctime: out.Ctime}, nil } diff --git a/internal/logic/user/user.go b/internal/logic/user/user.go index aae6a8d..ba0f64a 100644 --- a/internal/logic/user/user.go +++ b/internal/logic/user/user.go @@ -3,7 +3,6 @@ package internal import ( "context" "fmt" - "github.com/go-resty/resty/v2" "github.com/gogf/gf/v2/frame/g" "server/internal/consts" "server/internal/dao" @@ -268,20 +267,16 @@ func (s *sUser) UnBoundUrl(ctx context.Context, in *model.UserBoundUrlIn) (out * }, nil } func (s *sUser) BoundInfo(ctx context.Context, in *model.UserBoundInfoIn) (out *model.UserBoundInfoOut, err error) { - url, err := gamelife.GetGamelifeClient(ctx).GetBound(ctx, in.PopenId) + result, err := gamelife.GetGamelifeClient(ctx).GetBound(ctx, in.PopenId) if err != nil { - return nil, ecode.Fail.Sub("获取查询用户绑定信息 url 失败") - } - var result model.UserBoundResult - resp, err := resty.New().R().SetResult(&result).Post(url) - if err != nil { - return nil, ecode.Fail.Sub("调用游戏人生接口出现异常") - } - if resp.StatusCode() != 200 { - return nil, ecode.Fail.Sub("调用游戏人生接口失败") + return nil, err } return &model.UserBoundInfoOut{ - IsBound: result.Result, + IsBound: result.Result, + AppNames: result.AppNames, + Ctime: result.Ctime, + Nick: result.Nick, + Utype: result.Utype, }, nil } diff --git a/internal/model/user.go b/internal/model/user.go index 52695cf..59825f9 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -135,17 +135,18 @@ type GetPhoneCodeOut struct { } type UserGamelifeCache struct { - Aes string `json:"aes"` - IV string `json:"iv"` - Token string `json:"token"` + Aes string `json:"aes"` + IV string `json:"iv"` + Token string `json:"token"` + Params string `json:"params"` } type UserBoundResult struct { - Result bool `json:"result"` - Nick string `json:"nick"` - Ctime uint32 `json:"ctime"` - Utype int8 `json:"utype"` - AppName []string `json:"app_name"` + Result bool `json:"result"` + Nick string `json:"nick"` + Ctime uint32 `json:"ctime"` + Utype int8 `json:"utype"` + AppNames []string `json:"app_names"` } type UserBoundUrlIn struct { PopenId string @@ -166,5 +167,9 @@ type UserBoundInfoIn struct { PopenId string } type UserBoundInfoOut struct { - IsBound bool + IsBound bool + AppNames []string + Ctime uint32 + Nick string + Utype int8 } diff --git a/utility/encrypt/aes.go b/utility/encrypt/aes.go index 27e48a0..2a2d902 100644 --- a/utility/encrypt/aes.go +++ b/utility/encrypt/aes.go @@ -1,58 +1,31 @@ package encrypt import ( + "bytes" "crypto/aes" "crypto/cipher" - "errors" ) -// AesEncrypt 使用 AES-CBC 模式对数据进行加密。 -// -// 参数: -// - plainText: 原始明文数据(必须是任意长度) -// - key: 加密密钥(长度必须是 16、24 或 32 字节) -// - iv: 初始化向量(必须是 16 字节) -// -// 返回值: -// - 加密后的密文 -// - 错误信息(如果加密失败) -func AesEncrypt(plainText, key, iv []byte) ([]byte, error) { +// PKCS5 填充 +func pkcs5Padding(src []byte, blockSize int) []byte { + padding := blockSize - len(src)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(src, padText...) +} + +func AesEncryptCBCPKCS5(plainText, key, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) if err != nil { return nil, err } - if len(iv) != aes.BlockSize { - return nil, errors.New("IV 长度必须为 16 字节") - } - plainText = pkcs7Padding(plainText, aes.BlockSize) - cipherText := make([]byte, len(plainText)) + // PKCS5 填充(与 PKCS7 相同,只是 block size 为 8) + paddedText := pkcs5Padding(plainText, block.BlockSize()) + cipherText := make([]byte, len(paddedText)) mode := cipher.NewCBCEncrypter(block, iv) - mode.CryptBlocks(cipherText, plainText) + mode.CryptBlocks(cipherText, paddedText) return cipherText, nil } - -// pkcs7Padding 对数据进行 PKCS7 填充。 -// -// 参数: -// - data: 原始数据 -// - blockSize: 块大小(通常为 16) -// -// 返回值: -// - 填充后的数据 -func pkcs7Padding(data []byte, blockSize int) []byte { - padding := blockSize - len(data)%blockSize - padText := bytesRepeat(byte(padding), padding) - return append(data, padText...) -} - -// bytesRepeat 返回一个重复 count 次的字节切片。 -func bytesRepeat(b byte, count int) []byte { - buf := make([]byte, count) - for i := 0; i < count; i++ { - buf[i] = b - } - return buf -} diff --git a/utility/gamelife/gamelife.go b/utility/gamelife/gamelife.go index 5a817f8..0d6a92d 100644 --- a/utility/gamelife/gamelife.go +++ b/utility/gamelife/gamelife.go @@ -48,8 +48,8 @@ func newgamelifeClient(ctx context.Context) *gamelifeClient { "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", + "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", }, } glog.Infof(ctx, "初始化 gamelifeClient 成功") @@ -167,10 +167,6 @@ func (s *gamelifeClient) GetUrl(ctx context.Context, popenid, appname, nickname 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 { @@ -203,48 +199,48 @@ func (s *gamelifeClient) GetUrl(ctx context.Context, popenid, appname, nickname } // 加密用户信息 - aesEncrypt, err := encrypt.AesEncrypt(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.IV)) + 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 := baseUrl.Query() - queryParams.Add("app_name", appname) - queryParams.Add("mini_program_band", consts.GamelifeMiniProgramBand) + queryParams := url.Values{} 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))) + queryParams.Add("app_name", appname) - // 根据 isBound 设置 bind_type 和 nickname if bindType == 1 { queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeQQ) } else { queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeWX) } - // 仅在解绑时添加 nickname + 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() - baseUrl.RawQuery = queryParams.Encode() - return baseUrl.String(), nil + // 拼接最终 URL + url := fmt.Sprintf("%s?%s", rooturl, queryParams.Encode()) + return url, 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 失败") - } +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 "", ecode.Fail.Sub("从缓存中获取用户信息失败") + return nil, ecode.Fail.Sub("从缓存中获取用户信息失败") } var gamelifeCache model.UserGamelifeCache @@ -252,38 +248,44 @@ func (s *gamelifeClient) GetBound(ctx context.Context, popenid string) (string, // 如果缓存不存在或已过期,重新调用 GetUserKeyIV data, err := s.GetUserKeyIV(ctx, popenid) if err != nil { - return "", ecode.Fail.Sub("获取用户信息失败") + 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 "", ecode.Fail.Sub("解析用户信息失败") + return nil, ecode.Fail.Sub("解析用户信息失败") } } - // 序列化原始数据 + + // 加密原始数据 oriData := g.Map{ "PopenId": popenid, "TimeStamp": time.Now().Unix(), } marshal, err := json.Marshal(oriData) if err != nil { - return "", ecode.Fail.Sub("序列化用户信息失败") + return nil, ecode.Fail.Sub("序列化用户信息失败") } - - // 加密用户信息 - aesEncrypt, err := encrypt.AesEncrypt(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.IV)) + aesEncrypt, err := encrypt.AesEncryptCBCPKCS5(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.IV)) if err != nil { - return "", ecode.Fail.Sub("加密用户信息失败") + return nil, 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() + 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 baseUrl.String(), nil + return &result, nil } diff --git a/utility/jwt/jwt.go b/utility/jwt/jwt.go index 4287d10..b267875 100644 --- a/utility/jwt/jwt.go +++ b/utility/jwt/jwt.go @@ -63,7 +63,7 @@ type ( func GenerateToken(in *TokenIn) (string, error) { expire := in.ExpireTime if expire <= 0 { - expire = 2 * time.Hour + expire = 24 * time.Hour } claims := jwtClaims{