package service import ( "context" "strings" "time" roomeventsv1 "hyapp.local/api/proto/events/room/v1" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/tencentim" "hyapp/pkg/xerr" "hyapp/services/room-service/internal/room/command" "hyapp/services/room-service/internal/room/outbox" "hyapp/services/room-service/internal/room/rank" "hyapp/services/room-service/internal/room/state" ) func (s *Service) requireRoomOpenForEntry(ctx context.Context, roomID string) error { closed, err := s.isRoomClosed(ctx, roomID) if err != nil || !closed { return err } return xerr.New(xerr.RoomClosed, "room closed") } func (s *Service) isRoomClosed(ctx context.Context, roomID string) (bool, error) { roomMeta, exists, err := s.repository.GetRoomMeta(ctx, roomID) if err != nil || !exists { return false, err } return isClosedRoomStatus(roomMeta.Status), nil } func isClosedRoomStatus(status string) bool { value := strings.TrimSpace(status) return value != "" && !strings.EqualFold(value, state.RoomStatusActive) } // CloseRoom 关闭房间,统一清理 presence、麦位、读模型状态和房间外事件。 func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (*roomv1.CloseRoomResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) cmd := command.CloseRoom{ Base: baseFromMeta(req.GetMeta()), Reason: strings.TrimSpace(req.GetReason()), } if cmd.Reason == "" { cmd.Reason = "closed_by_owner" } result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { if current == nil { return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found") } if current.Status == state.RoomStatusClosed { return mutationResult{snapshot: current.ToProto()}, nil, nil } if current.Status != state.RoomStatusActive && current.Status != state.RoomStatusClosing { return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not active") } if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil { return mutationResult{}, nil, err } type occupiedMicSeat struct { userID int64 seatNo int32 micSessionID string } occupiedSeats := make([]occupiedMicSeat, 0, len(current.MicSeats)) for _, seat := range current.MicSeats { if seat.UserID != 0 && seat.MicSessionID != "" { occupiedSeats = append(occupiedSeats, occupiedMicSeat{ userID: seat.UserID, seatNo: seat.SeatNo, micSessionID: seat.MicSessionID, }) } } current.Status = state.RoomStatusClosed current.OnlineUsers = map[int64]*state.RoomUserState{} for index := range current.MicSeats { current.ClearMicSession(index) } current.Version++ closedEvent, err := outbox.Build(current.RoomID, "RoomClosed", current.Version, now, &roomeventsv1.RoomClosed{ ActorUserId: cmd.ActorUserID(), Reason: cmd.Reason, }) if err != nil { return mutationResult{}, nil, err } records := make([]outbox.Record, 0, len(occupiedSeats)+1) for _, seat := range occupiedSeats { // 关房是 Room Cell 统一清场,给每个占麦 session 单独写 down 事件,保证用户时长能闭合。 micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, cmd.ActorUserID(), seat.userID, seat.seatNo, seat.micSessionID, "room_closed") if err != nil { return mutationResult{}, nil, err } records = append(records, micEvent) } records = append(records, closedEvent) return mutationResult{ snapshot: current.ToProto(), roomStatus: state.RoomStatusClosed, syncEvent: &tencentim.RoomEvent{ EventID: closedEvent.EventID, RoomID: current.RoomID, EventType: "room_closed", ActorUserID: cmd.ActorUserID(), RoomVersion: current.Version, Attributes: map[string]string{ "reason": cmd.Reason, }, }, }, records, nil }) if err != nil { return nil, err } return &roomv1.CloseRoomResponse{ Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, }, nil }