hyapp-server/docs/tencent-rtc-token-uid-mapping-development.md
2026-05-05 22:10:26 +08:00

605 lines
25 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.

# 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` 回调晚于新的业务心跳到达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/tencent-rtc-token-uid-mapping-development.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 状态边界的前提下继续迭代。