# 幸运礼物重构第三步:链路接入与上线验收 本文档定义幸运礼物重构第三步。第三步把 v2 配置和 v2 抽奖引擎接回房间送礼主链路,完成返奖、IM、后台审计、灰度上线和旧逻辑删除。当前不兼容旧代码,切换完成后旧幸运礼物抽奖入口、旧规则表和旧模拟器都应下线。 ## 目标 | 目标 | 说明 | | --- | --- | | 主链路接入 | `room-service` 扣费后调用 v2 抽奖 | | 返奖一致 | `activity_outbox` 调 `wallet-service` 入账,钱包 outbox 通知余额 | | 房间表现一致 | `lucky_gift_drawn` IM 和区域播报由 outbox 补偿 | | 后台可审计 | 后台能查 draw、候选表、控制模式、RTP、奖池和风控快照 | | 可灰度回滚 | 按 `pool_id/room_id/region` 控制 v2 开关 | | 删除旧逻辑 | 验收完成后移除旧 v1 抽奖和旧配置入口 | ## 主链路 ```text Client -> gateway POST /api/v1/rooms/gift/send -> room-service 校验房间和目标 -> wallet-service DebitGift -> room-service 调 activity-service ExecuteLuckyGiftDrawV2 -> activity-service 本地事务写 draw + outbox -> room-service 落 Room Cell 礼物、热度、榜单 -> gateway 返回 lucky_gift -> activity worker 返奖入账 -> activity worker 投递 lucky_gift_drawn IM -> wallet/notice 投递 WalletBalanceChanged ``` ## 接口要求 ### App 送礼请求 幸运礼物必须传明确 `pool_id`: ```json { "room_id": "room_1", "command_id": "cmd_001", "target_type": "user", "target_user_ids": [10002], "gift_id": "lucky_star_100", "gift_count": 1, "pool_id": "lucky_100" } ``` 不再支持“礼物类型是 lucky 但不传 pool_id 时走 default”的隐式路径。 ### 送礼响应 ```json { "lucky_gift": { "enabled": true, "draw_id": "lucky_draw_xxx", "command_id": "cmd_001", "pool_id": "lucky_100", "rule_version": 12, "stage": "normal", "control_mode": "normal", "selected_tier_id": "normal_0_5x", "multiplier_ppm": 500000, "base_reward_coins": 50, "activity_subsidy_coins": 0, "effective_reward_coins": 50, "reward_status": "pending", "created_at_ms": 1779259000000 } } ``` 客户端规则: | 场景 | 处理 | | --- | --- | | `reward_status=pending` | 展示中奖结果和“奖励处理中”,不本地加余额 | | 收到 `WalletBalanceChanged` | 更新余额 | | 收到同一 `draw_id` 的 IM | 去重,不重复播放 | | `lucky_gift` 为空 | 按普通礼物处理 | ## outbox 要求 抽奖事务只写事实和 outbox: | outbox | 作用 | | --- | --- | | `LuckyGiftRewardCredit` | 调钱包返奖 | | `LuckyGiftDrawnRoomIM` | 投递房间开奖结果 | | `LuckyGiftBigWinBroadcast` | 投递区域或全站播报 | 顺序要求: 1. 先返奖入账。 2. 再投递房间开奖结果。 3. 最后标记 draw 为 `granted`。 如果返奖已成功但 IM 失败: | 状态 | 处理 | | --- | --- | | draw | 不能标记 `failed`,避免覆盖账务事实 | | outbox | 继续重试 IM | | 后台 | 展示“返奖成功,表现补偿中” | ## 后台审计 后台必须支持: | 页面 | 能力 | | --- | --- | | 抽奖列表 | 按 `pool_id/user_id/room_id/status/control_mode` 查询 | | 抽奖详情 | 展示候选表、过滤原因、权重、随机落点 | | RTP 看板 | 展示目标 RTP、实际 RTP、控制模式分布 | | 奖池看板 | 展示 platform / room / gift 水位 | | 风控看板 | 展示用户、设备、房间、主播过滤次数 | | outbox 看板 | 展示 pending/retryable/failed | 抽奖详情必须能回答: ```text 为什么这次是 0x? 为什么 100x 被过滤? 为什么进入 boost/cooldown? 这次随机落点在哪里? 返奖是否到账? 房间 IM 是否投递? ``` ## 灰度策略 | 阶段 | 范围 | 要求 | | --- | --- | --- | | shadow | 只计算 v2,不返奖、不发 IM | 和模拟结果接近 | | internal | 内部房间、小额礼物 | outbox 和钱包稳定 | | room whitelist | 白名单房间 | 用户体验指标正常 | | region canary | 单区域小流量 | RTP 和投诉稳定 | | full | 全量开放 | 旧逻辑可删除 | 灰度开关: | 开关 | 作用 | | --- | --- | | `lucky_gift_v2_enabled` | 全局 v2 开关 | | `pool_v2_enabled` | 单奖池开关 | | `room_whitelist` | 房间白名单 | | `region_whitelist` | 区域白名单 | | `disable_high_tier` | 临时关闭高倍 | | `reward_credit_enabled` | 钱包事故时暂停返奖 dispatch,draw 保持 pending | ## 监控指标 | 指标 | 告警 | | --- | --- | | `base_rtp` | 超出目标波动带 | | `effective_rtp` | 高于预算预期 | | `zero_rate` | 高于配置预期 | | `zero_streak_p95` | 超过体验阈值 | | `control_mode_boost_ratio` | 长期过高说明概率或奖池不健康 | | `control_mode_cooldown_ratio` | 长期过高说明派奖过多 | | `pool_balance_min` | 接近 reserve | | `risk_filter_count` | 过高说明风控在替代概率控制 | | `outbox_pending_count` | 堆积 | | `reward_credit_latency` | 到账延迟 | | `draw_error_count` | 抽奖事务失败 | ## 旧逻辑删除 全量稳定后删除: | 旧内容 | 删除条件 | | --- | --- | | 旧 `lucky_gift_rules` 写入口 | v2 后台发布稳定 | | 旧 `multiplier_ppms -> tiers(weight=0)` 归一化 | v2 引擎全量 | | 旧 `selectLuckyCandidate` 自动权重模型 | v2 引擎全量 | | 旧 `tools/lucky-gift-v2-demo` 写死倍率模拟 | 新模拟器覆盖 | | 空 `pool_id` 默认奖池路径 | App 全部传明确 `pool_id` | ## 实施任务 1. 增加 `ExecuteLuckyGiftDrawV2` 内部接口。 2. `room-service` 送礼链路改调 v2。 3. gateway 响应增加 `stage/control_mode`。 4. activity outbox 拆分返奖、房间 IM、区域播报事件。 5. 后台增加抽奖详情和候选表审计。 6. 增加灰度开关和监控指标。 7. shadow draw 跑满一个结算窗口。 8. 灰度切流。 9. 全量后删除旧逻辑。 ## 验收标准 | 验收项 | 标准 | | --- | --- | | App 参数 | 幸运礼物不传 `pool_id` 必须拒绝,不走 default | | 扣费顺序 | 钱包扣费成功后才调用 v2 抽奖 | | 事务边界 | 抽奖事务内不调用钱包返奖或 IM | | 同步响应 | `lucky_gift` 返回 `stage/control_mode/multiplier_ppm/reward_status` | | 余额一致 | 客户端余额只由 `WalletBalanceChanged` 或余额查询更新 | | outbox 幂等 | 重试不会重复返奖、重复 IM 可由 `event_id/draw_id` 去重 | | 后台审计 | 任一 draw 可查看完整候选表和过滤原因 | | shadow 验证 | shadow 结果的 RTP、命中率、连续 0 分布与模拟报告一致 | | 灰度指标 | 灰度期间 base RTP 在控制带内,outbox 无持续堆积 | | 体验指标 | 100 金币礼物 `zero_streak_p95` 不超过配置阈值 | | 删除旧逻辑 | 全量稳定后旧配置入口、旧抽奖引擎、旧默认奖池路径已删除 | | 全量测试 | `make test` 通过 | 第三步完成后,幸运礼物 v2 才算真正上线。前两步只完成规则和引擎,不能直接对外开放。