hyapp-server/docs/腾讯RTC票据UID映射开发.md
2026-05-11 19:26:01 +08:00

25 KiB
Raw Blame History

Tencent RTC Token And UID Mapping Development Guide

本文档定义 Phase 2 “腾讯 RTC 语音接入”中,gateway-service 如何签发腾讯 RTC 进房凭证,以及系统内部 user_idroom_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,否则无法进房

参考官方文档:

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_idrtc_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

首版映射规则:

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

首版映射规则:

internal room_id string -> RTC strRoomId
RTC numeric roomId -> always 0 or omitted

响应字段建议同时返回业务名和 RTC 名,避免客户端混用:

{
  "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 入口:

POST /api/v1/rtc/token
Authorization: Bearer <access_token>

Request:

{
  "room_id": "room_1001"
}

Response data:

{
  "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

显式房间心跳入口:

POST /api/v1/rooms/heartbeat
Authorization: Bearer <access_token>

Request:

{
  "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 服务端事件回调入口:

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

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}

签发顺序:

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

新增配置块:

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_rtctencent_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 语义。

推荐做法:

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.GenerateUserSigGenerateToken 包装通用算法。
  • 单测用固定时间验证 IM 和 RTC 生成结果稳定,避免重构破坏现有 IM 登录。

Handler

新增:

services/gateway-service/internal/transport/http/rtc_handler.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 或后续构造器必须同时接收 TencentIMConfigTencentRTCConfig,并保持 IM usersig 行为不变。
  • handler 签发前校验 tencent_rtc.enabled=truesdk_app_id>0secret_key 非空、user_sig_ttl>0room_id_type=stringapp_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 已确认发布音频 MicDownLeaveRoomKickUser 后清空

需要确认的稳定行为:

  • VerifyRoomPresence 能区分 room_not_found/not_in_room/user_banned
  • JoinRoom 重连刷新 last_seen_at_ms
  • LeaveRoomVerifyRoomPresence 返回 false。
  • Kick 后 VerifyRoomPresence 返回 user_banned
  • MicUp 返回 mic_session_idpublish_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/audiencegateway 可以从 room-service 增加只读查询或扩展 guard response不要让 gateway 自己缓存麦位状态。
  • 后续接 RTC webhook 时webhook payload 必须带 mic_session_idroom_versionevent_time_msroom-service 只接受当前 session 且事件时间不旧于已接受事件的数据,旧 audience/leave 事件不能清掉新上麦状态。

Client Flow

标准进房:

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

上麦发布音频:

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)

重连:

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

离房:

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_iduser_idrtc_user_idrequest_id 可以记录日志;user_sigsecret_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 或配置失败原因

指标建议:

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 configtencent_rtc
  5. 三份 gateway config 增加 tencent_rtc 示例。
  6. config.TencentRTCapp.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 protoSeatState.publish_state/mic_session_id/publish_deadline_msMicUpResponseConfirmMicPublishing
  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.yamldocs/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=10001rtc_user_id == "10001"
request_id 透传 response envelope 和 guard RPC 使用同一个 request_id
tencent_rtc.enabled=false HTTP 500 envelope不返回 UserSig
room_id_typestring 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) 并释放麦位

建议补充手工验证:

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

涉及代码实现后至少运行:

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 ./...

涉及配置后运行:

docker compose config

涉及 protobuf 时运行:

make proto
go test ./...

Done Definition

完成标准:

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 状态边界的前提下继续迭代。