diff --git a/backend/go-service/app/meeting/controller/meeting_controller.go b/backend/go-service/app/meeting/controller/meeting_controller.go index 43fd820..71db87e 100644 --- a/backend/go-service/app/meeting/controller/meeting_controller.go +++ b/backend/go-service/app/meeting/controller/meeting_controller.go @@ -9,6 +9,7 @@ import ( "github.com/echochat/backend/app/dto" "github.com/echochat/backend/app/meeting/model" "github.com/echochat/backend/app/meeting/service" + "github.com/echochat/backend/config" "github.com/echochat/backend/pkg/logs" "github.com/echochat/backend/pkg/middleware" "github.com/echochat/backend/pkg/utils" @@ -20,11 +21,48 @@ import ( // Task 5 完成:填充 12 个接口的请求解析、业务调用、DTO 转换、错误码映射 type MeetingController struct { meetingService *service.MeetingService + meetingConfig *config.MeetingConfig } // NewMeetingController 创建 MeetingController 实例 -func NewMeetingController(meetingService *service.MeetingService) *MeetingController { - return &MeetingController{meetingService: meetingService} +func NewMeetingController(meetingService *service.MeetingService, meetingConfig *config.MeetingConfig) *MeetingController { + return &MeetingController{meetingService: meetingService, meetingConfig: meetingConfig} +} + +type cloudLawCreateMeetingRequest struct { + CallID string `json:"callId"` + CallerID int64 `json:"callerId"` + ReceiverID int64 `json:"receiverId"` + ReceiverType string `json:"receiverType"` + Title string `json:"title"` + CallType string `json:"callType"` +} + +func (ctl *MeetingController) requireCloudLawInternalToken(c *gin.Context) bool { + expected := "" + if ctl.meetingConfig != nil { + expected = ctl.meetingConfig.CloudLawInternalToken + } + if expected == "" { + utils.ResponseForbidden(c, "云律内部接口未配置") + return false + } + if c.GetHeader("X-Internal-Token") != expected { + utils.ResponseUnauthorized(c, "内部访问令牌无效") + return false + } + return true +} + +func cloudLawMeetingPayload(room *model.MeetingRoom, onlineCount int64) gin.H { + return gin.H{ + "roomId": strconv.FormatInt(room.ID, 10), + "roomCode": room.RoomCode, + "code": room.RoomCode, + "meetingCode": room.RoomCode, + "status": room.Status, + "onlineCount": onlineCount, + } } // requireUserID 统一的当前用户取值,失败直接写 401 并返回 false @@ -162,6 +200,72 @@ func chatToDTO(m *model.MeetingChat, userMap map[int64]service.UserDisplayInfo) // ====== REST API ====== +func (ctl *MeetingController) CloudLawCreateMeeting(c *gin.Context) { + if !ctl.requireCloudLawInternalToken(c) { + return + } + var req cloudLawCreateMeetingRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "参数校验失败: "+err.Error()) + return + } + if req.CallerID <= 0 { + utils.ResponseBadRequest(c, "callerId不能为空") + return + } + title := req.Title + if title == "" { + title = "云律立即连线" + } + room, _, _, err := ctl.meetingService.CreateRoom(c.Request.Context(), req.CallerID, &dto.CreateMeetingRoomRequest{ + Title: title, + MaxMembers: 2, + }) + if err != nil { + ctl.handleError(c, err, "创建云律会议失败") + return + } + payload := cloudLawMeetingPayload(room, 1) + payload["callId"] = req.CallID + payload["receiverId"] = req.ReceiverID + payload["receiverType"] = req.ReceiverType + payload["callType"] = req.CallType + utils.ResponseOK(c, payload) +} + +func (ctl *MeetingController) CloudLawGetMeeting(c *gin.Context) { + if !ctl.requireCloudLawInternalToken(c) { + return + } + code := c.Param("code") + if code == "" { + utils.ResponseBadRequest(c, "会议号不能为空") + return + } + room, _, onlineCount, err := ctl.meetingService.GetRoomForInternal(c.Request.Context(), code) + if err != nil { + ctl.handleError(c, err, "获取云律会议详情失败") + return + } + utils.ResponseOK(c, cloudLawMeetingPayload(room, onlineCount)) +} + +func (ctl *MeetingController) CloudLawEndMeeting(c *gin.Context) { + if !ctl.requireCloudLawInternalToken(c) { + return + } + code := c.Param("code") + if code == "" { + utils.ResponseBadRequest(c, "会议号不能为空") + return + } + if err := ctl.meetingService.EndRoomForInternal(c.Request.Context(), code); err != nil { + ctl.handleError(c, err, "结束云律会议失败") + return + } + utils.ResponseOK(c, gin.H{"roomCode": code, "code": code, "meetingCode": code}) +} + // CreateRoom POST /api/v1/meeting/rooms func (ctl *MeetingController) CreateRoom(c *gin.Context) { userID, ok := requireUserID(c) diff --git a/backend/go-service/app/meeting/router.go b/backend/go-service/app/meeting/router.go index 764af07..51156a8 100644 --- a/backend/go-service/app/meeting/router.go +++ b/backend/go-service/app/meeting/router.go @@ -46,4 +46,11 @@ func RegisterRoutes( // 鉴权由 controller 内部用 X-Internal-Secret header + INTERNAL_WEBHOOK_SECRET 校验 // 不走 JWT,因为调用方是 media-server 而非用户 r.POST("/internal/meeting/recordings/failure", recordingCtrl.MediaServerFailureWebhook) + + internalCloudLaw := r.Group("/internal/cloud-law") + { + internalCloudLaw.POST("/meetings", ctrl.CloudLawCreateMeeting) + internalCloudLaw.GET("/meetings/:code", ctrl.CloudLawGetMeeting) + internalCloudLaw.POST("/meetings/:code/end", ctrl.CloudLawEndMeeting) + } } diff --git a/backend/go-service/app/meeting/service/meeting_service.go b/backend/go-service/app/meeting/service/meeting_service.go index 48e6efe..1293c33 100644 --- a/backend/go-service/app/meeting/service/meeting_service.go +++ b/backend/go-service/app/meeting/service/meeting_service.go @@ -364,6 +364,39 @@ func (s *MeetingService) GetRoomByCode(ctx context.Context, userID int64, code s return room, participants, activeCount, nil } +func (s *MeetingService) GetRoomForInternal(ctx context.Context, code string) (*model.MeetingRoom, []model.MeetingParticipant, int64, error) { + room, err := s.roomDAO.GetByCode(ctx, code) + if err != nil { + return nil, nil, 0, err + } + if room == nil { + return nil, nil, 0, ErrMeetingNotFound + } + participants, err := s.participantDAO.ListByRoom(ctx, room.ID) + if err != nil { + return nil, nil, 0, err + } + activeCount, err := s.participantDAO.CountActiveByRoom(ctx, room.ID) + if err != nil { + return nil, nil, 0, err + } + return room, participants, activeCount, nil +} + +func (s *MeetingService) EndRoomForInternal(ctx context.Context, code string) error { + room, err := s.roomDAO.GetByCode(ctx, code) + if err != nil { + return err + } + if room == nil { + return ErrMeetingNotFound + } + if room.Status == constants.MeetingStatusEnded { + return nil + } + return s.EndRoom(ctx, room.HostID, code) +} + // JoinRoom 加入会议 // 校验顺序:房间存在 → 未结束 → 单点参会 → 密码锁定 → 密码校验 → 容量 → 写 participant → 广播 meeting.member.joined func (s *MeetingService) JoinRoom(ctx context.Context, userID int64, code, password string) (*model.MeetingRoom, *model.MeetingParticipant, string, error) { diff --git a/backend/go-service/app/provider/wire_gen.go b/backend/go-service/app/provider/wire_gen.go index edd750d..437edd7 100644 --- a/backend/go-service/app/provider/wire_gen.go +++ b/backend/go-service/app/provider/wire_gen.go @@ -119,7 +119,7 @@ func InitializeApp(cfg *config.Config) (*App, error) { // admin 会议管理服务在 recording service 构造后再创建,从而支持"强制结束录制"兜底 meetingManageService := service2.NewMeetingManageService(meetingManageDAO, meetingRecordingService) meetingManageController := controller2.NewMeetingManageController(meetingManageService) - meetingController := controller7.NewMeetingController(meetingService) + meetingController := controller7.NewMeetingController(meetingService, meetingConfig) meetingRecordingController := controller7.NewMeetingRecordingController(meetingRecordingService, meetingConfig) meetingWSHandler := controller7.NewMeetingWSHandler(meetingSignalService, hub) meetingCleanupTask := task2.NewMeetingCleanupTask(meetingLifecycleService, meetingRoomDAO, meetingChatDAO) diff --git a/backend/go-service/config/config.dev.yaml b/backend/go-service/config/config.dev.yaml index 16831ee..c6587a8 100644 --- a/backend/go-service/config/config.dev.yaml +++ b/backend/go-service/config/config.dev.yaml @@ -92,6 +92,7 @@ meeting: # media-server 失败回调共享密钥:必须与 media-server 端 INTERNAL_WEBHOOK_SECRET 环境变量一致 # 留空时 /internal/meeting/recordings/failure 全部 403(默认安全) internal_webhook_secret: internal-webhook-12xqs + cloud_law_internal_token: cloud-law-internal-xx14 recording_transcode_enabled: true recording_transcode_ffmpeg_path: /home/ykf/jenkins/yuyin/ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg recording_transcode_timeout_seconds: 600 \ No newline at end of file diff --git a/backend/go-service/config/config.docker.yaml b/backend/go-service/config/config.docker.yaml index 21329fc..ab5569b 100644 --- a/backend/go-service/config/config.docker.yaml +++ b/backend/go-service/config/config.docker.yaml @@ -83,6 +83,7 @@ meeting: # media-server 失败回调共享密钥:必须与 docker-compose 中 media-server 服务的 INTERNAL_WEBHOOK_SECRET 一致 # 生产部署请通过环境变量 ECHOCHAT_MEETING_INTERNAL_WEBHOOK_SECRET 覆盖(viper.AutomaticEnv 已开启) internal_webhook_secret: internal-webhook-12xqs + cloud_law_internal_token: cloud-law-internal-xx14 recording_transcode_enabled: true recording_transcode_ffmpeg_path: ffmpeg recording_transcode_timeout_seconds: 600 diff --git a/backend/go-service/config/config.go b/backend/go-service/config/config.go index 2073865..4d7c803 100644 --- a/backend/go-service/config/config.go +++ b/backend/go-service/config/config.go @@ -83,6 +83,7 @@ type MeetingConfig struct { RecordingTranscodeFFmpegPath string `mapstructure:"recording_transcode_ffmpeg_path"` RecordingTranscodeTimeoutSeconds int `mapstructure:"recording_transcode_timeout_seconds"` StaleRoomHours int `mapstructure:"stale_room_hours"` // 活跃超过此小时且无成员视为 stale,默认 4 + CloudLawInternalToken string `mapstructure:"cloud_law_internal_token"` } // MediaServerConfig Node media-server 接入配置(Phase 2e-2 Task 7)