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