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