feat: 前台匿名浏览、登录引导、客服入口与返水增强

前台:
- 未登录可浏览首页/赛事/赔率,下注等操作弹出登录引导(去登录/继续浏览)
- 顶部新增客服入口与 iframe 弹窗
- 登录页支持暂不登录返回浏览

API:
- 首页/赛事/冠军盘接口改为公开访问,支持 X-Locale 头
- JWT 守卫支持可选认证

返水:
- 注单新增 is_cashbacked 字段,发放时自动标记
- 预览展示玩家余额,明确平台直发不从代理扣款
- 后台注单列表与玩家历史展示回水状态

其他:
- 串关禁止同场重复选号(SAME_MATCH)
- 补充结算资金流分析文档

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 09:36:44 +08:00
parent 785fa4416d
commit 844727c82e
35 changed files with 1007 additions and 49 deletions

View File

@@ -0,0 +1,272 @@
## 赛事结算全流程与资金链路分析报告
---
### 一、赛事生命周期
比赛经历以下状态流转:
```
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 元 |