381 lines
14 KiB
Markdown
381 lines
14 KiB
Markdown
# Voice Room Basic Loop Implementation
|
||
|
||
本文档定义语音房 v1 基础闭环的实现顺序和验收边界。目标不是做完整语音房,而是先让“登录用户创建房间、进入房间、登录腾讯云 IM、加入房间群、接收房间系统消息、离房、重连恢复”成为可测闭环。
|
||
|
||
Room Cell 的完整状态机见:
|
||
|
||
```text
|
||
docs/房间Cell状态机.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` | `RoomCreated` outbox 由 worker 异步确保 `room_id` 对应群组存在 |
|
||
| 房间系统消息 | `room-service` | 房间事件只写 outbox,腾讯云 IM 由 outbox worker 异步投递 |
|
||
| 实时权限守卫 | `gateway-service` + `room-service` | 腾讯云 IM 入群前和群内发言前必须回查房间业务态 |
|
||
| 腾讯云 RTC 进房票据 | `gateway-service` | `JoinRoom` 成功后在响应中内嵌 RTC token;`/api/v1/rtc/token` 只做补偿和刷新 |
|
||
|
||
本阶段不做:
|
||
|
||
| Excluded | Reason |
|
||
| --- | --- |
|
||
| 自研 WebSocket | 实时连接和消息通道由腾讯云 IM 承担 |
|
||
| 全量 IM 历史库 | 群历史优先使用腾讯云 IM 能力 |
|
||
| RTC 录制/审核 | 只先接入进房听音频所需的 token 和回调,不处理录制与审核 |
|
||
| 复杂单聊会话 | 先保证房间基础闭环 |
|
||
| 热门房多活 | 当前仍是单房间单活 + 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
|
||
participant RTC as Tencent Cloud RTC
|
||
|
||
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: create_group(room_id)
|
||
T-->>R: group ready
|
||
R->>R: command log + snapshot + outbox
|
||
R-->>G: room DTO with im_group_id
|
||
G-->>C: room envelope with im_group_id
|
||
|
||
C->>G: GET /api/v1/im/usersig
|
||
G-->>C: SDKAppID + user_id + UserSig
|
||
C->>T: SDK login(user_id, user_sig)
|
||
|
||
C->>G: POST /api/v1/rooms/join
|
||
G->>R: JoinRoom(actor_user_id)
|
||
R->>R: update Room Cell presence
|
||
R-->>G: RoomSnapshot
|
||
G-->>C: initial room envelope + im_group_id + RTC token
|
||
R-->>T: outbox worker send_group_msg(room_user_joined)
|
||
|
||
C->>T: joinGroup(room_id)
|
||
T->>G: CallbackBeforeApplyJoinGroup
|
||
G->>R: VerifyRoomPresence(room_id, user_id)
|
||
R-->>G: present=true
|
||
G-->>T: allow
|
||
|
||
C->>RTC: enterRoom(strRoomId from JoinRoom rtc.token)
|
||
|
||
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 房间群存在。创建房间是低频动作,主链路宁可因为腾讯 IM 建群失败返回错误,也不能让 App 在 `JoinRoom` 成功后遇到 group-not-ready。
|
||
同一个 `actor_user_id` 只能作为 owner 创建一个房间;加入、主持或管理别人的房间不受该 owner 限制影响。
|
||
|
||
主链路顺序:
|
||
|
||
```text
|
||
validate request
|
||
acquire Redis lease
|
||
check room meta
|
||
check owner has no room
|
||
ensure Tencent IM room group
|
||
initialize RoomState
|
||
write room meta + command log + snapshot + outbox
|
||
install Room Cell
|
||
return room DTO with im_group_id
|
||
```
|
||
|
||
如果同步 `EnsureRoomGroup` 失败:
|
||
|
||
- `CreateRoom` 返回失败,不写房间 meta、command log、snapshot 或 presence 投影。
|
||
- App 不进入房间页,不调用 `JoinRoom` 或腾讯 IM SDK `joinGroup`。
|
||
- 如果腾讯 IM 已建群但后续本地持久化失败,后续同 GroupID 建群按幂等成功处理。
|
||
- 失败原因不应该泄露腾讯云密钥、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 系统消息由 worker 异步投递。
|
||
|
||
### 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`。给单个用户的私有通知不从这个 bridge 发,统一由 notice-service 读取 owner outbox 后走腾讯云 IM C2C。
|
||
|
||
客户端只依赖以下稳定字段:
|
||
|
||
```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` |
|
||
|
||
`room_user_kicked` 还会由 notice-service 读取同一条 `RoomUserKicked` 事实,给被踢用户发送 C2C `room_notice`。room-service 仍负责把目标用户移出腾讯 IM 群;notice-service 不处理建群、退群或群成员权限。
|
||
|
||
## 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 outbox 建群、JoinRoom、回调守卫、LeaveRoom、重连恢复。
|
||
|
||
## Acceptance Tests
|
||
|
||
必须有自动化测试覆盖:
|
||
|
||
| Test | Expected Result |
|
||
| --- | --- |
|
||
| UserSig 签发 | 已登录用户拿到 `sdk_app_id/user_id/user_sig`,匿名用户失败 |
|
||
| UserSig 配置缺失 | 返回统一错误,不返回假票据 |
|
||
| CreateRoom 建群失败 | 返回失败,MySQL 不写 room meta/snapshot/command/outbox |
|
||
| 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 继续正确 |
|
||
| outbox 投递失败 | 房间状态已提交,outbox 进入 `retryable` 或最终 `failed` |
|
||
|
||
## 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、礼物、活动、房间推荐都可以在不破坏边界的前提下继续迭代。
|