445 lines
21 KiB
Markdown
445 lines
21 KiB
Markdown
# 通用抽奖引擎技术文档
|
||
|
||
本文档描述当前服务端通用抽奖能力的代码结构、运行口径和转盘落地方式。抽奖通用能力目前由 `activity-service` 持有,钱包入账由 `wallet-service` 持有。幸运礼物已经复用通用权重选择;转盘新增独立配置、抽奖记录、RTP 统计、钱包 reason 和后台查询入口。
|
||
|
||
## 一、边界和结论
|
||
|
||
| 模块 | Owner | 当前职责 | 不做什么 | 代码位置 |
|
||
| --- | --- | --- | --- | --- |
|
||
| 通用抽奖引擎 | `activity-service` | 候选归一化、权重随机、奖品类型归一化、RTP 计值归一化 | 不读取数据库、不判断资格、不改奖池、不发奖 | `services/activity-service/internal/domain/lotteryengine/engine.go` |
|
||
| 幸运礼物 | `activity-service` | 继续拥有幸运礼物规则、奖池、RTP 窗口、风控、记录和 outbox;候选随机改为调用通用引擎 | 不改变原有幸运礼物 RTP 观察和不可支付过滤逻辑 | `services/activity-service/internal/storage/mysql/lucky_gift_repository.go` |
|
||
| 转盘 | `activity-service` | 独立规则版本、奖档、奖池、RTP 窗口、抽奖记录、后台统计、金币快路径发奖 | 不复用幸运礼物表;不把道具/装扮价值计入 RTP | `services/activity-service/internal/domain/wheel`、`internal/service/wheel`、`internal/storage/mysql/wheel_repository.go` |
|
||
| 钱包 reason | `wallet-service` | 新增 `wheel_reward` 入账 reason、交易、分录、钱包 outbox | 不判断转盘中奖资格,不计算 RTP | `services/wallet-service/internal/service/wallet/service.go`、`internal/storage/mysql/repository.go` |
|
||
| Proto/gRPC | `api/proto` + transport | 暴露转盘抽奖和后台配置/记录/统计 RPC | 当前未新增 gateway HTTP 包装 | `api/proto/activity/v1/activity.proto`、`services/activity-service/internal/transport/grpc/wheel_server.go` |
|
||
|
||
核心原则:
|
||
|
||
1. 通用引擎只负责“从已过滤候选里按权重选中一个奖档”。
|
||
2. 业务模块必须在调用引擎前完成资格、价格、奖池、风控和预算过滤。
|
||
3. RTP 只使用 `rtp_value_coins` 作为分子;转盘道具和装扮无论配置价值多少,运行态都强制为 0。
|
||
4. 金币发放必须走钱包独立 reason `wheel_reward`,不能混用 `lucky_gift_reward`。
|
||
5. 转盘配置、记录和统计使用 `wheel_*` 表,不能写入幸运礼物表。
|
||
|
||
## 二、代码索引
|
||
|
||
### 2.1 通用抽奖引擎
|
||
|
||
| 文件 | 关键符号 | 说明 |
|
||
| --- | --- | --- |
|
||
| `services/activity-service/internal/domain/lotteryengine/engine.go` | `Candidate` | 引擎唯一输入视图:`ID`、`Weight`、`PrizeKind`、`RTPValueCoins` |
|
||
| 同上 | `NormalizeCandidates` | 复制并清洗候选;负权重拒绝;全部权重为 0 时退化为等权随机 |
|
||
| 同上 | `SelectWeighted` | 使用 `crypto/rand` 生成随机下标,按权重选择候选 |
|
||
| 同上 | `NormalizePrizeKind` | 把 `coin/gift/prop/dress` 及同义值归一化 |
|
||
| 同上 | `NormalizeRTPValueCoins` | 金币/礼物保留非负 RTP 价值;`prop/dress` 强制归零 |
|
||
| `services/activity-service/internal/domain/lotteryengine/engine_test.go` | 单测 | 覆盖权重选择、全 0 权重兜底、道具/装扮 RTP 归零 |
|
||
|
||
### 2.2 转盘代码
|
||
|
||
| 文件 | 关键符号 | 说明 |
|
||
| --- | --- | --- |
|
||
| `services/activity-service/internal/domain/wheel/wheel.go` | `RuleConfig`、`Tier`、`DrawCommand`、`DrawResult`、`DrawSummary` | 转盘领域模型 |
|
||
| `services/activity-service/internal/service/wheel/config.go` | `NormalizeRuleConfig`、`ValidateRuleConfig` | 后台配置发布前归一化和硬边界校验 |
|
||
| `services/activity-service/internal/service/wheel/service.go` | `Service.Draw`、`UpsertConfig`、`GetConfig`、`ListDraws`、`GetDrawSummary` | 转盘用例入口 |
|
||
| 同上 | `creditCoinRewardFastPath` | 金币奖品同步调用钱包 `CreditWheelReward`,成功后回写 draw、统计和 outbox |
|
||
| `services/activity-service/internal/storage/mysql/wheel_repository.go` | `PublishWheelRuleConfig` | 发布不可变规则版本和奖档 |
|
||
| 同上 | `ExecuteWheelDraw` | 转盘抽奖主事务:幂等、规则锁定、价格校验、RTP 窗口、奖池、记录、统计、outbox |
|
||
| 同上 | `executeSingleWheelDraw` | 单次命中奖档、更新 RTP 窗口、写 draw record 和 outbox |
|
||
| 同上 | `wheelPayableCandidates` | 按奖池容量和单次 RTP 上限过滤候选;道具/装扮 RTP 价值归零 |
|
||
| 同上 | `ListWheelDraws`、`GetWheelDrawSummary` | 后台抽奖明细和汇总统计读取 |
|
||
| 同上 | `MarkWheelDrawsGranted` | 金币发放成功后回写记录、统计和 `activity_outbox` |
|
||
| `services/activity-service/internal/storage/mysql/wheel_schema.go` | `ensureWheelTables` | 运行时兜底创建 `wheel_*` 表 |
|
||
| `services/activity-service/deploy/mysql/initdb/001_activity_service.sql` | `wheel_*` DDL | 新环境初始化表结构 |
|
||
| `services/activity-service/internal/transport/grpc/wheel_server.go` | `WheelServer`、`AdminWheelServer` | 转盘 App 抽奖和后台配置/统计 gRPC |
|
||
| `services/activity-service/internal/app/services.go` | `wheelservice.New` | activity-service 装配转盘 service |
|
||
| `services/activity-service/internal/app/grpc_registration.go` | `RegisterWheelServiceServer`、`RegisterAdminWheelServiceServer` | 注册转盘 gRPC 服务 |
|
||
|
||
### 2.3 钱包代码
|
||
|
||
| 文件 | 关键符号 | 说明 |
|
||
| --- | --- | --- |
|
||
| `api/proto/wallet/v1/wallet.proto` | `CreditWheelRewardRequest`、`CreditWheelReward` | 钱包转盘金币奖品入账 RPC |
|
||
| `services/wallet-service/internal/domain/ledger/ledger.go` | `WheelRewardCommand`、`WheelRewardReceipt` | 钱包 service/repository 间领域命令 |
|
||
| `services/wallet-service/internal/service/wallet/service.go` | `CreditWheelReward` | 校验转盘金币奖品入账命令,固定钱包职责边界 |
|
||
| `services/wallet-service/internal/storage/mysql/repository.go` | `bizTypeWheelReward`、`CreditWheelReward` | 交易、账户余额、分录和钱包 outbox 同事务提交 |
|
||
| 同上 | `wheelRewardRequestHash` | 保证同一转盘 draw 幂等入账 |
|
||
| `services/wallet-service/internal/transport/grpc/server.go` | `CreditWheelReward` | gRPC 请求转换为钱包领域命令 |
|
||
|
||
### 2.4 Proto 契约
|
||
|
||
| 文件 | RPC/Message | 说明 |
|
||
| --- | --- | --- |
|
||
| `api/proto/activity/v1/activity.proto` | `WheelService.ExecuteWheelDraw` | 扣费后执行转盘抽奖 |
|
||
| 同上 | `AdminWheelService.GetWheelConfig` | 后台读取当前转盘配置 |
|
||
| 同上 | `AdminWheelService.UpsertWheelConfig` | 后台发布新规则版本 |
|
||
| 同上 | `AdminWheelService.ListWheelDraws` | 后台查看抽奖明细 |
|
||
| 同上 | `AdminWheelService.GetWheelDrawSummary` | 后台查看转盘统计汇总 |
|
||
| 同上 | `WheelRuleConfig`、`WheelPrizeTier` | 转盘规则和奖档配置 |
|
||
| 同上 | `WheelDrawMeta`、`WheelDrawResult`、`WheelDrawSummary` | 抽奖入参、结果和统计 |
|
||
| `api/proto/wallet/v1/wallet.proto` | `CreditWheelRewardRequest`、`CreditWheelRewardResponse` | 转盘金币发放钱包契约 |
|
||
|
||
## 三、通用引擎模型
|
||
|
||
### 3.1 Candidate
|
||
|
||
`lotteryengine.Candidate` 是通用引擎唯一关心的数据:
|
||
|
||
| 字段 | 含义 | 规则 |
|
||
| --- | --- | --- |
|
||
| `ID` | 业务奖档 ID | 引擎只原样返回,业务侧负责保证唯一 |
|
||
| `Weight` | 权重 | 负数拒绝;正数参与随机;全部为 0 时每个候选权重按 1 处理 |
|
||
| `PrizeKind` | 奖品类型 | 归一化为 `coin/gift/prop/dress` 等标准值 |
|
||
| `RTPValueCoins` | 计入 RTP 的金币价值 | 负数按 0;`prop/dress` 强制为 0 |
|
||
|
||
引擎不直接使用 `RewardCoins`、`RewardID`、库存、奖池余额、用户信息。这些都属于业务模块。
|
||
|
||
### 3.2 权重选择
|
||
|
||
流程:
|
||
|
||
1. 调用方传入已过滤候选。
|
||
2. `NormalizeCandidates` 清洗权重、奖品类型和 RTP 价值。
|
||
3. `SelectWeighted` 汇总权重。
|
||
4. 用 `crypto/rand.Int` 生成 `[0,totalWeight)` 的随机数。
|
||
5. 按权重区间返回命中的 `Candidate`。
|
||
|
||
设计原因:
|
||
|
||
| 设计 | 原因 |
|
||
| --- | --- |
|
||
| 使用 `crypto/rand` | 高价值奖品不能被时间种子或进程启动顺序预测 |
|
||
| 全 0 权重兜底为等权 | 兼容旧配置或临时配置错误,避免整个活动不可抽 |
|
||
| 负权重直接失败 | 负权重没有业务语义,继续运行会让概率不可解释 |
|
||
| 不在引擎内做 RTP 修正 | RTP 修正依赖奖池、预算、用户风险和活动策略,必须留在业务层 |
|
||
|
||
## 四、转盘配置模型
|
||
|
||
### 4.1 RuleConfig
|
||
|
||
`domain/wheel.RuleConfig` 是一个不可变版本快照:
|
||
|
||
| 字段 | 含义 |
|
||
| --- | --- |
|
||
| `WheelID` | 转盘 ID,空值归一为 `default` |
|
||
| `RuleVersion` | 不可变规则版本,每次发布 +1 |
|
||
| `Enabled` | 是否启用 |
|
||
| `DrawPriceCoins` | 单抽价格 |
|
||
| `TargetRTPPPM` | RTP 目标,ppm |
|
||
| `PoolRatePPM` | 每笔消耗进入转盘奖池比例,ppm |
|
||
| `SettlementWindowDraws` | RTP 观察窗口抽数 |
|
||
| `InitialPoolCoins` | 初始奖池水位 |
|
||
| `PoolReserveCoins` | 奖池保底水位 |
|
||
| `MaxSingleRTPPayout` | 单次可支付的 RTP 价值上限 |
|
||
| `Tiers` | 奖档列表 |
|
||
|
||
### 4.2 Tier
|
||
|
||
| 字段 | 含义 | 特殊规则 |
|
||
| --- | --- | --- |
|
||
| `TierID` | 奖档 ID | 同一规则版本内必须唯一 |
|
||
| `DisplayName` | 展示名称快照 | 只用于后台和前端展示 |
|
||
| `RewardType` | `coin/gift/prop/dress` | 会调用通用引擎归一化 |
|
||
| `RewardID` | 礼物、道具、装扮资源 ID | 非金币奖品必填 |
|
||
| `RewardCount` | 奖励数量 | 小于等于 0 时归一为 1 |
|
||
| `RewardCoins` | 金币奖励金额 | `coin` 使用 |
|
||
| `RTPValueCoins` | 计入 RTP 的价值 | `prop/dress` 强制为 0 |
|
||
| `WeightPPM` | 随机权重 | 当前作为权重值使用,不要求总和等于 1000000 |
|
||
| `Enabled` | 是否启用 | 至少需要一个启用奖档 |
|
||
| `MetadataJSON` | 展示和发放扩展快照 | 空值或非法 JSON 归一为 `{}` |
|
||
|
||
### 4.3 配置校验
|
||
|
||
入口:`services/activity-service/internal/service/wheel/config.go`
|
||
|
||
规则:
|
||
|
||
1. `draw_price_coins` 必须大于 0。
|
||
2. `target_rtp_ppm`、`pool_rate_ppm` 不能为负。
|
||
3. `settlement_window_draws` 必须大于 0。
|
||
4. 奖档不能为空。
|
||
5. `tier_id` 必须唯一。
|
||
6. `weight_ppm` 不能为负。
|
||
7. `coin` 奖档的 `reward_coins` 不能为负。
|
||
8. `gift/prop/dress` 必须有 `reward_id`。
|
||
9. `prop/dress` 的 `rtp_value_coins` 在归一化后必须为 0。
|
||
10. 至少一个奖档启用。
|
||
|
||
## 五、转盘抽奖链路
|
||
|
||
### 5.1 调用前提
|
||
|
||
转盘抽奖 RPC 是扣费后的服务端事实入口:
|
||
|
||
| 阶段 | Owner | 说明 |
|
||
| --- | --- | --- |
|
||
| 扣费 | 调用方 + wallet-service | 先扣用户金币,拿到真实 `coin_spent` 和 `paid_at_ms` |
|
||
| 抽奖 | activity-service | 使用 `ExecuteWheelDraw` 固化中奖结果和 RTP |
|
||
| 金币发放 | wallet-service | `coin` 奖品通过 `CreditWheelReward` 入账 |
|
||
| 礼物/道具/装扮发放 | 对应资产 owner | 当前只固化 pending 记录和 outbox 事实,后续接资产发放接口 |
|
||
|
||
转盘 repository 会校验:
|
||
|
||
```text
|
||
coin_spent == draw_price_coins * draw_count
|
||
```
|
||
|
||
这样避免客户端或调用方伪造 RTP 分母。
|
||
|
||
### 5.2 主事务
|
||
|
||
入口:`Repository.ExecuteWheelDraw`
|
||
|
||
事务内顺序:
|
||
|
||
1. 归一化 `wheel_id` 和 `draw_count`。
|
||
2. 按 `command_id` 收集已有 draw,完整存在则幂等返回。
|
||
3. 如果只存在部分子抽,返回冲突,避免重复发奖。
|
||
4. 锁定当前启用的转盘规则版本。
|
||
5. 校验 `coin_spent` 与当前单价一致。
|
||
6. 获取或创建 RTP 窗口。
|
||
7. 获取或创建转盘奖池。
|
||
8. 按 `draw_count` 拆分每次消耗。
|
||
9. 每一抽调用 `executeSingleWheelDraw`。
|
||
10. 汇总写回奖池净变化。
|
||
11. 更新后台统计。
|
||
12. 提交事务。
|
||
|
||
### 5.3 单抽
|
||
|
||
入口:`executeSingleWheelDraw`
|
||
|
||
单抽顺序:
|
||
|
||
1. `wheelPayableCandidates` 过滤可支付候选。
|
||
2. 组装 `lotteryengine.Candidate`。
|
||
3. 调用 `lotteryengine.SelectWeighted` 命中奖档。
|
||
4. 用命中奖档的 `RTPValueCoins` 更新奖池和 RTP 窗口。
|
||
5. 写入 `wheel_draw_records`。
|
||
6. 如果奖品需要发放,写入 `activity_outbox`,事件类型为 `WheelRewardSettlement`。
|
||
7. 返回 `DrawResult`。
|
||
|
||
候选过滤口径:
|
||
|
||
| 过滤项 | 说明 |
|
||
| --- | --- |
|
||
| `tier.Enabled` | 未启用奖档不参与 |
|
||
| `RTPValueCoins` | `prop/dress` 强制为 0 |
|
||
| 奖池容量 | `pool.balance - reserve_floor` |
|
||
| 单次上限 | `max_single_rtp_payout` 大于 0 时限制容量 |
|
||
|
||
注意:道具/装扮 `RTPValueCoins=0`,所以不会因为展示价值占用奖池,也不会提高 RTP 分子。
|
||
|
||
### 5.4 批量抽奖
|
||
|
||
当前转盘支持 `draw_count`:
|
||
|
||
| 功能 | 代码 | 说明 |
|
||
| --- | --- | --- |
|
||
| 子命令 ID | `wheelSubCommandID` | 单抽使用原 command,多抽追加 `#000001` 这类后缀 |
|
||
| 金额拆分 | 复用 `luckyDrawUnitSpend` | 总金额按次数拆分,余数给前几抽 |
|
||
| 幂等聚合 | `collectWheelDrawsByCommand`、`aggregateWheelDrawResults` | 完整存在则聚合返回 |
|
||
| 发放限制 | `creditCoinRewardFastPath` | 当前只对单抽金币做同步快路径,多抽保留 pending/outbox |
|
||
|
||
## 六、RTP 口径
|
||
|
||
### 6.1 分子和分母
|
||
|
||
| 项 | 字段 | 来源 |
|
||
| --- | --- | --- |
|
||
| RTP 分母 | `coin_spent` / `wager_coins` | 用户实际扣费金币 |
|
||
| RTP 分子 | `rtp_value_coins` / `actual_rtp_value_coins` | 命中奖档计入 RTP 的价值 |
|
||
| 目标值 | `target_rtp_ppm` | 后台规则配置 |
|
||
| 当前值 | `actual_rtp_ppm` | `actual_rtp_value_coins * 1000000 / wager_coins` |
|
||
|
||
### 6.2 奖品类型口径
|
||
|
||
| 奖品类型 | 是否可配置 | 是否发放 | 是否计入 RTP |
|
||
| --- | --- | --- | --- |
|
||
| `coin` | 是 | 钱包 `wheel_reward` | 是,使用 `rtp_value_coins` |
|
||
| `gift` | 是 | 待接礼物/资源 owner | 是,使用 `rtp_value_coins` |
|
||
| `prop` | 是 | 待接道具 owner | 否,强制 0 |
|
||
| `dress` | 是 | 待接装扮 owner | 否,强制 0 |
|
||
|
||
强制归零的位置:
|
||
|
||
| 层 | 代码 |
|
||
| --- | --- |
|
||
| 通用引擎 | `lotteryengine.NormalizeRTPValueCoins` |
|
||
| 转盘配置 | `wheel.NormalizeRuleConfig` |
|
||
| 转盘仓储 | `normalizeWheelTiers`、`wheelPayableCandidates` |
|
||
| 转盘测试 | `services/activity-service/internal/service/wheel/config_test.go`、`domain/lotteryengine/engine_test.go` |
|
||
|
||
## 七、发奖和 outbox
|
||
|
||
### 7.1 金币奖品
|
||
|
||
金币奖品路径:
|
||
|
||
1. `ExecuteWheelDraw` 写入 `wheel_draw_records`,初始状态为 `pending`。
|
||
2. `Service.Draw` 在事务提交后进入 `creditCoinRewardFastPath`。
|
||
3. 调用 wallet-service `CreditWheelReward`。
|
||
4. wallet-service 以 `biz_type=wheel_reward` 写交易、账户、分录和 wallet outbox。
|
||
5. activity-service 调用 `MarkWheelDrawsGranted`。
|
||
6. `MarkWheelDrawsGranted` 同事务更新:
|
||
- `wheel_draw_records.reward_status = granted`
|
||
- `wheel_draw_records.reward_transaction_id`
|
||
- `wheel_draw_stats.pending_draws/granted_draws`
|
||
- `activity_outbox.status = delivered`
|
||
|
||
钱包 reason:
|
||
|
||
| 项 | 值 |
|
||
| --- | --- |
|
||
| wallet biz type | `wheel_reward` |
|
||
| RPC reason | `wheel_reward` |
|
||
| command ID | `wheel_reward:<draw_id>` |
|
||
| 钱包事件 | `WalletWheelRewardCredited` |
|
||
|
||
### 7.2 礼物/道具/装扮奖品
|
||
|
||
当前状态:
|
||
|
||
1. 抽奖结果会落 `wheel_draw_records`。
|
||
2. `reward_status` 为 `pending`。
|
||
3. `activity_outbox` 会写 `WheelRewardSettlement`。
|
||
4. 具体发放 worker 和资产 owner RPC 还没有接入。
|
||
|
||
后续接入时必须遵守:
|
||
|
||
1. 不允许在 activity-service 直接改钱包或资产表。
|
||
2. 发放成功后调用转盘仓储状态回写方法,保持 draw record、统计和 outbox 一致。
|
||
3. 道具/装扮发放成功也不能把展示价值写入 RTP。
|
||
4. 礼物如果要计入 RTP,必须使用抽奖时固化的 `rtp_value_coins`,不能按发放时礼物价格重新估值。
|
||
|
||
## 八、后台记录和统计
|
||
|
||
### 8.1 明细
|
||
|
||
入口:`AdminWheelService.ListWheelDraws`
|
||
|
||
仓储:`Repository.ListWheelDraws`
|
||
|
||
支持筛选:
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `wheel_id` | 转盘 ID |
|
||
| `user_id` | 用户 ID |
|
||
| `status` | `pending/granted/failed` |
|
||
| `page/page_size` | 分页,`page_size` 最大 100 |
|
||
|
||
返回字段包括:
|
||
|
||
`draw_id`、`command_id`、`wheel_id`、`rule_version`、`selected_tier_id`、`reward_type`、`reward_id`、`reward_count`、`reward_coins`、`rtp_value_coins`、`reward_status`、`wallet_transaction_id`、`rtp_window_index`、`metadata_json`。
|
||
|
||
### 8.2 汇总
|
||
|
||
入口:`AdminWheelService.GetWheelDrawSummary`
|
||
|
||
仓储:`Repository.GetWheelDrawSummary`
|
||
|
||
统计字段:
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `total_draws` | 抽奖次数 |
|
||
| `unique_users` | 去重用户数 |
|
||
| `total_spent_coins` | 总消耗金币 |
|
||
| `total_rtp_value_coins` | 计入 RTP 的返奖价值 |
|
||
| `actual_rtp_ppm` | 实际 RTP |
|
||
| `pending_draws` | 待发放 |
|
||
| `granted_draws` | 已发放 |
|
||
| `failed_draws` | 失败 |
|
||
|
||
当只按 `wheel_id` 汇总时,优先读 `wheel_draw_stats`;带 `user_id/status` 过滤时直接聚合 `wheel_draw_records`。
|
||
|
||
## 九、数据表
|
||
|
||
| 表 | 用途 | 代码 |
|
||
| --- | --- | --- |
|
||
| `wheel_rule_versions` | 转盘不可变规则版本 | `wheel_schema.go`、`001_activity_service.sql` |
|
||
| `wheel_prize_tiers` | 每个规则版本的奖档快照 | 同上 |
|
||
| `wheel_rtp_windows` | RTP 观察窗口 | `getOpenWheelRTPWindow`、`createWheelRTPWindow` |
|
||
| `wheel_pools` | 转盘奖池水位 | `getOrCreateWheelPool`、`persistWheelPoolDelta` |
|
||
| `wheel_draw_records` | 抽奖事实明细 | `executeSingleWheelDraw`、`ListWheelDraws` |
|
||
| `wheel_draw_stats` | 后台汇总统计 | `updateWheelStats`、`GetWheelDrawSummary` |
|
||
| `wheel_draw_stat_users` | 统计去重用户 | `updateWheelStats` |
|
||
| `activity_outbox` | 转盘发奖补偿事实 | `insertWheelRewardOutbox`、`MarkWheelDrawsGranted` |
|
||
| `wallet_transactions` | 钱包交易 | `wallet-service/internal/storage/mysql.Repository.CreditWheelReward` |
|
||
| `wallet_outbox` | 钱包余额和奖励事实 | 同上 |
|
||
|
||
## 十、和幸运礼物的关系
|
||
|
||
幸运礼物不是直接迁移成转盘,也不是把转盘塞进幸运礼物:
|
||
|
||
| 项 | 幸运礼物 | 转盘 |
|
||
| --- | --- | --- |
|
||
| 规则维度 | `pool_id`,房间送礼场景 | `wheel_id`,H5/活动转盘场景 |
|
||
| 奖品 | 金币返奖 | 金币、礼物、道具、装扮 |
|
||
| RTP 口径 | 原幸运礼物基础返奖口径 | `rtp_value_coins`,道具/装扮强制 0 |
|
||
| 记录表 | `lucky_draw_records` | `wheel_draw_records` |
|
||
| 钱包 reason | `lucky_gift_reward` | `wheel_reward` |
|
||
| 通用引擎使用 | 候选随机复用 `lotteryengine.SelectWeighted` | 候选随机和 RTP 归一化都复用 |
|
||
|
||
幸运礼物保留自己的奖池、风控、用户阶段和 room IM 逻辑。通用引擎只替换了“已过滤候选里的权重随机”这一小段,避免改动原业务行为。
|
||
|
||
## 十一、扩展新抽奖活动的步骤
|
||
|
||
新增一个抽奖活动时,不要复制幸运礼物或转盘整套逻辑。按下面顺序接入:
|
||
|
||
1. 新建独立 domain,定义自己的 `RuleConfig`、`Tier`、`DrawCommand`、`DrawResult`。
|
||
2. 新建 service,负责入参归一化、配置校验和跨服务调用。
|
||
3. 新建 storage,负责规则版本、奖池、RTP 窗口、记录、统计和 outbox。
|
||
4. 业务层先过滤资格、预算、库存、奖池和风控。
|
||
5. 把过滤后的候选转换成 `lotteryengine.Candidate`。
|
||
6. 调用 `lotteryengine.SelectWeighted`。
|
||
7. 用返回候选固化抽奖结果。
|
||
8. 如果有金币奖品,在 wallet-service 增加独立 reason,不复用已有 reason。
|
||
9. 如果有非金币奖品,接对应 owner service 的发放接口。
|
||
10. 补 proto、gRPC、表结构、initdb 和测试。
|
||
|
||
必须明确回答的问题:
|
||
|
||
| 问题 | 必须落地的位置 |
|
||
| --- | --- |
|
||
| 奖品有哪些类型 | domain `Tier.RewardType` 和 proto |
|
||
| 哪些奖品计入 RTP | `RTPValueCoins` 归一化 |
|
||
| 哪个服务发奖 | service 跨服务依赖 |
|
||
| 钱包 reason 是什么 | wallet-service domain/service/storage/proto |
|
||
| 如何幂等 | command ID、draw ID、钱包 request hash |
|
||
| 后台查什么 | draw records 和 summary |
|
||
| 失败如何补偿 | activity outbox 或 owner outbox |
|
||
|
||
## 十二、验证命令
|
||
|
||
当前相关验证命令:
|
||
|
||
```bash
|
||
PATH=/Users/hy/go/bin:$PATH make proto
|
||
go test ./services/activity-service/internal/domain/lotteryengine
|
||
go test ./services/activity-service/internal/service/wheel
|
||
go test ./services/activity-service/internal/storage/mysql
|
||
go test ./services/activity-service/internal/transport/grpc
|
||
go test ./services/activity-service/internal/app
|
||
go test ./services/wallet-service/internal/service/wallet ./services/wallet-service/internal/storage/mysql ./services/wallet-service/internal/transport/grpc
|
||
go test ./services/activity-service/...
|
||
```
|
||
|
||
已知当前仓库全量 `make test` 仍会失败在既有 MifaPay 配置测试:
|
||
|
||
```text
|
||
services/wallet-service/internal/config.TestLoadMifaPayMerchantConfig
|
||
mifapay merchant identity mismatch in ../../configs/config.docker.yaml
|
||
```
|
||
|
||
该失败与通用抽奖和转盘改动无关。
|
||
|
||
## 十三、当前未完成事项
|
||
|
||
| 事项 | 原因 | 后续接入位置 |
|
||
| --- | --- | --- |
|
||
| 礼物奖品真实发放 | 需要确认礼物/资源 owner 的发放接口和背包表现 | activity-service 转盘 outbox worker + 对应 owner RPC |
|
||
| 道具/装扮真实发放 | 需要确认装扮资源 owner、有效期和背包协议 | activity-service 转盘 outbox worker + resource/wallet owner |
|
||
| Gateway HTTP/H5 接口 | 当前只到 activity gRPC;H5 页面还需要 gateway 包装 | `services/gateway-service/internal/transport/http/activityapi` |
|
||
| 后台管理页面 | 当前只有 gRPC 后台能力,未接 admin web 模块 | `server/admin` |
|
||
|