2026-05-14 00:25:02 +08:00

136 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
// 进房入口使用专门错误码gateway 可以稳定映射成客户端房间关闭态。
return xerr.New(xerr.RoomClosed, "room closed")
}
func (s *Service) isRoomClosed(ctx context.Context, roomID string) (bool, error) {
// 关闭状态先读 rooms 元数据,避免为了拒绝进房恢复完整 Room Cell。
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)
// 空状态兼容测试构造;非 active 一律按不可进入处理closing 也不接收新用户。
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 {
// 关房命令幂等,重复请求只返回当前关闭快照,不重复写 RoomClosed 事件。
return mutationResult{snapshot: current.ToProto()}, nil, nil
}
if current.Status != state.RoomStatusActive && current.Status != state.RoomStatusClosing {
// creating 等非开放状态不允许用 CloseRoom 直接跳过创建收尾语义。
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 != "" {
// 清场前先保存占麦会话,后续 ClearMicSession 会抹掉 mic_session_id。
occupiedSeats = append(occupiedSeats, occupiedMicSeat{
userID: seat.UserID,
seatNo: seat.SeatNo,
micSessionID: seat.MicSessionID,
})
}
}
// 关房是 Room Cell 的终态变更presence 和麦位都在同一版本下清空。
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
}