# 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 新增 gateway 入口: ```text POST /api/v1/rtc/token Authorization: Bearer ``` 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 | ## 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 ``` 签发顺序: ```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" ``` 配置规则: - `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 状态边界的前提下继续迭代。