# Voice Room Basic Loop Implementation 本文档定义语音房 v1 基础闭环的实现顺序和验收边界。目标不是做完整语音房,而是先让“登录用户创建房间、进入房间、登录腾讯云 IM、加入房间群、接收房间系统消息、离房、重连恢复”成为可测闭环。 Room Cell 的完整状态机见: ```text docs/room-cell-state-machines.md ``` ## Scope 本阶段必须打通: | Capability | Owner | Required Result | | --- | --- | --- | | 用户身份 | `user-service` | gateway 能从 access token 得到稳定 `user_id` | | HTTP 命令入口 | `gateway-service` | 所有房间命令走 `/api/v1` envelope | | 房间业务态 | `room-service` | Room Cell 持有 presence、麦位、禁言、踢人、快照和恢复 | | 腾讯云 IM 登录票据 | `gateway-service` | `/api/v1/im/usersig` 返回 `sdk_app_id/user_id/user_sig/expire_at_ms` | | 腾讯云 IM 群组 | `room-service` | `CreateRoom` 时确保 `room_id` 对应群组存在 | | 房间系统消息 | `room-service` | 房间事件同步推腾讯云 IM,失败后由 outbox 补投 | | 实时权限守卫 | `gateway-service` + `room-service` | 腾讯云 IM 入群前和群内发言前必须回查房间业务态 | 本阶段不做: | Excluded | Reason | | --- | --- | | 自研 WebSocket | 实时连接和消息通道由腾讯云 IM 承担 | | 全量 IM 历史库 | 群历史优先使用腾讯云 IM 能力 | | RTC token | 下一阶段接入腾讯云 RTC | | 复杂单聊会话 | 先保证房间基础闭环 | | 热门房多活 | 当前仍是单房间单活 + Redis lease 接管 | ## Required Config `gateway-service` 负责客户端 UserSig 和腾讯云 IM 回调入口: ```yaml tencent_im: sdk_app_id: 20036101 secret_key: "TENCENT_IM_SECRET_KEY" user_sig_ttl: "24h" callback_url: "https://api.example.com/api/v1/tencent-im/callback" callback_auth_token: "TENCENT_IM_CALLBACK_AUTH_TOKEN" ``` `room-service` 负责服务端 REST API 建群和系统消息: ```yaml tencent_im: enabled: true sdk_app_id: 20036101 secret_key: "TENCENT_IM_SECRET_KEY" admin_identifier: "900100100" admin_user_sig_ttl: "24h" endpoint: "adminapisgp.im.qcloud.com" group_type: "ChatRoom" request_timeout: "5s" ``` 配置规则: - `sdk_app_id`、`secret_key` 必须在 gateway 和 room-service 中保持一致。 - `endpoint` 按腾讯云 IM 应用区域填写;新加坡使用 `adminapisgp.im.qcloud.com`。 - `secret_key` 和 `callback_auth_token` 在线上必须走私有配置源;本地调试配置只用于本地联调。 - 本地 Docker 联调时必须让 gateway 和 room-service 使用同一套腾讯云 IM 配置,否则 UserSig、建群和回调守卫会出现跨应用不一致。 ## Target Flow ```mermaid sequenceDiagram participant C as Client participant G as gateway-service participant U as user-service participant R as room-service participant T as Tencent Cloud IM C->>G: Login / RefreshToken G->>U: gRPC auth U-->>G: access token with user_id G-->>C: token envelope C->>G: POST /api/v1/rooms/create(room_name, room_avatar?, room_description?) G->>R: CreateRoom(actor_user_id, room profile) R->>T: REST create_group(room_id) R->>R: command log + snapshot + outbox R-->>G: RoomSnapshot G-->>C: room envelope C->>G: POST /api/v1/rooms/join G->>R: JoinRoom(actor_user_id) R->>R: update Room Cell presence R->>T: REST send_group_msg(room_user_joined) R-->>G: RoomSnapshot G-->>C: room envelope C->>G: GET /api/v1/im/usersig G-->>C: SDKAppID + user_id + UserSig C->>T: SDK login(user_id, user_sig) C->>T: joinGroup(room_id) T->>G: CallbackBeforeApplyJoinGroup G->>R: VerifyRoomPresence(room_id, user_id) R-->>G: present=true G-->>T: allow C->>T: send group message T->>G: CallbackBeforeSendMsg G->>R: CheckSpeakPermission(room_id, user_id) R-->>G: allowed=true G-->>T: allow ``` ## Gateway Implementation ### 1. UserSig 已存在入口: ```text GET /api/v1/im/usersig Authorization: Bearer ``` 实现约束: - `user_id` 必须来自 access token,不接受客户端传入。 - 腾讯云 IM identifier 使用内部长 `user_id` 的十进制字符串。 - `secret_key` 缺失时必须 fail-closed,返回统一 envelope 错误,不能返回假票据。 - `request_id` 只做链路追踪,不作为 UserSig 幂等键。 ### 2. Callback Endpoint 新增入口: ```text POST /api/v1/tencent-im/callback ``` 该入口不使用客户端 JWT。它只接受腾讯云 IM 服务端回调,并按 `CallbackCommand` 分发。 必须支持的回调: | CallbackCommand | Purpose | Internal RPC | | --- | --- | --- | | `Group.CallbackBeforeApplyJoinGroup` | 入群前确认用户业务上在房间内 | `VerifyRoomPresence` | | `Group.CallbackBeforeSendMsg` | 群内发言前确认未被禁言、未被踢、房间允许聊天 | `CheckSpeakPermission` | 回调处理规则: - 必须校验回调来源。启用腾讯云 IM 回调鉴权后,使用 `callback_auth_token` 或控制台约定的鉴权参数校验。 - `CallbackCommand` 不在白名单内时返回允许,但要记录日志,避免未知回调阻断腾讯云 IM。 - `GroupId` 映射为 `room_id`。 - `Operator_Account` 或 `From_Account` 映射为内部长 `user_id`,只能接受十进制字符串。 - 解析失败时拒绝该次入群或发言,避免绕过业务守卫。 建议结构: ```text services/gateway-service/internal/transport/http/ tencent_im_callback.go services/gateway-service/internal/tencentim/ callback.go verifier.go command.go ``` gateway 不保存房间状态,只做回调鉴权、字段解析、调用 room guard RPC 和返回腾讯云 IM 需要的响应。 ## Room-Service Implementation ### 1. CreateRoom `CreateRoom` 必须在房间创建链路中确保腾讯云 IM 群组存在。 同一个 `actor_user_id` 只能作为 owner 创建一个房间;加入、主持或管理别人的房间不受该 owner 限制影响。 主链路顺序: ```text validate request acquire Redis lease check room meta check owner has no room EnsureRoomGroup(room_id) initialize RoomState write room meta + command log + snapshot + outbox install Room Cell return snapshot ``` 如果 `EnsureRoomGroup` 失败: - `CreateRoom` 不应该提交房间状态。 - 客户端可以重试同一个创建请求。 - 失败原因不应该泄露腾讯云密钥、UserSig 或完整公网响应。 ### 2. JoinRoom `JoinRoom` 只修改 room-service 业务 presence,不创建腾讯云 IM 连接。 幂等规则: - 用户已在房间内时刷新 `last_seen_at_ms`。 - 不重复增加在线人数。 - 不重复广播进房系统消息,除非产品明确要求“重新进房提示”。 当前实现如果重复 JoinRoom 直接返回快照,需要补齐 `last_seen_at_ms` 刷新,否则断线重连无法把 presence 延长。 ### 3. LeaveRoom `LeaveRoom` 只移除业务 presence。 要求: - 用户不在房间内时幂等返回,不报错。 - 用户离房后 `VerifyRoomPresence` 必须返回 `present=false`。 - 如果用户在麦上,离房必须同步释放麦位。 - 离房事件写 outbox,并尝试推腾讯云 IM 系统消息。 ### 4. Guard RPC `VerifyRoomPresence` 是入群前守卫。 返回原因: | Reason | Meaning | | --- | --- | | `room_not_found` | 房间不存在或不可恢复 | | `not_in_room` | 用户没有业务 presence | | `user_banned` | 用户被踢或封禁 | `CheckSpeakPermission` 是发言前守卫。 返回原因: | Reason | Meaning | | --- | --- | | `room_not_found` | 房间不存在或不可恢复 | | `not_in_room` | 用户没有业务 presence | | `user_banned` | 用户被踢或封禁 | | `user_muted` | 用户被禁言 | | `chat_disabled` | 房间关闭公屏 | 守卫 RPC 不写状态,不发 outbox,只读取当前 Room Cell 或恢复后的 snapshot 状态。 ## Tencent IM Message Contract room-service 发给腾讯云 IM 的房间系统消息使用 `TIMCustomElem`。 客户端只依赖以下稳定字段: ```json { "event_id": "evt_xxx", "room_id": "room_1001", "event_type": "room_user_joined", "actor_user_id": 10001, "target_user_id": 10001, "seat_no": 1, "gift_value": 20, "room_heat": 100, "room_version": 7, "attributes": { "reason": "stale_timeout" } } ``` 字段规则: - `event_id` 是客户端去重键。 - `room_version` 是房间状态排序参考,不替代腾讯云 IM 消息序列。 - `event_type` 使用 snake_case,不能直接暴露 protobuf 事件名。 - 系统事件字段按当前 OpenAPI/proto 契约输出;新增字段必须同步 gateway、room-service 和客户端测试。 首版系统事件: | Event Type | Source | | --- | --- | | `room_user_joined` | `RoomUserJoined` | | `room_user_left` | `RoomUserLeft` | | `room_mic_changed` | `RoomMicChanged` | | `room_user_muted` | `RoomUserMuted` | | `room_user_kicked` | `RoomUserKicked` | | `room_gift_sent` | `RoomGiftSent` | ## Recovery Rules 节点重启或接管时: 1. 新节点获得 Redis lease。 2. 从 MySQL 读取最新 snapshot。 3. 回放 snapshot version 之后的 command log。 4. 安装 Room Cell。 5. 客户端通过 `JoinRoom` 幂等刷新 presence。 6. 客户端重新获取 UserSig 或复用未过期 UserSig 登录腾讯云 IM。 7. 客户端重新加入房间群,并用最新 `RoomSnapshot` 覆盖本地状态。 恢复不做: - 不恢复本地 socket。 - 不重新扣费。 - 不重复调用腾讯云 IM 发送历史系统消息。 - 不把 Redis 当恢复来源。 ## Implementation Order 按这个顺序实现,避免先做复杂能力导致闭环不可测: 1. **锁定配置**:确认 gateway 和 room-service 的 `sdk_app_id`、`secret_key`、新加坡 endpoint、管理员账号一致。 2. **补 gateway 回调 handler**:实现 `/api/v1/tencent-im/callback`,支持入群前和发言前回调。 3. **补 room guard client**:gateway 内部 client 增加 `RoomGuardService` 调用。 4. **补 JoinRoom 重连刷新**:重复 JoinRoom 刷新 `last_seen_at_ms`,但不重复发进房系统消息。 5. **补 LeaveRoom 麦位释放**:离房时如果在麦上,释放麦位并产生一致的房间事件。 6. **补 presence stale worker**:按 `last_seen_at_ms` 清理超时 presence,写 `RoomUserLeft`。 7. **补集成测试**:覆盖 UserSig、CreateRoom 建群、JoinRoom、回调守卫、LeaveRoom、重连恢复。 ## Acceptance Tests 必须有自动化测试覆盖: | Test | Expected Result | | --- | --- | | UserSig 签发 | 已登录用户拿到 `sdk_app_id/user_id/user_sig`,匿名用户失败 | | UserSig 配置缺失 | 返回统一错误,不返回假票据 | | CreateRoom 建群失败 | 房间状态不提交,命令可重试 | | CreateRoom 成功 | MySQL meta/snapshot/command/outbox 都存在 | | JoinRoom 成功 | presence 增加,返回 RoomSnapshot | | 重复 JoinRoom | 刷新 `last_seen_at_ms`,不重复创建 presence | | 入群前回调允许 | 已进房且未被 ban 的用户返回 allow | | 入群前回调拒绝 | 未进房或被踢用户返回 deny | | 发言前回调允许 | 已进房、未禁言、房间聊天开启 | | 发言前回调拒绝 | 被禁言、被踢、未进房或房间聊天关闭 | | LeaveRoom 后发言 | `CheckSpeakPermission` 返回 `not_in_room` | | room-service 接管恢复 | snapshot + command log 恢复后守卫 RPC 继续正确 | | PublishRoomEvent 失败 | 房间状态已提交,outbox 保留可补投事件 | ## Operational Checks 上线前必须人工确认: - 腾讯云 IM 控制台应用区域是新加坡,REST endpoint 使用 `adminapisgp.im.qcloud.com`。 - 腾讯云 IM 控制台已配置回调 URL,且 URL 指向 gateway 公网入口。 - 回调鉴权 token 和 gateway 配置一致。 - 已启用 `Group.CallbackBeforeApplyJoinGroup` 和 `Group.CallbackBeforeSendMsg`。 - gateway 和 room-service 的 `SDKAppID`、`SecretKey` 一致。 - 线上配置没有把真实 secret 写入仓库。 - room outbox 积压、腾讯云 IM REST 错误率、UserSig 签发失败率都有监控。 ## Done Definition 基础闭环完成的标准: ```text User Login -> CreateRoom -> JoinRoom -> Get UserSig -> Tencent IM SDK Login -> Join Tencent IM Group -> Callback VerifyRoomPresence allow -> Send Room Chat -> Callback CheckSpeakPermission allow -> Mic/Gift/Kick/Mute system event delivered -> LeaveRoom -> Callback guard denies further speak/join -> Reconnect via JoinRoom + UserSig + IM group join ``` 只要这条链路稳定,后续 RTC、礼物、活动、房间推荐都可以在不破坏边界的前提下继续迭代。