hyapp-server/docs/game-service游戏结构设计.md
2026-06-11 01:02:16 +08:00

15 KiB
Raw Permalink Blame History

game-service 游戏结构设计

本文定义 game-service 后续承载游戏的结构边界。当前仓库已经有第三方 H5 游戏接入能力:游戏列表、启动会话、厂商回调、钱包改账、统计 outbox 和等级事件 outbox 都在 game-service 内。本文补的是自研小游戏的结构,避免把骰子、猜拳这类玩法塞进第三方平台 adapter。

结论

  • game-service 是游戏事实 owner。游戏列表、启动会话、匹配、局内状态、结算、补偿和游戏事件都归它管理。
  • gateway-service 只做 App HTTP 入口、鉴权、参数转换和 gRPC 转发,不保存游戏状态。
  • wallet-service 仍然是金币 owner。game-service 只能通过钱包 gRPC 扣款、派奖、退款,不能直接改余额。
  • room-service 不拥有游戏状态。房间只提供入口、房间上下文和必要的展示事件。
  • 第三方 H5 游戏继续走现有 platform/catalog/session/callback/order 结构。
  • 自研小游戏单独按 game_code 建领域模块、服务模块和表,不提前抽一个复杂通用引擎。
  • 只有当两个以上自研游戏出现相同匹配、计时、结算、IM 投递逻辑时,再把公共能力抽到 internal/domain/minigameinternal/service/minigame

当前事实

现有入口:

  • App 游戏列表:GET /api/v1/games
  • 最近游戏:GET /api/v1/games/recent
  • 桥接脚本:GET /api/v1/games/bridge-script
  • 启动游戏:POST /api/v1/games/{game_id}/launch
  • 厂商回调:POST /api/v1/game-callbacks/{platform_code}/{operation}

现有内部 RPC

  • GameAppService.ListGames
  • GameAppService.ListRecentGames
  • GameAppService.GetBridgeScript
  • GameAppService.LaunchGame
  • GameCallbackService.HandleCallback
  • GameAdminService 管理平台和目录
  • GameCronService.ProcessLevelEventOutboxBatch

现有核心表:

  • game_platforms:第三方平台配置
  • game_catalogApp 看到的游戏目录
  • game_display_rules:按场景、区域、语言和客户端平台控制展示
  • game_launch_sessionsH5 启动会话
  • game_orders:第三方或游戏结算引起的钱包订单
  • game_callback_logs:第三方回调审计
  • game_level_event_outbox:游戏消耗投递 activity-service
  • game_outbox:游戏订单事实投递 RocketMQ给 statistics-service 等下游消费

分层结构

gateway-service
  internal/transport/http/gameapi
    只接 App HTTP拿 user_id、request_id、app_code 后转 game-service gRPC

api/proto/game/v1
  game.proto
    所有跨服务契约,新增 App 接口或后台接口先改这里

services/game-service
  internal/domain/game
    已有通用游戏目录、平台、会话、订单、回调、outbox 领域对象

  internal/service/game
    已有第三方 H5 平台接入和游戏列表、启动、回调主流程

  internal/domain/<game_code>
    自研游戏纯规则不能依赖数据库、gRPC、HTTP、钱包 client

  internal/service/<game_code>
    自研游戏用例层,负责匹配、状态推进、调用钱包、写 outbox

  internal/storage/mysql/<game_code>_repository.go
    自研游戏持久化,行锁、幂等、状态转移都在这里收口

  internal/transport/grpc
    把 game.proto 的 RPC 转成 service 调用,不写业务规则

目录原则:

  • internal/service/game 保留给已有平台接入,不继续堆自研玩法。
  • 每个自研玩法用独立包,例如 internal/domain/diceinternal/service/dice
  • 自研玩法可以复用 game_cataloggame_display_rules 做列表展示。
  • 自研玩法不走第三方 PlatformAdapter,也不走 HandleCallback
  • 自研玩法的 HTTP 入口仍在 gateway跨服务契约仍在 api/proto/game/v1/game.proto

游戏类型

类型 例子 列表和启动 状态 owner 结算方式
第三方 H5 百顺、VivaGames、Reyou 现有 /games/launch 第三方平台game-service 保存会话和订单 厂商回调到 game-service再调 wallet-service
自研 H5 多人结算 骰子首版 复用 /games/launch,局内操作走新增 HTTP game-service game-service 直接调 wallet-service
自研实时对战 猜拳、后续双人骰子 复用 /games/launch,匹配和出招走新增 HTTP game-service game-service 调 wallet-serviceIM 只下发事件

自研游戏统一链路

sequenceDiagram
  participant App
  participant Gateway as gateway-service
  participant Game as game-service
  participant Wallet as wallet-service
  participant Notice as notice-service/Tencent IM
  participant DB as hyapp_game

  App->>Gateway: POST /api/v1/games/{game_id}/launch
  Gateway->>Game: LaunchGame
  Game->>DB: create game_launch_sessions
  Game-->>Gateway: launch_url/session_id
  Gateway-->>App: H5 url

  App->>Gateway: POST /api/v1/games/dice/matches
  Gateway->>Game: Create self-game match
  Game->>DB: create match + first participant
  App->>Gateway: POST /api/v1/games/dice/matches/{match_id}/join
  Gateway->>Game: JoinDiceMatch
  Game->>DB: lock match, insert participant
  App->>Gateway: POST /api/v1/games/dice/matches/{match_id}/roll
  Gateway->>Game: RollDiceMatch
  Game->>DB: claim match for settling
  Game->>Wallet: ApplyGameCoinChange debit/credit/refund
  Wallet-->>Game: wallet_transaction_id/balance_after
  Game->>DB: save dice points, orders, result and outbox
  Game-->>Gateway: match/result state
  Gateway-->>App: business response
  Game-->>Notice: optional realtime outbox
  Notice-->>App: optional IM custom event

关键约束:

  • 用户身份只信任 gateway 从 JWT 得到的 user_id
  • request_id 只做链路追踪,不做业务幂等键。
  • 业务幂等键使用 command_idmatch_idround_idprovider_order_id 或唯一索引。
  • 所有时间字段使用 UTC epoch milliseconds字段名统一 *_ms
  • 金币金额必须是正数,扣款、派奖、退款用 op_type 表达方向。
  • 同一局的投注、派奖、退款必须能通过 match_id/round_id/order_id 追踪。

自研游戏表结构原则

不要用一个大表兼容所有玩法。每个玩法先建自己的事实表,公共 outbox 可以复用。

建议公共表:

game_realtime_outbox

用于自研游戏的 IM 实时事件。不要复用 game_outbox,因为 game_outbox 是订单和统计事实,字段强依赖 order_id/op_type/coin_amount

关键字段:

  • app_code
  • event_id
  • event_type
  • game_code
  • match_id
  • target_user_id
  • payload_json
  • status
  • worker_id
  • lock_until_ms
  • retry_count
  • next_retry_at_ms
  • last_error
  • created_at_ms
  • updated_at_ms

推荐索引:

  • PRIMARY KEY(app_code, event_id)
  • idx_game_realtime_claim(app_code, status, next_retry_at_ms, created_at_ms, event_id)
  • idx_game_realtime_target(app_code, target_user_id, created_at_ms)

Redis 和 MySQL 边界

局信息可以同时出现在 MySQL 和 Redis但不能是“双重事实”。

  • MySQL 是唯一事实。参与人、扣款、派奖、结果、状态流转和补偿都必须能只靠 MySQL 恢复。
  • Redis 只保存热状态。可以存等待队列、房间当前局快照、倒计时、座位临时锁和防重复点击标记。
  • Redis 数据必须可丢。Redis 重启、Key 过期或缓存击穿时,服务必须从 MySQL 查回当前局。
  • 金币相关状态只以 MySQL 和 wallet-service 返回为准,不以 Redis 为准。
  • 关键状态推进用 MySQL 事务、唯一索引和行锁收口Redis 锁只能减少并发压力,不能作为最终防线。

推荐 Key

  • game:dice:room:{app_code}:{room_id}:active_match房间当前局快照TTL 按局时长加缓冲。
  • game:dice:match:{app_code}:{match_id}局详情快照TTL 按局时长加缓冲。
  • game:dice:join_lock:{app_code}:{match_id}:短 TTL 加入锁,用来减少并发抢座。
  • game:dice:action:{app_code}:{match_id}:{user_id}:{action}:短 TTL 动作防抖,最终幂等仍看 MySQL。

写入顺序:

  1. 先在 MySQL 事务里锁定 game_dice_matches
  2. 创建/加入只写 game_dice_participants 和局状态。
  3. 开奖阶段用钱包幂等键写 game_orders,再保存点数、派奖和局结果。
  4. MySQL 事实提交后刷新 Redis 快照。
  5. Redis 刷新失败只记录日志和重试,不回滚已提交的 MySQL 事实。

骰子游戏首版结构

首版按多人可扩展结构设计。当前实现要求至少 2 人,默认最多 2 人,硬上限 6 人3 人局只增加参与者行不改表。H5 只负责展示和发起操作,随机、扣款、派奖和结果都由 game-service 完成。

当前骰子规则是单局胜场制。每个参与者每轮只扔 1 颗骰子,服务端把这个点数写入 dice_points[0]点数高的一方获胜。同点不是终局不退本金、不派奖H5 展示点数 2 秒后继续调用 roll,同一个 match_id 会一直重投到分出胜负。

模块

services/game-service/internal/domain/dice
  rule.go       赔率、可选押注、结果计算
  match.go      match 状态、参与者、局次、结果

services/game-service/internal/service/dice
  service.go    创建局、加入局、扣款、开奖、派奖、查询

services/game-service/internal/storage/mysql/dice_repository.go
  创建局、参与人行锁读取、状态推进、订单关联、查询

状态机

stateDiagram-v2
  [*] --> created
  created --> joining
  joining --> settling
  created --> settling
  settling --> payout_applying
  payout_applying --> settled
  settling --> failed
  payout_applying --> failed

状态说明:

  • created:收到创建请求,已生成 match_id
  • joining:等待参与者加入,人数达到 min_players 后可以开奖。
  • settling:已抢占结算权,正在扣押注、生成骰子结果。
  • payout_applying:骰子点数已经固定,正在派奖或退款;重试必须复用旧点数。
  • settled:局已结束,结果固定。
  • failed:扣款、派奖或退款失败,后续由补偿任务继续处理。

game_dice_matches

关键字段:

  • app_code
  • match_id
  • game_id
  • platform_code
  • provider_game_id
  • room_id
  • region_id
  • min_players
  • max_players
  • current_players
  • stake_coin
  • round_no
  • result
  • status
  • join_deadline_ms
  • created_at_ms
  • updated_at_ms
  • settled_at_ms

索引:

  • PRIMARY KEY(app_code, match_id)
  • idx_game_dice_room_active(app_code, room_id, status, created_at_ms)
  • idx_game_dice_status_time(app_code, status, updated_at_ms, match_id)

game_dice_participants

一局一个参与者一行。2 人、3 人或 6 人只改变参与者数量,不改主表结构。

关键字段:

  • app_code
  • match_id
  • user_id
  • seat_no
  • status
  • stake_coin
  • dice_points_json
  • result
  • payout_coin
  • debit_order_id
  • payout_order_id
  • refund_order_id
  • balance_after
  • joined_at_ms
  • updated_at_ms

索引:

  • PRIMARY KEY(app_code, match_id, user_id)
  • UNIQUE KEY uk_game_dice_seat(app_code, match_id, seat_no)
  • idx_game_dice_participant_user(app_code, user_id, updated_at_ms)
  • idx_game_dice_participant_status(app_code, match_id, status, seat_no)

game_dice_round_logs

如果首版只有一局,可以先不建。若后续做多轮、连胜、多人奖池或房间内广播,再补小局日志表。

关键字段:

  • app_code
  • match_id
  • round_no
  • result_json
  • created_at_ms

骰子接口草案

接口文档只保留地址、参数、返回值和相关 IM。

1. 创建骰子局

地址:POST /api/v1/games/dice/matches

参数:

  • game_id
  • room_id
  • stake_coin
  • min_players
  • max_players

返回值:

  • match_id
  • status
  • min_players
  • max_players
  • current_players
  • stake_coin
  • participants
  • server_time_ms

相关 IM

  • 首版无

2. 加入骰子局

地址:POST /api/v1/games/dice/matches/{match_id}/join

参数:

  • match_id

返回值:

  • match_id
  • status
  • min_players
  • max_players
  • current_players
  • stake_coin
  • participants
  • server_time_ms

相关 IM

  • 首版无

3. 查询骰子局

地址:GET /api/v1/games/dice/matches/{match_id}

参数:

  • match_id

返回值:

  • match_id
  • status
  • min_players
  • max_players
  • current_players
  • stake_coin
  • result
  • participants
  • server_time_ms

相关 IM

  • 首版无

4. 开奖

地址:POST /api/v1/games/dice/matches/{match_id}/roll

参数:

  • match_id

返回值:

  • match_id
  • status
  • stake_coin
  • result
  • participants
  • server_time_ms

相关 IM

  • 首版无
  • 如果后续做房间公屏展示,再增加 dice_match_settled

钱包和订单

自研游戏也要落 game_orders,不能直接只写玩法表。

骰子推荐订单:

  • 押注扣款:op_type=debitprovider_order_id=dice:{match_id}:{user_id}:debit
  • 派奖加款:op_type=creditprovider_order_id=dice:{match_id}:{user_id}:credit
  • 异常退款:op_type=refundprovider_order_id=dice:{match_id}:{user_id}:refund

wallet-serviceCommandId

  • game:dice:{match_id}:{user_id}:debit
  • game:dice:{match_id}:{user_id}:credit
  • game:dice:{match_id}:{user_id}:refund

扣款成功后:

  • game_orders
  • game_outbox,供统计使用
  • game_level_event_outbox,供 activity-service 游戏等级使用

派奖和退款是否进等级轨道由产品规则决定,默认只有 debit 进游戏等级。

IM 事件边界

首版骰子不强依赖 IM。客户端发起 HTTPHTTP 返回结果。

需要 IM 的场景:

  • 双人匹配成功
  • 倒计时开始
  • 双方状态变化
  • 结果揭晓
  • 对方离开或超时

IM 规则:

  • 客户端不能通过 IM 提交游戏动作。
  • 客户端动作必须走 HTTP 到 gateway再转 game-service。
  • IM 只承载服务端事实事件。
  • IM payload 必须带 event_idgame_codematch_idserver_time_ms
  • 客户端按 event_id 去重。

上线顺序

  1. 先补 api/proto/game/v1/game.proto 的骰子 RPC。
  2. 生成 protomake proto
  3. gateway-service/internal/transport/http/gameapi 加 HTTP handler。
  4. game-servicedomain/diceservice/dicestorage/mysql/dice_repository.go
  5. 更新 services/game-service/deploy/mysql/initdb/001_game_service.sql
  6. 更新 runtime Migrate,保证线上库自动补表和索引。
  7. 后台把骰子写入 game_catalog 和默认 game_display_rules
  8. H5 game/touzi 通过启动页拿 game_id/session_id,局内操作走新增骰子接口。
  9. 跑测试:make protogo test ./services/game-service/... ./services/gateway-service/...
  10. 本地拉起:docker compose config,必要时 docker compose up -d game-service gateway-service 验证。

不做的事

  • 不把骰子局状态放进 room-service
  • 不让 H5 直接调 wallet-service
  • 不把骰子动作放进第三方 HandleCallback
  • 不把所有玩法硬塞进一个通用 game_matches 大表。
  • 不用 Redis 做唯一事实。Redis 后续只能做短期匹配队列、限流或缓存MySQL 仍是恢复来源。