diff --git a/backend/go-service/app/meeting/controller/meeting_controller.go b/backend/go-service/app/meeting/controller/meeting_controller.go index f83120b..c0eed45 100644 --- a/backend/go-service/app/meeting/controller/meeting_controller.go +++ b/backend/go-service/app/meeting/controller/meeting_controller.go @@ -41,6 +41,14 @@ type cloudLawCreateMeetingRequest struct { CallType string `json:"callType"` } +type cloudLawJoinMeetingRequest struct { + ReceiverID int64 `json:"receiverId"` + ReceiverType string `json:"receiverType"` + ReceiverPhone string `json:"receiverPhone"` + ReceiverName string `json:"receiverName"` + CallID string `json:"callId"` +} + func (ctl *MeetingController) requireCloudLawInternalToken(c *gin.Context) bool { expected := "" if ctl.meetingConfig != nil { @@ -270,6 +278,48 @@ func (ctl *MeetingController) CloudLawGetMeeting(c *gin.Context) { utils.ResponseOK(c, cloudLawMeetingPayload(room, onlineCount)) } +func (ctl *MeetingController) CloudLawJoinMeeting(c *gin.Context) { + if !ctl.requireCloudLawInternalToken(c) { + return + } + code := c.Param("code") + if code == "" { + utils.ResponseBadRequest(c, "会议号不能为空") + return + } + var req cloudLawJoinMeetingRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "参数校验失败: "+err.Error()) + return + } + req.ReceiverPhone = strings.TrimSpace(req.ReceiverPhone) + req.ReceiverName = strings.TrimSpace(req.ReceiverName) + if req.ReceiverPhone == "" { + utils.ResponseBadRequest(c, "receiverPhone不能为空") + return + } + user, created, err := ctl.meetingService.EnsureCloudLawUserByPhone(c.Request.Context(), req.ReceiverPhone, req.ReceiverName) + if err != nil { + ctl.handleError(c, err, "创建云律会议接单人失败") + return + } + room, participant, _, err := ctl.meetingService.JoinRoomForInternal(c.Request.Context(), user.ID, code) + if err != nil { + ctl.handleError(c, err, "云律接单人加入会议失败") + return + } + payload := cloudLawMeetingPayload(room, 0) + payload["callId"] = req.CallID + payload["receiverId"] = req.ReceiverID + payload["receiverType"] = req.ReceiverType + payload["echoChatParticipantUserId"] = strconv.FormatInt(user.ID, 10) + payload["echoChatParticipantUserCreated"] = created + if participant != nil { + payload["participantId"] = strconv.FormatInt(participant.ID, 10) + } + utils.ResponseOK(c, payload) +} + func (ctl *MeetingController) CloudLawEndMeeting(c *gin.Context) { if !ctl.requireCloudLawInternalToken(c) { return diff --git a/backend/go-service/app/meeting/router.go b/backend/go-service/app/meeting/router.go index 56c955f..e4676bd 100644 --- a/backend/go-service/app/meeting/router.go +++ b/backend/go-service/app/meeting/router.go @@ -50,6 +50,7 @@ func RegisterRoutes( registerCloudLawRoutes := func(group *gin.RouterGroup) { group.POST("/meetings", ctrl.CloudLawCreateMeeting) group.GET("/meetings/:code", ctrl.CloudLawGetMeeting) + group.POST("/meetings/:code/join", ctrl.CloudLawJoinMeeting) group.POST("/meetings/:code/end", ctrl.CloudLawEndMeeting) } registerCloudLawRoutes(r.Group("/api/v1/cloud-law")) diff --git a/backend/go-service/app/meeting/service/meeting_service.go b/backend/go-service/app/meeting/service/meeting_service.go index fb769b8..e6b8f9a 100644 --- a/backend/go-service/app/meeting/service/meeting_service.go +++ b/backend/go-service/app/meeting/service/meeting_service.go @@ -355,7 +355,62 @@ func (s *MeetingService) CreateRoom(ctx context.Context, hostID int64, req *dto. } func (s *MeetingService) CreateRoomForInternal(ctx context.Context, hostID int64, req *dto.CreateMeetingRoomRequest) (*model.MeetingRoom, *model.MeetingParticipant, string, error) { - return s.createRoom(ctx, hostID, req, false, false, "service.meeting_service.CreateRoomForInternal") + return s.createRoom(ctx, hostID, req, false, true, "service.meeting_service.CreateRoomForInternal") +} + +func (s *MeetingService) JoinRoomForInternal(ctx context.Context, userID int64, code string) (*model.MeetingRoom, *model.MeetingParticipant, string, error) { + funcName := "service.meeting_service.JoinRoomForInternal" + room, err := s.roomDAO.GetByCode(ctx, code) + if err != nil { + return nil, nil, "", err + } + if room == nil { + return nil, nil, "", ErrMeetingNotFound + } + if room.Status == constants.MeetingStatusEnded { + return nil, nil, "", ErrMeetingEnded + } + if existing, pErr := s.participantDAO.GetByRoomAndUser(ctx, room.ID, userID); pErr != nil { + return nil, nil, "", pErr + } else if existing != nil && existing.IsActive() { + routerID, _ := s.mediaOrchestrator.ResolveRouterID(code) + return room, existing, routerID, nil + } + activeCount, err := s.participantDAO.CountActiveByRoom(ctx, room.ID) + if err != nil { + return nil, nil, "", err + } + if int(activeCount) >= room.MaxMembers { + return nil, nil, "", ErrMeetingFull + } + participant, err := s.participantDAO.JoinRoom(ctx, room.ID, userID, constants.MeetingRoleParticipant) + if err != nil { + if errors.Is(err, dao.ErrAlreadyInMeeting) { + existing, getErr := s.participantDAO.GetByRoomAndUser(ctx, room.ID, userID) + if getErr != nil { + return nil, nil, "", getErr + } + routerID, _ := s.mediaOrchestrator.ResolveRouterID(code) + return room, existing, routerID, nil + } + return nil, nil, "", err + } + if s.lifecycleSvc != nil { + s.lifecycleSvc.CancelEmptyTTL(ctx, code) + } + routerID, _ := s.mediaOrchestrator.ResolveRouterID(code) + if routerID == "" { + newID, mediaErr := s.mediaOrchestrator.CreateRouter(ctx, code) + if mediaErr != nil { + if _, leaveErr := s.participantDAO.LeaveRoom(ctx, room.ID, userID, constants.MeetingLeftReasonSelf); leaveErr != nil { + logs.Warn(ctx, funcName, "内部入会补偿 LeaveRoom 失败", zap.Int64("room_id", room.ID), zap.Int64("user_id", userID), zap.Error(leaveErr)) + } + return nil, nil, "", ErrMediaServiceUnavailable + } + routerID = newID + } + logs.Info(ctx, funcName, "云律接单人加入会议成功", zap.String("room_code", code), zap.Int64("user_id", userID)) + return room, participant, routerID, nil } func (s *MeetingService) createRoom(ctx context.Context, hostID int64, req *dto.CreateMeetingRoomRequest, enforceSingleActive bool, addHostParticipant bool, funcName string) (*model.MeetingRoom, *model.MeetingParticipant, string, error) {