docs: add lucky gift product document

This commit is contained in:
zhx 2026-06-22 14:02:13 +08:00
parent 995b52ce9d
commit af1f6da657

View File

@ -0,0 +1,172 @@
# 幸运礼物产品文档
本文档按当前代码实现描述幸运礼物产品能力。所有功能点均给出实现依据;未在代码中闭环的能力不写成已上线能力。
## 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` 再候选过滤。 |
| 房间气氛池 | 当前保留房间气氛池账本和结果字段,且气氛奖励口径独立于基础 RTPv2 主候选默认仍来自基础 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*`。 |