321 lines
12 KiB
Markdown
321 lines
12 KiB
Markdown
<!-- 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` | 上游原始游戏 ID(POP 回调 `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 解析后透传
|