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

@@ -5,7 +5,7 @@
> **上级路线图:** [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md)
> **分支:** `feature/phase2e-2-meeting-mvp`
> **预估总工时:** **约 17 人日**17 个 Task含 PoC 与 UI 打磨)
> **最后更新:** 2026-04-21实施计划首版落盘
> **最后更新:** 2026-04-21Task 0-5 ✅ 已落地,下一步 Task 6 WS 信令
---
@@ -273,24 +273,35 @@ flowchart LR
- **顺手修复 admin wire 存量 bug**计划未列Task 4 重生成 wire 时暴露了 admin 模块 `MessageManage` 系列 provider 缺失的遗留问题,当场补上避免阻塞后续开发
- **工作量****0.5 人日**(实际约 0.4 人日,含存量问题修复约 0.1 人日)
### Task 5会议 REST 接口(创建/加入/离开/结束/列表/详情 + 邀请链接兑换 + 邀请)
### Task 5会议 REST 接口(创建/加入/离开/结束/列表/详情 + 邀请链接兑换 + 邀请)2026-04-21 完成)
- **目标**:填充 Task 4 骨架中的业务逻辑,完整实现设计 §6.2 的 12 个接口
- **依赖**T4
- **主要产出**
- 会议号生成:`GenerateRoomCode()` 随机 9 位数字,冲突重试(< 3
- 密码 bcrypt 存取`service.setPassword / verifyPassword`
- 加入会议时校验会议存在未结束容量未满密码正确用户未在其他会议
- 离开 / 结束参与者表 `left_at` 填入 + duration 计算 + 触发 mediasoup 资源清理
- 邀请链接 Token`GenerateInviteToken() -> Redis SET EX 600``RedeemInviteToken(token)` 校验并删除
- `POST /rooms/:code/invite` 接受 `{invitee_ids, group_ids}`生成 Token 后调 `NotifyPusher`
- DTO`app/dto/meeting_dto.go` 完整定义请求/响应结构
- API 文档`docs/api/frontend/meeting.md` 新建全部 12 接口 + 示例
- **检查点**
- Postman 手测 12 接口全部 2xx错误场景返回正确错误码`meeting_not_found` / `meeting_full` / `password_incorrect`
- 容量限制 9 人加入返回 `meeting_full`
- 密码连续错误 5 次锁定 10 分钟
- **工作量****1.5 人日**
- **主要产出**(全部实装并端到端通过验证)
- **DTO 层**`backend/go-service/app/dto/meeting_dto.go`169 行)定义 13 个 DTO`MeetingRoomDTO`/`MeetingParticipantDTO`/`MeetingChatDTO` 基础 + 10 个请求/响应类型),所有请求体有 `binding` 标签
- **工具层**`backend/go-service/pkg/utils/meeting_code.go` 会议号生成(`crypto/rand` + 3 组 3 位数字生成 `XXX-XXX-XXX`,冲突重试 5 次)+ 邀请 Token32 位 hex
- **Service 层**`MeetingService` 12 业务方法 + 11 个 sentinel 错误 + 4 个辅助函数(`assertIsActiveParticipant`/`assertIsHost`/`generateUniqueRoomCode`/`broadcastToActiveParticipants`
- **Controller 层**12 个 Gin 处理器 + `handleError` 领域错误 → HTTP 映射404/403/400/500 四档)+ DTO 转换辅助(`roomToDTO`/`participantToDTO`/`chatToDTO`
- **Stub 接口**:新增 `MediaOrchestrator` 接口 + `NoopMediaOrchestrator`Task 7 替换WS 广播走 `pubsub.PublishToUser` 逐人Task 6 改为 `BroadcastToMeeting``NotifyPusher.PushBatch` 复用 Phase 2e-1
- **路径修正**`router.go` 将 Task 4 占位路径对齐设计:`GET /rooms``GET /rooms/mine``POST /invites/:token/redeem``POST /invite-tokens/:token/redeem`
- **DAO 契约修复**`meeting_room_dao.GetByID/GetByCode` + `meeting_participant_dao.GetByRoomAndUser/FindActiveByUser``gorm.ErrRecordNotFound` 转为 `(nil, nil)`service 统一 `result == nil` 判定
- **密码限流**:同 `(user_id, code)` 5 次错误 → Redis `echo:meeting:pwd:fail:...` 锁 10 分钟(`ErrMeetingPasswordLocked`
- **单点参会**:用 `meeting_participants` JOIN `status != 2` 判断用户是否已在其他活跃会议(`ErrAlreadyInOtherMeeting`
- **host 自动转让**host 离会时若仍有其他活跃成员 → 自动将 host 转给"最早加入者",广播 `meeting.host.changed`;若无人则房间 `ended_reason=empty_ttl`
- **邀请 Token 安全**:响应不返回 token仅通过 `NotifyPusher.PushBatch.Extra.invite_token` 定向下发;兑换后保留 60 秒冗余由 Redis TTL 自然过期
- **API 文档**`docs/api/frontend/meeting.md` 重写为 280 行的 12 接口完整文档(路径总览 + 领域错误码映射表 + 逐接口参数/响应示例 + WebSocket 事件关联表 + 验证记录)
- **检查点**(全部通过):
- `go build ./...` / `go vet ./...` / `wire ./app/provider` 零告警
- 端到端脚本 `/tmp/meeting_t5_test.sh` 用 3 用户场景覆盖12 接口 happy path + 5 类错误路径(密码错 / 房间不存在 / 单点参会冲突 / 非 host 越权 / 邀请链接失效)→ **PASS=19 / FAIL=0**
- DB 侧核验 `meeting_rooms.status` / `meeting_participants.left_at/duration` / `meeting_chats` 写入正确Redis 侧核验 `echo:meeting:invite:{token}` TTL=600s
- 服务日志全链路 trace_idWS 广播 `meeting.member.joined/left/chat/host.changed/room.ended` 事件全部发出
- **实际产出 vs 计划差异**
- **密码连续错误 5 次锁 10 分钟**Task 5 已实现,与计划一致
- **容量限制**MVP 硬上限为 **8**(设计 D05超过将 `ErrMeetingFull`;计划里误写"第 9 人加入返回 meeting_full"表述已与硬上限对齐
- **`kick` 请求体**:设计文档曾讨论 `{target_user_id, request_id}` 的幂等字段Task 5 DTO 定义为 `{user_id}`(与 `TransferHostRequest.target_user_id` 命名区分),`request_id` 幂等保护留待 Task 6 WS 侧统一处理WS 场景更多)
- **InviteUsersResponse**:出于安全考虑不返回 token仅返回 `{pushed, skipped}`;测试时通过 Redis 获取 token
- **错误码中文化**:使用中文 `message`(与项目惯例一致)而非英文 `meeting_not_found` code前端通过 HTTP 状态码 + trace_id 区分
- **工作量****实际 1 人日**< 预估 1.5 人日 DTO 设计充分 + DAO 契约修复一次到位
### Task 6WS 信令 11 事件处理器