--- 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-7 ✅ / Task 8-16 待执行):mediasoup Node.js 独立 `media-server/` + 即时会议(≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + 双态部署(本机 + 公网 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 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`