- 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路 - 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请 - 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等 - 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口 - 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新 - 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
411 lines
15 KiB
Markdown
411 lines
15 KiB
Markdown
# 结算与返水金额规则
|
||
|
||
本文档为 **Reference(参考)**:只描述**金额如何计算**,不涉及管理端操作步骤。各玩法定义与判赢规则见 [投注玩法说明.md](./投注玩法说明.md);赛事结算流程见 [settlement-and-fund-flow-analysis.md](./settlement-and-fund-flow-analysis.md)。
|
||
|
||
---
|
||
|
||
## 1. 说明与术语
|
||
|
||
| 术语 | 含义 |
|
||
|------|------|
|
||
| **stake** | 投注本金(`Bet.stake`) |
|
||
| **odds** | 欧赔,含本金系数(`BetSelection.odds` 或 leg 快照) |
|
||
| **payout** | 派彩总额,**含本金**(函数 `calculatePayout` / `calculateParlayPayout` 的返回值) |
|
||
| **actualReturn** | 确认结算后写入注单的派彩,与 `payout` 一致 |
|
||
| **净盈亏** | `payout - stake`(LOSE 时 payout=0,净亏 = -stake) |
|
||
| **rate** | 返水比例,**小数**(`0.01` = 1%) |
|
||
| **lineAmount** | 单笔注单返水:`stake × rate` |
|
||
|
||
**精度**:业务金额在库中为 `Decimal(18,4)`;返水比例为 `Decimal(8,4)`。计算链使用 Prisma `Decimal` 全精度,**无额外 round**(Admin UI 百分比输入另有 `rate-percent.ts` 转换,不影响 API 计算)。
|
||
|
||
### 代码真源索引
|
||
|
||
| 主题 | 文件 |
|
||
|------|------|
|
||
| 单关/串关派彩、四分之一盘 | `apps/api/src/domains/settlement/domain/settlement-calculator.ts` |
|
||
| 确认结算、`actualReturn`、注单状态 | `apps/api/src/domains/settlement/settlement.service.ts` |
|
||
| 钱包冻结/结算/重结算 | `apps/api/src/domains/ledger/wallet.service.ts` |
|
||
| 返水费率 | `apps/api/src/domains/operations/cashback/cashback-rate.resolver.ts` |
|
||
| 返水聚合/批次/入账 | `apps/api/src/domains/operations/cashback/cashback.service.ts` |
|
||
| 下注限额、`potentialReturn` | `apps/api/src/domains/betting/betting-limits.service.ts`、`bets.service.ts` |
|
||
| 代理授信 | `apps/api/src/domains/agent/agent-credit.service.ts` |
|
||
| 串关选盘限制 | `packages/shared/src/betting-rules.ts` |
|
||
| 可执行数值对照 | `apps/api/src/infrastructure/database/run-settlement-audit.ts` |
|
||
|
||
---
|
||
|
||
## 2. 单关派彩公式
|
||
|
||
函数:`calculatePayout(stake, odds, result)`
|
||
|
||
| 腿级结果 `SelectionResult` | 公式 | 说明 |
|
||
|---------------------------|------|------|
|
||
| **WIN** | `stake × odds` | 全赢 |
|
||
| **HALF_WIN** | `stake/2 × odds + stake/2` | 等价于 `stake × (odds + 1) / 2` |
|
||
| **PUSH** | `stake` | 走水,退本 |
|
||
| **VOID** | `stake` | 作废,退本 |
|
||
| **HALF_LOSE** | `stake / 2` | 半输,退一半本金 |
|
||
| **LOSE** | `0` | 全输 |
|
||
|
||
### 数值示例(stake = 100)
|
||
|
||
| 用例 ID | 场景 | odds | 结果 | payout | 净盈亏 |
|
||
|---------|------|------|------|--------|--------|
|
||
| BF001 | 1X2 主胜全赢 | 2.0 | WIN | **200.00** | +100.00 |
|
||
| BF002 | 1X2 和局选项全输 | — | LOSE | **0.00** | -100.00 |
|
||
| S009 | 让球 -1 全赢 | 1.85 | WIN | **185.00** | +85.00 |
|
||
| S010 | 让球 -1 走水 | 1.85 | PUSH | **100.00** | 0.00 |
|
||
| S011 | 让球 -0.25 半输 @ 0-0 | 1.85 | HALF_LOSE | **50.00** | -50.00 |
|
||
| S011B | 半赢派彩系数 | 1.85 | HALF_WIN | **142.50** | +42.50 |
|
||
| S012 | 让球 -0.5 全输 @ 0-0 | 1.85 | LOSE | **0.00** | -100.00 |
|
||
| S015 | 大小 小球 0-0 | 1.95 | WIN | **195.00** | +95.00 |
|
||
|
||
---
|
||
|
||
## 3. 串关派彩公式
|
||
|
||
函数:`calculateParlayPayout(stake, legs[])`
|
||
返回:`{ betResult, payout, effectiveOdds }`
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
start[开始] --> anyLose{任一脚 LOSE?}
|
||
anyLose -->|是| lost["payout=0, betResult=LOST"]
|
||
anyLose -->|否| combine[连乘有效因子]
|
||
combine --> allPush{全部 PUSH 或 VOID?}
|
||
allPush -->|是| push["payout=stake, betResult=PUSH"]
|
||
allPush -->|否| won["payout=stake×combinedOdds, betResult=WON"]
|
||
```
|
||
|
||
### 各腿有效因子
|
||
|
||
| 腿级结果 | 连乘因子 |
|
||
|----------|----------|
|
||
| WIN | × `odds` |
|
||
| HALF_WIN | × `(odds + 1) / 2` |
|
||
| HALF_LOSE | × `0.5` |
|
||
| PUSH / VOID | × `1.0`(不参与升赔) |
|
||
| LOSE | 整单终止,payout = 0 |
|
||
|
||
**最终**:`payout = stake × combinedOdds`(除非 LOST 或全 PUSH/VOID)。
|
||
|
||
### 数值示例(stake = 100)
|
||
|
||
| 用例 ID | 腿组合 | effectiveOdds | payout | 净盈亏 |
|
||
|---------|--------|---------------|--------|--------|
|
||
| S016 | 1.8 WIN × 2.0 WIN | 3.6000 | **360.00** | +260.00 |
|
||
| S017 | 1.8 WIN × 2.0 LOSE | 0 | **0.00** | -100.00 |
|
||
| S018 | 1.8 WIN × 2.0 PUSH × 1.9 WIN | 3.4200 | **342.00** | +242.00 |
|
||
| S019 | 1.8 PUSH × 2.0 VOID | 1.0000 | **100.00** | 0.00 |
|
||
|
||
### 串关下注限制
|
||
|
||
串关**禁止**四分之一让球/大小盘(`.25` / `.75` 线),见 `canSelectForParlay` → 错误码 `QUARTER_LINE`。单关可使用四分之一盘。
|
||
|
||
### 跨场结算时序(影响何时产生 payout)
|
||
|
||
1. 结算**某一场比赛**时,只更新该场对应腿的 `resultStatus`。
|
||
2. 若其他场腿尚无结果 → 注单仍为 **PENDING**,此时无最终 `payout`。
|
||
3. **任一脚在本场结算为 LOSE** 且其余腿已有结果 → 整单 **LOST**,`payout = 0`。
|
||
4. 全部腿均有结果后 → 调用 `calculateParlayPayout` 一次,写入 `actualReturn` 并入账。
|
||
|
||
---
|
||
|
||
## 4. 四分之一盘(.25 / .75)
|
||
|
||
让球/大小盘为 `.25` 或 `.75` 时,`settleHandicap` / `settleOverUnder` 拆成**两条半盘**分别判定 WIN/PUSH/LOSE,再合并:
|
||
|
||
| 两半组合 | 合并结果 |
|
||
|----------|----------|
|
||
| 两赢 | WIN |
|
||
| 两输 | LOSE |
|
||
| 一赢一平 | HALF_WIN |
|
||
| 一输一平 | HALF_LOSE |
|
||
| 一赢一输 | PUSH |
|
||
|
||
**示例(S011)**:主让 -0.25,比分 0-0 → **HALF_LOSE** → payout = 50(stake=100)。
|
||
|
||
---
|
||
|
||
## 5. `actualReturn` 与注单 `status` 映射
|
||
|
||
确认结算时(`SettlementService.confirmSettlement`):
|
||
|
||
- **单关(1 腿)**:`result = settleSelection(...)` → `payout = calculatePayout(...)` → **`actualReturn = payout`**
|
||
- **单关多腿 / 串关**:各腿结果齐备后 → **`actualReturn = calculateParlayPayout(...).payout`**
|
||
|
||
注单状态由 `betStatusFromSelection` 映射:
|
||
|
||
| 腿级 / 整单结果 | `Bet.status` |
|
||
|-----------------|--------------|
|
||
| LOSE | **LOST** |
|
||
| PUSH、VOID | **PUSH** |
|
||
| WIN、HALF_WIN、**HALF_LOSE** | **WON** |
|
||
|
||
### 易错点
|
||
|
||
| 场景 | actualReturn | Bet.status |
|
||
|------|--------------|------------|
|
||
| 单关半输(HALF_LOSE) | `stake/2`(如 50) | **WON**(部分返还仍记赢单) |
|
||
| 串关任一脚 LOSE | 0 | **LOST** |
|
||
| 串关全 PUSH/VOID | stake | **PUSH** |
|
||
|
||
串关确认入账时,钱包侧 `result` 简化为:整单 LOST → `LOSE`;整单 PUSH → `PUSH`;整单 WON → `WIN`(不区分腿级半赢半输)。
|
||
|
||
---
|
||
|
||
## 6. 钱包金额变动(结算侧)
|
||
|
||
### 下注冻结(`freezeForBet`)
|
||
|
||
```
|
||
availableBalance -= stake
|
||
frozenBalance += stake
|
||
transactionType = BET_FREEZE
|
||
```
|
||
|
||
余额不足(`availableBalance < stake`)→ `INSUFFICIENT_BALANCE`。
|
||
|
||
### 结算入账(`settleBet`)
|
||
|
||
```
|
||
frozenBalance -= stake
|
||
availableBalance += payout // payout 即 actualReturn
|
||
```
|
||
|
||
| 传入 result | 流水类型 | payout 典型值 |
|
||
|-------------|----------|---------------|
|
||
| WIN | BET_SETTLE_WIN | stake × odds |
|
||
| HALF_WIN | BET_SETTLE_WIN | 半赢派彩 |
|
||
| LOSE | BET_SETTLE_LOSE | 0 |
|
||
| HALF_LOSE | BET_SETTLE_LOSE | stake / 2 |
|
||
| PUSH | BET_SETTLE_PUSH | stake |
|
||
| VOID | BET_VOID_REFUND | stake |
|
||
|
||
幂等键:`businessKey = settle:{batchNo}:{betNo}`。
|
||
|
||
### 作废 / 取消比赛
|
||
|
||
- `Bet.status = VOID`,**`actualReturn = stake`**
|
||
- 调用 `settleBet(..., payout=stake, result='VOID')` → **BET_VOID_REFUND**
|
||
|
||
### 重结算(`confirmResettlement`)
|
||
|
||
```
|
||
delta = 新 payout - 旧 actualReturn
|
||
```
|
||
|
||
- `delta > 0` → `applyResettleDelta`,流水 **BET_SETTLE_WIN**
|
||
- `delta < 0` → 流水 **RESETTLE_REVERSE**(可扣至**负余额**)
|
||
- 注单 `settlementStatus = RESETTLED`,`actualReturn` 更新为新 payout
|
||
|
||
### 端到端钱包示例
|
||
|
||
| 用例 ID | 流程 | 钱包变化 | actualReturn |
|
||
|---------|------|----------|--------------|
|
||
| BF001 | 单关赢 100@2.0,2-1 | 1000 → 900 avail + 100 frozen → **1100 avail** | 200 |
|
||
| BF002 | 单关输(和局+2-1) | 1000 → **900 avail** | 0 |
|
||
| BF003 | 幂等 50 注 | 500 → **450 avail + 50 frozen** | — |
|
||
| BF004 | 余额不足 | **50 不变** | — |
|
||
| BF005 | 代理线玩家输 100 | 玩家 **900 avail** | 0 |
|
||
|
||
BF001 流水顺序:`MANUAL_DEPOSIT` → `BET_FREEZE` → `BET_SETTLE_WIN`。
|
||
|
||
---
|
||
|
||
## 7. 下注时金额校验(与派彩相关)
|
||
|
||
### potentialReturn(下单时估算)
|
||
|
||
| 类型 | 公式 |
|
||
|------|------|
|
||
| 单关 | `stake × odds` |
|
||
| 串关 | `stake × ∏(各腿 odds)` |
|
||
|
||
按**全赢**估算,不含半赢/走水;用于 `maxPayout*` 校验。
|
||
|
||
### 默认限额(`BettingLimitsService`,可被 `system_config` 覆盖)
|
||
|
||
| 配置键 | 默认值 |
|
||
|--------|--------|
|
||
| `bet.min_stake` | 1 |
|
||
| `bet.max_stake_single` | 50,000 |
|
||
| `bet.max_stake_parlay` | 20,000 |
|
||
| `bet.max_payout_single` | 500,000 |
|
||
| `bet.max_payout_parlay` | 1,000,000 |
|
||
| `bet.daily_stake_limit` | 200,000(按 `placedAt` 当日,排除 VOID/CANCELLED) |
|
||
|
||
返水基数 `stake` 为通过上述校验后的下注金额,**与返水 rate 无联动**。
|
||
|
||
---
|
||
|
||
## 8. 返水金额算法
|
||
|
||
### 8.1 单笔返水
|
||
|
||
```
|
||
lineAmount = stake × rate
|
||
```
|
||
|
||
- **stake**:整单本金;串关**不按 leg 拆分**。
|
||
- 实现:`cashback.service.ts` → `bet.stake.mul(rate)`。
|
||
|
||
### 8.2 费率解析(`resolveCashbackRateForBet`)
|
||
|
||
**规则表 `CashbackRule` 优先级**(数值越大越优先):
|
||
|
||
| 优先级 | targetType | 匹配条件 |
|
||
|--------|------------|----------|
|
||
| 3 | USER | `targetId === userId` |
|
||
| 2 | AGENT | `targetId === agentId`(玩家直属代理) |
|
||
| 1 | GLOBAL | 无 targetId 限制 |
|
||
|
||
- 规则带 **marketType** 时,注单**任一** `BetSelection.marketType` 命中才适用;否则跳过该规则。
|
||
- **同优先级**:遍历规则时 `priority > best.priority`(**严格大于**),先写入的同优先级规则不会被后者覆盖。
|
||
- **无规则命中** → 使用 `agentDefaultRate`(见下节)。
|
||
|
||
### 8.3 默认费率 `agentDefaultRate`(无 CashbackRule 时)
|
||
|
||
| 玩家类型 | 来源 |
|
||
|----------|------|
|
||
| 有 `parentId`(代理线下) | 直属代理 `AgentProfile.cashbackRate`;无 profile → **0** |
|
||
| 无 parent,邀请人 sponsor 为 ADMIN | `system_config` → `cashback.admin_invite_rate` |
|
||
| 无 parent 的其他平台直属 | `system_config` → `cashback.platform_direct_rate` |
|
||
|
||
未配置时平台直属/邀请费率默认 **0**。子代理 `cashbackRate` 不得高于父代理。
|
||
|
||
### 8.4 合格注单(进入返水批次)
|
||
|
||
查询条件(`aggregatePeriod`):
|
||
|
||
```
|
||
status IN ('WON', 'LOST')
|
||
settledAt 落在 [periodStart 00:00:00.000, periodEnd 23:59:59.999]
|
||
rate > 0
|
||
未被其他 PREVIEW / CONFIRMED 批次的 cashback_bets 占用
|
||
```
|
||
|
||
**不包含**:PENDING、VOID、CANCELLED、**PUSH** 等。
|
||
|
||
周期按 **`settledAt`**,不是 `placedAt`。
|
||
|
||
### 8.5 批次汇总
|
||
|
||
| 层级 | 公式 |
|
||
|------|------|
|
||
| 单笔 | `lineAmount = stake × rate` |
|
||
| 玩家 | `amount = Σ lineAmount`,`effectiveStake = Σ stake` |
|
||
| 展示 rate | `amount / effectiveStake`(加权平均,仅展示) |
|
||
| 平台批次 | `totalAmount = Σ 玩家 amount` |
|
||
|
||
### 8.6 发放入账
|
||
|
||
确认批次(`confirmBatch`)时:
|
||
|
||
| 字段 | 值 |
|
||
|------|-----|
|
||
| transactionType | **CASHBACK_DEPOSIT** |
|
||
| amount | 玩家批次 `item.amount`(正数) |
|
||
| businessKey | `cashback:{batchNo}:{userId}` |
|
||
| 效果 | `availableBalance += amount` |
|
||
|
||
**平台直充,不扣代理 credit**;confirm **不**调用 `recalculateUsedCredit`。
|
||
同时将批次内注单 `Bet.isCashbacked = true`。
|
||
|
||
### 8.7 数值示例(费率 → 到账)
|
||
|
||
| 用例 ID | 场景 | rate | stake 100 | stake 500 | stake 1000 |
|
||
|---------|------|------|-----------|-----------|------------|
|
||
| CB001 | 玩家专属规则 | 0.03 (3%) | 3.00 | 15.00 | 30.00 |
|
||
| CB002 | 玩法专属 FT_HANDICAP | 0.005 (0.5%) | 0.50 | 2.50 | 5.00 |
|
||
| CB003 | 无规则,代理默认 | 0.02 (2%) | 2.00 | 10.00 | 20.00 |
|
||
| CB004 | 玩法不匹配回退默认 | 0.01 (1%) | 1.00 | — | — |
|
||
|
||
### 8.8 本地 dev seed
|
||
|
||
- `agent1` / `agent2` 的 `AgentProfile.cashbackRate` 默认为 schema **0**,seed 未显式设置。
|
||
- seed **不创建** `CashbackRule`,**不写入** 平台返水 `system_config`。
|
||
- 本地要产生返水批次,需在管理端配置代理默认比例、CashbackRule 或平台直属费率。
|
||
|
||
---
|
||
|
||
## 9. 代理授信与金额的间接关系
|
||
|
||
```
|
||
usedCredit = directPlayerLiability + childExposure
|
||
availableCredit = creditLimit - usedCredit
|
||
```
|
||
|
||
- **directPlayerLiability** = Σ(直属玩家的 `availableBalance + frozenBalance`)
|
||
- **childExposure** = Σ max(子代理 `creditLimit`, 子代理 `usedCredit`)
|
||
|
||
| 事件 | 与返水/结算关系 |
|
||
|------|----------------|
|
||
| 玩家结算输/赢 | 改变余额 → 结算后重算 usedCredit |
|
||
| 返水 confirm | **不**扣代理 credit,**不**即时重算;余额增加后**下次重算**计入负债 |
|
||
| 代理给玩家上分 | 检查 `availableCredit ≥ amount` |
|
||
|
||
**BF005**:玩家输 100 → 玩家 avail **900**;代理 `usedCredit` **1000 → 900**(结算后重算)。
|
||
|
||
---
|
||
|
||
## 10. 数值对照总表与验证
|
||
|
||
### 10.1 派彩速查(stake = 100)
|
||
|
||
| 类型 | 场景 | payout |
|
||
|------|------|--------|
|
||
| 单关 | 1X2 @2.0 WIN | 200.00 |
|
||
| 单关 | LOSE | 0.00 |
|
||
| 单关 | 让球 -1 @1.85 WIN | 185.00 |
|
||
| 单关 | PUSH | 100.00 |
|
||
| 单关 | HALF_LOSE | 50.00 |
|
||
| 单关 | HALF_WIN @1.85 | 142.50 |
|
||
| 单关 | 小球 @1.95 WIN | 195.00 |
|
||
| 串关 | 1.8×2.0 全中 | 360.00 |
|
||
| 串关 | 一关 LOSE | 0.00 |
|
||
| 串关 | 含 PUSH | 342.00 |
|
||
| 串关 | 全 PUSH/VOID | 100.00 |
|
||
|
||
### 10.2 返水速查(rate × stake)
|
||
|
||
| rate | stake 100 | stake 1000 |
|
||
|------|-----------|------------|
|
||
| 3% (0.03) | 3.00 | 30.00 |
|
||
| 2% (0.02) | 2.00 | 20.00 |
|
||
| 0.5% (0.005) | 0.50 | 5.00 |
|
||
|
||
### 10.3 维护与回归命令
|
||
|
||
```bash
|
||
# 打印与本文一致的金额对照表
|
||
pnpm --filter @thebet365/api audit:settlement
|
||
|
||
# 结算引擎单测(含 S016–S019 串关)
|
||
pnpm --filter @thebet365/api exec jest settlement-calculator.spec.ts --runInBand
|
||
|
||
# 全量 smoke(含 S009–S019、CB001–CB004、BF001–BF005)
|
||
pnpm test:smoke
|
||
```
|
||
|
||
修改 `settlement-calculator.ts`、`cashback-rate.resolver.ts` 或 smoke 期望金额时,**须同步更新本文档**并重新运行上述命令。
|
||
|
||
### 10.4 Smoke / 单测用例索引
|
||
|
||
| 套件 | ID 范围 | 内容 |
|
||
|------|---------|------|
|
||
| settlement | S009–S019 | 让球/大小/串关派彩数值 |
|
||
| settlement | S011B | HALF_WIN 系数 |
|
||
| cashback | CB001–CB004 | 费率解析 |
|
||
| bet-flow | BF001–BF005 | 钱包 + 代理额度端到端 |
|
||
|
||
---
|
||
|
||
## 附录:支持的 marketType(结算判定入口)
|
||
|
||
金额规则与下列盘口共用 `settleSelection`;具体比分判定逻辑见 `settlement-calculator.ts`,本文不展开。
|
||
|
||
`FT_1X2`、`HT_1X2`、`FT_ODD_EVEN`、`FT_HANDICAP`、`HT_HANDICAP`、`FT_OVER_UNDER`、`HT_OVER_UNDER`、`FT_CORRECT_SCORE`、`HT_CORRECT_SCORE`、`SH_CORRECT_SCORE`、`HT_FT`、`FT_TOTAL_GOALS`、球队进球、角球/牌数相关盘、`OUTRIGHT_WINNER` 等。
|
||
|
||
未知 marketType 默认腿级结果 **LOSE**(payout = 0)。
|