hyapp-server/docs/voice-room-client-api-flow.md
2026-05-06 21:22:39 +08:00

494 lines
14 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 R as room-service
participant IM as Tencent IM SDK
participant RTC as Tencent RTC SDK
C->>G: POST /api/v1/rooms/join
G->>R: JoinRoom(actor_user_id from token)
R-->>G: room snapshot + user presence
G-->>C: room snapshot
C->>G: GET /api/v1/im/usersig
G-->>C: IM user_sig
C->>IM: login(user_id, user_sig)
C->>IM: joinGroup(room_id)
C->>G: POST /api/v1/rtc/token
G->>R: VerifyRoomPresence(room_id, user_id)
G-->>C: RTC user_sig, str_room_id, role=audience
C->>RTC: enterRoom(strRoomId, audience)
loop while room page is active
C->>G: POST /api/v1/rooms/heartbeat
end
```
### Required Order
| Step | Interface | Required | Reason |
| --- | --- | --- | --- |
| 1 | `POST /api/v1/rooms/join` | yes | 建立业务 presence后续 IM/RTC 守卫依赖它 |
| 2 | `GET /api/v1/im/usersig` | yes | 登录腾讯云 IM接收房间群消息和系统事件 |
| 3 | Tencent IM SDK `login` + `joinGroup` | yes | 本仓库不承载房间长连接 |
| 4 | `POST /api/v1/rtc/token` | yes for voice room audio | 签发 TRTC 票据;当前返回 `role=audience` |
| 5 | Tencent RTC SDK `enterRoom` | yes for voice room audio | 进入真实音频频道旁听 |
| 6 | `POST /api/v1/rooms/heartbeat` | yes | 刷新业务 presence避免 stale timeout |
如果产品允许“只看公屏不听语音”,第 4、5 步可以延迟到用户打开音频时。但语音房默认应进入 RTC audience以保证打开房间即可听到声音。
## 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
},
"user": {
"user_id": 10001,
"role": "audience",
"joined_at_ms": 1778000000000,
"last_seen_at_ms": 1778000000000
},
"room": {}
}
```
Rules:
- `room_id` 必须来自房间列表、分享或恢复接口,不能由客户端拼业务含义。
- `command_id` 用于房间命令幂等;客户端重试同一次进房必须复用同一个值。
- `role` 只作为展示默认值,不能通过自报 `admin/host/owner` 获得权限。
- JoinRoom 成功后才能登录 IM 进群和获取 RTC token。
### 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
}
```
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(room_id)
```
### 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)
```
### 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/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 |
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_user_id": 10002,
"gift_id": "rose",
"gift_count": 1
}
```
Rules:
- `SendGift` 必须先由 wallet-service 扣费成功,再落房间表现。
- 房间表现、热度、本地榜通过 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。
- 房间命令响应里的 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 的用户不能读取完整在线用户和麦位快照。