hyapp-server/docs/语音房客户端接口流程.md
2026-05-12 20:07:49 +08:00

771 lines
24 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.

# 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
```mermaid
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_idmiss 后回源 `room_list_entries`
- 房间卡片短 TTL 缓存key 可按 `app_code + region_id + room_id`,但 MySQL `room_list_entries` 仍是事实来源。
- `friend/following` 关系 ID 短 TTL 缓存:只能放在 gateway 或 user-service 读优化层,`follow/unfollow/accept/delete` 成功后必须主动失效。
- `visited` room_id 列表缓存:可以由 JoinRoom 成功后刷新Redis 写失败不能影响 JoinRoom。
不建议首版缓存 `friend/following` 完整房间卡片列表。这个列表同时依赖 user-service 关系事实、room-service 房间 active 状态、区域过滤和搜索条件,失效面较大;先保持同步查询,等 QPS 或延迟数据证明需要缓存再加。
## Core Interfaces
### Join Room
```http
POST /api/v1/rooms/join
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_join_<room_id>_<user_id>_<nonce>",
"role": "audience"
}
```
Response `data`:
```json
{
"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
```http
GET /api/v1/rooms/{room_id}/detail
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"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
```http
GET /api/v1/rooms/{room_id}/online-users?page=1&page_size=50
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"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
```http
GET /api/v1/rooms/{room_id}/gift-panel
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"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
```http
GET /api/v1/users/room-display-profiles:batch?user_ids=10001,10002
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"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
```http
GET /api/v1/im/usersig
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"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:
```text
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`
```http
POST /api/v1/rtc/token
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx"
}
```
Response `data`:
```json
{
"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:
```text
Tencent RTC SDK enterRoom(strRoomId=resp.str_room_id, userId=resp.rtc_user_id, role=audience)
```
### Update Room Profile
房主在房间内修改房间资料和座位数时调用:
```http
POST /api/v1/rooms/profile/update
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"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
```http
POST /api/v1/rooms/heartbeat
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_heartbeat_<room_id>_<user_id>_<tick>"
}
```
Rules:
- 只刷新已经存在的业务 presence不能替代 JoinRoom。
- 用户已经离房、被踢或房间关闭时,客户端应停止心跳并清理本地房间态。
- 心跳间隔由客户端配置控制,但必须小于 room-service 的 presence stale 窗口。
### Leave Room
```http
POST /api/v1/rooms/leave
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_leave_<room_id>_<user_id>_<nonce>"
}
```
Rules:
- 主动关闭房间页、切换账号、被客户端确认退出时调用。
- 如果用户在麦上LeaveRoom 会释放麦位。
- LeaveRoom 后客户端必须停止 heartbeat、退出 RTC 房、退出或忽略 IM 房间群消息。
## Mic Flow
### Mic Up
```http
POST /api/v1/rooms/mic/up
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_mic_up_<room_id>_<user_id>_<nonce>",
"seat_no": 1
}
```
Response `data`:
```json
{
"result": {},
"seat_no": 1,
"mic_session_id": "mic_xxx",
"publish_deadline_ms": 1778000015000,
"room": {}
}
```
Client actions after success:
1. 保存 `mic_session_id``room.version``publish_deadline_ms`
2. 确保已有 RTC token没有或过期则调用 `POST /api/v1/rtc/token`
3. RTC SDK 切换 `anchor`
4. RTC SDK 调 `startLocalAudio`
5. SDK 本地发流成功后调用 `POST /api/v1/rooms/mic/publishing/confirm`
Rules:
- `MicUp` 成功只代表业务麦位占用成功,不代表 RTC 已经发布音频成功。
- 服务端状态先进入 `pending_publish`
- 超过 `publish_deadline_ms` 未确认room-service 会自动 MicDownreason 为 `publish_timeout`
### Confirm Mic Publishing
```http
POST /api/v1/rooms/mic/publishing/confirm
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"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` 表示确认当前 actorRTC webhook 路径才需要显式 target。
- `mic_session_id` 必须等于当前麦位会话。
- `room_version``event_time_ms` 用于丢弃旧事件,避免旧 audience/leave 事件清理新会话。
- 成功后麦位 `publish_state=publishing`
### Mic Down
```http
POST /api/v1/rooms/mic/down
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"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
```http
POST /api/v1/rooms/mic/change
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"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
```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"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
```http
GET /api/v1/rooms/current
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"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
1. access token 有效时先调 `GET /api/v1/rooms/current`
2. 如果 `has_current_room=false`,回房间列表或主界面。
3. 如果 `has_current_room=true`,先恢复 IM再恢复 RTC。
4. 如果用户原本在麦上,必须使用返回的 `mic_session_id` 继续确认或等待服务端超时下麦。
5. 恢复成功后恢复 `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>` 主动同步接口。
```http
GET /api/v1/rooms/snapshot?room_id=<room_id>
```
该接口只读 Room Cell/snapshot不刷新 presence不隐式 JoinRoom不替代 heartbeat。gateway 使用 access token user_id 作为 viewerroom-service 要求 viewer 仍在该房间;未进房、被踢或被 ban 的用户不能读取完整在线用户和麦位快照。