Files
EchoChat/test-report-phase2e-2-meeting.md
bujinyuan 2452ca7332 docs(phase2e-2): Task 16 收官 · 同步 6 份文档 + 落盘 test-report-phase2e-2-meeting.md
Phase 2e-2 会议 MVP 整体切 (Task 0-16 全部完成),本提交是 Task 16 最后一步
"文档同步"批次,配合历次 commit(cdaa39d P0 / ea2bf96 P1 / f5ae095 资源生命周期 /
5ed14c2 E2E 剧本 / c35097a P2+Nit)完成阶段收官。

同步范围:
- docs/progress/CURRENT_STATUS.md
  · 顶部元信息:"当前阶段"从"代码开发阶段 🚧"切为"已完成 "
  · 新增 2026-04-24 Task 16 收官小节:资源生命周期专项 / Playwright 5 场景 /
    P2+Nit 收尾 / 完成度汇总(Task 0-16 + 审查 4 P0 + 8 P1 + 7 P2 + 7 Nit)
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md
  · 状态切 ;Task 16 行切  + 实际交付小节(5 commits + 5 场景剧本 + 6 份文档)
- docs/plans/2026-04-20-phase2e-design.md
  · Phase 2e 顶层状态:2e-2 切 ;最后更新写明 Task 16 收官
- docs/reviews/2026-04-23-phase2e-2-code-review.md
  · (历次 commit c35097a 已追加)"Task 16 修复追踪"小节包含已修复表 + 推迟登记表
- .cursor/rules/project-context.mdc
  · Phase 2e 进度条更新;新增 Task 15 / 16 两个完整 bullet(Task 16 含 5 commit 详解)
- test-report-phase2e-2-meeting.md(新增)
  · 八章完整验证报告:测试环境 / 构建 / 功能 / E2E 剧本 / 代码审查修复 /
    部署双态 / 已知限制 / 验收结论

构建&清理复核(均在 Task 16 历次 commit 中验证通过):
- go vet ./... / go build ./... / npm run build:h5 / npx tsc --noEmit 全绿
- console.log / TODO / FIXME 扫描 0 残留

Made-with: Cursor
2026-04-23 17:53:54 +08:00

278 lines
17 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.

# Phase 2e-2 会议 MVP · 测试验证报告
> 创建日期2026-04-24
> 分支:`feature/phase2e-2-meeting-mvp`(基于 `feature/phase2c-group-read-receipt` 衍生)
> 范围Phase 2e-2 会议 MVP 全部 17 个 Task0-16覆盖 mediasoup `media-server` + Go meeting 模块 + 前端会议主链路 + 代码审查 4 P0 / 8 P1 / 7 P2 / 7 Nit 修复闭环
> 相关文档:`docs/plans/2026-04-21-phase2e-2-design.md`、`docs/plans/2026-04-21-phase2e-2-implementation.plan.md`、`docs/reviews/2026-04-23-phase2e-2-code-review.md`、`.playwright-mcp/task16/scenarios/`
---
## 一、测试环境
| 组件 | 版本/路径 | 启动方式 |
|---|---|---|
| Backend Go 服务 | `backend/go-service/` | `go run cmd/server/main.go`(端口 8085 |
| Node Media 服务 | `media-server/` | `npm run dev`(端口 3300mediasoup RTC 40000-40199 UDP/TCP |
| Frontend 用户端 | `frontend/`uni-app H5 | `npm run dev:h5`HTTPS + 自签证书getUserMedia 要求 secure context |
| PostgreSQL | 14+ | `deploy/docker/postgres/{init.sql,phase2e2_migration.sql}` 初始化 |
| Redis | 7+ | JWT Token / WS Pub/Sub / 会议生命周期 key / 聊天限流 / 资源追踪 |
| MinIO | 8.0+ | 对象存储(本阶段仅作依赖启动,未新增 bucket 使用) |
| coturn | 4.6+(可选) | `docker compose --profile public up -d coturn` 公网部署态启用 |
> DDL`meeting_rooms` / `meeting_participants` / `meeting_chats` 三张表 + `meeting_invite_redemptions` 辅助(见 `phase2e2_migration.sql`)。
> 媒体端口mediasoup WebRTC 40000-40199 UDP+TCPcoturn STUN/TURN 3478 UDP+TCP + TLS 5349 + relay 49152-65535 UDP。
---
## 二、构建验证
### 2.1 Go 服务 ✅
```bash
cd backend/go-service && go vet ./... && go build ./...
# 输出:无报错
```
Wire 注入链路已同步:`ws.ProvideWSHandler` 新增 `*config.ServerConfig` 入参CheckOrigin 白名单需要),`provider.go` 新增 `provideServerConfig` 并注册到 `InfraSet``wire_gen.go` 重生成通过。
### 2.2 Node media-server ✅
```bash
cd media-server && npx tsc --noEmit
# 输出:无报错
```
`DELETE /internal/v1/transports/:id` 新路由transport 闭环清理)+ `internal-auth` isPrivatePath 按 path 匹配(剔除 query/hash均通过类型检查。
### 2.3 前端 H5 构建 ✅
```bash
cd frontend && npm run build:h5
# 输出DONE Build complete.(仅遗留 uni-app legacy JS API / dynamic import 历史告警0 本阶段新增 warning
```
### 2.4 遗留 console / TODO / FIXME 扫描 ✅
```bash
rg -n 'console\.log' frontend/src | wc -l # 9全部为 WS 生命周期日志入口 + 默认 logger 包装)
rg -n 'TODO|FIXME' backend/go-service/app/meeting media-server/src frontend/src/pages/meeting
# 0
```
---
## 三、功能验证清单
### 3.1 会议核心 REST API12 个接口)
| # | 接口 | 方法 | 验收点 | 状态 |
|---|---|---|---|---|
| 1 | `POST /api/v1/meeting/rooms` | 发起人 | 9 位会议号 + `password_hash`bcrypt+ `router_id/rtp_capabilities` 返回 | ✅ |
| 2 | `GET /api/v1/meeting/rooms/:code` | 任一登录用户 | 房间信息 + host 信息 + 成员数量 | ✅ |
| 3 | `GET /api/v1/meeting/rooms/:code/participants` | 成员 | 活跃成员列表(含 role | ✅ |
| 4 | `POST /api/v1/meeting/rooms/:code/join` | 用户 | 密码校验 + 单点参会 + Router 复用 | ✅ |
| 5 | `POST /api/v1/meeting/rooms/:code/leave` | 成员 | host 离会自动转让最早加入者 + 空房转 empty_ttl | ✅ |
| 6 | `POST /api/v1/meeting/rooms/:code/end` | host | 事务 + `SELECT FOR UPDATE` 快照活跃成员 + `MarkEnded` + `LeaveAllActive`P1-4 修复) | ✅ |
| 7 | `POST /api/v1/meeting/rooms/:code/transfer-host` | host | 事务合并 `role` + `host_id` 双更新P1-1 修复) | ✅ |
| 8 | `POST /api/v1/meeting/rooms/:code/kick` | host | 广播 `meeting.member.kicked` + 被踢者 `lastEndedReason=kicked` | ✅ |
| 9 | `POST /api/v1/meeting/rooms/:code/chats` | 成员 | 服务端 utf8 500 字符上限 + Redis INCR 30 条/60s 限流P2-7 修复) | ✅ |
| 10 | `GET /api/v1/meeting/rooms/:code/chats?before=&limit=` | 成员 | 游标分页P1-2 修复:由 GORM 直查改走 DAO | ✅ |
| 11 | `POST /api/v1/meeting/rooms/:code/invites` | host | 邀请 token 仅走 NotifyPusher 下发,不回写调用方 | ✅ |
| 12 | `POST /api/v1/meeting/invite-tokens/:token/redeem` | 用户 | Redis TTL 600s + `expired_at` 随 payload 下发 | ✅ |
| 附 | `GET /api/v1/meeting/rooms/mine` | 用户 | `meeting_rooms` JOIN `meeting_participants` + DISTINCT + 游标分页P1-3 修复N+1 消除) | ✅ |
### 3.2 WebSocket 信令协议14 事件)
| 方向 | 事件 | 验收点 | 状态 |
|---|---|---|---|
| C→S | `meeting.room.join` | ACK + 同步补推 existing producers / member statesP1-8 修复:改为 join 返回前补推) | ✅ |
| C→S | `meeting.room.leave` | 仅清 media 资源,不删 participant 行 | ✅ |
| C→S | `meeting.member.state.changed` | 支持 `target_user_id` 主持人静音他人host-only 校验) | ✅ |
| C→S | `meeting.transport.create` / `.connect` | 归属校验走 Redis SetP0-1 修复) | ✅ |
| C→S | `meeting.produce.start` | Redis Set 追加 `producer:<id>` 便于清理 | ✅ |
| C→S | `meeting.consume.start` / `.resume` | transport_id 归属校验P0-2 修复) | ✅ |
| C→S | `meeting.producer.close` | 只能关自己的 producerP0-1 修复) | ✅ |
| S→C | `meeting.member.joined` / `.left` / `.kicked` | 广播时机与 REST 顺序归一 | ✅ |
| S→C | `meeting.member.state.changed` | 含自己广播,确保 history 回放正确 | ✅ |
| S→C | `meeting.member.producer.new` | `closed` 布尔字段驱动前端 `_onProducerNew` | ✅ |
| S→C | `meeting.host.changed` | 主动/被动转让统一口径 | ✅ |
| S→C | `meeting.room.ended` | `reason` 映射前端 `MEETING_ENDED_REASON_LABEL`P2-8 修复:补 `kicked` | ✅ |
### 3.3 会议生命周期状态机Task 8
| 场景 | 预期 | 状态 |
|---|---|---|
| host WS 掉线 120s 内重连 | `host_grace` key 存在local timer + Redis key 双保险取消 | ✅ |
| host WS 掉线超过 120s | `HandleHostGraceExpired` 独立 SETNX 锁抢占P1-7 修复)+ 自动转让最早加入者 | ✅ |
| 全员离开后 300s 空房 | `empty_ttl` TTL 过期 → `HandleEmptyRoomExpired``MarkEnded(empty_ttl_expired)` + 清 Redis 残留 | ✅ |
| 空房 TTL 期间有人入会 | `CancelEmptyTTL` 撤销定时器Router 复用 | ✅ |
| 4 小时 stale 活跃房 | `MeetingCleanupTask` 扫表 `MarkEnded(system_error)` | ✅ |
| go-service 重启 | `RescheduleFromRedis` 按 PTTL 重建 local timer | ✅ |
### 3.4 前端主链路Task 9-15
| 模块 | 验收点 | 状态 |
|---|---|---|
| `utils/mediasoup-client.js` | Device/Transport/Producer/Consumer 全生命周期 + `ensureSendTransport/ensureRecvTransport` in-flight Promise 锁 + `markRaw` 防代理 | ✅ |
| `store/meeting.js` | 本地状态机 + 14 WS 事件桥 + 20+ action + `_reset` 清空所有 Map/Set + `_pendingBroadcastTimers` 集中清理 | ✅ |
| `pages/meeting/preview.vue` | 设备预览 + `previewSeq` 序号防竞态 + 200ms 切换防抖P2-2 修复) | ✅ |
| `pages/meeting/room.vue` | 会议室主页 + 6 项原创 UI + `onLoad` redirectTo 后 `return`P2-3 修复) | ✅ |
| `pages/meeting/join.vue` + `preview.vue` | 密码 in-memory storeP0-4 修复:不再拼 URL query | ✅ |
| `components/meeting/MemberPanel.vue` | 主持人四件套(请他静音/开麦/转让/踢出) | ✅ |
| `components/meeting/SelfVideoFloat.vue` | 桌面 280×180 恒浮窗 + 四角吸附 + 图钉切换 | ✅ |
| `components/meeting/NetworkBadge.vue` | 3 条 SVG 波浪 + 相位错开 + `@keyframes wave-flow` | ✅ |
| `components/meeting/ChatPanel.vue` | 会议内聊天 + WS ACK Promise 化 + REST 列表游标分页 | ✅ |
---
## 四、Playwright MCP E2E 剧本
位置:`.playwright-mcp/task16/scenarios/`
| 编号 | 文件 | 覆盖要点 |
|---|---|---|
| 01 | `01-create-and-join-by-code.md` | 发起会议 + 会议号加入 + 互推音视频 + 结束会议 |
| 02 | `02-invite-link-with-password.md` | 邀请链接 + 密码入会 + 密码错 / 会议已结束边界 |
| 03 | `03-notify-quickjoin.md` | 通知中心"立即加入"一键入会(`meeting_invite` 卡片) |
| 04 | `04-host-toolkit-and-transfer.md` | 主持人四件套 + 主动转让 + 掉线 120s 被动自动转让 |
| 05 | `05-reopen-5times-resource-cleanup.md` | 连续 5 次开/散会压测 Redis `resourceTrackKey` + mediasoup Router + 定时器无泄漏 |
| 附 | `manual-regression.md` | 纯手工验收点(浏览器权限/移动端横竖屏/设备热插拔) |
**回归执行方式**:本地启动 `docker compose up -d``media-server npm run dev``backend go run cmd/server/main.go``frontend npm run dev:h5`HTTPS 模式) → 两浏览器分别登录两账号 → 按剧本逐步点击验证。剧本支持直接交给 Playwright MCP 驱动半自动回归。
**Task 15 已归档的 7 屏截图回归**`.playwright-mcp/task15/{01..07}-*.png`(首页 / 设备预览 / 空场浮窗+波浪 / 单人静音 / 流光+柔性 3-tri+氛围色 / 图钉切回 4 人网格 / 主持人菜单条件渲染),作为 UI 视觉基线保留。
---
## 五、代码审查修复验证Task 16 核心交付)
### 5.1 code-reviewer 全栈审计
- 调用 `code-reviewer` 子代理对 Go meeting 模块 + media-server + frontend meeting + Task 15 三轮回归补丁做一次重写版全栈审计
- 输出:`docs/reviews/2026-04-23-phase2e-2-code-review.md`4 P0 + 8 P1 + 8 P2 + 11 Nit
- 亮点章节Redis Hash `member_state` 补推机制、`HTTPMediaOrchestrator` `sync.Map + LoadOrStore` 幂等、mediasoup-client `ensureSendTransport/ensureRecvTransport` in-flight 锁、`MeetingCleanupTask` 双保险兜底
### 5.2 修复闭环26 项)
**commit `cdaa39d` · P0 × 4**
| ID | 问题 | 修复 |
|---|---|---|
| P0-1 | `OnProducerClose` 不校验 producer 归属 | 新增 `assertOwnsResource`Redis Set `echo:meeting:res:<room>:<user>`)集成 5 个 C→S 入口 |
| P0-2 | `OnConsumeStart` 不校验 `transport_id` 归属 | 同 P0-1 复用 |
| P0-3 | `CreateRoom` Router 失败仅 Warn 继续 | fail-closed`LeaveRoom` + `MarkEnded(system_error)` 补偿 + `ErrMediaServiceUnavailable` HTTP 500 |
| P0-4 | 加入密码明文拼 URL | `meetingStore.draftJoinPayload = { code, password }` in-memory 传递 + preview 读取后立即置空 |
**commit `ea2bf96` · P1 × 8**
| ID | 问题 | 修复 |
|---|---|---|
| P1-1 | 主持人转让非原子两事务 | DAO `TransferHost` 事务合并 `role` + `host_id`service/lifecycle 移除冗余 `UpdateHost` |
| P1-2 | `ListChatMessages` / `ListMyMeetings` 绕 DAO | 新增 `MeetingChatDAO.ListByRoomBefore`(反向游标) |
| P1-3 | `ListMyMeetings` N+1 放大 6 倍 | `ListJoinedRoomsByUser` JOIN + DISTINCT + 游标 |
| P1-4 | `EndRoom` 无行锁幽灵参会者 | 事务 + `SELECT FOR UPDATE` + 快照 + `MarkEnded` + `LeaveAllActive` |
| P1-5 | 后台 goroutine 丢 trace_id | `logs.DetachContext(ctx)` 替换 10+ 处 `context.Background()` |
| P1-6 | `_broadcastSelfState` 吞错 | 最多 2 次指数退避700ms→2100ms+ stale patch 识别放弃 |
| P1-7 | TTL 自然过期盲区 | 独立 SETNX `host_grace_handling:*` / `empty_ttl_handling:*`TTL 60s |
| P1-8 | REST + WS 顺序不确定 | `pushExistingRoomState` 改同步补推,消除并行窗口 |
**commit `f5ae095` · 资源生命周期专项F1-F3 前端 / B1-B3 后端)**
- 前端 Pinia `_reset` + `exitEndedRoom` + `_pendingBroadcastTimers` 追踪 `setTimeout` 清理
- 后端 `cleanupRoomRedisResidual` DEL Redis `resourceTrackKey:*` + `memberStateKey`
- 后端 `MeetingLifecycleService.OnRoomEnded` 中央清理钩子撤销 hostGrace/emptyTTL local timer + Redis key
- `room.vue` `onUnload` 兜底清理 MediaEngine
**commit `5ed14c2` · Playwright 5 场景剧本 + .gitignore 精细化**
**commit `c35097a` · P2 × 7 + Nit × 7**
| 类别 | 条目 | 状态 |
|---|---|---|
| P2-1 | `cleanupUserResources` 新增 `transport` 分支 + `DELETE /internal/v1/transports/:id` + `MediaOrchestrator.CloseTransport` | ✅ |
| P2-2 | `preview.vue` `previewSeq` + 200ms 防抖 | ✅ |
| P2-3 | `room.vue` `onLoad` redirectTo 后 `return` | ✅ |
| P2-6 | `generateUniqueRoomCode` 重试耗尽返回 `ErrRoomCodeConflict` | ✅ |
| P2-7 | `SendChatMessage` utf8 500 字符 + Redis INCR 30 条/60s | ✅ |
| P2-8 | `MEETING_ENDED_REASON_KICKED` + `OnWSDisconnect` 常量化 | ✅ |
| P2-4 / P2-5 | WS token 迁移 / ChatService 拆分 | ⏳ 推迟 Phase 2f |
| Nit splitn | `strings.SplitN` 替换手写解析 | ✅ |
| Nit ttl | `MeetingResourceTrackTTLSeconds = 3600` 中央化 | ✅ |
| Nit origin | `ws/handler.go` CheckOrigin 白名单(`server.ws_allowed_origins` + `server.mode` | ✅ |
| Nit timeout | `TimeoutMS` 5000→10000ms + `CreateRouterRetry` 1 次 300ms 退避 | ✅ |
| Nit redispass | `deploy-public.sh` REDIS_PASSWORD × `redis.conf` 联动校验 | ✅ |
| Nit routepath | `internal-auth.ts` isPrivatePath 按 path 匹配剔除 query/hash | ✅ |
| Nit inflight | `mediasoup-client.js` finally 覆盖 resolve/reject + 注释强化 | ✅ |
| Nit ports / appdata / rfc3339 | docker-compose 端口 / `producer.appData` / 广播时间 RFC3339 | ⏳ 推迟 |
| Nit review | `_onMemberLeft` 整槽 vs `_onProducerNew closed=true` 精确匹配粒度走读 | ✅ 结论正确无需改动 |
### 5.3 登记推迟5 项)
详见审查报告 "Task 16 修复追踪 · 推迟到独立阶段" 表P2-4 WS token 迁出 URL query、P2-5 `MeetingChatService` 拆分、docker-compose 200 端口收敛、`consumer.service.ts producer.appData` 资格校验、广播时间 RFC3339 统一。这些项属于安全专项 / 架构重构 / 部署专项 / 跨模块协议统一,单独在 Phase 2f/3 落地。
---
## 六、部署验证
### 6.1 本机 Demo 模式 ✅
```bash
./scripts/start.sh full # 启动 postgres/redis/minio/go-service/media-server
npm --prefix frontend run dev:h5
```
- `deploy/.env` 使用 `deploy/.env.local.example``MEDIASOUP_ANNOUNCED_IP=""`(自动取内网 IP`TURN_ENABLED=false`
- 两浏览器互推音视频通过(需要 HTTPS + 自签证书)
### 6.2 公网模式(`--profile public`
```bash
cp deploy/.env.public.example deploy/.env
# 编辑:替换 _REPLACE_WITH_STRONG_PASSWORD_ / _REPLACE_WITH_YOUR_PUBLIC_IP_ 等
./scripts/deploy-public.sh
```
- `scripts/deploy-public.sh` 四步校验:
1. env 校验(`_REPLACE_WITH_*_` 占位符 + `MEDIASOUP_ANNOUNCED_IP` 非空 + `DEPLOY_MODE=public` + **REDIS_PASSWORD × redis.conf requirepass 联动校验**
2. 本机端口扫描8085/3300/40000-40199 UDP 段 + 3478/5349 如启用 TURN
3. Docker daemon + Compose V2 检测
4. `docker compose --profile public up -d --build`
- coturn `network_mode: host`Linux 强制)+ `profiles: ["public"]` 隔离本地 Demo
### 6.3 CheckOrigin 白名单
公网部署时建议在 `deploy/.env` 添加:
```dotenv
ECHOCHAT_SERVER_MODE=release
ECHOCHAT_SERVER_WS_ALLOWED_ORIGINS=https://echo.example.com,https://app.example.com
```
go-service 启动后 WS 握手会按白名单严格匹配 Origin非同源 + 非白名单直接拒绝。
---
## 七、已知限制与未来工作
| 限制 | 当前缓解 | 未来计划 |
|---|---|---|
| 多 go-service 实例 WS 广播未接 Redis Pub/Sub 中转 | 单实例部署 | Phase 2f 统一 WS 集群化 |
| WS token 仍在 URL query | 反向代理日志需手动脱敏 | Phase 2f 安全专项:迁移到 WS 首帧鉴权帧 |
| `MeetingChatService` 嵌入主 `MeetingService` | 当前 500+ 行仍可维护 | Phase 2f 服务分层专项拆分 |
| 广播时间戳部分走 Unix 秒,部分走 Gorm default | 前端做兼容解析 | Phase 2f 跨模块协议梳理统一 RFC3339 |
| mediasoup Worker crash 恢复尚未演练 | 单 Worker 模式 + `died` 指数退避 | Phase 3 容量与高可用阶段实机演练 |
| TURN ephemeral credentials 未落地 | `.env.public.example` 使用 static credential | Phase 3 安全加固 |
| RTCPeerConnection.getStats 未对接真实网络质量 | NetworkBadge 当前用占位值 | Phase 2e-3 会议增强 |
---
## 八、验收结论
- 17 个 Task 全部交付,测试环境下 REST + WS + 生命周期 + UI 主链路全部可用
- 代码审查 4 P0 + 8 P1 + 7 P2 + 7 Nit 修复闭环5 项低优先级事项已登记推迟到 Phase 2f/3
- 构建链 `go vet / go build / npm run build:h5 / npx tsc --noEmit` 全绿
- E2E Playwright 剧本覆盖 5 个关键场景,手工回归 checklist 齐备
- 资源生命周期(前端 Pinia / 后端 Redis / mediasoup / 定时器)清理闭环,满足"会议结束后资源必须清理干净"硬约束
**结论**Phase 2e-2 会议 MVP 达到可交付 Demo + 内网试跑水平,分支 `feature/phase2e-2-meeting-mvp` 具备合并主干条件。