This commit is contained in:
ray zhou
2026-06-29 14:51:55 +08:00
parent 225fb2bd28
commit 2dd9f17da9
319 changed files with 29461 additions and 9412 deletions

View File

@@ -0,0 +1,320 @@
<!-- 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 解析后透传