Files
cursor/plans/wallet bet success MQ-8500b436.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

12 KiB
Raw Blame History


todos:

  • id: "define-constants" content: "在 slot-foundation 新增 WalletEventMQ 常量exchange + routingKey" status: pending
  • id: "add-publisher" content: "在 slot-wallet 新增 WalletBetSuccessMqPublisher封装 {type, uid, data, ts} 载荷组装" status: pending
  • id: "extend-wallet-dto" content: "slot-wallet / slot-pwa DTO 与 Validate 增加 provider_code、game_code 透传字段" status: pending
  • id: "hook-bet-success" content: "在 WalletBetWinLogic::bet() commit 后调用 publisher幂等命中跳过" status: pending
  • id: "pwa-pass-game-context" content: "slot-pwa CashLogic 调 wallet 时透传 provider_code如 pop与 game_code" status: pending
  • id: "consumer-bind" content: "各消费服务自建 queue 并 bind topic 模式wallet.bet.success / wallet.bet.#" status: pending isProject: false

wallet.bet.success 消息体设计方案

背景与定位

当前 slot-wallet 下注成功后已有两类 MQ但都不适合作为通用钱包事件总线

现有通道 载荷 局限
slot_console / console_bus { uid, type:'bet', data:{ source, amount } } 仅统计,缺 biz_id / round_id / 资金拆分
slot_activity / trial_reward_bus { type, uid, data, ts } 活动专用,且目前由 slot-pwa 在钱包 API 返回后投递,非 wallet 权威源
slot_hub WebSocket 钱包刷新 面向客户端,非业务事件

slot-foundation 文档/测试中的示例 {"uid":1001,"bet_amount":500} 过于简陋,无法支撑多服务幂等与金额语义区分。

新事件定位wallet 事务提交后的权威下注成功事件,供 activity / console / 统计 / 返水等各自按 Topic 模式订阅(如 wallet.bet.#wallet.#)。

sequenceDiagram
    participant Caller as 调用方_PWA或游戏网关
    participant Wallet as slot_wallet
    participant DB as wallet_log
    participant Exchange as slot.wallet.event.exchange
    participant Activity as slot_activity
    participant Stats as 统计服务

    Caller->>Wallet: bet(biz_id, fee, round_id, ...)
    Wallet->>DB: commit 写入 biz_type=bet
    Wallet->>Exchange: publish wallet.bet.success
    Exchange-->>Activity: 各自 queue 绑定
    Exchange-->>Stats: 各自 queue 绑定

推荐信封结构

沿用 TrialRewardMqPublisher 的约定,便于消费方统一解析:

{
  "type": "wallet.bet.success",
  "uid": 12345,
  "ts": 1718092800,
  "data": { }
}
  • type:与 routingKey 一致,便于日志/死信/转发时脱离 routing 元数据也能识别
  • uid:放信封层,消费方可快速过滤
  • ts事件产生时间Unix 秒),非 DB created_at
  • data:业务载荷(见下)

推荐 data 字段(通用可扩展)

1. 幂等与追溯(必填)

字段 来源 说明
biz_id WalletUpdateRequestDTO->biz_id 业务幂等键,消费方应用 uid + biz_id 去重
ledger_id wallet_log.id wallet 侧唯一流水号,需查账时可直接定位
round_id request 关联同局 win
trace_id request可空 链路追踪

2. 渠道上下文(必填)

字段 来源
currency request
source request
organization request

3. 金额语义(必填,三者不可混用)

下注存在三种路径(WalletBetWinLogic::bet()),金额字段必须区分:

字段 含义 计算规则
bet_amount 名义下注额 恒等于 request fee(最小货币单位,*1000
wallet_deduct_amount 实际扣款额 real/trial = feeFS 代付 = 0
valid_bet_amount 计入打码/有效投注额 real = betResult.withdraw + betResult.deposittrial = 0FS 代付 = fee(全额计入,与 TrialValidBetNotifyService 一致)

消费方按业务选用:bet_amount 做流水统计;valid_bet_amount 做活动/返水;wallet_deduct_amount 做资金变动核对。

4. 下注分类(必填)

字段 取值 判定
bet_mode trial / real_money / free_spin 对应三条执行分支

辅助布尔字段(便于过滤,与 bet_mode 冗余但实用):

  • is_fs_paid_bet0/1
  • fs_account_idFS 路径时 > 0否则 0

5. 资金拆分摘要real_money 路径建议带trial/FS 可简化)

"deduction": {
  "bonus": 0,
  "deposit": 3000,
  "withdraw": 2000,
  "hit_side": "deposit"
}

来源:BetService::execute() 返回的 betResult。供返水、RTP、资金归因等消费方使用。

不建议在 MQ 中带完整 fund_detail / remark JSON体积大、含 lot_no 等内部细节);需要时可凭 ledger_id 回查 wallet_log

6. 游戏身份(多上游平台场景,建议必填)

slot-pwa 已有完整游戏映射链(见下),但 slot-wallet 不查 g_game 表,因此游戏身份必须由调用方(通常是 slot-pwa在下注请求中透传wallet 原样写入 MQ。

flowchart LR
    subgraph pwa [slot_pwa]
        ProviderCode["provider_code 如 pop"]
        ExternalCode["vendor_game_id 即 external_game_code"]
        GameCode["game_code 本平台编码"]
    end
    subgraph mapping [g_game_platform_mapping]
        MapRow["platform_id + external_game_code"]
    end
    subgraph game [g_game]
        GameRow["game_code 全局唯一"]
    end
    ProviderCode --> MapRow
    ExternalCode --> MapRow
    MapRow --> GameRow
    GameRow --> GameCode
字段 对应 slot-pwa 概念 说明
provider_code GPlatformModel.code / GameRoundModel.provider_code 上游平台编码,如 pop;多平台并存时必填
game_code GGameModel.game_code / PopTransferCallbackContext::gameCode() 本平台全局唯一游戏编码;消费方可直接关联 g_game
vendor_game_id GGamePlatformMappingModel.external_game_code 上游原始游戏 IDPOP 回调 gameid);与 provider_code 组合可唯一定位 mapping
game_category WalletTrialGameContextResolver::resolveGameCategory() SLOTS;默认 SLOTS

唯一定位规则(与 PopGameResolveService 一致):

  • 主键语义:provider_code + vendor_game_idg_game_platform_mappingg_game
  • 消费方若只需本平台游戏:读 game_code 即可
  • 消费方若需区分上游来源:用 provider_code + vendor_game_id
  • 三者齐全时消费方可交叉校验,避免 mapping 变更导致歧义

当前 gapCashLogic::invokeWalletBetOrWin() 已解析出 boundGameproviderCode(),但调 wallet 时仅传了 vendor_game_id / game_category未传 provider_code / game_code——落地时需补齐。

wallet 层不持久化游戏字段到 wallet_log,必须在 commit 后从 request 即时写入 MQ。


完整示例

真实资金下注

{
  "type": "wallet.bet.success",
  "uid": 12345,
  "ts": 1718092800,
  "data": {
    "biz_id": "pg_tx_20240611_001",
    "ledger_id": 987654321,
    "currency": "USD",
    "round_id": "round_abc123",
    "source": "myslot",
    "organization": 1001,
    "trace_id": "trace_xyz",
    "bet_amount": 5000,
    "wallet_deduct_amount": 5000,
    "valid_bet_amount": 5000,
    "bet_mode": "real_money",
    "is_fs_paid_bet": 0,
    "fs_account_id": 0,
    "provider_code": "pop",
    "game_code": "pop_sweet_bonanza",
    "game_category": "SLOTS",
    "vendor_game_id": "pg_soft_001",
    "deduction": {
      "bonus": 0,
      "deposit": 3000,
      "withdraw": 2000,
      "hit_side": "deposit"
    }
  }
}

FS 代付下注

{
  "type": "wallet.bet.success",
  "uid": 12345,
  "ts": 1718092800,
  "data": {
    "biz_id": "pg_tx_fs_001",
    "ledger_id": 987654322,
    "currency": "USD",
    "round_id": "round_fs_001",
    "source": "myslot",
    "organization": 1001,
    "trace_id": "",
    "bet_amount": 2000,
    "wallet_deduct_amount": 0,
    "valid_bet_amount": 2000,
    "bet_mode": "free_spin",
    "is_fs_paid_bet": 1,
    "fs_account_id": 5566,
    "provider_code": "pop",
    "game_code": "pop_sweet_bonanza",
    "game_category": "SLOTS",
    "vendor_game_id": "pg_soft_001",
    "deduction": {
      "bonus": 0,
      "deposit": 0,
      "withdraw": 0,
      "hit_side": ""
    }
  }
}

试玩期下注

{
  "type": "wallet.bet.success",
  "uid": 12345,
  "ts": 1718092800,
  "data": {
    "biz_id": "trial_bet_001",
    "ledger_id": 987654323,
    "currency": "USD",
    "round_id": "round_trial_001",
    "source": "myslot",
    "organization": 1001,
    "trace_id": "",
    "bet_amount": 1000,
    "wallet_deduct_amount": 1000,
    "valid_bet_amount": 0,
    "bet_mode": "trial",
    "is_fs_paid_bet": 0,
    "fs_account_id": 0,
    "provider_code": "pop",
    "game_code": "pop_trial_slots",
    "game_category": "SLOTS",
    "vendor_game_id": "pg_trial_001",
    "deduction": {
      "bonus": 1000,
      "deposit": 0,
      "withdraw": 0,
      "hit_side": "bonus"
    }
  }
}

关键行为约定

  1. 仅首次成功投递:幂等命中(findByBizIdAndType 已有记录)时发 MQ避免重复消费。
  2. 投递时机commit() 之后,与现有 sendConsoleBus('bet') 同级(WalletBetWinLogic L106-108)。
  3. 失败策略MQ 发送失败只记 warning 日志,不回滚已提交的钱包事务(与 TrialRewardMqPublisher 一致)。
  4. 消费方幂等:建议以 uid + biz_idledger_id 做唯一约束/去重。
  5. 与 trial_reward_bus 关系:短期可并存;长期 activity 可改为订阅 wallet.bet.success 并按 bet_mode / valid_bet_amount 自行路由,逐步去掉 PWA 侧重复投递。

若落地实现(后续步骤)

  1. slot-foundation:新增 WalletEventMQ 常量类(EXCHANGE = slot.wallet.event.exchangeROUTING_KEY_BET_SUCCESS = wallet.bet.success)。
  2. slot-wallet
    • WalletUpdateRequestDTO / Validate 增加 provider_codegame_code(字符串,可空但游戏下注场景应由调用方传入)
    • 新增 WalletBetSuccessMqPublisher(或通用 WalletEventMqPublisher
    • WalletBetWinLogic::bet() commit 后组装上述 data 并 publish
    • 抽取 resolveBetMode() / resolveValidBetAmount() 私有方法,避免金额语义散落
  3. slot-pwa(游戏上下文来源):
  4. 消费方:各服务自建 queue + bindwallet.bet.successwallet.bet.#),按 provider_code / game_code 过滤统计。

设计原则小结

  • 信封统一{ type, uid, data, ts },与 activity 事件一致
  • 金额三分bet_amount / wallet_deduct_amount / valid_bet_amount,避免 FS、trial、real 混用
  • 自洽但不臃肿:含消费方 80% 场景所需字段;深度明细靠 ledger_id 回查
  • 游戏三元组provider_code + vendor_game_id + game_code,对齐 slot-pwa 映射,支持多上游平台
  • 权威源在 wallet:从 PWA 侧活动通知逐步收敛到 wallet 发出的事件;游戏身份由 PWA 解析后透传