hyapp-server/docs/voice-room-basic-loop-implementation.md
2026-04-27 02:29:42 +08:00

364 lines
12 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.

# Voice Room Basic Loop Implementation
本文档定义语音房 v1 基础闭环的实现顺序和验收边界。目标不是做完整语音房,而是先让“登录用户创建房间、进入房间、登录腾讯云 IM、加入房间群、接收房间系统消息、离房、重连恢复”成为可测闭环。
## 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
G->>R: CreateRoom(actor_user_id)
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 <access_token>
```
实现约束:
- `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 群组存在。
主链路顺序:
```text
validate request
acquire Redis lease
check room meta
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 事件名。
- 新增字段必须兼容旧客户端,不能修改已有字段语义。
首版系统事件:
| 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、礼物、活动、房间推荐都可以在不破坏边界的前提下继续迭代。