---
description: EchoChat 项目上下文与开发记忆 - 每次新对话自动加载
alwaysApply: true
---

# EchoChat 项目上下文

## 快速恢复上下文

**新对话开始时，必须先读取以下文件恢复项目记忆：**

1. `docs/progress/CURRENT_STATUS.md` — 项目开发进度、已完成 Task、关键技术决策、下一步工作
2. 当前阶段的实施计划文档（位于 `docs/plans/` 目录下）

## 当前进度

- **Phase 1（基础设施与用户认证）**：✅ 全部完成（11 个 Task）
- **Phase 2a（WebSocket 实时通讯与联系人管理）**：✅ 全部完成（13 个 Task + 后期 Bug 修复 3 项）
- **Phase 2b（即时通讯消息系统）**：✅ 全部完成（10 个 Task + 代码审查修复 7 项 + 用户测试修复 8 项），设计文档 `docs/plans/2026-03-03-phase2b-design.md`
- 分支：`feature/phase2b-instant-messaging`
- **Phase 2c（群聊与已读回执）**：✅ 全部完成（14 个 Task + 代码审查修复 14 项 + 浏览器测试修复 21 项），设计文档 `docs/plans/2026-03-04-phase2c-design.md`
- 分支：`feature/phase2c-group-read-receipt`
- **Phase 2d（消息类型扩展）**：✅ 全部完成（14 个 Task + 代码审查修复 2 项 + 富媒体 Bug 修复 4 项 + UX 优化 1 项），设计文档 `docs/plans/2026-03-04-phase2d-design.md`
- **⚠️ 分支说明**：Phase 2d 的实际开发工作误用了 `feature/phase2c-group-read-receipt` 分支，`feature/phase2d-message-types` 分支未承载 2d 代码。此次偏差不做回溯修复；**Phase 2e 起必须为每个阶段新建独立分支**（`feature/phase2e-xxx`）。
- **Phase 2e（会议与通知系统）**：🚧 规划完成，拆分为三子阶段，设计文档 `docs/plans/2026-04-20-phase2e-design.md`
  - 2e-1 通知系统（3-4 天）✅ **已完成**：统一通知中心 + 11 种类型 + Pusher 接口注入 + 30 天清理 + 管理员广播 + 前端 5 分类 Tab
    * 单端 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）+ 响应式（桌面/手机）
    * 专用设计：`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
    * **Task 0 ✅ PoC Spike 完成（2026-04-21）**：`media-server/poc/` 跑通 2 浏览器 ↔ Node ↔ mediasoup，Playwright 双 tab 自动化验证 2 人会议 4 transports/4 producers/4 consumers/RSS 61MB，peer 离开资源自动清理；锁定 mediasoup + fastify + mediasoup-client 技术栈，**不改用 livekit-server**；归档 `media-server/docs/poc-notes.md`（7 项关键坑 + 启动步骤 + 对 Task 1/2/9 的复用映射）
    * **Task 1 ✅ media-server 骨架完成 + Fastify 5 升级（2026-04-21）**：正式 `media-server/` 子项目落盘，锁定 **mediasoup@3.19.0 + fastify@5.8.5 + fastify-plugin@5.1.0 + @fastify/sensible@6.0.4 + @fastify/websocket@11.2.0 + pino@9.3.2 + zod@3.23.8**；src 五件套（`app.ts` / `config.ts` / `utils/logger.ts` / `mediasoup/worker.ts` / `middlewares/internal-auth.ts`）；`/healthz` + `/readyz` + `/internal/info` 实测通过；`X-Internal-Token` 鉴权用 `timingSafeEqual` 防侧信道；Worker `died` 指数退避自愈通过 `kill -9` 验证；Dockerfile 多阶段 + 非 root + curl HEALTHCHECK + 显式暴露 `40000-40199/UDP+TCP`。**升级关键决策**：Fastify 4 已于 2025-06-30 结束 LTS，升级 v5 同时用 `loggerInstance: logger` 消灭两个 pino 实例，插件全部 GA 支持 v5，升级改动仅 2 文件 ~20 行
    * **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 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 自动化脚本回归
    * **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 幂等转 ok；media-server 日志对齐 `router created × 2 / webrtc transport created / router closed explicitly`
    * **Task 6 ✅ WebSocket 信令协议 13 事件全量落地（2026-04-21）**：`meeting.*` 事件族从 Task 5 的 `PublishToUser` 循环升级为完整 WS 信令协议。产出：(1) `app/constants/meeting.go` WS 事件常量与设计 §6.3 对齐（3 房间 + 5 成员 + 5 媒体 + 1 聊天）+ `MeetingWSClientEvents` 白名单限制客户端仅能发起 8 个 C→S 事件（防恶意客户端伪造 `room.ended` 等广播）；(2) `service/interfaces.go` 扩容 `MediaOrchestrator` 至 9 方法（Router/Transport/Producer/Consumer 全生命周期）+ 配套 5 个 DTO + `NoopMediaOrchestrator` 9 占位实现；(3) **新建 `service/meeting_broadcaster.go`**（75 行）抽离 `BroadcastToMeeting` / `PublishToUser` 统一广播层，REST + WS 共用；(4) `service/meeting_service.go` 12 方法改调 `broadcaster.*` 不再直连 `ws.PubSub`；(5) **新建 `service/meeting_signal_service.go`**（430 行）承载 8 C→S 事件业务：`OnRoomJoin`/`OnRoomLeave`/`OnMemberStateChanged`/`OnTransportCreate/Connect`/`OnProduceStart`/`OnConsumeStart`/`OnProducerClose`；**Redis 资源追踪** `echo:meeting:resources:{room_id}:{user_id}`（Set，TTL 1 小时）+ `cleanupUserResources`（WS 断开钩子遍历清理 transport/producer/consumer）+ host 权限校验（非 host 改他人状态返回 `-1 仅主持人可执行此操作`）；(6) **新建 `controller/meeting_ws_handler.go`**（200 行）薄层：构造时 `hub.RegisterEvent` 注册 8 C→S 事件，每 handler 仅 JSON 反序列化 + 调 `signalSvc.On*` + 构造 ACK；(7) `provider.go` + `wire_gen.go` 扩充三个新 provider；(8) `docs/api/frontend/meeting.md` 追加 §WebSocket 信令协议（200 行）：16 事件总览 + 8 C→S 完整契约 + S→C 广播契约 + 架构 + 错误处理。**关键设计决策**：(a) Broadcaster 单独抽层让 Task 5 REST 广播与 Task 6 WS 广播复用同一对象；(b) C→S 白名单防伪造；(c) Redis Set 追踪媒体资源即使 Go 进程崩溃也不泄漏 mediasoup 端；(d) MediaOrchestrator 接口完整化让 Task 7 只需替换绑定不改 signal service / handler 代码；(e) ACK `code=0/-1` 与 REST 领域错误口径完全一致；(f) `meeting.room.leave` 只清 WS 资源不改 participant 表（真正离会仍需 REST `/leave`），允许客户端 WS 重连刷新 transport 不退会。验证：`go build` / `go vet` / `wire` 全绿；端到端 WS 冒烟 `/tmp/meeting_ws_t6_test.mjs` **PASS=18 / FAIL=0**，覆盖 8 C→S 白名单事件 + 3 S→C 广播 + 3 错误路径（非 host 越权/不存在会议号/WS leave 资源清理）。测试期间修复 `waitEvent` 死循环 bug（pop→push 自循环）改为 stash 缓冲区归还模式
    * **Task 5 ✅ Go meeting 模块 12 个 REST 接口业务逻辑全量落地（2026-04-21）**：`MeetingService` + `MeetingController` 从 501 占位升级为完整实现，对接 PostgreSQL / Redis / NotifyPusher / PubSub / MediaOrchestrator。产出：(1) `app/dto/meeting_dto.go` 13 个 DTO（3 基础 + 10 请求/响应）；(2) `pkg/utils/meeting_code.go` 生成 9 位 `XXX-XXX-XXX` 会议号 + 32 位 hex 邀请令牌；(3) `service/meeting_service.go` 12 业务方法 + 11 领域错误 + `assertIsActiveParticipant`/`assertIsHost`/`generateUniqueRoomCode`/`broadcastToActiveParticipants` 辅助；(4) `controller/meeting_controller.go` 12 Gin 处理器 + `handleError` 领域错误 → HTTP 映射 + `roomToDTO`/`participantToDTO`/`chatToDTO` 转换；(5) `service/interfaces.go` 新增 `MediaOrchestrator` 接口 + `NoopMediaOrchestrator` 占位（Task 7 替换）；(6) `provider.go` 注册 `NoopMediaOrchestrator`；(7) `router.go` 路径对齐设计 `GET /rooms/mine` + `POST /invite-tokens/:token/redeem`；(8) `docs/api/frontend/meeting.md` 重写为 280 行的 12 接口完整文档。**DAO 契约修复**：`meeting_room_dao.GetByID/GetByCode` + `meeting_participant_dao.GetByRoomAndUser/FindActiveByUser` 全部将 `gorm.ErrRecordNotFound` 转换为 `(nil, nil)`，由 service 统一 `result == nil` 判定，消除 500 误报。**关键设计决策**：(a) 单点参会用 `meeting_participants` JOIN `meeting_rooms.status != 2` 判断；(b) 密码限流用 `echo:meeting:pwd:fail:{code}:{user_id}` 5 次锁 10 分钟（`ErrMeetingPasswordLocked`）；(c) host 离会若还有其他活跃成员自动转让给"最早加入者"并广播 `meeting.host.changed`；(d) 邀请 token 不返回给调用方，仅通过 `NotifyPusher.PushBatch.Extra.invite_token` 定向下发；兑换后保留 60 秒冗余由 Redis TTL 自然过期；(e) 创建类接口 201、动作类 200、领域错误按 404/403/400 三档映射。**Stub 策略**：`MediaOrchestrator.CreateRouter/CloseRouter` 当前 Noop（Task 7 接入 HTTPMediaOrchestrator）；WS 广播暂用 `pubsub.PublishToUser` 逐人循环（Task 6 封装为 `BroadcastToMeeting` 无感替换）；`NotifyPusher.PushBatch` 复用 Phase 2e-1 成果。验证：`go build` / `go vet` / `wire` 全绿；启动 server + 3 用户端到端脚本 `/tmp/meeting_t5_test.sh` **PASS=19 / FAIL=0**，覆盖 12 接口 happy path + 5 类错误路径（密码错/房间不存在/单点参会冲突/非 host 越权/邀请链接失效）
  - 2e-3 会议增强（7-10 天）📋 待开发：预约会议（`type=2`）+ 定时提醒（`meeting_reminder`）+ 等候室/锁定会议 + 设备预览高级参数（降噪/回声/虚拟背景）
  - 分支：`feature/phase2e-2-meeting-mvp`（Phase 2e-2 专用，从 `feature/phase2c-group-read-receipt` 衍生）
  - **关键技术锁定**：维持 mediasoup SFU 架构（非 Mesh），前端用 mediasoup-client，信令复用现有 WS Hub
  - **显式推迟清单**（必须留档，见设计文档 §九）：
    * → Phase 2f：WS Hub 多端连接改造、会议管理后台、通知广播发布 UI、管理端仪表板、操作日志页、系统配置管理、通知分类开关
    * → 第二期：屏幕共享、会议录制、虚拟背景、微信登录、互动直播、视频消息、表情包、消息转发/引用
    * → 第三期：微服务拆分、K8s、跨服 Worker 集群、AI 语音转文字
- **开发者运维脚本（2026-04-20）**：新增 `scripts/{start,stop,status}.sh` 三件套，`scripts/dev-setup.sh` 补齐 MinIO 健康检查；遵循 setup/start 职责分离（Rails/Django 社区模式）
- **聊天页 UX 优化（2026-04-20）**：单聊/群聊引入「新消息悬浮提示 + 按需已读」机制（贴底自动滚+已读，远离底部显示"N 条新消息"悬浮按钮），消除"视图未见但对方已读"的 UX 错位；**后续修复**：watch 误将"加载更多历史消息"计入 newMsgCount → 改为对比「末尾消息标识（tail id/client_msg_id）」而非 `messages.length`，真实双账号（duanlingyun ↔ bojinyuan）端到端验证通过
- **语音消息 H5 兼容（2026-04-20）**：修复 PC Chrome 无法按住"按住说话"的问题，链路涉及 4 层（事件 + 录音 API + 上传 + 后端校验）：VoiceRecorder 并行监听 touch/mouse 事件；新增 `H5Recorder` 类（基于 `MediaRecorder + getUserMedia`）作为 uni 录音接口在 H5 端的回退；`uploadVoice` 增加 blob 分支走 `fetch+FormData` 精确控制 filename；后端 `allowedVoiceExts` 扩展 `.webm/.ogg`。Playwright 端到端验证：录音计时正常（3s→5s 跳动）、Blob 上传成功、数据库 `im_messages` type=3 记录写入
- **语音未听红点（2026-04-20，仿微信）**：语音需"听"才算真消费，仅通用已读不能准确反映 → 为「对方发来的」语音叠加独立状态。`chat store` 新增 `voicePlayedMap` + 4 API（`markVoicePlayed/isVoicePlayed/loadVoicePlayedState/resetVoicePlayedState`），localStorage 按 userId 隔离（`echo:voice-played:{userId}`），不同步后端（私人视图态）；`MsgVoice.vue` 自主消费 store（单聊/群聊同构），模板加 `.unplayed-dot`（10rpx 红圆），`onTogglePlay` 点击即标记清除；`user.logout()` 补 `resetVoicePlayedState()` 防串用户。Playwright 验证：duanlingyun(13) ↔ bojinyuan(7) 会话 6，初始 5 个红点（对方 5 条语音） → 点击 2 条 → 红点减至 3 → 页面刷新仍为 3（持久化生效）
- **已读状态刷新持久化（2026-04-20）**：修复"刷新后所有已读标签变未读 / 群聊 N人已读消失"的体验倒退 bug，根因是前端 `readStatusMap`/`groupReadCountMap` 仅由 WS 事件填充、刷新后无 API 补回。扩展 `HistoryMessageResponse` 新增 `peer_last_read_msg_id`（单聊）+ `read_count_map`（群聊仅含自己发送消息）；`GetHistoryMessages` 按会话类型分支填充（`GetPeerUserID`+`GetMember` 或 `readRecorder.GetReadCountBatch`）；前端 `loadHistoryMessages` 仅在首次加载时回填（避免加载更多时覆盖 WS 增量）。Playwright 双端验证：单聊会话 6（peer_last_read=202）首屏 20 已读+1 未读、群聊 8 刷新后 13 条自己消息全部显示"N人已读"（11×1人 + 2×2人）；不新增 API，不增加请求次数，向前兼容
- 范围：图片/语音/文件消息完整流程 + 管理端消息管理（列表+统计+撤回+删除）+ ECharts 仪表板
- **跨模块通信模式**：接口注入标准（ws.FriendIDsGetter / im.FriendChecker / im.UserInfoGetter / notify.UserInfoResolver → contact.FriendshipDAO，im.OfflineMessagePusher → ws.Handler，contact.OnlineChecker → ws.OnlineService，im.GroupInfoGetter → group.GroupDAO，im.MessageReadRecorder → group.MessageReadDAO，group.UserInfoProvider → auth.UserDAO，group.MessageWriter → im.MessageDAO，admin.MessageManageService → im.ConversationDAO + ws.PubSub，**contact.NotifyPusher / group.NotifyPusher → notify.NotifyService**，**ws.NotifyConnectHook → notify.NotifyService**）

## 项目概述

EchoChat 是一个实时音视频通讯平台，包含三个子项目：
- `backend/go-service/` — Go 后端（Gin + GORM + Wire + Redis + gorilla/websocket）
- `frontend/` — 前台用户端（uni-app + Vue 3.4 + Pinia 2.x）
- `admin/` — 后台管理端（Vue 3.5+ + Element Plus + Pinia 3.x）

已实现模块：auth（认证）、contact（联系人）、ws（WebSocket）、im（即时通讯）、group（群聊）、file（文件上传）、admin（管理端含消息管理）、im（即时通讯 + 已读回执）、group（群聊管理）、file（文件上传/MinIO）
前端常量：`frontend/src/constants/group.js`（GROUP_ROLE / GROUP_STATUS / JOIN_REQUEST_STATUS，与后端 constants/group.go 对齐）

## 核心开发规则

1. **前端设计**：必须使用 openskills 安装的 `ui-ux-pro-max` 技能包，脚本绝对路径为 `/Users/bojinyuan/.agent/skills/ui-ux-pro-max/scripts/search.py`。**严禁使用** `.cursor/skills/ui-ux-pro-max.bak/` 目录下的任何文件，该目录已废弃。禁止手动设计系统
2. **工作流**：使用 superpowers 流程控制开发节奏
3. **两端差异**：`frontend/` 和 `admin/` 是完全独立的项目，技术栈不需要统一
4. **模块系统**：前端统一使用 ESM（`export`/`import`），禁止 CommonJS
5. **Go 常量命名**：camelCase（`UserStatusActive`），非大写下划线
6. **API 响应**：统一 `{ "code": 0, "message": "success", "data": ... }`
7. **JWT 策略**：有状态 JWT，Token 存 Redis（按 clientType 隔离：`echo:auth:token:{frontend|admin}:{user_id}`），前后台互不影响
8. **角色等级体系**：`auth_roles.level` 字段（值越小权限越高：1=超管, 10=管理员, 100=普通用户），所有管理操作强制执行层级权限校验
9. **代码注释**：所有公开函数、组件、Store 必须有详细注释
10. **后端架构规范**：详见 `docs/conventions/backend-module-architecture.md`（模块分层/接口注入/日志/错误处理/批量查询/系统消息/Store 封装等）
11. **代码风格全局一致（最高优先级）**：新编写的任何模块代码，必须严格参照已有模块的实际代码实现和风格，禁止自创新封装、新 API、新模式。详见下方「代码风格全局一致规则」
12. **文档自动同步（强制）**：每个 Task 或功能开发完成后，必须自动执行文档同步，详见下方「文档自动同步规则」
13. **验证方式**：使用 Playwright MCP 进行页面自动化验证
14. **代码审查**：每个 Task 完成后，使用 `code-reviewer` 子代理进行结构化审查，确保代码质量和计划一致性
15. **完成验证**：使用 `verification-before-completion` 技能，在声称完成前运行验证命令并确认输出
16. **本地启停脚本（强制）**：启动/停止三端服务统一使用 `scripts/` 下的脚本，**禁止在聊天中直接指导用户敲 `cd backend && go run ...` 等原始命令**：
    - 首次初始化：`./scripts/dev-setup.sh`（检查 Docker、拉起 Postgres/Redis/MinIO 并健康等待）
    - 日常启动：`./scripts/start.sh`（端口占用自动跳过，支持 `backend|frontend|admin|docker|--no-docker` 单项启动）
    - 日常停止：`./scripts/stop.sh`（默认保留 Docker 容器，`--all` 全关）
    - 状态查看：`./scripts/status.sh`
    - 后台进程的 PID 与日志位于 `.run/`（已 gitignore），排障优先查看 `.run/logs/*.log`

## 代码风格全局一致规则（最高优先级，强制执行）

> **核心原则：编写任何新模块/新文件的代码前，必须先阅读同层级现有模块的实际代码，严格复制其风格，禁止引入不存在的 API、封装或模式。**

### 执行流程（强制）

1. **写代码前**：先用 Read/Grep 工具读取同类型现有文件（如写新 Controller 前先读 `im_controller.go` 和 `contact_controller.go`）
2. **写代码时**：逐行对照现有代码的导入、结构体定义、方法签名、日志调用、错误处理模式
3. **写代码后**：与参照文件做差异比对，确认风格完全一致

### Go 后端各层代码风格（以实际代码为准）

详细的风格规范和代码模板见 `docs/conventions/backend-module-architecture.md`。以下是关键约定摘要：

| 层级 | 接收器 | 日志 | funcName | 错误处理 |
|------|--------|------|----------|---------|
| Controller（前台业务） | `ctl` | 不记日志 | 无 | 方法级 `handleError` |
| Controller（auth/admin） | `ctrl`/`ctl` | 有日志 | 有 | `handleAuthError` 包级函数 / 内联 |
| Service | `s` | `logs.Info/Debug/Error` | `"service.{file}.{Method}"` | 包顶部 `var ErrXxx` |
| DAO | `d` | `logs.Info/Debug/Error` | `"dao.{file}.{Method}"` | `logs.Error` + 返回 err |

### 日志 API（以实际代码为准）

项目中 logs 包只有 `logs.Info/Debug/Warn/Error/Fatal` 五个方法，签名为 `logs.Xxx(ctx, funcName, message, ...zap.Field)`。
**不存在** `logs.LogFunctionEntry` / `logs.LogFunctionExit` / `logs.LogSuccess` 等方法，**严禁使用不存在的 API**。

### 依赖管理

- **禁止随意拉取最新版本的依赖包**，必须选择与当前 Go 版本（go.mod 中的 `go` 指令）兼容的版本
- **禁止触发 Go 工具链自动升级**，如果某依赖要求更高版本的 Go，必须选择兼容版本而非升级 Go
- 添加依赖前先检查 go.mod 中的 Go 版本和已有依赖，优先复用已有依赖

### 前端代码风格

- 前端新页面/组件/Store 的编写，同样必须先阅读现有同类文件，严格遵循已有的代码结构、命名规范、状态管理模式
- 禁止引入项目中未使用的新 UI 框架、状态管理库或工具函数库

## 文档自动同步规则（强制执行）

**触发时机**：每个 Task 或功能模块开发完成后，代码提交前，必须自动检查并更新以下文档，无需用户提醒。

### 必须检查的文档清单

| 文档 | 路径 | 更新条件 |
|------|------|---------|
| 项目进度 | `docs/progress/CURRENT_STATUS.md` | 每个 Task 完成后更新 Task 状态表、新增功能描述 |
| 架构设计 | `docs/architecture/system-architecture.md` | 新增模块、路由、中间件、数据流变化时更新 |
| 当前阶段设计文档 | `docs/plans/20xx-xx-xx-phaseXx-design.md` | 设计变更、状态变更时更新 |
| 总体系统设计 | `docs/plans/2026-02-27-echochat-system-design.md` | API 列表、页面结构、数据库表、分期规划变更时更新 |
| API 文档导航 | `docs/api/README.md` | 新增 API 文档文件时更新导航表和目录结构 |
| 模块 API 文档 | `docs/api/{frontend,admin}/*.md` | 新增/修改 API 接口时更新对应模块文档 |
| 开发规范 | `docs/conventions/frontend-backend-integration.md` | 新增通用规范（错误处理、协议、联动模式）时更新 |
| 项目规则 | `.cursor/rules/project-context.mdc` | 进度变更、新规则、新模块时更新 |

### 文档质量要求

1. **单文件 ≤ 500 行**：超过时拆分为独立文件，在导航中添加链接
2. **状态标记实时**：Task 完成后立即将状态标记从 🔜/📋 改为 ✅
3. **结构一致**：新增内容遵循现有文档的格式和层级结构
4. **交叉引用**：相关文档之间保持引用链接一致（如设计文档引用 API 文档路径）
5. **日期更新**：文档头部的「最后更新」日期保持最新

### 每阶段（Phase）开始前的文档准备

- 创建阶段设计文档 `docs/plans/YYYY-MM-DD-phaseXx-design.md`
- 创建阶段实施计划 `docs/plans/YYYY-MM-DD-phaseXx-implementation.plan.md`
- 更新 `CURRENT_STATUS.md` 的下一阶段规划（含 Task 清单）
- 更新 `project-context.mdc` 的当前进度和待实现模块
- 更新 `system-architecture.md` 新增模块的状态标记（🔜）
- 更新 `echochat-system-design.md` 的分期规划 + 新增 API 列表
- 更新 `api/README.md` 新增模块的文档导航
- 创建新的功能分支

### 每个 Task 完成后的文档同步

- 更新 Task 状态标记（📋 → ✅）
- 更新 `CURRENT_STATUS.md` 功能描述和技术细节
- 如有新增 API → 更新/创建对应的 `docs/api/{frontend,admin}/*.md`
- 如有新增 WS 事件 → 更新 `docs/api/websocket.md`
- 如有架构变更 → 更新 `system-architecture.md`
- 代码提交前确认所有文档已同步

### 每阶段（Phase）结束时的额外检查

- 当前阶段设计文档状态标记为「✅ 已完成」
- 总体设计文档的开发分期部分更新完成标记
- project-context.mdc 的当前进度更新
- CURRENT_STATUS.md 的下一阶段规划更新
- 前后端集成规范 `docs/conventions/frontend-backend-integration.md` 的最后更新时间

## 前后端联动规范（必须遵守）

> 详细规范见 `docs/conventions/frontend-backend-integration.md`

1. **前后台路由严格分离（最高优先级）**：
   - 前台用户端 API：`/api/v1/auth/*`、`/api/v1/im/*`、`/api/v1/meeting/*` 等
   - 后台管理端 API：`/api/v1/admin/auth/*`、`/api/v1/admin/users/*` 等
   - **禁止任何混用**：admin 前端不得调用 `/api/v1/auth/*`，frontend 不得调用 `/api/v1/admin/*`
   - 新增功能时必须先确认归属哪端，使用对应的路由前缀
2. **Token Redis 存储隔离**：按 `clientType` 隔离：`echo:auth:token:{frontend|admin}:{user_id}`，JWT Claims 包含 `client_type` 字段
3. **错误提示统一**：前端所有 HTTP 错误提示必须优先使用后端 `data.message`，禁止硬编码覆盖后端信息
4. **安全防护**：后端登录接口对"用户不存在"与"密码错误"统一返回 401 + "账号或密码错误"
5. **401 场景区分**：前端拦截器区分「登录请求的 401」（仅提示错误）和「已认证请求的 401」（清 Token + 跳转登录页）
6. **响应格式一致**：后端所有响应必须使用 `utils.Response*` 系列函数
7. **业务错误映射**：后端 Controller 的 `handleError` 函数必须覆盖所有已知业务错误，不能忽略 error（`_`）

## 设计系统

持久化在 `design-system/echochat/` 目录：
- `MASTER.md` — 全局设计规范
- `pages/*.md` — 页面级覆盖规则
- 色板：Primary `#2563EB` / BG `#F8FAFC` / Text `#1E293B`
