# 结算与返水金额规则

本文档为 **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）。
