7.0 KiB
7.0 KiB
幸运礼物重构第二步:抽奖引擎与模拟器
本文档定义幸运礼物重构第二步。第二步基于第一步的新配置契约,实现 v2 抽奖引擎和真实配置模拟器。该步骤不兼容旧抽奖逻辑,不再使用“RTP 缺口直接压缩成 1 到 2 个倍率候选”的模型。
目标
| 目标 | 说明 |
|---|---|
| 体验概率表为主 | 用户看到的倍率分布首先来自后台阶段概率表 |
| RTP 控制为约束 | RTP 只做模式切换、权重调整和硬边界过滤 |
| 奖池不透支 | platform / room / gift 三层基础池都不能低于 reserve |
| 风控不后置 | 用户、设备、房间、主播上限必须在随机前过滤 |
| 完整审计 | 每次抽奖保存候选、过滤、权重、随机落点和快照 |
| 模拟先行 | 后台发布前能用同一套规则跑用户体验和成本模拟 |
新抽奖原则
| 原则 | 说明 |
|---|---|
| 先候选后过滤 | 先按阶段概率表生成候选,再按奖池/RTP/风控过滤 |
| 先过滤后随机 | 不能随机后发现超预算再改结果 |
| 正常模式不用强纠偏 | 实际 RTP 在波动带内时按基础概率表抽 |
| 纠偏只调权重 | boost/cooldown 只调整权重,不直接指定结果 |
| 高倍要水位 | 高倍必须满足 RTP headroom 和三层池容量 |
| 补贴分口径 | 活动补贴和房间气氛不计入基础 RTP |
控制模式
抽奖前根据当前窗口计算 control_mode:
actual_rtp_ppm = actual_base_payout * 1000000 / wager_coins
lower = target_rtp_ppm - control_band_ppm
upper = target_rtp_ppm + control_band_ppm
| 模式 | 触发条件 | 行为 |
|---|---|---|
normal |
lower <= actual_rtp_ppm <= upper |
使用基础概率表 |
boost |
actual_rtp_ppm < lower |
降低 0x,提高小奖和中低奖 |
cooldown |
actual_rtp_ppm > upper |
提高 0x,降低中高奖 |
high_lock |
高倍水位不足 | 过滤高倍,保留小奖 |
hard_lock |
基础小奖都不可支付 | 拒绝幸运抽奖,不允许先扣费后给不可解释结果 |
候选生成
输入:
| 输入 | 来源 |
|---|---|
pool_id |
room-service 请求 |
coin_spent |
wallet-service 扣费结果 |
stage |
用户在该 pool 下累计付费抽数 |
rule_version |
active lucky rule |
tier table |
第一阶段配置 |
rtp windows |
activity-service MySQL |
pools |
activity-service MySQL |
risk counters |
activity-service MySQL |
候选计算:
reward_coins = floor(coin_spent * multiplier_ppm / 1000000)
候选结构:
{
"tier_id": "normal_0_5x",
"multiplier_ppm": 500000,
"base_reward_coins": 50,
"source": "base_rtp",
"base_weight_ppm": 120000,
"adjusted_weight_ppm": 135000,
"enabled": true,
"filtered_reason": ""
}
权重调整
基础概率表是主体验,控制模式只能做有限调整。
建议第一版调整系数:
| 倍率区间 | boost |
normal |
cooldown |
|---|---|---|---|
0x |
0.70 |
1.00 |
1.30 |
0x < x <= 1x |
1.20 |
1.00 |
0.90 |
1x < x <= 5x |
1.10 |
1.00 |
0.60 |
5x+ |
1.00 |
1.00 |
0.30 |
调整后重新归一化:
adjusted_weight_ppm = adjusted_weight * 1000000 / sum(adjusted_weight)
如果某个奖档被过滤,它的权重可以进入 0x,但必须记录 filtered_reason。
过滤规则
随机前必须过滤:
| 过滤项 | 规则 |
|---|---|
| 单次上限 | reward_coins <= max_single_payout |
| 用户小时/日上限 | 不超过剩余额度 |
| 设备日上限 | 不超过剩余额度 |
| 房间小时上限 | 不超过剩余额度 |
| 主播日上限 | 不超过剩余额度 |
| 三层基础池 | platform / room / gift 扣减后不低于 reserve |
| RTP 上限 | cooldown 下过滤中高倍 |
| 高倍水位 | 高倍必须满足 high_water_only 条件 |
| 活动预算 | 补贴档不超过总预算和 UTC 日预算 |
过滤后如果只有 0x:
| 场景 | 行为 |
|---|---|
normal/cooldown 且配置允许 |
可继续抽,但审计记录“只剩 0x” |
boost 仍只剩 0x |
说明奖池或风控配置不健康,返回 hard_lock 或降级普通礼物 |
连续多次只剩 0x |
后台告警,禁止继续发布该规则 |
随机与审计
随机使用安全随机:
random_index = crypto_rand_int(sum(adjusted_weight))
每次 draw 必须保存:
| 字段 | 说明 |
|---|---|
candidate_table_json |
完整候选列表、调整前后权重、过滤原因 |
random_index |
随机落点 |
random_digest |
随机材料摘要 |
control_mode |
normal/boost/cooldown/high_lock/hard_lock |
rtp_snapshot_json |
抽前 RTP |
pool_snapshot_json |
抽前奖池 |
risk_snapshot_json |
抽前风控剩余额度 |
模拟器
模拟器必须读取第一步的真实配置 JSON,不再使用写死的倍率表。
输入:
go run ./tools/lucky-gift-v2-sim \
-config ./tmp/lucky_100_rule.json \
-users 10000 \
-rooms 200 \
-draws-min 0 \
-draws-max 1000 \
-gift-price 100 \
-seed 20260527
输出指标:
| 指标 | 说明 |
|---|---|
base_rtp |
基础返奖率 |
effective_rtp |
用户可见返奖率 |
hit_rate |
金币命中率 |
zero_rate |
0x 比例 |
zero_streak_p50/p95/p99 |
连续 0x 分布 |
tier_hit_rate |
各倍率命中率 |
control_mode_count |
各控制模式出现次数 |
pool_min_balance |
最低奖池水位 |
risk_filter_count |
风控过滤次数 |
user_net_p05/median/p95 |
用户净输赢体感 |
实施任务
- 新增 v2 抽奖引擎 package,和旧引擎隔离。
- 实现 stage 选择。
- 实现 control mode。
- 实现候选生成、过滤、权重调整和归一化。
- 实现完整审计落库。
- 实现 v2 模拟器读取真实配置。
- 用模拟器接入后台发布校验。
- 增加单元测试、并发事务测试和模拟快照测试。
验收标准
| 验收项 | 标准 |
|---|---|
| 正常模式 | RTP 在波动带内时,候选权重等于或接近基础概率表 |
| boost 模式 | RTP 低于下沿时,0x 权重下降,小奖权重上升 |
| cooldown 模式 | RTP 高于上沿时,中高奖权重下降 |
| 高倍过滤 | 高水位不足时,高倍不会进入随机候选 |
| 先过滤后随机 | 所有被随机选中的候选都必须可支付 |
| 完整审计 | candidate_table_json 可解释每个奖档是否可选、为何过滤、最终权重 |
| 随机可审计 | 每条 draw 有 random_index 和 random_digest |
| 小额体验 | 100 金币配置在模拟中不能长期只出 0x,zero_streak_p95 达到配置阈值 |
| RTP 收敛 | 100k 流水窗口内基础 RTP 偏差不超过配置波动带;1m 流水窗口内更稳定 |
| 奖池安全 | 任一模拟场景下三层池不低于 reserve |
| 测试 | go test ./services/activity-service/internal/service/luckygift ./services/activity-service/internal/storage/mysql ./tools/lucky-gift-v2-sim 通过 |
第二步完成后,v2 引擎可以离线模拟和 shadow draw,但还不直接替换线上送礼主链路。