hyapp-server/docs/语音房客户端接口流程.md
2026-07-04 01:53:10 +08:00

922 lines
29 KiB
Markdown
Raw Permalink 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|followed
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 + is_followed
opt user toggles room follow
C->>G: POST|DELETE /api/v1/rooms/{room_id}/follow
G->>R: FollowRoom|UnfollowRoom(user_id from token)
R-->>G: current follow state
G-->>C: is_followed
end
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\|followed` | yes for Mine lists | Mine 列表 tab`visited` 由 JoinRoom 成功写入,`friend/following` 由 gateway 先查 user-service 关系,`followed` 直接读取 room-service 的房间关注关系 |
| 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 | 详情页聚合包;返回 `is_followed`,用于关注按钮初始状态 |
| 10 | `POST /api/v1/rooms/{room_id}/follow` / `DELETE /api/v1/rooms/{room_id}/follow` | user action | 建立或取消当前用户对房间的关注关系gateway 只使用 token user_id |
| 11 | `GET /api/v1/rooms/{room_id}/gift-panel` | lazy on gift button | 打开礼物面板时再拉,不放进房间首屏 |
| 12 | `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/followed` 不强制加 Redis。原因是关注、取关、同意好友和删除好友都要求列表立即反映关系事实`friend/following` 直接读 user-service MySQL 关系表,`followed` 直接读 room-service 的 `room_follows`,再查 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` 成功后必须主动失效。
- `followed` 房间 ID 短 TTL 缓存:只能放在 room-service 读优化层,`FollowRoom/UnfollowRoom` 成功后必须主动失效。
- `visited` room_id 列表缓存:可以由 JoinRoom 成功后刷新Redis 写失败不能影响 JoinRoom。
不建议首版缓存 `friend/following/followed` 完整房间卡片列表。这个列表同时依赖关系事实、room-service 房间 active 状态、区域过滤和搜索条件,失效面较大;先保持同步查询,等 QPS 或延迟数据证明需要缓存再加。
## Core Interfaces
### Room List Cards
```http
GET /api/v1/rooms?tab=hot&query=100001
GET /api/v1/rooms/feeds?tab=followed
Authorization: Bearer <access_token>
```
Rules:
- 公共发现列表、房间搜索和 Mine 页房间流都返回同一套房间卡片字段。
- `locked=true` 表示 JoinRoom 需要 `password`;列表和搜索只返回锁房标识,不返回明文或哈希。
- `im_group_id` 只代表腾讯 IM 目标群,不能绕过 JoinRoom 的锁房、ban 和 presence 校验。
### 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",
"password": "1234"
}
```
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,
"locked": 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` 获得权限。
- 带锁房间的新进入用户必须传正确 `password`;密码错误返回 403不创建 presence不签发 RTC token。
- 房主不需要密码进入自己的房间;已在房内的用户不会因为房主重新设置密码被挤出。
- `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,
"is_followed": true,
"permissions": {},
"profiles": [],
"im": {},
"rtc": {}
}
```
Rules:
- `detail` 只读 Room Cell/snapshot不刷新 heartbeat不隐式进房。
- viewer 必须仍在 room-service presence 内;未进房、被踢或被 ban 返回 `PERMISSION_DENIED`
- `is_followed` 来自 room-service 的 `room_follows` 关系表,只表达当前 token 用户是否关注该房间,不写入 Room Cell 快照。
- `permissions.can_close_room` 表示设计稿右上角电源按钮可用,但当前语义是“退出房间”,不是关闭房间生命周期。
- `profiles` 只返回首屏需要的 owner、host、viewer、麦位用户和榜单 top 用户。完整在线用户资料走在线用户分页或 `users/room-display-profiles:batch`
### Follow Room
```http
POST /api/v1/rooms/{room_id}/follow
DELETE /api/v1/rooms/{room_id}/follow
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"room_id": "lalu_xxx",
"is_followed": true,
"followed_at_ms": 1700000000123,
"server_time_ms": 1700000000123
}
```
Rules:
- POST 重复关注保持幂等;已 active 的关注不会刷新 `followed_at_ms`,避免列表排序被重复点击打乱。
- DELETE 未关注或历史关系已取消时按幂等成功返回 `is_followed=false`
- 关注列表走 `GET /api/v1/rooms/feeds?tab=followed`,只返回当前仍 active 且命中 viewer 区域桶的房间卡片。
### 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": {},
"profile_card": {
"resource_id": 8104,
"animation_url": "https://cdn.example/profile-card.mp4",
"metadata_json": "{\"profile_card_layout\":{\"source_height\":1680,\"content_top\":143,\"content_height\":1525}}"
},
"vehicle": {},
"charm": 10
}
]
}
```
Rules:
- 该接口专门服务房间麦位、公屏和礼物动效展示;不要复用“我的页”或用户主页重接口。
- 当前已接入用户基础资料,`vip/level/badges/avatar_frame/profile_card/vehicle/charm` 保持稳定字段;资料卡高度从 `profile_card.metadata_json.profile_card_layout` 解析。
### 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_v2",
"type": "global_broadcast"
},
{
"group_id": "hy_lalu_bc_r_v2_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`
### Room Backgrounds
房间背景图是房间维度素材;保存素材不立即改变当前房间背景,设置背景会写入 Room Cell 快照,`GET /api/v1/rooms/{room_id}/detail``data.room.background_url` 返回当前生效背景。
```http
POST /api/v1/rooms/backgrounds/save
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx",
"image_url": "https://cdn.example/room-bg.png"
}
```
```http
GET /api/v1/rooms/{room_id}/backgrounds
Authorization: Bearer <access_token>
```
Response `data.backgrounds[]` 返回 `background_id``image_url`、创建人和时间;`data.selected_background_url` 是当前生效背景。
```http
POST /api/v1/rooms/backgrounds/set
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_background_<room_id>_<user_id>_<nonce>",
"background_id": "101"
}
```
Rules:
- 只有房主可以保存、查询和设置自己房间的背景图。
- `image_url` 应先通过上传接口拿到对象存储 URL再传给保存接口。
- 设置成功后返回最新 `room.background_url`,并通过 `room_background_changed` 房间系统消息同步;消息 `attributes` 包含 `background_id``room_background_url`
### 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 Heartbeat
地址:
```http
POST /api/v1/rooms/mic/heartbeat
Authorization: Bearer <access_token>
Content-Type: application/json
```
参数:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_mic_heartbeat_<room_id>_<user_id>_<nonce>",
"target_user_id": 0,
"mic_session_id": "mic_xxx"
}
```
返回值:
```json
{
"result": {},
"seat_no": 1,
"mic_heartbeat_at_ms": 1778000005000,
"room": {}
}
```
相关 IM
- 无独立 IM。该接口只刷新当前 `publishing` 麦位会话的服务端心跳时间和房间 presence。
- `target_user_id=0` 表示当前登录用户自己。
- `mic_session_id` 必须等于当前麦位会话。
- `pending_publish` 不能调用该接口开始计时,必须先调用 `POST /api/v1/rooms/mic/publishing/confirm`
- 重试同一次心跳复用同一个 `command_id`;下一次心跳必须换新的 `command_id`
### 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/password` | `room_id, command_id, locked, password` | 房主设置或清空入房密码;成功后房间快照、列表和搜索的 `locked` 同步刷新 |
| `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。
- 踢人成功后 room-service 会移除业务 presence、释放麦位、写入房间 ban并通过 room-service 房间 IM bridge 补偿移出腾讯 IM 房间群。
- 被踢本人私有通知由 notice-service 监听 `room_outbox.RoomUserKicked` 后发送 C2C `room_notice`notice-service 不负责建群、踢群成员或房间群系统消息。
- 踢人成功后 room-service 会尽力调用 TRTC `RemoveUserByStrRoomId` 移出 RTC 房间;失败不回滚踢人事实,客户端仍必须按 IM/RTC 回调和后续 403 兜底清理。
- 客户端应通过腾讯云 IM 房间系统消息、notice-service C2C 私有通知、IM 群成员移除回调和 RTC 退房回调更新 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 的用户不能读取完整在线用户和麦位快照。