Files
EchoChat/.cursor/rules/project-context.mdc
bujinyuan a01e973335 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
2026-04-22 17:59:48 +08:00

208 lines
46 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
description: EchoChat 项目上下文与开发记忆 - 每次新对话自动加载
alwaysApply: true
---
# EchoChat 项目上下文
## 快速恢复上下文
**新对话开始时,必须先读取以下文件恢复项目记忆:**
1. `docs/progress/CURRENT_STATUS.md` — 项目开发进度、已完成 Task、关键技术决策、下一步工作
2. 当前阶段的实施计划文档(位于 `docs/plans/` 目录下)
## 当前进度
- **Phase 1基础设施与用户认证**:✅ 全部完成11 个 Task
- **Phase 2aWebSocket 实时通讯与联系人管理)**:✅ 全部完成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-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
* **Task 0 ✅ PoC Spike 完成2026-04-21**`media-server/poc/` 跑通 2 浏览器 ↔ Node ↔ mediasoupPlaywright 双 tab 自动化验证 2 人会议 4 transports/4 producers/4 consumers/RSS 61MBpeer 离开资源自动清理;锁定 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` 级联关闭下游 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 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+ 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`
* **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}`SetTTL 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` 死循环 bugpop→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 个 DTO3 基础 + 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` 当前 NoopTask 7 接入 HTTPMediaOrchestratorWS 广播暂用 `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 2fWS 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 双端验证:单聊会话 6peer_last_read=202首屏 20 已读+1 未读、群聊 8 刷新后 13 条自己消息全部显示"N人已读"11×1人 + 2×2人不新增 API不增加请求次数向前兼容
- 范围:图片/语音/文件消息完整流程 + 管理端消息管理(列表+统计+撤回+删除)+ ECharts 仪表板
- **跨模块通信模式**接口注入标准ws.FriendIDsGetter / im.FriendChecker / im.UserInfoGetter / notify.UserInfoResolver → contact.FriendshipDAOim.OfflineMessagePusher → ws.Handlercontact.OnlineChecker → ws.OnlineServiceim.GroupInfoGetter → group.GroupDAOim.MessageReadRecorder → group.MessageReadDAOgroup.UserInfoProvider → auth.UserDAOgroup.MessageWriter → im.MessageDAOadmin.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联系人、wsWebSocket、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 策略**:有状态 JWTToken 存 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` |
| Controllerauth/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`