24 KiB
Voice Room Client API Flow
本文定义用户进入语音房后的客户端接口调用顺序、触发条件和服务边界。本文只描述 App 侧要调用的 gateway HTTP 接口、腾讯云 IM/RTC SDK 动作,以及不能由客户端直接调用的内部守卫接口。
Scope
| Area | Owner | Rule |
|---|---|---|
| HTTP 入口 | gateway-service |
鉴权、profile gate、协议转换、腾讯 IM/RTC token 签发 |
| 房间业务状态 | room-service |
Room Cell、presence、麦位、房管、snapshot、outbox |
| 房间长连接和公屏 | 腾讯云 IM SDK | 客户端登录、房间群、公屏、系统消息接收 |
| 真实音频频道 | 腾讯云 RTC SDK | 进 RTC 房、切换 audience/anchor、发音频 |
| 用户主数据 | user-service |
登录、资料完成状态、用户区域、push token |
关键边界:
- 客户端进入语音房必须先
JoinRoom写入 room-service 业务 presence,再进 IM 群和 RTC 房。 room-service不保存客户端 socket,也不保存腾讯云 RTC 连接态。- 腾讯云 IM 群成员关系不是业务 presence;是否允许进群、发言、签 RTC token 仍以 room-service 守卫为准。
- gateway 只使用 access token 中的
user_id作为 actor,客户端不能自报actor_user_id、owner_user_id、host_user_id。 - 所有
/api/v1业务响应使用{code,message,request_id,data}envelope。
Minimal Enter Room Flow
sequenceDiagram
participant C as Client
participant G as gateway-service
participant U as user-service
participant R as room-service
participant IM as Tencent IM SDK
participant RTC as Tencent RTC SDK
C->>G: GET /api/v1/im/usersig
G-->>C: IM user_sig
C->>IM: login(user_id, user_sig)
C->>G: GET /api/v1/rooms/me
G->>R: GetMyRoom(owner_user_id from token)
R-->>G: owner room card from rooms + snapshot
G-->>C: has_room + room card + im_group_id
C->>G: GET /api/v1/rooms/feeds?tab=visited|friend|following
opt tab=friend|following
G->>U: ListFriends/ListFollowing(cursor)
U-->>G: related user ids + relation times
end
G->>R: ListRoomFeeds(viewer region + related users)
R-->>G: room cards
G-->>C: room cards + im_group_id
C->>G: POST /api/v1/rooms/join
G->>R: JoinRoom(actor_user_id from token)
R-->>G: room snapshot + user presence
G-->>C: room initial data + rtc token
par realtime bootstrap
C->>IM: joinGroup(im_group_id)
and audio bootstrap
C->>RTC: enterRoom(strRoomId from join.rtc.token, audience)
end
C->>G: GET /api/v1/rooms/snapshot
G-->>C: optional active-room resync
C->>G: GET /api/v1/rooms/{room_id}/detail
G-->>C: render-ready room detail package
loop while room page is active
C->>G: POST /api/v1/rooms/heartbeat
end
Required Order
| Step | Interface | Required | Reason |
|---|---|---|---|
| 1 | GET /api/v1/im/usersig |
yes | 用户级 IM 登录票据,可在 App 启动后预取并按 TTL 复用 |
| 2 | Tencent IM SDK login |
yes | 客户端先建立 IM 身份,房间群仍等 JoinRoom 成功后再进 |
| 3 | GET /api/v1/rooms/me |
yes for Mine top card | 直接读取 owner 房间权威元数据,不依赖发现页投影 |
| 4 | GET /api/v1/rooms/feeds?tab=visited|friend|following |
yes for Mine lists | Mine 列表 tab;visited 由 JoinRoom 成功写入,friend/following 由 gateway 先查 user-service 关系,再让 room-service 返回这些关系用户的 active 房间卡片 |
| 5 | POST /api/v1/rooms/join |
yes | 建立业务 presence,并返回首屏房间数据和内嵌 RTC token |
| 6 | Tencent IM SDK joinGroup(im_group_id) |
yes | 必须在 JoinRoom 成功后执行,IM 回调守卫会校验 room-service presence |
| 7 | Tencent RTC SDK enterRoom |
yes for voice room audio | 使用 JoinRoom 响应里的 rtc.token 进入真实音频频道旁听 |
| 8 | POST /api/v1/rooms/heartbeat |
yes | 刷新业务 presence,避免 stale timeout |
| 9 | GET /api/v1/rooms/{room_id}/detail |
optional after join/restore | 详情页聚合包;用于 JoinRoom 后补偿刷新、回前台、设计稿首屏重绘 |
| 10 | GET /api/v1/rooms/{room_id}/gift-panel |
lazy on gift button | 打开礼物面板时再拉,不放进房间首屏 |
| 11 | GET /api/v1/rooms/{room_id}/online-users |
lazy on online count | 点击右上角在线人数时分页拉取 |
如果 JoinRoom 响应里的 rtc.available=false,客户端可以先展示房间页并重试 POST /api/v1/rtc/token;RTC 失败不能回滚已经成功的业务 presence。
Mine Feed Cache Boundary
当前 friend/following 不强制加 Redis。原因是关注、取关、同意好友和删除好友都要求列表立即反映关系事实;首版直接读 user-service MySQL 关系表,再查 room-service active 房间卡片,正确性更直接。
后续可以加 Redis 的位置:
hot/new公共列表:room-service 用 Redis ZSET 缓存排序 room_id,miss 后回源room_list_entries。- 房间卡片短 TTL 缓存:key 可按
app_code + region_id + room_id,但 MySQLroom_list_entries仍是事实来源。 friend/following关系 ID 短 TTL 缓存:只能放在 gateway 或 user-service 读优化层,follow/unfollow/accept/delete成功后必须主动失效。visitedroom_id 列表缓存:可以由 JoinRoom 成功后刷新,Redis 写失败不能影响 JoinRoom。
不建议首版缓存 friend/following 完整房间卡片列表。这个列表同时依赖 user-service 关系事实、room-service 房间 active 状态、区域过滤和搜索条件,失效面较大;先保持同步查询,等 QPS 或延迟数据证明需要缓存再加。
Core Interfaces
Join Room
POST /api/v1/rooms/join
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_join_<room_id>_<user_id>_<nonce>",
"role": "audience"
}
Response data:
{
"result": {
"applied": true,
"room_version": 2,
"server_time_ms": 1778000000000
},
"room": {
"room_id": "lalu_xxx",
"im_group_id": "lalu_xxx",
"room_short_id": "100001",
"title": "Live Room",
"cover_url": "https://cdn.example/room.png",
"description": "welcome",
"owner_user_id": "10001",
"host_user_id": "10001",
"mode": "voice",
"status": "active",
"chat_enabled": true,
"heat": 1000,
"online_count": 26,
"seat_count": 15,
"occupied_seat_count": 2,
"version": 2
},
"viewer": {
"user_id": "10001",
"role": "audience",
"joined_at_ms": 1778000000000,
"last_seen_at_ms": 1778000000000
},
"seats": [
{
"seat_no": 1,
"user_id": "10002",
"locked": false,
"publish_state": "publishing",
"mic_session_id": "mic_xxx"
}
],
"contribution_rank": [
{
"user_id": "10002",
"score": 100,
"gift_value": 100
}
],
"profiles": [
{
"user_id": "10002",
"display_user_id": "200002",
"username": "host",
"avatar": "https://cdn.example/avatar.png"
}
],
"im": {
"group_id": "lalu_xxx",
"need_join_group": true
},
"rtc": {
"need_token": true,
"available": true,
"token": {
"sdk_app_id": 20036101,
"user_id": "10001",
"rtc_user_id": "10001",
"user_sig": "xxx",
"expire_at_ms": 1778007200000,
"room_id": "lalu_xxx",
"rtc_room_id": "lalu_xxx",
"rtc_room_id_type": "string",
"str_room_id": "lalu_xxx",
"app_scene": "voice_chat_room",
"role": "audience"
}
},
"server_time_ms": 1778000000000
}
Rules:
room_id必须来自房间列表、分享或恢复接口,不能由客户端拼业务含义。command_id用于房间命令幂等;客户端重试同一次进房必须复用同一个值。role只作为展示默认值,不能通过自报admin/host/owner获得权限。im.group_id当前等于room_id,但客户端必须使用服务端返回值,不能硬编码映射规则。- JoinRoom 成功后才能调用腾讯 IM SDK
joinGroup;列表里的im_group_id只用于准备,不代表入群授权。 - JoinRoom 会尝试内嵌签发 RTC token;签发失败时返回
rtc.available=false,客户端可以继续展示房间并单独重试/api/v1/rtc/token。
Room Detail
GET /api/v1/rooms/{room_id}/detail
Authorization: Bearer <access_token>
Response data:
{
"room": {},
"viewer": {},
"seats": [],
"rank_top": [],
"online_count": 30,
"permissions": {},
"profiles": [],
"im": {},
"rtc": {}
}
Rules:
detail只读 Room Cell/snapshot,不刷新 heartbeat,不隐式进房。- viewer 必须仍在 room-service presence 内;未进房、被踢或被 ban 返回
PERMISSION_DENIED。 permissions.can_close_room表示设计稿右上角电源按钮可用,但当前语义是“退出房间”,不是关闭房间生命周期。profiles只返回首屏需要的 owner、host、viewer、麦位用户和榜单 top 用户。完整在线用户资料走在线用户分页或users/room-display-profiles:batch。
Online Users
GET /api/v1/rooms/{room_id}/online-users?page=1&page_size=50
Authorization: Bearer <access_token>
Response data:
{
"items": [
{
"user_id": "10002",
"role": "audience",
"last_seen_at_ms": 1778000000000,
"profile": {}
}
],
"total": 30,
"page": 1,
"page_size": 50,
"server_time_ms": 1778000000000
}
Rules:
- 该接口读
room_user_presence分页投影,不拉完整RoomSnapshot做列表分页。 page_size默认 50,服务端最大 100。- 用户展示资料失败不影响在线列表主数据,客户端可用
GET /api/v1/users/room-display-profiles:batch补偿。
Gift Panel
GET /api/v1/rooms/{room_id}/gift-panel
Authorization: Bearer <access_token>
Response data:
{
"coin_balance": 1123123,
"recipients": [],
"tabs": [],
"gifts": [],
"quantity_presets": [1, 9, 99, 999]
}
Rules:
- 打开礼物面板时再请求,不放进
rooms/detail,避免房间首屏每次都查钱包和礼物大列表。 gifts来自 wallet-service 礼物配置,按房间visible_region_id过滤。recipients首版包含麦上用户;超过一个麦上用户时附带target_type=all_mic占位。coin_balance只查COIN余额;余额不足时送礼接口仍以 wallet-service 扣费结果为准。
Room Display Profiles
GET /api/v1/users/room-display-profiles:batch?user_ids=10001,10002
Authorization: Bearer <access_token>
Response data:
{
"profiles": [
{
"user_id": "10002",
"username": "Jack",
"avatar": "https://cdn.example/avatar.png",
"display_user_id": "123456",
"vip": {},
"level": {},
"badges": [],
"avatar_frame": {},
"vehicle": {},
"charm": 10
}
]
}
Rules:
- 该接口专门服务房间麦位、公屏和礼物动效展示;不要复用“我的页”或用户主页重接口。
- 当前已接入用户基础资料,
vip/level/badges/avatar_frame/vehicle/charm保持稳定字段,后续接入对应 read model 后填充。
IM UserSig
GET /api/v1/im/usersig
Authorization: Bearer <access_token>
Response data:
{
"sdk_app_id": 20036101,
"user_id": "10001",
"user_sig": "xxx",
"expire_at_ms": 1778086400000,
"join_groups": [
{
"group_id": "hy_lalu_bc_g",
"type": "global_broadcast"
},
{
"group_id": "hy_lalu_bc_r_1001",
"type": "region_broadcast",
"region_id": 1001
}
]
}
Rules:
- 当前 gateway 路由是
GET /api/v1/im/usersig。 user_id是内部不可变长user_id的十进制字符串,必须直接作为腾讯云 IM identifier。- UserSig 只用于腾讯云 IM SDK 登录,不用于本仓库鉴权。
- 未完成资料用户会被 profile gate 拒绝。
Client SDK action:
Tencent IM SDK login(user_id, user_sig)
Tencent IM SDK joinGroup(join_response.im.group_id)
RTC Token
POST /api/v1/rtc/token 是 JoinRoom 内嵌签发失败、token 过期或 RTC 重连时的补偿入口;首次进房应优先使用 JoinRoom 响应里的 rtc.token。
POST /api/v1/rtc/token
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx"
}
Response data:
{
"sdk_app_id": 20036101,
"user_id": "10001",
"rtc_user_id": "10001",
"user_sig": "xxx",
"expire_at_ms": 1778007200000,
"room_id": "lalu_xxx",
"rtc_room_id": "lalu_xxx",
"rtc_room_id_type": "string",
"str_room_id": "lalu_xxx",
"app_scene": "voice_chat_room",
"role": "audience"
}
Rules:
- gateway 会先调用 room-service
VerifyRoomPresence,用户不在房间、被踢、被 ban 时不会签发 RTC token。 - 当前只支持 TRTC
strRoomId,客户端不能把room_id转为数字roomId。 - 当前 token 返回
role=audience;上麦成功后由客户端切anchor并确认发流。 - token 过期时重新调用该接口,不使用 refresh token。
Client SDK action:
Tencent RTC SDK enterRoom(strRoomId=resp.str_room_id, userId=resp.rtc_user_id, role=audience)
Update Room Profile
房主在房间内修改房间资料和座位数时调用:
POST /api/v1/rooms/profile/update
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_profile_<room_id>_<user_id>_<nonce>",
"room_name": "Live Room",
"room_avatar": "https://cdn.example/room.png",
"room_description": "welcome",
"seat_count": 20
}
Rules:
- 只有当前在房间内的 owner 可以执行。
room_name、room_avatar、room_description、seat_count都是可选字段;客户端只传本次要修改的字段。seat_count只能传后台已启用的10/15/20/25/30子集,当前默认配置全部启用。- 创建房间可以省略
seat_count使用后台默认值;修改资料时不能传0。 - 缩小座位数时,被删除的高编号麦位必须全部空闲且未锁,否则返回
conflict,客户端需要先处理麦位再重试。 - 成功响应返回最新
room和seats,客户端直接刷新房间标题、头像、简介和麦位列表。 - 成功后房间系统消息事件名为
room_profile_updated。
Heartbeat
POST /api/v1/rooms/heartbeat
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_heartbeat_<room_id>_<user_id>_<tick>"
}
Rules:
- 只刷新已经存在的业务 presence,不能替代 JoinRoom。
- 用户已经离房、被踢或房间关闭时,客户端应停止心跳并清理本地房间态。
- 心跳间隔由客户端配置控制,但必须小于 room-service 的 presence stale 窗口。
Leave Room
POST /api/v1/rooms/leave
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_leave_<room_id>_<user_id>_<nonce>"
}
Rules:
- 主动关闭房间页、切换账号、被客户端确认退出时调用。
- 如果用户在麦上,LeaveRoom 会释放麦位。
- LeaveRoom 后客户端必须停止 heartbeat、退出 RTC 房、退出或忽略 IM 房间群消息。
Mic Flow
Mic Up
POST /api/v1/rooms/mic/up
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_mic_up_<room_id>_<user_id>_<nonce>",
"seat_no": 1
}
Response data:
{
"result": {},
"seat_no": 1,
"mic_session_id": "mic_xxx",
"publish_deadline_ms": 1778000015000,
"room": {}
}
Client actions after success:
- 保存
mic_session_id、room.version和publish_deadline_ms。 - 确保已有 RTC token;没有或过期则调用
POST /api/v1/rtc/token。 - RTC SDK 切换
anchor。 - RTC SDK 调
startLocalAudio。 - SDK 本地发流成功后调用
POST /api/v1/rooms/mic/publishing/confirm。
Rules:
MicUp成功只代表业务麦位占用成功,不代表 RTC 已经发布音频成功。- 服务端状态先进入
pending_publish。 - 超过
publish_deadline_ms未确认,room-service 会自动 MicDown,reason 为publish_timeout。
Confirm Mic Publishing
POST /api/v1/rooms/mic/publishing/confirm
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_confirm_publish_<room_id>_<user_id>_<nonce>",
"target_user_id": 0,
"mic_session_id": "mic_xxx",
"room_version": 12,
"event_time_ms": 1778000001000,
"source": "client"
}
Rules:
target_user_id=0表示确认当前 actor;RTC webhook 路径才需要显式 target。mic_session_id必须等于当前麦位会话。room_version和event_time_ms用于丢弃旧事件,避免旧 audience/leave 事件清理新会话。- 成功后麦位
publish_state=publishing。
Mic Down
POST /api/v1/rooms/mic/down
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_mic_down_<room_id>_<user_id>_<nonce>",
"target_user_id": 0,
"reason": "user_action"
}
Rules:
- 普通用户下自己麦时
target_user_id可传 0 或自己 user_id。 - 房管下别人麦时传目标用户
target_user_id。 - 客户端收到成功响应或房间系统消息后必须停止本地音频并切回
audience。
Change Mic Seat
POST /api/v1/rooms/mic/change
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_change_mic_<room_id>_<actor_user_id>_<nonce>",
"target_user_id": 10002,
"seat_no": 2
}
Rules:
- 换麦位只更新业务麦位和 UI,不需要重新进入 RTC。
- 如果 target 当前在发流,客户端继续保持 RTC 上行。
Room Management APIs
这些接口只允许有权限的 owner/host/admin 调用;gateway 不判断权限,最终由 room-service Room Cell 判断。
| Interface | Body | Result |
|---|---|---|
POST /api/v1/rooms/mic/lock |
room_id, command_id, seat_no, locked |
锁/解锁麦位 |
POST /api/v1/rooms/mic/mute |
room_id, command_id, target_user_id, muted |
修改麦克风静音状态;该状态会同步给房间其他用户 |
POST /api/v1/rooms/chat/enabled |
room_id, command_id, enabled |
开/关公屏,影响发言守卫 |
POST /api/v1/rooms/admin/set |
room_id, command_id, target_user_id, is_admin |
设置或取消房管 |
POST /api/v1/rooms/host/transfer |
room_id, command_id, target_user_id |
转移主持人 |
POST /api/v1/rooms/user/mute |
room_id, command_id, target_user_id, muted, reason |
禁言或解禁 |
POST /api/v1/rooms/user/kick |
room_id, command_id, target_user_id, reason |
踢人并 ban,释放 presence 和麦位 |
POST /api/v1/rooms/user/unban |
room_id, command_id, target_user_id, reason |
解除房间 ban |
POST /api/v1/rooms/close |
room_id, command_id |
设计稿电源按钮:当前用户退出房间,不关闭房间 |
Management event handling:
- 管理动作成功后,room-service 写 command log、snapshot 和 outbox。
- 客户端应通过腾讯云 IM 房间系统消息更新 UI。
- 如果本地 UI 状态和系统消息冲突,以最新 room snapshot 和
room_version为准。
Gift And Interaction
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
{
"room_id": "lalu_xxx",
"command_id": "cmd_send_gift_<room_id>_<user_id>_<nonce>",
"target_type": "user",
"target_user_ids": [10002],
"gift_id": "rose",
"gift_count": 1
}
Rules:
SendGift必须先由 wallet-service 扣费成功,再落房间表现。- 旧客户端的
target_user_id仍兼容;新客户端统一传target_type + target_user_ids。 - 当前已支持
target_type=user;all_mic/all_room/couple先保留协议字段,等账务拆单和房间表现策略补齐后开放。 - 房间表现、热度、本地榜通过 room snapshot 和房间系统消息同步。
- 公屏文本消息不走 gateway HTTP;客户端使用腾讯云 IM 群消息。
Restore And Reconnect
Current Room
GET /api/v1/rooms/current
Authorization: Bearer <access_token>
Response data:
{
"has_current_room": true,
"room_id": "lalu_xxx",
"room_version": 12,
"role": "audience",
"mic_session_id": "",
"publish_state": "",
"need_join_im_group": true,
"need_rtc_token": false,
"server_time_ms": 1778000000000
}
Client restore rules:
| Response | Client Action |
|---|---|
has_current_room=false |
清理本地房间态,不展示恢复入口 |
need_join_im_group=true |
调 GET /api/v1/im/usersig,重新登录 IM 并 join group |
need_rtc_token=true |
调 POST /api/v1/rtc/token,重新进入 RTC |
publish_state=pending_publish |
重新执行 SDK 发流并确认当前 mic_session_id |
publish_state=publishing |
确认本地 RTC 上行是否仍存在;不确定时重新发流并确认 |
rooms/current 是只读恢复探测,不会隐式 JoinRoom,不刷新 heartbeat,也不改变 Room Cell。
Reconnect Order
- access token 有效时先调
GET /api/v1/rooms/current。 - 如果
has_current_room=false,回房间列表或主界面。 - 如果
has_current_room=true,先恢复 IM,再恢复 RTC。 - 如果用户原本在麦上,必须使用返回的
mic_session_id继续确认或等待服务端超时下麦。 - 恢复成功后恢复
POST /api/v1/rooms/heartbeat定时器。
Server Callback Boundaries
客户端不要直接调用这些接口或内部 RPC:
| Interface | Caller | Purpose |
|---|---|---|
POST /api/v1/tencent-im/callback |
Tencent IM | 入群、发言等服务端回调入口 |
POST /api/v1/tencent-rtc/callback |
Tencent RTC | RTC 音频事件回调入口 |
room-service.VerifyRoomPresence |
gateway / callback | 判断用户是否仍在房间、是否被 ban |
room-service.CheckSpeakPermission |
gateway / callback | 判断用户是否允许发公屏 |
客户端只消费这些回调产生的房间系统消息和最新 snapshot,不直接操作内部守卫。
Error Handling
| Error | Meaning | Client Action |
|---|---|---|
UNAUTHORIZED / AUTH_REQUIRED |
access token 无效 | refresh 成功后重试;refresh 失败回登录 |
PROFILE_REQUIRED |
资料未完成 | 跳资料补全页,不能进入房间 |
NOT_FOUND |
房间或用户事实不存在 | 清本地目标房间,回列表 |
ROOM_CLOSED |
房间已关闭 | 清房间态,回列表 |
PERMISSION_DENIED |
被踢、被 ban、无权限 | 停止对应操作,按系统消息更新 UI |
CONFLICT |
麦位占用、命令冲突、状态冲突 | 拉最新 snapshot 或等待房间系统消息修正 |
UPSTREAM_ERROR |
内部服务或腾讯云依赖异常 | 保留本地登录态,展示重试 |
不要因为普通网络失败或 5xx 清空登录态。只有 refresh 明确失败、session revoked 或用户主动 logout 才清 token。
Room Snapshot Sync
当前客户端拿完整房间状态的来源:
JoinRoom响应里的 room snapshot。GET /api/v1/rooms/{room_id}/detail的首屏聚合包。- 房间命令响应里的 room snapshot。
- 腾讯云 IM 房间系统消息。
rooms/current的恢复摘要。GET /api/v1/rooms/snapshot?room_id=<room_id>主动同步接口。
GET /api/v1/rooms/snapshot?room_id=<room_id>
该接口只读 Room Cell/snapshot,不刷新 presence,不隐式 JoinRoom,不替代 heartbeat。gateway 使用 access token user_id 作为 viewer,room-service 要求 viewer 仍在该房间;未进房、被踢或被 ban 的用户不能读取完整在线用户和麦位快照。