22 KiB
22 KiB
幸运礼物实现文档
本文档描述当前代码里的幸运礼物实现。旧的产品方案、重构分步文档、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 |
| 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 |
通过 |
十、后续维护规则
- 后续幸运礼物规则、字段、算法或接口变化,先改代码,再同步本文档对应功能点和验收项。
- 新增后台字段必须同时标明:后台 DTO、proto 字段、domain 字段、发布校验、运行读取、数据库字段。
- 新增抽奖行为必须写明是否影响:候选概率、奖池水位、RTP 观察、风控计数、draw record、outbox payload、客户端同步响应。
- 批量抽奖不能重新引入“每抽一条发奖 outbox”。抽奖可以是 N 次,钱包发奖必须按一次送礼命令聚合。
- 不允许用 RTP 缺口在运行时动态改概率。RTP 只在发布校验和窗口观察中使用;运行时只能过滤不可支付奖档。