hyapp-server/docs/幸运礼物动态策略V3实现与模拟.md

307 lines
25 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.

# 幸运礼物动态策略 V3实现边界与模拟验收
## 1. 版本边界
本文只描述 `dynamic_v3` 的规则、持久化契约和可重复模拟口径,不改变已经发布的 `fixed_v2` 规则语义,也不代表该策略已在线上自动启用。
- `strategy_version=fixed_v2`:继续按历史不可变规则执行;旧请求或旧记录缺少 `strategy_version` 时必须归一为 `fixed_v2`
- `strategy_version=dynamic_v3`:只有显式发布并启用的规则才进入动态水位、充值加权、保底、大奖和六维风控。不能因为服务升级而把存量奖池隐式切换到 V3。
- 新奖池未配置时返回 `dynamic_v3``disabled` 草稿。各 App 必须先补齐累计消费大奖门槛、普通/高阶充值门槛及六项金额上限,才允许启用。
- 启用前还必须确认 user/gateway/room 已升级到由 `auth_sessions.device_id` 签入 access JWT再通过 `room.RequestMeta` 和可恢复 command 原样传递设备作用域。旧 JWT 缺 `device_id``fixed_v2` 仍兼容,`dynamic_v3` 必须 fail-close不得用 `session_id`/`command_id` 伪造设备。
- 启用前还必须确认 user/gateway/room/wallet 已按顺序升级access JWT 和 room meta 携带 auth session 绑定的可信 `device_id`wallet 回执携带交易 `paid_at_ms`、充值快照和 `gift_income_coins`。任一动态字段缺失或真实收礼返币比例与规则 `anchor_rate_ppm` 不一致时运行侧 fail-close。
- 比例、概率和倍率统一使用 ppm`1_000_000=100%=1x`;钱只使用整数金币;业务时间只使用 UTC epoch milliseconds。
- 离线模拟使用固定 seed 的 `math/rand`;生产抽奖必须注入不可预测的安全随机源,不能复用模拟 RNG。
## 2. 已确认的产品口径
### 2.1 资金拆分采用 98/1/1
每笔已成功扣费的幸运礼物按整数守恒拆分:
| 去向 | 默认比例 | 作用 |
| --- | ---: | --- |
| 公共奖金池 `P` | 98% | 在本抽开奖前先入池,承担用户返奖 |
| 平台盈利 | 1% | 只记盈利拆账,不进入可返奖余额 |
| 收礼主播收益 | 1% | 规则计算值必须与钱包已落账回执完全一致,不允许缺字段或其他返币比例静默覆盖规则 |
原图同一行同时出现“抽取 98%”和“92% 可灵活配置”两者冲突。V3 采用能够与 1%+1% 合计为 100% 的 **98/1/1**“92%”视为原图笔误,不进入默认配置、代码或模拟。比例可发布为其他值时,三项仍必须非负且合计 100%,不允许出现去向不明的余额。
公共池与主播份额分别向下取整,无法再拆的整币余数归平台盈利,任何价格都满足:
```text
public_pool_coins + profit_coins + anchor_return_coins = coin_spent
```
### 2.2 RTP 是观察目标,不是透支承诺
- 发布目标为 98%,正常观察带为 95%101%98%±3 个百分点)。
- RTP 分子是已经开奖形成的不可撤销返奖负债,分母是对应已扣费流水;分母为 0 时是“无有效样本”,不能当成 0% 去触发补偿大奖。
- 95%101% 是资金充足且样本量足够时的运营观察目标,不是每抽、每用户、每窗口的硬保证。
- 当奖池、单次限额或任一风控窗口不足时,安全约束优先;允许窗口 `underpaid` 或短期偏离 95%101%,禁止为了追 RTP 透支奖池。
- `settlement_window_wager` 是当前不可变规则版本的真实金币流水阈值。单抽整体计入它开始时所在窗口;若本抽使累计流水达到或超过阈值,下一抽才开启新窗口。`gift_price_reference` 只用于体验阶段的等价抽数,不参与 V3 滚窗。
### 2.3 原图概率表不能作为生产默认概率
原图示例为:
| 倍率 | 概率 | EV 贡献 |
| ---: | ---: | ---: |
| 0x | 60% | 0x |
| 5x | 20% | 1x |
| 50x | 15% | 7.5x |
| 200x | 4% | 8x |
| 1000x | 1% | 10x |
| 合计 | 100% | **26.5x** |
因此该表的名义 `EV=26.5x`,即 `RTP=2650%`,不可能同时是 98% RTP。它只用于验证原图的 `P/W` 删档、删 0 和重抽分支,不能写入生产默认基础奖档。
`dynamic_v3` 的安全发布草稿采用独立的 98% 静态 EV`0x@5% + 0.5x@4% + 1x@86% + 2x@5% = 0.98x``200x/500x/1000x` 作为大奖集合单独配置,不能在缺少经过成本校验的概率时照搬进基础概率表。
## 3. 一次抽奖的确定顺序
一次真实抽奖按以下顺序完成;同一事务内锁住的状态必须与 draw record 一起提交:
1. `user-service``auth_sessions.device_id` 签入 access JWTgateway 验签后写入 room `RequestMeta`room saga 固化该设备快照,禁止用可轮换的 session 或 command ID 代替。
2. `wallet-service` 完成真实扣费,并在幂等回执中固化 `coin_spent`、交易 `paid_at_ms`、最近 7/30 个 UTC 自然日充值额、最后充值时间及主播收益金额。
3. `lucky-gift-service` 只消费上述可信事实不直连钱包库不按当前余额、IP、服务时区、恢复时间或客户端自报值推断充值画像和时间归属。
4. 本抽金币按 98/1/1 拆分;公共池份额先计入 `P`,再比较本抽奖金 `W`
5.`app_code + pool_id + rule_version` 读取不可变规则锁定公共池、用户日状态、72 小时桶/边界事件、连 0 状态和六维风控计数。
6. 用钱包快照选择充值层级,再对所有非 0 基础档应用水位和最近充值因子0 档接收剩余概率。
7. 优先处理已持久化的“消费里程碑大奖 token”其次判断 RTP 补偿大奖,最后进入普通概率抽奖。
8. 对选中的 `W` 依次执行奖池、单用户日大奖次数和六维风控检查;任何一项不满足都不能发奖。
9. 写入抽奖事实、决策快照、奖池/窗口/风控/用户状态及 outbox。钱包返奖状态独立收敛为 `pending/granted/failed`
这套顺序的核心约束是:先有已落账的扣费事实,再入池,再开奖;任何体验规则都不能制造不存在的资金。
## 4. 动态概率、分层和保底
### 4.1 充值分层
V3 固定有 `novice/normal/advanced` 三层,但两个门槛由每个不可变规则版本配置:
- 最近 7 日和最近 30 日均按 UTC 自然日统计且包含当天;分别覆盖“今天及前 6 个 UTC 日”和“今天及前 29 个 UTC 日”,上界为钱包扣费时刻。
- 用户只在同时满足某层的 7 日和 30 日最低充值门槛时才进入该层;不能只满足其中一维就越级。
- `novice` 是无条件兜底层,它的 `0/0` 只是协议和存储 sentinel不表示“只有充值为 0 才是新手”。若 `7d < normal_7d` **或** `30d < normal_30d`,即使两个充值值都非零,用户仍属于 `novice`
- `normal` 启用前至少一维必须为正数;`advanced` 两维均不得低于 `normal`且至少一维严格更高。disabled 草稿可保留 `0/0` 表示尚未配置,但不能以该状态启用。
- 阶段阈值相等时算已达到,即使用 `>=`;两维都等于普通门槛时进入 `normal`。所有金额门槛按 App 金币口径显式发布。
充值快照由 `wallet-service` 在礼物扣费事务中查询并写入交易 metadata同一 `command_id` 重放返回首次快照,避免重试时跨层或丢失五分钟加成。
### 4.2 水位和最近充值因子
设某层的非 0 档基础权重为 `base_weight`,则:
```text
adjusted_weight = base_weight × water_factor × recharge_factor
```
因子是相对乘法,不是把百分点直接相加:
| 条件 | 默认因子 | 边界 |
| --- | ---: | --- |
| `P < low_watermark` | 0.70 | 恰等于低水位时不下调 |
| `low_watermark <= P <= high_watermark` | 1.00 | 两个端点都属于中水位 |
| `P > high_watermark` | 1.30 | 恰等于高水位时不上调 |
| 有可信充值事实且处于充值后窗口 | 1.10 | `[last_recharged_at_ms, last_recharged_at_ms + 300000)` |
例如低水位并处于充值后五分钟时,非 0 权重为 `base × 0.70 × 1.10 = base × 0.77`,不是 `base × 0.80`。五分钟窗口包含起点和第 `299999ms`,不包含第 `300000ms`;未来时间、缺失充值事实或 `last_recharged_at_ms=0` 都不加权。
调整后:若正奖权重和不超过 100%0 档权重取剩余值;若正奖权重和超过 100%,按相同比例归一化正奖权重并把 0 档置为 0。该归一化只解决概率守恒不绕过资金和风控。
### 4.3 连续 5 次 0 后的第 6 抽
- `loss_streak` 只在最终可见返奖为 0 时加一,任意正返奖后清零。
- 前 5 次连续为 0 后,第 6 抽从普通候选中排除 0 档,选择可支付的非 0 档。
- “第 6 抽不出 0”是有资金、有剩余额度时的体验保证如果所有正奖都超过 `P` 或六维风控剩余额度,仍返回 0并记录 `miss_protection_blocked_no_payable_tier`。不得透支来兑现保底。
## 5. `P/W` 安全重抽
`P` 是本抽入池后的公共奖金池可用余额,`W=gift_price × multiplier`。普通抽奖执行:
1. 第一次按当前动态权重抽档。
2. `W <= P` 且所有风控均允许时直接支付;`W == P` 可以支付,支付后 `P=0`
3.`W > P`,同时删除该中奖档和 0 档,再对剩余正奖档重抽。
4. 重抽仍有 `W > P` 时继续删除该中奖档0 档只在第一次资金不足时删除一次。
5. 若最终没有可支付的正奖档,安全回退 0不改变 `P` 为负数。
原图 `P=800`、单价 10 的脚本化示例只用于分支验收:先抽到 `200x(W=2000)` 时删除 `200x+0x`;若再抽到 `1000x(W=10000)`,再删除 `1000x`;随后只能在仍有权重且 `W<=800` 的正奖中选择。该示例证明重抽语义,不证明原图概率具有可用 RTP。
奖池不足、风险额度不足和个人大奖次数达到上限是不同的审计原因。只有 `W>P` 触发原图规定的“删中奖档并同时删 0”其他风控拒绝不能伪装成奖池不足。
## 6. 两种大奖机制
大奖集合由 `jackpot_multiplier_ppms` 配置,默认产品集合为 `200x/500x/1000x`。原图没有给出三个大奖之间的概率,因此当前不可变规则明确采用“在可支付集合中等权随机”;这不属于普通基础概率。若产品需要非等权,必须先新增显式权重契约和成本验收,不能从普通奖档概率猜算。
### 6.1 机制一RTP 亏损补偿大奖
本抽开始时同时满足以下条件,才允许从当前可支付大奖集合随机选择:
- 全局结算 RTP `<= jackpot_global_rtp_max_ppm`,默认 98%
- 用户当前 UTC 日 RTP `<= jackpot_user_day_rtp_max_ppm`,默认 96%
- 用户滚动 72 小时 RTP `<= jackpot_user_72h_rtp_max_ppm`,默认 96%
- 用户本 UTC 日大奖次数 `< max_jackpot_hits_per_user_day`,默认 5
- 候选 `W<=P` 且不超过六维风控的最小剩余额度。
任一 RTP 分母为 0、任一 RTP 高于阈值、奖池不足或风控不足,都只回到普通抽奖,不强发大奖。滚动 72 小时是 `[now_ms-72h, now_ms)` 的 UTC 业务窗口,不能替换成“最近 3 个本地自然日”。
### 6.2 机制二:消费里程碑大奖
- `jackpot_spend_threshold_coins` 是当前不可变规则版本动态配置的“用户 UTC 日累计消费金币门槛”。它不绑定任何固定法币金额,服务也不读取实时汇率推断。
- 用户 UTC 日累计消费每跨过一个完整门槛,持久化一个大奖 token同一流水重试不能重复发 token。消费进度和当日命中次数在 UTC 日切换时归零,但已赚到且因资金不足未消费的 token 保存在用户/奖池状态中,可跨日等待下一抽。
- 当前抽跨过的门槛只能影响下一抽,不能回头改变已经完成的当前抽。
- 下一抽优先消费已有 token若大奖集合因 `P`、日 5 次上限或风控不可支付token 保留,用户仍获得本次普通抽奖。
- 同一抽同时满足机制一和机制二时,已承诺的里程碑 token 优先;一个 draw 最多结算一个最终奖档。
原图写的是“定时任务”。实现改为在 owner 事务里按已结算送礼事实同步计算跨过的整数门槛并落 token结果仍只影响下一抽但不会出现“用户已经完成门槛、下一抽先于 cron 扫描到达”的竞态,也不需要 cron-service 回扫明细或直接拥有大奖状态。这个事件驱动实现是有意的工程等价替换;若产品需要固定延迟发放,必须另增明确生效时间字段,而不是依赖不确定的扫描周期。
## 7. 六维风控与时间口径
所有启用的 `dynamic_v3` 规则必须配置正数上限:
| 维度 | 配置字段 | UTC 窗口 |
| --- | --- | --- |
| 单次返奖 | `max_single_payout` | 当前 draw |
| 用户小时 | `user_hourly_payout_cap` | 当前 UTC 小时 |
| 用户日 | `user_daily_payout_cap` | 当前 UTC 自然日 |
| 设备日 | `device_daily_payout_cap` | 当前 UTC 自然日 |
| 房间小时 | `room_hourly_payout_cap` | 当前 UTC 小时 |
| 主播日 | `anchor_daily_payout_cap` | 当前 UTC 自然日 |
一次候选的有效容量是 `P` 和六项剩余额度的最小值。候选金额等于剩余额度时允许,超过任一维度即拒绝;计数更新与 draw record 必须在同一锁定事务内,避免并发抽奖分别看到同一份余额。
设备日 scope 只接受 user-service 已落库 session 的 `device_id`,由签名 JWT 贯通到 room 和 lucky旧 token/旧 command 缺字段时 `fixed_v2` 可继续重放,但 `dynamic_v3` 必须拒绝开奖,不能回退到 `session_id` 或每单变化的 `command_id`。外部 App 的 device ID 由已认证调用方负责稳定提供V3 缺失同样拒绝;服务只保存 App 内稳定 hash避免暴露原始标识。
所有时间范围遵守 `[start_ms,end_ms)`包含开始毫秒不包含结束毫秒。UTC 日、UTC 小时、充值 5 分钟和滚动 72 小时都不得使用 `time.Local`、容器时区或客户端时区切分。
## 8. 批量 N 次的语义
`gift_count=N` 表示按顺序执行 N 次抽奖,不是把总金额当成一次抽奖,也不是“只开奖一次但保证中奖”。例如 `gift_count=99` 必须产生 99 次状态推进。
-`coin_spent` 拆成 N 个整数单份,余数从前往后每份加 1三资金桶使用联合累计配额保证每一抽 `public_i+profit_i+anchor_i=unit_spent_i`,同时整批三桶精确等于钱包回执总拆账。
-`i` 抽看到第 `i-1` 抽后的 `P`、连 0、RTP、token、日大奖次数和风控计数不能并行抽 N 次后再合并。
- 若第 `i` 抽使当前 RTP 窗口真实累计流水达到或超过 `settlement_window_wager`,第 `i+1` 抽必须先滚到新窗口;不能按 `ceil(window_wager/gift_price_reference)` 折算抽数,也不能把整批强塞进旧窗口后再一次性关窗。
- 每个子抽用稳定子幂等键(现有约定为 `command_id#序号`)保留独立 draw record重放整批不能跳过、补写或重复支付其中一抽。
- 一批内共享状态只锁一次并按顺序在内存推进,最终批量持久化;性能优化不能改变顺序语义。
- 聚合钱包返奖和房间表现可以按送礼命令合并,但审计仍保留 N 条单抽事实。
外部 App 的 `/lucky-gifts/send` 也遵守同一语义:`dynamic_v3` 逐次执行最多 999 抽、写 `external_lucky_gift_draw_items`,最后只把总奖励聚合返回;外部调用方自行入账,因此不生成 HyApp wallet 返奖 outbox。外部 `dynamic_v3` 必须提交稳定 `device_id` 和真实扣费 `paid_at_ms`,同设备多账号共享设备日上限;但 luck-gateway 目前只有 App allowlist不能独立证明设备真实性所以调用方必须在自身认证/请求签名边界内绑定这些值owner 不得用 `external_user_id``request_id` 或请求到达时间兜底。当前外部协议没有可验证的充值 owner 快照,运行侧明确按 `novice` 且不加最近充值因子,不能从 `metadata` 猜充值额。`fixed_v2` 外部请求继续保持缺少新事实的历史兼容路径。
## 9. 接口和配置字段
### 9.1 事实输入
`wallet.v1.DebitGiftResponse` 新增以下幂等回执字段:
- `recharge_seven_day_coins`
- `recharge_thirty_day_coins`
- `last_recharged_at_ms`
- `paid_at_ms`
`luckygift.v1.LuckyGiftMeta` 接收:
- `device_id`(从已验签 access JWT 穿过 room command 恢复链路)
- `paid_at_ms`(逐目标 wallet transaction 的稳定创建时间)
- `recharge_7d_coins`
- `recharge_30d_coins`
- `last_recharged_at_ms`
- `gift_income_coins`
`room.v1.RequestMeta.device_id` 只由 gateway 从验签 JWT 的 `device_id` claim 写入;`ExternalGiftDrawRequest.device_id` 由外部认证调用方承担稳定性责任。V3 对两个入口都不提供 session/command fallback。其余结算字段只来自 wallet owner 的成功回执。`request_id` 只做链路追踪;抽奖幂等仍使用 `command_id`
### 9.2 不可变规则字段
`LuckyGiftRuleConfig` / `lucky_gift_rule_versions` 的 V3 字段分组如下:
| 决策 | 字段 |
| --- | --- |
| 版本和目标 | `strategy_version`, `target_rtp_ppm`, `settlement_window_wager`, `control_band_ppm`, `gift_price_reference` |
| 资金拆分 | `pool_rate_ppm`, `profit_rate_ppm`, `anchor_rate_ppm`, `initial_pool_coins` |
| 保底和水位 | `loss_streak_guarantee`, `low_watermark_coins`, `low_water_nonzero_factor_ppm`, `high_watermark_coins`, `high_water_nonzero_factor_ppm` |
| 最近充值 | `recharge_boost_window_ms`, `recharge_boost_factor_ppm` |
| 大奖 | `jackpot_multiplier_ppms`, `jackpot_global_rtp_max_ppm`, `jackpot_user_day_rtp_max_ppm`, `jackpot_user_72h_rtp_max_ppm`, `jackpot_spend_threshold_coins`, `max_jackpot_hits_per_user_day` |
| 六维风控 | `max_single_payout`, `user_hourly_payout_cap`, `user_daily_payout_cap`, `device_daily_payout_cap`, `room_hourly_payout_cap`, `anchor_daily_payout_cap` |
| 充值层级 | `stages[].min_recharge_7d_coins`, `stages[].min_recharge_30d_coins`, `stages[].tiers[]` |
后台沿用现有 `/admin/activity/lucky-gifts/v2/config` 配置入口URL 中的 `v2` 是接口兼容标识,不等于 `strategy_version`。发布时必须把上述字段完整映射到内部 protobuf、domain 和 MySQL不允许只在 HTTP JSON 中临时扩字段。
## 10. 持久化与审计
| 表 | V3 事实 |
| --- | --- |
| `lucky_gift_rule_versions` | `fixed_v2/dynamic_v3` 不可变版本、资金拆分、水位、大奖门槛和六项上限 |
| `lucky_gift_stage_tiers` | 普通/高阶充值门槛、新手兜底 sentinel、基础倍率和基础概率 |
| `lucky_pools` | 公共池 `balance/total_in/total_out`,以及 `profit_total/anchor_income_total/initial_seed_total` |
| `lucky_rtp_windows` | 全局/奖池窗口的流水、目标返奖、实际返奖和 `open/closed/underpaid` 状态 |
| `lucky_user_rtp_hour_buckets` | 用户 UTC 小时流水/返奖桶,用于严格滚动 72 小时 |
| `lucky_user_rtp_boundary_events` | 只补齐 `[now-72h,now)` 首尾两个不足一小时的片段;与完整小时桶组合后不近似、不回扫全量 draw |
| `lucky_user_strategy_days` | 用户 UTC 日流水、返奖、消费和已命中大奖次数 |
| `lucky_user_states` | 累计抽数、等价流水、`loss_streak` 和可跨日保留的待消费 token |
| `lucky_risk_counters` | 用户/设备/房间/主播的 UTC 小时或日累计返奖 |
| `lucky_draw_records` | 单抽最终奖档、金额、规则版本、候选/奖池/RTP 决策快照和钱包状态 |
| `external_lucky_gift_draw_items` | 外部 App `gift_count=N` 的 N 条顺序子抽事实;外部聚合行只负责幂等响应 |
| `lucky_gift_command_locks` | 内部 `app_code+command_id` 首次并发互斥;等待者在首事务提交后回读同一批 draw 事实 |
| `lucky_gift_outbox` | 钱包补偿与投递审计,不替代 draw 业务事实 |
每条 V3 draw 的审计快照至少要能还原:阶段、规则版本、抽前池余额、本抽入池、可用池、六维最小容量、各档基础/调整后权重及因子、原始档、每次删档/重抽、三项 RTP 条件、大奖机制、token 产生/消费/保留、最终原因和阻断原因。
状态边界:
- `reward_status=pending/granted/failed` 只表示自动钱包入账状态;抽奖一旦向用户展示就形成不可撤销负债,`failed` 表示仍欠用户、必须重放或人工补偿,不能反向释放公共池/RTP/风控额度,也不能取消奖励。已 `granted` 后 IM 失败不能把账务事实改成 `failed`
- outbox 使用 `pending/retryable/delivering/delivered/failed`;失败原因、重试次数和锁到期时间必须可查。
- 规则不可变发布;事故回放必须读取 draw 当时的 `rule_version` 和快照,不能套用最新配置重算。
## 11. 本地模拟矩阵
模拟必须调用与生产相同的纯策略内核,并使用固定 seed 输出人类摘要和 JSON。以下每项都是独立验收不用一个长跑 RTP 数字替代边界测试:
| 组 | 输入矩阵 | 必须证明 |
| --- | --- | --- |
| 名义 EV | 原图 0/5/50/200/1000 概率 | `26.5x/2650%``passes_target=false` 是预期成功 |
| 资金拆分 | 0、1、极小金额、普通金额 | 三份非负且逐笔整数守恒;冷启动只注资一次 |
| 原图 P/W | `P=800, price=10`,脚本依次命中 5x、200x、1000x | 5x 直付200x 删除自身+01000x 继续删除;最终不透支 |
| P/W 边界 | `W=P-1/P/P+1`、全部正奖不可付 | 等于可付,大于重抽,候选耗尽回 0`P` 永不为负 |
| 连 0 保底 | streak 05第 6 抽资金足/不足 | 第 6 抽排除 0无可支付正奖时安全回 0并留阻断原因 |
| 水位 | `low-1/low/low+1/high-1/high/high+1` | 只在 `<low` 下调、`>high` 上调,端点为中水位 |
| 充值窗口 | 起点、`+299999ms``+300000ms`、未来时间、无回执 | 严格 `[start,end)`,只有可信快照生效 |
| 因子组合 | 低/中/高水位 × 充值开/关 | 因子相乘;概率合计始终 100% |
| 充值分层 | 各 7/30 日门槛的下 1、等于、上 1 | 必须同时达到两维,选择最高满足层 |
| 批量 | N=1、N=99、金额有余数 | 恰好 N 次顺序推进,金额守恒,单抽事实与重放一致 |
| 结算窗口 | 阈值 99、参考价 10、已 10 抽但仅 98 流水,再执行 99 份共 100 金币 | 第 1 抽进入旧窗并使流水到 100后 98 抽进入新窗;证明不按抽数提前滚动 |
| 大奖机制一 | 全局/当日/72h 各自通过和失败、分母 0 | 三门同时通过才有资格,零样本不触发 |
| 大奖机制二 | 门槛前、恰好跨过、一次跨多个门槛、资金不足 | token 只影响下一抽,幂等产生,资金不足保留 |
| 大奖集合 | 200x/500x/1000x 分别可付/不可付 | 只在可付集合随机,不能选中后透支 |
| 日 5 次 | 已中 4 次、5 次UTC 日边界 | 第 5 次可中,第 6 次大奖被阻断,新 UTC 日重新计数 |
| 六维风控 | 每一维分别取 `W=remaining``W=remaining+1` | 等于允许,超过任一维拒绝,原因可审计 |
| 组合优先级 | token + RTP 补偿 + 连 0 + 低水位 + 充值 | token 优先,其后 RTP再普通安全门永远最后裁决 |
| 并发/幂等 | 同 command 重放、同用户并发状态版本 | 只结算一次;状态无丢失更新,奖池不为负 |
| 有资金长跑 | 固定 seed、足量冷启动资金、各充值层 | 报告实际 RTP/命中率/池底,不把 95%101%当资金不足时硬断言 |
### 11.1 可重复命令
```bash
go test ./services/lucky-gift-service/internal/domain/luckygift
go test ./services/wallet-service/internal/storage/mysql -run 'LuckyGiftRechargeSnapshot'
go run ./services/lucky-gift-service/cmd/strategy-sim \
-seed 20260712 \
-monte-carlo-draws 100000 \
-long-run-draws 100000 \
-group-long-run-draws 30000 \
-json-out /tmp/lucky-gift-dynamic-v3-simulation.json
```
策略模拟进程只有在全部场景满足断言时退出 0。总报告必须同时满足
- `nominal_probability_table.expected_multiplier=26.5`
- `nominal_probability_table.nominal_rtp_percent=2650`
- `nominal_probability_table.passes_target=false`
- `all_passed=true`
- 所有场景输出固定 seed、输入、最终奖档/状态、池底和阻断原因,不能只打印“通过”。
## 12. 上线前验收门
1. 存量规则空 `strategy_version` 读取和重放仍为 `fixed_v2`,且无需补 V3 金额字段。
2.`dynamic_v3` 启用规则的三项拆分必须合计 100%,每层基础概率必须合计 100%,静态 EV 必须落在目标 RTP±控制带内。
3. 累计消费大奖门槛、低/高水位、普通/高阶充值门槛、大奖集合、日次数和六项风控都必须按 App 明确配置;不能依赖代码默认值直接上线。
4. user JWT、gateway/room meta、wallet 回执、lucky-gift 请求、protobuf 生成代码、配置读写和 MySQL schema 必须端到端包含新增字段;动态入口缺可信 `device_id` 或 owner `paid_at_ms` 必须 fail-close。
5. 本地单测、完整模拟、`make proto``make test` 通过后,仍需用隔离奖池灰度;灰度验收重点是奖池不为负、幂等不重复发奖、审计可回放,而不是短窗口 RTP 恰好等于 98%。