前台: - 未登录可浏览首页/赛事/赔率,下注等操作弹出登录引导(去登录/继续浏览) - 顶部新增客服入口与 iframe 弹窗 - 登录页支持暂不登录返回浏览 API: - 首页/赛事/冠军盘接口改为公开访问,支持 X-Locale 头 - JWT 守卫支持可选认证 返水: - 注单新增 is_cashbacked 字段,发放时自动标记 - 预览展示玩家余额,明确平台直发不从代理扣款 - 后台注单列表与玩家历史展示回水状态 其他: - 串关禁止同场重复选号(SAME_MATCH) - 补充结算资金流分析文档 Co-authored-by: Cursor <cursoragent@cursor.com>
11 KiB
11 KiB
赛事结算全流程与资金链路分析报告
一、赛事生命周期
比赛经历以下状态流转:
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)
串关跨多场比赛,结算逻辑更复杂:
- 结算某场比赛时:只计算该场比赛中涉及的"腿"(leg),其他场次的腿等待后续结算
- 所有腿都有结果后:按串关规则合并计算总赔率
- 任一腿为 LOSE:整单作废,派彩 = 0
- 全部腿为 PUSH/VOID:整单走水,退还本金
- 混合结果:
- 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)系统
返水率解析优先级(从高到低)
- 玩家个人规则(targetType = USER)
- 代理规则(targetType = AGENT)
- 全局规则(targetType = GLOBAL)
- 代理默认 cashbackRate
返水流程
- 预览:加载周期内所有 WON/LOST 的注单,计算有效投注额 × 返水率
- 确认:逐笔调用
wallet.deposit()发放返水,交易类型CASHBACK_DEPOSIT - 取消:仅 PREVIEW 状态可取消
返水公式
返水金额 = 有效投注额 × 返水率
有效投注额 = 周期内所有已结算(WON/LOST)注单的投注金额之和
九、安全与并发控制
| 机制 | 说明 |
|---|---|
| 行级锁 | 每次钱包操作使用 SELECT ... FOR UPDATE,PostgreSQL 悲观锁串行化 |
| 幂等性 | 注单表有 UNIQUE(userId, requestId) 约束,重复提交直接返回已有注单 |
| 乐观版本 | Wallet.version 每次变动递增,提供审计轨迹 |
| 事务完整性 | 下注+冻结在同一事务;一场比赛所有注单结算在同一事务 |
| 余额校验 | 下注前检查 availableBalance ≥ stake,提现前检查 availableBalance ≥ amount |
| 重新结算 | delta 可为负数,允许追扣(余额可能变负) |
十、重新结算(纠错机制)
当管理员发现比分录入错误时:
- 预览重新结算:用新比分重新计算所有注单结果,对比新旧派彩差额
- 确认重新结算:
- 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 元 |