docs: update lucky gift flutter integration

This commit is contained in:
zhx 2026-06-01 16:31:13 +08:00
parent 8c4a3e1754
commit 10bcfadbc4
4 changed files with 372 additions and 18 deletions

View File

@ -0,0 +1,331 @@
# 幸运礼物 Flutter 对接
本文只保留 App 对接需要的信息地址、参数、返回值、IM 字段。服务端实现细节看 [幸运礼物实现文档](../幸运礼物实现文档.md),礼物面板通用字段看 [礼物 Tab Flutter 对接](礼物TabFlutter对接.md)。
## 1. 送礼前检查
地址:
```http
POST /api/v1/activities/lucky-gifts/check
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
请求参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `gift_id` | string | 是 | 礼物 ID。 |
| `pool_id` | string | 是 | 幸运礼物奖池 ID例如 `lucky``super_lucky` 或后台配置值。 |
请求示例:
```json
{
"room_id": "lalu_room_001",
"gift_id": "rose",
"pool_id": "super_lucky"
}
```
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 是否可参与幸运礼物抽奖。 |
| `reason` | string | 不可参与原因;可为空。 |
| `room_id` | string | 房间 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `gift_price` | int64 | 当前规则参考礼物价格,只展示用,扣费以后端送礼结果为准。 |
| `rule_version` | int64 | 命中的幸运礼物规则版本。 |
| `target_rtp_ppm` | int64 | 目标 RTPppm 口径;`950000` 表示 95%。 |
| `experience_pool` | string | 当前用户体验池,例如 `novice``normal``advanced`。 |
返回示例:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"enabled": true,
"reason": "",
"room_id": "lalu_room_001",
"gift_id": "rose",
"pool_id": "super_lucky",
"gift_price": 100,
"rule_version": 12,
"target_rtp_ppm": 950000,
"experience_pool": "novice"
}
}
```
## 2. 发送幸运礼物
地址:
```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
请求参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `command_id` | string | 是 | 本次送礼幂等键;同一次重试必须复用。 |
| `target_type` | string | 否 | 当前固定传 `user`;为空后端按 `user`。 |
| `target_user_ids` | int64[] | 是 | 收礼用户列表;当前只传 1 个用户。 |
| `target_user_id` | int64 | 否 | 旧单目标兼容字段,新代码优先用 `target_user_ids`。 |
| `gift_id` | string | 是 | 礼物 ID。 |
| `gift_count` | int32 | 是 | 礼物数量,必须大于 0批量送礼会聚合返回一次中奖结果。 |
| `pool_id` | string | 是 | 幸运礼物奖池 ID普通礼物不传。 |
请求示例:
```json
{
"room_id": "lalu_room_001",
"command_id": "gift_1779259000000_10001_rose_01",
"target_type": "user",
"target_user_ids": [10002],
"gift_id": "rose",
"gift_count": 9,
"pool_id": "super_lucky"
}
```
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次命令是否实际落地;重复提交同一 `command_id` 可能为 `false`。 |
| `result.room_version` | int64 | 房间版本。 |
| `result.server_time_ms` | int64 | 服务端时间。 |
| `billing_receipt_id` | string | 送礼扣费回执。 |
| `room_heat` | int64 | 送礼后的房间热度。 |
| `gift_rank` | array | 当前房间礼物榜。 |
| `room` | object | 房间快照。 |
| `treasure` | object | 房间宝箱状态。 |
| `lucky_gift` | object | 幸运礼物抽奖结果;普通礼物可为空。 |
`lucky_gift` 返回字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 本次是否触发幸运礼物。 |
| `draw_id` | string | 聚合抽奖 ID。 |
| `command_id` | string | 本次送礼命令 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `rule_version` | int64 | 规则版本。 |
| `experience_pool` | string | 用户体验池。 |
| `selected_tier_id` | string | 命中奖档 ID。 |
| `multiplier_ppm` | int64 | 倍率 ppm`2000000` 表示 2x。 |
| `base_reward_coins` | int64 | 基础奖池返奖金币。 |
| `room_atmosphere_reward_coins` | int64 | 房间气氛池返奖金币。 |
| `activity_subsidy_coins` | int64 | 活动补贴返奖金币。 |
| `effective_reward_coins` | int64 | 实际中奖金币0 表示未中奖。 |
| `reward_status` | string | `granted` 已入账;`pending`/`granting` 到账处理中;`failed` 需要人工处理。 |
| `stage_feedback` | bool | 是否有阶段反馈表现。 |
| `high_multiplier` | bool | 是否高倍率中奖。 |
| `created_at_ms` | int64 | 抽奖创建时间。 |
| `wallet_transaction_id` | string | 返奖钱包交易 ID已入账时有值。 |
| `coin_balance_after` | int64 | 返奖入账后的 COIN 可用余额。 |
返回示例:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"result": {
"applied": true,
"room_version": 14,
"server_time_ms": 1779259000000
},
"billing_receipt_id": "receipt-lucky",
"room_heat": 123456,
"gift_rank": [],
"room": {},
"treasure": {},
"lucky_gift": {
"enabled": true,
"draw_id": "lucky_draw_test",
"command_id": "gift_1779259000000_10001_rose_01",
"pool_id": "super_lucky",
"gift_id": "rose",
"rule_version": 12,
"experience_pool": "novice",
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"base_reward_coins": 1800,
"room_atmosphere_reward_coins": 0,
"activity_subsidy_coins": 0,
"effective_reward_coins": 1800,
"reward_status": "granted",
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000,
"wallet_transaction_id": "wallet_tx_lucky",
"coin_balance_after": 8800
}
}
}
```
客户端处理:
- `reward_status=granted``effective_reward_coins > 0`:立即展示中奖,并用 `coin_balance_after` 覆盖本地金币余额。
- `reward_status=pending``granting`:展示“到账处理中”,不要本地加金币,不要生成新 `command_id` 重发。
- `effective_reward_coins=0`:未中奖,只展示普通送礼表现。
- 同一用户同一房间连续发送幸运礼物时,客户端需要本地排队;重试必须复用原 `command_id`
## 3. IM 字段
### 3.1 送礼房间 IM
用途:房间内播放普通送礼动效、同步热度和礼物榜。幸运礼物也会先产生这条送礼 IM。
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `room_gift_sent` |
| `Data.event_type` | `room_gift_sent` |
`Data` 核心字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | 房间事件 ID用于去重。 |
| `room_id` | string | 房间 ID。 |
| `room_version` | int64 | 房间版本。 |
| `occurred_at_ms` | int64 | 事件发生时间。 |
| `actor_user_id` | string/int64 | 送礼用户 ID。 |
| `target_user_id` | string/int64 | 收礼用户 ID。 |
| `gift_value` | int64 | 本次送礼房间价值/热度口径。 |
| `room_heat` | int64 | 送礼后的房间热度。 |
| `attributes.gift_id` | string | 礼物 ID。 |
| `attributes.pool_id` | string | 幸运礼物奖池 ID普通礼物可为空。 |
| `attributes.gift_count` | string | 礼物数量,字符串格式。 |
| `attributes.billing_receipt_id` | string | 钱包扣费回执。 |
| `attributes.coin_spent` | string | 本次实际消耗金币,字符串格式。 |
| `attributes.gift_point_added` | string | 主播礼物积分增量,字符串格式。 |
| `attributes.price_version` | string | 价格版本。 |
示例:
```json
{
"event_type": "room_gift_sent",
"event_id": "room_event_xxx",
"room_id": "lalu_room_001",
"room_version": 14,
"occurred_at_ms": 1779259000000,
"actor_user_id": "10001",
"target_user_id": "10002",
"gift_value": 900,
"room_heat": 123456,
"attributes": {
"gift_id": "rose",
"pool_id": "super_lucky",
"gift_count": "9",
"billing_receipt_id": "receipt-lucky",
"coin_spent": "900",
"gift_point_added": "900",
"price_version": "v1"
}
}
```
### 3.2 幸运礼物开奖 IM
用途:房间公屏展示幸运礼物中奖结果、其他用户同步中奖表现。当前用户自己的余额以 HTTP 返回的 `coin_balance_after` 为准,不等这条 IM。
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `lucky_gift_drawn` |
| `Data.event_type` | `lucky_gift_drawn` |
`Data` 核心字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | 开奖事件 ID用于去重。 |
| `app_code` | string | App Code。 |
| `draw_id` | string | 聚合抽奖 ID。 |
| `command_id` | string | 送礼命令 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `room_id` | string | 房间 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `gift_count` | int32 | 礼物数量。 |
| `user_id` | int64 | 中奖用户 ID。 |
| `sender_user_id` | int64 | 送礼用户 ID。 |
| `target_user_id` | int64 | 收礼用户 ID。 |
| `visible_region_id` | int64 | 展示区域 ID。 |
| `coin_spent` | int64 | 本次送礼消耗金币。 |
| `rule_version` | int64 | 规则版本。 |
| `experience_pool` | string | 用户体验池。 |
| `selected_tier_id` | string | 命中奖档 ID。 |
| `multiplier_ppm` | int64 | 命中倍率 ppm。 |
| `base_reward_coins` | int64 | 基础奖池返奖金币。 |
| `room_atmosphere_reward_coins` | int64 | 房间气氛池返奖金币。 |
| `activity_subsidy_coins` | int64 | 活动补贴返奖金币。 |
| `effective_reward_coins` | int64 | 实际中奖金币。 |
| `stage_feedback` | bool | 是否有阶段反馈表现。 |
| `high_multiplier` | bool | 是否高倍率中奖。 |
| `created_at_ms` | int64 | 抽奖创建时间。 |
示例:
```json
{
"event_type": "lucky_gift_drawn",
"event_id": "lucky_gift_drawn:lucky_draw_test",
"app_code": "lalu",
"draw_id": "lucky_draw_test",
"command_id": "gift_1779259000000_10001_rose_01",
"pool_id": "super_lucky",
"room_id": "lalu_room_001",
"gift_id": "rose",
"gift_count": 9,
"user_id": 10001,
"sender_user_id": 10001,
"target_user_id": 10002,
"visible_region_id": 688,
"coin_spent": 900,
"rule_version": 12,
"experience_pool": "novice",
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"base_reward_coins": 1800,
"room_atmosphere_reward_coins": 0,
"activity_subsidy_coins": 0,
"effective_reward_coins": 1800,
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000
}
```
## 4. 状态处理
| 场景 | App 处理 |
| --- | --- |
| `lucky_gift` 为空 | 当普通礼物处理。 |
| `effective_reward_coins=0` | 未中奖,不更新中奖余额。 |
| `reward_status=granted` | 已入账;用 `coin_balance_after` 更新余额。 |
| `reward_status=pending` / `granting` | 到账处理中;等待钱包通知、刷新余额或后端补偿。 |
| `reward_status=failed` | 展示异常状态,记录 `request_id/draw_id/command_id` 方便排查。 |
| HTTP 超时 | 保留原 `command_id` 重试,不生成新命令。 |

View File

@ -10,10 +10,11 @@
- 房间送礼面板入口是 `GET /api/v1/rooms/{room_id}/gift-panel`,返回当前房间可送礼物、可收礼人、金币余额和房间内可展示 Tab。
- `GET /api/v1/gift-tabs` 不能替代 `GET /api/v1/rooms/{room_id}/gift-panel`App 真正送礼前必须以房间面板返回的数据为准。
- 送礼只调用 `POST /api/v1/rooms/gift/send`
- 金币余额以礼物面板、送礼响应后的钱包通知或 `GET /api/v1/wallet/me/balances` 为准。
- 金币余额以礼物面板、送礼响应里的 `lucky_gift.coin_balance_after`钱包通知或 `GET /api/v1/wallet/me/balances`准;普通礼物和未中奖场景仍以后续钱包事实校准。
- 房间内其他用户看到的礼物表现以腾讯云 IM 自定义消息 `room_gift_sent` 为准。
- `command_id` 是送礼幂等键;同一次用户点击重试必须复用同一个值。
- 当前 App 送礼主流程只按单个用户收礼处理:`target_type=user``target_user_ids` 只传 1 个用户。
- 幸运礼物的专门接入规则见 [幸运礼物 Flutter 对接](幸运礼物Flutter对接.md)。
## 交互流程
@ -23,9 +24,9 @@
4. 用 `tabs` 渲染当前房间分类,用 `gifts` 渲染礼物网格,用 `recipients` 渲染收礼人。
5. 用户选择收礼人、礼物和数量。
6. 点击发送时生成稳定 `command_id`,调用 `POST /api/v1/rooms/gift/send`
7. 发送成功后用响应里的 `room_heat``gift_rank``treasure` 更新房间 UI。
7. 发送成功后用响应里的 `room_heat``gift_rank``treasure` 更新房间 UI;如果有 `lucky_gift` 且已入账,用 `coin_balance_after` 立即刷新金币余额
8. 监听房间 IM `room_gift_sent`,同步播放礼物动效。
9. 监听私有钱包通知或主动刷新余额,不本地扣减余额作为最终值。
9. 普通礼物和未中奖余额仍监听私有钱包通知或主动刷新余额,不本地扣减余额作为最终值。
## 获取礼物 Tab 配置
@ -294,7 +295,25 @@ Content-Type: application/json
}
],
"room": {},
"treasure": {}
"treasure": {},
"lucky_gift": {
"enabled": true,
"draw_id": "lucky_draw_test",
"command_id": "gift_1779259000000_10001_rose_01",
"pool_id": "super_lucky",
"gift_id": "rose",
"rule_version": 12,
"experience_pool": "novice",
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"effective_reward_coins": 1800,
"reward_status": "granted",
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000,
"wallet_transaction_id": "wallet_tx_lucky",
"coin_balance_after": 8800
}
}
}
```
@ -311,13 +330,16 @@ Content-Type: application/json
| `gift_rank` | 最新房间礼物榜。 |
| `room` | 房间快照字段较大App 可按已有房间快照解析逻辑局部更新。 |
| `treasure` | 房间宝箱状态;宝箱 UI 以该字段或宝箱 IM 事件更新。 |
| `lucky_gift` | 幸运礼物抽奖结果;普通礼物可为空。`reward_status=granted` 且中奖时,`coin_balance_after` 是当前用户返奖入账后的余额。 |
发送后处理:
- 成功后立即关闭 loading保留礼物面板或按产品交互关闭。
- 用 `room_heat``gift_rank` 更新房间顶部和贡献榜。
- 播放当前用户送礼动效时按 `command_id` 去重,避免 HTTP 响应和 IM 各播放一次。
- 不要本地直接写最终余额;等待 `WalletBalanceChanged` 私有 IM 或主动刷新余额。
- 如果 `lucky_gift.reward_status=granted``lucky_gift.effective_reward_coins > 0`,立即用 `lucky_gift.coin_balance_after` 覆盖本地金币余额。
- 如果 `lucky_gift.reward_status=pending`,展示中奖到账处理中,不要生成新 `command_id` 重送;补偿 worker 会继续使用同一个 draw 发放。
- 普通礼物和未中奖场景不要本地直接写最终余额;等待 `WalletBalanceChanged` 私有 IM 或主动刷新余额。
## 房间 IM 同步

View File

@ -1,6 +1,6 @@
# 幸运礼物实现文档
本文档描述当前代码里的幸运礼物实现。旧的产品方案、重构分步文档、Flutter 单独对接文档和早期 activity-service 开发文档已经下线;后续以本文档为唯一实现说明
本文档描述当前代码里的幸运礼物服务端实现。旧的产品方案、重构分步文档和早期 activity-service 开发文档已经下线;服务端实现以后续本文档为准Flutter App 对接以 [幸运礼物 Flutter 对接](flutter对接/幸运礼物Flutter对接.md) 为准
当前实现不兼容旧玩法假设。幸运礼物以 `pool_id` 为规则维度,以 `activity-service` 为规则、抽奖、奖池、RTP 窗口、风控、抽奖事实和 outbox owner扣费和返奖入账属于 `wallet-service`;房间状态、热度、礼物榜和房间 outbox 属于 `room-service`
@ -8,12 +8,12 @@
| 功能点 | 当前规则 | 代码函数实现 | 验收 |
| --- | --- | --- | --- |
| 服务边界 | `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 |
| 服务边界 | `activity-service` 只做幸运礼物规则和开奖结果,不改 Room Cell不直接改钱包表;返奖必须通过 wallet-service gRPC | `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 快路径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 |
| 聚合发放 | 批量抽奖可以有 N 条抽奖明细,但正常路径只同步给钱包加币 1 次outbox 保留 1 条聚合事件用于补偿和表现 | `insertLuckyAggregateDrawOutbox``creditLuckyGiftRewardFastPath`、`decodeDrawOutboxPayload`、`MarkLuckyGiftDrawsGranted` | 已验收:批量只按聚合 draw 发放worker 遇到已 granted 会跳过钱包 |
## 二、配置模型
@ -22,7 +22,7 @@
| 功能点 | 字段或 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` 字段给送礼用户 |
| 抽奖结果 | `LuckyGiftDrawResult`,包含 `multiplier_ppm``effective_reward_coins``reward_status``wallet_transaction_id``coin_balance_after`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` 查询 |
@ -69,7 +69,7 @@
| 先扣费 | 房间链路先调用 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` |
| 同步响应 | 当前送礼用户立即拿到抽奖结果;中奖快路径成功时同时拿到返奖交易 ID 和账后余额 | `creditLuckyGiftRewardFastPath`、`luckyGiftResultFromProto`、`roomv1.SendGiftResponse.LuckyGift` | 已验收room response 带 `lucky_gift.wallet_transaction_id` 和 `lucky_gift.coin_balance_after` |
| 房间事实 | 送礼本身仍写 Room Cell、热度、榜单和 room outbox | `Service.SendGift``outbox.Build` | 已验收:幸运礼物不替代普通送礼事实 |
## 四、抽奖事务
@ -111,16 +111,17 @@
| 聚合响应 | 批量响应把金额、倍数、阶段反馈和高倍标记聚合 | `luckyAggregateDrawResults``luckyAggregateBudgetSourcesJSON` | 已验收:总倍数为各命中倍数相加,不是平均倍率 |
| 聚合发放 | 整批只写一条 outboxpayload 里有聚合 `draw_id` 和全部 `draw_ids` | `insertLuckyAggregateDrawOutbox` | 已验收:待发放队列按送礼命令增长,不按中奖子抽增长 |
## 五、返奖和房间表现
## 五、返奖快路径和房间表现
| 功能点 | 行为 | 代码函数实现 | 验收 |
| --- | --- | --- | --- |
| worker 循环 | 持续 claim `LuckyGiftDrawn` outbox | `Service.RunWorker``Service.ProcessPendingDrawOutbox``Repository.ClaimPendingLuckyGiftOutbox` | 已验收claim 使用 `delivering` 锁和 lock TTL |
| 同步返奖快路径 | 抽奖事务提交后,如果 `effective_reward_coins > 0`,立即调用 wallet-service `CreditLuckyGiftReward`;成功后把 draw 标记为 `granted` 并把交易 ID、账后余额写回响应 | `Service.Draw``creditLuckyGiftRewardFastPath``creditLuckyGiftReward``MarkLuckyGiftDrawsGranted` | 已验收:不在 activity MySQL 事务里调用 wallet成功响应可直接给 App 刷余额 |
| 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 |
| 钱包补偿 | worker 先检查 draw 是否已 `granted`;已发奖则跳过 wallet未发奖才用聚合 `draw_id` 生成幂等 command 补发一次 | `Service.processDrawOutbox``GetLuckyGiftDrawRewardState`、`walletv1.CreditLuckyGiftReward` 调用 | 已验收:wallet 成功但 activity 标记失败时worker 可用同一 command 补标;已 granted 不重复加币 |
| 房间开奖结果 IM | worker 投递 `lucky_gift_drawn` 房间群消息 | `publishLuckyGiftDrawn` | 已验收:发布失败时 outbox 可重试;已返奖的 draw 不会因为 IM 失败被标 failed |
| 区域播报 | 符合条件的中奖写区域播报 outbox | `shouldPublishLuckyGiftRegionBroadcast``publishLuckyGiftRegionBroadcast``luckyGiftRegionBroadcastPayload` | 已验收:当前阈值为有区域、有效奖励、聚合倍数不低于 1x |
| 成功收敛 | 钱包、区域播报、房间 IM 都成功后批量标记 draw records 为 granted | `MarkLuckyGiftDrawsGranted``markLuckyGiftDraws``MarkLuckyGiftOutboxDelivered` | 已验收:批量更新按 500 个 draw_id 分片 |
| 成功收敛 | 钱包成功后 draw records 先收敛`granted`;区域播报和房间 IM 成功后 outbox 标记 delivered | `MarkLuckyGiftDrawsGranted``markLuckyGiftDraws``MarkLuckyGiftOutboxDelivered` | 已验收:批量更新按 500 个 draw_id 分片 |
| 失败收敛 | 未返奖且达到最大重试时,批量标记 pending draw failed | `markOutboxFailed``MarkLuckyGiftDrawsFailed` | 已验收:已返奖但 IM 失败不会覆盖账务事实 |
| 重试退避 | outbox 失败按指数退避重试 | `luckyGiftBackoff``MarkLuckyGiftOutboxRetryable` | 已验收:错误保存在 `activity_outbox.last_error` |
@ -148,7 +149,7 @@
| `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 |
| `activity_outbox` | 幸运礼物钱包补偿和表现副作用,不再承载正常发币主路径 | `insertLuckyAggregateDrawOutbox``ClaimPendingLuckyGiftOutbox``MarkLuckyGiftOutboxDelivered` | 已验收:批量送礼只写一条 `LuckyGiftDrawn` outbox,已 granted 时 worker 跳过 wallet |
## 八、运行边界
@ -159,7 +160,7 @@
| 小额礼物 | 阶段按累计流水折算,单抽奖励按单份金额缩放,风控上限不被缩小 | `luckyRuntimeConfig``luckyEquivalentDraws``luckyRiskCapacity` | 已验收:单测覆盖大额后小额仍可中小奖 |
| 高倍奖 | 发布校验要求高倍档 `HighWaterOnly`,运行时仍受奖池和风控过滤 | `validateRuleStage``selectLuckyCandidate` | 已验收:高倍率未标高水位会拒绝发布 |
| 批量 777 份 | 抽 777 次,保留 777 条明细,状态写入和 outbox 聚合 | `executeOptimizedLuckyGiftDrawBatch``stageLuckyBatchDraw``persistLuckyBatchDraw` | 已验收:代码结构已移出循环内 SQL 状态写入 |
| worker 重试 | 钱包用聚合 `draw_id` 幂等outbox 失败可重试 | `processDrawOutbox`、`MarkLuckyGiftOutboxRetryable` | 已验收:成功后 delivered不重复加币 |
| worker 重试 | 钱包补偿用聚合 `draw_id` 幂等outbox 失败可重试;已 `granted` 的 draw 只补 IM/播报,不重复加币 | `processDrawOutbox`、`GetLuckyGiftDrawRewardState`、`MarkLuckyGiftOutboxRetryable` | 已验收:成功后 delivered不重复加币 |
| 已返奖但 IM 失败 | 不把 draw 标 failed只让 outbox failed 并保留告警空间 | `markOutboxFailed` | 已验收:`alreadyCredited` 分支不覆盖 draw 状态 |
## 九、代码验收清单

View File

@ -148,7 +148,7 @@
| 活动配置 | `TODO` | 只有 activity status projection | 活动规则、时间、奖池、风控配置未实现 |
| LuckyGift Check | `DONE` | `CheckLuckyGift` RPC、gateway 检查接口和 room-service 扣费前检查已实现 | App 礼物面板按 `pool_id` 联调 |
| LuckyGift Draw | `DONE` | `ExecuteLuckyGiftDraw`、三层奖池、RTP 窗口、风控计数、批量抽奖和幂等已实现 | 运营配置和长压测继续补 |
| 奖励入账 | `DONE` | activity outbox worker 调 wallet `CreditLuckyGiftReward`,批量抽奖按一次送礼聚合发放 | 线上监控和积压告警需补 |
| 奖励入账 | `DONE` | 幸运礼物中奖后优先同步调 wallet `CreditLuckyGiftReward` 入账并把余额返回给 Appactivity outbox worker 只做钱包补偿、IM 和区域播报 | 同步发奖成功率、补偿 pending age 和播报积压告警需补 |
| 幸运礼物审计 | `DONE` | `lucky_draw_records`、后台 draws 和 draw-summary 查询已实现 | 后台可视化维度继续补 |
## Messaging Product Surface