docs(phase2e-2): 同步 Task 10-12 + UI 打磨进度至 CURRENT_STATUS / 实施计划 / project-context

- CURRENT_STATUS.md:
  * 头部"最后更新"升级到 2026-04-22
  * 新增完整段落记录 Task 10-12 + 一轮深度 UI 打磨
    (uni-button ::after 全视口遮罩 / toolbar z-index 提升 /
    面板 toggle / 全局 H5 容器铺满 / 静默 stale 清理 /
    视频 HD / 会议聊天真实昵称)
  * 附 Playwright 验证结论与相关 commit 列表

- 2026-04-21-phase2e-2-implementation.plan.md:
  * 顶部状态改为"代码开发中(Task 0-12  / Task 13-16 待执行)"
  * 新增 16 行进度看板表格 + 2026-04-22 UI 打磨专项要点
  * Task 10/11/12 header 标注  完成日期

- .cursor/rules/project-context.mdc:
  * 当前进度条目更新为"Task 0-12 "
  * 新增 Task 10-12 + UI 打磨的独立条目
    完整收录三轮攻坚(uni-button 遮罩两次 + z-index +
    全局容器 + stale 静默 + HD + toggle)的技术细节

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-22 16:45:56 +08:00
parent cacf4ae074
commit d218b84843
3 changed files with 127 additions and 8 deletions

View File

@@ -27,7 +27,7 @@ alwaysApply: true
* 单端 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`
* API 文档:`docs/api/frontend/notify.md`
- 2e-2 会议 MVP约 17 天)🚧 **代码开发中**Task 0-9 ✅ / Task 10-16 待执行mediasoup Node.js 独立 `media-server/` + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + **会议生命周期状态机**host 宽限期 + 自动转让 + 空房 TTL+ 双态部署(本机 + 公网 coturn+ 响应式(桌面/手机)
- 2e-2 会议 MVP约 17 天)🚧 **代码开发中**Task 0-12 ✅ / Task 13-16 待执行mediasoup Node.js 独立 `media-server/` + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + **会议生命周期状态机**host 宽限期 + 自动转让 + 空房 TTL+ 双态部署(本机 + 公网 coturn+ 响应式(桌面/手机)
* 专用设计:`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详见设计文档 §三
* **重要修订**`meeting_rooms.password` → `password_hash`bcrypt新增 `meeting_chats` 表 + `ended_reason` / `left_reason` 字段,新增 `echo:meeting:invite:{token}` / `host_grace:{code}` Redis key
@@ -36,6 +36,7 @@ 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 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 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 9 ✅ 前端 mediasoup-client + Pinia Store 落地2026-04-21**:前端新增 `constants/meeting.js`150 行14 个 WS 事件 + 本地状态机常量)/ `api/meeting.js`120 行12 REST 全量封装)/ `utils/mediasoup-client.js`270 行,`createMediaEngine` 封装 Device/Transport/Producer/Consumer 全生命周期 + `#ifdef H5` 平台隔离 + `markRaw` 防 Vue 深度代理)/ `store/meeting.js`615 行Pinia Store本地状态机 + 8 个广播事件桥 + 20+ actionWS 监听进房注册/离房注销);扩展 `services/websocket.js` 新增 `sendWithAck(event, data, timeoutMS)` Promise 化接口 + `pendingAcks` Map + `_handleAck` 路由(自动识别 `*.ack` 后缀)+ `_rejectAllPendingAcks` 断线清理。**后端补齐**:新增 `meeting.consume.resume` WS 事件(`constants/meeting.go` + `MeetingSignalService.OnConsumeResume` + `MeetingWSHandler.handleConsumeResume` + `MediaOrchestrator.ResumeConsumer` 调 Node `POST /internal/v1/consumers/:id/resume``CreateMeetingRoomResponse` / `JoinMeetingRoomResponse` 新增 `router_id` / `rtp_capabilities``HTTPMediaOrchestrator` 用 `routerInfoCache{ID, RtpCapabilities}` 同时缓存两者 + `ResolveRouterInfo` 返回给前端供 `Device.load()`)。**临时调试页**`pages/meeting/debug.vue`280 行)会议状态面板 + 生命周期按钮 + 音/视频开关 + 远端参与者渲染 + 聊天面板,`<video>`/`<audio>` 用 `document.createElement` 原生 DOM 挂到 `<view>` 容器(绕过 uni-h5 Video/Audio 组件不支持 `srcObject` 的限制),`#ifdef H5` 保护非 H5 构建正常。**7 项决策锁定**Q1=H5 Only 多端策略 / Q2=npm dependencies / Q3=进房注册 WS 监听 / Q4=12 接口全量封装 / Q5=services 层 sendWithAck / Q6=**WS 暴露 meeting.consume.resume**(而非 Go 自动 resume理由官方规范 + DOM 挂完 track 再 resume 避免首帧黑屏 + 未来 simulcast/订阅变更/后台节流扩展点)/ Q7=debug.vue 页手测。验证:`go vet ./...` + `go test ./app/meeting/...` 全绿;`npm run build:h5` 通过,无本次新增 warning剩余 `chat.js/notify.js dynamic import` + Sass legacy API 告警均为 pre-existing。**已知遗留**(a) Consumer resume 当前在 store 层立即触发未等 `<video>.onloadedmetadata`,首帧可能 50-100ms 抖动Task 11 会议室主页将下移到页面层;(b) `_cleanupRemoteProducer` 当前粗粒度关闭 slot 全部 ConsumerTask 11 重构为 `Map<producerId, consumer>` 精细索引;(c) Chrome 两 tab 手测 + WS 断线重入会需用户本地运行验证Task 16 补 Playwright 自动化脚本回归
* **Task 8 ✅ 会议生命周期状态机 + Router 幂等双层防御2026-04-21**:设计 §6.5 的 5 类状态跃迁副作用host 掉线宽限期 / 宽限期内重连 / 自动转让 / 空房 TTL / TTL 过期销毁)全部落地;同步修复 Task 7 遗留的"CreateRoom + JoinRoom 重复调 `CreateRouter`"行为。产出:(1) `config/config.go` 新增 `MeetingConfig{HostGraceSeconds=120, EmptyRoomTTLSeconds=300, CleanupIntervalSeconds=30, StaleRoomHours=4}` + dev/docker yaml(2) `app/meeting/service/meeting_lifecycle_service.go`(新 470 行)封装 `OnHostDisconnect` / `OnHostReconnect` / `HandleHostGraceExpired` / `OnAllMembersLeft` / `CancelEmptyTTL` / `HandleEmptyRoomExpired` 6 钩子,`sync.Map` 管理本地 `time.AfterFunc` timer + Redis key 双保险;`RescheduleFromRedis` 重启按剩余 PTTL 重建 timer**Redis key TTL = 业务时长 + max(CleanupIntervalSeconds*2, 30s) buffer**(调试期实测发现若 Redis TTL 与本地 timer 同步到期,`DEL` 会因 key 已被 Redis 自动过期返回 0 被误判为"已被其他路径处理"而跳过业务逻辑,加 buffer 让本地 timer 先触发 DEL 命中);并发保护靠入口 `DEL` 返回值1 = 本次处理0 = 已被其他路径处理);(3) `app/meeting/dao/meeting_room_dao.go` 新增 `ListStaleActive(hoursAgo, limit)` 扫陈旧活跃房间;(4) `app/meeting/service/interfaces.go` `MediaOrchestrator` 新增 `ResolveRouterID(roomCode) (string, bool)` 让业务层直接查缓存不创建;(5) `app/meeting/service/http_media_orchestrator.go` `CreateRouter` 入口查 `sync.Map` 命中直接返回HTTP 层幂等防御);(6) `app/meeting/service/meeting_service.go` `JoinRoom` **移除 `CreateRouter` 调用**改 `lifecycleSvc.CancelEmptyTTL` + `ResolveRouterID``LeaveRoom` 空房分支改调 `lifecycleSvc.OnAllMembersLeft` 不再立即销毁房间;(7) `app/meeting/service/meeting_signal_service.go` 新增 `OnWSDisconnect(userID)` 实现 `ws.MeetingDisconnectHook` 接口host 掉线调宽限期 + 非 host 仅清 media 资源,决策 `q1_nonhost_disconnect=a1_keep_current` 不动 participant 表,长期不活跃由 4 小时后台兜底),`OnRoomJoin` 追加 host 重连钩子;(8) `app/ws/handler.go` 定义 `MeetingDisconnectHook` 接口 + `SetMeetingDisconnectHook` 注入,解耦 ws → meeting 反向依赖;(9) `app/meeting/task/meeting_cleanup_task.go`(新 220 行)启动时 `RescheduleFromRedis`;每 `CleanupIntervalSeconds` 秒 `ScanExpired` 兜底扫 `host_grace:*` / `empty_ttl:*` + 扫 stale active rooms → `MarkEnded(reason=system_error)`;每小时清理 Ended 会议旧聊天;(10) `app/provider/{provider,wire,wire_gen}.go` + `cmd/server/main.go` 注入新 service/task + `wire.Bind(new(ws.MeetingDisconnectHook), new(*service.MeetingSignalService))` + 启动/停止 cleanup task(11) `media-server/src/app.ts` `/internal/info` 追加 `stats.routers` 供 E2E 断言 Router 幂等;(12) `docs/verify/meeting_t8_verify.mjs`(新 320 行5 场景 E2E 脚本(用 env 加速 TTL 到 3s 跑测)。**关键设计决策**(a) Redis TTL 加 buffer 避免 DEL 与自动过期并发误判(修复 bug 的核心);(b) 本地 timer 低延迟 + cleanup task 兜底,多实例由入口 DEL 保证唯一处理权;(c) Router 幂等双层防御(业务层 `JoinRoom` 不创建 + HTTP 层 `sync.Map` 命中直接返回,决策 `q2_router_dedup=a2_both`(d) 全员 leave 不再立即销毁房间,由 `empty_ttl` + 本地 timer 实现"可复活"语义;(e) 普通成员 WS 断开仅清 media 资源不动 DB。验证`go build`/`go vet`/`ReadLints` 全绿(保留 Task 7 遗留 pre-existing 警告);启动 media-server + go-service 跑 E2E **20/20 PASS**5 场景全部验证host 宽限期过期自动转让 + 宽限期内重连保留 + 空房 TTL 复活 + 空房 TTL 过期销毁 + JoinRoom 不再重复 Router
* **Task 7 ✅ HTTPMediaOrchestrator Go↔Node 媒体链路打通2026-04-21**`NoopMediaOrchestrator` 替换为真实 HTTP 实现,`MeetingService` / `MeetingSignalService` 的 9 个媒体操作全部走 `POST/DELETE /internal/v1/*` 调到 Node media-server。产出(1) `config/config.go` 新增 `MediaServerConfig{BaseURL, InternalToken, TimeoutMS(5000), CloseTimeoutMS(2000), CloseRetry(2)}` + dev/docker 两份 yaml 配置dev 直连 `localhost:3300`docker 走服务名 `media-server:3300`(2) `app/meeting/service/http_media_orchestrator.go`(新 340 行)用 `net/http` 标准库实现 `MediaOrchestrator` 8 方法,`context.WithTimeout` 驱动差异化超时,创建类一次性透传,**关闭类指数退避 200ms→500ms 最多 CloseRetry+1 次**(幂等安全);`X-Internal-Token` header 与 `media-server/.env MEDIA_INTERNAL_TOKEN` 配对;错误类型 `ErrMediaResourceNotFound`404与 `ErrMediaServerError`5xx/超时/网络错)可供 `errors.Is` 精准区分;**`sync.Map` 本地 `roomCode ↔ routerID` 缓存**兼容设计 §6.6 `CloseRouter(roomCode)` 签名但 Node 以 routerID 为主键的约束go-service 重启缓存丢失与 Node 重启 Router 释放状态自然同步;(3) `app/meeting/provider.go` 切换 `wire.Bind` 到 `*HTTPMediaOrchestrator``wire_gen.go` 再生;(4) `docs/verify/meeting_t7_verify.mjs`(新 140 行E2E 验证脚本。**关键设计决策**(a) 关闭类幂等重试 vs 创建类一次性透传 —— 防孤儿资源;(b) 错误语义二分 —— 让 404 关闭转 nil、5xx 透传 WS ACK(c) 接口维持 8 方法不含 `ResumeConsumer` —— Node REST 已就绪但 WS 契约未暴露,留给 Task 9 前端 mediasoup-client 接入时按需补齐。**已知遗留**(i) CreateRoom + JoinRoom 各自调 `CreateRouter` 重复创建Task 8 需改为"仅首次创建 Router 其余复用"(ii) `/transports/:id/stats` Node 暂未实现Task 10 可观测性补齐。验证:`go build`/`go vet` 全绿;启动 media-server(Node 3300) + go-service(Go 8085)E2E **16/16 PASS** —— REST 创建/加入/结束会议触发 Router 创建/销毁WS `transport.create` 返回真实 mediasoup `id`(非 noop- 前缀)+ 非空 `iceCandidates[]` + `dtlsParameters.fingerprints[]`,关闭虚构 producer 触发 Node 404 → Go 幂等转 okmedia-server 日志对齐 `router created × 2 / webrtc transport created / router closed explicitly`

View File

@@ -1,11 +1,47 @@
# Phase 2e-2 实施计划:会议 MVP多人音视频
> **状态:** 📋 待执行(设计文档定稿后进入代码开发
> **状态:** 🚧 代码开发中Task 0-12 ✅ / Task 13-16 待执行
> **设计文档:** [Phase 2e-2 设计文档](./2026-04-21-phase2e-2-design.md)
> **上级路线图:** [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md)
> **分支:** `feature/phase2e-2-meeting-mvp`
> **预估总工时:** **约 17 人日**17 个 Task含 PoC 与 UI 打磨)
> **最后更新:** 2026-04-21Task 0-8已落地,会议生命周期状态机 E2E 20/20 PASS下一步 Task 9 前端 mediasoup-client 接入
> **最后更新:** 2026-04-22Task 10-12前端会议 UI 主链路打通;叠加一轮深度 UI 打磨uni-button ::after 全视口遮罩修复、toolbar z-index 提升、面板 toggle、视频 HD、静默 stale 清理。下一步 Task 13 主持人权限全链路
### 进度看板
| Task | 标题 | 状态 | 说明 |
|---|---|---|---|
| Task 0 | mediasoup PoC Spike | ✅ | 2026-04-21 完成 |
| Task 1 | media-server 骨架 | ✅ | Fastify 5 |
| Task 2 | Node 9 个 REST API | ✅ | Router/Transport/Producer/Consumer |
| Task 3 | 数据库 DDL + DAO | ✅ | 3 张表 + 9 索引 |
| Task 4 | Go meeting 骨架 | ✅ | 12 路由 + Wire |
| Task 5 | 会议 REST 接口 | ✅ | 12 接口全量业务 |
| Task 6 | WS 信令处理器 | ✅ | 13 事件 |
| Task 7 | Go↔Node HTTP Client | ✅ | HTTPMediaOrchestrator |
| Task 8 | 生命周期状态机 | ✅ | E2E 20/20 PASS |
| Task 9 | 前端 mediasoup + Store | ✅ | REST/WS/媒体三链路 |
| **Task 10** | **前端预览/创建/加入页** | ✅ | `create.vue` / `join.vue` / `preview.vue` + Hub `index.vue` |
| **Task 11** | **会议室主页 + 核心组件** | ✅ | `room.vue` + VideoGrid/VideoTile/Toolbar/MemberPanel/InviteDialog |
| **Task 12** | **会议内聊天面板** | ✅ | `ChatPanel.vue` + WS `meeting.chat.new` + user_name/avatar 补齐 |
| Task 13 | meeting_invite 通知对接 | ⏳ | 下一步 |
| Task 14 | docker-compose + 双态 | ⏳ | |
| Task 15 | 观测性与告警 | ⏳ | |
| Task 16 | E2E + 文档同步 | ⏳ | |
### 2026-04-22 UI 打磨专项Task 10-12 之后的体验收敛)
这一轮不属于新 Task是对已落地页面的深度打磨统一记录在 CURRENT_STATUS.md 对应段落,要点速览:
1. **uni-button `::after` 全视口遮罩(两处)**
- MeetingToolbar`.btn::after { content:none }` + `.btn { position:relative }` 解除"离开按钮吃掉所有兄弟按钮点击"
- ChatPanel`.btn-send::after { content:none; display:none }` 解除因 `all:unset` 导致 `::after` 回溯到 body 形成全屏遮罩
2. **toolbar z-index 提升至 210**:覆盖 MemberPanel/InviteDialog 的 `.panel-root` (z=200) mask保证抽屉打开时按钮仍可点击 + toggle。
3. **成员/邀请/聊天按钮 toggle**`openMembers/openInvite/openChat` 全部改为取反切换,符合用户"再次点击关闭"的直觉。
4. **H5 全局容器铺满视口**`App.vue``html/body/#app/uni-app/uni-page*` 全部 `width/height: 100%`,修复"整体偏左上角"。
5. **刷新后残留会议的静默清理**`cleanupStaleMeetings` + `joinAndEnter/createAndEnter` 首次静默 + 自动重试;`leaveRoom({silent:true})` 抑制"你不在此会议中"toast。
6. **设备预览 ↔ 会议内同分辨率**:统一 1280×720 / 24-30fps。
7. **会议聊天显示真实昵称**:后端 `ResolveUsersDisplay` 批量补齐 `user_name` / `user_avatar`,前端 ChatPanel 显示昵称而非"用户 54"。
---
@@ -413,7 +449,7 @@ flowchart LR
- 关闭标签后另一端 1 秒内收到 `meeting.member.left`
- **工作量****1.5 人日**
### Task 10前端设备预览页 + 创建页 + 加入页
### Task 10前端设备预览页 + 创建页 + 加入页2026-04-21/22 完成)
- **目标**完成 `/pages/meeting/{create,join,preview}.vue` 3 个页面
- **依赖**T5
@@ -432,7 +468,7 @@ flowchart LR
- 权限被拒绝时给出清晰引导
- **工作量****1 人日**
### Task 11前端会议室主页视频网格 + 工具栏 + 成员面板 + 响应式)
### Task 11前端会议室主页视频网格 + 工具栏 + 成员面板 + 响应式)2026-04-21/22 完成,含一轮 UI 深度打磨)
- **目标**实现 `/pages/meeting/room.vue` + 6 个主要组件
- **依赖**T9
@@ -450,7 +486,7 @@ flowchart LR
- 手机浏览器单列主画面 + 缩略抽屉工作正常
- **工作量****2 人日**
### Task 12会议内聊天面板
### Task 12会议内聊天面板2026-04-21/22 完成)
- **目标**实现 `ChatPanel.vue` + 后端 `meeting_chats` 相关接口
- **依赖**T11

View File

@@ -1,7 +1,7 @@
# EchoChat 项目开发进度
> **最后更新**2026-04-21Phase 2e-2 Task 9 前端 mediasoup-client + Pinia Store 落地REST/WS/媒体三链路打通,`npm run build:h5` 通过
> **当前阶段**Phase 2e-2 会议 MVP **代码开发阶段** 🚧Task 0-9 ✅ / Task 10-16 待执行)
> **最后更新**2026-04-22Phase 2e-2 Task 10-12 前端会议 UI 主链路全部落地 + uni-button H5 遮罩深度修复 + 面板 toggle 交互打磨
> **当前阶段**Phase 2e-2 会议 MVP **代码开发阶段** 🚧Task 0-12 ✅ / Task 13-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,88 @@
---
## 🎯 2026-04-22 Phase 2e-2 Task 10-12 + UI 打磨:会议 MVP 前端主链路全部打通
**交付**Task 10创建/加入/设备预览三页、Task 11会议室主页 + 核心组件、Task 12会议内聊天面板顺序落地叠加一轮深度 UI 打磨,解决了 uni-app H5 `<uni-button>` 点击遮罩、全屏面板 z-index 覆盖、刷新后"残留会议"提示、视频清晰度、面板 toggle 交互等一整串体验问题,现前端 → REST → WS → 媒体 → 渲染 → 内置聊天链路全部可用。
### 产出文件(本轮新建 / 改动要点)
| 文件 | 角色 | 作用 |
|---|---|---|
| `frontend/src/pages/meeting/create.vue` (新) | Task 10 | 立即/预约创建表单,校验 + 跳转 preview |
| `frontend/src/pages/meeting/join.vue` (新) | Task 10 | 会议号/密码输入 + 识别 `xxx-yyy-zzz` 格式 |
| `frontend/src/pages/meeting/preview.vue` (新/改) | Task 10 + UI | 设备预览 + 默认启用开关;`getUserMedia` 升级到 1280x720 / 24-30fps和会议内同分辨率统一 |
| `frontend/src/pages/meeting/index.vue` (重写) | UI 打磨 | 从"功能开发中"占位改为 **会议 Hub**:即时创建 + 加入会议双入口卡片 |
| `frontend/src/pages/meeting/room.vue` (新/多轮改) | Task 11 + UI | 视频网格 + 顶部栏 + Toolbar + 抽屉聊天 + 离会弹窗;修复 z-index/刷新/toggle/输入遮挡等 5 个子问题 |
| `frontend/src/components/meeting/MeetingToolbar.vue` (新/改) | Task 11 + UI | 6 个工具按钮;**`.btn::after` 禁用修复点击穿透**`z-index: 210` 浮在成员面板 mask 之上 |
| `frontend/src/components/meeting/VideoGrid.vue` / `VideoTile.vue` (新) | Task 11 | 自适应 1/2/3-4/5-9/10+ 网格 + 本地/远端 tile + 主持人徽章 |
| `frontend/src/components/meeting/MemberPanel.vue` (新) | Task 11 | 右侧抽屉成员列表 + 踢出/转让菜单 |
| `frontend/src/components/meeting/InviteDialog.vue` (新) | Task 11 | 会议号/密码/邀请链接复制 |
| `frontend/src/components/meeting/ChatPanel.vue` (新/多轮改) | Task 12 + UI | 聊天抽屉 → 侧边栏布局,消息流 + 头像聚合 + 懒加载更多;**`.btn-send::after` 彻底 `content:none`** 解决关闭/输入/发送全部被遮挡 |
| `frontend/src/store/meeting.js` (改) | Task 10-12 + UI | 新增 `cleanupStaleMeetings`(静默清理)+ `createAndEnter`/`joinAndEnter` 自动重试 + 视频约束 HD |
| `frontend/src/api/meeting.js` (改) | UI | `createRoom`/`joinRoom`/`leaveRoom` 支持 `options.silent` 透传 |
| `frontend/src/utils/request.js` (改) | UI | 请求层新增 `silent` 选项抑制 toast给 stale 清理等场景用 |
| `frontend/src/App.vue` (改) | UI | H5 全局 CSS`html/body/#app/uni-app/uni-page/uni-page-body` 均 100% 宽高,修复"整体布局偏左上" |
| `backend/go-service/app/meeting/service/meeting_service.go` (改) | UI/Task 12 | `UserDisplayInfo` + `resolveUserDisplay``SendChatMessage` WS 载荷带 `user_name`/`user_avatar` |
| `backend/go-service/app/meeting/controller/meeting_controller.go` (改) | UI/Task 12 | `chatToDTO` 支持 `userMap``SendChat`/`ListChats``ResolveUsersDisplay` |
| `backend/go-service/app/dto/meeting_dto.go` (改) | UI/Task 12 | `MeetingChatDTO` 新增 `UserName` / `UserAvatar` |
### 核心技术决策与攻坚点
1. **uni-app H5 `<uni-button>::after` 点击遮罩**(两轮):
- **第一轮(工具栏)**:原生 `<button>` 被 uni-app 编译为 `<uni-button>`,自动注入 `::after { content:" "; position:absolute; inset:0 -1200px -80px 0 }` 作为点击反馈遮罩2400×160。在 MeetingToolbar 里,最后一个"离开"按钮的 `::after` **横跨整个 toolbar 并 DOM 顺序最上**,导致所有按钮的中心点命中检测都落在"离开"按钮上,表现为"除了离开按钮其他全都点不响应"、"离开弹窗的取消也触发结束会议"。修复:`.btn { position: relative } .btn::after { content: none; display: none }`,同步给 `.leave-btn::after` 加防御。
- **第二轮(聊天面板)**`.btn-send { all: unset }``position` 重置为 `static`,导致 `::after``absolute` 定位沿 DOM 树回溯到 `<body>` 作为 containing block`inset: 0 -1200px -80px 0` 直接形成**覆盖整个视口并向右下溢出的超级遮罩**,关闭按钮 / 输入框 / 发送按钮全部被挡。修复:把原本只 `border: none``.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 }`**,使得抽屉打开时 toolbar 浮在 mask 之上(仍低于 `.leave-mask` 的 220同时 `openMembers` / `openInvite` 改为 toggle`openChat` 行为对齐。
3. **`uni-app` 全局容器偏左上**:桌面大窗口下 `uni-app` / `uni-page` 祖先默认不 100% 拉伸,导致 `position: fixed; inset: 0``.room` 相对于偏小的祖先盒子定位,出现"左上角一小块"的 bug。修复`App.vue``/* #ifdef H5 */` 块统一给 `html, body, #app, uni-app, uni-page*` 全部 `width/height: 100%`,并给 `.room``100vw / 100vh / z-index: 1000` 作为二道防御。
4. **刷新后"残留会议"体验**
- `joinAndEnter` / `createAndEnter` 包 try/catch首次调用走 `silent: true`(不弹 toast若后端返回 `staleMeetingHint` 类错误则先调 `cleanupStaleMeetings()` → 再重试一次。
- `cleanupStaleMeetings` 内部调 `meetingApi.leaveRoom({ silent: true })`,把 `400: 你不在此会议中` 这类预期错误静默掉。
- 用户路径:刷新 → 回到 Hub → 再次 Join/Create → 不再看到"你已在其他会议中"或"你不在当前会议中"。
5. **预览/会议内视频画质一致性**`preview.vue``store/meeting.js#startLocalVideo` 原本用 640×480 → 统一改为 `width:{ideal:1280} height:{ideal:720} frameRate:{ideal:24,max:30}`,设备预览看到什么画质,入会后就是什么画质。
6. **会议内聊天**:后端 `SendChatMessage` WS 广播 + REST 列表均通过 `ResolveUsersDisplay` 批量补齐 `user_name/user_avatar`,前端 ChatPanel 显示真实昵称而非"用户 54"`ChatPanel` 由父 `room.vue` 通过 props 受控,消息分组聚合头像,滚动位置保持策略区分"用户上拉中 / 自动贴底"。
### Playwright 验证
- `MeetingToolbar` 每个按钮 `elementFromPoint(center) === 自身``hitSelf=true`)✅
- 离会弹窗"取消" `hitSelf=true`,点击后 `.leave-mask` 消失URL 不跳转 ✅
- 聊天面板关闭按钮、textarea、发送按钮 `hitSelf=true`
- 成员按钮两次点击 → 面板打开 → 面板关闭 ✅
- 聊天按钮 toggle、邀请按钮 toggle 同样生效 ✅
### 已知待改进项
1. **Task 13 管理员权限全链路**(主持人静音他人 / 踢出 / 转让 / 结束)仍需专项 E2E。
2. **Task 14 通知中心邀请接入**:收到 `meeting_invite` notification 直接跳转 Join 页面,待落地。
3. **Task 15 统一观测性**:会议内 RTC 状态、WS 重连、媒体 rtp 状态尚无 dashboard。
4. **Task 16 E2E**Playwright 回归脚本需补 "两用户跨 tab 互看 + 内置聊天 + 主持人操作" 全景脚本。
### 相关提交
```
cacf4ae fix(meeting): 提升 toolbar z-index 确保成员/邀请面板开启时仍可点击
a311c03 fix(meeting): 聊天面板点击失效 & 成员/邀请按钮支持 toggle
3aa8174 fix(meeting): 抹除 uni-button ::after 遮罩修复工具栏点击失效
ff46dc5 fix(phase2e-2): 视频 HD 分辨率 + toolbar 防溢出 + 聊天按钮 toggle
ce09315 feat(phase2e-2): 会议首页落地 "创建 / 加入" 双入口
c529ff4 fix(phase2e-2): 全局 H5 容器铺满视口 + 静默自动清理 stale 会议
0c9f048 fix(phase2e-2): 修复刷新后重入会失败 + 聊天输入区布局防御
16225c4 fix(phase2e-2): 会议内聊天面板 UI 布局优化
1222bf5 feat(phase2e-2): 会议内聊天面板落地Task 12
59f824a feat(phase2e-2): 会议室主页与核心 UI 组件落地Task 11
eb3857a feat(phase2e-2): 前端创建/加入/设备预览 3 页落地Task 10
```
### 下一步
进入 **Task 13主持人权限全链路**(静音他人 / 移除 / 转让 / 结束),前端 MemberPanel 已铺好行尾菜单,后端 REST/WS 事件已就绪,本 Task 聚焦前后端握手 + E2E。
---
## 🎯 2026-04-21 Phase 2e-2 Task 9 前端 mediasoup-client + Pinia Store 落地
**交付**:前端新增 5 个模块(`constants/meeting.js` / `api/meeting.js` / `services/websocket.js``sendWithAck` 扩展 / `utils/mediasoup-client.js` / `store/meeting.js`+ 1 个临时调试页(`pages/meeting/debug.vue`),把 REST12 接口)/ WebSocket14 事件)/ mediasoup 媒体三链路全部汇聚到 Pinia Store 对外提供 ~20 个 action后端补齐 `meeting.consume.resume` WS 事件 + `RouterRtpCapabilities` 透传到 REST 响应;`npm run build:h5` 通过(无本次新增 warning