feat(deploy): Task 14 完成 docker-compose 扩展 + 环境变量双态部署
- deploy/docker-compose.dev.yml 新增 media-server 服务 + coturn(profiles:public),
全部服务参数改为 env 变量注入,go-service 依赖 media-server
- deploy/.env.example / .env.local.example / .env.public.example 三份模板,
公网模板用 _REPLACE_WITH_*_ 占位符防"示例值上生产"
- scripts/start.sh / stop.sh / status.sh 新增 full 子命令对接 docker compose --profile public
- scripts/deploy-public.sh 新建:env 校验 + 端口 checklist + Docker 自检 +
--profile public up -d --build + 健康检查闭环
- docs/deployment/meeting-mvp.md 新建:双态部署全流程 + 强密码生成 + 防火墙 + FAQ
- 双模式 docker compose config 校验通过;deploy-public.sh 三种错误场景按预期退出
同步更新 CURRENT_STATUS / implementation.plan / project-context
Task 0-14 ✅ / 剩余 Task 15-16
Made-with: Cursor
This commit is contained in:
@@ -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-13 ✅ / Task 14-16 待执行):mediasoup Node.js 独立 `media-server/` + 即时会议(≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + **会议生命周期状态机**(host 宽限期 + 自动转让 + 空房 TTL)+ 双态部署(本机 + 公网 coturn)+ 响应式(桌面/手机)
|
||||
- 2e-2 会议 MVP(约 17 天)🚧 **代码开发中**(Task 0-14 ✅ / Task 15-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` 级联关闭下游 consumer;Consumer 强制 `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,较首版 +2pp);9 接口 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/Nits(m4/m6~m10、n1~n10)登记至 Task 16 收尾清单
|
||||
* **Task 3 ✅ Go meeting 模块数据库 DDL + Model + DAO 完成(2026-04-21)**:三张持久化表(`meeting_rooms` / `meeting_participants` / `meeting_chats`)落地 PostgreSQL,DDL 同时写入 `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 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 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+ action,WS 监听进房注册/离房注销);扩展 `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 全部 Consumer,Task 11 重构为 `Map<producerId, consumer>` 精细索引;(c) Chrome 两 tab 手测 + WS 断线重入会需用户本地运行验证,Task 16 补 Playwright 自动化脚本回归
|
||||
|
||||
Reference in New Issue
Block a user