- 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路 - 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请 - 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等 - 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口 - 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新 - 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
15 KiB
结算与返水金额规则
本文档为 Reference(参考):只描述金额如何计算,不涉及管理端操作步骤。各玩法定义与判赢规则见 投注玩法说明.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 }
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)
- 结算某一场比赛时,只更新该场对应腿的
resultStatus。 - 若其他场腿尚无结果 → 注单仍为 PENDING,此时无最终
payout。 - 任一脚在本场结算为 LOSE 且其余腿已有结果 → 整单 LOST,
payout = 0。 - 全部腿均有结果后 → 调用
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 = 50(stake=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_WINdelta < 0→ 流水 RESETTLE_REVERSE(可扣至负余额)- 注单
settlementStatus = RESETTLED,actualReturn更新为新 payout
端到端钱包示例
| 用例 ID | 流程 | 钱包变化 | actualReturn |
|---|---|---|---|
| BF001 | 单关赢 100@2.0,2-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 维护与回归命令
# 打印与本文一致的金额对照表
pnpm --filter @thebet365/api audit:settlement
# 结算引擎单测(含 S016–S019 串关)
pnpm --filter @thebet365/api exec jest settlement-calculator.spec.ts --runInBand
# 全量 smoke(含 S009–S019、CB001–CB004、BF001–BF005)
pnpm test:smoke
修改 settlement-calculator.ts、cashback-rate.resolver.ts 或 smoke 期望金额时,须同步更新本文档并重新运行上述命令。
10.4 Smoke / 单测用例索引
| 套件 | ID 范围 | 内容 |
|---|---|---|
| settlement | S009–S019 | 让球/大小/串关派彩数值 |
| settlement | S011B | HALF_WIN 系数 |
| cashback | CB001–CB004 | 费率解析 |
| bet-flow | BF001–BF005 | 钱包 + 代理额度端到端 |
附录:支持的 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)。