Files
thebet365/docs/结算与返水金额规则.md
Mars afb5c5437e feat: 充值订单审计与重新申请,优化赛事展示和余额刷新
- 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路
- 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请
- 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等
- 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口
- 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新
- 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 14:52:05 +08:00

411 lines
15 KiB
Markdown
Raw 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.
# 结算与返水金额规则
本文档为 **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 = 50stake=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.02-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
# 结算引擎单测(含 S016S019 串关)
pnpm --filter @thebet365/api exec jest settlement-calculator.spec.ts --runInBand
# 全量 smoke含 S009S019、CB001CB004、BF001BF005
pnpm test:smoke
```
修改 `settlement-calculator.ts``cashback-rate.resolver.ts` 或 smoke 期望金额时,**须同步更新本文档**并重新运行上述命令。
### 10.4 Smoke / 单测用例索引
| 套件 | ID 范围 | 内容 |
|------|---------|------|
| settlement | S009S019 | 让球/大小/串关派彩数值 |
| settlement | S011B | HALF_WIN 系数 |
| cashback | CB001CB004 | 费率解析 |
| bet-flow | BF001BF005 | 钱包 + 代理额度端到端 |
---
## 附录:支持的 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