diff --git a/internal/consts/gamelife.go b/internal/consts/gamelife.go index d4974c3..13d597f 100644 --- a/internal/consts/gamelife.go +++ b/internal/consts/gamelife.go @@ -10,7 +10,11 @@ const ( ) const ( - GetNonLoginTaskList = "GetNonloginTaskList" - GetTaskList = "GetTaskList" - QueryUserRoleList = "QueryUserRoleList" + GetNonLoginTaskList = "GetNonloginTaskList" + GetTaskList = "GetTaskList" + QueryUserRoleList = "QueryUserRoleList" + GetGift = "GetGift" + GetUserGoodsList = "GetUserGoodsList" + ExchangeGoods = "ExchangeGoods" + QueryUserGoodsDetail = "QueryUserGoodsDetail" ) diff --git a/internal/model/gamelife.go b/internal/model/gamelife.go index 0c76d1c..cd9e1cf 100644 --- a/internal/model/gamelife.go +++ b/internal/model/gamelife.go @@ -69,10 +69,160 @@ type UserRole struct { type UserRoleListResponse struct { RoleList []UserRole `json:"role_list"` } -type QQNetbarActivityIn struct { - ServiceName string // 服务名称 - TaskParam TaskParam // 参数体 - UserRoleParam UserRoleParam - PopenId string - BindType int +type GiftParam struct { + TaskId string `json:"task_id"` // 任务ID,必填 + AreaId int `json:"area_id,omitempty"` // 区域ID, + Gid int `json:"gid"` // 游戏ID,必填 + RoleIdx string `json:"roleIdx,omitempty"` // 角色ID +} + +type GoodsParam struct { + Gid int `json:"gid"` // 游戏ID,必填 + AppFilter string `json:"appfilter"` // 按照gid过滤游戏 + BigTime int64 `json:"big_time"` // 大时间:如果要控制时间范围,大的时间传这里,必填 + Pageidx string `json:"pageidx"` // 页码, 首页为空 + Num int `json:"num"` // 数量 + OrderType string `json:"order_type"` // winningtime:根据获取时间排序 overduetime:根据过期时间排序 + OrderByDesc bool `json:"orderby_desc"` // 降序 + GoodsStatus int `json:"goodsstatus"` // 查询的物品状态:0:查所有 2是已发放(成功和失败),4是未发放且未过期,6是未发放且已过期 +} + +type GoodsResponse struct { + Waters []water `json:"waters"` // 兑换流水 + PageIdx string `json:"pageidx"` // 下一页的页码 + Total int64 `json:"total"` // 总数 +} + +type ExchangeGoodsParam struct { + Water water // 兑换流水 + Gid int `json:"gid"` // 游戏ID,必填 + AreaId int `json:"area_id"` // 区服 id + ReloIdx string `json:"reloidx"` //FIXME +} +type ExchangeGoodsResponse struct { + Water water // 兑换流水 +} +type QueryUserGoodsDetailParam struct { + Gid int `json:"gid"` + WinningTime int64 `json:"winningtime"` // 用户领取礼包时间 + OrderId string `json:"orderid"` // 户领取流水订单id + IsActInfo int64 `json:"is_actinfo"` // 是否需要活动相关信息 0:默认需要 1:不需要 + IsDocument int64 `json:"is_document"` // 是否需要文案信息,使用限制信息、代金券图片、使用方式 0:默认需要 1:不需要 + IsDetail int64 `json:"is_detail"` // 是否需要物品详情 0:默认需要 1:不需要 +} +type QueryUserGoodsDetailResponse struct { + Water water // 兑换流水 +} +type QQNetbarActivityIn struct { + ServiceName string // 服务名称 + TaskParam TaskParam // 任务参数 + UserRoleParam UserRoleParam // 查询用户角色 + GiftParam GiftParam // 领取物品接口使用 + GoodsParam GoodsParam // 查询物品列表接口使用 + ExchangeGoodsParam ExchangeGoodsParam // 兑换物品接口使用 + QueryUserGoodsDetailParam QueryUserGoodsDetailParam // 查询用户物品详情接口使用 + PopenId string + BindType int + NickName string +} + +type GiftResponse struct { + GiftItem giftItem `json:"rst_list"` +} + +type gameRoleInfo struct { + // FIXME +} + +type userAddress struct { + // FIXME +} + +type waterExtraInfo struct { + // FIXME +} + +type goodsType struct { + // FIXME +} + +type goodsDisplayType struct { + // FIXME +} + +type merchantInfo struct { + // FIXME +} + +type goodsExtraInfo struct { + // FIXME +} + +type water struct { + Uid string `json:"uid"` // 用户id + UidType uint32 `json:"uidtype"` // 用户id的类型(0是openid,1是uin) + WinningTime uint32 `json:"winningtime"` // 获奖时间 + GoodsId uint32 `json:"goodsid"` // 物品id + GoodsCfg goodsCfg `json:"goodscfg"` // 物品配置置信息 + Gid uint32 `json:"gid"` // 活动id,领取的游戏渠道 + OverdueTime uint32 `json:"overduetime"` // 物品的过期时间 + Status uint32 `json:"status"` // 领取状态: 0未发放,1已发放,2是已发放且失败(失败),4是未发放且未过期,6物品已过期(未发放) + RecvUid string `json:"recvuid"` // 领奖的用户 id (qq号) + Role gameRoleInfo `json:"role"` // 游戏角色信息 + CdKey string `json:"cdkey"` // cdkey,实物的兑换码/游戏道具兑换码 + WaterType uint32 `json:"watertype"` // 抽奖流水类型, 0是抽奖,1是核销,2是 tgp + SrcType uint32 `json:"srctype"` // 抽奖来源类型id + SrcName string `json:"srcname"` // 抽奖来源类型名称 + ActId string `json:"actid"` // 活动id + LotteryId uint32 `json:"lotteryid"` // 抽奖区id + GiftId uint32 `json:"giftid"` // 礼包id + ShopId uint32 `json:"shopid"` // 门店id + Extra string `json:"extra"` // 线下O2O相关信息 + RecvTime uint32 `json:"recvtime"` // 领取时间 + Code string `json:"code"` // 核销码 + TransEq string `json:"traneq"` // 交易流水号 + Filter1 string `json:"filter1"` // 用于筛选流水记录的业务自定义条件1 + Filter2 string `json:"filter2"` // 用于筛选流水记录的业务自定义条件2 + UserAddrType uint32 `json:"useraddrtype"` // 用户地址类型 (1是默认地址, 2是用户确认后地址)——仅面向邮寄的物品使用 + UserAddr userAddress `json:"useraddr"` // 用户地址信息——仅面向类型的物品使用 + Cltip string `json:"cltip"` // 用户领奖ip + Bsave uint32 `json:"bsave"` // 是否保存到物品表 + SaveType uint32 `json:"savetype"` // 0表示用户积分流水, 1表示用户物品表,2表示折扣券 + Sid uint32 `json:"sid"` // 用户流水id,和uid1共同确定一个物品 + NeedPoints uint32 `json:"needpoints"` // 物品消耗的钱 + ExtraInfo waterExtraInfo `json:"extrainfo"` // 其余信息 + AppFilter string `json:"appfilter"` // 业务拓展数据,王者人生为空 + OrderId string `json:"orderid"` // 订单id,唯一标记一条流水 + MercId string `json:"mercid"` // 商家 id + TipOpenId string `json:"tipopenid"` // 游戏人生openid + GoodsIdStr string `json:"goods_id_str"` // 字符串类型Goods id + GiftIdStr string `json:"gift_id_str"` // 字符串类型Gift id + LottIdStr string `json:"lott_id_str"` // 字符串类型Lott id +} +type goodsCfg struct { + GoodsId uint32 `json:"goodsid"` // 物品id + Gid uint32 `json:"gid"` // 游戏id + GoodsType goodsType `json:"goodstype"` // 物品类型 + Maid string `json:"maid"` // ma单号 + Each uint32 `json:"each"` // 每次发奖的数量 + GoodsName string `json:"goodsname"` // 物品名称 + GoodsDetail string `json:"goodsdetail"` // 物品说明信息 + OverdueTime uint32 `json:"overduetime"` // 物品的过期时间 + Status uint32 `json:"status"` // 物品状态 0-下线 1-上线 + GoodsIcon string `json:"goodsicon"` // 物品图片地址 + Provider string `json:"provider"` // 物品提供方名称 + RecvWay string `json:"recvway"` // 领取方式 + RecvWayUrl string `json:"recvwayurl"` // 领取方式详情url + MailUrl string `json:"mailurl"` // 邮寄地址url + DisplayType goodsDisplayType `json:"displaytype"` // 物品展示类型(枚举或自定义类型) + Merchant merchantInfo `json:"merchant"` // 商家信息(需另外定义) + Extra goodsExtraInfo `json:"extra"` // 其它信息(需另外定义) + GoodsIdStr string `json:"goods_id_str"` // 字符串类型的物品ID + AppId string `json:"app_id"` // 所属业务ID + ActId string `json:"act_id"` // 活动ID +} + +type giftItem struct { + Result int `json:"result"` // 1-奖励发放成功,2-奖励兑换失败(礼包发放背包环节成功,需要重新引导领奖) 3-针对只需要发背包,然后引导用户到人生应用背包领奖的奖励类型,发奖成功了 4-water错误(一般是orderid),不做兑换操作 + Water water `json:"water"` // 礼包列表数据 } diff --git a/internal/model/user.go b/internal/model/user.go index 75666a7..cacc48b 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -166,7 +166,9 @@ type UserUnBoundUrlOut struct { } type UserBoundInfoIn struct { - PopenId string + PopenId string + BindType int + AppName string } type UserBoundInfoOut struct { IsBound bool diff --git a/utility/gamelife/gamelife.go b/utility/gamelife/gamelife.go index 814e5e2..41e6f02 100644 --- a/utility/gamelife/gamelife.go +++ b/utility/gamelife/gamelife.go @@ -108,19 +108,19 @@ func newGamelifeClient(ctx context.Context) *gamelifeClient { } // 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) { +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 model.UserGamelifeCache{}, ecode.Fail.Sub("序列化 json 数据出现异常") + return nil, ecode.Fail.Sub("序列化 json 数据出现异常") } encrypted, err := rsa.GetRsaClient().EncryptWithRsaPublicKey(marshaled) if err != nil { - return model.UserGamelifeCache{}, ecode.Fail.Sub("序列化 json 数据出现异常") + return nil, ecode.Fail.Sub("序列化 json 数据出现异常") } type httpResult struct { @@ -136,17 +136,17 @@ func (s *gamelifeClient) GetUserKeyIV(ctx context.Context, popenID string) (mode SetResult(&result). Post(s.keyIVURLMap[s.config.Mode]) if err != nil || resp.StatusCode() != 200 { - return model.UserGamelifeCache{}, ecode.Fail.Sub("获取用户信息失败") + return nil, ecode.Fail.Sub("获取用户信息失败") } decoded, err := encrypt.Base64Decode(result.Secret) if err != nil { - return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败") + return nil, ecode.Fail.Sub("解密用户信息失败") } plain, err := rsa.GetRsaClient().DecryptWithRsaPrivateKey(decoded) if err != nil { - return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败") + return nil, ecode.Fail.Sub("解密用户信息失败") } var aesResult struct { @@ -154,17 +154,17 @@ func (s *gamelifeClient) GetUserKeyIV(ctx context.Context, popenID string) (mode IV string `json:"iv"` } if err := json.Unmarshal(plain, &aesResult); err != nil { - return model.UserGamelifeCache{}, ecode.Fail.Sub("解密用户信息失败") + return nil, ecode.Fail.Sub("解密用户信息失败") } - cache := model.UserGamelifeCache{ + 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 model.UserGamelifeCache{}, ecode.Fail.Sub("设置用户信息失败") + return nil, ecode.Fail.Sub("设置用户信息失败") } return cache, nil @@ -178,33 +178,38 @@ func (s *gamelifeClient) GetUrl(ctx context.Context, popenID, appName, nickname if !isBound { rootURL = s.unBoundURLMap[s.config.Mode] } - - cache, err := s.getOrRefreshCache(ctx, popenID) + cache, err := s.ensureUserCacheWithParams(ctx, popenID, appName, nickname, bindType) if err != nil { return "", err } - cache.Params, err = s.buildQueryParams(ctx, popenID, cache, appName, nickname, bindType, isBound) - if err != nil { - return "", err - } - - cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenID) - ttl, err := g.Redis().TTL(ctx, cacheKey) - if err != nil { - return "", ecode.Fail.Sub("获取缓存过期时间失败") - } - if err := g.Redis().SetEX(ctx, cacheKey, cache, ttl); err != nil { - return "", ecode.Fail.Sub("更新缓存失败") - } - return fmt.Sprintf("%s?%s", rootURL, cache.Params), nil } +func (s *gamelifeClient) ensureUserCache(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 +} // 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.getOrRefreshCache(ctx, popenID) + cache, err := s.ensureUserCache(ctx, popenID) if err != nil { return nil, err } @@ -252,7 +257,7 @@ func (s *gamelifeClient) GetBound(ctx context.Context, popenID string) (*model.U // 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) { +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(), @@ -310,31 +315,11 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar return &result, nil case consts.GetTaskList: - cache, err := s.getOrRefreshCache(ctx, in.PopenId) + value, err := dao.Games.Ctx(ctx).Where(do.Games{GameId: in.QueryUserGoodsDetailParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() if err != nil { - return nil, err - } - cacheKey := fmt.Sprintf(consts.GameLifeUserKey, in.PopenId) - if cache.Params == "" { - // Reconstruct Params with default values - 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.Params, err = s.buildQueryParams(ctx, in.PopenId, cache, value.String(), "", in.BindType, true) - if err != nil { - return nil, err - } - // Update cache with new Params - ttl, err := g.Redis().TTL(ctx, cacheKey) - if err != nil { - return nil, ecode.Fail.Sub("获取缓存过期时间失败") - } - if err := g.Redis().SetEX(ctx, cacheKey, cache, ttl); err != nil { - return nil, ecode.Fail.Sub("更新缓存失败") - } + return nil, ecode.Fail.Sub("获取游戏编码失败") } + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) in.TaskParam.BrandId = s.config.BrandID var result model.GameTaskResponse resp, err := client.R(). @@ -347,31 +332,11 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar } return &result, nil case consts.QueryUserRoleList: - cache, err := s.getOrRefreshCache(ctx, in.PopenId) + value, err := dao.Games.Ctx(ctx).Where(do.Games{GameId: in.QueryUserGoodsDetailParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() if err != nil { - return nil, err - } - cacheKey := fmt.Sprintf(consts.GameLifeUserKey, in.PopenId) - if cache.Params == "" { - // Reconstruct Params with default values - 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.Params, err = s.buildQueryParams(ctx, in.PopenId, cache, value.String(), "", in.BindType, true) - if err != nil { - return nil, err - } - // Update cache with new Params - ttl, err := g.Redis().TTL(ctx, cacheKey) - if err != nil { - return nil, ecode.Fail.Sub("获取缓存过期时间失败") - } - if err := g.Redis().SetEX(ctx, cacheKey, cache, ttl); err != nil { - return nil, ecode.Fail.Sub("更新缓存失败") - } + return nil, ecode.Fail.Sub("获取游戏编码失败") } + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) var result model.UserRoleListResponse resp, err := client.R(). SetContext(ctx). @@ -382,26 +347,113 @@ func (s *gamelifeClient) RequestActivity(ctx context.Context, in *model.QQNetbar return nil, ecode.Fail.Sub("请求出现异常") } return &result.RoleList, nil - + case consts.GetGift: + 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.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + var result model.GiftResponse + resp, err := client.R(). + SetContext(ctx). + SetBody(in.GiftParam). + SetResult(&result). + Post(fmt.Sprintf("%s%s?%s", taskURL, consts.GetGift, cache.Params)) + if err != nil || resp.IsError() { + return nil, ecode.Fail.Sub("请求出现异常") + } + return &result, nil + case consts.GetUserGoodsList: + 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.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + var result model.GoodsResponse + resp, err := client.R(). + SetContext(ctx). + SetBody(in.GoodsParam). + SetResult(&result). + Post(fmt.Sprintf("%s%s?%s", taskURL, consts.GetUserGoodsList, cache.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.QueryUserGoodsDetailParam.Gid}).Fields(dao.Games.Columns().GameCode).Value() + if err != nil { + return nil, ecode.Fail.Sub("获取游戏编码失败") + } + cache, err := s.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + var result model.ExchangeGoodsResponse + resp, err := client.R(). + SetContext(ctx). + SetBody(in.ExchangeGoodsParam). + SetResult(&result). + Post(fmt.Sprintf("%s%s?%s", taskURL, consts.ExchangeGoods, cache.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.ensureUserCacheWithParams(ctx, in.PopenId, value.String(), in.NickName, in.BindType) + 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, cache.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)) } } -// getOrRefreshCache retrieves user cache or refreshes it if expired. -func (s *gamelifeClient) getOrRefreshCache(ctx context.Context, popenID string) (model.UserGamelifeCache, error) { - cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenID) +// ensureUserCacheWithParams ensures user cache exists and contains a valid Params string. +// It handles cache retrieval, fallback refresh, Params generation, and cache update. +func (s *gamelifeClient) ensureUserCacheWithParams(ctx context.Context, popenId, appname, nickname string, bindType int) (*model.UserGamelifeCache, error) { + cacheKey := fmt.Sprintf(consts.GameLifeUserKey, popenId) cacheData, err := g.Redis().Get(ctx, cacheKey) if err != nil { - return model.UserGamelifeCache{}, ecode.Fail.Sub("从缓存中获取用户信息失败") + return nil, ecode.Fail.Sub("从缓存中获取用户信息失败") } - var cache model.UserGamelifeCache + var cache *model.UserGamelifeCache if cacheData.IsEmpty() { - return s.GetUserKeyIV(ctx, popenID) + 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("解析用户信息失败") + } } - if err := json.Unmarshal(cacheData.Bytes(), &cache); err != nil { - return model.UserGamelifeCache{}, ecode.Fail.Sub("解析用户信息失败") + + if cache.Params == "" { + cache.Params, err = s.buildQueryParams(ctx, popenId, cache, appname, nickname, bindType, true) + if err != nil { + return nil, err + } + + ttl, err := g.Redis().TTL(ctx, cacheKey) + if err != nil { + return nil, ecode.Fail.Sub("获取缓存过期时间失败") + } + if err := g.Redis().SetEX(ctx, cacheKey, cache, ttl); err != nil { + return nil, ecode.Fail.Sub("更新缓存失败") + } } + return cache, nil }