hyapp-server/docs/flutter对接/语音房宝箱Flutter对接.md
2026-05-20 18:07:33 +08:00

21 KiB
Raw Permalink Blame History

语音房宝箱 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不按用户本地时区。

客户端流程

  1. 用户成功 JoinRoom 后调用 GET /api/v1/rooms/{room_id}/treasure 初始化 7 级物料和当前进度。
  2. 用户送礼调用 POST /api/v1/rooms/gift/send;成功响应里的 data.treasure 可立即更新当前宝箱状态。
  3. 房间内监听腾讯 IM 群 TIMCustomElem.Ext=room_system_message,处理宝箱进度、倒计时、开箱和发奖。
  4. App 全局或区域播报监听 TIMCustomElem.Ext=im_broadcastbroadcast_type=room_treasure
  5. 任意 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 noneregionglobal
open_delay_ms int64 满能量后到开箱的倒计时配置。
broadcast_delay_ms int64 后台配置字段;当前房间事件会立即进入播报链路。
reward_stack_policy string allow_stackpriority_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 当前返回 idlecountdown
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_roomtop1igniter
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 兼容 Stringnum

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_DENIEDNOT_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_idigniter_user_id 在此刻锁定;后续离房或下线也不影响这两类奖励发放。
  • 倒计时期间再收到普通 room_gift_sent,只更新礼物表现和热度,不改变宝箱倒计时。

注意:房间群 IM 字段名是 countdown_start_msHTTP 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_roomtop1igniter,是否叠加由 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_changedroom_treasure_countdown_startedPOST /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 重新同步。