feat(phase2e-2): Go meeting 模块 12 个 REST 接口业务逻辑全量落地(Task 5)

主要产出:
- DTO 层:app/dto/meeting_dto.go 完整定义 13 个 DTO(请求/响应/基础共用)
- Service 层:MeetingService 12 业务方法 + 11 个 sentinel 错误 + 4 辅助
  - CreateRoom/JoinRoom/LeaveRoom/EndRoom 核心生命周期
  - KickMember/TransferHost/GetRoom/ListMyMeetings 会议管理
  - InviteUsers(+ NotifyPusher)/ RedeemInviteToken 邀请链路
  - SendChat/ListChats 会议内聊天
- Controller 层:12 handler + handleError 领域错误 → HTTP 映射
- 工具层:pkg/utils/meeting_code.go(XXX-XXX-XXX + invite token)
- Stub 接口:MediaOrchestrator(Task 7 替换)+ NoopMediaOrchestrator
- 路径修正:/rooms → /rooms/mine、/invites → /invite-tokens 对齐设计
- DAO 契约修复:GetByID/GetByCode/GetByRoomAndUser/FindActiveByUser
  将 gorm.ErrRecordNotFound 转为 (nil, nil),service 统一 nil 判定
- 安全强化:密码 bcrypt + 5 次错误锁 10 分钟;邀请 token 仅通过
  NotifyPusher.Extra 定向下发,响应不回传
- host 自动转让:host 离会时将最早加入者提升为 host
- 单点参会:用户同一时间仅能在一个活跃会议
- API 文档:docs/api/frontend/meeting.md 重写为 12 接口完整规范
- Wire 依赖注入:MediaOrchestrator + NoopMediaOrchestrator provider

验证:
- go build / go vet / wire 零告警
- 端到端 3 用户场景:12 happy path + 5 错误路径 PASS=19 / FAIL=0
  覆盖密码错/房间不存在/单点冲突/越权/邀请失效

文档同步:
- docs/progress/CURRENT_STATUS.md 增补 Task 5 章节
- .cursor/rules/project-context.mdc 更新阶段状态
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md 标记 T5 

下一步:Task 6(WS 信令协议 + BroadcastToMeeting 替换 PublishToUser 循环)

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-21 16:39:59 +08:00
parent d22f61fb9b
commit e235e001b3
14 changed files with 1737 additions and 254 deletions

View File

@@ -27,7 +27,7 @@ alwaysApply: true
* 单端 WS 连接架构(沿用),不做多端已读同步(设计文档 §3.1/§3.5/§九 已修订,多端改造推迟到 Phase 2f/二期)
* 专用设计:`docs/plans/2026-04-20-phase2e-1-design.md`;实施计划:`docs/plans/2026-04-20-phase2e-1-implementation.plan.md`;验证报告:`test-report-phase2e-1-notification.md`
* API 文档:`docs/api/frontend/notify.md`
- 2e-2 会议 MVP约 17 天)🚧 **代码开发中**Task 0-4 ✅ / Task 5-16 待执行mediasoup Node.js 独立 `media-server/` + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + 双态部署(本机 + 公网 coturn+ 响应式(桌面/手机)
- 2e-2 会议 MVP约 17 天)🚧 **代码开发中**Task 0-5 ✅ / Task 6-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
@@ -36,6 +36,7 @@ alwaysApply: true
* **Task 2 ✅ 9 个内部 REST API 完成2026-04-21含代码审查修复**`media-server/` 落地 Router/Transport/Producer/Consumer 四类资源的 9 个接口,全部挂 `/internal/v1/*` 前缀;**zod 手动 parse + 全局 errorHandler** 方案(不引入 fastify-type-provider-zod 避免 zod v4 依赖冲突);`AppError` 统一错误码(`NOT_FOUND`/`CONFLICT`/`CAN_NOT_CONSUME`/`ROUTER_LIMIT_EXCEEDED`/`MEDIASOUP_ERROR`+ `VALIDATION_ERROR`/`UNAUTHORIZED`/`INTERNAL_ERROR`;所有资源用 **Map + `observer.once('close')` 自清理**`producerclose` 级联关闭下游 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 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