hyapp-server/docs/幸运礼物线上落地文档.md

15 KiB
Raw Blame History

幸运礼物线上落地文档

本文档按当前代码实现整理幸运礼物的服务边界、配置语义、抽奖链路、体验模拟方式和已知问题。当前幸运礼物由 activity-service 持有规则、RTP 窗口、奖池、风控计数、抽奖事实和异步副作用 outbox金币扣费与返奖入账仍归 wallet-service,房间状态、热度、榜单和送礼消息仍归 room-service

相关代码

模块 文件 当前职责
protobuf 契约 api/proto/activity/v1/activity.proto LuckyGiftConfigmultiplier_ppmsLuckyGiftDrawResult.multiplier_ppm、后台配置和审计 RPC
房间送礼主链路 services/room-service/internal/room/service/gift.go 送礼前可检查奖池,扣费成功后同步调用 ExecuteLuckyGiftDraw,把 lucky_gift 放入 SendGiftResponse
幸运礼物领域模型 services/activity-service/internal/domain/luckygift/lucky_gift.go 规则、奖档、Check/Draw 命令、抽奖结果和审计查询结构
规则归一化 services/activity-service/internal/service/luckygift/config.go 后台输入 multiplier_ppms 后生成三档体验池奖档;权重不是后台配置项
抽奖服务 services/activity-service/internal/service/luckygift/service.go Check、Draw、outbox worker、返奖入账、房间 IM、区域播报
MySQL 抽奖事务 services/activity-service/internal/storage/mysql/lucky_gift_repository.go 规则锁、RTP 窗口、三层池、风控计数、候选过滤、随机、draw fact、outbox
activity schema services/activity-service/deploy/mysql/initdb/001_activity_service.sql lucky_gift_ruleslucky_rtp_windowslucky_poolslucky_user_stateslucky_draw_records 等表
gateway 检查接口 services/gateway-service/internal/transport/http/activityapi/lucky_gift_handler.go POST /api/v1/activities/lucky-gifts/check,当前要求 room_id/gift_id/pool_id 都非空
gateway 送礼接口 services/gateway-service/internal/transport/http/roomapi/room_handler.go POST /api/v1/rooms/gift/send 透传 pool_idcommand_id
admin 配置接口 server/admin/internal/modules/luckygift/handler.go 后台读写 pool_id 维度的规则,支持 multiplier_ppms
体验模拟工具 tools/lucky-gift-v2-demo/main.go 离线模拟用户 RTP 分布、奖池水位、风控限制和高倍命中

当前范围

能力 状态 代码事实
送礼前检查 已实现 gateway check 要求 pool_idactivity 按 pool_id 读规则,未配置返回 not_configured
扣费后抽奖 已实现 room-service 先调 wallet-service.DebitGift,扣费成功后调 activity-service.ExecuteLuckyGiftDraw
同步返回抽奖结果 已实现 SendGiftResponse.lucky_gift 直接返回给送礼用户
后台多奖池 已实现 规则主键实际是 (app_code, gift_id),但当前 gift_id 列保存的是 pool_id
多倍数配置 已实现 后台配置 multiplier_ppms,服务端生成每个体验池的候选倍率
RTP 双窗口 已实现 当前窗口 scope 是 poolpool_gift,都按 pool_id 隔离
三层基础奖池 已实现 platform / room / gift 三层池按 pool_id 隔离room 池再加 room_id
风控计数 已实现 user hour/day、device day、room hour、anchor day
房间气氛池 已实现 只在基础 RTP 不需要强制追平时参与候选
活动补贴池 已实现 只在基础 RTP 不需要强制追平时参与候选
返奖入账 已实现 抽奖事务写 activity_outboxworker 调 wallet-service.CreditLuckyGiftReward
房间中奖 IM 已实现 worker 投递 lucky_gift_drawn 群消息
区域中奖播报 已实现 multiplier_ppm >= 1x 且有 visible_region_id 时写区域播报 outbox
完整候选审计 未完整 candidate_tiers_json 当前只保存命中候选和限制标记,不保存完整过滤后候选表
随机摘要审计 未实现 当前没有 random_digest 字段落库

主链路

Client
  -> gateway POST /api/v1/rooms/gift/send
  -> room-service SendGift
  -> 可选 CheckLuckyGift(pool_id 非空时送礼前检查)
  -> wallet-service DebitGift
  -> activity-service ExecuteLuckyGiftDraw
  -> activity-service 本地事务写 draw fact + activity_outbox
  -> room-service 更新 Room Cell 热度、榜单、礼物 outbox
  -> gateway 返回 data.lucky_gift
  -> lucky gift worker 补偿返奖入账和 lucky_gift_drawn IM

当前有两个触发入口:

入口 行为
请求显式传 pool_id room-service 会在扣费前调用 CheckLuckyGift,规则未启用则不扣费
请求未传 pool_id 但钱包返回 gift_type_code=lucky/super_lucky room-service 会在扣费后按默认奖池抽奖,activity-service 把空 pool_id 归一化为 default

客户端 check 接口当前要求 pool_id 必填,所以 App 不应依赖“空 pool_id 默认奖池”的隐式路径。礼物面板需要下发明确的 pool_id

多倍数配置

后台现在的核心配置不是固定概率表,而是倍率列表:

{
  "pool_id": "lucky",
  "gift_price": 100,
  "target_rtp_ppm": 950000,
  "pool_rate_ppm": 950000,
  "high_multiplier": 100,
  "large_tier_enabled": true,
  "multiplier_ppms": [
    0,
    200000,
    500000,
    1000000,
    2000000,
    5000000,
    10000000,
    20000000,
    50000000,
    100000000,
    500000000
  ]
}

倍率单位是 ppm

含义 gift_price=100 时基础返奖
0 未中奖 0
200000 0.2x 20
500000 0.5x 50
1000000 1x 100
2000000 2x 200
100000000 100x 10000
500000000 500x 50000

服务端发布配置时会执行这些归一化:

  1. 如果 multiplier_ppms 非空,直接用它去重、排序并过滤负数。
  2. 如果 multiplier_ppms 为空,则从传入 tiers 里反推倍率。
  3. 如果两者都为空,则使用默认倍率列表。
  4. 每个倍率都会生成 noviceintermediateadvanced 三个体验池各一条奖档。
  5. 生成奖档的 weight 固定为 0,真实权重由抽奖时 RTP 窗口动态计算。
  6. multiplier_ppm >= high_multiplier * 1000000 的奖档会标记 high_water_only

这意味着当前后台不能按体验池单独配置概率,也不能直接配置“新手 2x 权重大、中级 5x 权重大”这类固定权重。后台能控制的是可用倍率集合、RTP 目标、窗口大小、奖池水位、风控上限和高倍开关。

抽奖算法事实

抽奖事务按 command_id 幂等。重复请求返回同一条 lucky_draw_records,不会重复消耗奖池或 RTP。

核心步骤:

1. 锁定 command_id 对应 draw fact存在则直接返回
2. 按 pool_id 锁定幸运礼物规则;未启用则失败
3. 用真实 coin_spent 替换配置里的参考 gift_price并等比缩放奖档和风控上限
4. 锁定用户在该 pool_id 下的累计付费抽数,选择 novice/intermediate/advanced
5. 锁定 pool RTP 窗口和 pool_gift RTP 子窗口
6. 锁定 platform、room、gift 三层基础奖池
7. 锁定房间气氛池、活动预算和风控计数
8. 计算本次基础 RTP 最小必付额 min_required
9. 过滤低于 min_required、超过奖池容量、超过风控容量或高水位不足的倍率
10. 从可支付基础候选中按 RTP 缺口自动生成 1 个或 2 个带权候选
11. 当 min_required=0 时,额外加入房间气氛、活动补贴和阶段反馈候选
12. 使用 crypto/rand 做一次加权随机
13. 更新 RTP 窗口、三层池、气氛池、活动预算、风控计数、用户抽数
14. 写 lucky_draw_records有返奖或展示副作用时写 activity_outbox

运行时基础候选权重不是后台配置权重。luckyAutoWeightedBaseCandidates 会按当前窗口缺口选择:

RTP 状态 当前行为
没有基础 RTP 缺口 优先选择 0x 候选;如果倍率列表没有 0,会选择最低可用倍率
缺口正好落在某个倍率以下 只给最接近且可支付的低档或高档候选分配 1000000 权重
缺口落在两个倍率之间 只保留相邻两个倍率,权重按线性插值分配
没有可用候选但必须追平 生成 rtp_balance 修正候选;如果超过容量则返回冲突

因此,多倍数列表不是“所有倍率每次都按固定概率参与随机”。它是 RTP 控制器可选择的离散返奖点。

用户体验模拟

体验模拟工具用于评估参数是否会带来可接受的用户体感分布。它输出全局 RTP、有效 RTP、100k/1m 窗口偏差、奖池水位、用户 RTP 分位数和用户净输赢分位数。

基础命令:

go run ./tools/lucky-gift-v2-demo \
  -users 1000 \
  -devices 900 \
  -rooms 50 \
  -draws-min 0 \
  -draws-max 100000 \
  -unique-draws \
  -base-rtp 95 \
  -cost 500 \
  -global-window 100000 \
  -gift-window 100000 \
  -pool-rate-ppm 950000 \
  -high-multiplier 100 \
  -high-water-pool-multiple 2 \
  -user-csv /tmp/lucky-gift-users.csv

重点看这些输出:

输出 判断方式
base_rtp 基础成本口径,应贴近 base-rtp
effective_rtp 用户可见返还,允许因气氛池和活动补贴高于基础 RTP
100k_blocks max_abs_deviation 应小于 1pp
1m_blocks max_abs_deviation 应小于 0.5pp
high_multiplier_hits 评估高倍实际出现频率是否符合运营预期
risk_cap_limited_draws 过高说明风控上限压得太低,会牺牲 RTP 追平能力
pool_cap_limited_draws 过高说明初始奖池、入池比例或 reserve 配置不健康
user_effective_rtp_p05/median/p95 用户体验分布;单用户不会收敛到全局 RTP
user_net_p05/median/p95 用户净输赢体感;用于判断投诉风险和高倍刺激强度

当前模拟工具的限制:

限制 影响
不能直接输入 multiplier_ppms 它使用内置新手/中级/高级 tier table不能完全复现后台任意倍率列表
使用伪随机 math/rand 便于复现实验,不等同线上 crypto/rand
不连接真实 MySQL 不验证行锁、幂等、outbox 和钱包补偿
不模拟真实礼物面板 只模拟付费抽数、成本、奖池和用户结果分布

使用模拟结论上线前,仍需用真实 activity-service MySQL 测试覆盖配置发布、抽奖事务、重复 command_id、outbox 返奖和 IM 投递。

当前问题

问题 影响 建议动作
lucky_gift_rules.gift_id 实际保存 pool_id 文档、后台和审计容易把真实礼物 ID 与奖池 ID 混淆 后续 DB 字段改名为 pool_id,或在所有接口文档明确“规则表 gift_id 是历史列名”
后台传入 tiers.weight 不生效 运营以为配置了固定概率,但线上按 RTP 缺口自动权重抽 后台隐藏或只读展示 tiers.weight,主编辑项改为 multiplier_ppms
multiplier_ppms 可以不含 0 没有 RTP 缺口时最低正倍率可能被强制选中,用户体感和成本都会异常 后台校验建议强制包含 0,除非明确做“每抽必返”奖池
三个体验池使用同一倍率集合 无法做到新手池低倍密集、高级池高倍更多的差异化倍率表 增加按 pool 分组的倍率配置,或保留统一倍率但明确只能靠阶段和 RTP 控制差异
当前服务不使用后台固定概率 “概率表版本”“权重表”类文档与代码不一致 文档统一改为“倍率列表 + 运行时 RTP 自动权重”
candidate_tiers_json 不保存完整候选表 后台审计无法复盘每次过滤后的完整随机空间 落库完整候选列表、过滤原因、总权重和随机落点
没有 random_digest 不能证明随机材料与候选表一致 增加随机摘要或 seed commit-reveal 方案
同步返回通常是 pending 客户端如果看到返奖就展示“已到账”,会早于钱包入账事实 客户端只在钱包 IM 或余额刷新后更新余额HTTP 只展示“中奖/处理中”
check 接口要求 pool_id,送礼链路又支持空 pool_id 默认奖池 App 行为和服务端隐式兜底不一致 礼物面板必须明确下发 pool_id,客户端幸运礼物送礼必传
区域播报阈值硬编码为 >= 1x 后台不能独立控制哪些中奖进入区域飘屏 增加 broadcast_min_multiplier_ppmbroadcast_min_reward_coins
模拟工具不能输入多倍数列表 不能验证“多个倍数配置”对实际体感的影响 tools/lucky-gift-v2-demo 增加 -multipliers-ppm 参数,或新增基于线上 Config 的模拟入口

后台配置建议

目标 建议
常规幸运礼物 multiplier_ppms 包含 0/0.2x/0.5x/1x/2x/5x/10x/20x,高倍关闭或保持较高水位门槛
超级幸运礼物 在常规列表基础上增加 50x/100x/500x,同时提高初始池、水位倍数和风控 cap
新手体验 保留 0.2x/0.5x/1x/2x,不要只配置 1x 以上倍率
降低投诉 降低 effective_rtp 波动,增加小倍率数量,减少 500x 等极高倍
控制成本 降低 target_rtp_ppm 或补贴预算,不要用过低风控 cap 硬压基础 RTP

高风险配置示例:

配置 风险
multiplier_ppms=[1000000,2000000] 没有未中奖档,低缺口时仍可能返 1x
target_rtp_ppm=950000 但最大倍率低于 1x 发布会被校验拒绝,无法追平 RTP
large_tier_enabled=true 但初始池很低 高倍长期被过滤,用户看到配置但实际不出
user_hourly_payout_cap 低于小时期望返奖 风控会阻断 RTP 追平,触发 cannot be settled

验证

文档或配置语义变更后至少跑相关单元测试:

go test ./services/activity-service/internal/service/luckygift
go test ./services/activity-service/internal/storage/mysql
go test ./services/room-service/internal/room/service
go test ./services/gateway-service/internal/transport/http/activityapi

参数上线前跑体验模拟:

go run ./tools/lucky-gift-v2-demo \
  -users 1000 \
  -devices 900 \
  -rooms 50 \
  -draws-min 0 \
  -draws-max 100000 \
  -unique-draws \
  -base-rtp 95 \
  -cost 500 \
  -global-window 100000 \
  -gift-window 100000 \
  -user-csv /tmp/lucky-gift-users.csv

验收口径:

要求
100000 paid draws 基础 RTP 偏差 < 1pp
1000000 paid draws 基础 RTP 偏差 < 0.5pp
pool safety 三层池和气氛池不低于 reserve
idempotency 重复 command_id 返回同一 draw
reward status 有返奖时先 pendingworker 成功后 granted
Flutter 体验 不本地加余额;余额以 WalletBalanceChanged 或余额查询为准