hyapp-server/docs/flutter对接/幸运礼物Flutter对接.md
2026-05-26 03:06:02 +08:00

583 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 幸运礼物 Flutter 对接
本文描述 Flutter App 对接幸运礼物所需的 HTTP 接口、请求参数、返回值、错误处理和腾讯云 IM 自定义消息。幸运礼物支持多个互相独立的奖池App 只负责传入 `pool_id`,不计算 RTP、倍率概率、奖池水位或中奖结果。
## 基础约定
本地开发地址:
```text
http://127.0.0.1:13000
```
业务接口统一响应 envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
| 字段 | 说明 |
| --- | --- |
| `code` | 成功固定 `OK`;失败为稳定错误码。 |
| `message` | 短错误文案,不用于客户端分支判断。 |
| `request_id` | 服务端链路追踪 ID客户端日志要记录。 |
| `data` | 成功业务数据;失败时通常不存在。 |
通用请求头:
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>`。 |
| `X-App-Code` | 否 | App 编码,默认 `lalu`;登录态接口最终以 token 内 app_code 为准。 |
| `Content-Type` | POST 必填 | `application/json`。 |
所有 `*_ms` 都是 Unix epoch milliseconds。金额单位都是整数金币。
## 核心概念
| 概念 | 说明 |
| --- | --- |
| `gift_id` | 用户实际送出的幸运礼物 ID例如某个 100 金币、5000 金币或 500000 金币礼物。 |
| `pool_id` | 后台配置的幸运礼物奖池 ID例如 `pool_95``pool_98`。不同 `pool_id` 的 RTP、奖池、风控和用户阶段完全隔离。 |
| `command_id` | 本次送礼动作幂等键。用户同一次点击产生一个值HTTP 超时或重试必须复用。 |
| `coin_spent` | 服务端按礼物价格和数量计算的实际扣费金额App 不上传该字段。 |
| `multiplier_ppm` | 倍率的 ppm 表达,`1x=1000000``0.5x=500000``100x=100000000`。 |
| `base_reward_coins` | 基础 RTP 返奖,计入平台成本控制。 |
| `effective_reward_coins` | 用户可见总奖励,等于基础返奖、房间气氛奖励、活动补贴之和。 |
App 不要展示后台 RTP 数值,不要展示实时概率,不要本地推算“下一次该中”。客户端只展示服务端返回的结果。
## 当前实现状态
当前仓库已经具备:
| 能力 | 状态 |
| --- | --- |
| `POST /api/v1/rooms/gift/send` 支持 `pool_id` 入参 | 已有 |
| 房间送礼 IM `room_gift_sent` 可携带 `pool_id` | 已有 |
| `POST /api/v1/activities/lucky-gifts/check` | 已有 |
| `activity-service` 内部 gRPC `CheckLuckyGift` / `ExecuteLuckyGiftDraw` | 已有 |
| 后台按 `pool_id` 配置多奖池 | 已有 |
| 送礼响应直接返回抽奖结果 `data.lucky_gift` | 已有 |
| 房间 IM `lucky_gift_drawn` | 已有 |
| 中奖返奖入账并触发 `WalletBalanceChanged` | 已有 |
简洁版接入清单见 [幸运礼物接口闭环Flutter对接.md](/Users/hy/Documents/hy/hyapp-server/docs/flutter对接/幸运礼物接口闭环Flutter对接.md)。
## 客户端流程
1. 进入房间后调用 `GET /api/v1/rooms/{room_id}/gift-panel` 获取礼物面板、金币余额和收礼人。
2. 找到 `gift_type_code=lucky``gift_type_code=super_lucky` 的礼物,读取该礼物对应的 `pool_id`。当前可从 `presentation_json.lucky_pool_id` 下发,后续建议提升为顶层字段 `lucky_pool_id`
3. 用户点击幸运礼物前,可调用 `POST /api/v1/activities/lucky-gifts/check` 判断奖池是否启用。
4. 用户确认送礼时调用 `POST /api/v1/rooms/gift/send`,请求体带上 `pool_id`
5. 成功响应里如果有 `lucky_gift`,立即播放中奖或未中奖反馈。
6. 房间内监听 `room_gift_sent``lucky_gift_drawn` IM给其他用户同步展示。
7. 金币余额以 `WalletBalanceChanged` 私有 IM 或 `GET /api/v1/wallet/me/balances` 刷新为准,不在本地推算。
## 获取礼物面板
```http
GET /api/v1/rooms/{room_id}/gift-panel
Authorization: Bearer <access_token>
X-App-Code: lalu
```
路径参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前语音房 ID。 |
成功响应节选:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"coin_balance": 1000000,
"recipients": [],
"tabs": [],
"gifts": [
{
"gift_id": "lucky_star_100",
"name": "幸运星",
"gift_type_code": "lucky",
"charge_asset_type": "COIN",
"coin_price": 100,
"presentation_json": "{\"lucky_pool_id\":\"pool_95\"}",
"resource": {
"asset_url": "https://cdn.example/lucky_star.png",
"animation_url": "https://cdn.example/lucky_star.svga"
}
}
],
"quantity_presets": [1, 9, 99, 999]
}
}
```
幸运礼物识别规则:
| 字段 | 说明 |
| --- | --- |
| `gift_type_code=lucky` | 普通幸运礼物。 |
| `gift_type_code=super_lucky` | 超级幸运礼物。 |
| `presentation_json.lucky_pool_id` | 建议下发的奖池 ID为空时 App 传 `default` 或不传,由服务端按默认奖池处理。 |
| `coin_price` | 单个礼物价格;实际扣费仍以服务端钱包价格为准。 |
## 检查幸运礼物状态
建议新增 App HTTP 接口:
```http
POST /api/v1/activities/lucky-gifts/check
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
请求体:
```json
{
"room_id": "lalu_xxx",
"gift_id": "lucky_star_100",
"pool_id": "pool_95"
}
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `gift_id` | string | 是 | 用户准备送出的幸运礼物 ID。 |
| `pool_id` | string | 否 | 奖池 ID为空时服务端使用 `default`。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"enabled": true,
"reason": "enabled",
"pool_id": "pool_95",
"gift_id": "lucky_star_100",
"rule_version": 12,
"experience_pool": "novice",
"server_time_ms": 1779259000000
}
}
```
返回字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 是否允许当前用户在当前房间抽该奖池。 |
| `reason` | string | 服务端原因,客户端只做日志或灰度排查。 |
| `pool_id` | string | 实际使用的奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `rule_version` | int64 | 当前奖池规则版本。 |
| `experience_pool` | string | `novice``intermediate``advanced`,只影响展示文案或动效强度。 |
| `server_time_ms` | int64 | 服务端当前时间。 |
客户端处理:
| 场景 | 处理 |
| --- | --- |
| `enabled=true` | 允许点击送礼。 |
| `enabled=false` | 置灰幸运抽奖入口,但普通礼物展示可继续。 |
| 接口失败 | 不在本地兜底开奖;可以提示稍后重试或隐藏幸运玩法。 |
## 送礼并抽奖
已存在 App HTTP 入口:
```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
请求体:
```json
{
"room_id": "lalu_xxx",
"command_id": "gift_1779259000000_10001_01",
"target_type": "user",
"target_user_ids": [10002],
"gift_id": "lucky_star_100",
"gift_count": 1,
"pool_id": "pool_95"
}
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `command_id` | string | 是 | 本次用户动作幂等键;同一次重试必须复用。 |
| `target_type` | string | 否 | 当前主流程使用 `user`;为空按 `user`。 |
| `target_user_ids` | int64[] | 是 | 收礼用户 ID`target_type=user` 时必须且只能 1 个。 |
| `target_user_id` | int64 | 否 | 单目标兼容字段;新代码优先用 `target_user_ids`。 |
| `gift_id` | string | 是 | 礼物 ID。 |
| `gift_count` | int32 | 是 | 礼物数量,必须大于 0。 |
| `pool_id` | string | 否 | 幸运礼物奖池 ID不同奖池完全隔离。普通礼物可不传。 |
成功响应节选:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"result": {
"applied": true,
"room_version": 14,
"server_time_ms": 1779259000000
},
"billing_receipt_id": "br_xxx",
"room_heat": 123456,
"gift_rank": [],
"room": {},
"treasure": {},
"lucky_gift": {
"enabled": true,
"draw_id": "lucky_draw_xxx",
"command_id": "gift_1779259000000_10001_01",
"pool_id": "pool_95",
"gift_id": "lucky_star_100",
"rule_version": 12,
"experience_pool": "novice",
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"base_reward_coins": 200,
"room_atmosphere_reward_coins": 0,
"activity_subsidy_coins": 0,
"effective_reward_coins": 200,
"reward_status": "granted",
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000
}
}
}
```
`lucky_gift` 字段为建议新增。普通礼物或奖池未启用时可以为空。
`data` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次命令是否实际落地;幂等重放可能为 `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 | 和送礼请求一致的幂等键。 |
| `pool_id` | string | 本次使用的奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `rule_version` | int64 | 抽奖时使用的规则版本。 |
| `experience_pool` | string | `novice``intermediate``advanced`。 |
| `selected_tier_id` | string | 服务端选中的奖档 ID。 |
| `multiplier_ppm` | int64 | 建议新增;用于直接展示倍率。后端暂未返回时客户端不要本地解析倍率。 |
| `base_reward_coins` | int64 | 基础 RTP 返奖。 |
| `room_atmosphere_reward_coins` | int64 | 房间气氛补贴奖励。 |
| `activity_subsidy_coins` | int64 | 活动补贴奖励。 |
| `effective_reward_coins` | int64 | 用户可见总奖励。 |
| `reward_status` | string | `pending``granted``failed`。 |
| `stage_feedback` | bool | 是否触发阶段反馈。可能没有金币奖励。 |
| `high_multiplier` | bool | 是否属于高倍结果,可用于强化动效。 |
| `created_at_ms` | int64 | 抽奖结果生成时间。 |
展示规则:
| 场景 | 处理 |
| --- | --- |
| `lucky_gift` 为空 | 按普通礼物成功展示。 |
| `effective_reward_coins=0``stage_feedback=false` | 展示未中奖或普通送礼效果。 |
| `stage_feedback=true` | 展示阶段进度、幸运值或轻量动画;不要本地加金币。 |
| `effective_reward_coins>0``reward_status=granted` | 展示中奖金额和到账效果;余额仍以钱包 IM 为准。 |
| `effective_reward_coins>0``reward_status=pending` | 展示“奖励处理中”,等待钱包 IM 或余额刷新。 |
| `reward_status=failed` | 不本地加金币;提示稍后刷新或走补偿查询。 |
| `high_multiplier=true` | 播放高倍中奖动效,并等待房间 IM 同步其他用户。 |
## 错误处理
通用错误:
| HTTP | `code` | 说明 | Flutter 处理 |
| --- | --- | --- | --- |
| 400 | `INVALID_JSON` | JSON 解析失败。 | 修正客户端请求体。 |
| 400 | `INVALID_ARGUMENT` | 参数不合法,例如缺 `room_id``gift_id``command_id`。 | 不重试,提示操作失败并打点。 |
| 401 | `UNAUTHORIZED` | token 无效或过期。 | 走登录刷新或重新登录。 |
| 403 | `PROFILE_REQUIRED` | 用户未完成资料。 | 引导补全资料。 |
| 403 | `PERMISSION_DENIED` | 没有权限。 | 提示不可操作。 |
| 409 | `INSUFFICIENT_BALANCE` | 金币不足。 | 弹充值或余额不足提示。 |
| 409 | `ROOM_CLOSED` | 房间已关闭。 | 退出或刷新房间状态。 |
| 409 | `RULE_NOT_ACTIVE` | 幸运礼物奖池未启用。 | 置灰幸运玩法,不本地开奖。 |
| 409 | `IDEMPOTENCY_CONFLICT` | 同一 `command_id` 对应的请求内容不一致。 | 生成新的 `command_id` 重新发起新动作;旧动作不再重放。 |
| 409 | `CONFLICT` | 规则、奖池、水位或风控状态冲突。 | 提示稍后再试,必要时刷新面板。 |
| 502 | `UPSTREAM_ERROR` | room、wallet、activity 等依赖暂不可用。 | 可保留同一个 `command_id` 重试同一次送礼。 |
| 500 | `INTERNAL_ERROR` | 服务端内部异常。 | 记录 `request_id`,提示稍后重试。 |
幂等和重试规则:
| 场景 | 处理 |
| --- | --- |
| HTTP 超时、断网、502 | 使用同一个 `command_id` 重试。 |
| 409 `IDEMPOTENCY_CONFLICT` | 不要复用该 `command_id`;说明客户端把同一个幂等键用于不同请求。 |
| 收到成功响应且有 `billing_receipt_id` | 扣费已成功,不要再次生成新命令补发。 |
| 成功响应丢失但钱包余额已变化 | 以钱包余额和房间 IM 为准,避免重复扣费。 |
| App 进程重启 | 本地持久化最近未完成的 `command_id`,优先用同一键查询或重试。 |
## IM房间送礼消息
已存在房间 IM
| 字段 | 值 |
| --- | --- |
| 腾讯消息类型 | `TIMCustomElem` |
| `Desc` | `room_gift_sent` |
| `Ext` / `extension` | `room_system_message` |
| 群 ID | 当前 `room_id` |
| `CloudCustomData` | 与 `TIMCustomElem.Data` 相同的 JSON 字符串 |
Payload 示例:
```json
{
"event_id": "evt_xxx",
"room_id": "lalu_xxx",
"event_type": "room_gift_sent",
"actor_user_id": 10001,
"target_user_id": 10002,
"gift_value": 100,
"room_heat": 123456,
"room_version": 14,
"attributes": {
"gift_id": "lucky_star_100",
"pool_id": "pool_95",
"gift_count": "1",
"billing_receipt_id": "br_xxx",
"coin_spent": "100",
"gift_point_added": "10",
"price_version": "pv_xxx"
}
}
```
处理规则:
| 场景 | 处理 |
| --- | --- |
| `Ext` 不是 `room_system_message` | 忽略。 |
| `event_type` 不是 `room_gift_sent` | 交给其他房间消息处理器。 |
| `event_id` 已处理 | 忽略。 |
| `room_version` 小于本地当前版本 | 忽略或只展示轻量动画,不回滚房间状态。 |
| `attributes.pool_id` 为空 | 按普通礼物处理。 |
| `attributes.pool_id` 非空 | 展示幸运礼物送出效果,但中奖结果仍等 `lucky_gift_drawn` 或 HTTP `lucky_gift`。 |
## IM幸运礼物开奖结果
建议新增房间 IM
| 字段 | 值 |
| --- | --- |
| 腾讯消息类型 | `TIMCustomElem` |
| `Desc` | `lucky_gift_drawn` |
| `Ext` / `extension` | `room_system_message` |
| 群 ID | 当前 `room_id` |
| `CloudCustomData` | 与 `TIMCustomElem.Data` 相同的 JSON 字符串 |
Payload 示例:
```json
{
"event_id": "lucky_draw_event_xxx",
"room_id": "lalu_xxx",
"event_type": "lucky_gift_drawn",
"room_version": 14,
"draw_id": "lucky_draw_xxx",
"command_id": "gift_1779259000000_10001_01",
"pool_id": "pool_95",
"gift_id": "lucky_star_100",
"gift_count": 1,
"sender_user_id": 10001,
"target_user_id": 10002,
"coin_spent": 100,
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"effective_reward_coins": 200,
"reward_status": "granted",
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | IM 事件幂等键。 |
| `event_type` | string | 固定 `lucky_gift_drawn`。 |
| `room_id` | string | 房间 ID。 |
| `room_version` | int64 | 房间版本。 |
| `draw_id` | string | 抽奖记录 ID。 |
| `command_id` | string | 送礼幂等键。 |
| `pool_id` | string | 奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `gift_count` | int32 | 礼物数量。 |
| `sender_user_id` | int64 | 送礼用户。 |
| `target_user_id` | int64 | 收礼用户。 |
| `coin_spent` | int64 | 本次实际扣费。 |
| `selected_tier_id` | string | 中奖奖档。 |
| `multiplier_ppm` | int64 | 中奖倍率,建议新增。 |
| `effective_reward_coins` | int64 | 用户可见奖励金币。 |
| `reward_status` | string | `pending``granted``failed`。 |
| `stage_feedback` | bool | 是否阶段反馈。 |
| `high_multiplier` | bool | 是否高倍结果。 |
| `created_at_ms` | int64 | 事件创建时间。 |
展示规则:
| 场景 | 处理 |
| --- | --- |
| 当前用户是 `sender_user_id` | 如果 HTTP 已展示过同一 `draw_id`,只更新状态,不重复播中奖动画。 |
| 当前用户不是 `sender_user_id` | 展示房间内中奖横幅、飘屏或公屏提示。 |
| `effective_reward_coins=0` | 不展示大奖飘屏,只保留普通送礼效果或阶段反馈。 |
| `high_multiplier=true` | 展示高倍动画。 |
| `reward_status=pending` | 文案使用“奖励处理中”,不要展示已到账。 |
| JSON 解析失败或字段缺失 | 忽略并记录客户端日志。 |
## IM钱包余额变更
幸运礼物扣费和返奖都应通过钱包私有 IM 同步余额。该 IM 已在钱包文档中定义。
| 字段 | 值 |
| --- | --- |
| 腾讯消息类型 | `TIMCustomElem` |
| `Desc` | `WalletBalanceChanged` |
| `Ext` / `extension` | `wallet_notice` |
| 通道 | C2C 私有消息 |
Payload 示例:
```json
{
"event_type": "WalletBalanceChanged",
"event_id": "wev_xxx",
"app_code": "lalu",
"transaction_id": "tx_xxx",
"command_id": "gift_1779259000000_10001_01",
"user_id": "10001",
"asset_type": "COIN",
"available_delta": -100,
"frozen_delta": 0,
"available_after": 999900,
"frozen_after": 0,
"balance_version": 18,
"created_at_ms": 1779259000000,
"source_created_at_ms": 1779259000000,
"metadata": {
"biz_type": "gift_debit",
"gift_id": "lucky_star_100",
"pool_id": "pool_95"
}
}
```
返奖到账时 `available_delta` 为正数,`metadata.biz_type` 建议使用 `lucky_gift_reward`
处理规则:
| 场景 | 处理 |
| --- | --- |
| `Ext` 不是 `wallet_notice` | 忽略。 |
| `event_type` 不是 `WalletBalanceChanged` | 忽略。 |
| `event_id` 已处理 | 忽略。 |
| 本地 `asset_type` 的版本 `>= balance_version` | 忽略。 |
| 长时间没有收到余额 IM | 调 `GET /api/v1/wallet/me/balances` 兜底刷新。 |
## IM高倍全站或区域播报
建议新增播报 IM用于高倍中奖或运营配置的大额中奖展示。
| 字段 | 值 |
| --- | --- |
| 腾讯消息类型 | `TIMCustomElem` |
| `Desc` | `lucky_gift_big_win` |
| `Ext` / `extension` | `im_broadcast` |
| 群 ID | 全站播报群或区域播报群 |
Payload 示例:
```json
{
"event_id": "lucky_big_win_xxx",
"broadcast_type": "lucky_gift_big_win",
"scope": "region",
"app_code": "lalu",
"region_id": 1001,
"room_id": "lalu_xxx",
"sender_user_id": 10001,
"target_user_id": 10002,
"pool_id": "pool_95",
"gift_id": "lucky_star_100",
"multiplier_ppm": 100000000,
"effective_reward_coins": 10000,
"sent_at_ms": 1779259000000,
"action": {
"type": "enter_room",
"room_id": "lalu_xxx"
}
}
```
处理规则:
| 场景 | 处理 |
| --- | --- |
| 当前用户已在该房间且已收到 `lucky_gift_drawn` | 可以只展示一次,避免重复刷屏。 |
| 当前用户不在该房间 | 展示全站或区域飘屏,点击 `action` 进房。 |
| `event_id` 已处理 | 忽略。 |
## App 最小对接顺序
1. 登录后调用 `GET /api/v1/im/usersig`,登录腾讯云 IM。
2. 进入房间后加入房间 IM 群,并调用 `GET /api/v1/rooms/{room_id}/gift-panel`
3. 从礼物面板识别幸运礼物和对应 `pool_id`
4. 用户点击送礼时生成 `command_id`,调用 `POST /api/v1/rooms/gift/send`
5. 使用 HTTP 响应里的 `lucky_gift` 做当前用户即时反馈。
6. 监听房间 IM `room_gift_sent``lucky_gift_drawn`,同步其他用户看到的表现。
7. 监听 C2C IM `WalletBalanceChanged`,更新金币余额。
8. 任意 IM 丢失、页面重进或余额不一致时,重新拉取礼物面板和钱包余额纠正状态。