20 KiB
幸运礼物 Flutter 对接
本文描述 Flutter App 对接幸运礼物所需的 HTTP 接口、请求参数、返回值、错误处理和腾讯云 IM 自定义消息。幸运礼物支持多个互相独立的奖池,App 只负责传入 pool_id,不计算 RTP、倍率概率、奖池水位或中奖结果。
基础约定
本地开发地址:
http://127.0.0.1:13000
业务接口统一响应 envelope:
{
"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。
客户端流程
- 进入房间后调用
GET /api/v1/rooms/{room_id}/gift-panel获取礼物面板、金币余额和收礼人。 - 找到
gift_type_code=lucky或gift_type_code=super_lucky的礼物,读取该礼物对应的pool_id。当前可从presentation_json.lucky_pool_id下发,后续建议提升为顶层字段lucky_pool_id。 - 用户点击幸运礼物前,可调用
POST /api/v1/activities/lucky-gifts/check判断奖池是否启用;当前 check 接口要求pool_id必填。 - 用户确认送礼时调用
POST /api/v1/rooms/gift/send,请求体带上pool_id。 - 成功响应里如果有
lucky_gift,立即播放中奖或未中奖反馈。 - 房间内监听
room_gift_sent和lucky_gift_drawnIM,给其他用户同步展示。 - 金币余额以
WalletBalanceChanged私有 IM 或GET /api/v1/wallet/me/balances刷新为准,不在本地推算。
获取礼物面板
GET /api/v1/rooms/{room_id}/gift-panel
Authorization: Bearer <access_token>
X-App-Code: lalu
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
room_id |
string | 是 | 当前语音房 ID。 |
成功响应节选:
{
"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 接口:
POST /api/v1/activities/lucky-gifts/check
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/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;当前 gateway check 接口必填。 |
成功响应:
{
"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 |
置灰幸运抽奖入口,但普通礼物展示可继续。 |
| 接口失败 | 不在本地兜底开奖;可以提示稍后重试或隐藏幸运玩法。 |
注意:room-service 内部仍会把空 pool_id 归一化为默认奖池,但 App check 接口不接受空值。客户端必须从礼物面板拿到明确 pool_id 后再走幸运礼物流程。
送礼并抽奖
已存在 App HTTP 入口:
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/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;不同奖池完全隔离。普通礼物可不传。 |
成功响应节选:
{
"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 | 已返回;用于直接展示倍率。客户端不要从 selected_tier_id 解析倍率。 |
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 示例:
{
"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 示例:
{
"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 示例:
{
"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,用于高倍中奖或运营配置的大额中奖展示;当前服务端触发阈值是 multiplier_ppm >= 1000000 且有 visible_region_id。
| 字段 | 值 |
|---|---|
| 腾讯消息类型 | TIMCustomElem |
Desc |
lucky_gift_big_win |
Ext / extension |
im_broadcast |
| 群 ID | 全站播报群或区域播报群 |
Payload 示例:
{
"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 最小对接顺序
- 登录后调用
GET /api/v1/im/usersig,登录腾讯云 IM。 - 进入房间后加入房间 IM 群,并调用
GET /api/v1/rooms/{room_id}/gift-panel。 - 从礼物面板识别幸运礼物和对应
pool_id。 - 用户点击送礼时生成
command_id,调用POST /api/v1/rooms/gift/send。 - 使用 HTTP 响应里的
lucky_gift做当前用户即时反馈。 - 监听房间 IM
room_gift_sent、lucky_gift_drawn,同步其他用户看到的表现。 - 监听 C2C IM
WalletBalanceChanged,更新金币余额。 - 任意 IM 丢失、页面重进或余额不一致时,重新拉取礼物面板和钱包余额纠正状态。