# EchoChat 项目开发进度 > **最后更新**:2026-04-24(Phase 2e-2 Task 14 docker-compose 双态扩展完成:media-server + coturn 容器编排 + 三份 .env 模板 + deploy-public.sh + docs/deployment/meeting-mvp.md 双态部署指南) > **当前阶段**:Phase 2e-2 会议 MVP **代码开发阶段** 🚧(Task 0-14 ✅ / Task 15-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`(✅ 已完成) > **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-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 人日) --- ## 🚀 2026-04-24 Phase 2e-2 Task 14:docker-compose 扩展 + 环境变量双态开关完成 **交付**:EchoChat 会议 MVP 正式具备**本机 Demo / 公网部署**双态一键化能力。`deploy/docker-compose.dev.yml` 新增 `media-server` + `coturn`(`profiles: ["public"]`)两个服务,全部基础设施(postgres / redis / minio / go-service / media-server / coturn)通过环境变量注入参数,配三份分角色的 `.env.*.example` 模板;`scripts/start|stop|status.sh` 扩展 `full` 子命令用 docker compose 跑完整栈;新建 `scripts/deploy-public.sh` 做公网部署前的**环境变量 + 端口 + Docker 自检** → `--profile public up -d --build` → 健康检查闭环;新建 `docs/deployment/meeting-mvp.md` 双态部署指南。 ### 产出文件 | 文件 | 类型 | 作用 | |---|---|---| | `deploy/docker-compose.dev.yml`(改) | compose 编排 | 新增 `media-server`(env 注入 `MEDIASOUP_ANNOUNCED_IP` / `MEDIA_INTERNAL_TOKEN` / `MEDIASOUP_RTC_MIN_PORT` / `MEDIASOUP_RTC_MAX_PORT`)、`coturn`(`profiles:["public"]` + `network_mode: host` + env 注入 realm / user / credential / 端口段);`go-service` 新增 `depends_on: media-server`;postgres / redis / minio 全部改为 env 变量可覆盖 | | `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 注释 | | `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` 子命令 → `docker compose -f docker-compose.dev.yml --profile public stop` 优雅停止含 coturn 的全量容器 | | `scripts/status.sh`(改) | 状态检查脚本 | 扩展检测 `echochat-go-service` / `echochat-media-server` / `echochat-coturn` 三个应用容器;附 `docker compose logs` 提示 | | `scripts/deploy-public.sh`(新建,`chmod +x`) | 公网部署脚本 | 4 步闭环:`step_validate_env`(`.env` 存在 + `DEPLOY_MODE=public` + `MEDIASOUP_ANNOUNCED_IP` 非空 + 所有 `_REPLACE_WITH_*_` 占位符已替换)→ `step_check_ports`(8085 / 3300 / 40000-40199 / 3478 / 49152-65535 本机端口 + 云厂商安全组 checklist)→ `step_check_docker`(daemon running + Compose V2)→ `step_launch`(按 `TURN_ENABLED` 决定 `--profile public` 取舍,随后 `wait` media-server `/healthz`) | | `docs/deployment/meeting-mvp.md`(新建) | 部署指南 | 双态部署完整流程:本机 Demo(`cp .env.local.example .env && scripts/start.sh full`);公网(复制 `.env.public.example` → 替换占位符 → `scripts/deploy-public.sh`);验证 + FAQ(`python3/g++` / 视频问题 / `coturn` 网络模式 / HTTPS / 数据库备份) | | `docs/plans/2026-04-21-phase2e-2-implementation.plan.md`(改) | 进度文档 | Task 14 行标记 ✅ + 展开详细产出 + 验证记录 | ### 关键技术点 1. **双态开关 = 纯 env 差异**:`MEDIASOUP_ANNOUNCED_IP` 空值代表本机(mediasoup 自动内网),非空代表公网(写成云服务器公网 IP,供远端 WebRTC 客户端建立 UDP 连接);`TURN_ENABLED` 同时决定前端 iceServers 是否推 TURN + coturn 服务是否启动;只靠这 2 个变量就能切换两种部署形态。 2. **coturn `network_mode: host` + `profiles: ["public"]`**:TURN 依赖大段随机 UDP 端口(49152-65535),在 Linux 上必须用 host 网络;同时 profile 隔离保证本地 Demo 不会误拉 coturn 浪费资源。`docker compose --profile public up` 才会启动它。 3. **`deploy-public.sh` 的占位符自动校验**:扫描 `.env` 里是否仍有 `_REPLACE_WITH_*_` 字样,没替换干净直接退出并列出未替换字段,杜绝「示例值带上生产环境」的低级事故。 4. **Compose V2 `--profile` 位置敏感**:发现 `docker compose -f x.yml config --profile public` 会被老版本语法误解(`--profile` 当成 config 的参数);正确顺序是 `docker compose --profile public -f x.yml config`(`--profile` 作为顶层 flag)。脚本里统一用顶层 flag。 5. **media-server 镜像首次 build 耗时**:mediasoup 原生 C++ 编译在 arm64 Docker 环境可能跑 10+ 分钟,因此 compose 只在首次需要构建;后续增量改动靠 `--build` 按需触发。公网 x86 服务器正常 2-3 分钟完成。 6. **为什么不起独立 compose 文件**:`docker-compose.dev.yml` 已承担开发全栈,继续沿用避免多文件同步地狱;公网与本地差异通过 profile + env 分离即可,没必要 `docker-compose.prod.yml`。 ### 验证记录 - `docker compose -f docker-compose.dev.yml config --quiet`(local 模式,默认 profile):✅ 无报错。 - `docker compose --profile public -f docker-compose.dev.yml config --quiet`(public 模式):✅ 无报错;services 列表 = `coturn / go-service / media-server / minio / postgres / redis`。 - `scripts/deploy-public.sh` 三场景验证: - `.env` 缺失 → 输出"❌ deploy/.env 不存在"并退出 1 ✅ - 含 `_REPLACE_WITH_*_` 占位符 → 输出未替换字段列表 + 退出 1 ✅ - `MEDIASOUP_ANNOUNCED_IP=""` 空值 → 输出"❌ MEDIASOUP_ANNOUNCED_IP 不能为空"+ 退出 1 ✅ - 完整正确配置 → 顺序通过三步校验 → 进入 `step_launch` ✅ - `compose build media-server` 单跑:因 mediasoup arm64 编译 > 15 min 主动中断,**不影响** Task 14 核心交付(配置 + 脚本 + 文档),公网 x86 环境属正常时间。 ### 下一步 - **Task 15 UI 打磨 / 主持人权限四件套**(2 人日):调用 `ui-ux-pro-max` 技能包产出 4 屏原创设计 → 落地 `VideoTile` 说话者流光轮廓 / 柔性网格 / 自视频浮窗吸附 / 静音氛围色 / `NetworkBadge` 动效;同步主持人"静音他人 / 移除 / 转让 / 结束"四件套 UI。 - **Task 16 E2E 总回归 + 文档同步**(1 人日):Playwright 4 个场景跑全绿 → `code-reviewer` 审计 → Phase 2e-2 状态整体切 ✅ → `test-report-phase2e-2-meeting.md` 落盘。 --- ## 🎯 2026-04-23 Phase 2e-2 Task 13:`meeting_invite` 通知卡片对接完成 **交付**:打通「A 在会议内邀请 B → B 通知中心弹出专属卡片 → 点立即加入一键入会 → 过期卡片自动灰显」完整链路;同时修复一个隐藏很深的回归 bug——前端 WS 客户端把 `.ack` 事件吞掉不 `_emit`,导致 `chat.js` 订阅的 `im.message.send.ack` / `im.message.read.ack` 从未触发,消息永远卡在 loading 圆圈、下方「已读 / 未读」标签也永不渲染。 ### 产出文件 | 文件 | 类型 | 作用 | |---|---|---| | `backend/go-service/app/meeting/service/meeting_service.go`(改) | 后端 service | `InviteUsers` 中 `notifyService.PushPayload.Extra` 补齐 `inviter_id / inviter_name / inviter_avatar / expired_at`(Unix 秒,与 Redis TTL `MeetingInviteTokenTTL=600s` 一致),对齐设计 §10.1 | | `frontend/src/constants/notify.js`(改) | 前端常量 | `supportsInlineAction` 扩展支持 `NOTIFY_TYPE_MEETING_INVITE`;新增 `NOTIFY_INLINE_ACTION_LABEL` 映射:meeting_invite → `{accept:"立即加入", reject:"稍后"}`;`NOTIFY_INLINE_ACTION_DEFAULT` 兜底 | | `frontend/src/components/notify/NotifyItem.vue`(改) | 前端通用卡片组件 | 动态按钮文案(`actionLabel` 计算属性);`isExpired` 计算属性对 meeting_invite 额外比对 `extra.expired_at * 1000 < Date.now()`;过期态合并为单个 disabled 的"邀请已过期"按钮;新增 `.notify-btn--expired` 样式 | | `frontend/src/pages/notify/index.vue`(改) | 前端通知中心 | `handleAccept` / `handleReject` / `_navigateByNotify` 增加 `NOTIFY_TYPE_MEETING_INVITE` 分支;新增 `_navigateToMeetingInvite(extra)` 辅助,跳 `/pages/meeting/preview?mode=join&code=xxx`;过期时 toast 提示"邀请已过期"不跳转 | | `frontend/src/services/websocket.js`(改·关键 Bug Fix) | 前端 WS 客户端 | `_onMessage` 内命中 `.ack` 事件后**移除 `return`**,改为同时走 `_handleAck`(Promise `pendingAcks` 通路)+ `_emit`(订阅通路),修复下方 chat 卡圈 bug | | `frontend/src/store/chat.js`(改·关键 Bug Fix) | 前端聊天 store | `_appendMessage` 遇到命中本地 `client_msg_id` 的临时消息(`_sending=true`),不再直接判定 dup 丢弃,而是**就地合并服务端字段**并显式置 `_sending:false / _failed:false`,保证即便 `.ack` 漏了、仅凭 `im.message.new` 广播也能把圆圈切换为真实状态 | | `docs/plans/2026-04-21-phase2e-2-implementation.plan.md`(改) | 进度文档 | Task 13 行标记 ✅ + 展开详细产出 | ### 关键技术点 1. **后端 extra 字段 SSOT 在 service**:设计 §10.1 要求的 8 个字段(`room_code / room_title / has_password / invite_token / inviter_id / inviter_name / inviter_avatar / expired_at`)统一在 `InviteUsers` 里装配进 `PushPayload.Extra`,下游 notify pusher 只负责透传;这样前端只读 `extra.*`,不再去解析通用的 `actor_id / actor_name`(那两个属于通用 notify 基础设施,会议邀请专属字段放 `extra` 更清晰)。 2. **过期态完全由 `expired_at` 驱动**:前端不再额外调接口校验,纯客户端 `Date.now()` 比对即可。即便用户把通知放了 30 分钟再点击,UI 能立即自动灰显、不触发无效跳转、也不会把脏 token 发到后端。与 Redis `echo:meeting:invite:{token}` 的 TTL(600s)对齐。 3. **`supportsInlineAction` 与按钮文案解耦**:原来只有 friend_request 有「同意 / 拒绝」按钮。现在扩展到 meeting_invite(「立即加入 / 稍后」)。`NOTIFY_INLINE_ACTION_LABEL` 是一个 type → `{accept, reject}` 的 map,后续再加「群邀请」「入群申请」等只需扩这张表 + 在 `handleAccept / handleReject` 加 switch 分支。 4. **`.ack` 双通路修复的起因**:`websocket.js` 早期版本用"命中 `.ack` 就 `_handleAck` 后 `return`"的 pattern,是因为当时 ACK 只给 Promise 消费;但 Phase 2a 群组已读 / Phase 2e-2 会议聊天后,`chat.js` / `meeting.js` 都在 `wsService.on('xxx.ack', ...)` 订阅 ACK 做"服务端持久化成功 → 把临时消息替换为真实消息"的逻辑。`return` 直接吞掉了 emit,这条关键事件从未触发,临时消息 `_sending` 一直为 true。修掉后还需要在 `chat.js._appendMessage` 做广播帧的就地合并,作为"丢 ACK 但不丢广播"场景的兜底。 5. **为什么不在前端加 polling 兜底**:项目 SSOT 要求「尽量减少前端轮询 / 服务端主推优先」。所以修的是 WS 事件分发路径,而不是额外拉轮询。此外 `chat.js._appendMessage` 的就地合并本质上是"广播帧也具备推进本地状态的能力",比轮询更轻量。 ### 验证记录 - **后端 REST 双角色验证**(curl 登录 testuser1 / testuser2 → testuser1 创会 → 邀请 testuser2 → testuser2 拉通知列表): - `GET /api/v1/notifications?category=meeting` 返回 1 条 `type=meeting_invite` - `extra` 完整度:`room_code / room_title / has_password / invite_token / inviter_id / inviter_name / inviter_avatar / expired_at` 全部存在且值正确 - `expired_at = invited_at + 600` 与 Redis TTL 对齐 - **前端聊天卡圈 Bug 验证**(用户手测):用户确认"经过我测试,这个问题已经修复好了",双用户发消息立即显示「已读 / 未读」标签,不再出现发送方消息前显示 loading 圆圈。 - **Playwright 前端 UI 回归**(2026-04-23 补做,采用"A 侧 curl 邀请 + B 侧 Playwright 登录"策略绕开多用户上下文限制): - A 侧 curl:testuser1 创会 `room_code=516-162-828` → 邀请 testuser2(id=9);后端返回 `pushed:1, skipped:0` - B 侧 Playwright:testuser2 登录 → 访问 `/pages/notify/index` → 通知列表成功渲染 2 张 `meeting_invite` 卡片: * **新邀请卡片**(刚刚)→ 底部显示「立即加入 / 稍后」两个独立按钮 ✅ * **过期邀请卡片**(10 分钟前,`expired_at` 已过去)→ 双按钮合并为单个 disabled 的「邀请已过期」按钮 ✅ - 点击"立即加入"→ URL 跳到 `/pages/meeting/preview?mode=join&code=516-162-828` ✅(room_code 与邀请精确一致) - preview 页正确渲染 `"即将加入会议 516-162-828,确认设备和显示名称后加入会议"`,摄像头/麦克风/扬声器/显示名称/默认开关全部可用 ✅ - 截图留档:`task13-notify-meeting-invite-cards.png` / `task13-preview-page-after-invite-accept.png` ### 下一步 - **Task 14**(1 人日):docker-compose 扩展 + 本机/公网双态环境变量开关,`media-server` 服务挂入 compose,`coturn` 进 `profiles: [public]`;脚本侧 `start.sh` 扩 `media` / `full` 子命令。 - **Task 15 主持人权限四件套** 和 **Task 16 E2E 总回归** 挨着 Task 14 之后收尾,MVP 就完成了。 --- ## 📋 2026-04-21 Phase 2e-2 设计阶段启动 **交付物**:两份新建文档 + 三份上游文档同步更新,已锁定 Phase 2e-2 会议 MVP 的全部架构决策与实施拆分。 ### 新建文档 | 文档 | 规模 | 核心内容 | |---|---|---| | `docs/plans/2026-04-21-phase2e-2-design.md` | 16 章节 | 文档定位 / 范围边界 / 11 项关键决策 / 架构 mermaid 3 张 / 数据模型 DDL 3 张 / Go 后端 / Node media-server / 前端 / UI/UX / 通知对接 / 安全性能 / 风险 / 验收 / 衔接 / 关联 / 变更记录 | | `docs/plans/2026-04-21-phase2e-2-implementation.plan.md` | 17 个 Task | Task 0 PoC → Task 16 E2E + 文档同步,共约 17 人日,含依赖拓扑 mermaid | ### 11 项关键决策(Plan 模式 5 轮澄清锁定) | 编号 | 决策 | 选择 | |---|---|---| | D01 | 部署形态 | 本机 + 公网双态(环境变量切换) | | D02 | 前端端形 | 仅 H5 浏览器 | | D03 | Go-Node 协同 | Go 主控 + Node 无状态包装(权威状态在 Go) | | D04 | 入会前设备预览 | MVP 纳入(独立预览页) | | D05 | 加入体验 | 套餐 C:会议号 + 密码 + 邀请链接 + 通知中心 `meeting_invite` | | D06 | 会议生命周期 | host 掉线 2 分钟宽限 + 自动转让(最早加入者)+ 空房 5 分钟 TTL | | D07 | UI 风格 | 飞书简洁框架 + EchoChat 原创(流光轮廓 / 柔性网格 / 静音氛围色) | | D08 | 主持人权限 | 四件套(静音他人 / 移除 / 转让 / 结束) | | D09 | 媒体服务目录 | `media-server/` 根级子项目 | | D10 | 移动端适配 | 桌面 + 手机双端响应式 | | D11 | 会议内聊天 | MVP 纳入(独立 `meeting_chats` 表,24 小时后清理) | ### 数据模型修订(相对总设计) - `meeting_rooms.password` → `password_hash VARCHAR(255)`(bcrypt 哈希替换明文) - `meeting_rooms` 新增 `ended_reason`(结束原因:host_ended/empty_ttl/admin_force/system_error) - `meeting_participants` 新增 `left_reason` - 新表 `meeting_chats`(独立于 `im_messages`,24 小时 TTL) - 新增 Redis key:`echo:meeting:invite:{token}`(邀请链接)、`echo:meeting:host_grace:{code}`(主持人宽限期) ### 上游文档同步 - `docs/plans/2026-04-20-phase2e-design.md` §四 精简为引用(指向 2e-2 专用设计 + 实施计划),§五 从 2e-3 范围里移除「会议邀请」(已上移 2e-2) - `.cursor/rules/project-context.mdc` 当前进度追加 Phase 2e-2 设计阶段条目 - 本文件(CURRENT_STATUS.md)更新头部 + 新增本段 ### 下一步 - **评审设计文档**:由用户 Review 两份新建文档 - **进入代码开发**:评审通过后从 Task 0(mediasoup PoC Spike)启动,预计 17 人日完成 MVP --- ## 🚀 2026-04-21 Phase 2e-2 Task 3 Go meeting 模块数据库 DDL + Model + DAO 完成 **交付**:Phase 2e-2 会议 MVP 的三张持久化表(`meeting_rooms` / `meeting_participants` / `meeting_chats`)完整落地到 PostgreSQL,配套 Go 侧 `app/meeting/{model,dao}` + 统一常量 `app/constants/meeting.go`;DDL 同时写入 `init.sql`(全量初始化)与 `phase2e2_migration.sql`(增量升级),在真实 postgres 容器跑通 CRUD + UNIQUE 约束 + CASCADE 级联删除 + 主持人转让事务。 ### 产出文件 | 文件 | 行数 | 作用 | |---|---|---| | `deploy/docker/postgres/init.sql`(追加) | +119 | 3 张表 DDL + 9 个索引 + COMMENT 全量文档 | | `deploy/docker/postgres/phase2e2_migration.sql` | 90 | 增量升级脚本(`IF NOT EXISTS` 幂等),用于已运行环境无损追加 | | `backend/go-service/app/constants/meeting.go` | 110 | 8 组常量:会议类型/状态/角色/结束原因/离会原因/默认配置/WS 事件(与 group/notify 同构) | | `backend/go-service/app/meeting/model/meeting_room.go` | 30 | `MeetingRoom` 结构体 + GORM 复合索引 tag + `TableName()` | | `backend/go-service/app/meeting/model/meeting_participant.go` | 27 | `MeetingParticipant` 结构体 + `IsActive()` 辅助 + 联合唯一索引 tag | | `backend/go-service/app/meeting/model/meeting_chat.go` | 18 | `MeetingChat` 结构体,纯文本 content + 房间聚合索引 | | `backend/go-service/app/meeting/dao/meeting_room_dao.go` | 170 | 9 个方法:`Create/GetByID/GetByCode/ExistsCode/MarkStarted/MarkEnded/UpdateHost/UpdateSettings/ListByHost/ListExpiredForCleanup` | | `backend/go-service/app/meeting/dao/meeting_participant_dao.go` | 235 | 11 个方法:`JoinRoom`(含重入复用)、`LeaveRoom`、`LeaveAllActive`、`TransferHost`(事务)、`FindActiveByUser`(JOIN 校验单点参会)、各类列表/计数/角色更新 | | `backend/go-service/app/meeting/dao/meeting_chat_dao.go` | 85 | 4 个方法:`Create/ListByRoom`(游标分页)/`DeleteByRoomIDs`(清理任务)/`CountByRoom` | ### 关键设计决策 1. **常量目录对齐项目风格(偏离实施计划草案)**:实施计划草案写的是 `app/meeting/constants/{meeting_status,meeting_role}.go`,但项目现有风格是"模块级常量统一放在 `app/constants/.go` 单文件"(见 `app/constants/group.go` / `notify.go`)。按 `project-context.mdc` 第 11 条「代码风格全局一致(最高优先级)」,本次采用 `app/constants/meeting.go` 单文件承载所有会议常量,同步修订实施计划。 2. **时间字段统一 TIMESTAMP(0)**:设计文档草案用了 `TIMESTAMPTZ`,但项目所有表(`auth_users` / `im_messages` / `notify_notifications`)统一使用 `TIMESTAMP(0)`(见 init.sql),Go model 搭配 `gorm:"type:timestamp(0)"`。本次 DDL 改为 `TIMESTAMP(0)` 保持一致。 3. **冗余索引移除**:设计文档草案写了 `idx_meeting_rooms_code`,但 `room_code UNIQUE NOT NULL` 已经自动建 B-tree 索引,冗余索引已移除避免双倍维护成本。 4. **重入复用单条参与者记录**:`JoinRoom` 使用事务,若 (room_id, user_id) 已存在且 `left_at IS NOT NULL` → UPDATE 复用该行(`joined_at=NOW, left_at=NULL, duration=0`);仍活跃则返回 `ErrAlreadyInMeeting` 供上层转 409。避免每次重入写新记录污染审计数据。 5. **`duration` 使用 SQL 表达式计算**:`LeaveRoom` 用 `EXTRACT(EPOCH FROM (? - joined_at))::INT` 走数据库时间而非 Go 端 `time.Now()`,避免跨时区/NTP 漂移导致负 duration。 6. **`MarkEnded` 乐观锁**:仅对 `status != ended` 的行 UPDATE,重复结束只保留首次原因,不被覆盖。 7. **无 Go 单元测试(遵循项目现有风格)**:项目 Go 侧 0 个 `_test.go`,统一用"代码审查 + 真实 postgres psql 验证 + Playwright E2E"三层守护。本次 Task 3 验收用 psql 脚本跑通 8 类场景(创建、UNIQUE 约束 ×2、主持人转让事务、聊天写入、`duration` 精确匹配、CASCADE 清零),全部通过。 ### 验证记录 - `go build ./...` ✅ 零报错 - `go vet ./...` ✅ 零报错 - `ReadLints app/meeting/ app/constants/meeting.go` ✅ 零 Lint 问题 - `docker exec echochat-postgres psql ... < phase2e2_migration.sql` ✅ 全部 `CREATE TABLE/INDEX/COMMENT` 成功 - `psql -c "\d meeting_*"` ✅ 3 张表结构、9 个索引、所有外键约束(含 `ON DELETE CASCADE`)正确生成 - psql 集成测试 ✅ 场景汇总: - `INSERT meeting_rooms` + 重复插 `room_code` → `unique_violation` 触发 - `INSERT meeting_participants` + 重复 `(room_id,user_id)` → `unique_violation` 触发 - `UPDATE role=0 WHERE role=1` / `UPDATE role=1 WHERE left_at IS NULL` 事务链 → 主持人转让成功 - `INSERT meeting_chats ×2` → 2 行写入 - `UPDATE left_at = NOW()+10s` + `duration = EXTRACT(EPOCH ...)` → `duration=10` 精确匹配 - `DELETE meeting_rooms` → `participants` 残留 0 / `chats` 残留 0(CASCADE 生效) ### 下一步 - **Task 4**(0.5 人日):Go 侧 `meeting` 模块的 service / controller / router 骨架,完成依赖注入 + 空实现占位,建立 `POST /api/meeting/create` 等路由的握手层。 --- ## 🚀 2026-04-21 Phase 2e-2 Task 4 Go meeting 模块骨架(service / controller / router / wire)完成 **交付**:`app/meeting/` 模块 service 层 17 个空方法 + controller 层 12 个 Gin 处理器 + `/api/v1/meeting/*` 路由全局挂载 + Wire 依赖注入全局打通;附带修复 admin 模块 `MessageManageService/Controller` provider 缺失的存量问题;`go build ./...` / `go vet ./...` / `wire ./app/provider` 全绿;实机启动 server 确认 12 条路由全部注册并通过 JWT 鉴权(未授权返回 401 `缺少认证信息`)。 ### 产出文件 | 文件 | 行数 | 作用 | |---|---|---| | `backend/go-service/app/meeting/service/interfaces.go` | 25 | 外部依赖接口抽象:`NotifyPusher` / `UserInfoResolver` / `OnlineChecker`,为后续 Task 5-15 解耦 notify/contact/ws 模块 | | `backend/go-service/app/meeting/service/meeting_service.go` | 165 | `MeetingService` 结构体 + 8 个 sentinel error(`ErrMeetingNotFound` 等)+ 17 个空方法占位(全部返回 `ErrNotImplemented`),为 Task 5-10 业务逻辑预留挂载点 | | `backend/go-service/app/meeting/controller/meeting_controller.go` | 150 | `MeetingController` + `responseNotImplemented`(返回 501)+ `requireUserID` 辅助 + 12 个 Gin 处理器,全部返回 501 占位 | | `backend/go-service/app/meeting/router.go` | 35 | `RegisterRoutes()` 将 12 条路由按设计文档挂到 `/api/v1/meeting/*`,统一套用 `jwtAuth` 中间件 | | `backend/go-service/app/meeting/provider.go` | 22 | `MeetingSet = wire.NewSet(DAO×3, Service, Controller)`,与其他模块 `Set` 命名一致 | | `backend/go-service/app/provider/wire.go`(改) | +10 | 挂入 `meetingApp.MeetingSet` + 3 条 `wire.Bind`:`NotifyPusher→NotifyService`、`UserInfoResolver→FriendshipDAO`、`OnlineChecker→ws.OnlineService` | | `backend/go-service/app/provider/provider.go`(改) | +6 | `App` struct 新增 `MeetingService` / `MeetingController` 字段 + `NewApp` 形参 | | `backend/go-service/app/provider/wire_gen.go`(自动生成) | +30 | `wire` 命令自动重生成,按拓扑序串联 meeting 模块依赖 | | `backend/go-service/router/router.go`(改) | +3 | `meetingApp.RegisterRoutes(engine, app.MeetingController, jwtAuth)` 挂载 | | `backend/go-service/app/admin/provider.go`(改) | +6 | **存量修复**:补齐 `MessageManageDAO/Service/Controller` 至 `AdminSet`,修复旧版 wire 未能发现 provider 的 bug | ### 路由清单(12 条全部验证) | 方法 | 路径 | 处理器 | 当前行为 | |---|---|---|---| | POST | `/api/v1/meeting/rooms` | `CreateRoom` | 501 NotImplemented | | GET | `/api/v1/meeting/rooms` | `ListMyMeetings` | 501 | | GET | `/api/v1/meeting/rooms/:code` | `GetRoom` | 501 | | POST | `/api/v1/meeting/rooms/:code/join` | `JoinRoom` | 501 | | POST | `/api/v1/meeting/rooms/:code/leave` | `LeaveRoom` | 501 | | POST | `/api/v1/meeting/rooms/:code/end` | `EndRoom` | 501 | | POST | `/api/v1/meeting/rooms/:code/transfer-host` | `TransferHost` | 501 | | POST | `/api/v1/meeting/rooms/:code/kick` | `KickMember` | 501 | | POST | `/api/v1/meeting/rooms/:code/invite` | `InviteUsers` | 501 | | POST | `/api/v1/meeting/invites/:token/redeem` | `RedeemInvite` | 501 | | POST | `/api/v1/meeting/rooms/:code/chats` | `SendChat` | 501 | | GET | `/api/v1/meeting/rooms/:code/chats` | `ListChats` | 501 | ### 关键设计决策 - **接口隔离(`interfaces.go`)**:对 notify/contact/ws 只依赖接口而非具体类型,避免后续实现时出现循环依赖;`OnlineChecker.IsOnline` 签名与现存 `ws.OnlineService` 一致(返回单个 `bool`,内部吞噬 error),保持最小改动面。 - **骨架返回 501(而非 404/200)**:`responseNotImplemented` 统一返回 501 + `ErrNotImplemented` 消息,前端联调/Postman 验证时能明确区分"未实现"与"路由缺失";与 group/contact 模块骨架风格保持一致。 - **存量问题一并修复**:`admin/provider.go` 漏注册 `MessageManage{DAO,Service,Controller}` 是一个跟 Task 4 无关的 wire 老 bug,本轮顺手修掉,使 `wire ./app/provider` 重生成不再报错;已在 commit 描述中注明。 - **Wire Bind 方向**:`wire.Bind(new(Interface), new(*ConcreteType))` 遵循"接口依赖指向具体类型"的惯例,与 Phase 2e-1 notify 模块的 Bind 写法保持一致。 - **路由顺序**:`RegisterRoutes` 中 12 条路由按"会议生命周期 → 成员管理 → 邀请 → 聊天"的业务流排列,与设计文档 §5.3 的清单逐一对应。 ### 验证执行 1. `go build ./...` → 无任何 warning/error 2. `go vet ./...` → 无提示 3. `go run -mod=mod github.com/google/wire/cmd/wire ./app/provider` → `wire_gen.go` 成功重生成 4. `GIN_MODE=debug go run cmd/server/main.go` 后台启动 → 日志打印 12 条 `[GIN-debug] ... meeting/controller.(*MeetingController).XxxRoom-fm (6 handlers)`,与路由表一一匹配 5. `curl -X POST http://localhost:8085/api/v1/meeting/rooms`(无 token)→ **401** `{"code":401,"message":"缺少认证信息",...}`,JWT 中间件生效 6. `curl -X GET http://localhost:8085/api/v1/meeting/rooms` → **401** 7. `curl -X POST http://localhost:8085/api/v1/meeting/invites/abc/redeem` → **401** 8. `pkill -f "go run cmd/server/main.go"` → 进程退出,端口 8085 释放 ### 下一步 - **Task 5**(1.5 人日):`MeetingService.CreateRoom` + `JoinRoom` + `LeaveRoom` + `EndRoom` 核心业务逻辑(6 位会议号生成 + bcrypt 密码校验 + 人数上限 + Redis host 宽限期 Timer 骨架),替换当前 `ErrNotImplemented` 占位。 --- ## 🎯 2026-04-22 Phase 2e-2 Task 10-12 + UI 打磨:会议 MVP 前端主链路全部打通 **交付**:Task 10(创建/加入/设备预览三页)、Task 11(会议室主页 + 核心组件)、Task 12(会议内聊天面板)顺序落地,叠加一轮深度 UI 打磨,解决了 uni-app H5 `` 点击遮罩、全屏面板 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 `::after` 点击遮罩**(两轮): - **第一轮(工具栏)**:原生 `