12 KiB
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 秒),非 DBcreated_atdata:业务载荷(见下)
推荐 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 = fee;FS 代付 = 0 |
valid_bet_amount |
计入打码/有效投注额 | real = betResult.withdraw + betResult.deposit;trial = 0;FS 代付 = fee(全额计入,与 TrialValidBetNotifyService 一致) |
消费方按业务选用:
bet_amount做流水统计;valid_bet_amount做活动/返水;wallet_deduct_amount做资金变动核对。
4. 下注分类(必填)
| 字段 | 取值 | 判定 |
|---|---|---|
bet_mode |
trial / real_money / free_spin |
对应三条执行分支 |
辅助布尔字段(便于过滤,与 bet_mode 冗余但实用):
is_fs_paid_bet:0/1fs_account_id:FS 路径时 > 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 |
上游原始游戏 ID(POP 回调 gameid);与 provider_code 组合可唯一定位 mapping |
game_category |
WalletTrialGameContextResolver::resolveGameCategory() |
如 SLOTS;默认 SLOTS |
唯一定位规则(与 PopGameResolveService 一致):
- 主键语义:
provider_code+vendor_game_id→g_game_platform_mapping→g_game - 消费方若只需本平台游戏:读
game_code即可 - 消费方若需区分上游来源:用
provider_code+vendor_game_id - 三者齐全时消费方可交叉校验,避免 mapping 变更导致歧义
当前 gap:CashLogic::invokeWalletBetOrWin() 已解析出 boundGame 与 providerCode(),但调 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"
}
}
}
关键行为约定
- 仅首次成功投递:幂等命中(
findByBizIdAndType已有记录)时不发 MQ,避免重复消费。 - 投递时机:
commit()之后,与现有sendConsoleBus('bet')同级(WalletBetWinLogicL106-108)。 - 失败策略:MQ 发送失败只记 warning 日志,不回滚已提交的钱包事务(与
TrialRewardMqPublisher一致)。 - 消费方幂等:建议以
uid + biz_id或ledger_id做唯一约束/去重。 - 与 trial_reward_bus 关系:短期可并存;长期 activity 可改为订阅
wallet.bet.success并按bet_mode/valid_bet_amount自行路由,逐步去掉 PWA 侧重复投递。
若落地实现(后续步骤)
- slot-foundation:新增
WalletEventMQ常量类(EXCHANGE = slot.wallet.event.exchange、ROUTING_KEY_BET_SUCCESS = wallet.bet.success)。 - slot-wallet:
WalletUpdateRequestDTO/ Validate 增加provider_code、game_code(字符串,可空但游戏下注场景应由调用方传入)- 新增
WalletBetSuccessMqPublisher(或通用WalletEventMqPublisher) - 在
WalletBetWinLogic::bet()commit 后组装上述data并 publish - 抽取
resolveBetMode()/resolveValidBetAmount()私有方法,避免金额语义散落
- slot-pwa(游戏上下文来源):
WalletWinOrBetRequestDTO增加provider_code、game_codeCashLogic::invokeWalletBetOrWin()从transferContext->providerCode()与boundGame->game_code透传WalletService::appendTrialGameContext()非空时写入请求体- 后续新上游平台(非 pop)沿用同一字段,平台 code 取自各自
providerCode()实现
- 消费方:各服务自建 queue + bind(如
wallet.bet.success或wallet.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 解析后透传