hyapp-server/docs/幸运礼物产品文档.md

173 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.

# 幸运礼物产品文档
本文档按当前代码实现描述幸运礼物产品能力。所有功能点均给出实现依据;未在代码中闭环的能力不写成已上线能力。
## 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*`。 |