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

@@ -0,0 +1,168 @@
package dto
// ====== 会议模块基础 DTOPhase 2e-2======
// MeetingRoomDTO 会议房间传输对象
// 用于 CreateRoom / GetRoom / JoinRoom / ListMyMeetings 的响应体
// 字段与 model.MeetingRoom 对齐敏感字段password_hash不对外暴露
type MeetingRoomDTO struct {
ID int64 `json:"id"`
RoomCode string `json:"room_code"`
Title string `json:"title"`
HostID int64 `json:"host_id"`
HostName string `json:"host_name,omitempty"`
HostAvatar string `json:"host_avatar,omitempty"`
Type int `json:"type"`
HasPassword bool `json:"has_password"`
MaxMembers int `json:"max_members"`
Status int `json:"status"`
StatusLabel string `json:"status_label,omitempty"`
ScheduledAt string `json:"scheduled_at,omitempty"`
StartedAt string `json:"started_at,omitempty"`
EndedAt string `json:"ended_at,omitempty"`
EndedReason *string `json:"ended_reason,omitempty"`
Settings string `json:"settings"`
CreatedAt string `json:"created_at"`
OnlineCount int `json:"online_count"`
}
// MeetingParticipantDTO 会议参与者传输对象
// 用于 GetRoom 的成员列表响应活跃用户left_at=NULL在前
type MeetingParticipantDTO struct {
ID int64 `json:"id"`
RoomID int64 `json:"room_id"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name,omitempty"`
UserAvatar string `json:"user_avatar,omitempty"`
Role int `json:"role"`
RoleLabel string `json:"role_label,omitempty"`
JoinedAt string `json:"joined_at"`
LeftAt string `json:"left_at,omitempty"`
LeftReason *string `json:"left_reason,omitempty"`
Duration int `json:"duration"`
IsActive bool `json:"is_active"`
}
// MeetingChatDTO 会议内聊天消息传输对象
// 用于 SendChat 的响应 + ListChats 的列表项
type MeetingChatDTO struct {
ID int64 `json:"id"`
RoomID int64 `json:"room_id"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name,omitempty"`
UserAvatar string `json:"user_avatar,omitempty"`
Content string `json:"content"`
CreatedAt string `json:"created_at"`
}
// ====== REST API 请求 / 响应 ======
// CreateMeetingRoomRequest 创建即时会议请求
// POST /api/v1/meeting/rooms
type CreateMeetingRoomRequest struct {
Title string `json:"title" binding:"required,min=1,max=200"` // 会议标题
Password string `json:"password" binding:"omitempty,min=4,max=20"` // 可选入会密码(纯文本入参,服务端 bcrypt 哈希存储)
MaxMembers int `json:"max_members" binding:"omitempty,min=2,max=8"` // 可选上限MVP 硬上限 8留参数为 Phase 2e-3 扩展)
}
// CreateMeetingRoomResponse 创建会议响应
type CreateMeetingRoomResponse struct {
Room MeetingRoomDTO `json:"room"`
}
// JoinMeetingRoomRequest 加入会议请求
// POST /api/v1/meeting/rooms/:code/join
type JoinMeetingRoomRequest struct {
Password string `json:"password" binding:"omitempty,max=20"` // 入会密码(如房间设密则必传)
}
// JoinMeetingRoomResponse 加入会议响应
type JoinMeetingRoomResponse struct {
Room MeetingRoomDTO `json:"room"`
Participant MeetingParticipantDTO `json:"participant"`
RouterID string `json:"router_id,omitempty"` // mediasoup Router IDTask 7 接入 Node 后填充)
}
// LeaveMeetingRoomResponse 离开会议响应
type LeaveMeetingRoomResponse struct {
Duration int `json:"duration"` // 本次参会时长(秒)
}
// GetMeetingRoomResponse 会议详情响应
// GET /api/v1/meeting/rooms/:code
type GetMeetingRoomResponse struct {
Room MeetingRoomDTO `json:"room"`
Participants []MeetingParticipantDTO `json:"participants"`
OnlineCount int `json:"online_count"`
}
// ListMyMeetingsRequest 我的会议列表查询参数
// GET /api/v1/meeting/rooms/mine
type ListMyMeetingsRequest struct {
Status *int `form:"status"` // 状态过滤nil=全部0/1/2
BeforeID int64 `form:"before_id"` // 游标分页
Limit int `form:"limit"` // 页大小,默认 20最大 50
}
// ListMyMeetingsResponse 我的会议列表响应
type ListMyMeetingsResponse struct {
List []MeetingRoomDTO `json:"list"`
HasMore bool `json:"has_more"`
}
// InviteUsersRequest 邀请用户请求
// POST /api/v1/meeting/rooms/:code/invite
type InviteUsersRequest struct {
InviteeIDs []int64 `json:"invitee_ids" binding:"required,min=1,max=50,dive,gt=0"` // 被邀请用户 ID 数组(去重后)
}
// InviteUsersResponse 邀请结果响应
type InviteUsersResponse struct {
Pushed int `json:"pushed"` // 成功发送邀请通知的数量(在线+离线都算)
Skipped int `json:"skipped"` // 跳过的数量(已在会中 / 重复 ID 等)
}
// KickMemberRequest 踢人请求
// POST /api/v1/meeting/rooms/:code/kick
type KickMemberRequest struct {
UserID int64 `json:"user_id" binding:"required,gt=0"`
}
// TransferHostRequest 主持人转让请求
// POST /api/v1/meeting/rooms/:code/transfer-host
type TransferHostRequest struct {
TargetUserID int64 `json:"target_user_id" binding:"required,gt=0"`
}
// SendMeetingChatRequest 发送会议内聊天请求
// POST /api/v1/meeting/rooms/:code/chats
type SendMeetingChatRequest struct {
Content string `json:"content" binding:"required,min=1,max=500"`
}
// SendMeetingChatResponse 发送聊天响应
type SendMeetingChatResponse struct {
Message MeetingChatDTO `json:"message"`
}
// ListMeetingChatsRequest 会议内聊天列表查询参数
// GET /api/v1/meeting/rooms/:code/chats
type ListMeetingChatsRequest struct {
BeforeID int64 `form:"before_id"` // 游标分页
Limit int `form:"limit"` // 页大小,默认 30最大 100
}
// ListMeetingChatsResponse 会议内聊天列表响应
type ListMeetingChatsResponse struct {
List []MeetingChatDTO `json:"list"`
HasMore bool `json:"has_more"`
}
// RedeemInviteTokenResponse 邀请 Token 兑换响应
// POST /api/v1/meeting/invite-tokens/:token/redeem
// 兑换成功仅返回会议号 + 邀请人,前端自行调 JoinRoom 入会
type RedeemInviteTokenResponse struct {
RoomCode string `json:"room_code"`
InviterID int64 `json:"inviter_id"`
HasPassword bool `json:"has_password"`
}