feat: 充值订单审计与重新申请,优化赛事展示和余额刷新

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-15 14:52:05 +08:00
parent 73a94e6be3
commit afb5c5437e
55 changed files with 3050 additions and 160 deletions

View File

@@ -11,6 +11,8 @@
## MVP 验收清单18 项)
玩法与串关规则参考:[投注玩法说明.md](./投注玩法说明.md)。
- [ ] 玩家可登录、改密码、切换语言
- [ ] 代理可创建直属玩家
- [ ] 代理可给直属玩家上分/下分

430
docs/投注玩法说明.md Normal file
View File

@@ -0,0 +1,430 @@
# 投注玩法说明
本文档为 **Explanation说明**:集中描述 thebet365 第一版**足球赛前盘**的每种玩法如何下注、如何判赢。
派彩金额公式见 [结算与返水金额规则.md](./结算与返水金额规则.md);结算操作流程见 [settlement-and-fund-flow-analysis.md](./settlement-and-fund-flow-analysis.md)。
---
## 1. 范围与注单类型
### 1.1 产品范围v1
| 项 | 说明 |
|----|------|
| 运动 | 仅足球(`SPORT_TYPE_FOOTBALL` |
| 时段 | **赛前盘**:开球时间之前可下注(`isPreMatchKickoff` |
| 不含 | 滚球、Cash Out、改单、系统串关 |
与玩家端「我的 → 投注规则」文案一致(`apps/player/src/i18n/zh-CN.ts` `rules_p1``rules_p5`)。
### 1.2 注单类型
| 类型 | 枚举 | 说明 |
|------|------|------|
| 单关 | `SINGLE` | 1 个选项1 笔本金 |
| 串关 | `PARLAY` | 25 腿(`PARLAY_MIN_LEGS=2``PARLAY_MAX_LEGS=5`),赔率连乘 |
**数据模型**:库表为 `Bet`(注单)+ `BetSelection`(选项腿)。玩家端 Pinia store 名 `betSlip`,逻辑上等价于「投注单」。
### 1.3 API 入口
| 操作 | 路径 |
|------|------|
| 单关下注 | `POST /api/player/bets/single` |
| 串关下注 | `POST /api/player/bets/parlay` |
---
## 2. 玩法总览表
权威目录:`packages/shared/src/market-catalog.ts``FOOTBALL_MARKET_CATALOG`**共 18 种**足球盘口)。
| marketType | 中文名 | 时段 | 主要选项 | 默认线 | 单关 | 串关 | 串关序 | 结算分类 | 结算依据 | 默认 seed |
|------------|--------|------|----------|--------|------|------|--------|----------|----------|-----------|
| `FT_1X2` | 全场 1X2 | FT | HOME / DRAW / AWAY | — | ✓ | ✓ | 3 | SCORE | 全场比分 | ✓ |
| `FT_HANDICAP` | 全场让球 | FT | HOME / AWAY | -0.5 | ✓ | ✓ | 1 | HANDICAP | 全场比分 + 让球线 | ✓ |
| `FT_OVER_UNDER` | 全场大小 | FT | OVER / UNDER | 2.5 | ✓ | ✓ | 2 | TOTAL | 全场总进球 + 大小线 | ✓ |
| `FT_ODD_EVEN` | 全场单双 | FT | ODD / EVEN | — | ✓ | ✓ | 4 | ODD_EVEN | 全场总进球奇偶 | ✓ |
| `HT_1X2` | 半场 1X2 | HT | HOME / DRAW / AWAY | — | ✓ | ✓ | 7 | SCORE | 半场比分 | ✓ |
| `HT_HANDICAP` | 半场让球 | HT | HOME / AWAY | -0.5 | ✓ | ✓ | 5 | HANDICAP | 半场比分 + 让球线 | ✓ |
| `HT_OVER_UNDER` | 半场大小 | HT | OVER / UNDER | 1.5 | ✓ | ✓ | 6 | TOTAL | 半场总进球 + 大小线 | ✓ |
| `FT_CORRECT_SCORE` | 全场波胆 | FT | 见 §3.E | — | ✓ | ✗ | — | CORRECT_SCORE | 全场精确比分 | ✓ |
| `HT_CORRECT_SCORE` | 上半场波胆 | HT | 见 §3.E | — | ✓ | ✗ | — | CORRECT_SCORE | 半场精确比分 | ✗ |
| `SH_CORRECT_SCORE` | 下半场波胆 | SH | 见 §3.E | — | ✓ | ✗ | — | CORRECT_SCORE | 下半场比分FTHT | ✗ |
| `OUTRIGHT_WINNER` | 冠军 | OUTRIGHT | 球队 code | — | ✓ | ✗ | — | OUTRIGHT | 冠军球队 code | ✗* |
| `FT_TEAM_TOTAL_HOME` | 主队进球大小 | FT | OVER / UNDER | 1.5 | ✓ | ✓ | 8 | TOTAL | 主队全场进球 | ✓ |
| `FT_TEAM_TOTAL_AWAY` | 客队进球大小 | FT | OVER / UNDER | 1.5 | ✓ | ✓ | 9 | TOTAL | 客队全场进球 | ✓ |
| `HT_FT` | 半全场 | FT | 9 组合,见 §3.F | — | ✓ | ✓ | 10 | SCORE | 半场结果 + 全场结果 | ✓ |
| `FT_TOTAL_GOALS` | 总进球数 | FT | TG_0_1 … TG_7_PLUS | — | ✓ | ✓ | 11 | SCORE | 全场总进球区间 | ✓ |
| `FT_CORNERS_HANDICAP` | 全场角球让球 | FT | HOME / AWAY | -0.5 | ✓ | ✓ | 12 | MANUAL_STATS | 主客角球 + 让球线 | ✓ |
| `FT_CORNERS_OVER_UNDER` | 全场角球大小 | FT | OVER / UNDER | 8.5 | ✓ | ✓ | 13 | MANUAL_STATS | 角球总数 + 大小线 | ✓ |
| `FT_CARDS_OVER_UNDER` | 全场罚牌大小 | FT | OVER / UNDER | 3.5 | ✓ | ✓ | 14 | MANUAL_STATS | 罚牌总数 + 大小线 | ✓ |
\* `OUTRIGHT_WINNER` 不在常规赛事 seed 模板中,由世界杯 48 强等单独同步,见 [默认数据说明.md](./默认数据说明.md)。
**默认 seed 刻意不含**`HT_CORRECT_SCORE``SH_CORRECT_SCORE`(系统仍识别并可结算)。
---
## 3. 按类别详解
以下判赢逻辑对应 `apps/api/src/domains/settlement/domain/settlement-calculator.ts``settleSelection()`
腿级结果:`WIN` | `HALF_WIN` | `PUSH` | `HALF_LOSE` | `LOSE` | `VOID`
### 3.A 独赢盘1X2
**结算分类**`SCORE`
#### FT_1X2全场 1X2 / Full Time 1X2
| 项 | 内容 |
|----|------|
| 玩法说明 | 预测**全场 90 分钟**(含补时,不含加时/点球)结束时的赛果 |
| 选项 | `HOME` 主胜 · `DRAW` 和局 · `AWAY` 客胜 |
| 判赢 | 比较 `ftHome``ftAway`:主胜 / 和 / 客胜 |
| 示例 | 全场 2:1`HOME`**WIN**;选 `DRAW`**LOSE** |
| 串关 | 允许(`parlayOrder=3` |
| 快照字段 | 无盘口线 |
#### HT_1X2半场 1X2
| 项 | 内容 |
|----|------|
| 玩法说明 | 预测**上半场**结束时的赛果 |
| 选项 | `HOME` / `DRAW` / `AWAY` |
| 判赢 | 比较 `htHome``htAway` |
| 示例 | 半场 0:0、全场 1:0选半场 `DRAW`**WIN** |
| 串关 | 允许(`parlayOrder=7` |
---
### 3.B 让球盘(亚洲盘)
**结算分类**`HANDICAP`(角球盘为 `MANUAL_STATS`,算法同为让球)
**通用规则**
- 选项:`HOME`(主队受让/让球侧)/ `AWAY`(客队)
- 让球线存于 `BetSelection.handicapLine`(下注时快照)
- 主队视角:`adj = 本队进球 + handicapLine 对手进球`
- `adj > 0` → WIN`adj = 0` → PUSH`adj < 0` → LOSE
- 客队选项使用 **相反符号** 的让球线(代码内对客队取 `-line`
- 四分之一盘(`.25` / `.75`)可产生半赢半输,见 [§5](#5-四分之一盘25--75)
#### FT_HANDICAP全场让球
| 项 | 内容 |
|----|------|
| 默认线 | -0.5(表示主队让半球,即主队需净胜至少 1 球才全赢) |
| 统计 | `ftHome``ftAway` |
| 串关 | 允许;**禁止** `.25`/`.75` 线进入串关 |
**示例**(线 -0.5,选主队 `HOME`
| 比分 | 结果 |
|------|------|
| 2:1 | WIN2 + (-0.5) 1 = 0.5 > 0 |
| 1:1 | LOSE |
| 1:0 | WIN |
#### HT_HANDICAP半场让球
| 项 | 内容 |
|----|------|
| 默认线 | -0.5 |
| 统计 | `htHome``htAway` |
| 串关 | 允许(禁止四分之一线) |
#### FT_CORNERS_HANDICAP全场角球让球
| 项 | 内容 |
|----|------|
| 默认线 | -0.5 |
| 统计 | 管理员录入 `homeCorners``awayCorners`;缺失时结算报错 `SETTLEMENT_STAT_MISSING` |
| 判赢 | 与足球让球相同,但用角球数代替进球 |
| 串关 | 允许(禁止四分之一线) |
---
### 3.C 大小盘Over/Under
**结算分类**`TOTAL`(角球/罚牌为 `MANUAL_STATS`,算法同为大小)
**通用规则**
- 选项:`OVER`(大)/ `UNDER`(小)
- 大小线存于 `BetSelection.totalLine`
- 大球:`总进球 > line` → WIN`= line` → PUSH`< line` → LOSE
- 小球:相反
- 支持四分之一盘半赢半输;串关禁止 `.25`/`.75` 线
| marketType | 中文名 | 默认线 | 统计对象 |
|------------|--------|--------|----------|
| `FT_OVER_UNDER` | 全场大小 | 2.5 | `ftHome + ftAway` |
| `HT_OVER_UNDER` | 半场大小 | 1.5 | `htHome + htAway` |
| `FT_TEAM_TOTAL_HOME` | 主队进球大小 | 1.5 | `ftHome` |
| `FT_TEAM_TOTAL_AWAY` | 客队进球大小 | 1.5 | `ftAway` |
| `FT_CORNERS_OVER_UNDER` | 全场角球大小 | 8.5 | `homeCorners + awayCorners` |
| `FT_CARDS_OVER_UNDER` | 全场罚牌大小 | 3.5 | `homeCards + awayCards` |
**示例**`FT_OVER_UNDER` 线 2.5,选 `OVER`
| 比分 | 总进球 | 结果 |
|------|--------|------|
| 2:1 | 3 | WIN |
| 1:1 | 2 | LOSE |
| 2:0 | 2 | LOSE2 不大于 2.5 |
角球/罚牌盘:结算前须在管理端录入对应统计字段,否则无法确认结算。
---
### 3.D 单双Odd/Even
#### FT_ODD_EVEN全场单双
| 项 | 内容 |
|----|------|
| 选项 | `ODD` 单 · `EVEN` 双 |
| 判赢 | 全场总进球 `ftHome + ftAway` 为奇数或偶数 |
| 示例 | 0:0 → 总进球 0 → **双EVEN 赢)** |
| 串关 | 允许(`parlayOrder=4` |
---
### 3.E 波胆Correct Score— 仅单关
**结算分类**`CORRECT_SCORE`
**串关**:全部 `allowParlay: false`,不可进入串关。
| marketType | 比分来源 |
|------------|----------|
| `FT_CORRECT_SCORE` | 全场 `ftHome:ftAway` |
| `HT_CORRECT_SCORE` | 半场 `htHome:htAway` |
| `SH_CORRECT_SCORE` | 下半场 `(ftHomehtHome):(ftAwayhtAway)` |
**选项类型**
1. **精确比分**code 形如 `SCORE_2_1`(表示 2:1
2. **其它主胜** `OTHER_HOME`:主胜,且精确比分不在模板列表中
3. **其它和局** `OTHER_DRAW`:和局,且精确比分不在模板列表中
4. **其它客胜** `OTHER_AWAY`:客胜,且精确比分不在模板列表中
**模板列表**`market-catalog.ts`
- 全场:`FT_CORRECT_SCORE_TEMPLATE`28 项,含 3 个 OTHER_*
- 半场/下半场:`HT_CORRECT_SCORE_TEMPLATE`17 项)
**判赢逻辑**
-`SCORE_h_a`:实际比分完全相等 → WIN否则 LOSE
- 若实际比分在模板中有对应 `SCORE_*` 项,则 OTHER_* 选项 LOSE
- 若实际比分**不在**模板中,则按主胜/和/客胜归到对应 OTHER_* → WIN
**示例**(全场波胆,实际 4:2
- 模板含 `SCORE_4_2` → 选 `SCORE_4_2` **WIN**,选 `OTHER_HOME` **LOSE**
- 若模板不含 4:2 → 选 `OTHER_HOME` **WIN**(主队胜)
---
### 3.F 组合 / 区间
#### HT_FT半全场 / Half TimeFull Time
| 项 | 内容 |
|----|------|
| 玩法说明 | 同时预测**半场结果**与**全场结果** |
| 选项 code | `{半场}_{全场}`,各为 `HOME` / `DRAW` / `AWAY`,共 9 种 |
| 判赢 | 实际组合须与选项完全一致 |
| code | 含义 |
|------|------|
| `HOME_HOME` | 半场主胜 + 全场主胜 |
| `HOME_DRAW` | 半场主胜 + 全场和 |
| `HOME_AWAY` | 半场主胜 + 全场客胜 |
| `DRAW_HOME` | 半场和 + 全场主胜 |
| `DRAW_DRAW` | 半场和 + 全场和 |
| `DRAW_AWAY` | 半场和 + 全场客胜 |
| `AWAY_HOME` | 半场客胜 + 全场主胜 |
| `AWAY_DRAW` | 半场客胜 + 全场和 |
| `AWAY_AWAY` | 半场客胜 + 全场客胜 |
**示例**:半场 1:0、全场 1:1 → 半场 HOME、全场 DRAW → 选 `HOME_DRAW` **WIN**
#### FT_TOTAL_GOALS总进球数区间
| 选项 code | 区间 |
|-----------|------|
| `TG_0_1` | 01 球 |
| `TG_2_3` | 23 球 |
| `TG_4_6` | 46 球 |
| `TG_7_PLUS` | 7 球及以上 |
判赢:按全场 `ftHome + ftAway` 落入区间。
**示例**3:2 → 总进球 5 → 选 `TG_4_6` **WIN**
---
### 3.G 冠军Outright
#### OUTRIGHT_WINNER
| 项 | 内容 |
|----|------|
| 玩法说明 | 预测联赛/赛事**最终冠军**(如世界杯夺冠球队) |
| 选项 | 各参赛队 `selectionCode` = 球队 code`FRA``BRA` |
| 判赢 | `selectionCode === winnerTeamCode`(管理员指定冠军) |
| 串关 | **禁止** |
| 前置条件 | 联赛内常规赛事须全部 `SETTLED``CANCELLED` 后才可结算冠军盘 |
| 玩家入口 | `/bet` →「优胜冠军」→ `OutrightBetModal`**不走** BetSlip 串关) |
---
## 4. 串关规则
实现:`packages/shared/src/betting-rules.ts``canSelectForParlay`)、`apps/api/src/domains/betting/bets.service.ts``apps/player/src/stores/betSlip.ts`
| 规则 | 说明 |
|------|------|
| 腿数 | **25 腿** |
| 同场限制 | **每场比赛最多 1 项**(前端 `SAME_MATCH`;后端 `PARLAY_SAME_MATCH_FORBIDDEN` |
| 禁止玩法 | 波胆3 种)、冠军盘 |
| 禁止盘口线 | 让球/大小族(含队进球、角球、罚牌)的 **`.25` / `.75` 线**`QUARTER_LINE` |
| 可串关玩法 | `PARLAY_MARKET_TYPES`**14 种**(见总览表「串关=✓」行) |
| 串关列表 API | `GET /api/player/matches?scope=parlay` 仅返回含可串关盘口的赛事 |
**串关派彩**(连乘有效因子、任一脚 LOSE 整单 LOST 等)见 [结算与返水金额规则.md §3](./结算与返水金额规则.md)。
---
## 5. 四分之一盘(.25 / .75
让球/大小盘盘口线为 **0.25 或 0.75** 的整数倍时(如 -0.25、2.75),系统将盘口**拆成两条半盘**分别判定,再合并为腿级结果:
| 两半组合 | 合并结果 |
|----------|----------|
| 两赢 | WIN |
| 两输 | LOSE |
| 一赢一走 | HALF_WIN |
| 一输一走 | HALF_LOSE |
| 一赢一输 | PUSH |
| 场景 | 单关 | 串关 |
|------|------|------|
| 四分之一线 | **允许**下注 | **禁止**选入(`QUARTER_LINE` |
**示例 1** — 全场让球 **-0.25**,选主队,比分 **0:0**
- 拆为 -0 与 -0.5 两半:-0 → PUSH-0.5 → LOSE
- 合并 → **HALF_LOSE**(退一半本金,见派彩文档)
**示例 2** — 全场大小 **2.25**,选小球,总进球 **2**
- 拆为 2.0 与 2.5:对 2.0 小球 PUSH对 2.5 小球 WIN
- 合并 → **HALF_WIN**
---
## 6. 下注校验与限额
### 6.1 下注校验(`BetsService.validateSelection`
| 校验项 | 说明 |
|--------|------|
| 盘口状态 | 选项与盘口均为 `OPEN` |
| 玩家可见 | `showOnPlayer = true` |
| 赛事 | `PUBLISHED`;非冠军盘须未开球 |
| 运动 | 仅足球 |
| 赔率版本 | 请求 `oddsVersion` 须与库内一致 |
| 单关/串关 | 单关要求 `allowSingle`;串关走 `canSelectForParlay` |
| 资金 | 通过 `FundsPostingService.freezeBet` 冻结本金 |
### 6.2 默认限额(`betting-limits.service.ts`
| 配置项 | 默认值 |
|--------|--------|
| 最小单注 | 1 |
| 单关最大投注 | 50,000 |
| 串关最大投注 | 20,000 |
| 单关最高派彩 | 500,000 |
| 串关最高派彩 | 1,000,000 |
| 玩家每日投注上限 | 200,000 |
可在管理端 `systemConfig` 覆盖;`potentialReturn` 超限会拒单(`MAX_PAYOUT`)。
### 6.3 其它
- **未知 marketType** 结算时默认 **LOSE**
- **选项 code 回退**:若快照无 code可从中文名推断`settlement-helpers.ts`
- **玩家选盘**:赛事详情 → 点赔率 → `BetSlipDrawer`;波胆用 `CorrectScorePanel`
---
## 7. 下注与结算流程摘要
```mermaid
sequenceDiagram
participant Player
participant API
participant Admin
Player->>API: POST /player/bets/single 或 parlay
API->>API: validateSelection + freezeBet
Admin->>API: recordScore
Admin->>API: previewSettlement
Admin->>API: confirmSettlement
API->>API: settleSelection 逐腿判赢
API->>API: calculatePayout 或 calculateParlayPayout
API->>Player: 钱包 settleBet 入账
```
| 阶段 | 说明 |
|------|------|
| 下注 | 创建 `Bet` + `BetSelection`,冻结 `stake` |
| 录入比分 | 赛事 → `PENDING_SETTLEMENT` |
| 预览 | 生成 `SettlementBatch`PREVIEW可含角球/罚牌统计 |
| 确认 | 写入腿级 `resultStatus`、注单 `status`/`actualReturn`,解冻并派彩 |
**跨场串关**:每场结算时只更新该场腿的 `resultStatus`;全部腿有结果后才调用 `calculateParlayPayout` 一次。
详细流程见 [settlement-and-fund-flow-analysis.md](./settlement-and-fund-flow-analysis.md)。
---
## 8. 代码真源与相关文档
### 8.1 代码索引
| 主题 | 文件 |
|------|------|
| 玩法目录18 种) | `packages/shared/src/market-catalog.ts` |
| 串关选盘规则 | `packages/shared/src/betting-rules.ts` |
| 下注与校验 | `apps/api/src/domains/betting/bets.service.ts` |
| 下注限额 | `apps/api/src/domains/betting/betting-limits.service.ts` |
| 腿级结算 | `apps/api/src/domains/settlement/domain/settlement-calculator.ts` |
| 结算流程 | `apps/api/src/domains/settlement/settlement.service.ts` |
| 结算辅助 | `apps/api/src/domains/settlement/domain/settlement-helpers.ts` |
| 单元测试 | `apps/api/src/domains/settlement/domain/settlement-calculator.spec.ts` |
| Smoke 用例 | `apps/api/src/domains/operations/smoke-tests/smoke-test.cases.ts` |
| 玩家投注单 | `apps/player/src/stores/betSlip.ts` |
### 8.2 相关文档
| 文档 | 内容 |
|------|------|
| [结算与返水金额规则.md](./结算与返水金额规则.md) | 单关/串关派彩公式、四分之一盘金额、返水 |
| [settlement-and-fund-flow-analysis.md](./settlement-and-fund-flow-analysis.md) | 结算三步、钱包流水 |
| [默认数据说明.md](./默认数据说明.md) | seed 盘口范围、48 强冠军盘 |
| [UAT_CHECKLIST.md](./UAT_CHECKLIST.md) | 投注与串关 UAT 项 |
### 8.3 文档分工
| 本文档 | 结算金额文档 |
|--------|--------------|
| 每种玩法怎么选、怎么判赢 | stake × odds 怎么算 |
| 串关能否选、同场限制 | 串关连乘因子与 payout |
| 角球/罚牌统计要求 | HALF_WIN 派彩数值示例 |
---
*最后更新:与 `FOOTBALL_MARKET_CATALOG`18 种)及 `settlement-calculator.ts` 实现对齐。*

View File

@@ -0,0 +1,410 @@
# 结算与返水金额规则
本文档为 **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

View File

@@ -69,6 +69,8 @@
## 三、默认赛事与盘口
各玩法判赢规则与 18 种盘口完整说明见 [投注玩法说明.md](./投注玩法说明.md)。
### 联赛
| 代码 | 名称 |