hyapp-server/docs/幸运礼物重构第二步-抽奖引擎与模拟器.md
2026-05-27 22:12:16 +08:00

7.0 KiB
Raw Blame History

幸运礼物重构第二步:抽奖引擎与模拟器

本文档定义幸运礼物重构第二步。第二步基于第一步的新配置契约,实现 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 用户净输赢体感

实施任务

  1. 新增 v2 抽奖引擎 package和旧引擎隔离。
  2. 实现 stage 选择。
  3. 实现 control mode。
  4. 实现候选生成、过滤、权重调整和归一化。
  5. 实现完整审计落库。
  6. 实现 v2 模拟器读取真实配置。
  7. 用模拟器接入后台发布校验。
  8. 增加单元测试、并发事务测试和模拟快照测试。

验收标准

验收项 标准
正常模式 RTP 在波动带内时,候选权重等于或接近基础概率表
boost 模式 RTP 低于下沿时,0x 权重下降,小奖权重上升
cooldown 模式 RTP 高于上沿时,中高奖权重下降
高倍过滤 高水位不足时,高倍不会进入随机候选
先过滤后随机 所有被随机选中的候选都必须可支付
完整审计 candidate_table_json 可解释每个奖档是否可选、为何过滤、最终权重
随机可审计 每条 draw 有 random_indexrandom_digest
小额体验 100 金币配置在模拟中不能长期只出 0xzero_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但还不直接替换线上送礼主链路。