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

206 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 幸运礼物重构第二步:抽奖引擎与模拟器
本文档定义幸运礼物重构第二步。第二步基于第一步的新配置契约,实现 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但还不直接替换线上送礼主链路。