# 幸运礼物产品文档

本文档按当前代码实现描述幸运礼物产品能力。所有功能点均给出实现依据；未在代码中闭环的能力不写成已上线能力。

## 1. 产品定位和范围

幸运礼物是建立在房间送礼链路上的付费抽奖玩法。用户发送带 `pool_id` 的幸运礼物，系统先完成金币扣费，再按奖池规则抽奖；中奖金币由钱包服务异步入账，开奖结果通过房间 IM 和区域播报做表现。

| 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- |
| 付费抽奖定位 | 幸运礼物不是独立支付入口，而是送礼成功后的抽奖附加玩法。 | `services/room-service/internal/room/service/gift.go` 的 `SendGift` 先调用 `wallet.DebitGift`，成功后再调用 `ExecuteLuckyGiftDraw`。 |
| 规则隔离维度 | 规则按 `pool_id` 隔离，不按真实礼物 ID 绑定规则；一个奖池可服务不同价格礼物。 | `docs/幸运礼物实现文档.md`；`services/activity-service/internal/storage/mysql/lucky_gift_rule_config_repository.go` 的 `GetLuckyGiftRuleConfig`、`getLuckyGiftRuleConfig`；`luckyRuntimeConfig` 按真实 `coin_spent` 缩放奖档金额。 |
| 服务边界 | `activity-service` 负责规则、抽奖、奖池、风控、抽奖事实和 outbox；`wallet-service` 负责扣费和返奖入账；`room-service` 负责房间状态、热度、榜单和房间 outbox。 | `services/activity-service/internal/service/luckygift/service.go` 的 `Repository`、`WalletClient`、`RoomPublisher` 接口；`services/room-service/internal/room/service/gift.go`；`services/wallet-service/internal/service/wallet/service.go` 的 `CreditLuckyGiftReward`。 |
| 不兼容旧玩法假设 | 当前只以 v2 规则版本为准；旧文档和旧 gift_id 规则模型不作为产品口径。 | `docs/幸运礼物实现文档.md` 开头说明；后台路由固定为 `/admin/activity/lucky-gifts/v2/*`，见 `server/admin/internal/modules/luckygift/routes.go`。 |

## 2. 角色和使用场景

| 角色 | 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- | --- |
| 送礼用户 | 发送幸运礼物并获得即时抽奖结果 | 用户在房间内给一个在线用户送礼，送礼成功后当前用户同步拿到 `lucky_gift` 结果。 | `SendGift` 校验送礼者和收礼者都在房间；响应包含 `roomv1.SendGiftResponse.LuckyGift`；转换逻辑在 `luckyGiftResultFromProto`。 |
| 收礼用户/主播 | 作为礼物目标和主播风控主体 | 目标用户必须在房间内；主播风控优先使用房主 ID，缺失时退回房间 ID。 | `SendGift` 校验 `TargetUserID`；`luckyGiftAnchorID`。 |
| 房间内用户 | 看到送礼事实和开奖结果表现 | 普通送礼表现继续广播；中奖结果由 activity outbox worker 发布 `lucky_gift_drawn` 房间群消息。 | `RoomGiftSent` outbox 构建在 `SendGift`；开奖结果发布在 `publishLuckyGiftDrawn`。 |
| 运营管理员 | 配置奖池、阶段奖档、RTP、风控上限并查看数据 | 后台提供奖池配置、发布、列表、抽奖汇总和抽奖明细接口；前端提供配置页、模拟工具和数据概览。 | 后端 `server/admin/internal/modules/luckygift/handler.go`；前端 `hyapp-admin-platform/src/features/lucky-gift/pages/LuckyGiftConfigPage.jsx`。 |

## 3. 用户端功能

| 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- |
| 送礼前资格检查 | App 可在送礼前调用检查接口确认奖池是否可用；该接口只检查，不扣费、不抽奖。 | `POST /api/v1/activities/lucky-gifts/check` 注册在 `services/gateway-service/internal/transport/http/httproutes/router.go`；处理函数 `activityapi.Handler.checkLuckyGift`。 |
| 检查参数 | 检查接口要求 `room_id`、`gift_id`、`pool_id` 必填，用户 ID 来自登录态。 | `services/gateway-service/internal/transport/http/activityapi/lucky_gift_handler.go` 的 `luckyGiftCheckRequest` 和必填校验。 |
| 检查返回 | 返回是否启用、原因、奖池 ID、礼物 ID、参考价格、规则版本、目标 RTP、用户当前体验池。 | `luckyGiftCheckData`；`activityv1.CheckLuckyGiftResponse`；`Repository.CheckLuckyGift` 读取用户状态并返回 `ExperiencePool`。 |
| 显式奖池触发 | 新客户端传 `pool_id` 时，系统必须先检查规则启用，再扣费；规则未启用则整笔送礼失败。 | `SendGift` 中 `cmd.PoolID != ""` 分支调用 `CheckLuckyGift`，未启用返回 `RuleNotActive`，位置在扣费调用前。 |
| 旧客户端兼容触发 | 未显式传 `pool_id` 时，若钱包返回礼物类型为 `lucky` 或 `super_lucky`，仍可触发幸运礼物抽奖。 | `Service.shouldDrawLuckyGift` 判断 `giftTypeCode` 为 `lucky/super_lucky`。 |
| 扣费优先 | 抽奖只使用钱包扣费成功后的 `coin_spent`，不信任客户端价格。 | `SendGift` 调用 `wallet.DebitGift` 后，将 `billing.GetCoinSpent()` 写入 `LuckyGiftMeta.CoinSpent`。 |
| 设备风控标识 | 设备维度优先使用房间命令的 `session_id`，没有时退回 `command_id`，保证风控 scope 不为空。 | `luckyGiftDeviceID`。 |
| 同步抽奖结果 | 送礼响应中返回抽奖 ID、奖池、规则版本、体验池、命中奖档、倍率、奖励金币、发放状态、阶段反馈和高倍标识。 | `api/proto/room/v1/room.proto` 的 `LuckyGiftDrawResult`；`luckyGiftResultFromProto`。 |
| 普通送礼事实保留 | 幸运礼物不会替代普通送礼；房间热度、本地礼物榜、RoomGiftSent、RoomHeatChanged、RoomRankChanged 仍照常落地。 | `SendGift` 在抽奖后继续更新 `current.Heat`、`GiftRank`，并构建 `RoomGiftSent`、`RoomHeatChanged`、`RoomRankChanged` outbox。 |
| 批量送礼 | `gift_count=N` 表示抽 N 次；响应展示整次送礼的聚合倍数和总奖励。 | `executeLuckyGiftDrawBatch`、`luckyDrawUnitSpend`、`stageLuckyBatchDraw`、`luckyAggregateDrawResults`。 |
| 批量金额拆分 | 总扣费按 `gift_count` 拆为 N 个单抽金额，余数分给前几抽，保证子抽金额总和等于扣费事实。 | `luckyDrawUnitSpend`；单测 `TestLuckyDrawUnitSpendSplitsBatchSpendByGiftCount`。 |

## 4. 抽奖规则

| 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- |
| 奖池配置开关 | 每个 `pool_id` 有独立启用状态；未配置或未启用时不能抽奖。 | `Service.GetConfig` 未配置返回 disabled 草稿；`Repository.CheckLuckyGift` 未配置返回 `enabled=false`；`executeSingleLuckyGiftDraw` 和 `executeOptimizedLuckyGiftDrawBatch` 未启用返回 `RuleNotActive`。 |
| 不可变规则版本 | 每次后台发布都新增 `rule_version`，不覆盖历史版本。 | `Repository.PublishLuckyGiftRuleConfig` 事务内锁最新版本并写入 `lucky_gift_rule_versions`、`lucky_gift_stage_tiers`。 |
| 最新规则读取 | App 检查、抽奖运行和后台列表都读取当前奖池最新版本。 | `getLuckyGiftRuleConfig` 按 `rule_version DESC LIMIT 1`；`ListLuckyGiftRuleConfigs` 使用每个 pool 的 `MAX(rule_version)`。 |
| RTP 目标 | `target_rtp_ppm` 表示基础返奖目标，后台业务单位是百分比，服务端持久化为 ppm。 | `server/admin/internal/modules/luckygift/handler.go` 的 `percentToPPM`、`ppmToPercent`；`validateRuleConfig` 限制 10%-99%。 |
| 入池比例 | 每笔扣费按 `pool_rate_ppm` 进入基础奖池，且入池比例必须不低于目标 RTP。 | `validateRuleConfig` 校验 `PoolRatePPM >= TargetRTPPPM`；`luckyBasePoolIn`、`creditLuckyBasePools`。 |
| 结算窗口 | RTP 结算窗口按金币流水配置，运行时按参考价格折算窗口抽数；窗口只用于观察和结算，不改变随机概率。 | `luckyRuntimeConfigFromRuleConfig` 用 `SettlementWindowWager / GiftPriceReference` 折算窗口；`getOpenLuckyRTPWindow`；`selectLuckyCandidate` 注释和实现不读取 RTP 缺口。 |
| 波动带 | 后台发布时校验每个阶段期望 RTP 是否落在 `目标 RTP ± 波动带`。 | `validateRuleStageExpectedRTP`；前端 `luckyGiftConfigFormSchema` 同步校验。 |
| 参考价格 | `gift_price_reference` 用于配置奖档金额、结算窗口和等价抽数；真实抽奖按本次单抽 `coin_spent` 缩放奖档金额。 | `luckyRuntimeConfigFromRuleConfig`、`luckyRuntimeConfig`、`luckyScaleReward`。 |
| 用户阶段 | 用户分为新手、正常、高阶三个阶段；阶段由累计金币流水折算等价抽数决定。 | `luckyEquivalentDraws`、`luckyExperiencePool`；用户状态表字段见 `lucky_user_states`。 |
| 阶段奖档 | 每个阶段都配置一组奖档，每个奖档包含倍率、概率、预算来源、高水位、播报级别、启用状态；当前 v2 抽奖主链路只把 `base_rtp` 奖档转换为随机候选。 | `LuckyGiftRuleStage`、`LuckyGiftRuleTier` proto；`domain.RuleStage`、`domain.RuleTier`；后台 DTO `stageDTO`、`tierDTO`；运行转换见 `luckyRuntimeTiersFromRuleStages`。 |
| 概率闭合 | 每个阶段启用奖档概率合计必须为 100%，且必须存在 0x 档和至少一个基础 RTP 奖档。 | `validateRuleStage`；前端 `luckyGiftConfigFormSchema`。 |
| 高倍限制 | 高倍率奖档必须标记高水位，运行时仍受奖池和风控过滤。 | 后端 `validateRuleStage` 对高倍率校验；前端 schema 对 5x 及以上要求 `highWaterOnly`；`selectLuckyCandidate` 对高倍和 `LargeTierEnabled` 做过滤。 |
| 随机候选 | 抽奖只使用后台配置概率；奖池和风控只删除不可支付候选，不动态注入 RTP 修正档。 | `selectLuckyCandidate`、`luckyConfiguredWeightCandidates`；单测 `TestLuckyConfiguredWeightCandidatesUsesConfiguredWeightsWithoutForcedSettlement`。 |
| 无可支付奖档 | 所有正奖档都因奖池或风控不可支付时，本抽返回 0 倍 `no_payable_tier`，不透支。 | `selectLuckyCandidate` 返回 `no_payable_tier`；单测 `TestSelectLuckyCandidateFallsBackToZeroWhenAllPositiveTiersAreUnpayable`。 |

## 5. 奖池、预算和风控

| 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- |
| 三层基础奖池 | 基础返奖由平台池、房间池、礼物池共同承担；任一层余额不足都会过滤奖档。 | `getOrCreateLuckyPool`、`luckySplitWeighted`、`luckyWeightedPoolCapacity`、`creditLuckyBasePools`、`debitLuckyPool`。 |
| 先入池后开奖 | 每笔扣费先按入池比例补充奖池，再过滤候选并开奖。 | 单抽路径先调用 `creditLuckyBasePools` 再 `selectLuckyCandidate`；批量路径在 `stageLuckyBatchDraw` 中先 `creditLuckyPoolInMemory` 再候选过滤。 |
| 房间气氛池 | 当前保留房间气氛池账本和结果字段，且气氛奖励口径独立于基础 RTP；v2 主候选默认仍来自基础 RTP。 | `lucky_room_atmosphere_pools` 表；`getOrCreateLuckyAtmosphere`、`creditLuckyAtmosphere`、`updateLuckyAtmosphereNet`；`applyLuckyDraw` 注释说明气氛奖励不进入基础 RTP。 |
| 活动补贴预算 | 当前保留活动补贴总预算和 UTC 日预算账本；v2 后台第一阶段未暴露活动预算，运行主候选默认不从活动补贴生成。 | `lucky_activity_budgets` 表；`getLuckyActivityRemaining`、`getOrCreateLuckyActivityTotalRemaining`；`luckyRuntimeConfigFromRuleConfig` 将 `ActivityBudget`、`ActivityDailyLimit` 置为 0。 |
| 单次风控上限 | 单抽奖励不能超过规则配置的单次返奖上限。 | `validateRuleConfig` 要求 `MaxSinglePayout > 0`；`luckyRiskCapacity` 首先使用 `MaxSinglePayout`。 |
| 用户小时/日上限 | 同一用户在小时和 UTC 自然日内有返奖上限。 | `getLuckyRiskCounters` 创建 `user:hour`、`user:day` 计数；`luckyRiskCapacity` 和 `updateLuckyRiskCounters`。 |
| 设备日上限 | 同一设备在 UTC 自然日内有返奖上限。 | `getLuckyRiskCounters` 的 `device/day`；`luckyGiftDeviceID` 保证设备 scope。 |
| 房间小时上限 | 同一房间每小时有返奖上限。 | `getLuckyRiskCounters` 的 `room/hour`；`luckyRiskCapacity`。 |
| 主播日上限 | 同一主播 UTC 自然日有返奖上限。 | `getLuckyRiskCounters` 的 `anchor/day`；`luckyGiftAnchorID`。 |
| 并发一致性 | 奖池、用户状态、RTP 窗口和风控计数在事务内锁定，避免并发透支。 | `Repository.ExecuteLuckyGiftDraw` 使用 MySQL 事务；`getLuckyUserStateForUpdate`、`getLatestLuckyRTPWindow(... FOR UPDATE)`、`getOrCreateLuckyPool(... FOR UPDATE)`、`getOrCreateLuckyRiskCounter(... FOR UPDATE)`。 |

## 6. 返奖和表现

| 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- |
| 异步发奖 | 抽奖事务只写事实和 outbox，不直接调用钱包；worker 后续处理返奖和表现。 | `applyLuckyDraw`、`insertLuckyAggregateDrawOutbox` 写 `activity_outbox`；外部 RPC 在 `processDrawOutbox`。 |
| 钱包入账 | 有有效奖励时，worker 调用钱包 `CreditLuckyGiftReward` 给中奖用户加 COIN。 | `Service.processDrawOutbox` 调用 `walletv1.CreditLuckyGiftReward`；钱包实现见 `services/wallet-service/internal/service/wallet/service.go` 和 `internal/storage/mysql/repository.go`。 |
| 返奖幂等 | 钱包命令 ID 由聚合 `draw_id` 派生，同一 draw 重试只加币一次。 | `processDrawOutbox` 使用 `CommandId: "lucky_reward:" + payload.DrawID`；钱包 `CreditLuckyGiftReward` 用 `lookupTransaction` 和 request hash 幂等；单测 `TestCreditLuckyGiftRewardIsIdempotent`。 |
| 房间开奖结果 | worker 发 `lucky_gift_drawn` 房间群自定义消息，用于房间内开奖结果表现。 | `publishLuckyGiftDrawn`；IM 消息 `Desc` 为 `lucky_gift_drawn`。 |
| 区域播报 | 当存在可见区域、有效奖励且聚合倍率不低于 1x 时，写区域播报 outbox。 | `shouldPublishLuckyGiftRegionBroadcast`、`publishLuckyGiftRegionBroadcast`、`luckyGiftRegionBroadcastPayload`。 |
| 成功收敛 | 钱包、区域播报和房间 IM 都成功后，整批 draw records 标记 `granted`，outbox 标记 `delivered`。 | `processDrawOutbox` 末尾调用 `MarkLuckyGiftDrawsGranted` 和 `MarkLuckyGiftOutboxDelivered`。 |
| 失败退避 | outbox 处理失败按指数退避重试；达到最大重试后标记失败。 | `markOutboxFailed`、`luckyGiftBackoff`、`MarkLuckyGiftOutboxRetryable`、`MarkLuckyGiftOutboxFailed`。 |
| 已返奖但表现失败 | 钱包已加币后，如果 IM 或播报失败，不把 draw 改成 failed，避免覆盖账务事实。 | `markOutboxFailed(... alreadyCredited=true)` 分支；`MarkLuckyGiftDrawsFailed` 注释约束 only pending。 |

## 7. 批量抽奖产品规则

| 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- |
| N 次独立随机 | `gift_count=N` 时内部执行 N 次抽奖，每次保留独立 draw record。 | `executeOptimizedLuckyGiftDrawBatch` 循环调用 `stageLuckyBatchDraw`；`insertLuckyDrawRecords` 批量写每条明细。 |
| 一次聚合发放 | 批量抽奖只产生一条聚合 `LuckyGiftDrawn` outbox，钱包只入账一次。 | `insertLuckyAggregateDrawOutbox` payload 包含 `draw_id` 和全部 `draw_ids`；`processDrawOutbox` 对聚合金额调用一次钱包。 |
| 聚合结果展示 | 批量响应展示总基础奖励、总气氛奖励、总活动补贴、总有效奖励、总倍率；总倍率是子抽倍率相加，不是平均倍率。 | `luckyAggregateDrawResults`。 |
| 批量性能 | 规则、用户状态、RTP 窗口、奖池、预算和风控行只锁一次，循环内推进内存状态，事务尾部批量写回。 | `executeOptimizedLuckyGiftDrawBatch`、`stageLuckyBatchDraw`、`persistLuckyBatchDraw`。 |
| 批量幂等 | 批量子抽命令使用 `command_id#序号`；重试读回已提交子抽，部分存在时报冲突。 | `luckyDrawSubCommandID`、`collectLuckyBatchDrawsByCommand`。 |

## 8. 后台配置功能

| 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- |
| 后台菜单 | 运营后台有“幸运礼物”菜单，权限码为 `lucky-gift:view` 和 `lucky-gift:update`。 | `server/admin/migrations/017_lucky_gift_navigation.sql`；`server/admin/internal/repository/seed.go`；前端 `routes.js`。 |
| 查看配置 | 管理员可按 `pool_id` 查看奖池配置；未配置时返回 disabled 默认草稿。 | `GET /admin/activity/lucky-gifts/v2/config`；`Handler.GetLuckyGiftConfig`；`Service.GetConfig`；前端 `getLuckyGiftConfig`。 |
| 新增奖池 | 前端允许输入新奖池 ID，并以默认草稿打开配置抽屉。 | `LuckyGiftConfigPage.jsx` 的 `AddPoolDialog`；`useLuckyGiftPage.submitAddPool`。 |
| 发布配置 | 管理员保存后发布为新的不可变规则版本。 | `PUT /admin/activity/lucky-gifts/v2/config`；`Handler.UpsertLuckyGiftConfig`；`Service.UpsertConfig`；`Repository.PublishLuckyGiftRuleConfig`。 |
| 奖池列表 | 管理员可查看已有奖池列表，每个奖池展示最新版本。 | `GET /admin/activity/lucky-gifts/v2/configs`；`Handler.ListLuckyGiftConfigs`；`Repository.ListLuckyGiftRuleConfigs`；前端 `listLuckyGiftConfigs`。 |
| 基础控制字段 | 后台可配置启用状态、RTP、入池比例、结算窗口流水、波动带、参考价格、新手/正常等价抽数。 | 前端 `LuckyGiftConfigDrawer` 的“基础控制”；后端 `configRequest` 和 `LuckyGiftRuleConfig` 字段。 |
| 风控上限字段 | 后台可配置单次、用户小时、用户每日、设备每日、房间小时、主播每日上限。 | 前端 `LuckyGiftConfigDrawer` 的“风控上限”；后端 `validateRuleConfig`。 |
| 阶段奖档编辑 | 后台可编辑新手、正常、高阶三个阶段的奖档 ID、倍率、概率、预算来源、广播级别、高水位和启用状态；其中 `reward_source` 当前用于配置和校验，运行随机只消费 `base_rtp`。 | `StageTierEditor`、`TierRow`；后端 `configToProto`、`configFromProto`；运行转换见 `luckyRuntimeTiersFromRuleStages`。 |
| 概率纠正工具 | 前端提供“纠正概率”工具，按目标 RTP 调整阶段概率，帮助运营形成可发布配置。 | `LuckyGiftConfigDrawer` 调用 `correctStageTierProbabilities`；实现见 `configModel.js`。 |
| 表单校验 | 前端提前校验概率闭合、RTP 波动带、0x 档、基础 RTP 档和高倍高水位；后端仍做最终校验。 | 前端 `luckyGiftConfigFormSchema`；后端 `validateRuleConfig`、`validateRuleStage`、`validateRuleStageExpectedRTP`。 |
| 随机模拟测试 | 后台配置页提供随机模拟，可输入用户数、房间数、单抽花费范围和抽奖次数范围，输出概览并下载 CSV。 | `LuckyGiftSimulationPanel.jsx`；`runLuckyGiftSimulation`。 |

## 9. 后台审计和数据

| 功能点 | 产品规则 | 实现依据 |
| --- | --- | --- |
| 抽奖明细查询 | 后台接口支持按 `pool_id/gift_id/user_id/room_id/status` 分页查询抽奖明细。 | `GET /admin/activity/lucky-gifts/v2/draws`；`Handler.ListLuckyGiftDraws`；`Repository.ListLuckyGiftDraws`；`luckyDrawWhereClause`。 |
| 抽奖汇总 | 后台接口和页面展示总抽数、参与用户、参与房间、消耗金币、返奖金币、实际 RTP、pending/granted/failed 数。 | `GET /admin/activity/lucky-gifts/v2/draw-summary`；`Repository.GetLuckyGiftDrawSummary`；前端 `LuckyGiftDrawSummaryPanel`。 |
| 审计快照 | 每条抽奖明细记录命中奖档、过滤原因、奖池快照、RTP 快照、发放状态和钱包交易 ID。 | `lucky_draw_records` 表；`luckyDrawRecordSnapshots`；`ListLuckyGiftDraws`。 |
| 统计大盘口径 | 统计服务从 `RoomGiftSent` 事件识别带 `pool_id` 的礼物，累计幸运礼物流水和去重付费人数。 | `RoomGiftSent.pool_id`；`statistics-service/internal/storage/mysql/repository.go` 的 `ConsumeRoomGift`；`stat_lucky_gift_day_payers`。 |
| 发放状态 | 抽奖记录状态分为 `pending`、`granted`、`failed`；无奖励且无表现副作用的记录可直接 `granted`。 | `domain.StatusPending/Granted/Failed`；`luckyInitialRewardStatus`；`MarkLuckyGiftDrawsGranted`、`MarkLuckyGiftDrawsFailed`。 |

## 10. 接口清单

| 端 | 接口 | 功能点 | 实现依据 |
| --- | --- | --- | --- |
| App HTTP | `POST /api/v1/activities/lucky-gifts/check` | 送礼前资格检查。 | `activityapi.Handler.checkLuckyGift`；`httproutes.registerTaskRoutes`。 |
| App HTTP | `POST /api/v1/rooms/gift/send` | 发礼物并在成功扣费后触发幸运礼物抽奖。 | `httproutes.registerRoomRoutes`；`room-service` 的 `SendGift`。 |
| 内部 gRPC | `LuckyGiftService.CheckLuckyGift` | 房间链路或 gateway 查询奖池可用性。 | `api/proto/activity/v1/activity.proto`；`LuckyGiftServer.CheckLuckyGift`。 |
| 内部 gRPC | `LuckyGiftService.ExecuteLuckyGiftDraw` | 扣费成功后的抽奖命令。 | `LuckyGiftMeta`、`LuckyGiftDrawResult`；`LuckyGiftServer.ExecuteLuckyGiftDraw`。 |
| 内部 gRPC | `WalletService.CreditLuckyGiftReward` | 抽奖 outbox worker 调钱包入账。 | `api/proto/wallet/v1/wallet.proto`；钱包 `CreditLuckyGiftReward`。 |
| 后台 HTTP | `GET /admin/activity/lucky-gifts/v2/config` | 获取单个奖池配置。 | `Handler.GetLuckyGiftConfig`。 |
| 后台 HTTP | `PUT /admin/activity/lucky-gifts/v2/config` | 发布奖池配置。 | `Handler.UpsertLuckyGiftConfig`。 |
| 后台 HTTP | `GET /admin/activity/lucky-gifts/v2/configs` | 获取奖池配置列表。 | `Handler.ListLuckyGiftConfigs`。 |
| 后台 HTTP | `GET /admin/activity/lucky-gifts/v2/draws` | 查询抽奖明细。 | `Handler.ListLuckyGiftDraws`。 |
| 后台 HTTP | `GET /admin/activity/lucky-gifts/v2/draw-summary` | 查询抽奖汇总。 | `Handler.GetLuckyGiftDrawSummary`。 |

## 11. 异常和边界

| 场景 | 产品处理 | 实现依据 |
| --- | --- | --- |
| 奖池未配置 | 检查接口返回不可用；后台返回可编辑 disabled 草稿。 | `Repository.CheckLuckyGift` 返回 `not_configured`；`Service.GetConfig` 返回 `DefaultRuleConfig`。 |
| 奖池关闭 | 显式传 `pool_id` 的送礼在扣费前失败；抽奖入口也会拒绝。 | `SendGift` 的 `CheckLuckyGift` 分支；`executeSingleLuckyGiftDraw`、`executeOptimizedLuckyGiftDrawBatch`。 |
| 抽奖失败 | 整笔送礼失败，不落普通礼物事实，依赖钱包扣费幂等处理重试。 | `SendGift` 在抽奖失败时直接返回错误，未进入 Room Cell 事件落地。 |
| outbox payload 损坏 | 无法解析时走失败收敛，不无限重试。 | `decodeDrawOutboxPayload`、`markOutboxFailed`。 |
| worker 依赖缺失 | 钱包、房间发布或区域播报依赖缺失时，outbox 进入重试或失败，不直接丢弃。 | `processDrawOutbox` 的 `wallet == nil`、`publisher == nil`、`broadcaster == nil` 分支。 |
| 小额礼物 | 阶段按累计流水折算，奖档金额按实际单抽金额缩放，风控上限保持绝对金币。 | `luckyRuntimeConfig` 注释和实现；单测 `TestLuckyRuntimeConfigScalesTierRewardsByActualSpendButKeepsRiskCapsAbsolute`。 |
| UTC 窗口 | 付费时间、风控小时/日、活动预算日均使用 UTC epoch ms。 | `Service.Draw` 补 `PaidAtMS` 使用 `UTC().UnixMilli()`；`getLuckyRiskCounters`、`getLuckyActivityRemaining` 使用 `time.UnixMilli(...).UTC()`。 |

## 12. 当前不作为已实现产品能力

| 能力 | 当前状态 | 实现依据 |
| --- | --- | --- |
| 运行时按 RTP 缺口动态调概率 | 不支持。RTP 只用于发布校验和窗口观察。 | `selectLuckyCandidate` 只使用配置权重并过滤不可支付候选。 |
| 多目标送礼抽奖拆分 | 不支持。当前房间送礼仅支持 `target_type=user` 且一个目标用户。 | `SendGift` 对非 `user` 和多目标返回 `InvalidArgument`。 |
| 后台配置三层奖池权重 | v2 后台当前不暴露，运行侧使用保守默认权重。 | `luckyRuntimeConfigFromRuleConfig` 固定 `PlatformPoolWeightPPM=200000`、`RoomPoolWeightPPM=300000`、`GiftPoolWeightPPM=500000`。 |
| 按奖档广播级别精细化播报 | 配置字段存在，但当前区域播报阈值由代码统一判断为有区域、有效奖励、倍率不低于 1x。 | `LuckyGiftRuleTier.broadcast_level`；`shouldPublishLuckyGiftRegionBroadcast`。 |
| 按 `reward_source` 生成活动补贴或表现候选 | 不支持。当前 v2 运行候选只读取 `base_rtp` 奖档，活动补贴和表现来源不会被转换成随机候选。 | `luckyRuntimeTiersFromRuleStages` 对非 `SourceBaseRTP` 的奖档 `continue`。 |

## 13. 验证依据

| 验证项 | 覆盖功能 | 实现依据 |
| --- | --- | --- |
| Activity 配置校验单测 | 默认配置、ppm 单位、防错、概率闭合、0x 档、风控上限、阶段阈值。 | `services/activity-service/internal/service/luckygift/config_test.go`。 |
| Activity worker 单测 | 钱包入账、房间 IM、批量一次发奖并批量 granted。 | `services/activity-service/internal/service/luckygift/service_test.go`。 |
| Activity 抽奖算法单测 | 真实金额缩放、等价抽数、批量拆分、聚合倍数、候选过滤、不可支付回退 0x。 | `services/activity-service/internal/storage/mysql/lucky_gift_repository_test.go`。 |
| Room 送礼链路单测 | 送礼后返回 `lucky_gift`，并把扣费后的金额传给抽奖。 | `services/room-service/internal/room/service/lucky_gift_test.go`。 |
| Gateway 检查接口单测 | App 检查接口正确转发 room/gift/pool/user 参数。 | `services/gateway-service/internal/transport/http/activityapi/lucky_gift_handler_test.go`。 |
| Wallet 返奖幂等单测 | 同一幸运礼物返奖命令只产生一条钱包交易和 outbox。 | `services/wallet-service/internal/service/wallet/service_test.go` 的 `TestCreditLuckyGiftRewardIsIdempotent`。 |
| 后台单位转换单测 | 百分比和倍率在后台 HTTP DTO 与 proto ppm 之间正确转换。 | `server/admin/internal/modules/luckygift/handler_test.go`。 |
| 前端表单与模拟单测 | 配置 payload、阶段 RTP、概率纠正和模拟确定性。 | `hyapp-admin-platform/src/features/lucky-gift/*test*`。 |
