package game import ( "context" "crypto/md5" "encoding/hex" "encoding/json" "strconv" "strings" "time" gamev1 "hyapp.local/api/proto/game/v1" "hyapp/pkg/xerr" gamedomain "hyapp/services/game-service/internal/domain/game" ) const ( leaderccCodeOK = 0 leaderccCodeTimeout = 4000 leaderccCodeInsufficientBalance = 4004 leaderccCodeInvalidArgument = 4005 leaderccCodeTokenInvalid = 10001 leaderccCodeRiskRejected = 10002 leaderccCodeDuplicatedOrder = 10003 leaderccCodeSignatureInvalid = 10004 leaderccDefaultTokenTTL = 24 * time.Hour ) type leaderccAdapterConfig struct { UIDMode string `json:"uid_mode"` DefaultLang string `json:"default_lang"` TokenTTLSeconds int64 `json:"token_ttl_seconds"` } func leaderccConfigFromPlatform(value any) leaderccAdapterConfig { var raw string switch typed := value.(type) { case gamedomain.Platform: raw = typed.AdapterConfigJSON case gamedomain.LaunchableGame: raw = typed.AdapterConfigJSON } var config leaderccAdapterConfig _ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config) config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode)) config.DefaultLang = strings.TrimSpace(config.DefaultLang) return config } func (c leaderccAdapterConfig) TokenTTL() time.Duration { if c.TokenTTLSeconds > 0 { return time.Duration(c.TokenTTLSeconds) * time.Second } return leaderccDefaultTokenTTL } func (s *Service) handleLeaderCCOperation(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest, operation string, requestHash string) ([]byte, string, string, bool, string, error) { if !callbackIPAllowed(req, platform.CallbackIPWhitelist) { raw, contentType := leaderccJSON(leaderccCodeRiskRejected, "ip forbidden", map[string]any{}) return raw, contentType, strconv.Itoa(leaderccCodeRiskRejected), false, "", nil } switch operation { case "userinfo", "user_info", "get_user_info": return s.handleLeaderCCUserInfo(ctx, app, platform, req) case "change_coin", "change_balance", "update_coin": return s.handleLeaderCCChangeCoin(ctx, app, platform, req, requestHash) case "repair_order", "repair", "repair_coin": return s.handleLeaderCCRepairOrder(ctx, app, platform, req) default: raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{}) return raw, contentType, strconv.Itoa(leaderccCodeOK), false, "", nil } } func (s *Service) handleLeaderCCUserInfo(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) { var body struct { GameID string `json:"gameId"` UID string `json:"uid"` Token string `json:"token"` RoomID string `json:"roomId"` Sign string `json:"sign"` } if err := decodeLeaderCCJSON(req.GetRawBody(), &body); err != nil { return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "") } if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.GameID, body.UID, body.Token, body.RoomID) { return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, "") } config := leaderccConfigFromPlatform(platform) session, err := s.validLeaderCCSession(ctx, app, body.Token) if err != nil || !leaderccSessionMatches(session, config, body.UID, body.GameID) { return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, "") } snapshot, err := s.yomiUserSnapshot(ctx, app, req, session.UserID) if err != nil { return leaderccAdapterError(leaderccCodeFromError(err), leaderccMessageFromError(err), true, "") } raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{ "uid": body.UID, "nickname": snapshot.Nickname, "avatar": snapshot.Avatar, "coin": snapshot.Balance, }) return raw, contentType, strconv.Itoa(leaderccCodeOK), true, "", nil } func (s *Service) handleLeaderCCChangeCoin(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest, requestHash string) ([]byte, string, string, bool, string, error) { var body struct { OrderID string `json:"orderId"` GameID string `json:"gameId"` RoundID string `json:"roundId"` UID string `json:"uid"` Coin int64 `json:"coin"` Type int32 `json:"type"` RewardType int32 `json:"rewardType"` Token string `json:"token"` WinID string `json:"winId"` RoomID string `json:"roomId"` Sign string `json:"sign"` } if err := decodeLeaderCCJSON(req.GetRawBody(), &body); err != nil { return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "") } providerOrderID := strings.TrimSpace(body.OrderID) if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.OrderID, body.GameID, body.RoundID, body.UID, strconv.FormatInt(body.Coin, 10), strconv.FormatInt(int64(body.Type), 10), strconv.FormatInt(int64(body.RewardType), 10), body.Token, body.WinID, body.RoomID) { return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, providerOrderID) } config := leaderccConfigFromPlatform(platform) session, err := s.validLeaderCCSession(ctx, app, body.Token) if err != nil || !leaderccSessionMatches(session, config, body.UID, body.GameID) { return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, providerOrderID) } opType := leaderccOpType(body.Type) if providerOrderID == "" || opType == "" || body.Coin < 0 { return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", true, providerOrderID) } result, err := s.applyYomiCoinChange(ctx, app, req, session, providerOrderID, body.RoundID, opType, body.Coin, body.RoomID, requestHash) if err != nil { code := leaderccCodeFromError(err) message := leaderccMessageFromError(err) balance := s.bestEffortCoinBalance(ctx, app, req.GetMeta().GetRequestId(), session.UserID) raw, contentType := leaderccJSON(code, message, map[string]any{"coin": balance}) return raw, contentType, strconv.Itoa(code), true, providerOrderID, nil } raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{"coin": result.BalanceAfter}) return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil } func (s *Service) handleLeaderCCRepairOrder(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) { var body struct { OrderID string `json:"orderId"` GameID string `json:"gameId"` RoundID string `json:"roundId"` UID string `json:"uid"` Coin int64 `json:"coin"` RewardType int32 `json:"rewardType"` WinID string `json:"winId"` RoomID string `json:"roomId"` Sign string `json:"sign"` } if err := decodeLeaderCCJSON(req.GetRawBody(), &body); err != nil { return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "") } providerOrderID := strings.TrimSpace(body.OrderID) if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.OrderID, body.GameID, body.RoundID, body.UID, strconv.FormatInt(body.Coin, 10), strconv.FormatInt(int64(body.RewardType), 10), body.WinID, body.RoomID) { return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, providerOrderID) } if providerOrderID == "" || strings.TrimSpace(body.GameID) == "" || strings.TrimSpace(body.UID) == "" || body.Coin < 0 { return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", true, providerOrderID) } _, _, err := s.repository.CreateRepairOrder(ctx, gamedomain.RepairOrder{ AppCode: app, RepairID: "grep_" + stableHash(app+"|"+platform.PlatformCode+"|"+providerOrderID), PlatformCode: platform.PlatformCode, ProviderOrderID: providerOrderID, Reason: "leadercc_repair_order", Status: gamedomain.RepairStatusLogged, }) if err != nil { return leaderccAdapterError(leaderccCodeFromError(err), leaderccMessageFromError(err), true, providerOrderID) } raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{}) return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil } func (s *Service) validLeaderCCSession(ctx context.Context, app string, token string) (gamedomain.LaunchSession, error) { session, err := s.validYomiSession(ctx, app, token) if err != nil { return gamedomain.LaunchSession{}, err } return session, nil } func leaderccSessionMatches(session gamedomain.LaunchSession, config leaderccAdapterConfig, uid string, gameID string) bool { if strings.TrimSpace(gameID) != "" && strings.TrimSpace(gameID) != strings.TrimSpace(session.ProviderGameID) { return false } return strings.TrimSpace(uid) == externalUserID(session, config.UIDMode) } func leaderccOpType(value int32) string { switch value { case 1: return "debit" case 2: return "credit" default: return "" } } func decodeLeaderCCJSON(raw []byte, out any) error { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() return decoder.Decode(out) } func leaderccSignValid(actual string, key string, parts ...string) bool { key = strings.TrimSpace(key) if key == "" { return false } joined := strings.Builder{} for _, part := range parts { joined.WriteString(strings.TrimSpace(part)) } joined.WriteString(key) sum := md5.Sum([]byte(joined.String())) expected := hex.EncodeToString(sum[:]) return strings.EqualFold(strings.TrimSpace(actual), expected) } func leaderccAdapterError(code int, message string, signatureValid bool, providerRequestID string) ([]byte, string, string, bool, string, error) { raw, contentType := leaderccJSON(code, message, map[string]any{}) return raw, contentType, strconv.Itoa(code), signatureValid, providerRequestID, nil } func leaderccJSON(errorCode int, message string, data any) ([]byte, string) { payload := map[string]any{"errorCode": errorCode} if data != nil { payload["data"] = data } if strings.TrimSpace(message) != "" { payload["message"] = message } return jsonResponse(payload) } func leaderccCodeFromError(err error) int { switch { case err == nil: return leaderccCodeOK case xerr.IsCode(err, xerr.InsufficientBalance): return leaderccCodeInsufficientBalance case xerr.IsCode(err, xerr.SessionExpired), xerr.IsCode(err, xerr.Unauthorized): return leaderccCodeTokenInvalid case xerr.IsCode(err, xerr.IdempotencyConflict): return leaderccCodeDuplicatedOrder case xerr.IsCode(err, xerr.Unavailable): return leaderccCodeTimeout default: return leaderccCodeInvalidArgument } } func leaderccMessageFromError(err error) string { switch leaderccCodeFromError(err) { case leaderccCodeInsufficientBalance: return "coin not enough" case leaderccCodeTokenInvalid: return "token invalid" case leaderccCodeDuplicatedOrder: return "duplicated order" case leaderccCodeTimeout: return "network timeout" default: if err != nil && strings.TrimSpace(xerr.MessageOf(err)) != "" { return xerr.MessageOf(err) } return "invalid argument" } }