# 幸运礼物重构第二步:抽奖引擎与模拟器 本文档定义幸运礼物重构第二步。第二步基于第一步的新配置契约,实现 v2 抽奖引擎和真实配置模拟器。该步骤不兼容旧抽奖逻辑,不再使用“RTP 缺口直接压缩成 1 到 2 个倍率候选”的模型。 ## 目标 | 目标 | 说明 | | --- | --- | | 体验概率表为主 | 用户看到的倍率分布首先来自后台阶段概率表 | | RTP 控制为约束 | RTP 只做模式切换、权重调整和硬边界过滤 | | 奖池不透支 | platform / room / gift 三层基础池都不能低于 reserve | | 风控不后置 | 用户、设备、房间、主播上限必须在随机前过滤 | | 完整审计 | 每次抽奖保存候选、过滤、权重、随机落点和快照 | | 模拟先行 | 后台发布前能用同一套规则跑用户体验和成本模拟 | ## 新抽奖原则 | 原则 | 说明 | | --- | --- | | 先候选后过滤 | 先按阶段概率表生成候选,再按奖池/RTP/风控过滤 | | 先过滤后随机 | 不能随机后发现超预算再改结果 | | 正常模式不用强纠偏 | 实际 RTP 在波动带内时按基础概率表抽 | | 纠偏只调权重 | `boost/cooldown` 只调整权重,不直接指定结果 | | 高倍要水位 | 高倍必须满足 RTP headroom 和三层池容量 | | 补贴分口径 | 活动补贴和房间气氛不计入基础 RTP | ## 控制模式 抽奖前根据当前窗口计算 `control_mode`: ```text 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 | 候选计算: ```text reward_coins = floor(coin_spent * multiplier_ppm / 1000000) ``` 候选结构: ```json { "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` | 调整后重新归一化: ```text 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` | 后台告警,禁止继续发布该规则 | ## 随机与审计 随机使用安全随机: ```text 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,不再使用写死的倍率表。 输入: ```bash 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_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,但还不直接替换线上送礼主链路。