feat(phase2e-2): WebSocket 信令协议 13 事件全量落地(Task 6)
将 meeting.* 事件族从 Task 5 的 PublishToUser 循环升级为完整 WS 信令协议:
抽离统一广播层、扩容 MediaOrchestrator 接口、实现 8 个 C→S 事件业务逻辑
+ Redis 资源追踪 + host 权限校验,端到端 18/18 PASS。
核心产出:
- 新建 MeetingBroadcaster(统一广播层,REST/WS 共用)
- 新建 MeetingSignalService 8 C→S 事件 + cleanupUserResources
- 新建 MeetingWSHandler 薄层 controller
- MediaOrchestrator 扩容 9 方法 + NoopMediaOrchestrator 占位(Task 7 替换)
- C→S 白名单机制防恶意伪造广播事件
- Redis Set 资源追踪防 mediasoup 端资源泄漏
文档同步:
- docs/api/frontend/meeting.md 追加 §WebSocket 信令协议(Task 6)200 行
- docs/progress/CURRENT_STATUS.md + project-context.mdc + 实施计划 Task 6 ✅
Made-with: Cursor
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
> 通用规范(认证、响应包络、通用错误码)见 [README.md](../README.md)
|
||||
> 会议内实时信令(Transport / Producer / Consumer / 控制事件)通过 WebSocket 完成,见 [websocket.md](../websocket.md)
|
||||
|
||||
**实施状态**:本文档对应 Phase 2e-2 Task 5 已落地的 12 个 REST 接口,统一前缀 `/api/v1/meeting`,全部需要 JWT 认证。Task 5 完成时间:2026-04-21。
|
||||
**实施状态**:本文档对应 Phase 2e-2 Task 5 / Task 6 已落地的 12 个 REST 接口 + 13 个 WebSocket 信令事件,统一前缀 `/api/v1/meeting`(REST)与 `/ws`(WebSocket),全部需要 JWT 认证。Task 5 完成时间:2026-04-21;Task 6 完成时间:2026-04-21。
|
||||
|
||||
**设计口径**:以 [`docs/plans/2026-04-21-phase2e-2-design.md`](../../plans/2026-04-21-phase2e-2-design.md) §6.2 为单一事实来源(SSOT)。
|
||||
|
||||
@@ -371,29 +371,272 @@
|
||||
|
||||
---
|
||||
|
||||
## WebSocket 事件关联
|
||||
## WebSocket 信令协议(Task 6)
|
||||
|
||||
参考 [websocket.md](../websocket.md) 的 `meeting.*` 事件族。Task 5 内部当前使用 `ws.PubSub.PublishToUser` 对活跃参会者逐个推送(Task 6 将封装为 `BroadcastToMeeting`,接口无感替换):
|
||||
全部会议相关实时信令走 `/ws?token=<access_token>` 统一通道,共 **13 个 `meeting.*` 事件**:8 个客户端→服务端(C→S)操作事件 + 5 个服务端→客户端(S→C)广播事件 + 2 个补充业务事件(聊天 + 被踢定向推送,与 REST 广播复用)。
|
||||
|
||||
| 事件 | 触发 REST | 载荷关键字段 |
|
||||
|------|-----------|-------------|
|
||||
| `meeting.member.joined` | JoinRoom | room_code / participant |
|
||||
| `meeting.member.left` | LeaveRoom / KickMember | room_code / user_id / reason |
|
||||
| `meeting.member.kicked` | KickMember(仅发给被踢者) | room_code |
|
||||
| `meeting.host.changed` | TransferHost / 隐式(host 离会后自动转让) | room_code / old_host_id / new_host_id |
|
||||
| `meeting.room.ended` | EndRoom / 空房 TTL | room_code / reason |
|
||||
| `meeting.chat` | SendChat | message 对象 |
|
||||
### 帧格式
|
||||
|
||||
所有消息使用 `ws.Message` 统一信封(JSON):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| event | string | 事件名,如 `meeting.room.join` |
|
||||
| seq | int64 | 客户端自增序号;服务端 ACK 原样回传,用于客户端匹配请求-响应 |
|
||||
| data | object | 业务载荷 |
|
||||
| code | int | 仅 ACK / 广播带:0=成功,-1=业务失败 |
|
||||
| message | string | 仅 ACK:失败时的人类可读原因(与 REST 领域错误口径一致) |
|
||||
| time | string | ISO8601 时间戳 |
|
||||
|
||||
**ACK 约定**:每一个 C→S 事件服务端都会回发 `<event>.ack`,`seq` 与请求一致;成功时 `code=0`,失败时 `code=-1` 且 `message` 含中文原因(如 `仅主持人可执行此操作`、`会议不存在`、`你当前未在会议中`)。
|
||||
|
||||
**连接级鉴权**:Token 校验见 [websocket.md](websocket.md) 连接管理;会议内事件额外在每次调用时校验 `(user_id, room_code)` 是否为当前活跃参会者,非法请求全部以 ACK 形式返回 `-1`,不会触发 401/403 HTTP 错误。
|
||||
|
||||
### 事件总览
|
||||
|
||||
| # | 方向 | 事件 | 用途 | 服务端动作 |
|
||||
|---|------|------|------|-----------|
|
||||
| 1 | C→S | `meeting.room.join` | 前端建立 WS 后声明加入某会议房间频道 | 验证活跃参会,记录 WS 会话 |
|
||||
| 2 | C→S | `meeting.room.leave` | 前端主动离开 WS 会议频道 | 清理媒体资源(transport / producer / consumer) |
|
||||
| 3 | C→S | `meeting.member.state.changed` | 成员更新自己的麦克风/摄像头;host 可指定 `target_user_id` 强制静音他人 | 广播 S→C 同名事件,权限不符返回 ACK `code=-1` |
|
||||
| 4 | C→S | `meeting.transport.create` | 请求创建 mediasoup WebRtcTransport(send/recv) | 调用 `mediaOrchestrator.CreateTransport`,返回 `id/iceParameters/iceCandidates/dtlsParameters` |
|
||||
| 5 | C→S | `meeting.transport.connect` | 提交 DTLS Parameters 完成 transport 连接 | `mediaOrchestrator.ConnectTransport` |
|
||||
| 6 | C→S | `meeting.produce.start` | 创建 Producer(上行流) | `mediaOrchestrator.CreateProducer`,成功后广播 `meeting.member.producer.new` |
|
||||
| 7 | C→S | `meeting.consume.start` | 创建 Consumer(订阅对端 Producer) | `mediaOrchestrator.CreateConsumer` |
|
||||
| 8 | C→S | `meeting.producer.close` | 关闭自己的 Producer | `mediaOrchestrator.CloseProducer`,广播 `meeting.member.producer.new` (closed=true) |
|
||||
| 9 | S→C | `meeting.member.joined` | 新成员加入广播(复用 REST /join) | 房间广播 |
|
||||
| 10 | S→C | `meeting.member.left` | 成员离开广播(REST /leave /kick 或 WS leave) | 房间广播 |
|
||||
| 11 | S→C | `meeting.member.kicked` | 定向通知被踢者(REST /kick) | `PublishToUser` |
|
||||
| 12 | S→C | `meeting.host.changed` | 主持人变更 | 房间广播 |
|
||||
| 13 | S→C | `meeting.room.ended` | 会议被结束(REST /end 或空房 TTL) | 房间广播 |
|
||||
| 14 | S→C | `meeting.member.state.changed` | 成员状态变化(静音/关摄像头/举手) | 房间广播 |
|
||||
| 15 | S→C | `meeting.member.producer.new` | 成员开启/关闭媒体流 | 房间广播,`closed=true` 表示关闭 |
|
||||
| 16 | S→C | `meeting.chat` | 会议内聊天(REST /chats) | 房间广播 |
|
||||
|
||||
> 说明:客户端仅注册 **C→S 白名单**中的 8 个事件(见 `app/constants/meeting.go:MeetingWSClientEvents`),其余 `meeting.*` 事件若由客户端发送均被静默丢弃,防止恶意客户端伪造广播。
|
||||
|
||||
### 客户端白名单(C→S)详细契约
|
||||
|
||||
#### 1. `meeting.room.join`
|
||||
|
||||
**请求载荷**
|
||||
|
||||
```json
|
||||
{ "room_code": "835-000-036" }
|
||||
```
|
||||
|
||||
**ACK** `code=0`:`data` 为空对象;失败原因:`会议不存在` / `你当前未在会议中`。
|
||||
|
||||
**说明**:WS 加入仅用于开启该房间的事件推送通道,REST `/join` 已将 participant 写入 DB,此事件**不会再次修改 participant 表**。
|
||||
|
||||
#### 2. `meeting.room.leave`
|
||||
|
||||
**请求载荷**
|
||||
|
||||
```json
|
||||
{ "room_code": "835-000-036" }
|
||||
```
|
||||
|
||||
**ACK** `code=0`:`data` 为空对象;服务端同时发起媒体资源清理:按 Redis key `echo:meeting:resources:{room_id}:{user_id}` 中记录的 transport / producer / consumer 依次调用 `mediaOrchestrator.CloseTransport/CloseProducer/CloseConsumer`,随后删除该 key。
|
||||
|
||||
**说明**:此事件**不会**将 `participant.left_at` 置空,若用户希望真正离会仍需调用 REST `POST /rooms/:code/leave`。此设计允许客户端在 WS 重连时调用 leave + join 刷新资源而不退出会议。
|
||||
|
||||
#### 3. `meeting.member.state.changed`
|
||||
|
||||
**请求载荷**
|
||||
|
||||
```json
|
||||
{
|
||||
"room_code": "835-000-036",
|
||||
"target_user_id": 17,
|
||||
"audio_enabled": false,
|
||||
"video_enabled": true,
|
||||
"hand_raised": false
|
||||
}
|
||||
```
|
||||
|
||||
- `target_user_id` 可选:省略或等于自己时为自我操作,任何人均可;指定为他人时需调用方为 host,否则 ACK `code=-1 message=仅主持人可执行此操作`。
|
||||
- `audio_enabled / video_enabled / hand_raised` 均为可选布尔字段,至少传一个。
|
||||
|
||||
**ACK** `code=0`:`data` 为空。**广播** `meeting.member.state.changed`:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_code": "835-000-036",
|
||||
"user_id": 17,
|
||||
"audio_enabled": false,
|
||||
"video_enabled": true,
|
||||
"hand_raised": false,
|
||||
"actor_id": 16
|
||||
}
|
||||
```
|
||||
|
||||
`actor_id` 为触发变更的 user_id(host 强制静音场景下与 `user_id` 不同)。
|
||||
|
||||
#### 4. `meeting.transport.create`
|
||||
|
||||
**请求载荷**
|
||||
|
||||
```json
|
||||
{ "room_code": "835-000-036", "direction": "send" }
|
||||
```
|
||||
|
||||
`direction`:`"send"` 或 `"recv"`。
|
||||
|
||||
**ACK** `code=0` `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "transport-abc",
|
||||
"iceParameters": { ... },
|
||||
"iceCandidates": [ ... ],
|
||||
"dtlsParameters": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**资源追踪**:服务端将 `transport-id` 按 `echo:meeting:resources:{room_id}:{user_id}` 的 Redis Set 记录,TTL 1 小时。
|
||||
|
||||
#### 5. `meeting.transport.connect`
|
||||
|
||||
**请求载荷**
|
||||
|
||||
```json
|
||||
{
|
||||
"room_code": "835-000-036",
|
||||
"transport_id": "transport-abc",
|
||||
"dtls_parameters": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**ACK** `code=0`:`data` 为空。
|
||||
|
||||
#### 6. `meeting.produce.start`
|
||||
|
||||
**请求载荷**
|
||||
|
||||
```json
|
||||
{
|
||||
"room_code": "835-000-036",
|
||||
"transport_id": "transport-abc",
|
||||
"kind": "audio",
|
||||
"rtp_parameters": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
`kind`: `"audio"` | `"video"`。
|
||||
|
||||
**ACK** `code=0` `data`:
|
||||
|
||||
```json
|
||||
{ "producer_id": "producer-xyz" }
|
||||
```
|
||||
|
||||
**同时广播** `meeting.member.producer.new`:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_code": "835-000-036",
|
||||
"user_id": 16,
|
||||
"producer_id": "producer-xyz",
|
||||
"kind": "audio",
|
||||
"closed": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. `meeting.consume.start`
|
||||
|
||||
**请求载荷**
|
||||
|
||||
```json
|
||||
{
|
||||
"room_code": "835-000-036",
|
||||
"transport_id": "transport-recv",
|
||||
"producer_id": "producer-xyz",
|
||||
"rtp_capabilities": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**ACK** `code=0` `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "consumer-def",
|
||||
"producerId": "producer-xyz",
|
||||
"kind": "audio",
|
||||
"rtpParameters": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**资源追踪**:consumer-id 也会进入 Redis resources Set。
|
||||
|
||||
#### 8. `meeting.producer.close`
|
||||
|
||||
**请求载荷**
|
||||
|
||||
```json
|
||||
{ "room_code": "835-000-036", "producer_id": "producer-xyz" }
|
||||
```
|
||||
|
||||
**ACK** `code=0`:`data` 为空。**广播** `meeting.member.producer.new` `closed=true`:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_code": "835-000-036",
|
||||
"user_id": 16,
|
||||
"producer_id": "producer-xyz",
|
||||
"closed": true
|
||||
}
|
||||
```
|
||||
|
||||
### 服务端广播(S→C)详细契约
|
||||
|
||||
| 事件 | 载荷字段 | 说明 |
|
||||
|------|---------|------|
|
||||
| `meeting.member.joined` | room_code / participant 对象 | REST /join 触发 |
|
||||
| `meeting.member.left` | room_code / user_id / reason | REST /leave /kick 或 WS 资源清理 |
|
||||
| `meeting.member.kicked` | room_code / reason | 仅发给被踢者本人,对应 REST /kick |
|
||||
| `meeting.host.changed` | room_code / old_host_id / new_host_id | REST /transfer-host 或 host 离会自动转让 |
|
||||
| `meeting.room.ended` | room_code / reason(`host_ended` / `empty_ttl`) | REST /end 或空房 TTL |
|
||||
| `meeting.member.state.changed` | 见白名单 #3 | WS `meeting.member.state.changed` 触发 |
|
||||
| `meeting.member.producer.new` | 见白名单 #6 / #8 | WS `meeting.produce.start` / `meeting.producer.close` 触发 |
|
||||
| `meeting.chat` | message 对象 | REST /chats 触发 |
|
||||
|
||||
### 架构
|
||||
|
||||
- **MeetingWSHandler**(controller 层):thin adapter,仅负责 ws.Hub 事件注册 + JSON 反序列化 + ACK 回写;位于 `app/meeting/controller/meeting_ws_handler.go`。
|
||||
- **MeetingSignalService**(service 层):承载 8 个 C→S 事件的业务逻辑(活跃参会校验、host 权限校验、mediaOrchestrator 调用、Redis 资源追踪、广播),位于 `app/meeting/service/meeting_signal_service.go`。
|
||||
- **MeetingBroadcaster**(service 层):封装 `BroadcastToMeeting`(查询活跃 participant 列表 → 逐个 `PubSub.PublishToUser`)与 `PublishToUser`,供 REST / WS 两个入口统一使用,位于 `app/meeting/service/meeting_broadcaster.go`。
|
||||
- **MediaOrchestrator**(interface):定义 9 个 mediasoup 操作方法;Task 6 使用 `NoopMediaOrchestrator` 占位(返回 stub IDs),Task 7 替换为 `HTTPMediaOrchestrator` 对接 Node media-server。
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 鉴权失败(token 无效 / 未传):WS 握手时直接 4001 关闭,不进入业务层。
|
||||
- 业务失败(非参会者 / 非 host / 会议已结束):ACK `code=-1` + 中文 `message`,与 REST 领域错误口径完全一致。
|
||||
- mediasoup 调用失败:`message` 形如 `mediaOrchestrator error: <原始错误>`,前端可据此降级。
|
||||
- WS 连接断开:服务端 `OnDisconnect` 钩子会对该用户所有活跃会议调用 `cleanupUserResources`(遍历 Redis `echo:meeting:resources:*:{user_id}` 完成 transport / producer / consumer 关闭),防止服务端资源泄漏。
|
||||
|
||||
---
|
||||
|
||||
## 验证记录
|
||||
|
||||
Task 5 端到端验证脚本(`/tmp/meeting_t5_test.sh`)结果:**19/19 PASS**,覆盖 12 接口的 happy path 与 5 类错误路径(密码错误 / 房间不存在 / 单点参会冲突 / 非 host 越权 / 邀请链接失效)。
|
||||
- **Task 5** 端到端验证脚本(`/tmp/meeting_t5_test.sh`)结果:**19/19 PASS**,覆盖 12 接口的 happy path 与 5 类错误路径(密码错误 / 房间不存在 / 单点参会冲突 / 非 host 越权 / 邀请链接失效)。
|
||||
- **Task 6** 端到端 WS 测试脚本(`/tmp/meeting_ws_t6_test.mjs`)结果:**18/18 PASS**,覆盖:
|
||||
- `meeting.room.join` 双端 ACK
|
||||
- `meeting.member.state.changed` 自我静音(ACK + 对端广播)
|
||||
- host 强制静音他人(ACK)
|
||||
- 非 host 强制静音他人 → ACK `code=-1 message=仅主持人可执行此操作`
|
||||
- `meeting.transport.create` 双方向(send / recv)
|
||||
- `meeting.transport.connect`
|
||||
- `meeting.produce.start` + `meeting.member.producer.new` 广播
|
||||
- `meeting.consume.start`
|
||||
- `meeting.producer.close` + `meeting.member.producer.new closed=true` 广播
|
||||
- `meeting.room.leave` + `meeting.member.left` 广播
|
||||
- 不存在会议号 `meeting.room.join` → ACK `code=-1`
|
||||
|
||||
---
|
||||
|
||||
## 后续任务关联
|
||||
|
||||
- **Task 6**:WebSocket 信令协议(`meeting.*` 事件、mediasoup transport/producer/consumer 流转)
|
||||
- **Task 7**:Go → Node HTTP 客户端,将 `NoopMediaOrchestrator` 替换为 `HTTPMediaOrchestrator`,接入真实 mediasoup Router
|
||||
- **Task 13**:通知卡片 UI 补齐 `meeting_invite` 内联按钮
|
||||
- **Task 7**:Go → Node HTTP 客户端,将 `NoopMediaOrchestrator` 替换为 `HTTPMediaOrchestrator`,接入真实 mediasoup Router,此时 WS 白名单事件契约与本文档完全不变,仅 `iceParameters / producer_id` 等字段由 stub 变为真实值。
|
||||
- **Task 8**:Vue 前端 mediasoup-client 接入,按本文 WS 契约实现 `mediasoup.Transport` 的 `connect / produce` 回调。
|
||||
- **Task 13**:通知卡片 UI 补齐 `meeting_invite` 内联按钮。
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
> **上级路线图:** [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md)
|
||||
> **分支:** `feature/phase2e-2-meeting-mvp`
|
||||
> **预估总工时:** **约 17 人日**(17 个 Task,含 PoC 与 UI 打磨)
|
||||
> **最后更新:** 2026-04-21(Task 0-5 ✅ 已落地,下一步 Task 6 WS 信令)
|
||||
> **最后更新:** 2026-04-21(Task 0-6 ✅ 已落地,下一步 Task 7 HTTPMediaOrchestrator)
|
||||
|
||||
---
|
||||
|
||||
@@ -303,22 +303,29 @@ flowchart LR
|
||||
- **错误码中文化**:使用中文 `message`(与项目惯例一致)而非英文 `meeting_not_found` code,前端通过 HTTP 状态码 + trace_id 区分
|
||||
- **工作量**:**实际 1 人日**(< 预估 1.5 人日,因 DTO 设计充分 + DAO 契约修复一次到位)
|
||||
|
||||
### Task 6:WS 信令 11 事件处理器
|
||||
### Task 6:WS 信令 13 事件处理器 ✅ **已完成(2026-04-21)**
|
||||
|
||||
- **目标**:实现设计 §6.3 的 11 个 WS 事件,完整对接到 `ws.Hub`
|
||||
- **依赖**:T4
|
||||
- **主要产出**:
|
||||
- `app/meeting/controller/meeting_ws_handler.go`:注册事件回调到 Hub
|
||||
- `app/meeting/service/meeting_signal_service.go`:3 组事件(房间 / 成员 / 媒体)的业务逻辑
|
||||
- `app/ws/hub.go` 接口扩展:`MeetingSignalDispatcher` 接口注入 + `DispatchMeeting(event, payload)` 方法
|
||||
- `app/meeting/constants/ws_events.go`:11 个事件名常量
|
||||
- 权限校验:所有事件 handler 入口调用 `assertIsParticipant` / `assertIsHost`
|
||||
- 广播:`Hub.BroadcastToMeeting(roomCode, event, payload, excludeUserID)` 辅助方法
|
||||
- **检查点**:
|
||||
- 通过 `wscat` 或临时前端脚本连入 WS,逐个事件手测
|
||||
- 未授权事件(非参与者发 `meeting.member.state.changed`)被拒绝
|
||||
- 事件广播覆盖正确(excludeUserID 生效)
|
||||
- **工作量**:**1.5 人日**
|
||||
- **目标**:实现设计 §6.3 的 WS 事件族(最终落地 13 个 = 3 房间 + 5 成员 + 5 媒体),完整对接到 `ws.Hub`
|
||||
- **依赖**:T4 ✅
|
||||
- **实际产出**:
|
||||
- `app/constants/meeting.go`(改):WS 事件常量与设计 §6.3 对齐 + `MeetingWSClientEvents` 白名单切片(8 个 C→S 事件)
|
||||
- `app/meeting/service/interfaces.go`(重构):`MediaOrchestrator` 扩容至 9 方法 + 5 DTO + `NoopMediaOrchestrator` 9 占位实现
|
||||
- `app/meeting/service/meeting_broadcaster.go`(新建,75 行):统一广播层 `BroadcastToMeeting` + `PublishToUser`,REST / WS 共用
|
||||
- `app/meeting/service/meeting_service.go`(重构):12 REST 方法改调 `broadcaster.*`,不再直连 `ws.PubSub`
|
||||
- `app/meeting/service/meeting_signal_service.go`(新建,430 行):8 C→S 事件业务 + Redis 资源追踪 + 资源清理 + host 权限校验
|
||||
- `app/meeting/controller/meeting_ws_handler.go`(新建,200 行):薄层 controller,`hub.RegisterEvent` 注册 + JSON 反序列化 + ACK
|
||||
- `app/meeting/provider.go` + `app/provider/provider.go`(改):Wire 挂入新 3 个 provider
|
||||
- `docs/api/frontend/meeting.md`(追加 2 节 +200 行):§WebSocket 信令协议(Task 6) + 验证记录
|
||||
- **实际检查点**:
|
||||
- `/tmp/meeting_ws_t6_test.mjs` 端到端 **18/18 PASS**:8 C→S 白名单事件 + 3 S→C 广播 + 3 类错误路径(非 host 越权/不存在会议号/WS leave 资源清理)
|
||||
- 非 host 尝试 `meeting.member.state.changed` 修改他人 → ACK `code=-1 message=仅主持人可执行此操作`
|
||||
- `meeting.room.leave` → 自动触发 `mediaOrchestrator.Close*` 清理 Redis 资源 Set
|
||||
- `go build` / `go vet` / `wire` 全绿
|
||||
- **偏离与说明**:
|
||||
- 最终事件数从实施计划的 11 个扩展为 13 个(+ 2 个业务补充事件:`meeting.chat.message` + `meeting.member.producer.new`),与设计文档 §6.3 一致
|
||||
- 权限校验入口改为在 `MeetingSignalService.On*` 方法内部调用 `assertIsActiveParticipant` / `assertIsHost`,不再依赖 handler 层前置断言,代码更易测试
|
||||
- 广播 API 统一为 `MeetingBroadcaster.BroadcastToMeeting`(含 `excludeUserIDs ...int64` 可变参数),比计划中"`Hub.BroadcastToMeeting` 方法" 更内聚,不污染 `ws.Hub` 通用接口
|
||||
- **实际工作量**:**1 人日**(比预估 1.5 人日节省,得益于 Task 5 已预置好 DAO / 错误链 / DTO)
|
||||
|
||||
### Task 7:Go → Node HTTP Client 封装
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# EchoChat 项目开发进度
|
||||
|
||||
> **最后更新**:2026-04-21(Phase 2e-2 Task 5 Go meeting 模块 12 个 REST 接口业务逻辑全量落地,端到端 19/19 PASS)
|
||||
> **当前阶段**:Phase 2e-2 会议 MVP **代码开发阶段** 🚧(Task 0-5 ✅ / Task 6-16 待执行)
|
||||
> **最后更新**:2026-04-21(Phase 2e-2 Task 6 WebSocket 信令协议落地,13 个 meeting.* 事件全量打通,端到端 18/18 PASS)
|
||||
> **当前阶段**:Phase 2e-2 会议 MVP **代码开发阶段** 🚧(Task 0-6 ✅ / Task 7-16 待执行)
|
||||
> **当前分支**:`feature/phase2e-2-meeting-mvp`(从 `feature/phase2c-group-read-receipt` 衍生)
|
||||
> **Phase 2e 整体设计**:`docs/plans/2026-04-20-phase2e-design.md`(三子阶段路线图 + 后续规划清单)
|
||||
> **Phase 2e-1 专用设计**:`docs/plans/2026-04-20-phase2e-1-design.md`(✅ 已完成)
|
||||
@@ -170,6 +170,63 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚀 2026-04-21 Phase 2e-2 Task 6 WebSocket 信令协议(13 事件)落地
|
||||
|
||||
**交付**:`meeting.*` 事件族从 Task 5 的 `PublishToUser` 循环升级为完整的 WS 信令协议;新建 `MeetingBroadcaster`(统一广播层)、`MeetingSignalService`(8 个 C→S 事件业务逻辑 + 资源追踪)、`MeetingWSHandler`(controller 薄层),`MediaOrchestrator` 接口扩容至 9 个方法覆盖 mediasoup 全生命周期(Task 7 真实实现前由 `NoopMediaOrchestrator` 占位);端到端 WS 冒烟脚本 `/tmp/meeting_ws_t6_test.mjs` **18/18 PASS**,覆盖 8 C→S 白名单事件 + 3 S→C 广播 + 3 类错误路径。
|
||||
|
||||
### 产出文件
|
||||
|
||||
| 文件 | 行数 | 作用 |
|
||||
|---|---|---|
|
||||
| `backend/go-service/app/constants/meeting.go`(改) | +25 | WS 事件常量与设计 §6.3 对齐(3 房间 + 5 成员 + 5 媒体 + 1 聊天),新增 `MeetingWSClientEvents` 白名单切片限制客户端只能发起 8 个 C→S 事件 |
|
||||
| `backend/go-service/app/meeting/service/interfaces.go`(重构) | 180 | `MediaOrchestrator` 扩容到 9 方法(Router/Transport/Producer/Consumer 全生命周期)+ 配套 DTO(`TransportInfo` / `ConsumerInfo` / `CreateTransportReq` / `CreateProducerReq` / `CreateConsumerReq`)+ `NoopMediaOrchestrator` 9 个占位实现(stub ID + 最小 JSON) |
|
||||
| `backend/go-service/app/meeting/service/meeting_broadcaster.go`(新) | 75 | `MeetingBroadcaster`:`BroadcastToMeeting`(查询活跃 participant → 批量 `PubSub.PublishToUser` + 可选 exclude)+ `PublishToUser`(定向推送)+ 并发安全的错误汇集 |
|
||||
| `backend/go-service/app/meeting/service/meeting_service.go`(改) | ±30 | 12 个 REST 方法重构:统一改为调用 `broadcaster.BroadcastToMeeting` / `broadcaster.PublishToUser`,移除直连 `ws.PubSub` 依赖,代码量精简约 15% |
|
||||
| `backend/go-service/app/meeting/service/meeting_signal_service.go`(新) | 430 | `MeetingSignalService` 8 个 C→S 事件(`OnRoomJoin`/`OnRoomLeave`/`OnMemberStateChanged`/`OnTransportCreate`/`OnTransportConnect`/`OnProduceStart`/`OnConsumeStart`/`OnProducerClose`)+ Redis 资源追踪 `echo:meeting:resources:{room_id}:{user_id}`(Set 结构,TTL 1 小时)+ `cleanupUserResources`(WS 断开钩子调用)+ host 权限校验(非 host 改他人状态返回 `仅主持人可执行此操作`) |
|
||||
| `backend/go-service/app/meeting/controller/meeting_ws_handler.go`(新) | 200 | `MeetingWSHandler` 薄层:构造时调用 `hub.RegisterEvent` 注册 8 C→S 事件,每个 handler 仅负责 JSON 反序列化 + 调 `signalSvc.On*` + 构造 ACK(`code=0/-1` + `message`)|
|
||||
| `backend/go-service/app/meeting/provider.go`(改) | +4 | `MeetingSet` 补全 `NewMeetingBroadcaster` / `NewMeetingSignalService` / `NewMeetingWSHandler` |
|
||||
| `backend/go-service/app/provider/{provider,wire_gen}.go`(改) | +8 | `App` 结构体新增 `MeetingSignalService` / `MeetingWSHandler` 字段,`wire` 重新生成 |
|
||||
| `docs/api/frontend/meeting.md`(追加 2 节) | +200 | 新增 §WebSocket 信令协议(Task 6):16 事件总览表 + 8 C→S 事件完整请求/ACK/广播契约 + S→C 广播契约 + 架构说明 + 错误处理表;§验证记录补充 Task 6 结果 |
|
||||
|
||||
### 8 C→S 白名单事件契约
|
||||
|
||||
| 事件 | 入参关键字段 | ACK data | 副作用 |
|
||||
|------|-------------|---------|--------|
|
||||
| `meeting.room.join` | room_code | `{}` | 校验活跃参会记录 |
|
||||
| `meeting.room.leave` | room_code | `{}` | 清理该用户所有 transport/producer/consumer |
|
||||
| `meeting.member.state.changed` | room_code, [target_user_id], audio_enabled?, video_enabled?, hand_raised? | `{}` | 广播 `meeting.member.state.changed`;非 host 改他人 → `-1` |
|
||||
| `meeting.transport.create` | room_code, direction(send/recv) | `{id, iceParameters, iceCandidates, dtlsParameters}` | 资源追踪 Redis set |
|
||||
| `meeting.transport.connect` | room_code, transport_id, dtls_parameters | `{}` | mediasoup connect |
|
||||
| `meeting.produce.start` | room_code, transport_id, kind, rtp_parameters | `{producer_id}` | 广播 `meeting.member.producer.new`;资源追踪 |
|
||||
| `meeting.consume.start` | room_code, transport_id, producer_id, rtp_capabilities | `{id, producerId, kind, rtpParameters}` | 资源追踪 |
|
||||
| `meeting.producer.close` | room_code, producer_id | `{}` | 广播 `meeting.member.producer.new closed=true` |
|
||||
|
||||
### 验证执行(Node.js + ws)
|
||||
|
||||
1. `go build ./...` / `go vet ./...` / `wire ./app/provider` 全绿
|
||||
2. 启动 server,跑 `/tmp/meeting_ws_t6_test.mjs`(双用户 + WS_TRACE 模式)
|
||||
3. 用例清单(18 个全部 PASS):
|
||||
- 房间/成员:`room.join` 双端 ACK、`state.changed` 自我静音 ACK + 对端广播、host 强制静音 ACK、**非 host 强制静音他人 → `-1 仅主持人可执行此操作`**
|
||||
- 媒体:`transport.create` send/recv 双向、`transport.connect`、`produce.start` ACK + `producer.new` 广播、`consume.start`、`producer.close` ACK + `producer.new closed=true` 广播
|
||||
- 离会/错误:`room.leave` + `member.left` 对端广播、不存在会议号 `room.join` → `-1`
|
||||
4. 测试期间修复 waitEvent 死循环 bug(msgQueue pop→push 自循环导致 ack 永远等不到)→ 改为 stash 临时缓冲区,完成后统一归还
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
1. **Broadcaster 单独抽层**:避免 service 方法散落直接 `PubSub.PublishToUser`,后续接入 Redis Cluster / 切换广播实现只需改一个文件;同时 Task 5 的 REST 广播与 Task 6 的 WS 事件广播完全复用同一个对象。
|
||||
2. **C→S 白名单机制**:`app/constants/meeting.go:MeetingWSClientEvents` 列出 8 个允许客户端发起的事件,`ws.Hub` 在分发前先过滤,防止恶意客户端直接发 `meeting.room.ended` 伪造房间结束。
|
||||
3. **资源追踪用 Redis Set**:每创建一个 transport/producer/consumer 都 `SADD echo:meeting:resources:{room_id}:{user_id} <resource_id>`,WS 断开或 room.leave 时 `SMEMBERS` 遍历清理;TTL 1 小时防止遗留占用,即使 Go 进程崩溃也不会泄漏 mediasoup 资源。
|
||||
4. **MediaOrchestrator 先抽 9 方法再实现**:Task 6 仍用 `Noop` 占位,但接口已完整定义 `CreateRouter / CloseRouter / CreateTransport / ConnectTransport / CreateProducer / CloseProducer / CreateConsumer / CloseConsumer`(外加 DTO 型号),Task 7 只需替换绑定即可让 WS 端变为真实 mediasoup,**无需修改 signal service / handler 代码**。
|
||||
5. **ACK `code` 语义统一**:成功 `0`、业务失败 `-1`(+ 中文 message),与 REST 领域错误口径完全一致,前端可直接复用一套 error toast 组件。
|
||||
6. **`meeting.room.leave` 只清 WS 资源不改 participant 表**:真正离会需 REST `/leave`(会影响 duration / host 自动转让);此设计允许客户端 WS 重连时发 leave+join 刷新 transport 而不退会。
|
||||
|
||||
### 下一步
|
||||
|
||||
- **Task 7**(1.5 人日):`HTTPMediaOrchestrator` 实现 — Go 调 Node media-server 9 个内部 REST API(Task 2 已全部跑通),替换 `NoopMediaOrchestrator`,前后端 WS 契约完全不变。
|
||||
- **Task 8**(2 人日):Vue 前端 mediasoup-client 接入 + 会议室页面骨架,按本次 §WebSocket 信令协议契约实现 `Transport.connect / produce` 回调。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 2026-04-21 Phase 2e-2 Task 5 Go meeting 模块 12 个 REST 接口业务逻辑全量落地
|
||||
|
||||
**交付**:`MeetingService` 12 个业务方法 + `MeetingController` 12 个 Gin 处理器从 501 占位升级为真实实现,完整的领域错误码映射、DTO 绑定、DAO 契约修复、权限辅助函数;端到端验证脚本 `/tmp/meeting_t5_test.sh` **19/19 PASS**,覆盖 12 接口 happy path + 5 类错误路径;`go build ./...` / `go vet ./...` / `wire ./app/provider` 全绿。
|
||||
|
||||
Reference in New Issue
Block a user