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

321 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 8500b436-6684-4cb3-a26f-8d927dd70f9b -->
---
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.#`)。
```mermaid
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`](slot-wallet/app/service/mq/TrialRewardMqPublisher.php) 的约定,便于消费方统一解析:
```json
{
"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()`](slot-wallet/app/api/logic/WalletBetWinLogic.php)),金额字段必须区分:
| 字段 | 含义 | 计算规则 |
|---|---|---|
| `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`](slot-wallet/app/service/trial/TrialValidBetNotifyService.php) 一致) |
> 消费方按业务选用:`bet_amount` 做流水统计;`valid_bet_amount` 做活动/返水;`wallet_deduct_amount` 做资金变动核对。
### 4. 下注分类(必填)
| 字段 | 取值 | 判定 |
|---|---|---|
| `bet_mode` | `trial` / `real_money` / `free_spin` | 对应三条执行分支 |
辅助布尔字段(便于过滤,与 `bet_mode` 冗余但实用):
- `is_fs_paid_bet`0/1
- `fs_account_id`FS 路径时 > 0否则 0
### 5. 资金拆分摘要real_money 路径建议带trial/FS 可简化)
```json
"deduction": {
"bonus": 0,
"deposit": 3000,
"withdraw": 2000,
"hit_side": "deposit"
}
```
来源:[`BetService::execute()`](slot-wallet/app/service/wallet/BetService.php) 返回的 `betResult`。供返水、RTP、资金归因等消费方使用。
**不建议**在 MQ 中带完整 `fund_detail` / `remark` JSON体积大、含 lot_no 等内部细节);需要时可凭 `ledger_id` 回查 `wallet_log`
### 6. 游戏身份(多上游平台场景,建议必填)
slot-pwa 已有完整游戏映射链(见下),但 **slot-wallet 不查 g_game 表**,因此游戏身份必须由调用方(通常是 slot-pwa在下注请求中透传wallet 原样写入 MQ。
```mermaid
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`](slot-pwa/app/service/game/PopGameResolveService.php) 一致):
- 主键语义:`provider_code` + `vendor_game_id``g_game_platform_mapping``g_game`
- 消费方若只需本平台游戏:读 `game_code` 即可
- 消费方若需区分上游来源:用 `provider_code` + `vendor_game_id`
- 三者齐全时消费方可交叉校验,避免 mapping 变更导致歧义
当前 gap[`CashLogic::invokeWalletBetOrWin()`](slot-pwa/app/pop/logic/CashLogic.php) 已解析出 `boundGame``providerCode()`,但调 wallet 时**仅传了** `vendor_game_id` / `game_category`**未传** `provider_code` / `game_code`——落地时需补齐。
wallet 层**不持久化**游戏字段到 `wallet_log`,必须在 commit 后从 request 即时写入 MQ。
---
## 完整示例
### 真实资金下注
```json
{
"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 代付下注
```json
{
"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": ""
}
}
}
```
### 试玩期下注
```json
{
"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](slot-wallet/app/api/logic/WalletBetWinLogic.php))。
3. **失败策略**MQ 发送失败只记 warning 日志,**不回滚**已提交的钱包事务(与 `TrialRewardMqPublisher` 一致)。
4. **消费方幂等**:建议以 `uid + biz_id``ledger_id` 做唯一约束/去重。
5. **与 trial_reward_bus 关系**:短期可并存;长期 activity 可改为订阅 `wallet.bet.success` 并按 `bet_mode` / `valid_bet_amount` 自行路由,逐步去掉 PWA 侧重复投递。
---
## 若落地实现(后续步骤)
1. **slot-foundation**:新增 [`WalletEventMQ`](slot-foundation/src/Constants/MQ/) 常量类(`EXCHANGE = slot.wallet.event.exchange``ROUTING_KEY_BET_SUCCESS = wallet.bet.success`)。
2. **slot-wallet**
- `WalletUpdateRequestDTO` / Validate 增加 `provider_code``game_code`(字符串,可空但游戏下注场景应由调用方传入)
- 新增 `WalletBetSuccessMqPublisher`(或通用 `WalletEventMqPublisher`
-`WalletBetWinLogic::bet()` commit 后组装上述 `data` 并 publish
- 抽取 `resolveBetMode()` / `resolveValidBetAmount()` 私有方法,避免金额语义散落
3. **slot-pwa**(游戏上下文来源):
- `WalletWinOrBetRequestDTO` 增加 `provider_code``game_code`
- [`CashLogic::invokeWalletBetOrWin()`](slot-pwa/app/pop/logic/CashLogic.php) 从 `transferContext->providerCode()``boundGame->game_code` 透传
- [`WalletService::appendTrialGameContext()`](slot-pwa/app/service/WalletService.php) 非空时写入请求体
- 后续新上游平台(非 pop沿用同一字段平台 code 取自各自 `providerCode()` 实现
4. **消费方**:各服务自建 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 解析后透传