606 lines
25 KiB
Markdown
606 lines
25 KiB
Markdown
# Tencent RTC Token And UID Mapping Development Guide
|
||
|
||
本文档定义 Phase 2 “腾讯 RTC 语音接入”中,`gateway-service` 如何签发腾讯 RTC 进房凭证,以及系统内部 `user_id`、`room_id` 如何映射到腾讯 RTC SDK 参数。目标是让用户在 `JoinRoom` 成功后可以进入真实语音频道,同时不把 RTC 在线态塞进 `room-service`。
|
||
|
||
## Scope
|
||
|
||
本阶段只做:
|
||
|
||
| Capability | Owner | Required Result |
|
||
| --- | --- | --- |
|
||
| RTC UserSig 签发 | `gateway-service` | 已鉴权且已进房用户可以拿到腾讯 RTC 所需 `SDKAppID/UserID/UserSig` |
|
||
| UID 映射 | `gateway-service` | 内部不可变 `user_id` 稳定映射为 RTC `UserID` 字符串 |
|
||
| 房间 ID 映射 | `gateway-service` + `room-service` | 内部 `room_id` 稳定映射为 RTC `strRoomId` |
|
||
| 进房权限 | `gateway-service` + `room-service` | 只有 room-service presence 中的用户才能拿 RTC token |
|
||
| 客户端进频道 | Client | 客户端使用返回参数调用腾讯 RTC SDK 进入语音房 |
|
||
|
||
本阶段不做:
|
||
|
||
| Not Included | Reason |
|
||
| --- | --- |
|
||
| 自研 RTC 网关 | 音频链路由腾讯 RTC SDK 和腾讯云承载 |
|
||
| RTC 在线态持久化 | RTC 连接态属于腾讯 RTC,不进入 Room Cell 核心状态 |
|
||
| 多端同时进同一 RTC 房 | 腾讯 RTC 对同一 `UserID` 同房多端有冲突约束,首版只支持单用户单端进同一 RTC 房 |
|
||
| 音频流审核/旁路直播 | 需要独立产品和合规设计 |
|
||
| 私有进房权限 `privateMapKey` | 首版不启用房间级腾讯侧权限;只适用于普通开放语音房,私密、付费或强权限房必须先补 `privateMapKey` |
|
||
|
||
## Official Constraints
|
||
|
||
需要遵守的腾讯 RTC 规则:
|
||
|
||
| Rule | Design Impact |
|
||
| --- | --- |
|
||
| UserSig 是腾讯 RTC/Chat 的用户鉴权凭证,生产环境应由业务服务端实时生成,不能把 SecretKey 放在客户端 | `gateway-service` 签发,客户端永远拿不到 `secret_key` |
|
||
| RTC SDK 进房需要 `SDKAppID + UserID + UserSig` | HTTP 响应必须返回这三项 |
|
||
| `UserID` 允许字母、数字、下划线、连字符等有限字符集合 | 内部 int64 `user_id` 转十进制字符串,天然满足限制 |
|
||
| 同一 `UserID` 不能在同一 TRTC 房间用两个设备同时进房 | 首版不做多端同房;后续要多端时必须新增 `rtc_user_id` 映射表和端标识 |
|
||
| TRTC 同时支持数字 `roomId` 和字符串 `strRoomId`,二者不能混用 | 本项目内部 `room_id` 是字符串,统一使用 `strRoomId`,客户端不要传数字 `roomId` |
|
||
| UserSig 只校验 `SDKAppID + UserID` 是否有权使用 TRTC 服务,不绑定具体房间 | `VerifyRoomPresence` 只能控制本服务是否签发 token;没有 `privateMapKey` 时腾讯 RTC 后端不会阻止同一 UserSig 进入其他 `strRoomId` |
|
||
| `privateMapKey` 才是 TRTC 房间级权限凭证 | 首版不开启应用级房间权限控制;一旦控制台开启,客户端必须传 `privateMapKey`,否则无法进房 |
|
||
|
||
参考官方文档:
|
||
|
||
- [Secure authentication with UserSig](https://trtc.io/document/34385?menulabel=serverapis&product=chat)
|
||
- [Entering a Room](https://trtc.io/document/47645)
|
||
- [TRTCParams](https://trtc.io/document/72275)
|
||
- [Advanced Permission Control](https://trtc.io/document/35157)
|
||
|
||
## Ownership
|
||
|
||
| Domain | Owner | Rule |
|
||
| --- | --- | --- |
|
||
| HTTP RTC token API | `gateway-service` | 只做鉴权、参数校验、room guard 查询和 UserSig 签发 |
|
||
| 房间业务 presence | `room-service` | 仍然是用户是否属于房间的事实来源 |
|
||
| RTC 音频连接 | 腾讯 RTC | 持有真实音频连接、音频流、频道内在线状态 |
|
||
| 用户身份 | `user-service` | `user_id` 发号和用户状态,不进入 RTC 高频链路 |
|
||
| 客户端音频操作 | Client | 根据 token 进房、切换麦克风、处理 RTC SDK 回调 |
|
||
|
||
边界:
|
||
|
||
- `gateway-service` 不能接受客户端自报 `user_id` 或 `rtc_user_id`。
|
||
- `room-service` 不签发 RTC token,不保存 RTC socket 或音频流状态。
|
||
- `room-service.VerifyRoomPresence` 是签发 RTC token 前的业务守卫。
|
||
- 腾讯 RTC `UserID` 不是展示号,不能使用 `display_user_id`、昵称、手机号或 provider subject。
|
||
- 首版 `VerifyRoomPresence + UserSig` 是业务入口守卫,不是腾讯 RTC 房间级强制鉴权。已拿到有效 UserSig 的客户端理论上可以在 TTL 内请求进入其他 `strRoomId`,因此首版只面向普通开放语音房。
|
||
- 私密房、付费房、活动专属房、合规要求强隔离的房型不能复用首版 token 方案,必须先新增 `privateMapKey` 签发、客户端传参、验收测试和回滚策略。
|
||
|
||
## UID Mapping
|
||
|
||
首版映射规则:
|
||
|
||
```text
|
||
internal user_id int64 -> strconv.FormatInt(user_id, 10) -> rtc_user_id / UserID
|
||
```
|
||
|
||
示例:
|
||
|
||
| Internal | Tencent RTC |
|
||
| --- | --- |
|
||
| `user_id=918274650129384448` | `UserID="918274650129384448"` |
|
||
| `user_id=10001` | `UserID="10001"` |
|
||
|
||
规则:
|
||
|
||
- 映射必须稳定、可重复、无数据库查询。
|
||
- `rtc_user_id` 必须等于腾讯 IM identifier,方便客户端和排障把 IM 用户、RTC 用户、房间业务用户对齐。
|
||
- `display_user_id` 只用于展示、搜索、登录名,不能参与 RTC 身份。
|
||
- access token 中的展示号快照不能作为 RTC 身份。
|
||
- 如果未来要支持同一用户多端同时进入同一 RTC 房,不能简单追加设备后缀;必须新增显式映射,例如 `rtc_user_sessions(user_id, device_id, rtc_user_id, room_id, expires_at_ms)`,并重新设计 IM/RTC/房间事件中的身份还原规则。
|
||
|
||
## Room ID Mapping
|
||
|
||
首版映射规则:
|
||
|
||
```text
|
||
internal room_id string -> RTC strRoomId
|
||
RTC numeric roomId -> always 0 or omitted
|
||
```
|
||
|
||
响应字段建议同时返回业务名和 RTC 名,避免客户端混用:
|
||
|
||
```json
|
||
{
|
||
"room_id": "room_1001",
|
||
"rtc_room_id": "room_1001",
|
||
"rtc_room_id_type": "string",
|
||
"str_room_id": "room_1001"
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- 客户端进腾讯 RTC 房时只使用 `strRoomId`。
|
||
- 客户端不能把 `"123"` 转成数字 `123`;这在 TRTC 中是两个不同房间。
|
||
- 新建房间时的 `room_id` 必须满足 RTC 字符串房间 ID 限制;校验要前移到 `CreateRoom` HTTP/gRPC 入口或 room-service 领域层,不能创建出业务可用但永远拿不到 RTC token 的房间。
|
||
- `/api/v1/rtc/token` 仍要重复校验 `room_id`,作为内部调用和入口绕过场景的 fail-closed 保护;非法房间 ID 返回 `INVALID_ARGUMENT`。
|
||
- 为同时满足腾讯 IM 群组和 RTC 房间限制,`room_id` 格式收敛到:`^[A-Za-z0-9_-]{1,48}$`。如果产品需要更丰富字符,必须同时确认 IM group id、RTC `strRoomId`、OpenAPI 和客户端 SDK 都接受。
|
||
|
||
## External HTTP API
|
||
|
||
RTC token gateway 入口:
|
||
|
||
```text
|
||
POST /api/v1/rtc/token
|
||
Authorization: Bearer <access_token>
|
||
```
|
||
|
||
Request:
|
||
|
||
```json
|
||
{
|
||
"room_id": "room_1001"
|
||
}
|
||
```
|
||
|
||
Response `data`:
|
||
|
||
```json
|
||
{
|
||
"sdk_app_id": 1400000000,
|
||
"user_id": "10001",
|
||
"rtc_user_id": "10001",
|
||
"user_sig": "usersig_xxx",
|
||
"expire_at_ms": 1777000000000,
|
||
"room_id": "room_1001",
|
||
"rtc_room_id": "room_1001",
|
||
"rtc_room_id_type": "string",
|
||
"str_room_id": "room_1001",
|
||
"app_scene": "voice_chat_room",
|
||
"role": "audience"
|
||
}
|
||
```
|
||
|
||
字段规则:
|
||
|
||
- `user_id` 保留腾讯 SDK 常用字段名,值是字符串形式内部长 `user_id`。
|
||
- `rtc_user_id` 是同一个值,用于客户端代码显式区分业务 `user_id` 和 RTC `UserID`。
|
||
- `user_sig` 是腾讯 RTC UserSig,不是腾讯 SecretKey。
|
||
- `expire_at_ms` 让客户端在 SDK 报 UserSig 过期前主动刷新。
|
||
- `role` 是当前推荐入房角色:不在麦上为 `audience`,已在麦上或房主/主持需要发流时为 `anchor`。VoiceChatRoom 场景下 `audience` 不能发布音频流,客户端只有在 `MicUp` 成功后才能切到 `anchor` 并开始采集。
|
||
- 首版 token 响应可以先返回 `audience`;后续如果 gateway 要按麦位返回 `anchor/audience`,必须从 room-service guard 或只读快照获取麦位事实,不能在 gateway 缓存房间状态。
|
||
|
||
错误映射:
|
||
|
||
| Case | HTTP | Code | Rule |
|
||
| --- | ---: | --- | --- |
|
||
| 未登录 | 401 | `UNAUTHORIZED` | middleware 拦截 |
|
||
| `room_id` 为空或不符合 RTC 限制 | 400 | `INVALID_ARGUMENT` | gateway 本地校验 |
|
||
| 不在房间 | 403 | `PERMISSION_DENIED` | `VerifyRoomPresence.present=false` |
|
||
| 房间不存在 | 404 | `NOT_FOUND` | guard reason 为 `room_not_found` |
|
||
| 用户被踢/封禁 | 403 | `PERMISSION_DENIED` | guard reason 为 `user_banned` |
|
||
| RTC 配置缺失 | 500 | `INTERNAL_ERROR` | fail-closed,不返回假 token |
|
||
| room-service 不可用 | 502 | `UPSTREAM_ERROR` | gateway 不能自判 presence |
|
||
|
||
显式房间心跳入口:
|
||
|
||
```text
|
||
POST /api/v1/rooms/heartbeat
|
||
Authorization: Bearer <access_token>
|
||
```
|
||
|
||
Request:
|
||
|
||
```json
|
||
{
|
||
"room_id": "room_1001",
|
||
"command_id": "cmd-heartbeat-1700000000"
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- 心跳只刷新已经存在的 room-service presence 的 `last_seen_at_ms`。
|
||
- 用户不在房间时返回 `NOT_FOUND`,不能通过心跳重新进入房间。
|
||
- 客户端应按小于 `room_presence_stale_after` 的间隔发送心跳;HTTP 重试同一次心跳必须复用同一个 `command_id`。
|
||
- 主动离房仍然必须调用 `/api/v1/rooms/leave`;心跳停止只代表后台 stale worker 最终清理。
|
||
|
||
腾讯 RTC 服务端事件回调入口:
|
||
|
||
```text
|
||
POST /api/v1/tencent-rtc/callback?app_code=default
|
||
Header: SdkAppId: <sdk_app_id>
|
||
Header: Sign: base64(hmacsha256(callback_sign_key, raw_body))
|
||
```
|
||
|
||
消费事件:
|
||
|
||
| Tencent Event | Internal Event | Room Cell Effect |
|
||
| --- | --- | --- |
|
||
| `EventGroupId=2, EventType=203` | `audio_started` | 当前麦位为 `pending_publish` 时确认成 `publishing` |
|
||
| `EventGroupId=2, EventType=204` | `audio_stopped` | 释放当前用户麦位 |
|
||
| `EventGroupId=1, EventType=104` | `room_exited` | 释放当前用户麦位,不移除业务 presence |
|
||
|
||
回调边界:
|
||
|
||
- gateway 只负责验签、校验 `SdkAppId`、解析 Tencent 字段和派生稳定 `command_id`。
|
||
- 房间状态变更只调用 room-service `ApplyRTCEvent`,仍进入 Room Cell、command log 和 outbox。
|
||
- 未知事件在验签成功后返回 `{"code":0}` 并忽略,避免腾讯新增事件导致无意义重试。
|
||
- room-service 暂时不可用时返回非 200,让腾讯 RTC 按平台策略重试。
|
||
- `room_exited` 只代表 RTC 媒体频道退出,用户仍可能停留在房间页重连;业务 presence 只能由 `LeaveRoom`、踢人、关房或 stale worker 清理。
|
||
- 如果一个旧 `room_exited` 回调晚于新的业务心跳到达,room-service 会保留较新的业务 presence 和新麦位会话,避免旧回调清掉重连用户。
|
||
|
||
## Internal Flow
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant G as gateway-service
|
||
participant R as room-service
|
||
participant T as Tencent RTC
|
||
|
||
C->>G: POST /api/v1/rooms/join
|
||
G->>R: JoinRoom(actor_user_id, room_id)
|
||
R-->>G: RoomSnapshot
|
||
G-->>C: JoinRoom envelope
|
||
|
||
C->>G: POST /api/v1/rtc/token(room_id)
|
||
G->>G: verify JWT, derive user_id
|
||
G->>R: VerifyRoomPresence(room_id, user_id)
|
||
R-->>G: present=true, room_version
|
||
G->>G: map user_id -> rtc_user_id
|
||
G->>G: map room_id -> strRoomId
|
||
G->>G: generate UserSig
|
||
G-->>C: SDKAppID/UserID/UserSig/strRoomId
|
||
|
||
C->>T: enterRoom(SDKAppID, UserID, UserSig, strRoomId)
|
||
T-->>C: RTC room entered
|
||
|
||
loop heartbeat before stale timeout
|
||
C->>G: POST /api/v1/rooms/heartbeat(room_id)
|
||
G->>R: RoomHeartbeat(actor_user_id, room_id)
|
||
R-->>G: refreshed last_seen_at_ms
|
||
end
|
||
|
||
T->>G: RTC callback 203/204/104 with Sign
|
||
G->>G: verify HMAC-SHA256 over raw body
|
||
G->>R: ApplyRTCEvent(audio_started/audio_stopped/room_exited)
|
||
R-->>G: RoomSnapshot
|
||
G-->>T: {"code":0}
|
||
```
|
||
|
||
签发顺序:
|
||
|
||
```text
|
||
validate HTTP JSON
|
||
extract user_id from access token
|
||
validate room_id format
|
||
call room-service.VerifyRoomPresence(room_id, user_id)
|
||
map user_id to rtc_user_id string
|
||
generate RTC UserSig from tencent_rtc config
|
||
return token response
|
||
```
|
||
|
||
## Gateway Implementation
|
||
|
||
### Config
|
||
|
||
新增配置块:
|
||
|
||
```yaml
|
||
tencent_rtc:
|
||
enabled: true
|
||
sdk_app_id: 1400000000
|
||
secret_key: "TENCENT_RTC_SECRET_KEY"
|
||
user_sig_ttl: "2h"
|
||
room_id_type: "string"
|
||
app_scene: "voice_chat_room"
|
||
callback_url: "https://api.example.com/api/v1/tencent-rtc/callback?app_code=default"
|
||
callback_sign_key: "TENCENT_RTC_CALLBACK_SIGN_KEY"
|
||
```
|
||
|
||
配置规则:
|
||
|
||
- `secret_key` 只能存在服务端配置或密钥系统,不能下发客户端。
|
||
- `callback_sign_key` 只用于腾讯 RTC 回调验签,不要复用 UserSig `secret_key`。
|
||
- `tencent_rtc` 与 `tencent_im` 分开配置,避免误把 IM 应用和 RTC 应用混用。
|
||
- 如果腾讯控制台实际使用同一个应用承载 Chat/RTC,可以让两块配置填同一组 `sdk_app_id/secret_key`,但仍必须显式配置。
|
||
- `user_sig_ttl` 不宜过长;建议 2h 到 24h,具体按客户端刷新能力和泄露窗口决定。
|
||
- `room_id_type` 首版固定 `string`,其他值启动失败。
|
||
|
||
### Package Boundary
|
||
|
||
当前 `pkg/tencentim.GenerateUserSig` 已实现 Tencent UserSig v2 算法。RTC 接入时不要让 gateway 的 RTC handler 直接依赖 `pkg/tencentim` 语义。
|
||
|
||
推荐做法:
|
||
|
||
```text
|
||
pkg/tencentsig/ 通用 UserSig 算法
|
||
pkg/tencentim/ IM REST 和 IM UserSig wrapper
|
||
pkg/tencentrtc/ RTC token response 和 UserSig wrapper
|
||
```
|
||
|
||
迁移规则:
|
||
|
||
- 先把通用签名算法抽到 `pkg/tencentsig`。
|
||
- 保持现有 `/api/v1/im/usersig` 行为不变。
|
||
- 新增 `pkg/tencentrtc.GenerateUserSig` 或 `GenerateToken` 包装通用算法。
|
||
- 单测用固定时间验证 IM 和 RTC 生成结果稳定,避免重构破坏现有 IM 登录。
|
||
|
||
### Handler
|
||
|
||
新增:
|
||
|
||
```text
|
||
services/gateway-service/internal/transport/http/rtc_handler.go
|
||
```
|
||
|
||
路由:
|
||
|
||
```go
|
||
mux.Handle(apiV1Prefix+"/rtc/token", apiHandler(jwtVerifier, h.issueTencentRTCToken))
|
||
```
|
||
|
||
handler 不写房间状态,只做:
|
||
|
||
- decode `room_id`
|
||
- 从 context 取 authenticated `user_id`
|
||
- 校验 `room_id`
|
||
- 调 `roomGuardClient.VerifyRoomPresence`
|
||
- 根据 guard response 映射 HTTP envelope
|
||
- 签发 UserSig
|
||
- 返回 token data
|
||
|
||
handler 装配规则:
|
||
|
||
- `services/gateway-service/internal/config.Config.TencentRTC` 必须从 `app.New` 传入 HTTP handler,不能只停留在 config 包。
|
||
- `NewHandlerWithConfig` 或后续构造器必须同时接收 `TencentIMConfig` 和 `TencentRTCConfig`,并保持 IM usersig 行为不变。
|
||
- handler 签发前校验 `tencent_rtc.enabled=true`、`sdk_app_id>0`、`secret_key` 非空、`user_sig_ttl>0`、`room_id_type=string`、`app_scene=voice_chat_room`;任一不满足都 fail-closed。
|
||
- 配置错误返回 `INTERNAL_ERROR`,依赖 room-service 失败返回 `UPSTREAM_ERROR`,两者不能混用。
|
||
|
||
### Room Guard Client
|
||
|
||
复用现有 `RoomGuardClient.VerifyRoomPresence`。
|
||
|
||
不新增 room-service RPC,除非后续需要更细的 RTC 进房策略,例如:
|
||
|
||
- 只有上麦用户能以 anchor 进房。
|
||
- 房间关闭 RTC。
|
||
- 房间分区域或分线路。
|
||
- 多端设备踢旧逻辑。
|
||
|
||
## Room-Service Changes
|
||
|
||
room-service 不保存 RTC socket 或在线态,但必须保存“业务占麦后是否完成 RTC 发流确认”的麦位状态,避免把 `MicUp` 当成音频已经发布成功。
|
||
|
||
当前麦位发流状态:
|
||
|
||
| State | Meaning | Transition |
|
||
| --- | --- | --- |
|
||
| empty / `idle` | 空麦或没有发流会话 | `MicUp` 后进入 `pending_publish` |
|
||
| `pending_publish` | 业务麦位占用成功,等待客户端或 RTC webhook 确认音频已发布 | `ConfirmMicPublishing` 后进入 `publishing`;超时自动 `MicDown(reason=publish_timeout)` |
|
||
| `publishing` | 当前 `mic_session_id` 已确认发布音频 | `MicDown`、`LeaveRoom`、`KickUser` 后清空 |
|
||
|
||
需要确认的稳定行为:
|
||
|
||
- `VerifyRoomPresence` 能区分 `room_not_found/not_in_room/user_banned`。
|
||
- `JoinRoom` 重连刷新 `last_seen_at_ms`。
|
||
- `LeaveRoom` 后 `VerifyRoomPresence` 返回 false。
|
||
- Kick 后 `VerifyRoomPresence` 返回 `user_banned`。
|
||
- `MicUp` 返回 `mic_session_id` 和 `publish_deadline_ms`,并在 `SeatState` 写入 `pending_publish`。
|
||
- `ConfirmMicPublishing(mic_session_id, room_version, event_time_ms)` 只确认当前麦位会话。
|
||
- pending 超过 `publish_deadline_ms` 未确认时,room-service 自动下麦并在事件 reason 中写 `publish_timeout`。
|
||
|
||
可选增强:
|
||
|
||
- `RoomSnapshot.room_ext` 后续可加入 `rtc_enabled=true`,但首版不需要。
|
||
- 如果要根据麦位返回 `role=anchor/audience`,gateway 可以从 room-service 增加只读查询或扩展 guard response;不要让 gateway 自己缓存麦位状态。
|
||
- 后续接 RTC webhook 时,webhook payload 必须带 `mic_session_id`、`room_version`、`event_time_ms`;room-service 只接受当前 session 且事件时间不旧于已接受事件的数据,旧 audience/leave 事件不能清掉新上麦状态。
|
||
|
||
## Client Flow
|
||
|
||
标准进房:
|
||
|
||
```text
|
||
1. Login / RefreshToken
|
||
2. POST /api/v1/rooms/join
|
||
3. POST /api/v1/im/usersig
|
||
4. IM SDK login + join group
|
||
5. POST /api/v1/rtc/token
|
||
6. RTC SDK enterRoom with SDKAppID/UserID/UserSig/strRoomId
|
||
7. Use JoinRoom RoomSnapshot as business UI state
|
||
```
|
||
|
||
上麦发布音频:
|
||
|
||
```text
|
||
1. POST /api/v1/rooms/mic/up
|
||
2. Client receives mic_session_id, publish_deadline_ms, and RoomSnapshot with publish_state=pending_publish
|
||
3. RTC SDK switchRole(anchor)
|
||
4. RTC SDK startLocalAudio
|
||
5. Client receives local publish success callback
|
||
6. POST /api/v1/rooms/mic/publishing/confirm with room_id, mic_session_id, room_version, event_time_ms, source=client
|
||
7. RoomSnapshot seat publish_state becomes publishing
|
||
8. If step 6 is not completed before publish_deadline_ms, room-service automatically MicDown(reason=publish_timeout)
|
||
```
|
||
|
||
重连:
|
||
|
||
```text
|
||
1. Refresh access token if needed
|
||
2. POST /api/v1/rooms/join to refresh business presence
|
||
3. If RTC UserSig expired or SDK requires re-enter, POST /api/v1/rtc/token
|
||
4. RTC SDK re-enter same strRoomId
|
||
5. Client reconciles UI with latest RoomSnapshot
|
||
```
|
||
|
||
离房:
|
||
|
||
```text
|
||
1. Client stops local audio if publishing
|
||
2. Client exits RTC room
|
||
3. Client exits IM group if product requires
|
||
4. POST /api/v1/rooms/leave
|
||
5. room-service presence removed
|
||
6. Later /api/v1/rtc/token for same room is denied
|
||
```
|
||
|
||
服务端原则:
|
||
|
||
- RTC 断线不立即等于业务离房。
|
||
- 主动 `LeaveRoom` 才是明确离房。
|
||
- 超过 presence stale timeout 后,业务 presence 被清理,新的 RTC token 会被拒绝。
|
||
- 客户端收到 Kick 系统消息后必须 `stopLocalAudio`、退出 RTC 房并退出 IM 群;服务端后续 token 签发也会拒绝。
|
||
- 下麦成功后客户端必须 `stopLocalAudio`,并在 VoiceChatRoom 场景调用 `switchRole(audience)` 或按 SDK 推荐方式停止发布。
|
||
- 客户端只能把 `publishing` 当成“服务端已接受音频发布确认”;`pending_publish` 只能展示为“正在连接麦克风/等待发流确认”。
|
||
- 客户端重复确认同一 SDK 成功回调时必须复用同一个 `command_id`;如果没有复用,room-service 仍会用 `mic_session_id + room_version + event_time_ms` 防止旧事件覆盖新会话。
|
||
|
||
## Security Rules
|
||
|
||
- UserSig 必须服务端生成。
|
||
- SecretKey 不进客户端、不进日志、不进错误响应。
|
||
- token endpoint 必须要求 access token。
|
||
- token endpoint 必须先查 `VerifyRoomPresence`,不能只要登录就签 RTC token。
|
||
- `room_id`、`user_id`、`rtc_user_id`、`request_id` 可以记录日志;`user_sig` 和 `secret_key` 不能记录。
|
||
- UserSig 过期只影响 RTC SDK 鉴权,不应改变房间业务 presence。
|
||
- 如果 `tencent_rtc.enabled=false` 或配置不完整,接口 fail-closed。
|
||
- UserSig 不是房间级权限凭证。首版普通房接受“签发入口校验 + 短 TTL + 客户端退出协议”的边界;需要腾讯侧强制房间准入时必须启用 `privateMapKey`。
|
||
- 不启用 `privateMapKey` 时,不要把 `/api/v1/rtc/token` 文案、产品策略或风控策略描述成“腾讯 RTC 后端已强制禁止跨房间进入”。
|
||
|
||
## Observability
|
||
|
||
日志字段:
|
||
|
||
| Field | Meaning |
|
||
| --- | --- |
|
||
| `request_id` | gateway HTTP 追踪 ID |
|
||
| `user_id` | 内部长 ID |
|
||
| `rtc_user_id` | 腾讯 RTC UserID |
|
||
| `room_id` | 内部房间 ID |
|
||
| `str_room_id` | RTC 字符串房间 ID |
|
||
| `room_version` | guard 返回的房间版本 |
|
||
| `mic_session_id` | 单次上麦发流会话 ID |
|
||
| `publish_state` | pending_publish / publishing |
|
||
| `publish_deadline_ms` | 发流确认截止时间 |
|
||
| `publish_event_time_ms` | 客户端 SDK 或 RTC webhook 事件时间 |
|
||
| `result` | issued / denied / config_error / upstream_error |
|
||
| `reason` | guard 或配置失败原因 |
|
||
|
||
指标建议:
|
||
|
||
```text
|
||
gateway_rtc_token_issued_total
|
||
gateway_rtc_token_denied_total
|
||
gateway_rtc_token_config_error_total
|
||
gateway_rtc_token_upstream_error_total
|
||
gateway_rtc_token_latency_ms
|
||
room_mic_publish_confirmed_total
|
||
room_mic_publish_timeout_total
|
||
room_mic_publish_stale_event_ignored_total
|
||
```
|
||
|
||
腾讯 RTC 外部进房成功率由客户端 SDK 上报或日志采集,不放进 gateway readiness。
|
||
|
||
## Implementation Order
|
||
|
||
1. 新增 `docs/腾讯RTC票据UID映射开发.md` 并确认产品边界。
|
||
2. 抽出通用 Tencent UserSig 算法到 `pkg/tencentsig`,保持 IM usersig 测试通过。
|
||
3. 新增 `pkg/tencentrtc`,定义 RTC token config/result 和 `GenerateToken`。
|
||
4. 扩展 gateway config:`tencent_rtc`。
|
||
5. 三份 gateway config 增加 `tencent_rtc` 示例。
|
||
6. 把 `config.TencentRTC` 从 `app.New` 装配到 HTTP handler,并补 handler 构造器单测。
|
||
7. 新增 gateway `/api/v1/rtc/token` handler 和 route。
|
||
8. handler 调 `RoomGuardClient.VerifyRoomPresence`。
|
||
9. 在 `CreateRoom` 入口或 room-service 领域层补统一 `room_id` 格式校验,并保持 `/api/v1/rtc/token` 二次校验。
|
||
10. 扩展 room proto:`SeatState.publish_state/mic_session_id/publish_deadline_ms`、`MicUpResponse`、`ConfirmMicPublishing`。
|
||
11. room-service 在 `MicUp` 写入 `pending_publish`,确认后写 `publishing`,超时自动 `MicDown(reason=publish_timeout)`。
|
||
12. gateway 增加 `/api/v1/rooms/mic/publishing/confirm` 并透传 request_id 到 room-service。
|
||
13. 更新 `docs/openapi/gateway.swagger.yaml` 和 `docs/openapi/room.swagger.yaml`。
|
||
14. 增加 gateway transport、room-service command、stale event 和 timeout 单测。
|
||
15. 跑 Phase 2 最小手工联调:JoinRoom 后签 RTC token,客户端用 `strRoomId` 进入语音房,再完成 MicUp -> ConfirmMicPublishing -> MicDown。
|
||
|
||
## Acceptance Tests
|
||
|
||
必须覆盖:
|
||
|
||
| Test | Expected Result |
|
||
| --- | --- |
|
||
| 已登录且已进房用户签发 RTC token | 返回 `sdk_app_id/user_id/rtc_user_id/user_sig/str_room_id/expire_at_ms` |
|
||
| 未登录用户请求 | HTTP 401 envelope |
|
||
| 未 JoinRoom 用户请求 | HTTP 403 envelope,不返回 UserSig |
|
||
| 房间不存在 | HTTP 404 envelope |
|
||
| 被踢用户请求 | HTTP 403 envelope |
|
||
| RTC 配置缺失 | HTTP 500 envelope,不返回假 UserSig |
|
||
| `room_id` 为空或格式非法 | HTTP 400 envelope |
|
||
| CreateRoom 使用不符合 RTC/IM 限制的 `room_id` | HTTP 400 envelope,房间不创建 |
|
||
| UserID 映射 | `user_id=10001` 时 `rtc_user_id == "10001"` |
|
||
| request_id 透传 | response envelope 和 guard RPC 使用同一个 `request_id` |
|
||
| `tencent_rtc.enabled=false` | HTTP 500 envelope,不返回 UserSig |
|
||
| `room_id_type` 非 `string` | handler fail-closed,不返回 UserSig |
|
||
| IM UserSig 不回归 | 现有 `/api/v1/im/usersig` 测试仍通过 |
|
||
| MicUp 后未确认发布 | 麦位进入 `pending_publish`,响应返回 `mic_session_id/publish_deadline_ms` |
|
||
| ConfirmMicPublishing 当前 session | 麦位进入 `publishing` |
|
||
| ConfirmMicPublishing 旧 session/旧版本/旧 event_time | 不覆盖当前麦位状态 |
|
||
| publish deadline 超时 | 自动 `MicDown(reason=publish_timeout)` 并释放麦位 |
|
||
|
||
建议补充手工验证:
|
||
|
||
```text
|
||
Login
|
||
-> CreateRoom
|
||
-> JoinRoom
|
||
-> Get IM UserSig
|
||
-> IM SDK login and join group
|
||
-> Get RTC token
|
||
-> RTC SDK enterRoom(strRoomId=room_id, userId=rtc_user_id)
|
||
-> MicUp
|
||
-> SeatState.publish_state=pending_publish, client receives mic_session_id
|
||
-> RTC SDK switchRole(anchor)
|
||
-> Client starts publishing audio
|
||
-> ConfirmMicPublishing(mic_session_id, room_version, event_time_ms)
|
||
-> SeatState.publish_state=publishing
|
||
-> Retry stale ConfirmMicPublishing with old mic_session_id/event_time_ms and verify no state rollback
|
||
-> MicDown
|
||
-> Client stops publishing audio and switches to audience
|
||
-> MicUp again but do not confirm
|
||
-> publish_deadline_ms expires and room-service auto MicDown(reason=publish_timeout)
|
||
-> LeaveRoom
|
||
-> RTC token denied for same room
|
||
```
|
||
|
||
## Verification Commands
|
||
|
||
涉及代码实现后至少运行:
|
||
|
||
```bash
|
||
go test ./pkg/tencentim
|
||
go test ./pkg/tencentsig
|
||
go test ./pkg/tencentrtc
|
||
go test ./services/gateway-service/internal/config
|
||
go test ./services/gateway-service/internal/transport/http
|
||
go test ./...
|
||
```
|
||
|
||
涉及配置后运行:
|
||
|
||
```bash
|
||
docker compose config
|
||
```
|
||
|
||
涉及 protobuf 时运行:
|
||
|
||
```bash
|
||
make proto
|
||
go test ./...
|
||
```
|
||
|
||
## Done Definition
|
||
|
||
完成标准:
|
||
|
||
```text
|
||
Authenticated user
|
||
-> JoinRoom succeeds
|
||
-> /api/v1/rtc/token checks room-service presence
|
||
-> gateway maps internal user_id to RTC UserID
|
||
-> gateway maps room_id to RTC strRoomId
|
||
-> gateway signs RTC UserSig on server
|
||
-> client enters Tencent RTC room
|
||
-> MicUp creates pending_publish mic_session
|
||
-> client confirms publishing with mic_session_id, room_version, event_time_ms
|
||
-> stale RTC/client events cannot clear a newer mic_session
|
||
-> unconfirmed pending_publish session auto MicDowns after deadline
|
||
-> LeaveRoom/Kick/stale cleanup makes later RTC token request fail
|
||
```
|
||
|
||
只要这条链路稳定,后续麦位上行、角色切换、音频审核和多端 RTC 策略都可以在不破坏 Room Cell 状态边界的前提下继续迭代。
|