package gamemanagement import ( "fmt" "strconv" "strings" "time" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/middleware" gamev1 "hyapp.local/api/proto/game/v1" "github.com/gin-gonic/gin" ) type platformRequest struct { PlatformCode string `json:"platformCode"` PlatformName string `json:"platformName"` Status string `json:"status"` APIBaseURL string `json:"apiBaseUrl"` // AdapterType 只允许服务端白名单中的值,避免后台误填导致回调走错协议。 AdapterType string `json:"adapterType"` // CallbackSecret 只有提交非空时才覆盖旧密钥,编辑其它字段时前端传空即可。 CallbackSecret string `json:"callbackSecret"` CallbackIPWhitelist []string `json:"callbackIpWhitelist"` // AdapterConfigJSON 是厂商扩展配置,保持 JSON 字符串透传给 game-service。 AdapterConfigJSON string `json:"adapterConfigJson"` SortOrder int32 `json:"sortOrder"` } type catalogRequest struct { GameID string `json:"gameId"` PlatformCode string `json:"platformCode"` ProviderGameID string `json:"providerGameId"` GameName string `json:"gameName"` Category string `json:"category"` IconURL string `json:"iconUrl"` CoverURL string `json:"coverUrl"` LaunchMode string `json:"launchMode"` Orientation string `json:"orientation"` SafeHeight int32 `json:"safeHeight"` MinCoin int64 `json:"minCoin"` Status string `json:"status"` SortOrder int32 `json:"sortOrder"` Tags []string `json:"tags"` } type statusRequest struct { Status string `json:"status"` } type diceConfigRequest struct { Status string `json:"status"` StakeOptions []diceStakeReq `json:"stakeOptions"` FeeBPS int32 `json:"feeBps"` PoolBPS int32 `json:"poolBps"` MinPlayers int32 `json:"minPlayers"` MaxPlayers int32 `json:"maxPlayers"` RobotEnabled bool `json:"robotEnabled"` RobotMatchWaitMS int64 `json:"robotMatchWaitMs"` } type diceStakeReq struct { StakeCoin int64 `json:"stakeCoin"` Enabled bool `json:"enabled"` SortOrder int32 `json:"sortOrder"` } type dicePoolAdjustRequest struct { AmountCoin int64 `json:"amountCoin"` Direction string `json:"direction"` Reason string `json:"reason"` } type selfGameStakePoolRequest struct { TargetBalanceCoin int64 `json:"targetBalanceCoin"` SafeBalanceCoin int64 `json:"safeBalanceCoin"` WarningBalanceCoin int64 `json:"warningBalanceCoin"` DangerBalanceCoin int64 `json:"dangerBalanceCoin"` Status string `json:"status"` } type selfGameNewUserPolicyRequest struct { Enabled bool `json:"enabled"` ProtectionRounds int32 `json:"protectionRounds"` ProtectionHours int32 `json:"protectionHours"` MaxStakeCoin int64 `json:"maxStakeCoin"` LifetimeSubsidyQuotaCoin int64 `json:"lifetimeSubsidyQuotaCoin"` Day1SubsidyQuotaCoin int64 `json:"day1SubsidyQuotaCoin"` SingleRoundSubsidyCapCoin int64 `json:"singleRoundSubsidyCapCoin"` StrategyLevel string `json:"strategyLevel"` LoseStreakProtectionEnabled bool `json:"loseStreakProtectionEnabled"` LoseStreakTrigger int32 `json:"loseStreakTrigger"` NormalPhaseTargetWinRatePercent int32 `json:"normalPhaseTargetWinRatePercent"` } type diceGenerateRobotsRequest struct { Count int32 `json:"count"` NicknamePrefix string `json:"nicknamePrefix"` NicknameLanguage string `json:"nicknameLanguage"` AvatarURLs []string `json:"avatarUrls"` Country string `json:"country"` Gender string `json:"gender"` } type diceRobotStatusRequest struct { Status string `json:"status"` } type roomRPSConfigRequest struct { Status string `json:"status"` ChallengeTimeoutMS int64 `json:"challengeTimeoutMs"` RevealCountdownMS int64 `json:"revealCountdownMs"` StakeGifts []roomRPSStakeGiftReq `json:"stakeGifts"` } type roomRPSStakeGiftReq struct { GiftID string `json:"giftId"` Enabled bool `json:"enabled"` SortOrder int32 `json:"sortOrder"` } func (r platformRequest) toProto(platformCode string) *gamev1.GamePlatform { if platformCode == "" { platformCode = r.PlatformCode } return &gamev1.GamePlatform{ PlatformCode: strings.TrimSpace(platformCode), PlatformName: strings.TrimSpace(r.PlatformName), Status: strings.TrimSpace(r.Status), ApiBaseUrl: strings.TrimSpace(r.APIBaseURL), AdapterType: strings.TrimSpace(r.AdapterType), CallbackSecret: strings.TrimSpace(r.CallbackSecret), CallbackIpWhitelist: compactTags(r.CallbackIPWhitelist), AdapterConfigJson: strings.TrimSpace(r.AdapterConfigJSON), SortOrder: r.SortOrder, } } func (r catalogRequest) toProto(gameID string) *gamev1.GameCatalogItem { if gameID == "" { gameID = r.GameID } return &gamev1.GameCatalogItem{ GameId: strings.TrimSpace(gameID), PlatformCode: strings.TrimSpace(r.PlatformCode), ProviderGameId: strings.TrimSpace(r.ProviderGameID), GameName: strings.TrimSpace(r.GameName), Category: strings.TrimSpace(r.Category), IconUrl: strings.TrimSpace(r.IconURL), CoverUrl: strings.TrimSpace(r.CoverURL), LaunchMode: strings.TrimSpace(r.LaunchMode), Orientation: strings.TrimSpace(r.Orientation), SafeHeight: r.SafeHeight, MinCoin: r.MinCoin, Status: strings.TrimSpace(r.Status), SortOrder: r.SortOrder, Tags: compactTags(r.Tags), } } func (r diceConfigRequest) toProto(gameID string) *gamev1.DiceConfig { options := make([]*gamev1.DiceStakeOption, 0, len(r.StakeOptions)) for _, option := range r.StakeOptions { options = append(options, &gamev1.DiceStakeOption{ StakeCoin: option.StakeCoin, Enabled: option.Enabled, SortOrder: option.SortOrder, }) } return &gamev1.DiceConfig{ GameId: strings.TrimSpace(gameID), Status: strings.TrimSpace(r.Status), StakeOptions: options, FeeBps: r.FeeBPS, PoolBps: r.PoolBPS, MinPlayers: r.MinPlayers, MaxPlayers: r.MaxPlayers, RobotEnabled: r.RobotEnabled, RobotMatchWaitMs: r.RobotMatchWaitMS, } } func (r selfGameNewUserPolicyRequest) toProto(gameID string) *gamev1.SelfGameNewUserPolicy { return &gamev1.SelfGameNewUserPolicy{ GameId: strings.TrimSpace(gameID), Enabled: r.Enabled, ProtectionRounds: r.ProtectionRounds, ProtectionHours: r.ProtectionHours, MaxStakeCoin: r.MaxStakeCoin, LifetimeSubsidyQuotaCoin: r.LifetimeSubsidyQuotaCoin, Day1SubsidyQuotaCoin: r.Day1SubsidyQuotaCoin, SingleRoundSubsidyCapCoin: r.SingleRoundSubsidyCapCoin, StrategyLevel: strings.TrimSpace(r.StrategyLevel), LoseStreakProtectionEnabled: r.LoseStreakProtectionEnabled, LoseStreakTrigger: r.LoseStreakTrigger, NormalPhaseTargetWinRatePercent: r.NormalPhaseTargetWinRatePercent, } } func (r selfGameStakePoolRequest) toProto(gameID string, stakeCoin int64) *gamev1.SelfGameStakePool { return &gamev1.SelfGameStakePool{ GameId: strings.TrimSpace(gameID), StakeCoin: stakeCoin, TargetBalanceCoin: r.TargetBalanceCoin, SafeBalanceCoin: r.SafeBalanceCoin, WarningBalanceCoin: r.WarningBalanceCoin, DangerBalanceCoin: r.DangerBalanceCoin, Status: strings.TrimSpace(r.Status), } } func (r roomRPSConfigRequest) toProto() (*gamev1.RoomRPSConfig, error) { status := strings.TrimSpace(r.Status) if status == "" { // 业务功能配置默认打开;后台没传 status 时不能默认为关闭,否则新环境上线后 App 入口会被静默隐藏。 status = "active" } timeoutMS := r.ChallengeTimeoutMS if timeoutMS <= 0 { // 无人应战超时使用产品默认 10 分钟;0 或负数按“未填写”处理,不允许保存成立刻过期。 timeoutMS = 10 * 60 * 1000 } revealMS := r.RevealCountdownMS if revealMS <= 0 { // 揭晓倒计时使用产品默认 3 秒;客户端动画和 IM 倒计时都依赖这个稳定默认值。 revealMS = 3 * 1000 } if len(r.StakeGifts) != 4 { // 房内猜拳礼物档位是产品固定四档,少一档会导致 Flutter 面板缺位,多一档会导致后台和客户端展示不一致。 return nil, fmt.Errorf("房内猜拳礼物档位必须配置 4 档") } gifts := make([]*gamev1.RoomRPSStakeGift, 0, len(r.StakeGifts)) seen := make(map[string]struct{}, len(r.StakeGifts)) for index, gift := range r.StakeGifts { giftID := strings.TrimSpace(gift.GiftID) if giftID == "" { // gift_id 是后台礼物表的真实主键,当前礼物体系允许字符串;空值不能表示“空档位”,空档位应该通过 enabled=false 表达。 return nil, fmt.Errorf("第 %d 档礼物 ID 不正确", index+1) } if _, ok := seen[giftID]; ok { // 同一个礼物不能重复配置成多档,否则结算金额和排序展示会变成同一个 gift_id 的多份事实。 return nil, fmt.Errorf("第 %d 档礼物 ID 重复", index+1) } seen[giftID] = struct{}{} sortOrder := gift.SortOrder if sortOrder <= 0 { // 未显式给排序时按提交顺序落库,保证前端拖拽顺序和运行时展示顺序一致。 sortOrder = int32(index + 1) } gifts = append(gifts, &gamev1.RoomRPSStakeGift{ GiftId: legacyRoomRPSGiftID(giftID), GiftIdText: giftID, Enabled: gift.Enabled, SortOrder: sortOrder, }) } return &gamev1.RoomRPSConfig{ // 房内猜拳和独立猜拳必须用不同 game_id;这里固定 room_rps,防止后台误把配置写到独立猜拳。 GameId: "room_rps", Status: status, ChallengeTimeoutMs: timeoutMS, RevealCountdownMs: revealMS, StakeGifts: gifts, }, nil } func legacyRoomRPSGiftID(giftID string) int64 { value, err := strconv.ParseInt(strings.TrimSpace(giftID), 10, 64) if err != nil || value <= 0 { return 0 } return value } func compactTags(tags []string) []string { out := make([]string, 0, len(tags)) seen := make(map[string]struct{}, len(tags)) for _, tag := range tags { value := strings.TrimSpace(tag) if value == "" { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} out = append(out, value) } return out } func requestMeta(c *gin.Context) *gamev1.RequestMeta { return &gamev1.RequestMeta{ RequestId: middleware.CurrentRequestID(c), Caller: "admin-server", ActorUserId: int64(middleware.CurrentUserID(c)), SentAtMs: time.Now().UnixMilli(), AppCode: appctx.FromContext(c.Request.Context()), } }