hyapp-server/docs/幸运礼物实现文档.md

24 KiB
Raw Blame History

幸运礼物实现文档

本文档描述当前代码里的幸运礼物服务端实现。旧的产品方案、重构分步文档和早期 activity-service 开发文档已经下线服务端实现以后续本文档为准Flutter App 对接以 幸运礼物 Flutter 对接 为准。

当前实现不兼容旧玩法假设。幸运礼物以 pool_id 为规则维度,以 activity-service 为规则、抽奖、奖池、RTP 窗口、风控、抽奖事实和 outbox owner扣费和返奖入账属于 wallet-service;房间状态、热度、礼物榜和房间 outbox 属于 room-service

一、核心原则

功能点 当前规则 代码函数实现 验收
服务边界 activity-service 只做幸运礼物规则和开奖结果,不改 Room Cell不直接改钱包表返奖必须通过 wallet-service gRPC services/activity-service/internal/service/luckygift.Service.Drawservices/activity-service/internal/service/luckygift.Service.processDrawOutboxservices/room-service/internal/room/service.Service.SendGift 已验收:抽奖在扣费后执行,正常返奖走同步 wallet gRPC 快路径outbox 只做补偿和表现
规则维度 线上规则按 pool_id 读取最新版本,不按真实 gift_id 读取旧规则 Repository.GetLuckyGiftRuleConfigRepository.getLuckyGiftRuleConfigluckyPoolID 已验收:代码读取 lucky_gift_rule_versionslucky_gift_stage_tiers
返奖率控制 后台发布时校验阶段期望 RTP 落在 target_rtp_ppm ± control_band_ppm,运行时不按 RTP 缺口改概率 validateRuleConfigvalidateRuleStageExpectedRTPselectLuckyCandidate 已验收:发布校验函数存在,候选随机只过滤不可支付档
用户体验 用户阶段按累计金币流水折算等价抽数,避免不同价格礼物用真实抽数误判阶段 luckyEquivalentDrawsluckyExperiencePoolgetLuckyUserStateForUpdate 已验收:状态表维护 cumulative_wager_coinsequivalent_drawsloss_streak
批量抽奖 gift_count=N 表示抽 N 次;每一抽按单份金币计算奖励,响应返回总中奖倍数和总奖励 executeLuckyGiftDrawBatchluckyDrawUnitSpendstageLuckyBatchDrawluckyAggregateDrawResults 已验收:单测覆盖 10 抽返回总倍数 11x 的聚合逻辑
聚合发放 批量抽奖可以有 N 条抽奖明细,但正常路径只同步给钱包加币 1 次outbox 保留 1 条聚合事件用于补偿和表现 insertLuckyAggregateDrawOutboxcreditLuckyGiftRewardFastPathdecodeDrawOutboxPayloadMarkLuckyGiftDrawsGranted 已验收:批量只按聚合 draw 发放worker 遇到已 granted 会跳过钱包

二、配置模型

2.1 Proto 契约

功能点 字段或 RPC 代码函数实现 验收
App/房间抽奖元信息 LuckyGiftMeta,包含 command_idpool_idgift_countcoin_spentpaid_at_msvisible_region_id api/proto/activity/v1/activity.proto LuckyGiftMetaLuckyGiftServer.ExecuteLuckyGiftDraw 已验收:房间送礼只把 wallet 已结算 coin_spent 传给 activity
抽奖结果 LuckyGiftDrawResult,包含 multiplier_ppmeffective_reward_coinsreward_statuswallet_transaction_idcoin_balance_after、RTP 窗口和阶段字段 luckyDrawResultToProtoluckyGiftResultFromProto 已验收room 返回 LuckyGift 字段给送礼用户,中奖同步成功时可直接刷新余额
后台规则 LuckyGiftRuleConfigLuckyGiftRuleStageLuckyGiftRuleTier luckyRuleConfigFromProtoluckyRuleConfigToProtoconfigToProtoconfigFromProto 已验收:后台百分比和倍率在 handler 内转成 ppm
后台审计 ListLuckyGiftDrawsGetLuckyGiftDrawSummary AdminLuckyGiftServer.ListLuckyGiftDrawsAdminLuckyGiftServer.GetLuckyGiftDrawSummary 已验收:按 pool_id/gift_id/user_id/room_id/status 查询

2.2 配置字段语义

字段 业务含义 代码函数实现 验收
target_rtp_ppm 基础返奖目标,例如 950000 表示 95% validateRuleConfigluckyRuntimeConfigFromRuleConfig 已验收:发布入口限制 10% 到 99%
pool_rate_ppm 每笔扣费进入基础奖池的比例,必须不低于目标 RTP validateRuleConfigluckyBasePoolIncreditLuckyBasePools 已验收:低于目标 RTP 会拒绝发布
settlement_window_wager RTP 结算窗口金币流水,运行时按参考价格折算窗口抽数 luckyRuntimeConfigFromRuleConfigluckyCeilDiv 已验收:窗口抽数由 settlement_window_wager / gift_price_reference 得到
control_band_ppm 发布时允许阶段期望 RTP 波动带 validateRuleStageExpectedRTP 已验收:阶段期望 RTP 不在区间内会返回 InvalidArgument
gift_price_reference 阶段阈值和奖档金额的配置参考价格 luckyRuntimeConfigFromRuleConfigluckyRuntimeConfig 已验收:真实抽奖按单抽 coin_spent 等比缩放奖档金额
novice_max_equivalent_draws 新手阶段最大等价抽数 luckyExperiencePoolDefaultRuleConfig 已验收:小额和高价礼物统一按累计流水折算阶段
normal_max_equivalent_draws 正常阶段最大等价抽数,超过后进入高级阶段 luckyExperiencePoolDefaultRuleConfig 已验收:正常阈值必须大于等于新手阈值
风控上限字段 单次、用户小时、用户日、设备日、房间小时、主播日返奖上限 validateRuleConfigluckyRiskCapacityupdateLuckyRiskCounters 已验收:所有上限必须为正数,候选过滤时统一参与
stages[].tiers[] 三个阶段的基础候选概率和倍率 validateRuleStagevalidateRuleStageExpectedRTPluckyRuntimeTiersFromRuleStages 已验收:每阶段启用概率和必须为 1000000且必须有 0x 档和基础 RTP 档

2.3 默认草稿和发布

功能点 行为 代码函数实现 验收
默认草稿 后台读取未配置奖池时返回 disabled 草稿,不会自动生效 Service.GetConfigDefaultRuleConfig 已验收:未找到配置时返回草稿,运行抽奖仍要求已发布启用规则
参数归一化 补齐 stage、reward_source、broadcast_level清理空格 normalizeRuleConfig 已验收:后台提交的空 reward_source 默认 base_rtp
发布校验 校验 RTP、入池比例、窗口、阶段、概率、风控和高倍水位 validateRuleConfigvalidateRuleStagevalidRuleStagevalidRewardSourcevalidBroadcastLevel 已验收service 层发布前执行完整校验
不可变版本 每次发布新增 rule_version,不覆盖历史版本 Repository.PublishLuckyGiftRuleConfig 已验收:事务里锁最新版本并写入 lucky_gift_rule_versionslucky_gift_stage_tiers
最新版本读取 后台和运行时都读取指定 pool_id 的最新版本 Repository.GetLuckyGiftRuleConfigRepository.ListLuckyGiftRuleConfigsRepository.getLuckyGiftRuleStages 已验收:列表按每个 pool 的最大 rule_version 返回

三、送礼链路

3.1 送礼前检查

功能点 行为 代码函数实现 验收
App 检查接口 POST /api/v1/activities/lucky-gifts/check 只做资格检查不扣费、不抽奖App 路径不随内部 v2 规则实现变化 activityapi.Handler.checkLuckyGift 已验收:接口要求 room_id/gift_id/pool_id 必填
gRPC 检查 gateway 或 room-service 通过 gRPC 查奖池是否启用 LuckyGiftServer.CheckLuckyGiftService.Check 已验收:入参缺失返回 InvalidArgument
规则读取 pool_id 查 v2 最新规则,未配置返回 disabled Repository.CheckLuckyGiftRepository.getLuckyGiftRuleConfig 已验收:不再从旧 lucky_gift_rules 作为运行规则来源
阶段展示 检查响应返回当前用户所在体验池 getLuckyUserStateluckyExperiencePool 已验收:响应包含 experience_pool

3.2 扣费后抽奖

功能点 行为 代码函数实现 验收
幸运礼物识别 显式传 pool_id 或钱包礼物类型为 lucky/super_lucky 时触发抽奖 Service.shouldDrawLuckyGift 已验收:旧客户端可由礼物类型触发,新客户端应传明确 pool_id
先扣费 房间链路先调用 wallet 扣费,成功后才调用 activity 抽奖 Service.SendGiftwallet.DebitGift 调用段 已验收:抽奖使用 billing.GetCoinSpent(),不信任客户端价格
抽奖命令 使用房间命令 ID 作为幂等根 ID Service.SendGiftLuckyGiftServer.ExecuteLuckyGiftDrawService.Draw 已验收:重复命令由抽奖记录幂等返回
设备和主播 scope 设备优先取 session_id主播优先取房主 ID luckyGiftDeviceIDluckyGiftAnchorID 已验收scope 不为空,风控计数可落表
同步响应 当前送礼用户立即拿到抽奖结果;中奖快路径成功时同时拿到返奖交易 ID 和账后余额 creditLuckyGiftRewardFastPathluckyGiftResultFromProtoroomv1.SendGiftResponse.LuckyGift 已验收room response 带 lucky_gift.wallet_transaction_idlucky_gift.coin_balance_after
房间事实 送礼本身仍写 Room Cell、热度、榜单和 room outbox Service.SendGiftoutbox.Build 已验收:幸运礼物不替代普通送礼事实

四、抽奖事务

4.1 入口和幂等

功能点 行为 代码函数实现 验收
service 入参校验 command、用户、设备、房间、主播、礼物、金额、数量必须有效 Service.Draw 已验收:gift_count <= 0 会被拒绝
MySQL 事务 抽奖事实、奖池、窗口、风控、用户状态和 outbox 在一个事务内提交 Repository.ExecuteLuckyGiftDraw 已验收:事务提交前不调用外部 RPC
单抽幂等 单抽按原始 command_id 查已有 draw executeSingleLuckyGiftDrawgetLuckyDrawByCommand 已验收:已存在时直接返回已有结果
批量幂等 批量按 command_id#序号 查子抽;部分存在时报冲突 executeLuckyGiftDrawBatchcollectLuckyBatchDrawsByCommandluckyDrawSubCommandID 已验收:避免重试补写导致重复返奖

4.2 单抽路径

功能点 行为 代码函数实现 验收
规则锁定 抽奖时锁定当前 pool_id 最新规则版本 executeSingleLuckyGiftDrawgetLuckyGiftRuleConfig(... forUpdate=true) 已验收:规则未启用返回 RuleNotActive
运行配置转换 v2 阶段奖档转换为运行候选,窗口流水转等价抽数 luckyRuntimeConfigFromRuleConfigluckyRuntimeTiersFromRuleStagesluckyRuntimePoolFromRuleStage 已验收:只读取 SourceBaseRTP 作为基础候选
实际金额缩放 同一奖池可服务不同价格礼物,奖励按本次 coin_spent 缩放 luckyRuntimeConfigluckyScaleReward 已验收:风险上限保持绝对金币,不随小额礼物缩小
阶段推进 累计流水折算等价抽数并选择阶段 luckyEquivalentDrawsluckyExperiencePoolluckyStageFeedback 已验收:阶段反馈只是表现,不强制发币
RTP 窗口 记录 wager、target payout、actual base payout 和 carry getOpenLuckyRTPWindowluckyRTPWindow.withCurrentWagercreateLuckyRTPWindow 已验收RTP 只观察结算,不直接改随机概率
三层基础奖池 平台、房间、礼物三层共同承担基础返奖,任一层不足都会过滤奖档 getOrCreateLuckyPoolcreditLuckyBasePoolsdebitLuckyPoolluckyWeightedPoolCapacity 已验收:候选过滤前先入池,容量按责任权重反推
房间气氛池 额外体验预算独立于基础 RTP getOrCreateLuckyAtmospherecreditLuckyAtmosphere 已验收:气氛奖励不写入基础 RTP payout
活动预算 活动补贴预算按 UTC 日切分 getLuckyActivityRemaininggetOrCreateLuckyActivityTotalRemaining 已验收:预算不足时不会产生活动补贴候选
风控计数 单次、用户、设备、房间、主播上限统一过滤可支付候选 getLuckyRiskCountersluckyRiskCapacityupdateLuckyRiskCounters 已验收:并发抽奖通过 FOR UPDATE 锁计数行
候选随机 只使用后台阶段概率,过滤不可支付档,不注入 RTP 修正档 selectLuckyCandidateluckyConfiguredWeightCandidatesluckySecureIndex 已验收:候选为空时落 no_payable_tier 0 倍,不透支
事实落库 写入 draw 明细和必要 outbox applyLuckyDrawluckyInitialRewardStatusluckyDrawNeedsOutbox 已验收:无奖励且无反馈时直接 granted

4.3 批量路径

功能点 行为 代码函数实现 验收
单份金额拆分 coin_spentgift_count 拆成 N 份,余数分配给前几抽 luckyDrawUnitSpend 已验收:单测覆盖 1001 金币拆成 101 + 9 个 100
循环外锁共享状态 规则、用户状态、RTP 窗口、奖池、气氛池、活动预算、风控计数只读取和锁定一次 executeOptimizedLuckyGiftDrawBatch 已验收:批量入口不再循环调用完整 executeSingleLuckyGiftDraw
循环内只做业务推进 每一抽在内存里推进阶段、窗口、入池、候选、出池、风控和 draw 明细 stageLuckyBatchDrawcreditLuckyPoolInMemorydebitLuckyPoolInMemoryincrementLuckyRiskCountersInMemory 已验收:避免 777 抽产生 777 轮 SQL 状态写入
批量落库 窗口、奖池、气氛池、风控、用户状态按本批净变化写一次 persistLuckyBatchDrawupdateLuckyPoolNetupdateLuckyAtmosphereNet 已验收:写入顺序保证 outbox 指向已存在 draw records
明细保留 每一抽仍保留一条 lucky_draw_records,用于审计和统计抽奖次数 insertLuckyDrawRecordsluckyDrawRecordArgsluckyDrawRecordSnapshots 已验收:批量 INSERT每 200 条一批
聚合响应 批量响应把金额、倍数、阶段反馈和高倍标记聚合 luckyAggregateDrawResultsluckyAggregateBudgetSourcesJSON 已验收:总倍数为各命中倍数相加,不是平均倍率
聚合发放 整批只写一条 outboxpayload 里有聚合 draw_id 和全部 draw_ids insertLuckyAggregateDrawOutbox 已验收:待发放队列按送礼命令增长,不按中奖子抽增长

五、返奖快路径和房间表现

功能点 行为 代码函数实现 验收
同步返奖快路径 抽奖事务提交后,如果 effective_reward_coins > 0,立即调用 wallet-service CreditLuckyGiftReward;成功后把 draw 标记为 granted 并把交易 ID、账后余额写回响应 Service.DrawcreditLuckyGiftRewardFastPathcreditLuckyGiftRewardMarkLuckyGiftDrawsGranted 已验收:不在 activity MySQL 事务里调用 wallet成功响应可直接给 App 刷余额
worker 循环 持续 claim LuckyGiftDrawn outbox但它现在是补偿和表现投递路径不是正常发币主路径 Service.RunWorkerService.ProcessPendingDrawOutboxRepository.ClaimPendingLuckyGiftOutbox 已验收claim 使用 delivering 锁和 lock TTL
payload 兼容 新 payload 支持 draw_ids,老 payload 只有单个 draw_id 时回填 decodeDrawOutboxPayload 已验收:单抽旧 payload 仍能发放
钱包补偿 worker 先检查 draw 是否已 granted;已发奖则跳过 wallet未发奖才用聚合 draw_id 生成幂等 command 补发一次 Service.processDrawOutboxGetLuckyGiftDrawRewardStatewalletv1.CreditLuckyGiftReward 调用 已验收wallet 成功但 activity 标记失败时worker 可用同一 command 补标;已 granted 不重复加币
房间开奖结果 IM worker 投递 lucky_gift_drawn 房间群消息 publishLuckyGiftDrawn 已验收:发布失败时 outbox 可重试;已返奖的 draw 不会因为 IM 失败被标 failed
区域播报 符合条件的中奖写区域播报 outbox shouldPublishLuckyGiftRegionBroadcastpublishLuckyGiftRegionBroadcastluckyGiftRegionBroadcastPayload 已验收:当前阈值为有区域、有效奖励、聚合倍数不低于 1x
成功收敛 钱包成功后 draw records 先收敛为 granted;区域播报和房间 IM 成功后 outbox 标记 delivered MarkLuckyGiftDrawsGrantedmarkLuckyGiftDrawsMarkLuckyGiftOutboxDelivered 已验收:批量更新按 500 个 draw_id 分片
失败收敛 未返奖且达到最大重试时,批量标记 pending draw failed markOutboxFailedMarkLuckyGiftDrawsFailed 已验收:已返奖但 IM 失败不会覆盖账务事实
重试退避 outbox 失败按指数退避重试 luckyGiftBackoffMarkLuckyGiftOutboxRetryable 已验收:错误保存在 activity_outbox.last_error

六、后台配置和审计

功能点 HTTP 入口 代码函数实现 验收
获取单个奖池配置 GET /admin/activity/lucky-gifts/v2/config?pool_id= Handler.GetLuckyGiftConfigAdminLuckyGiftServer.GetLuckyGiftConfigService.GetConfig 已验收:未配置返回 disabled 草稿
发布配置 PUT /admin/activity/lucky-gifts/v2/config Handler.UpsertLuckyGiftConfigconfigToProtoService.UpsertConfigRepository.PublishLuckyGiftRuleConfig 已验收:百分比/倍率转换为 ppm 后发布
配置列表 GET /admin/activity/lucky-gifts/v2/configs Handler.ListLuckyGiftConfigsService.ListConfigsRepository.ListLuckyGiftRuleConfigs 已验收:每个奖池只返回最新版本
抽奖明细 GET /admin/activity/lucky-gifts/v2/draws Handler.ListLuckyGiftDrawsService.ListDrawsRepository.ListLuckyGiftDrawsluckyDrawWhereClause 已验收:分页上限由 service 控制,支持状态筛选
抽奖汇总 GET /admin/activity/lucky-gifts/v2/draw-summary Handler.GetLuckyGiftDrawSummaryService.GetDrawSummaryRepository.GetLuckyGiftDrawSummary 已验收统计总抽数、参与用户、消耗、返奖、pending/granted/failed
后台路由权限 所有后台入口需要 lucky-gift:viewlucky-gift:update RegisterRoutes 已验收:路由注册在后台模块内

七、数据表

用途 代码函数实现 验收
lucky_gift_rule_versions v2 不可变规则版本 PublishLuckyGiftRuleConfiggetLuckyGiftRuleConfig 已验收schema 在 001_activity_service.sql 和迁移兜底中存在
lucky_gift_stage_tiers 每个规则版本的阶段奖档概率 PublishLuckyGiftRuleConfiggetLuckyGiftRuleStages 已验收:按 stage 和倍率顺序读取
lucky_rtp_windows RTP 观察窗口 getOpenLuckyRTPWindowcreateLuckyRTPWindowpersistLuckyBatchDrawapplyLuckyDraw 已验收:窗口关闭后创建新窗口
lucky_pools 平台、房间、礼物三层基础奖池 getOrCreateLuckyPoolupdateLuckyPoolNetcreditLuckyPooldebitLuckyPool 已验收:容量过滤使用 reserve floor
lucky_room_atmosphere_pools 房间气氛池 getOrCreateLuckyAtmosphereupdateLuckyAtmosphereNetcreditLuckyAtmosphere 已验收:独立于 Room Cell
lucky_activity_budgets 活动补贴预算 ensureLuckyActivityBudgetgetLuckyActivityRemaining 已验收:总预算和 UTC 日预算都支持
lucky_user_states 用户阶段、累计流水、等价抽数和连亏 getLuckyUserStategetLuckyUserStateForUpdatepersistLuckyBatchDrawapplyLuckyDraw 已验收:包含 cumulative_wager_coinsequivalent_drawsloss_streak
lucky_risk_counters 风控窗口累计返奖 getLuckyRiskCountersgetOrCreateLuckyRiskCounterupdateLuckyRiskCounters 已验收:用户小时、用户日、设备日、房间小时、主播日五类 scope
lucky_draw_records 单抽审计事实 insertLuckyDrawRecordsapplyLuckyDrawListLuckyGiftDraws 已验收:批量送礼仍保留单抽粒度
activity_outbox 幸运礼物钱包补偿和表现副作用,不再承载正常发币主路径 insertLuckyAggregateDrawOutboxClaimPendingLuckyGiftOutboxMarkLuckyGiftOutboxDelivered 已验收:批量送礼只写一条 LuckyGiftDrawn outbox已 granted 时 worker 跳过 wallet

八、运行边界

场景 当前处理 代码函数实现 验收
奖池未启用 送礼前检查或扣费后抽奖都会失败,不开奖 CheckLuckyGiftExecuteLuckyGiftDrawService.CheckService.Draw 已验收:未启用返回 RuleNotActive 或 enabled=false
所有正奖档不可支付 本抽返回 0 倍 no_payable_tier,不透支奖池和风控 selectLuckyCandidate 已验收:单测覆盖全正奖不可支付回退 0 倍
小额礼物 阶段按累计流水折算,单抽奖励按单份金额缩放,风控上限不被缩小 luckyRuntimeConfigluckyEquivalentDrawsluckyRiskCapacity 已验收:单测覆盖大额后小额仍可中小奖
高倍奖 发布校验要求高倍档 HighWaterOnly,运行时仍受奖池和风控过滤 validateRuleStageselectLuckyCandidate 已验收:高倍率未标高水位会拒绝发布
批量 777 份 抽 777 次,保留 777 条明细,状态写入和 outbox 聚合 executeOptimizedLuckyGiftDrawBatchstageLuckyBatchDrawpersistLuckyBatchDraw 已验收:代码结构已移出循环内 SQL 状态写入
worker 重试 钱包补偿用聚合 draw_id 幂等outbox 失败可重试;已 granted 的 draw 只补 IM/播报,不重复加币 processDrawOutboxGetLuckyGiftDrawRewardStateMarkLuckyGiftOutboxRetryable 已验收:成功后 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 通过

十、后续维护规则

  1. 后续幸运礼物规则、字段、算法或接口变化,先改代码,再同步本文档对应功能点和验收项。
  2. 新增后台字段必须同时标明:后台 DTO、proto 字段、domain 字段、发布校验、运行读取、数据库字段。
  3. 新增抽奖行为必须写明是否影响候选概率、奖池水位、RTP 观察、风控计数、draw record、outbox payload、客户端同步响应。
  4. 批量抽奖不能重新引入“每抽一条发奖 outbox”。抽奖可以是 N 次,钱包发奖必须按一次送礼命令聚合。
  5. 不允许用 RTP 缺口在运行时动态改概率。RTP 只在发布校验和窗口观察中使用;运行时只能过滤不可支付奖档。