# 幸运礼物实现文档 本文档描述当前代码里的幸运礼物实现。旧的产品方案、重构分步文档、Flutter 单独对接文档和早期 activity-service 开发文档已经下线;后续以本文档为唯一实现说明。 当前实现不兼容旧玩法假设。幸运礼物以 `pool_id` 为规则维度,以 `activity-service` 为规则、抽奖、奖池、RTP 窗口、风控、抽奖事实和 outbox owner;扣费和返奖入账属于 `wallet-service`;房间状态、热度、礼物榜和房间 outbox 属于 `room-service`。 ## 一、核心原则 | 功能点 | 当前规则 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | 服务边界 | `activity-service` 只做幸运礼物规则和开奖结果,不改 Room Cell,不直接改钱包余额 | `services/activity-service/internal/service/luckygift.Service.Draw`、`services/activity-service/internal/service/luckygift.Service.processDrawOutbox`、`services/room-service/internal/room/service.Service.SendGift` | 已验收:抽奖在扣费后执行,返奖通过 wallet gRPC,房间表现通过 IM/outbox | | 规则维度 | 线上规则按 `pool_id` 读取最新版本,不按真实 `gift_id` 读取旧规则 | `Repository.GetLuckyGiftRuleConfig`、`Repository.getLuckyGiftRuleConfig`、`luckyPoolID` | 已验收:代码读取 `lucky_gift_rule_versions` 和 `lucky_gift_stage_tiers` | | 返奖率控制 | 后台发布时校验阶段期望 RTP 落在 `target_rtp_ppm ± control_band_ppm`,运行时不按 RTP 缺口改概率 | `validateRuleConfig`、`validateRuleStageExpectedRTP`、`selectLuckyCandidate` | 已验收:发布校验函数存在,候选随机只过滤不可支付档 | | 用户体验 | 用户阶段按累计金币流水折算等价抽数,避免不同价格礼物用真实抽数误判阶段 | `luckyEquivalentDraws`、`luckyExperiencePool`、`getLuckyUserStateForUpdate` | 已验收:状态表维护 `cumulative_wager_coins`、`equivalent_draws`、`loss_streak` | | 批量抽奖 | `gift_count=N` 表示抽 N 次;每一抽按单份金币计算奖励,响应返回总中奖倍数和总奖励 | `executeLuckyGiftDrawBatch`、`luckyDrawUnitSpend`、`stageLuckyBatchDraw`、`luckyAggregateDrawResults` | 已验收:单测覆盖 10 抽返回总倍数 11x 的聚合逻辑 | | 聚合发放 | 批量抽奖可以有 N 条抽奖明细,但只写 1 条 `LuckyGiftDrawn` outbox,钱包只加币 1 次 | `insertLuckyAggregateDrawOutbox`、`decodeDrawOutboxPayload`、`MarkLuckyGiftDrawsGranted` | 已验收:worker 单测覆盖 `draw_ids` 批量 granted | ## 二、配置模型 ### 2.1 Proto 契约 | 功能点 | 字段或 RPC | 代码函数实现 | 验收 | | --- | --- | --- | --- | | App/房间抽奖元信息 | `LuckyGiftMeta`,包含 `command_id`、`pool_id`、`gift_count`、`coin_spent`、`paid_at_ms`、`visible_region_id` | `api/proto/activity/v1/activity.proto` `LuckyGiftMeta`、`LuckyGiftServer.ExecuteLuckyGiftDraw` | 已验收:房间送礼只把 wallet 已结算 `coin_spent` 传给 activity | | 抽奖结果 | `LuckyGiftDrawResult`,包含 `multiplier_ppm`、`effective_reward_coins`、`reward_status`、RTP 窗口和阶段字段 | `luckyDrawResultToProto`、`luckyGiftResultFromProto` | 已验收:room 返回 `LuckyGift` 字段给送礼用户 | | 后台规则 | `LuckyGiftRuleConfig`、`LuckyGiftRuleStage`、`LuckyGiftRuleTier` | `luckyRuleConfigFromProto`、`luckyRuleConfigToProto`、`configToProto`、`configFromProto` | 已验收:后台百分比和倍率在 handler 内转成 ppm | | 后台审计 | `ListLuckyGiftDraws`、`GetLuckyGiftDrawSummary` | `AdminLuckyGiftServer.ListLuckyGiftDraws`、`AdminLuckyGiftServer.GetLuckyGiftDrawSummary` | 已验收:按 `pool_id/gift_id/user_id/room_id/status` 查询 | ### 2.2 配置字段语义 | 字段 | 业务含义 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | `target_rtp_ppm` | 基础返奖目标,例如 950000 表示 95% | `validateRuleConfig`、`luckyRuntimeConfigFromRuleConfig` | 已验收:发布入口限制 10% 到 99% | | `pool_rate_ppm` | 每笔扣费进入基础奖池的比例,必须不低于目标 RTP | `validateRuleConfig`、`luckyBasePoolIn`、`creditLuckyBasePools` | 已验收:低于目标 RTP 会拒绝发布 | | `settlement_window_wager` | RTP 结算窗口金币流水,运行时按参考价格折算窗口抽数 | `luckyRuntimeConfigFromRuleConfig`、`luckyCeilDiv` | 已验收:窗口抽数由 `settlement_window_wager / gift_price_reference` 得到 | | `control_band_ppm` | 发布时允许阶段期望 RTP 波动带 | `validateRuleStageExpectedRTP` | 已验收:阶段期望 RTP 不在区间内会返回 InvalidArgument | | `gift_price_reference` | 阶段阈值和奖档金额的配置参考价格 | `luckyRuntimeConfigFromRuleConfig`、`luckyRuntimeConfig` | 已验收:真实抽奖按单抽 `coin_spent` 等比缩放奖档金额 | | `novice_max_equivalent_draws` | 新手阶段最大等价抽数 | `luckyExperiencePool`、`DefaultRuleConfig` | 已验收:小额和高价礼物统一按累计流水折算阶段 | | `normal_max_equivalent_draws` | 正常阶段最大等价抽数,超过后进入高级阶段 | `luckyExperiencePool`、`DefaultRuleConfig` | 已验收:正常阈值必须大于等于新手阈值 | | 风控上限字段 | 单次、用户小时、用户日、设备日、房间小时、主播日返奖上限 | `validateRuleConfig`、`luckyRiskCapacity`、`updateLuckyRiskCounters` | 已验收:所有上限必须为正数,候选过滤时统一参与 | | `stages[].tiers[]` | 三个阶段的基础候选概率和倍率 | `validateRuleStage`、`validateRuleStageExpectedRTP`、`luckyRuntimeTiersFromRuleStages` | 已验收:每阶段启用概率和必须为 1000000,且必须有 0x 档和基础 RTP 档 | ### 2.3 默认草稿和发布 | 功能点 | 行为 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | 默认草稿 | 后台读取未配置奖池时返回 disabled 草稿,不会自动生效 | `Service.GetConfig`、`DefaultRuleConfig` | 已验收:未找到配置时返回草稿,运行抽奖仍要求已发布启用规则 | | 参数归一化 | 补齐 stage、reward_source、broadcast_level,清理空格 | `normalizeRuleConfig` | 已验收:后台提交的空 `reward_source` 默认 `base_rtp` | | 发布校验 | 校验 RTP、入池比例、窗口、阶段、概率、风控和高倍水位 | `validateRuleConfig`、`validateRuleStage`、`validRuleStage`、`validRewardSource`、`validBroadcastLevel` | 已验收:service 层发布前执行完整校验 | | 不可变版本 | 每次发布新增 `rule_version`,不覆盖历史版本 | `Repository.PublishLuckyGiftRuleConfig` | 已验收:事务里锁最新版本并写入 `lucky_gift_rule_versions` 和 `lucky_gift_stage_tiers` | | 最新版本读取 | 后台和运行时都读取指定 `pool_id` 的最新版本 | `Repository.GetLuckyGiftRuleConfig`、`Repository.ListLuckyGiftRuleConfigs`、`Repository.getLuckyGiftRuleStages` | 已验收:列表按每个 pool 的最大 `rule_version` 返回 | ## 三、送礼链路 ### 3.1 送礼前检查 | 功能点 | 行为 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | App 检查接口 | `POST /api/v1/activities/lucky-gifts/check` 只做资格检查,不扣费、不抽奖 | `activityapi.Handler.checkLuckyGift` | 已验收:接口要求 `room_id/gift_id/pool_id` 必填 | | gRPC 检查 | gateway 或 room-service 通过 gRPC 查奖池是否启用 | `LuckyGiftServer.CheckLuckyGift`、`Service.Check` | 已验收:入参缺失返回 InvalidArgument | | 规则读取 | 按 `pool_id` 查 v2 最新规则,未配置返回 disabled | `Repository.CheckLuckyGift`、`Repository.getLuckyGiftRuleConfig` | 已验收:不再从旧 `lucky_gift_rules` 作为运行规则来源 | | 阶段展示 | 检查响应返回当前用户所在体验池 | `getLuckyUserState`、`luckyExperiencePool` | 已验收:响应包含 `experience_pool` | ### 3.2 扣费后抽奖 | 功能点 | 行为 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | 幸运礼物识别 | 显式传 `pool_id` 或钱包礼物类型为 `lucky/super_lucky` 时触发抽奖 | `Service.shouldDrawLuckyGift` | 已验收:旧客户端可由礼物类型触发,新客户端应传明确 `pool_id` | | 先扣费 | 房间链路先调用 wallet 扣费,成功后才调用 activity 抽奖 | `Service.SendGift`、`wallet.DebitGift` 调用段 | 已验收:抽奖使用 `billing.GetCoinSpent()`,不信任客户端价格 | | 抽奖命令 | 使用房间命令 ID 作为幂等根 ID | `Service.SendGift`、`LuckyGiftServer.ExecuteLuckyGiftDraw`、`Service.Draw` | 已验收:重复命令由抽奖记录幂等返回 | | 设备和主播 scope | 设备优先取 session_id,主播优先取房主 ID | `luckyGiftDeviceID`、`luckyGiftAnchorID` | 已验收:scope 不为空,风控计数可落表 | | 同步响应 | 当前送礼用户立即拿到抽奖结果 | `luckyGiftResultFromProto`、`roomv1.SendGiftResponse.LuckyGift` | 已验收:room response 带 `lucky_gift` | | 房间事实 | 送礼本身仍写 Room Cell、热度、榜单和 room outbox | `Service.SendGift`、`outbox.Build` | 已验收:幸运礼物不替代普通送礼事实 | ## 四、抽奖事务 ### 4.1 入口和幂等 | 功能点 | 行为 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | service 入参校验 | command、用户、设备、房间、主播、礼物、金额、数量必须有效 | `Service.Draw` | 已验收:`gift_count <= 0` 会被拒绝 | | MySQL 事务 | 抽奖事实、奖池、窗口、风控、用户状态和 outbox 在一个事务内提交 | `Repository.ExecuteLuckyGiftDraw` | 已验收:事务提交前不调用外部 RPC | | 单抽幂等 | 单抽按原始 `command_id` 查已有 draw | `executeSingleLuckyGiftDraw`、`getLuckyDrawByCommand` | 已验收:已存在时直接返回已有结果 | | 批量幂等 | 批量按 `command_id#序号` 查子抽;部分存在时报冲突 | `executeLuckyGiftDrawBatch`、`collectLuckyBatchDrawsByCommand`、`luckyDrawSubCommandID` | 已验收:避免重试补写导致重复返奖 | ### 4.2 单抽路径 | 功能点 | 行为 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | 规则锁定 | 抽奖时锁定当前 `pool_id` 最新规则版本 | `executeSingleLuckyGiftDraw`、`getLuckyGiftRuleConfig(... forUpdate=true)` | 已验收:规则未启用返回 RuleNotActive | | 运行配置转换 | v2 阶段奖档转换为运行候选,窗口流水转等价抽数 | `luckyRuntimeConfigFromRuleConfig`、`luckyRuntimeTiersFromRuleStages`、`luckyRuntimePoolFromRuleStage` | 已验收:只读取 `SourceBaseRTP` 作为基础候选 | | 实际金额缩放 | 同一奖池可服务不同价格礼物,奖励按本次 `coin_spent` 缩放 | `luckyRuntimeConfig`、`luckyScaleReward` | 已验收:风险上限保持绝对金币,不随小额礼物缩小 | | 阶段推进 | 累计流水折算等价抽数并选择阶段 | `luckyEquivalentDraws`、`luckyExperiencePool`、`luckyStageFeedback` | 已验收:阶段反馈只是表现,不强制发币 | | RTP 窗口 | 记录 wager、target payout、actual base payout 和 carry | `getOpenLuckyRTPWindow`、`luckyRTPWindow.withCurrentWager`、`createLuckyRTPWindow` | 已验收:RTP 只观察结算,不直接改随机概率 | | 三层基础奖池 | 平台、房间、礼物三层共同承担基础返奖,任一层不足都会过滤奖档 | `getOrCreateLuckyPool`、`creditLuckyBasePools`、`debitLuckyPool`、`luckyWeightedPoolCapacity` | 已验收:候选过滤前先入池,容量按责任权重反推 | | 房间气氛池 | 额外体验预算独立于基础 RTP | `getOrCreateLuckyAtmosphere`、`creditLuckyAtmosphere` | 已验收:气氛奖励不写入基础 RTP payout | | 活动预算 | 活动补贴预算按 UTC 日切分 | `getLuckyActivityRemaining`、`getOrCreateLuckyActivityTotalRemaining` | 已验收:预算不足时不会产生活动补贴候选 | | 风控计数 | 单次、用户、设备、房间、主播上限统一过滤可支付候选 | `getLuckyRiskCounters`、`luckyRiskCapacity`、`updateLuckyRiskCounters` | 已验收:并发抽奖通过 FOR UPDATE 锁计数行 | | 候选随机 | 只使用后台阶段概率,过滤不可支付档,不注入 RTP 修正档 | `selectLuckyCandidate`、`luckyConfiguredWeightCandidates`、`luckySecureIndex` | 已验收:候选为空时落 `no_payable_tier` 0 倍,不透支 | | 事实落库 | 写入 draw 明细和必要 outbox | `applyLuckyDraw`、`luckyInitialRewardStatus`、`luckyDrawNeedsOutbox` | 已验收:无奖励且无反馈时直接 `granted` | ### 4.3 批量路径 | 功能点 | 行为 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | 单份金额拆分 | `coin_spent` 按 `gift_count` 拆成 N 份,余数分配给前几抽 | `luckyDrawUnitSpend` | 已验收:单测覆盖 1001 金币拆成 101 + 9 个 100 | | 循环外锁共享状态 | 规则、用户状态、RTP 窗口、奖池、气氛池、活动预算、风控计数只读取和锁定一次 | `executeOptimizedLuckyGiftDrawBatch` | 已验收:批量入口不再循环调用完整 `executeSingleLuckyGiftDraw` | | 循环内只做业务推进 | 每一抽在内存里推进阶段、窗口、入池、候选、出池、风控和 draw 明细 | `stageLuckyBatchDraw`、`creditLuckyPoolInMemory`、`debitLuckyPoolInMemory`、`incrementLuckyRiskCountersInMemory` | 已验收:避免 777 抽产生 777 轮 SQL 状态写入 | | 批量落库 | 窗口、奖池、气氛池、风控、用户状态按本批净变化写一次 | `persistLuckyBatchDraw`、`updateLuckyPoolNet`、`updateLuckyAtmosphereNet` | 已验收:写入顺序保证 outbox 指向已存在 draw records | | 明细保留 | 每一抽仍保留一条 `lucky_draw_records`,用于审计和统计抽奖次数 | `insertLuckyDrawRecords`、`luckyDrawRecordArgs`、`luckyDrawRecordSnapshots` | 已验收:批量 INSERT,每 200 条一批 | | 聚合响应 | 批量响应把金额、倍数、阶段反馈和高倍标记聚合 | `luckyAggregateDrawResults`、`luckyAggregateBudgetSourcesJSON` | 已验收:总倍数为各命中倍数相加,不是平均倍率 | | 聚合发放 | 整批只写一条 outbox,payload 里有聚合 `draw_id` 和全部 `draw_ids` | `insertLuckyAggregateDrawOutbox` | 已验收:待发放队列按送礼命令增长,不按中奖子抽增长 | ## 五、返奖和房间表现 | 功能点 | 行为 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | worker 循环 | 持续 claim `LuckyGiftDrawn` outbox | `Service.RunWorker`、`Service.ProcessPendingDrawOutbox`、`Repository.ClaimPendingLuckyGiftOutbox` | 已验收:claim 使用 `delivering` 锁和 lock TTL | | payload 兼容 | 新 payload 支持 `draw_ids`,老 payload 只有单个 `draw_id` 时回填 | `decodeDrawOutboxPayload` | 已验收:单抽旧 payload 仍能发放 | | 钱包返奖 | 用聚合 `draw_id` 生成钱包幂等 command,只加币一次 | `Service.processDrawOutbox`、`walletv1.CreditLuckyGiftReward` 调用 | 已验收:批量 worker 单测覆盖一次钱包请求 | | 房间开奖结果 IM | worker 投递 `lucky_gift_drawn` 房间群消息 | `publishLuckyGiftDrawn` | 已验收:发布失败时 outbox 可重试,不提前 granted | | 区域播报 | 符合条件的中奖写区域播报 outbox | `shouldPublishLuckyGiftRegionBroadcast`、`publishLuckyGiftRegionBroadcast`、`luckyGiftRegionBroadcastPayload` | 已验收:当前阈值为有区域、有效奖励、聚合倍数不低于 1x | | 成功收敛 | 钱包、区域播报、房间 IM 都成功后批量标记 draw records 为 granted | `MarkLuckyGiftDrawsGranted`、`markLuckyGiftDraws`、`MarkLuckyGiftOutboxDelivered` | 已验收:批量更新按 500 个 draw_id 分片 | | 失败收敛 | 未返奖且达到最大重试时,批量标记 pending draw failed | `markOutboxFailed`、`MarkLuckyGiftDrawsFailed` | 已验收:已返奖但 IM 失败不会覆盖账务事实 | | 重试退避 | outbox 失败按指数退避重试 | `luckyGiftBackoff`、`MarkLuckyGiftOutboxRetryable` | 已验收:错误保存在 `activity_outbox.last_error` | ## 六、后台配置和审计 | 功能点 | HTTP 入口 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | 获取单个奖池配置 | `GET /admin/activity/lucky-gifts/config?pool_id=` | `Handler.GetLuckyGiftConfig`、`AdminLuckyGiftServer.GetLuckyGiftConfig`、`Service.GetConfig` | 已验收:未配置返回 disabled 草稿 | | 发布配置 | `PUT /admin/activity/lucky-gifts/config` | `Handler.UpsertLuckyGiftConfig`、`configToProto`、`Service.UpsertConfig`、`Repository.PublishLuckyGiftRuleConfig` | 已验收:百分比/倍率转换为 ppm 后发布 | | 配置列表 | `GET /admin/activity/lucky-gifts/configs` | `Handler.ListLuckyGiftConfigs`、`Service.ListConfigs`、`Repository.ListLuckyGiftRuleConfigs` | 已验收:每个奖池只返回最新版本 | | 抽奖明细 | `GET /admin/activity/lucky-gifts/draws` | `Handler.ListLuckyGiftDraws`、`Service.ListDraws`、`Repository.ListLuckyGiftDraws`、`luckyDrawWhereClause` | 已验收:分页上限由 service 控制,支持状态筛选 | | 抽奖汇总 | `GET /admin/activity/lucky-gifts/draw-summary` | `Handler.GetLuckyGiftDrawSummary`、`Service.GetDrawSummary`、`Repository.GetLuckyGiftDrawSummary` | 已验收:统计总抽数、参与用户、消耗、返奖、pending/granted/failed | | 后台路由权限 | 所有后台入口需要 `lucky-gift:view` 或 `lucky-gift:update` | `RegisterRoutes` | 已验收:路由注册在后台模块内 | ## 七、数据表 | 表 | 用途 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | `lucky_gift_rule_versions` | v2 不可变规则版本 | `PublishLuckyGiftRuleConfig`、`getLuckyGiftRuleConfig` | 已验收:schema 在 `001_activity_service.sql` 和迁移兜底中存在 | | `lucky_gift_stage_tiers` | 每个规则版本的阶段奖档概率 | `PublishLuckyGiftRuleConfig`、`getLuckyGiftRuleStages` | 已验收:按 stage 和倍率顺序读取 | | `lucky_rtp_windows` | RTP 观察窗口 | `getOpenLuckyRTPWindow`、`createLuckyRTPWindow`、`persistLuckyBatchDraw`、`applyLuckyDraw` | 已验收:窗口关闭后创建新窗口 | | `lucky_pools` | 平台、房间、礼物三层基础奖池 | `getOrCreateLuckyPool`、`updateLuckyPoolNet`、`creditLuckyPool`、`debitLuckyPool` | 已验收:容量过滤使用 reserve floor | | `lucky_room_atmosphere_pools` | 房间气氛池 | `getOrCreateLuckyAtmosphere`、`updateLuckyAtmosphereNet`、`creditLuckyAtmosphere` | 已验收:独立于 Room Cell | | `lucky_activity_budgets` | 活动补贴预算 | `ensureLuckyActivityBudget`、`getLuckyActivityRemaining` | 已验收:总预算和 UTC 日预算都支持 | | `lucky_user_states` | 用户阶段、累计流水、等价抽数和连亏 | `getLuckyUserState`、`getLuckyUserStateForUpdate`、`persistLuckyBatchDraw`、`applyLuckyDraw` | 已验收:包含 `cumulative_wager_coins`、`equivalent_draws`、`loss_streak` | | `lucky_risk_counters` | 风控窗口累计返奖 | `getLuckyRiskCounters`、`getOrCreateLuckyRiskCounter`、`updateLuckyRiskCounters` | 已验收:用户小时、用户日、设备日、房间小时、主播日五类 scope | | `lucky_draw_records` | 单抽审计事实 | `insertLuckyDrawRecords`、`applyLuckyDraw`、`ListLuckyGiftDraws` | 已验收:批量送礼仍保留单抽粒度 | | `activity_outbox` | 幸运礼物返奖和表现副作用 | `insertLuckyAggregateDrawOutbox`、`ClaimPendingLuckyGiftOutbox`、`MarkLuckyGiftOutboxDelivered` | 已验收:批量送礼只写一条 `LuckyGiftDrawn` outbox | ## 八、运行边界 | 场景 | 当前处理 | 代码函数实现 | 验收 | | --- | --- | --- | --- | | 奖池未启用 | 送礼前检查或扣费后抽奖都会失败,不开奖 | `CheckLuckyGift`、`ExecuteLuckyGiftDraw`、`Service.Check`、`Service.Draw` | 已验收:未启用返回 RuleNotActive 或 enabled=false | | 所有正奖档不可支付 | 本抽返回 0 倍 `no_payable_tier`,不透支奖池和风控 | `selectLuckyCandidate` | 已验收:单测覆盖全正奖不可支付回退 0 倍 | | 小额礼物 | 阶段按累计流水折算,单抽奖励按单份金额缩放,风控上限不被缩小 | `luckyRuntimeConfig`、`luckyEquivalentDraws`、`luckyRiskCapacity` | 已验收:单测覆盖大额后小额仍可中小奖 | | 高倍奖 | 发布校验要求高倍档 `HighWaterOnly`,运行时仍受奖池和风控过滤 | `validateRuleStage`、`selectLuckyCandidate` | 已验收:高倍率未标高水位会拒绝发布 | | 批量 777 份 | 抽 777 次,保留 777 条明细,状态写入和 outbox 聚合 | `executeOptimizedLuckyGiftDrawBatch`、`stageLuckyBatchDraw`、`persistLuckyBatchDraw` | 已验收:代码结构已移出循环内 SQL 状态写入 | | worker 重试 | 钱包用聚合 `draw_id` 幂等,outbox 失败可重试 | `processDrawOutbox`、`MarkLuckyGiftOutboxRetryable` | 已验收:成功后 delivered,不重复加币 | | 已返奖但 IM 失败 | 不把 draw 标 failed,只让 outbox failed 并保留告警空间 | `markOutboxFailed` | 已验收:`alreadyCredited` 分支不覆盖 draw 状态 | ## 九、代码验收清单 | 验收项 | 命令或检查 | 当前结果 | | --- | --- | --- | | 旧专属文档删除 | `test ! -e docs/幸运礼物Activity服务开发.md` 等 9 个旧文档 | 通过 | | 新文档唯一入口 | `test -f docs/幸运礼物实现文档.md` | 通过 | | 函数引用存在 | `rg -n "executeOptimizedLuckyGiftDrawBatch|stageLuckyBatchDraw|insertLuckyAggregateDrawOutbox|MarkLuckyGiftDrawsGranted"` | 通过 | | Activity 幸运礼物服务测试 | `go test ./services/activity-service/internal/service/luckygift` | 通过 | | Activity MySQL 抽奖测试 | `go test ./services/activity-service/internal/storage/mysql` | 通过 | | Room 送礼链路测试 | `go test ./services/room-service/internal/room/service` | 通过 | | Gateway 检查接口测试 | `go test ./services/gateway-service/internal/transport/http/activityapi` | 通过 | | Admin 幸运礼物模块测试 | `cd server/admin && go test ./internal/modules/luckygift` | 通过 | ## 十、后续维护规则 1. 后续幸运礼物规则、字段、算法或接口变化,先改代码,再同步本文档对应功能点和验收项。 2. 新增后台字段必须同时标明:后台 DTO、proto 字段、domain 字段、发布校验、运行读取、数据库字段。 3. 新增抽奖行为必须写明是否影响:候选概率、奖池水位、RTP 观察、风控计数、draw record、outbox payload、客户端同步响应。 4. 批量抽奖不能重新引入“每抽一条发奖 outbox”。抽奖可以是 N 次,钱包发奖必须按一次送礼命令聚合。 5. 不允许用 RTP 缺口在运行时动态改概率。RTP 只在发布校验和窗口观察中使用;运行时只能过滤不可支付奖档。