前台: - 未登录可浏览首页/赛事/赔率,下注等操作弹出登录引导(去登录/继续浏览) - 顶部新增客服入口与 iframe 弹窗 - 登录页支持暂不登录返回浏览 API: - 首页/赛事/冠军盘接口改为公开访问,支持 X-Locale 头 - JWT 守卫支持可选认证 返水: - 注单新增 is_cashbacked 字段,发放时自动标记 - 预览展示玩家余额,明确平台直发不从代理扣款 - 后台注单列表与玩家历史展示回水状态 其他: - 串关禁止同场重复选号(SAME_MATCH) - 补充结算资金流分析文档 Co-authored-by: Cursor <cursoragent@cursor.com>
273 lines
11 KiB
Markdown
273 lines
11 KiB
Markdown
## 赛事结算全流程与资金链路分析报告
|
||
|
||
---
|
||
|
||
### 一、赛事生命周期
|
||
|
||
比赛经历以下状态流转:
|
||
|
||
```
|
||
DRAFT → PUBLISHED → CLOSED → PENDING_SETTLEMENT → SETTLED
|
||
↓
|
||
CANCELLED(取消,全部退款)
|
||
```
|
||
|
||
| 状态转换 | 触发方式 |
|
||
|---------|---------|
|
||
| DRAFT → PUBLISHED | 管理员发布比赛 |
|
||
| PUBLISHED → CLOSED | 每分钟自动 cron 检测开赛时间到达,或管理员手动关闭 |
|
||
| CLOSED → PENDING_SETTLEMENT | 管理员录入比分 |
|
||
| PENDING_SETTLEMENT → SETTLED | 管理员确认结算 |
|
||
| 任意 → CANCELLED | 管理员取消比赛,所有未结算注单全额退款 |
|
||
|
||
玩家端看到三种简化状态:`open`(可投注)、`closed_pending`(封盘待结算)、`settled`(已结算)。
|
||
|
||
---
|
||
|
||
### 二、结算操作流程(三步走)
|
||
|
||
结算完全由管理员手动触发,分三个阶段:
|
||
|
||
**第一步:录入比分**
|
||
- 接口:`POST /admin/matches/:id/settlement/score`
|
||
- 管理员输入全场比分(主队/客队半场和全场进球),冠军盘只需选择获胜队伍
|
||
- 系统创建 `MatchScore` 记录,比赛状态变为 `PENDING_SETTLEMENT`
|
||
|
||
**第二步:预览结算**
|
||
- 接口:`POST /admin/matches/:id/settlement/preview`
|
||
- 系统自动计算所有该场次的 PENDING 注单结果(赢/输/走水/半赢/半输)
|
||
- 创建 `SettlementBatch`(状态 PREVIEW),汇总总注单数、总派彩、总退款
|
||
- 管理员可分页查看每笔注单的结算预览
|
||
|
||
**第三步:确认结算**
|
||
- 接口:`POST /admin/settlement/:batchId/confirm`
|
||
- 在单个数据库事务中:逐笔结算、更新钱包、标记比赛为 SETTLED
|
||
- 结算后触发所有相关代理的信用额度重算
|
||
|
||
---
|
||
|
||
### 三、单关 vs 串关结算差异
|
||
|
||
#### 单关投注(Single Bet)
|
||
|
||
每笔注单只有一个选项,直接根据比分计算结果:
|
||
|
||
| 结果 | 派彩公式 |
|
||
|------|---------|
|
||
| WIN(全赢) | stake × odds |
|
||
| HALF_WIN(半赢) | stake/2 × odds + stake/2 |
|
||
| PUSH(走水) | stake(全额退还) |
|
||
| HALF_LOSE(半输) | stake / 2 |
|
||
| LOSE(全输) | 0 |
|
||
|
||
#### 串关投注(Parlay Bet)
|
||
|
||
串关跨多场比赛,结算逻辑更复杂:
|
||
|
||
1. **结算某场比赛时**:只计算该场比赛中涉及的"腿"(leg),其他场次的腿等待后续结算
|
||
2. **所有腿都有结果后**:按串关规则合并计算总赔率
|
||
3. **任一腿为 LOSE**:整单作废,派彩 = 0
|
||
4. **全部腿为 PUSH/VOID**:整单走水,退还本金
|
||
5. **混合结果**:
|
||
- WIN:乘以该腿赔率
|
||
- HALF_WIN:乘以 `(odds + 1) / 2`
|
||
- HALF_LOSE:乘以 `0.5`
|
||
- PUSH/VOID:乘以 `1.0`(跳过)
|
||
- 最终派彩 = stake × 所有腿赔率的连乘
|
||
|
||
#### 亚盘让球/大小球(Quarter Line)
|
||
|
||
对于 `.25` 或 `.75` 盘口,系统拆分为两条半盘独立结算,再合并:
|
||
- 两半都赢 → WIN
|
||
- 两半都输 → LOSE
|
||
- 一赢一平 → HALF_WIN
|
||
- 一输一平 → HALF_LOSE
|
||
- 一赢一输 → PUSH
|
||
|
||
---
|
||
|
||
### 四、支持的盘口类型
|
||
|
||
| 盘口类型 | 结算依据 |
|
||
|---------|---------|
|
||
| FT_1X2(全场胜平负) | 全场比分 |
|
||
| HT_1X2(半场胜平负) | 半场比分 |
|
||
| FT_ODD_EVEN(全场单双) | 全场总进球奇偶(0-0 = 双) |
|
||
| FT_HANDICAP(全场让球) | 全场比分 + 让球盘 |
|
||
| HT_HANDICAP(半场让球) | 半场比分 + 让球盘 |
|
||
| FT_OVER_UNDER(全场大小) | 全场总进球 + 大小盘 |
|
||
| HT_OVER_UNDER(半场大小) | 半场总进球 + 大小盘 |
|
||
| FT_CORRECT_SCORE(波胆) | 精确比分匹配 |
|
||
| HT_CORRECT_SCORE(半场波胆) | 半场精确比分 |
|
||
| OUTRIGHT_WINNER(冠军) | 获胜队伍编号 |
|
||
|
||
---
|
||
|
||
### 五、资金链路全景图
|
||
|
||
```
|
||
充值(Admin/Agent → 玩家)
|
||
┌──────────────────────────────┐
|
||
│ availableBalance += 金额 │
|
||
│ 流水: MANUAL_DEPOSIT (+) │
|
||
│ Agent.usedCredit 重算 │
|
||
└──────────────────────────────┘
|
||
│
|
||
▼
|
||
下注(玩家 → 系统)
|
||
┌──────────────────────────────┐
|
||
│ availableBalance -= stake │
|
||
│ frozenBalance += stake │
|
||
│ 流水: BET_FREEZE (-stake) │
|
||
│ 注单状态: PENDING │
|
||
└──────────────────────────────┘
|
||
│
|
||
┌───────────────┼───────────────┐
|
||
▼ ▼ ▼
|
||
赢了 (WIN) 输了 (LOSE) 走水 (PUSH)
|
||
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
|
||
│ frozen -= stk│ │ frozen -= stk│ │ frozen -= stk │
|
||
│ avail += stk×odds │ │ avail += 0 │ │ avail += stk │
|
||
│ 流水: BET_SETTLE_WIN │ │ 流水: BET_SETTLE_LOSE │ │ 流水: BET_SETTLE_PUSH │
|
||
│ 注单: WON │ │ 注单: LOST │ │ 注单: PUSH │
|
||
└──────────────┘ └──────────────┘ └──────────────────┘
|
||
|
||
提现(玩家 → Admin/Agent)
|
||
┌──────────────────────────────┐
|
||
│ availableBalance -= 金额 │
|
||
│ 流水: MANUAL_WITHDRAW (-) │
|
||
│ Agent.usedCredit 重算 │
|
||
└──────────────────────────────┘
|
||
|
||
返水/返佣(系统 → 玩家)
|
||
┌──────────────────────────────┐
|
||
│ availableBalance += 金额 │
|
||
│ 流水: CASHBACK_DEPOSIT (+) │
|
||
│ 金额 = 有效投注额 × 返水率 │
|
||
└──────────────────────────────┘
|
||
|
||
重新结算(比分纠正)
|
||
┌──────────────────────────────┐
|
||
│ availableBalance += delta │
|
||
│ delta = 新派彩 - 旧派彩 │
|
||
│ 流水: BET_SETTLE_WIN 或 │
|
||
│ RESETTLE_REVERSE │
|
||
│ (delta 可为负数,余额可变负) │
|
||
└──────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### 六、钱包模型
|
||
|
||
每个玩家有一个钱包,包含两个余额字段:
|
||
|
||
- **availableBalance**(可用余额):可自由使用的金额
|
||
- **frozenBalance**(冻结余额):已下注未结算的金额
|
||
|
||
每次余额变动都会创建一条 `WalletTransaction` 记录,包含变动前后的快照(balanceBefore/After, frozenBefore/After),形成完整的审计链。
|
||
|
||
#### 所有交易类型
|
||
|
||
| 类别 | 类型 | 方向 |
|
||
|------|------|------|
|
||
| 充值 | MANUAL_DEPOSIT | 入账 (+) |
|
||
| 提现 | MANUAL_WITHDRAW | 出账 (-) |
|
||
| 下注冻结 | BET_FREEZE | available → frozen |
|
||
| 结算赢 | BET_SETTLE_WIN | frozen 释放 + 派彩入账 |
|
||
| 结算输 | BET_SETTLE_LOSE | frozen 释放,无入账 |
|
||
| 结算走水 | BET_SETTLE_PUSH | frozen 释放 + 退还本金 |
|
||
| 比赛取消退款 | BET_VOID_REFUND | frozen 释放 + 退还本金 |
|
||
| 重新结算调整 | RESETTLE_REVERSE | delta 调整(可正可负) |
|
||
| 返水 | CASHBACK_DEPOSIT | 入账 (+) |
|
||
|
||
---
|
||
|
||
### 七、代理信用体系
|
||
|
||
代理(Agent)没有真实钱包,采用**信用额度**模型:
|
||
|
||
```
|
||
可用信用 = creditLimit - usedCredit
|
||
```
|
||
|
||
其中 `usedCredit = directPlayerLiability + childAgentExposure`:
|
||
- `directPlayerLiability`:所有直属玩家的(可用余额 + 冻结余额)之和
|
||
- `childAgentExposure`:所有子代理的 max(creditLimit, usedCredit) 之和
|
||
|
||
#### 约束规则
|
||
- 子代理的 creditLimit 不能超过父代理
|
||
- 子代理的 cashbackRate 不能超过父代理
|
||
- 给玩家充值前,检查代理可用信用 ≥ 充值金额
|
||
- 每次充值/提现/结算后都会重算代理信用
|
||
|
||
---
|
||
|
||
### 八、返水(Cashback)系统
|
||
|
||
#### 返水率解析优先级(从高到低)
|
||
1. 玩家个人规则(targetType = USER)
|
||
2. 代理规则(targetType = AGENT)
|
||
3. 全局规则(targetType = GLOBAL)
|
||
4. 代理默认 cashbackRate
|
||
|
||
#### 返水流程
|
||
1. **预览**:加载周期内所有 WON/LOST 的注单,计算有效投注额 × 返水率
|
||
2. **确认**:逐笔调用 `wallet.deposit()` 发放返水,交易类型 `CASHBACK_DEPOSIT`
|
||
3. **取消**:仅 PREVIEW 状态可取消
|
||
|
||
#### 返水公式
|
||
```
|
||
返水金额 = 有效投注额 × 返水率
|
||
```
|
||
有效投注额 = 周期内所有已结算(WON/LOST)注单的投注金额之和
|
||
|
||
---
|
||
|
||
### 九、安全与并发控制
|
||
|
||
| 机制 | 说明 |
|
||
|------|------|
|
||
| 行级锁 | 每次钱包操作使用 `SELECT ... FOR UPDATE`,PostgreSQL 悲观锁串行化 |
|
||
| 幂等性 | 注单表有 `UNIQUE(userId, requestId)` 约束,重复提交直接返回已有注单 |
|
||
| 乐观版本 | `Wallet.version` 每次变动递增,提供审计轨迹 |
|
||
| 事务完整性 | 下注+冻结在同一事务;一场比赛所有注单结算在同一事务 |
|
||
| 余额校验 | 下注前检查 availableBalance ≥ stake,提现前检查 availableBalance ≥ amount |
|
||
| 重新结算 | delta 可为负数,允许追扣(余额可能变负) |
|
||
|
||
---
|
||
|
||
### 十、重新结算(纠错机制)
|
||
|
||
当管理员发现比分录入错误时:
|
||
|
||
1. **预览重新结算**:用新比分重新计算所有注单结果,对比新旧派彩差额
|
||
2. **确认重新结算**:
|
||
- delta > 0:补发差额(BET_SETTLE_WIN)
|
||
- delta < 0:追扣差额(RESETTLE_REVERSE),余额可能变负
|
||
- 注单标记 `settlementStatus = RESETTLED`
|
||
- 代理信用重算
|
||
|
||
---
|
||
|
||
### 十一、典型流程示例
|
||
|
||
**示例:玩家投注主胜 100 元,赔率 2.0**
|
||
|
||
| 步骤 | availableBalance | frozenBalance | 说明 |
|
||
|------|-----------------|---------------|------|
|
||
| 初始 | 1,000 | 0 | - |
|
||
| 下注 | 900 | 100 | BET_FREEZE |
|
||
| 结算(赢) | 1,100 | 0 | BET_SETTLE_WIN: +200 |
|
||
| 净收益 | +100 | - | 派彩 200 - 本金 100 |
|
||
|
||
**示例:串关 3 串 1,本金 100 元,赔率 2.0 × 3.0 × 1.5 = 9.0**
|
||
|
||
| 步骤 | 说明 |
|
||
|------|------|
|
||
| 下注 | 冻结 100 元 |
|
||
| 第一场结算(赢) | 该腿 WIN,注单仍 PENDING |
|
||
| 第二场结算(输) | 该腿 LOSE,整单 LOST,派彩 = 0 |
|
||
| 第三场 | 无需结算,注单已终结 |
|
||
| 结果 | 冻结释放,派彩 0,玩家亏损 100 元 |
|