package gameapi import ( "fmt" gamev1 "hyapp.local/api/proto/game/v1" ) type gameListData struct { Games []gameItemData `json:"games"` ServerTimeMS int64 `json:"server_time_ms"` } type gameItemData struct { GameID string `json:"game_id"` PlatformCode string `json:"platform_code"` NameKey string `json:"name_key"` Name string `json:"name"` IconURL string `json:"icon_url"` CoverURL string `json:"cover_url"` Category string `json:"category"` LaunchMode string `json:"launch_mode"` Orientation string `json:"orientation"` SafeHeight int32 `json:"safe_height"` SafeHeightV2 int32 `json:"safeHeight"` MinCoin int64 `json:"min_coin"` Enabled bool `json:"enabled"` Maintenance bool `json:"maintenance"` SortOrder int32 `json:"sort_order"` } type exploreWinnerListData struct { Items []exploreWinnerItemData `json:"items"` Total int `json:"total"` ServerTimeMS int64 `json:"server_time_ms"` } type exploreWinnerItemData struct { WinID string `json:"win_id"` GameCode string `json:"game_code"` GameID string `json:"game_id"` UserID string `json:"user_id"` UserIDNumber int64 `json:"user_id_number"` DisplayName string `json:"display_name"` AvatarURL string `json:"avatar_url"` CoinAmount int64 `json:"coin_amount"` WonAtMS int64 `json:"won_at_ms"` } type gameLaunchData struct { SessionID string `json:"session_id"` LaunchURL string `json:"launch_url"` ExpiresAtMS int64 `json:"expires_at_ms"` Orientation string `json:"orientation"` SafeHeight int32 `json:"safe_height"` SafeHeightV2 int32 `json:"safeHeight"` ServerTimeMS int64 `json:"server_time_ms"` } type gameBridgeScriptData struct { BridgeScriptURL string `json:"bridge_script_url"` BridgeScriptURLV2 string `json:"bridgeScriptUrl"` BridgeScriptVersion string `json:"bridge_script_version"` BridgeScriptVersionV2 string `json:"bridgeScriptVersion"` BridgeScriptSHA256 string `json:"bridge_script_sha256"` BridgeScriptSHA256V2 string `json:"bridgeScriptSha256"` ServerTimeMS int64 `json:"server_time_ms"` } type diceMatchData struct { Match diceMatchItemData `json:"match"` ServerTimeMS int64 `json:"server_time_ms"` } type diceConfigData struct { Config diceConfigItemData `json:"config"` ServerTimeMS int64 `json:"server_time_ms"` } type roomRPSConfigData struct { Config roomRPSConfigItemData `json:"config"` ServerTimeMS int64 `json:"server_time_ms"` } type roomRPSChallengeData struct { Challenge roomRPSChallengeItemData `json:"challenge"` ServerTimeMS int64 `json:"server_time_ms"` } type roomRPSChallengeListData struct { Challenges []*roomRPSChallengeItemData `json:"challenges"` NextCursor string `json:"next_cursor"` PageSize int32 `json:"page_size"` ServerTimeMS int64 `json:"server_time_ms"` } type diceConfigItemData struct { AppCode string `json:"app_code"` GameID string `json:"game_id"` Status string `json:"status"` StakeOptions []diceStakeOptionData `json:"stake_options"` FeeBPS int32 `json:"fee_bps"` PoolBPS int32 `json:"pool_bps"` MinPlayers int32 `json:"min_players"` MaxPlayers int32 `json:"max_players"` RobotEnabled bool `json:"robot_enabled"` RobotMatchWaitMS int64 `json:"robot_match_wait_ms"` PoolBalanceCoin int64 `json:"pool_balance_coin"` } type diceStakeOptionData struct { StakeCoin int64 `json:"stake_coin"` Enabled bool `json:"enabled"` SortOrder int32 `json:"sort_order"` } type roomRPSConfigItemData struct { AppCode string `json:"app_code"` GameID string `json:"game_id"` Status string `json:"status"` ChallengeTimeoutMS int64 `json:"challenge_timeout_ms"` RevealCountdownMS int64 `json:"reveal_countdown_ms"` StakeGifts []roomRPSStakeGiftData `json:"stake_gifts"` } type roomRPSStakeGiftData struct { GiftID string `json:"gift_id"` GiftIDNumber int64 `json:"gift_id_number"` GiftName string `json:"gift_name"` GiftIconURL string `json:"gift_icon_url"` GiftPriceCoin int64 `json:"gift_price_coin"` Enabled bool `json:"enabled"` SortOrder int32 `json:"sort_order"` } type diceMatchItemData struct { AppCode string `json:"app_code"` MatchID string `json:"match_id"` GameID string `json:"game_id"` RoomID string `json:"room_id"` RegionID int64 `json:"region_id"` MinPlayers int32 `json:"min_players"` MaxPlayers int32 `json:"max_players"` CurrentPlayers int32 `json:"current_players"` StakeCoin int64 `json:"stake_coin"` RoundNo int32 `json:"round_no"` Status string `json:"status"` Result string `json:"result"` Participants []diceParticipantItemData `json:"participants"` JoinDeadlineMS int64 `json:"join_deadline_ms"` ReadyAtMS int64 `json:"ready_at_ms"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` SettledAtMS int64 `json:"settled_at_ms"` CanceledAtMS int64 `json:"canceled_at_ms"` Phase string `json:"phase"` PhaseDeadlineMS int64 `json:"phase_deadline_ms"` FeeBPS int32 `json:"fee_bps"` PoolBPS int32 `json:"pool_bps"` MatchMode string `json:"match_mode"` ForcedResult string `json:"forced_result"` PoolDeltaCoin int64 `json:"pool_delta_coin"` } type diceParticipantItemData struct { UserID string `json:"user_id"` UserIDNumber int64 `json:"user_id_number"` SeatNo int32 `json:"seat_no"` Status string `json:"status"` StakeCoin int64 `json:"stake_coin"` DicePoints []int32 `json:"dice_points"` RPSGesture string `json:"rps_gesture"` Gesture string `json:"gesture"` Result string `json:"result"` PayoutCoin int64 `json:"payout_coin"` BalanceAfter int64 `json:"balance_after"` JoinedAtMS int64 `json:"joined_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` ParticipantType string `json:"participant_type"` IsRobot bool `json:"is_robot"` User diceUserData `json:"user"` AvatarFrame map[string]any `json:"avatar_frame"` } type diceUserData struct { UserID string `json:"user_id"` DisplayUserID string `json:"display_user_id"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` } type roomRPSChallengeItemData struct { AppCode string `json:"app_code"` ChallengeID string `json:"challenge_id"` RoomID string `json:"room_id"` RegionID int64 `json:"region_id"` Status string `json:"status"` StakeGiftID string `json:"stake_gift_id"` StakeGiftIDNumber int64 `json:"stake_gift_id_number"` StakeCoin int64 `json:"stake_coin"` Initiator roomRPSPlayerItemData `json:"initiator"` Challenger roomRPSPlayerItemData `json:"challenger"` WinnerUserID string `json:"winner_user_id"` WinnerUserIDNumber int64 `json:"winner_user_id_number"` SettlementStatus string `json:"settlement_status"` FailureReason string `json:"failure_reason"` TimeoutAtMS int64 `json:"timeout_at_ms"` RevealAtMS int64 `json:"reveal_at_ms"` CreatedAtMS int64 `json:"created_at_ms"` MatchedAtMS int64 `json:"matched_at_ms"` SettledAtMS int64 `json:"settled_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type roomRPSPlayerItemData struct { UserID string `json:"user_id"` UserIDNumber int64 `json:"user_id_number"` Gesture string `json:"gesture"` Result string `json:"result"` BalanceAfter int64 `json:"balance_after"` JoinedAtMS int64 `json:"joined_at_ms"` User diceUserData `json:"user"` } func gameListDataFromProto(resp *gamev1.ListGamesResponse) gameListData { if resp == nil { return gameListData{} } items := make([]gameItemData, 0, len(resp.GetGames())) for _, item := range resp.GetGames() { items = append(items, gameItemData{ GameID: item.GetGameId(), PlatformCode: item.GetPlatformCode(), NameKey: item.GetNameKey(), Name: item.GetName(), IconURL: item.GetIconUrl(), CoverURL: item.GetCoverUrl(), Category: item.GetCategory(), LaunchMode: item.GetLaunchMode(), Orientation: item.GetOrientation(), SafeHeight: item.GetSafeHeight(), SafeHeightV2: item.GetSafeHeight(), MinCoin: item.GetMinCoin(), Enabled: item.GetEnabled(), Maintenance: item.GetMaintenance(), SortOrder: item.GetSortOrder(), }) } return gameListData{Games: items, ServerTimeMS: resp.GetServerTimeMs()} } func exploreWinnerListDataFromProto(resp *gamev1.ListExploreWinnersResponse) exploreWinnerListData { if resp == nil { return exploreWinnerListData{} } items := make([]exploreWinnerItemData, 0, len(resp.GetWinners())) for _, item := range resp.GetWinners() { items = append(items, exploreWinnerItemData{ WinID: item.GetWinId(), GameCode: item.GetGameCode(), GameID: item.GetGameId(), UserID: int64String(item.GetUserId()), UserIDNumber: item.GetUserId(), DisplayName: item.GetDisplayName(), AvatarURL: item.GetAvatarUrl(), CoinAmount: item.GetCoinAmount(), WonAtMS: item.GetWonAtMs(), }) } return exploreWinnerListData{Items: items, Total: len(items), ServerTimeMS: resp.GetServerTimeMs()} } func gameLaunchDataFromProto(resp *gamev1.LaunchGameResponse) gameLaunchData { if resp == nil { return gameLaunchData{} } return gameLaunchData{ SessionID: resp.GetSessionId(), LaunchURL: resp.GetLaunchUrl(), ExpiresAtMS: resp.GetExpiresAtMs(), Orientation: resp.GetOrientation(), SafeHeight: resp.GetSafeHeight(), SafeHeightV2: resp.GetSafeHeight(), ServerTimeMS: resp.GetServerTimeMs(), } } func diceConfigDataFromProto(resp *gamev1.DiceConfigResponse) diceConfigData { if resp == nil { return diceConfigData{} } config := resp.GetConfig() options := make([]diceStakeOptionData, 0, len(config.GetStakeOptions())) for _, option := range config.GetStakeOptions() { options = append(options, diceStakeOptionData{ StakeCoin: option.GetStakeCoin(), Enabled: option.GetEnabled(), SortOrder: option.GetSortOrder(), }) } return diceConfigData{ Config: diceConfigItemData{ AppCode: config.GetAppCode(), GameID: config.GetGameId(), Status: config.GetStatus(), StakeOptions: options, FeeBPS: config.GetFeeBps(), PoolBPS: config.GetPoolBps(), MinPlayers: config.GetMinPlayers(), MaxPlayers: config.GetMaxPlayers(), RobotEnabled: config.GetRobotEnabled(), RobotMatchWaitMS: config.GetRobotMatchWaitMs(), PoolBalanceCoin: config.GetPoolBalanceCoin(), }, ServerTimeMS: resp.GetServerTimeMs(), } } func roomRPSConfigDataFromProto(resp *gamev1.RoomRPSConfigResponse) roomRPSConfigData { if resp == nil { return roomRPSConfigData{} } // 配置响应保持一层 config 包装,方便 Flutter 在同一个模型里同时读取配置和 server_time_ms 做倒计时校准。 return roomRPSConfigData{Config: roomRPSConfigItemDataFromProto(resp.GetConfig()), ServerTimeMS: resp.GetServerTimeMs()} } func roomRPSConfigItemDataFromProto(config *gamev1.RoomRPSConfig) roomRPSConfigItemData { if config == nil { return roomRPSConfigItemData{} } gifts := make([]roomRPSStakeGiftData, 0, len(config.GetStakeGifts())) for _, gift := range config.GetStakeGifts() { // 礼物 ID 同时返回字符串和数字;Flutter 展示、缓存和路由参数优先用字符串,提交 body 时仍可用数字字段。 gifts = append(gifts, roomRPSStakeGiftData{ GiftID: int64String(gift.GetGiftId()), GiftIDNumber: gift.GetGiftId(), GiftName: gift.GetGiftName(), GiftIconURL: gift.GetGiftIconUrl(), GiftPriceCoin: gift.GetGiftPriceCoin(), Enabled: gift.GetEnabled(), SortOrder: gift.GetSortOrder(), }) } return roomRPSConfigItemData{ AppCode: config.GetAppCode(), GameID: config.GetGameId(), Status: config.GetStatus(), ChallengeTimeoutMS: config.GetChallengeTimeoutMs(), RevealCountdownMS: config.GetRevealCountdownMs(), StakeGifts: gifts, } } func diceMatchDataFromProto(resp *gamev1.DiceMatchResponse) diceMatchData { if resp == nil { return diceMatchData{} } return diceMatchData{ Match: diceMatchItemDataFromProto(resp.GetMatch()), ServerTimeMS: resp.GetServerTimeMs(), } } func diceMatchItemDataFromProto(match *gamev1.DiceMatch) diceMatchItemData { if match == nil { return diceMatchItemData{} } participants := make([]diceParticipantItemData, 0, len(match.GetParticipants())) for _, participant := range match.GetParticipants() { participants = append(participants, diceParticipantItemDataFromProto(participant)) } return diceMatchItemData{ AppCode: match.GetAppCode(), MatchID: match.GetMatchId(), GameID: match.GetGameId(), RoomID: match.GetRoomId(), RegionID: match.GetRegionId(), MinPlayers: match.GetMinPlayers(), MaxPlayers: match.GetMaxPlayers(), CurrentPlayers: match.GetCurrentPlayers(), StakeCoin: match.GetStakeCoin(), RoundNo: match.GetRoundNo(), Status: match.GetStatus(), Result: match.GetResult(), Participants: participants, JoinDeadlineMS: match.GetJoinDeadlineMs(), ReadyAtMS: match.GetReadyAtMs(), CreatedAtMS: match.GetCreatedAtMs(), UpdatedAtMS: match.GetUpdatedAtMs(), SettledAtMS: match.GetSettledAtMs(), CanceledAtMS: match.GetCanceledAtMs(), Phase: match.GetPhase(), PhaseDeadlineMS: match.GetPhaseDeadlineMs(), FeeBPS: match.GetFeeBps(), PoolBPS: match.GetPoolBps(), MatchMode: match.GetMatchMode(), ForcedResult: match.GetForcedResult(), PoolDeltaCoin: match.GetPoolDeltaCoin(), } } func diceParticipantItemDataFromProto(participant *gamev1.DiceParticipant) diceParticipantItemData { if participant == nil { return diceParticipantItemData{} } return diceParticipantItemData{ UserID: int64String(participant.GetUserId()), UserIDNumber: participant.GetUserId(), SeatNo: participant.GetSeatNo(), Status: participant.GetStatus(), StakeCoin: participant.GetStakeCoin(), DicePoints: participant.GetDicePoints(), RPSGesture: participant.GetRpsGesture(), Gesture: participant.GetRpsGesture(), Result: participant.GetResult(), PayoutCoin: participant.GetPayoutCoin(), BalanceAfter: participant.GetBalanceAfter(), JoinedAtMS: participant.GetJoinedAtMs(), UpdatedAtMS: participant.GetUpdatedAtMs(), ParticipantType: participant.GetParticipantType(), IsRobot: participant.GetIsRobot(), AvatarFrame: map[string]any{}, } } func gameBridgeScriptDataFromProto(resp *gamev1.GetBridgeScriptResponse) gameBridgeScriptData { if resp == nil { return gameBridgeScriptData{} } config := resp.GetConfig() return gameBridgeScriptData{ BridgeScriptURL: config.GetBridgeScriptUrl(), BridgeScriptURLV2: config.GetBridgeScriptUrl(), BridgeScriptVersion: config.GetBridgeScriptVersion(), BridgeScriptVersionV2: config.GetBridgeScriptVersion(), BridgeScriptSHA256: config.GetBridgeScriptSha256(), BridgeScriptSHA256V2: config.GetBridgeScriptSha256(), ServerTimeMS: resp.GetServerTimeMs(), } } func int64String(value int64) string { return fmt.Sprintf("%d", value) } func roomRPSChallengeDataFromProto(resp *gamev1.RoomRPSChallengeResponse) roomRPSChallengeData { if resp == nil { return roomRPSChallengeData{} } // 单挑战响应和列表响应使用同一个 challenge 结构,避免 Flutter 为发起、应战、详情维护多套字段映射。 return roomRPSChallengeData{Challenge: roomRPSChallengeItemDataFromProto(resp.GetChallenge()), ServerTimeMS: resp.GetServerTimeMs()} } func roomRPSChallengeListDataFromProto(resp *gamev1.ListRoomRPSChallengesResponse) roomRPSChallengeListData { if resp == nil { return roomRPSChallengeListData{} } items := make([]*roomRPSChallengeItemData, 0, len(resp.GetChallenges())) for _, challenge := range resp.GetChallenges() { // 使用指针切片是为了后续展示资料补全可以原地写入 initiator/challenger.user,不再复制整份挑战单。 item := roomRPSChallengeItemDataFromProto(challenge) items = append(items, &item) } return roomRPSChallengeListData{ Challenges: items, NextCursor: resp.GetNextCursor(), PageSize: resp.GetPageSize(), ServerTimeMS: resp.GetServerTimeMs(), } } func roomRPSChallengeItemDataFromProto(challenge *gamev1.RoomRPSChallenge) roomRPSChallengeItemData { if challenge == nil { return roomRPSChallengeItemData{} } // game-service 返回的是订单事实,gateway 这里只做 JSON 形态转换;状态、胜负、结算失败原因不能在这里二次推导。 // 所有可能超过 JS 安全整数范围的 ID 都保留 string 和 number 双字段,Flutter 优先使用 string,后台排查仍可读 number。 return roomRPSChallengeItemData{ AppCode: challenge.GetAppCode(), ChallengeID: challenge.GetChallengeId(), RoomID: challenge.GetRoomId(), RegionID: challenge.GetRegionId(), Status: challenge.GetStatus(), StakeGiftID: int64String(challenge.GetStakeGiftId()), StakeGiftIDNumber: challenge.GetStakeGiftId(), StakeCoin: challenge.GetStakeCoin(), Initiator: roomRPSPlayerItemDataFromProto(challenge.GetInitiator()), Challenger: roomRPSPlayerItemDataFromProto(challenge.GetChallenger()), WinnerUserID: int64String(challenge.GetWinnerUserId()), WinnerUserIDNumber: challenge.GetWinnerUserId(), SettlementStatus: challenge.GetSettlementStatus(), FailureReason: challenge.GetFailureReason(), TimeoutAtMS: challenge.GetTimeoutAtMs(), RevealAtMS: challenge.GetRevealAtMs(), CreatedAtMS: challenge.GetCreatedAtMs(), MatchedAtMS: challenge.GetMatchedAtMs(), SettledAtMS: challenge.GetSettledAtMs(), UpdatedAtMS: challenge.GetUpdatedAtMs(), } } func roomRPSPlayerItemDataFromProto(player *gamev1.RoomRPSPlayer) roomRPSPlayerItemData { if player == nil { return roomRPSPlayerItemData{} } // user 先放 user_id 兜底;真正的 nickname/avatar 在 enrichRoomRPSChallenges 里批量补齐,资料失败不影响订单返回。 return roomRPSPlayerItemData{ UserID: int64String(player.GetUserId()), UserIDNumber: player.GetUserId(), Gesture: player.GetGesture(), Result: player.GetResult(), BalanceAfter: player.GetBalanceAfter(), JoinedAtMS: player.GetJoinedAtMs(), User: diceUserData{UserID: int64String(player.GetUserId())}, } }