# 幸运礼物线上落地文档

本文档定义当前幸运礼物在 `activity-service` 的线上落地边界、后台参数、抽奖事务和验证方式。幸运礼物只拥有活动决策、RTP 窗口、奖池和抽奖事实；金币扣费和返奖入账仍归 `wallet-service`，房间状态和房间表现仍归 `room-service`。

## Current Scope

本次落地包含：

| Scope | Status | Notes |
| --- | --- | --- |
| `LuckyGiftService.CheckLuckyGift` | done | 房间送礼前检查礼物是否启用，并返回当前体验池 |
| `LuckyGiftService.ExecuteLuckyGiftDraw` | done | 扣费成功后执行抽奖决策，写抽奖事实和 outbox |
| `AdminLuckyGiftService.GetLuckyGiftConfig` | done | 后台按 `pool_id` 读取幸运礼物奖池规则；未配置时返回默认草稿 |
| `AdminLuckyGiftService.UpsertLuckyGiftConfig` | done | 后台按 `pool_id` 发布独立幸运礼物奖池规则版本 |
| `AdminLuckyGiftService.ListLuckyGiftDraws` | done | 后台审计抽奖记录 |
| 三层基础奖池 | done | platform / room / gift |
| RTP 双窗口 | done | global + gift |
| 风控计数 | done | user hour/day、device day、room hour、anchor day |
| 房间气氛池 | done | 独立预算，不污染基础 RTP |
| 活动预算池 | done | 独立补贴，不污染基础 RTP |

暂不在 `activity-service` 事务内调用钱包或房间 RPC。抽奖事务只写事实和 `activity_outbox`；后续由 outbox dispatcher 或房间链路消费结果。

## Call Flow

```text
room-service SendGift
  1. CheckLuckyGift(meta, user_id, room_id, gift_id, pool_id)
  2. wallet-service 扣费成功
  3. ExecuteLuckyGiftDraw(command_id, user_id, device_id, room_id, anchor_id, gift_id, pool_id, coin_spent, paid_at_ms)
  4. activity-service 本地事务写 draw fact + outbox
  5. 事务后由钱包/房间表现链路处理 reward_status=pending 的结果
```

`command_id` 是抽奖幂等键。重复 `ExecuteLuckyGiftDraw` 会返回同一条 `lucky_draw_records`，不会重复消耗 RTP、奖池或预算。

后台按 `pool_id` 维护多份互相独立的幸运礼物奖池规则，实际抽奖仍记录用户发送的真实 `gift_id`。后台不暴露单抽消耗；服务端保存固定内部换算单位，不同幸运礼物实际消耗不同，抽奖事务按实际 `coin_spent` 等比缩放基础奖档，并按实际 `coin_spent` 累计 RTP 目标和奖池入账。同一个 `gift_id` 可以请求不同 `pool_id`，不同 `pool_id` 的 RTP 窗口、基础奖池、房间气氛池、活动预算、用户阶段和风控计数互不共享。

## Transaction Boundary

`ExecuteLuckyGiftDraw` 使用一个 MySQL 本地事务，事务内必须完成：

1. 按 `command_id` 锁定幂等记录。
2. 锁定 `lucky_gift_rules` 中 `pool_id` 对应规则。
3. 锁定或创建用户体验状态 `lucky_user_states`。
4. 锁定或创建全站 RTP 窗口、幸运礼物整体子窗口。
5. 锁定或创建平台、房间、幸运礼物整体三层基础奖池。
6. 锁定或创建房间气氛池。
7. 锁定或创建活动总预算桶和 UTC 日预算桶。
8. 锁定用户、设备、房间、主播风控计数。
9. 抽前过滤所有不可支付候选。
10. 执行一次服务端安全随机。
11. 更新 RTP 窗口、奖池、预算、风控计数、用户状态。
12. 插入 `lucky_draw_records`。
13. 必要时插入 `activity_outbox`。

事务内不得调用 `wallet-service`、`room-service` 或腾讯云 IM，避免外部 RPC 卡住奖池行锁。

## RTP Accounting

基础返奖只使用 `base_reward_coins`：

```text
base_rtp = sum(base_reward_coins) / sum(coin_spent)
```

用户可见返奖使用：

```text
effective_reward_coins = base_reward_coins
                       + room_atmosphere_reward_coins
                       + activity_subsidy_coins
```

房间气氛池和活动预算只能提高 `effective_rtp`，不能用于补平基础 RTP。

## Admin Parameters

后台参数分为 6 类。

后台 HTTP 入口：

| Method | Path | Permission | Meaning |
| --- | --- | --- | --- |
| `GET` | `/api/v1/admin/activity/lucky-gifts/config?pool_id=pool_95` | `lucky-gift:view` | 读取指定奖池规则；未配置返回默认草稿 |
| `PUT` | `/api/v1/admin/activity/lucky-gifts/config?pool_id=pool_95` | `lucky-gift:update` | 发布指定奖池的新规则版本 |
| `GET` | `/api/v1/admin/activity/lucky-gifts/draws` | `lucky-gift:view` | 查询抽奖审计记录 |

| Group | Fields | Meaning |
| --- | --- | --- |
| RTP 成本 | `target_rtp_ppm`, `pool_rate_ppm`, `global_window_draws`, `gift_window_draws` | 控制基础 RTP 和收敛窗口 |
| 三层池 | `platform_pool_weight_ppm`, `room_pool_weight_ppm`, `gift_pool_weight_ppm`, `initial_*_pool`, `*_reserve` | 控制平台、房间、礼物各自承担的基础返奖能力 |
| 倍率配置 | `multiplier_ppms`, generated `tiers` | 运营只配置用户可能命中的倍率；服务按 RTP 窗口、水位和风控自动生成实时概率 |
| 体验池 | `novice_draw_limit`, `intermediate_draw_limit` | 控制新手/中级/高级阶段，不直接决定成本 |
| 高倍 | `high_multiplier`, `high_water_pool_multiple`, `large_tier_enabled` | 控制 100x/500x 等高倍倍率是否可进入候选 |
| 风控 | `max_single_payout`, `user_hourly_payout_cap`, `user_daily_payout_cap`, `device_daily_payout_cap`, `room_hourly_payout_cap`, `anchor_daily_payout_cap` | 控制单用户、设备、房间、主播关联风险敞口 |
| 娱乐补贴 | `room_atmosphere_rate_ppm`, `room_atmosphere_initial`, `room_atmosphere_reserve`, `activity_budget`, `activity_daily_limit` | 控制房间爆点和活动补贴 |

调整方向：

| Goal | Primary Change |
| --- | --- |
| 降成本 | 降 `target_rtp_ppm`，降补贴预算，提高高水位倍数 |
| 增刺激 | 增加高倍倍率，增房间气氛和活动预算 |
| 新手更友好 | 增加 0.2x/0.5x/1x 等小倍率，延长 `novice_draw_limit` |
| 防套利 | 降低用户/设备/房间/主播上限，关闭高倍 |
| RTP 更稳 | 缩小 RTP 窗口 |
| 体验更自然 | 放大 RTP 窗口，但接受短周期波动 |

## Data Tables

| Table | Owner Data |
| --- | --- |
| `lucky_gift_rules` | 按 `pool_id` 保存幸运礼物奖池规则和后台可调参数 |
| `lucky_rtp_windows` | 按 `pool_id` 保存 RTP 控制窗口 |
| `lucky_pools` | 按 `pool_id` 保存 platform / room / gift 三层基础奖池 |
| `lucky_room_atmosphere_pools` | 按 `pool_id + room_id` 保存房间气氛预算 |
| `lucky_activity_budgets` | 按 `pool_id` 保存活动补贴预算；`budget_day=__total__` 控总额，UTC 日期行控日释放 |
| `lucky_user_states` | 用户在单个 `pool_id` 内的累计付费抽数 |
| `lucky_risk_counters` | 按 `pool_id` 保存风控窗口计数 |
| `lucky_draw_records` | 每次抽奖事实和审计快照 |

## Verification

Proto 变更后必须运行：

```bash
PATH=/tmp/codex-protoc/bin:$HOME/go/bin:$PATH make proto
```

服务编译验证：

```bash
go test ./services/activity-service/...
```

模型验证：

```bash
go run ./tools/lucky-gift-v2-demo \
  -users 1000 \
  -devices 900 \
  -rooms 50 \
  -draws-min 0 \
  -draws-max 100000 \
  -unique-draws \
  -base-rtp 95 \
  -initial 1000000000 \
  -cost 500 \
  -global-window 100000 \
  -gift-window 100000
```

验收口径：

| Check | Requirement |
| --- | --- |
| `100000` paid draws | 基础 RTP 偏差 `< 1%` |
| `1000000` paid draws | 基础 RTP 偏差 `< 0.5%` |
| lucky gift RTP | 幸运礼物整体子窗口独立收敛 |
| pool safety | 三层池不低于 reserve |
| subsidy isolation | 房间气氛/活动补贴不进入基础 RTP |
| idempotency | 重复 command_id 返回同一 draw |

## Rollout

建议上线顺序：

1. 后台创建规则草稿，`enabled=false`。
2. 跑模型模拟并审核参数。
3. 发布 `enabled=true` 到全局幸运礼物配置。
4. 只开放内部房间或白名单房间。
5. 观察 `base_rtp`、`effective_rtp`、pool balance、risk reject、outbox pending。
6. 稳定后逐步开放更多房间和礼物。

必须保留的开关：

| Switch | Effect |
| --- | --- |
| `enabled=false` | 全站幸运礼物停止抽奖 |
| `large_tier_enabled=false` | 关闭高倍奖档 |
| `activity_budget=0` | 关闭活动补贴 |
| `room_atmosphere_rate_ppm=0` | 关闭房间气氛补贴 |
| 降低各风控 cap | 快速压缩风险敞口 |
