// Package controller 提供 admin 模块的 HTTP 入口(会议管理) package controller import ( "errors" "strconv" "github.com/echochat/backend/app/admin/service" "github.com/echochat/backend/app/dto" meetingService "github.com/echochat/backend/app/meeting/service" "github.com/echochat/backend/pkg/logs" "github.com/echochat/backend/pkg/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // MeetingManageController 管理端会议管理 HTTP 控制器 // // 路由(在 admin/router.go 中挂载到 /api/v1/admin/meetings 下,JWT + admin 角色): // - GET /api/v1/admin/meetings 分页列表 + 多条件筛选 // - GET /api/v1/admin/meetings/stats 会议管理摘要统计 // - GET /api/v1/admin/meetings/:id 会议详情(含参与者、录制列表) type MeetingManageController struct { svc *service.MeetingManageService } // NewMeetingManageController Wire Provider func NewMeetingManageController(svc *service.MeetingManageService) *MeetingManageController { return &MeetingManageController{svc: svc} } // GetMeetingList GET /api/v1/admin/meetings func (ctl *MeetingManageController) GetMeetingList(c *gin.Context) { ctx := c.Request.Context() var req dto.AdminMeetingListRequest if err := c.ShouldBindQuery(&req); err != nil { utils.ResponseBadRequest(c, "参数格式错误") return } resp, err := ctl.svc.GetMeetingList(ctx, &req) if err != nil { ctl.handleError(c, err, "获取会议列表失败") return } utils.ResponseOK(c, resp) } // GetMeetingDetail GET /api/v1/admin/meetings/:id func (ctl *MeetingManageController) GetMeetingDetail(c *gin.Context) { ctx := c.Request.Context() id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil || id <= 0 { utils.ResponseBadRequest(c, "会议 ID 非法") return } resp, err := ctl.svc.GetMeetingDetail(ctx, id) if err != nil { ctl.handleError(c, err, "获取会议详情失败") return } utils.ResponseOK(c, resp) } // GetMeetingStats GET /api/v1/admin/meetings/stats func (ctl *MeetingManageController) GetMeetingStats(c *gin.Context) { ctx := c.Request.Context() resp, err := ctl.svc.GetStats(ctx) if err != nil { ctl.handleError(c, err, "获取会议统计失败") return } utils.ResponseOK(c, resp) } // ForceStopRecording POST /api/v1/admin/meetings/recordings/:id/force-stop // // 兼底场景:host 离线/失联未点停止;host 端点击停止后 ffmpeg 异常但 webhook 失效; // 任何长时间停留在 recording 状态的录制都可由此接口收尾。 // // 行为: // - 终态(ready/failed/uploading):幂等返回 200 // - recording 状态:走 media-server stop + MinIO 上传 + DB 状态机 func (ctl *MeetingManageController) ForceStopRecording(c *gin.Context) { ctx := c.Request.Context() id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil || id <= 0 { utils.ResponseBadRequest(c, "录制 ID 非法") return } if err := ctl.svc.ForceStopRecording(ctx, id); err != nil { ctl.handleError(c, err, "强制结束录制失败") return } utils.ResponseOK(c, gin.H{"recording_id": id, "stopped": true}) } // handleError 统一业务错误映射 func (ctl *MeetingManageController) handleError(c *gin.Context, err error, fallbackMsg string) { switch { case errors.Is(err, service.ErrMeetingNotFound), errors.Is(err, meetingService.ErrMeetingNotFound), errors.Is(err, meetingService.ErrRecordingNotFound): utils.ResponseNotFound(c, err.Error()) case errors.Is(err, meetingService.ErrRecordingNotStoppable): utils.ResponseBadRequest(c, err.Error()) case errors.Is(err, meetingService.ErrMediaServiceUnavailable): logs.Warn(c.Request.Context(), "controller.meeting_manage_controller.handleError", "媒体服务不可用", zap.Error(err)) utils.ResponseError(c, "媒体服务暂时不可用,请稍后重试") default: logs.Warn(c.Request.Context(), "controller.meeting_manage_controller.handleError", fallbackMsg, zap.Error(err)) utils.ResponseError(c, fallbackMsg) } }