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
This commit is contained in:
bujinyuan
2026-04-23 17:53:54 +08:00
parent c35097a0d8
commit 2452ca7332
5 changed files with 396 additions and 11 deletions

View File

@@ -27,7 +27,7 @@ alwaysApply: true
* 单端 WS 连接架构(沿用),不做多端已读同步(设计文档 §3.1/§3.5/§九 已修订,多端改造推迟到 Phase 2f/二期) * 单端 WS 连接架构(沿用),不做多端已读同步(设计文档 §3.1/§3.5/§九 已修订,多端改造推迟到 Phase 2f/二期)
* 专用设计:`docs/plans/2026-04-20-phase2e-1-design.md`;实施计划:`docs/plans/2026-04-20-phase2e-1-implementation.plan.md`;验证报告:`test-report-phase2e-1-notification.md` * 专用设计:`docs/plans/2026-04-20-phase2e-1-design.md`;实施计划:`docs/plans/2026-04-20-phase2e-1-implementation.plan.md`;验证报告:`test-report-phase2e-1-notification.md`
* API 文档:`docs/api/frontend/notify.md` * API 文档:`docs/api/frontend/notify.md`
- 2e-2 会议 MVP约 17 天)🚧 **代码开发中**Task 0-14 ✅ / Task 15-16 待执行mediasoup Node.js 独立 `media-server/` + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + **会议生命周期状态机**host 宽限期 + 自动转让 + 空房 TTL+ 双态部署(本机 + 公网 coturn+ 响应式(桌面/手机) - 2e-2 会议 MVP约 17 天) **已完成**Task 0-16 全部 ✅2026-04-24 收官mediasoup Node.js 独立 `media-server/` + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + **会议生命周期状态机**host 宽限期 + 自动转让 + 空房 TTL+ 双态部署(本机 + 公网 coturn+ 响应式(桌面/手机) + **code-reviewer 全栈审计**4 P0 + 8 P1 + 7 P2 + 7 Nit 闭环修复5 项登记推迟至 Phase 2f/3+ **资源生命周期专项**(前端 Pinia `_reset` 清理 broadcast timer + 后端 Redis `resourceTrackKey`/`memberStateKey` 显式 DEL + mediasoup Transport DELETE + lifecycle timer 统一撤销)+ **Playwright MCP 5 场景剧本 + 手工回归 checklist**`.playwright-mcp/task16/scenarios/`+ **test-report-phase2e-2-meeting.md** 落盘
* 专用设计:`docs/plans/2026-04-21-phase2e-2-design.md`16 章节);实施计划:`docs/plans/2026-04-21-phase2e-2-implementation.plan.md`17 个 Task * 专用设计:`docs/plans/2026-04-21-phase2e-2-design.md`16 章节);实施计划:`docs/plans/2026-04-21-phase2e-2-implementation.plan.md`17 个 Task
* 11 项关键决策已锁定D01-D11详见设计文档 §三 * 11 项关键决策已锁定D01-D11详见设计文档 §三
* **重要修订**`meeting_rooms.password` → `password_hash`bcrypt新增 `meeting_chats` 表 + `ended_reason` / `left_reason` 字段,新增 `echo:meeting:invite:{token}` / `host_grace:{code}` Redis key * **重要修订**`meeting_rooms.password` → `password_hash`bcrypt新增 `meeting_chats` 表 + `ended_reason` / `left_reason` 字段,新增 `echo:meeting:invite:{token}` / `host_grace:{code}` Redis key
@@ -36,6 +36,8 @@ alwaysApply: true
* **Task 2 ✅ 9 个内部 REST API 完成2026-04-21含代码审查修复**`media-server/` 落地 Router/Transport/Producer/Consumer 四类资源的 9 个接口,全部挂 `/internal/v1/*` 前缀;**zod 手动 parse + 全局 errorHandler** 方案(不引入 fastify-type-provider-zod 避免 zod v4 依赖冲突);`AppError` 统一错误码(`NOT_FOUND`/`CONFLICT`/`CAN_NOT_CONSUME`/`ROUTER_LIMIT_EXCEEDED`/`MEDIASOUP_ERROR`+ `VALIDATION_ERROR`/`UNAUTHORIZED`/`INTERNAL_ERROR`;所有资源用 **Map + `observer.once('close')` 自清理**`producerclose` 级联关闭下游 consumerConsumer 强制 `paused:true` 创建 + `/resume` 独立接口direction 强约束recv transport 拒 produce、send transport 拒 consume。**code-reviewer 子代理"有条件通过"**2 Major + 4 高价值 Minor 当场修复:(1) M1 `_clearXxxMap` 新增 `assertTestOnly` 守卫(生产误调用直接抛错);(2) M2 新增 `src/schemas/rtp.ts` 对 `rtpParameters` / `rtpCapabilities` 做 codecs 浅层校验mimeType/clockRate/payloadType 必填、codecs 数组 ≥1消除 `as unknown as` 双跳断言;(3) m1 `connectTransport` 改乐观锁(先置位再 await(4) m2 改读 `consumer.producerPaused`(5) m3 `producerclose` 改为 `once`(6) m5 `internal-auth` 改为反向白名单 `PRIVATE_PATH_PREFIXES = ['/internal/']`(默认开放)。**65 个 vitest 测试全过(~1s、覆盖率 82.87%/75.83%/91.3%/82.87%**stmts/branches/funcs/lines较首版 +2pp9 接口 happy path + 6 类错误路径人工 curl 全部按预期返回201/200/400/401/404/409。产出`src/schemas/*`6 文件,新增 `rtp.ts`+ `src/services/*`4 文件)+ `src/routes/*`4 文件)+ `src/middlewares/{error-handler,internal-auth}.ts` + `src/utils/{errors,test-guard}.ts` + `src/mediasoup/codecs.ts` + `vitest.config.ts` + `tests/*`8 spec 文件,含 `test-guard.spec.ts`)。余下 Minor/Nitsm4/m6~m10、n1~n10登记至 Task 16 收尾清单 * **Task 2 ✅ 9 个内部 REST API 完成2026-04-21含代码审查修复**`media-server/` 落地 Router/Transport/Producer/Consumer 四类资源的 9 个接口,全部挂 `/internal/v1/*` 前缀;**zod 手动 parse + 全局 errorHandler** 方案(不引入 fastify-type-provider-zod 避免 zod v4 依赖冲突);`AppError` 统一错误码(`NOT_FOUND`/`CONFLICT`/`CAN_NOT_CONSUME`/`ROUTER_LIMIT_EXCEEDED`/`MEDIASOUP_ERROR`+ `VALIDATION_ERROR`/`UNAUTHORIZED`/`INTERNAL_ERROR`;所有资源用 **Map + `observer.once('close')` 自清理**`producerclose` 级联关闭下游 consumerConsumer 强制 `paused:true` 创建 + `/resume` 独立接口direction 强约束recv transport 拒 produce、send transport 拒 consume。**code-reviewer 子代理"有条件通过"**2 Major + 4 高价值 Minor 当场修复:(1) M1 `_clearXxxMap` 新增 `assertTestOnly` 守卫(生产误调用直接抛错);(2) M2 新增 `src/schemas/rtp.ts` 对 `rtpParameters` / `rtpCapabilities` 做 codecs 浅层校验mimeType/clockRate/payloadType 必填、codecs 数组 ≥1消除 `as unknown as` 双跳断言;(3) m1 `connectTransport` 改乐观锁(先置位再 await(4) m2 改读 `consumer.producerPaused`(5) m3 `producerclose` 改为 `once`(6) m5 `internal-auth` 改为反向白名单 `PRIVATE_PATH_PREFIXES = ['/internal/']`(默认开放)。**65 个 vitest 测试全过(~1s、覆盖率 82.87%/75.83%/91.3%/82.87%**stmts/branches/funcs/lines较首版 +2pp9 接口 happy path + 6 类错误路径人工 curl 全部按预期返回201/200/400/401/404/409。产出`src/schemas/*`6 文件,新增 `rtp.ts`+ `src/services/*`4 文件)+ `src/routes/*`4 文件)+ `src/middlewares/{error-handler,internal-auth}.ts` + `src/utils/{errors,test-guard}.ts` + `src/mediasoup/codecs.ts` + `vitest.config.ts` + `tests/*`8 spec 文件,含 `test-guard.spec.ts`)。余下 Minor/Nitsm4/m6~m10、n1~n10登记至 Task 16 收尾清单
* **Task 3 ✅ Go meeting 模块数据库 DDL + Model + DAO 完成2026-04-21**:三张持久化表(`meeting_rooms` / `meeting_participants` / `meeting_chats`)落地 PostgreSQLDDL 同时写入 `init.sql`(全量初始化)与 `phase2e2_migration.sql`幂等增量升级。Go 侧 `backend/go-service/app/meeting/{model,dao}` + 统一常量 `app/constants/meeting.go`3 个 model + 3 个 DAO`meeting_room_dao.go` 9 方法 / `meeting_participant_dao.go` 11 方法 / `meeting_chat_dao.go` 4 方法),共 24 个持久化方法;`JoinRoom` 事务内复用离会后的旧记录(`left_at=NULL,joined_at=NOW,duration=0`)避免审计表污染;`LeaveRoom` 用 `EXTRACT(EPOCH FROM (? - joined_at))::INT` 走 DB 时间防跨时区漂移;`TransferHost` 事务链(`role=1→0` + `role=0→1``FindActiveByUser` 用 JOIN 校验用户单点参会;`MarkEnded` 乐观锁防重复覆盖 `ended_reason``ListExpiredForCleanup` 供后续清理任务批量扫描。**关键风格修正(偏离实施计划草案)**:按 `project-context` 第 11 条「代码风格全局一致(最高优先级)」,常量归入 `app/constants/meeting.go` 单文件(与 `group.go`/`notify.go` 同构),而非草案的 `app/meeting/constants/*.go`;时间字段统一 `TIMESTAMP(0)` 取代草案的 `TIMESTAMPTZ` 对齐项目所有现有表;冗余 `idx_meeting_rooms_code` 移除(`room_code UNIQUE` 已自动建索引)。验证:`go build ./...` / `go vet ./...` / `ReadLints` 零错误psql 集成脚本跑通 8 场景CRUD + 双 UNIQUE 约束 + 主持人转让事务 + `duration=10s` 精确匹配 + CASCADE 清零)。延续项目 Go 侧"零 `_test.go`"风格(用代码审查 + psql 真库验证 + Playwright E2E 三层守护) * **Task 3 ✅ Go meeting 模块数据库 DDL + Model + DAO 完成2026-04-21**:三张持久化表(`meeting_rooms` / `meeting_participants` / `meeting_chats`)落地 PostgreSQLDDL 同时写入 `init.sql`(全量初始化)与 `phase2e2_migration.sql`幂等增量升级。Go 侧 `backend/go-service/app/meeting/{model,dao}` + 统一常量 `app/constants/meeting.go`3 个 model + 3 个 DAO`meeting_room_dao.go` 9 方法 / `meeting_participant_dao.go` 11 方法 / `meeting_chat_dao.go` 4 方法),共 24 个持久化方法;`JoinRoom` 事务内复用离会后的旧记录(`left_at=NULL,joined_at=NOW,duration=0`)避免审计表污染;`LeaveRoom` 用 `EXTRACT(EPOCH FROM (? - joined_at))::INT` 走 DB 时间防跨时区漂移;`TransferHost` 事务链(`role=1→0` + `role=0→1``FindActiveByUser` 用 JOIN 校验用户单点参会;`MarkEnded` 乐观锁防重复覆盖 `ended_reason``ListExpiredForCleanup` 供后续清理任务批量扫描。**关键风格修正(偏离实施计划草案)**:按 `project-context` 第 11 条「代码风格全局一致(最高优先级)」,常量归入 `app/constants/meeting.go` 单文件(与 `group.go`/`notify.go` 同构),而非草案的 `app/meeting/constants/*.go`;时间字段统一 `TIMESTAMP(0)` 取代草案的 `TIMESTAMPTZ` 对齐项目所有现有表;冗余 `idx_meeting_rooms_code` 移除(`room_code UNIQUE` 已自动建索引)。验证:`go build ./...` / `go vet ./...` / `ReadLints` 零错误psql 集成脚本跑通 8 场景CRUD + 双 UNIQUE 约束 + 主持人转让事务 + `duration=10s` 精确匹配 + CASCADE 清零)。延续项目 Go 侧"零 `_test.go`"风格(用代码审查 + psql 真库验证 + Playwright E2E 三层守护)
* **Task 4 ✅ Go meeting 模块 service/controller/router 骨架完成2026-04-21**`app/meeting/` 补齐 service/controller/router/provider 四件套,接口 → 实现按设计文档 §5.3 一一对齐。产出:(1) `service/interfaces.go` 定义 3 个外部依赖接口(`NotifyPusher` / `UserInfoResolver` / `OnlineChecker`),解耦 notify/contact/ws 模块避免循环依赖;`OnlineChecker.IsOnline` 签名与现存 `ws.OnlineService` 一致(返回单 `bool`(2) `service/meeting_service.go` 声明 `MeetingService` + 8 个 sentinel error + 17 个业务方法空实现,全部返回 `ErrNotImplemented`(3) `controller/meeting_controller.go` 12 个 Gin 处理器 + `responseNotImplemented`501+ `requireUserID` 辅助;(4) `router.go` 12 条路由挂载到 `/api/v1/meeting/*` 并统一套 `jwtAuth` 中间件;(5) `provider.go` 定义 `MeetingSet = wire.NewSet(DAO×3, Service, Controller)`(6) 全局 `app/provider/wire.go` 挂入 `meetingApp.MeetingSet` + 3 条 `wire.Bind``NotifyPusher→NotifyService` / `UserInfoResolver→FriendshipDAO` / `OnlineChecker→ws.OnlineService`(7) `app/provider/provider.go` `App` 加 `MeetingService/MeetingController` 字段;(8) `router/router.go` 调用 `meetingApp.RegisterRoutes`。**顺手修复存量 bug**`admin/provider.go` 补齐 `MessageManage{DAO,Service,Controller}` 三个 provider解决旧版 `wire` 重生成报"no provider found"的遗留问题。验证:`go build ./...` / `go vet ./...` / `wire ./app/provider` 全绿;`GIN_MODE=debug` 启动 server 日志打印全部 12 条 `[GIN-debug] ... meeting/controller.(*MeetingController).Xxx-fm`curl 无 token 打 3 条代表性路由均返回 401 `缺少认证信息`JWT 中间件生效 * **Task 4 ✅ Go meeting 模块 service/controller/router 骨架完成2026-04-21**`app/meeting/` 补齐 service/controller/router/provider 四件套,接口 → 实现按设计文档 §5.3 一一对齐。产出:(1) `service/interfaces.go` 定义 3 个外部依赖接口(`NotifyPusher` / `UserInfoResolver` / `OnlineChecker`),解耦 notify/contact/ws 模块避免循环依赖;`OnlineChecker.IsOnline` 签名与现存 `ws.OnlineService` 一致(返回单 `bool`(2) `service/meeting_service.go` 声明 `MeetingService` + 8 个 sentinel error + 17 个业务方法空实现,全部返回 `ErrNotImplemented`(3) `controller/meeting_controller.go` 12 个 Gin 处理器 + `responseNotImplemented`501+ `requireUserID` 辅助;(4) `router.go` 12 条路由挂载到 `/api/v1/meeting/*` 并统一套 `jwtAuth` 中间件;(5) `provider.go` 定义 `MeetingSet = wire.NewSet(DAO×3, Service, Controller)`(6) 全局 `app/provider/wire.go` 挂入 `meetingApp.MeetingSet` + 3 条 `wire.Bind``NotifyPusher→NotifyService` / `UserInfoResolver→FriendshipDAO` / `OnlineChecker→ws.OnlineService`(7) `app/provider/provider.go` `App` 加 `MeetingService/MeetingController` 字段;(8) `router/router.go` 调用 `meetingApp.RegisterRoutes`。**顺手修复存量 bug**`admin/provider.go` 补齐 `MessageManage{DAO,Service,Controller}` 三个 provider解决旧版 `wire` 重生成报"no provider found"的遗留问题。验证:`go build ./...` / `go vet ./...` / `wire ./app/provider` 全绿;`GIN_MODE=debug` 启动 server 日志打印全部 12 条 `[GIN-debug] ... meeting/controller.(*MeetingController).Xxx-fm`curl 无 token 打 3 条代表性路由均返回 401 `缺少认证信息`JWT 中间件生效
* **Task 16 ✅ E2E 总回归 + 代码审查修复 + 文档同步完成2026-04-24 收官)**Phase 2e-2 会议 MVP **整体切 ✅**。调用 `code-reviewer` 子代理对 Go meeting 模块 + media-server + frontend meeting 做一次重写版全栈审计,落盘 `docs/reviews/2026-04-23-phase2e-2-code-review.md`4 P0 + 8 P1 + 8 P2 + 11 Nit**26 项闭环修复 + 5 项登记推迟至 Phase 2f/3**。分 5 个 commit 落地:(1) `cdaa39d` P0×4媒体资源归属 Redis Set 校验 / CreateRoom Router 失败改 fail-closed 补偿 / 会议密码从 URL query 迁到 Pinia `draftJoinPayload` in-memory(2) `ea2bf96` P1×8`TransferHost` 事务合并 + `UpdateHost` 冗余移除 / `ListChatMessages`+`ListMyMeetings` 改走 DAO 游标分页 / `EndRoom` 加 `SELECT…FOR UPDATE` + 事务化快照 / `logs.DetachContext` 保留 trace_id 剥离 cancel / `_broadcastSelfState` 2 次指数退避重试 / `HandleHostGraceExpired` 独立 SETNX 处理锁消除 TTL 自然过期盲区 / `pushExistingRoomState` 改同步补推消除并行窗口);(3) `f5ae095` 资源生命周期专项(前端 `_pendingBroadcastTimers` 集中清理 + `exitEndedRoom` action + `onUnload` 兜底;后端 `cleanupRoomRedisResidual` 统一 DEL Redis key + `MeetingLifecycleService.OnRoomEnded` 中央清理钩子撤销 hostGrace/emptyTTL 定时器);(4) `5ed14c2` Playwright MCP 5 场景剧本 + 手工回归 checklist 落盘 `.playwright-mcp/task16/`(含 `.gitignore` 精细化:运行时产物默认忽略 + Markdown 剧本精确白名单放行);(5) `c35097a` P2×7+Nit×7P2-1 `cleanupUserResources` 补 `transport:<id>` 分支 + media-server 新增 `DELETE /internal/v1/transports/:id` + Go `MediaOrchestrator.CloseTransport`P2-2 `preview.vue` `previewSeq` 序号 + 200ms 防抖杜绝快速切设备竞态P2-3 `room.vue` `onLoad` redirectTo 后 `redirectingToJoin=true` + return 避免闪 UIP2-6 `generateUniqueRoomCode` 重试耗尽 Error + `ErrRoomCodeConflict`P2-7 `SendChatMessage` 服务端 utf8 rune 500 字符上限 + Redis INCR 滑动窗口 30 条/60s + 3 个新错误类型 HTTP 400 映射P2-8 新增 `MEETING_ENDED_REASON_KICKED` + label + 后端 `OnWSDisconnect` 硬编码 `"ws_disconnect"`→`constants.MeetingLeftReasonDisconnect`Nit splitn/ttl 中央化/CheckOrigin 白名单/doRequest timeout 5→10s + CreateRouter 重试 300ms 退避/REDIS_PASSWORD×redis.conf 联动校验/isPrivatePath 按 path 匹配剔除 query/inflight lock 走读确认 + 注释强化/`_onMemberLeft` vs `_onProducerNew closed=true` 粒度走读复核)。登记推迟 5 项P2-4 WS token 迁出 URL query、P2-5 `MeetingChatService` 拆分、docker-compose 200 端口收敛、`producer.appData` 校验、广播时间 RFC3339 统一。验证:`go vet ./...` / `go build ./...` / `npm run build:h5` / `npx tsc --noEmit` 全绿;`console.log / TODO / FIXME` grep 0 残留6 份文档同步:本文件 + `CURRENT_STATUS.md` + `implementation.plan.md` + 审查报告 + `phase2e-2-design.md` + 新增 `test-report-phase2e-2-meeting.md`
* **Task 15 ✅ UI 打磨 + 主持人权限四件套完成2026-04-23**6 项原创 UI 特色全部落地(说话者探测双源 RTP stats + WebAudio / `SelfVideoFloat` 桌面 280×180 恒浮窗 + 四角吸附 + 图钉切换 / `MeetingToolbar.isAllMuted` computed 蓝紫氛围色 / `VideoGrid` `tile-slide-in` 入会滑入动画 / `NetworkBadge` 3 条 SVG 波浪相位错开 / 4 屏 design-system 文档);主持人四件套(请他静音 / 请他开麦 / 转让主持人 / 踢出会议)通过 `MemberPanel.vue` + `store.muteMember`/`unmuteMember`/`transferHost`/`kickMember` + WS 广播 `meeting.member.state.changed` / `meeting.host.changed` / `meeting.member.kicked` 完整链路打通;被操作侧自动响应(`_onMemberStateChanged` 检测 `changed_by !== myUid` → `stopLocalAudio/Video` + `uni.showToast`)。**后续媒体层稳定性补丁Task 15 三轮手工联调修复)**(A) 成员列表 audio/video 图标常灰:后端 `meeting.member.state.changed` 广播范围改为"含自己"确保进房后收到历史状态;前端 `_onMemberStateChanged` 对尚未在 `participants` 表中的 user_id 建占位行容错 REST+WS 时序错乱;(B) 单向流:`mediasoup-client` `ensureSendTransport/ensureRecvTransport` in-flight Promise 锁防重复创建;`_ensureRecvAllExisting` 进房后遍历补订阅已存在 producers(C) 同用户二次入会成员列表缺失:`store._reset` 清空所有 Map/Set 避免旧会议残留覆盖;入口 guard `if (_engine) return` 去除,转为 `_cleanupMedia()` 重置。Playwright MCP 7 屏截图回归通过(`.playwright-mcp/task15/{01..07}-*.png`)。下一步 Task 16 E2E 总回归 + 代码审查 + 文档同步。
* **Task 14 ✅ docker-compose 扩展 + 环境变量双态开关完成2026-04-24**EchoChat 会议 MVP 正式具备**本机 Demo / 公网部署**双态一键化能力。产出:(1) `deploy/docker-compose.dev.yml` 新增 `media-server` 服务env 注入 `MEDIASOUP_ANNOUNCED_IP` / `MEDIA_INTERNAL_TOKEN` / RTC 端口段 40000-40199+ `coturn` 服务(`profiles: ["public"]` + `network_mode: host` + env 注入 realm / user / credential / 端口段 49152-65535`go-service` 新增 `depends_on: media-server`postgres/redis/minio 全部改为 env 变量可覆盖;(2) `deploy/.env.example`(总模板,涵盖 `DEPLOY_MODE/DB/REDIS/JWT/MINIO/MEDIA/TURN` 全字段)+ `deploy/.env.local.example`(本机 Demo 预填值,`MEDIASOUP_ANNOUNCED_IP=""` 空 = 自动内网 IP`TURN_ENABLED=false`+ `deploy/.env.public.example`(公网部署模板,所有敏感字段使用 `_REPLACE_WITH_STRONG_PASSWORD_` / `_REPLACE_WITH_YOUR_PUBLIC_IP_` / `_REPLACE_WITH_TURN_SECRET_` 占位符 + 部署前 checklist(3) `scripts/start.sh` 新增 `full` 子命令(`ensure_env_file` + `start_full` → `docker compose -f docker-compose.dev.yml --profile public up -d --build``scripts/stop.sh` 新增 `full` 子命令;`scripts/status.sh` 扩展检测 `echochat-go-service/media-server/coturn` 三个应用容器;(4) `scripts/deploy-public.sh`(新建,`chmod +x``step_validate_env``.env` 存在 + `DEPLOY_MODE=public` + `MEDIASOUP_ANNOUNCED_IP` 非空 + 所有 `_REPLACE_WITH_*_` 占位符已替换)→ `step_check_ports`(本机端口 + 云厂商安全组 checklist→ `step_check_docker`daemon + Compose V2→ `step_launch`(按 `TURN_ENABLED` 决定 `--profile public` 取舍 + `wait` media-server `/healthz`(5) `docs/deployment/meeting-mvp.md`(新建):本机 Demo + 公网双态部署全流程 + coturn 配置 + 强密码生成 + 防火墙 checklist + FAQ`python3/g++` / 视频问题 / `coturn` 网络模式 / HTTPS / 数据库备份)。**关键设计决策**(a) 双态开关 = 纯 env 差异(`MEDIASOUP_ANNOUNCED_IP` 空 vs 非空 + `TURN_ENABLED` true vs false(b) coturn 在 Linux 上必须 `network_mode: host`TURN 依赖大段随机 UDP 端口)+ `profiles: ["public"]` 隔离保证本地 Demo 不浪费资源;(c) `deploy-public.sh` 扫描 `_REPLACE_WITH_*_` 占位符防"示例值上生产"事故;(d) Compose V2 `--profile` 必须作为顶层 flag`docker compose --profile public -f x.yml config`(e) 不起独立 `docker-compose.prod.yml`profile + env 分离即可。验证:`docker compose config` 两种模式local / public均 `--quiet` 通过;`deploy-public.sh` 三场景(`.env` 缺失 / 含占位符 / 空 `ANNOUNCED_IP`)全部按预期退出 1完整正确配置下顺序通过三步校验进入 `step_launch`。下一步 Task 15 UI 打磨 + 主持人权限四件套 * **Task 14 ✅ docker-compose 扩展 + 环境变量双态开关完成2026-04-24**EchoChat 会议 MVP 正式具备**本机 Demo / 公网部署**双态一键化能力。产出:(1) `deploy/docker-compose.dev.yml` 新增 `media-server` 服务env 注入 `MEDIASOUP_ANNOUNCED_IP` / `MEDIA_INTERNAL_TOKEN` / RTC 端口段 40000-40199+ `coturn` 服务(`profiles: ["public"]` + `network_mode: host` + env 注入 realm / user / credential / 端口段 49152-65535`go-service` 新增 `depends_on: media-server`postgres/redis/minio 全部改为 env 变量可覆盖;(2) `deploy/.env.example`(总模板,涵盖 `DEPLOY_MODE/DB/REDIS/JWT/MINIO/MEDIA/TURN` 全字段)+ `deploy/.env.local.example`(本机 Demo 预填值,`MEDIASOUP_ANNOUNCED_IP=""` 空 = 自动内网 IP`TURN_ENABLED=false`+ `deploy/.env.public.example`(公网部署模板,所有敏感字段使用 `_REPLACE_WITH_STRONG_PASSWORD_` / `_REPLACE_WITH_YOUR_PUBLIC_IP_` / `_REPLACE_WITH_TURN_SECRET_` 占位符 + 部署前 checklist(3) `scripts/start.sh` 新增 `full` 子命令(`ensure_env_file` + `start_full` → `docker compose -f docker-compose.dev.yml --profile public up -d --build``scripts/stop.sh` 新增 `full` 子命令;`scripts/status.sh` 扩展检测 `echochat-go-service/media-server/coturn` 三个应用容器;(4) `scripts/deploy-public.sh`(新建,`chmod +x``step_validate_env``.env` 存在 + `DEPLOY_MODE=public` + `MEDIASOUP_ANNOUNCED_IP` 非空 + 所有 `_REPLACE_WITH_*_` 占位符已替换)→ `step_check_ports`(本机端口 + 云厂商安全组 checklist→ `step_check_docker`daemon + Compose V2→ `step_launch`(按 `TURN_ENABLED` 决定 `--profile public` 取舍 + `wait` media-server `/healthz`(5) `docs/deployment/meeting-mvp.md`(新建):本机 Demo + 公网双态部署全流程 + coturn 配置 + 强密码生成 + 防火墙 checklist + FAQ`python3/g++` / 视频问题 / `coturn` 网络模式 / HTTPS / 数据库备份)。**关键设计决策**(a) 双态开关 = 纯 env 差异(`MEDIASOUP_ANNOUNCED_IP` 空 vs 非空 + `TURN_ENABLED` true vs false(b) coturn 在 Linux 上必须 `network_mode: host`TURN 依赖大段随机 UDP 端口)+ `profiles: ["public"]` 隔离保证本地 Demo 不浪费资源;(c) `deploy-public.sh` 扫描 `_REPLACE_WITH_*_` 占位符防"示例值上生产"事故;(d) Compose V2 `--profile` 必须作为顶层 flag`docker compose --profile public -f x.yml config`(e) 不起独立 `docker-compose.prod.yml`profile + env 分离即可。验证:`docker compose config` 两种模式local / public均 `--quiet` 通过;`deploy-public.sh` 三场景(`.env` 缺失 / 含占位符 / 空 `ANNOUNCED_IP`)全部按预期退出 1完整正确配置下顺序通过三步校验进入 `step_launch`。下一步 Task 15 UI 打磨 + 主持人权限四件套
* **Task 13 ✅ meeting_invite 通知卡片对接完成2026-04-23**:后端 `MeetingService.InviteUsers` 的 `PushPayload.Extra` 补齐设计 §10.1 要求的 `inviter_id / inviter_name / inviter_avatar / expired_at`Unix 秒,与 Redis TTL `MeetingInviteTokenTTL=600s` 一致,取代之前只有 `room_code/room_title/has_password/invite_token` 的简化版本);前端 `constants/notify.js` 把 `supportsInlineAction` 扩展支持 `meeting_invite`,新增 `NOTIFY_INLINE_ACTION_LABEL` 映射表让按钮文案按 type 动态变化meeting_invite → `立即加入 / 稍后`friend_request → `同意 / 拒绝``NotifyItem.vue` 新增 `isExpired` 计算属性(纯客户端比对 `extra.expired_at * 1000 < Date.now()`),过期态合并为单个 disabled 的"邀请已过期"按钮 + `.notify-btn--expired` 灰显样式;`pages/notify/index.vue` 在 `handleAccept`/`handleReject`/`_navigateByNotify` 均增加 `NOTIFY_TYPE_MEETING_INVITE` 分支,新增 `_navigateToMeetingInvite(extra)` 跳 `/pages/meeting/preview?mode=join&code=xxx`,过期时 toast 提示不跳转,`handleReject`"稍后")只 `markRead` 不发接口。**附带修复commit 310ea03一个隐藏很深的 WS `.ack` 回归 bug**`frontend/src/services/websocket.js` 早期的 `_onMessage` 在命中 `.ack` 事件后 `_handleAck(msg); return;`,直接吞掉了 `_emit`Phase 2a 以后 `chat.js`/`meeting.js` 都在 `wsService.on('xxx.ack', ...)` 订阅 ACK 做"临时消息替换为真实消息",这条关键事件从未触发 → 发送方消息永远 `_sending=true`、前面显示 loading 圆圈、下方「已读/未读」标签永不渲染。修复:移除 `return`,改为同时走 `_handleAck`Promise `pendingAcks` 通路)+ `_emit`(订阅通路)。补强:`chat.js._appendMessage` 命中 `client_msg_id` 的临时消息时不再 dedup 丢弃,而是就地合并服务端字段并显式清 `_sending:false/_failed:false`,作为"丢 ACK 但不丢广播"场景的兜底。验证curl 双角色端到端(登录 testuser1/testuser2 → 创会 → 邀请 → 拉通知列表)返回 `type=meeting_invite` 且 `extra` 8 字段齐全,`expired_at = invited_at + 600` 与 Redis TTL 对齐用户手测「消息卡圈」bug 确认修复。Playwright UI 回归因多用户上下文环境限制暂缓到 Task 16 总回归统一做。下一步 Task 14 docker-compose 扩展 + 本机/公网双态部署 * **Task 13 ✅ meeting_invite 通知卡片对接完成2026-04-23**:后端 `MeetingService.InviteUsers` 的 `PushPayload.Extra` 补齐设计 §10.1 要求的 `inviter_id / inviter_name / inviter_avatar / expired_at`Unix 秒,与 Redis TTL `MeetingInviteTokenTTL=600s` 一致,取代之前只有 `room_code/room_title/has_password/invite_token` 的简化版本);前端 `constants/notify.js` 把 `supportsInlineAction` 扩展支持 `meeting_invite`,新增 `NOTIFY_INLINE_ACTION_LABEL` 映射表让按钮文案按 type 动态变化meeting_invite → `立即加入 / 稍后`friend_request → `同意 / 拒绝``NotifyItem.vue` 新增 `isExpired` 计算属性(纯客户端比对 `extra.expired_at * 1000 < Date.now()`),过期态合并为单个 disabled 的"邀请已过期"按钮 + `.notify-btn--expired` 灰显样式;`pages/notify/index.vue` 在 `handleAccept`/`handleReject`/`_navigateByNotify` 均增加 `NOTIFY_TYPE_MEETING_INVITE` 分支,新增 `_navigateToMeetingInvite(extra)` 跳 `/pages/meeting/preview?mode=join&code=xxx`,过期时 toast 提示不跳转,`handleReject`"稍后")只 `markRead` 不发接口。**附带修复commit 310ea03一个隐藏很深的 WS `.ack` 回归 bug**`frontend/src/services/websocket.js` 早期的 `_onMessage` 在命中 `.ack` 事件后 `_handleAck(msg); return;`,直接吞掉了 `_emit`Phase 2a 以后 `chat.js`/`meeting.js` 都在 `wsService.on('xxx.ack', ...)` 订阅 ACK 做"临时消息替换为真实消息",这条关键事件从未触发 → 发送方消息永远 `_sending=true`、前面显示 loading 圆圈、下方「已读/未读」标签永不渲染。修复:移除 `return`,改为同时走 `_handleAck`Promise `pendingAcks` 通路)+ `_emit`(订阅通路)。补强:`chat.js._appendMessage` 命中 `client_msg_id` 的临时消息时不再 dedup 丢弃,而是就地合并服务端字段并显式清 `_sending:false/_failed:false`,作为"丢 ACK 但不丢广播"场景的兜底。验证curl 双角色端到端(登录 testuser1/testuser2 → 创会 → 邀请 → 拉通知列表)返回 `type=meeting_invite` 且 `extra` 8 字段齐全,`expired_at = invited_at + 600` 与 Redis TTL 对齐用户手测「消息卡圈」bug 确认修复。Playwright UI 回归因多用户上下文环境限制暂缓到 Task 16 总回归统一做。下一步 Task 14 docker-compose 扩展 + 本机/公网双态部署
* **Task 10-12 ✅ + 2026-04-22 UI 打磨:前端会议主链路全部打通(含三轮 uni-button 遮罩 / z-index / toggle 深度修复)**:前端 `/pages/meeting/{create,join,preview,room,index}.vue` 五页全量落地;组件 `MeetingToolbar` / `VideoGrid` / `VideoTile` / `MemberPanel` / `InviteDialog` / `ChatPanel` 全部完工;后端 `SendChatMessage` + `chatToDTO` 补齐 `user_name` / `user_avatar`WS 推 + REST 列表均带真实昵称);`store/meeting.js` 新增 `cleanupStaleMeetings` + `createAndEnter/joinAndEnter` 首次静默 + 自动重试。**关键技术攻坚**(1) **uni-button `::after` 全视口遮罩(两轮)**uni-app H5 自动给 `<button>` 编译后的 `<uni-button>` 注入 `::after { position:absolute; inset:0 -1200px -80px 0 }` 作为点击反馈2400×160 超大遮罩MeetingToolbar 里最后一个"离开"按钮的 `::after` 横跨整个 toolbar 导致其他按钮全部点不响应、离开弹窗的"取消"也触发结束会议 → `.btn::after { content:none; display:none }` + `.btn { position:relative }` 修复ChatPanel 里 `.btn-send { all:unset }` 把 position 重置为 static 导致 `::after` 的 absolute 定位沿 DOM 树回溯到 `<body>` 作为 containing block 形成全屏遮罩,关闭/输入框/发送全被挡 → `.btn-send::after { content:none; display:none }` 修复。(2) **面板 z-index 冲突**`MemberPanel/InviteDialog .panel-root` 是 `position:fixed; inset:0; z-index:200` 全屏 mask打开时把 toolbar 遮掉,用户再次点击"成员/邀请"按钮命中的是 mask 而非按钮导致 toggle 永远不生效 → `.toolbar { z-index:210 }` 浮在 mask 之上(仍低于 `.leave-mask` 的 220。(3) **H5 全局容器偏左上**:桌面大窗口下 `uni-app`/`uni-page` 祖先默认不 100% 拉伸,导致 `position:fixed; inset:0` 的 `.room` 相对偏小祖先盒子定位 → `App.vue` 的 H5 条件块给 `html/body/#app/uni-app/uni-page*` 全部 `width/height:100%``.room` 加 `100vw/100vh/z-index:1000` 二道防御。(4) **刷新后残留会议静默清理**`joinAndEnter/createAndEnter` 首次走 `silent:true` 不弹 toast后端 `staleMeetingHint` 类错误自动调 `cleanupStaleMeetings()` + 重试一次;`leaveRoom({silent:true})` 抑制"你不在此会议中"toast。(5) **预览↔会议内视频统一 HD**`preview.vue` + `store/meeting.js#startLocalVideo` 统一 `width:{ideal:1280} height:{ideal:720} frameRate:{ideal:24,max:30}`。(6) **成员/邀请/聊天按钮 toggle**`openMembers/openInvite/openChat` 全改为取反,符合"再次点击关闭"直觉。验证Playwright 实测所有 toolbar 按钮 + close/textarea/send + 成员二次点击均 `hitSelf=true`,离开弹窗取消不再误触结束。相关提交:`cacf4ae` / `a311c03` / `3aa8174` / `ff46dc5` / `ce09315` / `c529ff4` / `0c9f048` / `16225c4` / `1222bf5` / `59f824a` / `eb3857a`。下一步 Task 13 主持人权限全链路。 * **Task 10-12 ✅ + 2026-04-22 UI 打磨:前端会议主链路全部打通(含三轮 uni-button 遮罩 / z-index / toggle 深度修复)**:前端 `/pages/meeting/{create,join,preview,room,index}.vue` 五页全量落地;组件 `MeetingToolbar` / `VideoGrid` / `VideoTile` / `MemberPanel` / `InviteDialog` / `ChatPanel` 全部完工;后端 `SendChatMessage` + `chatToDTO` 补齐 `user_name` / `user_avatar`WS 推 + REST 列表均带真实昵称);`store/meeting.js` 新增 `cleanupStaleMeetings` + `createAndEnter/joinAndEnter` 首次静默 + 自动重试。**关键技术攻坚**(1) **uni-button `::after` 全视口遮罩(两轮)**uni-app H5 自动给 `<button>` 编译后的 `<uni-button>` 注入 `::after { position:absolute; inset:0 -1200px -80px 0 }` 作为点击反馈2400×160 超大遮罩MeetingToolbar 里最后一个"离开"按钮的 `::after` 横跨整个 toolbar 导致其他按钮全部点不响应、离开弹窗的"取消"也触发结束会议 → `.btn::after { content:none; display:none }` + `.btn { position:relative }` 修复ChatPanel 里 `.btn-send { all:unset }` 把 position 重置为 static 导致 `::after` 的 absolute 定位沿 DOM 树回溯到 `<body>` 作为 containing block 形成全屏遮罩,关闭/输入框/发送全被挡 → `.btn-send::after { content:none; display:none }` 修复。(2) **面板 z-index 冲突**`MemberPanel/InviteDialog .panel-root` 是 `position:fixed; inset:0; z-index:200` 全屏 mask打开时把 toolbar 遮掉,用户再次点击"成员/邀请"按钮命中的是 mask 而非按钮导致 toggle 永远不生效 → `.toolbar { z-index:210 }` 浮在 mask 之上(仍低于 `.leave-mask` 的 220。(3) **H5 全局容器偏左上**:桌面大窗口下 `uni-app`/`uni-page` 祖先默认不 100% 拉伸,导致 `position:fixed; inset:0` 的 `.room` 相对偏小祖先盒子定位 → `App.vue` 的 H5 条件块给 `html/body/#app/uni-app/uni-page*` 全部 `width/height:100%``.room` 加 `100vw/100vh/z-index:1000` 二道防御。(4) **刷新后残留会议静默清理**`joinAndEnter/createAndEnter` 首次走 `silent:true` 不弹 toast后端 `staleMeetingHint` 类错误自动调 `cleanupStaleMeetings()` + 重试一次;`leaveRoom({silent:true})` 抑制"你不在此会议中"toast。(5) **预览↔会议内视频统一 HD**`preview.vue` + `store/meeting.js#startLocalVideo` 统一 `width:{ideal:1280} height:{ideal:720} frameRate:{ideal:24,max:30}`。(6) **成员/邀请/聊天按钮 toggle**`openMembers/openInvite/openChat` 全改为取反,符合"再次点击关闭"直觉。验证Playwright 实测所有 toolbar 按钮 + close/textarea/send + 成员二次点击均 `hitSelf=true`,离开弹窗取消不再误触结束。相关提交:`cacf4ae` / `a311c03` / `3aa8174` / `ff46dc5` / `ce09315` / `c529ff4` / `0c9f048` / `16225c4` / `1222bf5` / `59f824a` / `eb3857a`。下一步 Task 13 主持人权限全链路。

View File

@@ -1,9 +1,9 @@
# Phase 2e 设计文档:会议与通知系统 # Phase 2e 设计文档:会议与通知系统
> **状态:** 🚧 进行中2e-1 ✅ 已完成2e-2 📋 **设计阶段完成待进入编码**2e-3 📋 待开发) > **状态:** 🚧 进行中2e-1 ✅ 已完成2e-2 **已完成**2e-3 📋 待开发)
> **分支:** `feature/phase2e-meeting-notification`2e-1`feature/phase2e-2-meeting-mvp`2e-2基于 `origin/feature/phase2c-group-read-receipt` > **分支:** `feature/phase2e-meeting-notification`2e-1`feature/phase2e-2-meeting-mvp`2e-2基于 `origin/feature/phase2c-group-read-receipt`
> **前置依赖:** Phase 2a联系人 + WS、Phase 2b即时通讯、Phase 2c群聊+已读、Phase 2d消息类型扩展全部完成 > **前置依赖:** Phase 2a联系人 + WS、Phase 2b即时通讯、Phase 2c群聊+已读、Phase 2d消息类型扩展全部完成
> **最后更新:** 2026-04-24Phase 2e-2 Task 0-14完成:PoC / media-server 骨架 / REST / DDL / Go 骨架 / 会议 REST / WS 信令 / Go↔Node HTTP / 生命周期 / 前端 mediasoup / 会议前后端页面 / 会议聊天 / meeting_invite 卡片 / docker-compose 双态;剩 Task 15 UI 打磨 + Task 16 E2E 收官 > **最后更新:** 2026-04-24Phase 2e-2 Task 0-16全部完成:Task 15 UI 打磨 + 主持人四件套 + Task 16 code-reviewer 全栈审计 + 4 P0/8 P1/7 P2/7 Nit 修复闭环 + 5 项登记推迟 + 资源生命周期专项审计 + Playwright 5 场景剧本 + test-report-phase2e-2-meeting.md 落盘Phase 2e-2 整体切 ✅,待合入主干
> **子阶段专用文档:** > **子阶段专用文档:**
> - 2e-1`docs/plans/2026-04-20-phase2e-1-design.md` + `docs/plans/2026-04-20-phase2e-1-implementation.plan.md`(✅ 已完成,含 TabBar「我的」未读红点优化 > - 2e-1`docs/plans/2026-04-20-phase2e-1-design.md` + `docs/plans/2026-04-20-phase2e-1-implementation.plan.md`(✅ 已完成,含 TabBar「我的」未读红点优化
> - 2e-2`docs/plans/2026-04-21-phase2e-2-design.md`16 章节)+ `docs/plans/2026-04-21-phase2e-2-implementation.plan.md`17 个 Task约 17 人日) > - 2e-2`docs/plans/2026-04-21-phase2e-2-design.md`16 章节)+ `docs/plans/2026-04-21-phase2e-2-implementation.plan.md`17 个 Task约 17 人日)
@@ -19,7 +19,7 @@
**核心交付物(按子阶段):** **核心交付物(按子阶段):**
- **Phase 2e-1 通知系统**3-4 人日)✅ 已完成:统一通知中心 + 11 种通知类型预留 + 跨模块 Pusher 接口 + TabBar「我的」未读红点 - **Phase 2e-1 通知系统**3-4 人日)✅ 已完成:统一通知中心 + 11 种通知类型预留 + 跨模块 Pusher 接口 + TabBar「我的」未读红点
- **Phase 2e-2 会议 MVP**(约 17 人日)📋 设计阶段完成mediasoup Node 媒体服务 + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + 双态部署(本机/公网 coturn+ 桌面/手机响应式 - **Phase 2e-2 会议 MVP**(约 17 人日)✅ 已完成mediasoup Node 媒体服务 + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + 双态部署(本机/公网 coturn+ 桌面/手机响应式 + 全栈 code-reviewer 审计闭环(验证报告:`test-report-phase2e-2-meeting.md`
- **Phase 2e-3 会议增强**7-10 人日):预约会议 + 定时提醒 + 等候室/锁定会议 + 设备预览高级参数(降噪/回声/虚拟背景) - **Phase 2e-3 会议增强**7-10 人日):预约会议 + 定时提醒 + 等候室/锁定会议 + 设备预览高级参数(降噪/回声/虚拟背景)
**不包含(明确推迟):** 见 [§九 后续规划清单](#九后续规划清单必须留档) **不包含(明确推迟):** 见 [§九 后续规划清单](#九后续规划清单必须留档)

View File

@@ -1,11 +1,11 @@
# Phase 2e-2 实施计划:会议 MVP多人音视频 # Phase 2e-2 实施计划:会议 MVP多人音视频
> **状态:** 🚧 代码开发Task 0-15 ✅ / Task 16 待执行 > **状态:** 代码开发完成Task 0-16 全部 ✅
> **设计文档:** [Phase 2e-2 设计文档](./2026-04-21-phase2e-2-design.md) > **设计文档:** [Phase 2e-2 设计文档](./2026-04-21-phase2e-2-design.md)
> **上级路线图:** [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md) > **上级路线图:** [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md)
> **分支:** `feature/phase2e-2-meeting-mvp` > **分支:** `feature/phase2e-2-meeting-mvp`
> **预估总工时:** **约 17 人日**17 个 Task含 PoC 与 UI 打磨) > **预估总工时:** **约 17 人日**17 个 Task含 PoC 与 UI 打磨)
> **最后更新:** 2026-04-23Task 15UI 打磨 + 主持人权限四件套完成6 项原创 UI 特色落地 / 说话者探测双源RTP stats + WebAudio / `SelfVideoFloat` 桌面恒浮窗 + 图钉切换 / `isAllMuted` 静音氛围色 / `NetworkBadge` 3 条波浪 / 主持人"请他静音/开麦/转让/踢出"四件套 / 4 屏 design-system 文档 / Playwright MCP 7 屏截图回归通过。下一步 Task 16 E2E + 文档同步 > **最后更新:** 2026-04-24Task 16E2E 总回归 + 文档同步完成code-reviewer 全栈审计 4 P0 + 8 P1 + 7 P2 + 7 Nit 闭环修复5 项登记推迟至 Phase 2f/3资源生命周期专项审计前端 Pinia _reset + _broadcastSelfState timer / 后端 Redis resourceTrackKey + memberStateKey + lifecycle timerPlaywright MCP 5 场景 + 手工回归剧本落盘 `.playwright-mcp/task16/`test-report-phase2e-2-meeting.md 落盘4 commitscdaa39d P0 / ea2bf96 P1 / f5ae095 资源生命周期 / 5ed14c2 E2E 剧本 / c35097a P2+Nit
### 进度看板 ### 进度看板
@@ -27,7 +27,7 @@
| **Task 13** | **meeting_invite 通知对接** | ✅ | extra 补齐 inviter_* / expired_at前端"立即加入/稍后"+ 过期态 + deep-link | | **Task 13** | **meeting_invite 通知对接** | ✅ | extra 补齐 inviter_* / expired_at前端"立即加入/稍后"+ 过期态 + deep-link |
| **Task 14** | **docker-compose + 双态** | ✅ | `media-server` + `coturn(public)` 编排 + 3 份 .env 模板 + `deploy-public.sh` + `docs/deployment/meeting-mvp.md` | | **Task 14** | **docker-compose + 双态** | ✅ | `media-server` + `coturn(public)` 编排 + 3 份 .env 模板 + `deploy-public.sh` + `docs/deployment/meeting-mvp.md` |
| **Task 15** | **`ui-ux-pro-max` UI 打磨 + 主持人权限四件套** | ✅ | 6 项原创 UI流光/柔性网格/浮窗/氛围色/滑入/波浪)+ 双源说话者探测 + 四件套 + 4 份设计文档 + Playwright MCP 7 屏回归 | | **Task 15** | **`ui-ux-pro-max` UI 打磨 + 主持人权限四件套** | ✅ | 6 项原创 UI流光/柔性网格/浮窗/氛围色/滑入/波浪)+ 双源说话者探测 + 四件套 + 4 份设计文档 + Playwright MCP 7 屏回归 |
| Task 16 | E2E + 文档同步 | | | | Task 16 | E2E + 文档同步 | | code-reviewer 审计 + P0/P1/P2/Nit 闭环 + 资源清理审计 + Playwright 5 场景 + 测试报告 |
### 2026-04-22 UI 打磨专项Task 10-12 之后的体验收敛) ### 2026-04-22 UI 打磨专项Task 10-12 之后的体验收敛)
@@ -597,6 +597,25 @@ flowchart LR
- 清理所有 TODO / FIXME / `console.log` 调试残留 - 清理所有 TODO / FIXME / `console.log` 调试残留
- **工作量****1 人日** - **工作量****1 人日**
- **实际交付2026-04-24 ✅)**
- **code-reviewer 全栈审计**`docs/reviews/2026-04-23-phase2e-2-code-review.md`4 P0 + 8 P1 + 8 P2 + 11 Nit
- **修复闭环**(详见 commit history
- `cdaa39d` 4 P0producer/consumer/transport 归属校验 / CreateRoom Router 失败补偿 / 密码 in-memory store
- `ea2bf96` 8 P1主持人转让原子事务 / List* 走 DAO 分页 / goroutine trace_id 透传 / broadcast ack 重试 / TTL 分布式锁兜底 / REST+WS 顺序归一等)
- `f5ae095` 资源生命周期专项MeetingLifecycleService.OnRoomEnded 定时器统一撤销 / Redis `resourceTrackKey` + `memberStateKey` 显式 DEL / 前端 Pinia `_reset` + `_pendingBroadcastTimers` 清理)
- `c35097a` 7 P2 + 7 Nittransport 清理 / preview 竞态防抖 / redirect return / chat 长度+频率限制 / kicked label / SplitN / 资源 TTL 中央化 / CheckOrigin 白名单 / doRequest 超时+重试 / REDIS_PASSWORD 联动校验 / isPrivatePath path 匹配 + 走读复核)
- 5 项登记推迟P2-4 WS token、P2-5 ChatService 拆分、docker-compose 端口收敛、producer.appData 校验、时间格式 RFC3339 统一),全部写入审查报告"推迟到独立阶段"小节
- **Playwright MCP 5 场景剧本**`.playwright-mcp/task16/scenarios/` commit `5ed14c2`
- `01-create-and-join-by-code.md`:发起会议 + 会议号入会 + 互推音视频
- `02-invite-link-with-password.md`:邀请链接 + 密码入会
- `03-notify-quickjoin.md`:通知中心一键入会
- `04-host-toolkit-and-transfer.md`:主持人四件套 + 主动/被动转让
- `05-reopen-5times-resource-cleanup.md`:连续 5 次开/散会资源清理回归
- `manual-regression.md`:纯手工验收点 checklist
- **文档同步**:本计划 + `CURRENT_STATUS.md` + `project-context.mdc` + `phase2e-design.md` + code-review 报告 + `test-report-phase2e-2-meeting.md` 落盘
- **清理复核**grep console.log/TODO/FIXME 确认 0 残留(仅保留结构化日志入口)
- **构建验证**go vet / go build / npm run build:h5 / npx tsc 全绿
--- ---
## 五、工作量汇总 ## 五、工作量汇总

View File

@@ -1,14 +1,101 @@
# EchoChat 项目开发进度 # EchoChat 项目开发进度
> **最后更新**2026-04-23Phase 2e-2 Task 15 UI 打磨 + 主持人权限四件套完成 + 媒体层稳定性补丁 + Task 16 全栈 code-reviewer 审计 + P0×4 / P1×7 修复落地 > **最后更新**2026-04-24Phase 2e-2 Task 16 ✅ 代码审查 + 资源生命周期审计 + P0/P1/P2/Nit 修复闭环 + Playwright 5 场景剧本 + 测试报告落盘
> **当前阶段**Phase 2e-2 会议 MVP **代码开发阶段** 🚧Task 0-15 ✅ / Task 16 进行中:审计 ✅ / P0 ✅ / P1 ✅ / P2+Nit + E2E 脚本 + 文档同步待执行 > **当前阶段**Phase 2e-2 会议 MVP **已完成** Task 0-16 全部 ✅,分支 `feature/phase2e-2-meeting-mvp` 待合并主干
> **当前分支**`feature/phase2e-2-meeting-mvp`(从 `feature/phase2c-group-read-receipt` 衍生) > **当前分支**`feature/phase2e-2-meeting-mvp`(从 `feature/phase2c-group-read-receipt` 衍生)
> **Phase 2e 整体设计**`docs/plans/2026-04-20-phase2e-design.md`(三子阶段路线图 + 后续规划清单) > **Phase 2e 整体设计**`docs/plans/2026-04-20-phase2e-design.md`(三子阶段路线图 + 后续规划清单)
> **Phase 2e-1 专用设计**`docs/plans/2026-04-20-phase2e-1-design.md`(✅ 已完成) > **Phase 2e-1 专用设计**`docs/plans/2026-04-20-phase2e-1-design.md`(✅ 已完成)
> **Phase 2e-1 实施计划**`docs/plans/2026-04-20-phase2e-1-implementation.plan.md`(✅ 11 个 Task 全部完成) > **Phase 2e-1 实施计划**`docs/plans/2026-04-20-phase2e-1-implementation.plan.md`(✅ 11 个 Task 全部完成)
> **Phase 2e-1 验证报告**`test-report-phase2e-1-notification.md` > **Phase 2e-1 验证报告**`test-report-phase2e-1-notification.md`
> **Phase 2e-2 专用设计**`docs/plans/2026-04-21-phase2e-2-design.md`📋 设计阶段16 章节) > **Phase 2e-2 专用设计**`docs/plans/2026-04-21-phase2e-2-design.md`✅ 已完成16 章节)
> **Phase 2e-2 实施计划**`docs/plans/2026-04-21-phase2e-2-implementation.plan.md`📋 17 个 Task 共约 17 人日 > **Phase 2e-2 实施计划**`docs/plans/2026-04-21-phase2e-2-implementation.plan.md` 17 个 Task 全部完成
> **Phase 2e-2 代码审查**`docs/reviews/2026-04-23-phase2e-2-code-review.md`4 P0 + 8 P1 + 8 P2 + 11 Nit已闭环 26 项 / 推迟 5 项)
> **Phase 2e-2 验证报告**`test-report-phase2e-2-meeting.md`
> **Phase 2e-2 E2E 剧本**`.playwright-mcp/task16/scenarios/`5 场景 + 手工回归 checklist
---
## 🏁 2026-04-24 Phase 2e-2 Task 16E2E 总回归 + P2/Nit 收尾 + 资源生命周期审计 + 文档同步 ✅
**交付**Phase 2e-2 会议 MVP 全部 17 个 Task 收官。Task 16 在 2026-04-23 完成 code-reviewer 全栈审计 + P0×4 + P1×8 修复commit `cdaa39d` / `ea2bf96`2026-04-24 继续完成:资源生命周期专项审计 + Playwright MCP 5 场景剧本化 + P2×7 + Nit×7 闭环 + 5 项登记推迟 + 文档同步。构建链 `go vet ./...` / `go build ./...` / `npm run build:h5` / `npx tsc --noEmit` 全绿。
### 本次增量2026-04-24
#### 1. 资源生命周期专项commit `f5ae095`
针对"会议结束后资源必须清理干净"的硬性约束,从前端 Pinia / 后端 Redis / lifecycle 定时器三维度补齐:
- **前端 `store/meeting.js`**
- `_broadcastSelfState` 重试 setTimeout 通过 `_pendingBroadcastTimers: Set<number>` 追踪,`_cleanupMedia``exitEndedRoom` 集中清理,杜绝会议结束后计时器残留回调
- `_onRoomEnded` 补齐 `_cleanupMedia` + `_unregisterListeners`;新增 `exitEndedRoom` action 供用户主动退出"已结束"态时触发完整 `_reset`
- `room.vue``onUnload` 钩子:离页时若仍持有 MediaEngine显式 `_cleanupMedia` 兜底
- **后端 `meeting_signal_service.go`**
- 新增 `cleanupRoomRedisResidual`DEL `resourceTrackKey:*` + `memberStateKey`),由 `EndRoom` / `HandleEmptyRoomExpired` / `HandleHostGraceExpired` 统一触发
- 与 P2-1 CloseTransport 形成"mediasoup 资源 + Redis 信令状态"双重闭环
- **后端 `meeting_lifecycle_service.go`**
- 新增 `OnRoomEnded(ctx, code)` 中央清理钩子:撤销 hostGrace / emptyTTL 的 local timer + DEL Redis key避免会议提前结束后定时器继续烧 CPU
- `EndRoom` 路径结束后调用 `OnRoomEnded``HandleHostGraceExpired` / `HandleEmptyRoomExpired` 各自执行完也调用(幂等)
#### 2. Playwright MCP 5 场景剧本commit `5ed14c2`
`.playwright-mcp/task16/scenarios/` 落盘 5 份场景脚本 + 1 份手工回归 checklist + 1 份 README
| 编号 | 文件 | 覆盖要点 |
|---|---|---|
| 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` | 纯手工验收点(浏览器权限、移动端横竖屏、设备热插拔) |
`.gitignore` 同步精细化:`.playwright-mcp/**/*.png|.log|.yml|.txt|.jpg` 默认忽略 + Task 16 Markdown 剧本精确白名单放行(历次 `task15/` 截图仍保持不跟踪)。
#### 3. P2/Nit 收尾批次commit `c35097a`
**P2 × 7 修复**
| ID | 范围 | 修复点 |
|---|---|---|
| P2-1 | meeting_signal_service / media-server / orchestrator | `cleanupUserResources` 新增 `transport` 分支media-server 新增 `DELETE /internal/v1/transports/:id`Go 端 `MediaOrchestrator.CloseTransport``doCloseRequest` 幂等重试 |
| P2-2 | `preview.vue` | `previewSeq` 序号 + `nextTick` 后 stale 判断丢弃过期 getUserMedia 结果;`onVideoChange`/`onAudioChange``scheduleRestartPreview` 200ms 防抖;`onBeforeUnmount` 清理定时器 |
| P2-3 | `room.vue` | `onLoad` redirectTo 时置 `redirectingToJoin=true` + return`onMounted` 早期 return 跳过初始化,杜绝跳转页面的闪 UI |
| P2-6 | `meeting_service.go` | `generateUniqueRoomCode` 重试成功时 Warn 码空间健康度;耗尽时 Error + `ErrRoomCodeConflict` |
| P2-7 | `meeting_service.go` + controller | `SendChatMessage` 服务端 `utf8.RuneCountInString` 500 字符上限Redis INCR + EXPIRE 滑动窗口 30 条/60s新错误类型 `ErrChatContentEmpty` / `ErrChatContentTooLong` / `ErrChatRateLimited` 映射 HTTP 400 |
| P2-8 | `constants/meeting.js` + `store/meeting.js` + `meeting_signal_service.go` | 新增 `MEETING_ENDED_REASON_KICKED` + label 文案;后端 `OnWSDisconnect` 硬编码 `"ws_disconnect"``constants.MeetingLeftReasonDisconnect` |
| - | - | P2-4WS token 迁出 URL query/ P2-5ChatService 拆分)登记推迟至 Phase 2f 安全 & 架构专项 |
**Nit × 7 修复 / 走读**
| ID | 修复点 |
|---|---|
| SplitN | `meeting_signal_service.go``kind:id` 解析两处统一使用 `strings.SplitN` |
| resourceTTL | 中央化到 `constants.MeetingResourceTrackTTLSeconds = 3600` |
| CheckOrigin | `ws/handler.go``config.Server.WSAllowedOrigins` + `Mode` 收敛dev 模式放行 / release 同源放行 + 严格白名单 |
| doRequest | 默认 `TimeoutMS` 5000→10000ms 兼容 mediasoup Worker 冷启动;新增 `CreateRouterRetry`(默认 1 次、300ms 退避)仅对非 404 错误重试 |
| REDIS_PASSWORD | `deploy-public.sh` 追加 `REDIS_PASSWORD` × `redis.conf` requirepass 联动校验;`redis.conf` 添加公网部署 TODO 注释 |
| isPrivatePath | `media-server/internal-auth.ts` 剔除 `?`/`#` 后再匹配白名单,避免 query 语义混淆 |
| inflight lock | `mediasoup-client.js` 走读确认 `finally` 已覆盖 resolve/reject 两路,追加注释强化 |
| cleanup 粒度 | `meeting.js` 走读复核:`_onMemberLeft`(整槽关闭) vs `_onProducerNew closed=true`(精确匹配 producerId) 粒度正确,无需改动 |
| docker 端口 / appData 校验 / RFC3339 | 登记推迟(见审查报告推迟登记表) |
#### 4. 文档同步
- `docs/reviews/2026-04-23-phase2e-2-code-review.md` 追加"Task 16 修复追踪2026-04-24 更新)"小节,含已修复表 + 推迟登记表
- `docs/plans/2026-04-21-phase2e-2-implementation.plan.md` Task 16 切 ✅ + 实际交付小节
- `CURRENT_STATUS.md`(本文件)收尾
- `test-report-phase2e-2-meeting.md` 落盘(本次新增)
- `project-context.mdc` 当前进度同步
### Phase 2e-2 完成度汇总
| 维度 | 状态 |
|---|---|
| Task 0-16 | 17 个 Task 全部 ✅ |
| 代码审查 | 4 P0 ✅ / 8 P1 ✅ / 7 P2 ✅ + 1 P2 推迟 / 7 Nit ✅ + 4 Nit 推迟 |
| 构建 | go vet / go build / npm run build:h5 / npx tsc --noEmit 全绿 |
| E2E | 5 场景剧本 + 手工回归 checklist 落盘,待用户 HTTPS 双端实机回归 |
| 分支 | `feature/phase2e-2-meeting-mvp`(待合入主干) |
--- ---

View File

@@ -0,0 +1,277 @@
# 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` 具备合并主干条件。