21 KiB
语音房宝箱 Flutter 对接
本文描述 Flutter App 对接语音房宝箱所需的 HTTP 接口、请求参数、返回值、错误处理和腾讯云 IM 自定义消息 key。宝箱进度、开箱和发奖事实由 room-service Room Cell 持有;客户端只展示服务端返回或 IM 推送的状态,不能本地计算能量、奖励或开箱结果。
基础约定
本地开发地址:
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。业务切日按 UTC,不按用户本地时区。
客户端流程
- 用户成功
JoinRoom后调用GET /api/v1/rooms/{room_id}/treasure初始化 7 级物料和当前进度。 - 用户送礼调用
POST /api/v1/rooms/gift/send;成功响应里的data.treasure可立即更新当前宝箱状态。 - 房间内监听腾讯 IM 群
TIMCustomElem.Ext=room_system_message,处理宝箱进度、倒计时、开箱和发奖。 - App 全局或区域播报监听
TIMCustomElem.Ext=im_broadcast且broadcast_type=room_treasure。 - 任意 IM 丢失、字段解析失败、版本倒退或页面重进时,重新调用
GET /api/v1/rooms/{room_id}/treasure纠正本地状态。
客户端必须用 event_id 去重,用 room_version 丢弃旧房间消息。
获取宝箱信息
GET /api/v1/rooms/{room_id}/treasure
Authorization: Bearer <access_token>
X-App-Code: lalu
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
room_id |
string | 是 | 当前语音房 ID,例如 lalu_534d076a-7cbd-4b23-a297-0c10a3c2fa72。 |
成功响应:
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"enabled": true,
"levels": [
{
"level": 1,
"energy_threshold": 1000,
"cover_url": "https://cdn.example/chest/lv1/cover.png",
"animation_url": "https://cdn.example/chest/lv1/idle.svga",
"opening_animation_url": "https://cdn.example/chest/lv1/opening.svga",
"opened_image_url": "https://cdn.example/chest/lv1/opened.png",
"in_room_rewards": [
{
"reward_item_id": "lv1_in_room_1",
"resource_group_id": 1001,
"weight": 10000,
"display_name": "Gold Pack",
"icon_url": "https://cdn.example/reward/gold.png"
}
],
"top1_rewards": [],
"igniter_rewards": []
}
],
"state": {
"current_level": 1,
"current_progress": 0,
"energy_threshold": 1000,
"status": "idle",
"reset_at_ms": 1779321600000,
"top1_user_id": "",
"igniter_user_id": "",
"box_id": "",
"config_version": 3,
"last_rewards": []
},
"broadcast_scope": "region",
"open_delay_ms": 30000,
"broadcast_delay_ms": 0,
"reward_stack_policy": "allow_stack",
"server_time_ms": 1779259000000
}
}
data 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
enabled |
bool | 宝箱总开关。false 时客户端隐藏或置灰宝箱入口。 |
levels |
array | 固定 7 个等级的物料、阈值和奖励池。 |
state |
object | 当前房间当前 UTC 日的宝箱状态。 |
broadcast_scope |
string | none、region、global。 |
open_delay_ms |
int64 | 满能量后到开箱的倒计时配置。 |
broadcast_delay_ms |
int64 | 后台配置字段;当前房间事件会立即进入播报链路。 |
reward_stack_policy |
string | allow_stack 或 priority_only。 |
server_time_ms |
int64 | 服务端当前时间,倒计时要以它校准本地时钟偏差。 |
levels[] 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
level |
int32 | 等级,1 到 7。 |
energy_threshold |
int64 | 当前等级满能量阈值。 |
cover_url |
string | 静态封面图。 |
animation_url |
string | 未开箱/蓄能态动效。 |
opening_animation_url |
string | 开箱动效。 |
opened_image_url |
string | 开箱后展示图。 |
in_room_rewards |
array | 开箱时仍在房用户的奖励候选展示。 |
top1_rewards |
array | 倒计时开始时贡献第一用户奖励候选展示。 |
igniter_rewards |
array | 点火人奖励候选展示。 |
奖励候选字段:
| 字段 | 类型 | 说明 |
|---|---|---|
reward_item_id |
string | 后台配置的奖励项 ID。 |
resource_group_id |
int64 | 钱包资源组 ID。 |
weight |
int64 | 抽奖权重,只用于展示或调试,不参与客户端抽奖。 |
display_name |
string | 奖励展示名称。 |
icon_url |
string | 奖励图标。 |
state 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
current_level |
int32 | 当前等级,1 到 7。 |
current_progress |
int64 | 当前等级已累计有效能量。 |
energy_threshold |
int64 | 当前等级阈值。 |
status |
string | 当前返回 idle 或 countdown。 |
countdown_started_at_ms |
int64 | 倒计时开始时间;非倒计时可为空或 0。 |
open_at_ms |
int64 | 预计开箱时间;status=countdown 时有效。 |
opened_at_ms |
int64 | 最近一次开箱时间。 |
reset_at_ms |
int64 | 下一个 UTC 0 点;到点后进度按新 UTC 日重置。 |
top1_user_id |
string | 倒计时开始时锁定的贡献第一;GET 接口中按字符串返回。 |
igniter_user_id |
string | 点火人;GET 接口中按字符串返回。 |
box_id |
string | 当前倒计时宝箱或最近状态的单轮幂等 ID。 |
config_version |
int64 | 当前状态关联的后台配置版本。 |
last_rewards |
array | 最近一次开箱发奖结果;客户端可用于展示开箱结果兜底。 |
last_rewards[] 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
reward_role |
string | in_room、top1、igniter。 |
user_id |
string | 获奖用户 ID 字符串。 |
reward_item_id |
string | 奖励项 ID。 |
resource_group_id |
int64 | 资源组 ID。 |
display_name |
string | 奖励展示名称。 |
icon_url |
string | 奖励图标。 |
grant_id |
string | 钱包发放记录 ID。 |
status |
string | 当前成功发放为 succeeded。 |
展示规则:
- 进度条使用
current_progress / energy_threshold,并且最大显示 100%。 - 倒计时使用
open_at_ms - server_time_ms初始化,再用本地 timer 驱动。 - 不要把送礼溢出能量展示到下一等级;服务端规则是溢出作废。
- 如果
status=countdown,继续送礼不会改变宝箱进度、点火人、贡献第一或open_at_ms。
送礼并更新宝箱
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
请求体:
{
"room_id": "lalu_xxx",
"command_id": "cmd_send_gift_lalu_xxx_12345678_1779259000000",
"target_type": "user",
"target_user_ids": [12345678],
"gift_id": "rocket",
"gift_count": 1
}
参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
room_id |
string | 是 | 当前房间 ID。 |
command_id |
string | 是 | 本次送礼动作幂等键;同一次 HTTP 重试必须复用。 |
target_type |
string | 否 | 当前只支持 user;为空按 user 处理。 |
target_user_ids |
int64[] | 是 | 收礼用户列表;target_type=user 时必须且只能 1 个。 |
target_user_id |
int64 | 否 | 旧客户端兼容字段;新客户端优先用 target_user_ids。 |
gift_id |
string | 是 | 礼物 ID。 |
gift_count |
int32 | 是 | 礼物数量,必须大于 0。 |
成功响应核心字段:
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"result": {
"applied": true,
"room_version": 7,
"server_time_ms": 1779259502200
},
"billing_receipt_id": "bill_xxx",
"room_heat": 500,
"gift_rank": [
{
"user_id": 12345678,
"score": 500,
"gift_value": 500,
"updated_at_ms": 1779259502104
}
],
"room": {},
"treasure": {
"current_level": 2,
"current_progress": 200,
"energy_threshold": 200,
"status": "countdown",
"countdown_started_at_ms": 1779259502104,
"open_at_ms": 1779259512104,
"reset_at_ms": 1779321600000,
"top1_user_id": 12345678,
"igniter_user_id": 12345678,
"box_id": "room_treasure_box_xxx",
"config_version": 3
}
}
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
data.result.applied |
bool | 命令是否产生新状态;同一 command_id 重试可能返回历史结果。 |
data.result.room_version |
int64 | 房间版本,用于和 IM 消息去重、排序。 |
data.billing_receipt_id |
string | 钱包扣费回执。 |
data.room_heat |
int64 | 送礼后的房间热度。 |
data.gift_rank |
array | 送礼后的本地贡献榜快照。 |
data.room |
object | 房间快照。 |
data.treasure |
object | 送礼后宝箱状态;送礼人可立即用它刷新 UI。 |
注意:GET /treasure 的用户 ID 字段按字符串返回,POST /gift/send 透出的 protobuf 状态里部分用户 ID 可能是数字。Flutter 侧建议用统一 helper 兼容 String 和 num。
String readId(dynamic value) {
if (value == null) return '';
if (value is String) return value;
if (value is num) return value.toInt().toString();
return '';
}
幂等规则:
| 场景 | 处理 |
|---|---|
| HTTP 超时后重试同一次送礼 | 复用原 command_id。 |
| 用户点了第二次送礼按钮 | 生成新的 command_id。 |
同一个 command_id 换了礼物、数量或目标 |
服务端返回冲突错误。 |
错误处理
失败响应:
{
"code": "PERMISSION_DENIED",
"message": "permission denied",
"request_id": "req_xxx"
}
通用错误:
| HTTP | code |
场景 | Flutter 处理 |
|---|---|---|---|
| 400 | INVALID_JSON |
请求体不是合法 JSON。 | 修正客户端请求,不重试。 |
| 400 | INVALID_ARGUMENT |
缺字段、数量非法、room_id 格式非法、暂不支持的 target_type。 |
修正参数,不自动重试。 |
| 401 | UNAUTHORIZED |
token 缺失、过期或无效。 | 走登录刷新或重新登录。 |
| 403 | PROFILE_REQUIRED |
用户资料未完成。 | 跳转资料补全流程。 |
| 403 | PERMISSION_DENIED |
不在房间、被 ban、无权限读取或操作。 | 退出房间页或刷新房间状态。 |
| 404 | NOT_FOUND |
房间不存在、送礼人不在房、收礼人不在房、礼物不可用。 | 刷新房间状态;礼物不可用时刷新礼物面板。 |
| 409 | ROOM_CLOSED |
房间已关闭。 | 退出房间页并刷新列表。 |
| 409 | CONFLICT |
状态或唯一约束冲突。 | 刷新后重试。 |
| 409 | IDEMPOTENCY_CONFLICT |
command_id 复用但请求内容不同。 |
新用户动作生成新 command_id;重试旧动作必须保持原请求体。 |
| 409 | INSUFFICIENT_BALANCE |
钱包余额不足。 | 弹充值/余额不足提示。 |
| 409 | LEDGER_CONFLICT |
钱包并发账本冲突。 | 可短延迟重试一次,仍失败则刷新余额。 |
| 502 | UPSTREAM_ERROR |
room/wallet 等依赖暂不可用。 | 提示稍后重试,可保留原 command_id 重试同一次送礼。 |
| 500 | INTERNAL_ERROR |
服务端内部错误。 | 提示失败并记录 request_id。 |
宝箱相关额外规则:
- 获取宝箱信息要求当前用户仍在房间业务 presence 内;离房后调用会返回
PERMISSION_DENIED或NOT_FOUND。 - 宝箱关闭时,
GET /treasure仍可能返回enabled=false和 7 级配置;客户端不展示进度入口即可。 - 发奖自动完成,没有 App 领取接口;客户端不要补发或重复调用钱包接口。
房间群 IM 消息
房间内宝箱进度、倒计时、开箱和发奖通过腾讯云 IM 房间群 TIMCustomElem 投递。
| 字段 | 值 |
|---|---|
MsgType |
TIMCustomElem |
MsgContent.Ext |
room_system_message |
MsgContent.Desc |
等于 Data.event_type |
MsgContent.Data |
JSON 字符串 |
CloudCustomData |
与 MsgContent.Data 相同的 JSON 字符串 |
Data 外层结构:
{
"event_id": "evt_xxx",
"room_id": "lalu_xxx",
"event_type": "room_treasure_progress_changed",
"actor_user_id": 12345678,
"target_user_id": 0,
"room_version": 8,
"attributes": {
"box_id": "room_treasure_box_xxx"
}
}
通用字段:
| 字段 | 类型 | 说明 |
|---|---|---|
event_id |
string | IM 去重键。 |
room_id |
string | 房间 ID。 |
event_type |
string | 事件类型,见下表。 |
actor_user_id |
int64 | 触发用户;倒计时/开箱通常为点火人。 |
target_user_id |
int64 | 目标用户;倒计时/开箱通常为 top1。 |
room_version |
int64 | 房间版本,本地版本更高时丢弃旧消息。 |
attributes |
map<string,string> | 业务字段;值全部按字符串传输,Flutter 侧按需转 int/bool/JSON。 |
宝箱房间群事件:
event_type |
Desc |
用途 |
|---|---|---|
room_treasure_progress_changed |
room_treasure_progress_changed |
有效宝箱能量增加。 |
room_treasure_countdown_started |
room_treasure_countdown_started |
当前等级满能量,进入倒计时。 |
room_treasure_opened |
room_treasure_opened |
宝箱已打开,包含公开奖励摘要。 |
room_treasure_reward_granted |
room_treasure_reward_granted |
发奖结果事件,包含奖励列表。 |
room_treasure_progress_changed
示例:
{
"event_id": "evt_progress",
"room_id": "lalu_xxx",
"event_type": "room_treasure_progress_changed",
"actor_user_id": 12345678,
"room_version": 6,
"attributes": {
"box_id": "room_treasure_box_xxx",
"level": "2",
"added_energy": "300",
"effective_added_energy": "200",
"overflow_energy": "100",
"current_progress": "200",
"energy_threshold": "200",
"status": "countdown",
"reset_at_ms": "1779321600000",
"gift_id": "rocket",
"gift_count": "3",
"command_id": "cmd_send_gift_xxx"
}
}
处理:
- 更新当前等级进度为
current_progress。 effective_added_energy是本次实际计入宝箱的能量。overflow_energy是作废能量,不能加到下一等级。- 如果
status=countdown,立即切换倒计时态,等待room_treasure_countdown_started补齐open_at_ms;如该消息丢失,调用 GET 兜底。
room_treasure_countdown_started
示例:
{
"event_id": "evt_countdown",
"room_id": "lalu_xxx",
"event_type": "room_treasure_countdown_started",
"actor_user_id": 12345678,
"target_user_id": 12345678,
"room_version": 6,
"attributes": {
"box_id": "room_treasure_box_xxx",
"level": "2",
"current_progress": "200",
"energy_threshold": "200",
"countdown_start_ms": "1779259502104",
"open_at_ms": "1779259512104",
"broadcast_scope": "region",
"visible_region_id": "1001",
"top1_user_id": "12345678",
"igniter_user_id": "12345678",
"command_id": "cmd_send_gift_xxx"
}
}
处理:
- 按
open_at_ms展示开箱倒计时。 top1_user_id和igniter_user_id在此刻锁定;后续离房或下线也不影响这两类奖励发放。- 倒计时期间再收到普通
room_gift_sent,只更新礼物表现和热度,不改变宝箱倒计时。
注意:房间群 IM 字段名是 countdown_start_ms,HTTP state 字段名是 countdown_started_at_ms。
room_treasure_opened
示例:
{
"event_id": "evt_opened",
"room_id": "lalu_xxx",
"event_type": "room_treasure_opened",
"actor_user_id": 12345678,
"target_user_id": 12345678,
"room_version": 8,
"attributes": {
"box_id": "room_treasure_box_xxx",
"level": "2",
"next_level": "3",
"opened_at_ms": "1779259512668",
"reset_at_ms": "1779321600000",
"top1_user_id": "12345678",
"igniter_user_id": "12345678",
"rewards_json": "[{\"reward_role\":\"in_room\",\"user_id\":12345678,\"reward_item_id\":\"lv2_in_room_1\",\"resource_group_id\":1001,\"display_name\":\"Gold Pack\",\"icon_url\":\"https://cdn.example/reward/gold.png\",\"grant_id\":\"rgr_xxx\",\"status\":\"succeeded\"}]"
}
}
处理:
- 播放当前等级
opening_animation_url,再展示opened_image_url或奖励弹窗。 - 解析
rewards_json得到奖励列表;如果解析失败,调用 GET 获取state.last_rewards。 - 本地状态进入
next_level,进度归 0,状态归idle。
room_treasure_reward_granted
示例:
{
"event_id": "evt_reward",
"room_id": "lalu_xxx",
"event_type": "room_treasure_reward_granted",
"room_version": 8,
"attributes": {
"box_id": "room_treasure_box_xxx",
"level": "2",
"rewards_json": "[{\"reward_role\":\"top1\",\"user_id\":12345678,\"reward_item_id\":\"lv2_top1_1\",\"resource_group_id\":1002,\"display_name\":\"Top Reward\",\"icon_url\":\"https://cdn.example/reward/top.png\",\"grant_id\":\"rgr_xxx\",\"status\":\"succeeded\"}]"
}
}
处理:
- 解析
rewards_json。 - 当前登录用户在奖励列表中时,展示“获得奖励”提示。
- 同一用户可能同时命中
in_room、top1、igniter,是否叠加由reward_stack_policy决定;客户端按服务端发下来的列表展示。
当前没有单独的 C2C 宝箱中奖私信,也没有 App 消息 Tab 宝箱入箱消息。后续如新增私有通知,应以新的 C2C Ext 文档为准,不复用房间群消息假装私信。
区域/全局播报 IM
满能量进入倒计时后,如果后台配置允许播报,activity-service 会向区域或全局播报群发送 TIMCustomElem。
| 字段 | 值 |
|---|---|
MsgType |
TIMCustomElem |
MsgContent.Ext |
im_broadcast |
MsgContent.Desc |
room_treasure |
MsgContent.Data |
JSON 字符串 |
CloudCustomData |
与 MsgContent.Data 相同的 JSON 字符串 |
Data 示例:
{
"event_id": "room_treasure_broadcast:lalu_xxx:room_treasure_box_xxx",
"broadcast_type": "room_treasure",
"scope": "region",
"app_code": "lalu",
"region_id": 1001,
"room_id": "lalu_xxx",
"box_id": "room_treasure_box_xxx",
"level": 2,
"open_at_ms": 1779259512104,
"top1_user_id": 12345678,
"igniter_user_id": 12345678,
"sent_at_ms": 1779259502200,
"room_version": 6,
"source_event_id": "evt_countdown",
"current_progress": 200,
"energy_threshold": 200,
"countdown_started_at_ms": 1779259502104,
"action": {
"type": "enter_room",
"room_id": "lalu_xxx"
}
}
处理:
broadcast_type=room_treasure时展示宝箱倒计时播报。- 点击播报按
action.type=enter_room跳转或进房。 scope=region表示区域播报群;scope=global表示全局播报群。- 播报只负责引导和展示,不代替房间内宝箱状态;进房后仍调用 GET 初始化。
普通送礼房间 IM
送礼成功还会产生普通房间群消息:
| 字段 | 值 |
|---|---|
MsgContent.Ext |
room_system_message |
MsgContent.Desc |
room_gift_sent |
Data.event_type |
room_gift_sent |
宝箱 UI 不应该只靠 room_gift_sent 推算能量;能量以 room_treasure_progress_changed、room_treasure_countdown_started、POST /gift/send.data.treasure 或 GET 结果为准。
Flutter 解析建议
IM attributes 的值都是字符串,建议统一封装解析:
int readInt(dynamic value, [int fallback = 0]) {
if (value == null) return fallback;
if (value is num) return value.toInt();
if (value is String) return int.tryParse(value) ?? fallback;
return fallback;
}
String readString(dynamic value) {
if (value == null) return '';
if (value is String) return value;
if (value is num) return value.toInt().toString();
return value.toString();
}
List<Map<String, dynamic>> readRewardsJson(dynamic value) {
final raw = readString(value);
if (raw.isEmpty) return const [];
final decoded = jsonDecode(raw);
if (decoded is! List) return const [];
return decoded.whereType<Map>().map((item) {
return item.map((key, val) => MapEntry(key.toString(), val));
}).toList();
}
推荐本地状态更新策略:
| 输入 | 处理 |
|---|---|
GET /treasure |
覆盖本地完整宝箱状态和 7 级物料。 |
POST /gift/send.data.treasure |
送礼人立即刷新当前状态。 |
room_treasure_progress_changed |
同房间、版本不旧时更新进度。 |
room_treasure_countdown_started |
切换倒计时,并锁定 top1/点火人展示。 |
room_treasure_opened |
播放开箱动效,状态切到下一等级 idle。 |
room_treasure_reward_granted |
展示当前用户中奖提示;按 event_id 去重。 |
im_broadcast + room_treasure |
展示区域/全局播报入口,不直接改当前房间状态。 |
如果客户端发现本地状态和服务端事件无法合并,例如当前 box_id 不一致、level 倒退、room_version 缺失,直接调用 GET /api/v1/rooms/{room_id}/treasure 重新同步。