388 lines
14 KiB
Go
388 lines
14 KiB
Go
package gameopen
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"strings"
|
||
|
||
"chatapp3-golang/internal/integration"
|
||
"chatapp3-golang/internal/model"
|
||
"chatapp3-golang/internal/service/taskcenter"
|
||
)
|
||
|
||
const (
|
||
hotgameVendorType = "HOTGAME"
|
||
hotgameCodeTokenInvalid = 1001
|
||
hotgameCodeInsufficientBalance = 2001
|
||
hotgameCodeDuplicateOrder = 3001
|
||
hotgameCodeBadRequest = 4001
|
||
hotgameCodeSignatureError = 4002
|
||
hotgameCodeServerError = 5000
|
||
)
|
||
|
||
// HotgameGetUserInfoRequest 是热游 /getUserInfo 回调入参。
|
||
type HotgameGetUserInfoRequest struct {
|
||
GameID string `json:"gameId"`
|
||
UID string `json:"uid"`
|
||
Token string `json:"token"`
|
||
Sign string `json:"sign"`
|
||
}
|
||
|
||
// HotgameUpdateBalanceRequest 是热游 /updateBalance 回调入参;roundId 文档是 int,所以这里兼容数字和字符串。
|
||
type HotgameUpdateBalanceRequest struct {
|
||
OrderID string `json:"orderId"`
|
||
GameID string `json:"gameId"`
|
||
RoundID any `json:"roundId"`
|
||
UID string `json:"uid"`
|
||
Coin int64 `json:"coin"`
|
||
Type int `json:"type"`
|
||
Token string `json:"token"`
|
||
Sign string `json:"sign"`
|
||
}
|
||
|
||
// HandleHotgameGetUserInfo 按热游协议查询玩家资料和余额。
|
||
func (s *GameOpenService) HandleHotgameGetUserInfo(ctx context.Context, req HotgameGetUserInfoRequest, rawJSON string) CallbackResponse {
|
||
if err := s.validateHotgameGetUserInfoRequest(req); err != nil {
|
||
resp := failResponse(hotgameCodeBadRequest, err.Error())
|
||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, "", req.toQueryUserRequest(), nil, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
appKey := s.resolveCallbackAppKeyForVendor(ctx, hotgameVendorType, req.GameID, req.Token)
|
||
if strings.TrimSpace(appKey) == "" {
|
||
resp := failResponse(hotgameCodeBadRequest, "app key is blank")
|
||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, "", req.toQueryUserRequest(), nil, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
if !verifySign(req.Sign, buildHotgameGetUserInfoSign(req, appKey)) {
|
||
resp := failResponse(hotgameCodeSignatureError, "signature error")
|
||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, "", req.toQueryUserRequest(), nil, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
|
||
req.Token = normalizeCallbackToken(req.Token)
|
||
credential, err := s.java.AuthenticateToken(ctx, req.Token)
|
||
if err != nil {
|
||
resp := failResponse(hotgameCodeTokenInvalid, "token invalid")
|
||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, "", req.toQueryUserRequest(), nil, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
resolved, err := s.resolveUserWithToken(ctx, credential, req.UID)
|
||
if err != nil {
|
||
resp := failResponse(hotgameCodeTokenInvalid, "uid token mismatch")
|
||
userID := int64(credential.UserID)
|
||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, credential.SysOrigin, req.toQueryUserRequest(), &userID, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
userID := resolved.userID
|
||
|
||
profile := resolved.profile
|
||
if int64(profile.ID) == 0 {
|
||
profile, err = s.java.GetUserProfile(ctx, userID)
|
||
}
|
||
if err != nil {
|
||
resp := failResponse(hotgameCodeServerError, "profile load failed")
|
||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, credential.SysOrigin, req.toQueryUserRequest(), &userID, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
balance, err := s.readUserBalance(ctx, userID)
|
||
if err != nil {
|
||
resp := failResponse(hotgameCodeServerError, "balance load failed")
|
||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, credential.SysOrigin, req.toQueryUserRequest(), &userID, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
|
||
resp := successResponse(map[string]any{
|
||
"uid": req.UID,
|
||
"nickname": profile.UserNickname,
|
||
"avatar": profile.UserAvatar,
|
||
"coin": balance,
|
||
"vipLevel": 0,
|
||
})
|
||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, credential.SysOrigin, req.toQueryUserRequest(), &userID, gameOpenLogStatusSuccess)
|
||
return resp
|
||
}
|
||
|
||
// HandleHotgameUpdateBalance 按热游协议处理实时扣加币,并用 orderId 做钱包事件幂等。
|
||
func (s *GameOpenService) HandleHotgameUpdateBalance(ctx context.Context, req HotgameUpdateBalanceRequest, rawJSON string) CallbackResponse {
|
||
updateReq := req.toUpdateCoinRequest()
|
||
if err := s.validateHotgameUpdateBalanceRequest(req); err != nil {
|
||
resp := failResponse(hotgameCodeBadRequest, err.Error())
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, "", updateReq, nil, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
appKey := s.resolveCallbackAppKeyForVendor(ctx, hotgameVendorType, req.GameID, req.Token)
|
||
if strings.TrimSpace(appKey) == "" {
|
||
resp := failResponse(hotgameCodeBadRequest, "app key is blank")
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, "", updateReq, nil, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
if !verifySign(req.Sign, buildHotgameUpdateBalanceSign(req, appKey)) {
|
||
resp := failResponse(hotgameCodeSignatureError, "signature error")
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, "", updateReq, nil, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
|
||
req.Token = normalizeCallbackToken(req.Token)
|
||
updateReq.Token = req.Token
|
||
credential, err := s.java.AuthenticateToken(ctx, req.Token)
|
||
if err != nil {
|
||
resp := failResponse(hotgameCodeTokenInvalid, "token invalid")
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, "", updateReq, nil, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
resolved, err := s.resolveUserWithToken(ctx, credential, req.UID)
|
||
if err != nil {
|
||
resp := failResponse(hotgameCodeTokenInvalid, "uid token mismatch")
|
||
userID := int64(credential.UserID)
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
userID := resolved.userID
|
||
|
||
eventID := "GAME_HOTGAME:" + strings.TrimSpace(req.OrderID)
|
||
exists, err := s.java.ExistsGoldEvent(ctx, eventID)
|
||
if err != nil {
|
||
resp := failResponse(hotgameCodeServerError, "event check failed")
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
if exists {
|
||
resp := failResponse(hotgameCodeDuplicateOrder, "duplicate orderId")
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
|
||
if err := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{
|
||
ReceiptType: receiptTypeFromType(req.Type),
|
||
UserID: userID,
|
||
SysOrigin: credential.SysOrigin,
|
||
EventID: eventID,
|
||
Remark: fmt.Sprintf("hotgame=%s type=%d round=%s", req.GameID, req.Type, updateReq.RoundID),
|
||
Amount: integration.NewPennyAmountPayloadFromDollar(req.Coin),
|
||
CloseDelayAsset: false,
|
||
OpUserType: "APP",
|
||
CustomizeOrigin: "GAME_HOTGAME_" + strings.TrimSpace(req.GameID),
|
||
CustomizeOriginDesc: "HOTGAME[" + strings.TrimSpace(req.GameID) + "]",
|
||
}); err != nil {
|
||
// 热游文档要求扣款失败必须返回失败码;支出失败统一归到余额不足类,防止游戏继续开奖。
|
||
code := hotgameCodeServerError
|
||
if req.Type == 1 || looksLikeInsufficientBalance(err) {
|
||
code = hotgameCodeInsufficientBalance
|
||
}
|
||
resp := failResponse(code, err.Error())
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
|
||
balance, err := s.readUserBalance(ctx, userID)
|
||
if err != nil {
|
||
resp := failResponse(hotgameCodeServerError, "balance load failed")
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
||
return resp
|
||
}
|
||
|
||
resp := successResponse(map[string]any{"coin": balance, "responseId": eventID})
|
||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusSuccess)
|
||
s.reportHotgameConsumeTaskEvent(ctx, credential.SysOrigin, userID, updateReq)
|
||
return resp
|
||
}
|
||
|
||
func (req HotgameGetUserInfoRequest) toQueryUserRequest() QueryUserRequest {
|
||
return QueryUserRequest{
|
||
GameID: req.GameID,
|
||
UID: req.UID,
|
||
Token: req.Token,
|
||
Sign: req.Sign,
|
||
}
|
||
}
|
||
|
||
func (req HotgameUpdateBalanceRequest) toUpdateCoinRequest() UpdateCoinRequest {
|
||
return UpdateCoinRequest{
|
||
OrderID: req.OrderID,
|
||
GameID: req.GameID,
|
||
RoundID: hotgameRoundIDString(req.RoundID),
|
||
UID: req.UID,
|
||
Coin: req.Coin,
|
||
Type: req.Type,
|
||
Token: req.Token,
|
||
Sign: req.Sign,
|
||
}
|
||
}
|
||
|
||
func (s *GameOpenService) validateHotgameGetUserInfoRequest(req HotgameGetUserInfoRequest) error {
|
||
switch {
|
||
case strings.TrimSpace(req.GameID) == "":
|
||
return newBadRequest("gameId is required")
|
||
case strings.TrimSpace(req.UID) == "":
|
||
return newBadRequest("uid is required")
|
||
case strings.TrimSpace(req.Token) == "":
|
||
return newBadRequest("token is required")
|
||
case strings.TrimSpace(req.Sign) == "":
|
||
return newBadRequest("sign is required")
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func (s *GameOpenService) validateHotgameUpdateBalanceRequest(req HotgameUpdateBalanceRequest) error {
|
||
switch {
|
||
case strings.TrimSpace(req.OrderID) == "":
|
||
return newBadRequest("orderId is required")
|
||
case strings.TrimSpace(req.GameID) == "":
|
||
return newBadRequest("gameId is required")
|
||
case strings.TrimSpace(hotgameRoundIDString(req.RoundID)) == "":
|
||
return newBadRequest("roundId is required")
|
||
case strings.TrimSpace(req.UID) == "":
|
||
return newBadRequest("uid is required")
|
||
case req.Coin <= 0:
|
||
return newBadRequest("coin is required")
|
||
case req.Type != 1 && req.Type != 2:
|
||
return newBadRequest("type must be 1 or 2")
|
||
case strings.TrimSpace(req.Token) == "":
|
||
return newBadRequest("token is required")
|
||
case strings.TrimSpace(req.Sign) == "":
|
||
return newBadRequest("sign is required")
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func buildHotgameGetUserInfoSign(req HotgameGetUserInfoRequest, key string) string {
|
||
return md5Hex(req.GameID + req.UID + req.Token + key)
|
||
}
|
||
|
||
func buildHotgameUpdateBalanceSign(req HotgameUpdateBalanceRequest, key string) string {
|
||
return md5Hex(
|
||
req.OrderID +
|
||
req.GameID +
|
||
hotgameRoundIDString(req.RoundID) +
|
||
req.UID +
|
||
fmt.Sprintf("%d", req.Coin) +
|
||
fmt.Sprintf("%d", req.Type) +
|
||
req.Token +
|
||
key,
|
||
)
|
||
}
|
||
|
||
func hotgameRoundIDString(value any) string {
|
||
switch typed := value.(type) {
|
||
case nil:
|
||
return ""
|
||
case string:
|
||
return strings.TrimSpace(typed)
|
||
case json.Number:
|
||
return strings.TrimSpace(typed.String())
|
||
case float64:
|
||
if typed == float64(int64(typed)) {
|
||
return fmt.Sprintf("%d", int64(typed))
|
||
}
|
||
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", typed), "0"), ".")
|
||
default:
|
||
return strings.TrimSpace(fmt.Sprint(typed))
|
||
}
|
||
}
|
||
|
||
func (s *GameOpenService) resolveCallbackAppKeyForVendor(ctx context.Context, vendorType string, gameID, token string) string {
|
||
if s != nil && s.repo.DB != nil {
|
||
if sysOrigin := callbackTokenSysOrigin(token); sysOrigin != "" {
|
||
if appKey := s.resolveActiveProviderAppKey(ctx, vendorType, sysOrigin); appKey != "" {
|
||
return appKey
|
||
}
|
||
}
|
||
if appKey := s.resolveProviderAppKeyByGame(ctx, vendorType, gameID); appKey != "" {
|
||
return appKey
|
||
}
|
||
}
|
||
if s == nil {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(s.cfg.GameOpen.AppKey)
|
||
}
|
||
|
||
func (s *GameOpenService) resolveProviderAppKeyByGame(ctx context.Context, vendorType string, gameID string) string {
|
||
gameID = strings.TrimSpace(gameID)
|
||
if gameID == "" {
|
||
return ""
|
||
}
|
||
switch strings.ToUpper(strings.TrimSpace(vendorType)) {
|
||
case "LINGXIAN":
|
||
return s.resolveLingxianAppKeyByGame(ctx, gameID)
|
||
case hotgameVendorType:
|
||
var row struct {
|
||
AppKey string
|
||
}
|
||
err := s.repo.DB.WithContext(ctx).
|
||
Table("hotgame_provider_config AS cfg").
|
||
Select("cfg.app_key AS app_key").
|
||
Joins("JOIN sys_game_list_vendor_ext ext ON ext.sys_origin = cfg.sys_origin AND ext.profile = cfg.profile AND ext.vendor_type = ? AND ext.enabled = ?", hotgameVendorType, true).
|
||
Where("cfg.active = ? AND ext.vendor_game_id = ?", true, gameID).
|
||
Order("cfg.update_time DESC").
|
||
Limit(1).
|
||
Scan(&row).Error
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(row.AppKey)
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func (s *GameOpenService) resolveActiveProviderAppKey(ctx context.Context, vendorType string, sysOrigin string) string {
|
||
switch strings.ToUpper(strings.TrimSpace(vendorType)) {
|
||
case "LINGXIAN":
|
||
return s.resolveActiveLingxianAppKey(ctx, sysOrigin)
|
||
case hotgameVendorType:
|
||
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
||
if sysOrigin == "" {
|
||
return ""
|
||
}
|
||
var row model.HotgameProviderConfig
|
||
err := s.repo.DB.WithContext(ctx).
|
||
Where("sys_origin = ? AND active = ?", sysOrigin, true).
|
||
Order("update_time DESC").
|
||
Limit(1).
|
||
First(&row).Error
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(row.AppKey)
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func looksLikeInsufficientBalance(err error) bool {
|
||
if err == nil {
|
||
return false
|
||
}
|
||
message := strings.ToLower(err.Error())
|
||
return strings.Contains(message, "insufficient") ||
|
||
strings.Contains(message, "balance not made") ||
|
||
strings.Contains(message, "余额不足")
|
||
}
|
||
|
||
func (s *GameOpenService) reportHotgameConsumeTaskEvent(ctx context.Context, sysOrigin string, userID int64, req UpdateCoinRequest) {
|
||
if s.taskReporter == nil || receiptTypeFromType(req.Type) != "EXPENDITURE" {
|
||
return
|
||
}
|
||
deltaValue := req.Coin
|
||
if deltaValue < 0 {
|
||
deltaValue = -deltaValue
|
||
}
|
||
if deltaValue <= 0 {
|
||
return
|
||
}
|
||
payload := map[string]any{
|
||
"provider": hotgameVendorType,
|
||
"orderId": strings.TrimSpace(req.OrderID),
|
||
"gameId": strings.TrimSpace(req.GameID),
|
||
"roundId": strings.TrimSpace(req.RoundID),
|
||
}
|
||
if err := s.taskReporter.ReportSimpleEvent(ctx, sysOrigin, "GAME_HOTGAME_CONSUME:"+strings.TrimSpace(req.OrderID), taskcenter.EventTypeGameConsumeGold, userID, deltaValue, payload); err != nil {
|
||
log.Printf("report hotgame task event failed: %v", err)
|
||
}
|
||
}
|