package service import ( "context" "strings" "time" "unicode/utf8" roomeventsv1 "hyapp.local/api/proto/events/room/v1" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" "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) AdminListRooms(ctx context.Context, req *roomv1.AdminListRoomsRequest) (*roomv1.AdminListRoomsResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) rooms, total, err := s.repository.AdminListRooms(ctx, AdminRoomListQuery{ AppCode: appcode.FromContext(ctx), Page: int(req.GetPage()), PageSize: int(req.GetPageSize()), Keyword: strings.TrimSpace(req.GetKeyword()), Status: strings.TrimSpace(req.GetStatus()), RegionID: req.GetVisibleRegionId(), OwnerUserID: req.GetOwnerUserId(), SortBy: strings.TrimSpace(req.GetSortBy()), SortDirection: strings.TrimSpace(req.GetSortDirection()), NowMS: s.clock.Now().UnixMilli(), }) if err != nil { return nil, err } items := make([]*roomv1.AdminRoomListItem, 0, len(rooms)) for _, room := range rooms { items = append(items, adminRoomListItemProto(room)) } return &roomv1.AdminListRoomsResponse{Rooms: items, Total: total, ServerTimeMs: s.clock.Now().UnixMilli()}, nil } func (s *Service) AdminGetRoom(ctx context.Context, req *roomv1.AdminGetRoomRequest) (*roomv1.AdminGetRoomResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) room, exists, err := s.repository.AdminGetRoom(ctx, req.GetRoomId(), s.clock.Now().UnixMilli()) if err != nil { return nil, err } if !exists { return nil, xerr.New(xerr.NotFound, "room not found") } return &roomv1.AdminGetRoomResponse{Room: adminRoomListItemProto(room), ServerTimeMs: s.clock.Now().UnixMilli()}, nil } func (s *Service) AdminUpdateRoom(ctx context.Context, req *roomv1.AdminUpdateRoomRequest) (*roomv1.AdminUpdateRoomResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) cmd, err := s.adminUpdateCommand(req) if err != nil { return nil, err } result, err := s.applyAdminRoomMutation(ctx, cmd, cmd) if err != nil { return nil, err } return &roomv1.AdminUpdateRoomResponse{ Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, }, nil } func (s *Service) AdminDeleteRoom(ctx context.Context, req *roomv1.AdminDeleteRoomRequest) (*roomv1.AdminDeleteRoomResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) if req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetRoomId()) == "" { return nil, xerr.New(xerr.InvalidArgument, "room_id is required") } cmd := command.AdminDeleteRoom{ Base: baseFromMeta(req.GetMeta()), AdminID: req.GetAdminId(), AdminName: strings.TrimSpace(req.GetAdminName()), } result, err := s.mutateRoom(ctx, cmd, false, func(_ time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { if current == nil { return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found") } // 删除不是“关闭”的另一种展示状态,而是后台确认移除房间恢复来源;状态只服务本次响应和内存 Cell 收尾。 current.Status = state.RoomStatusDeleted current.OnlineUsers = map[int64]*state.RoomUserState{} for index := range current.MicSeats { current.ClearMicSession(index) } current.Version++ return mutationResult{ snapshot: current.ToProto(), deleteRoom: true, }, nil, nil }) if err != nil { return nil, err } if result.applied { s.deleteRoomListCacheBestEffort(ctx, cmd.RoomID()) s.forgetLoadedRoom(ctx, cmd.RoomID()) } return &roomv1.AdminDeleteRoomResponse{ Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, }, nil } func (s *Service) adminUpdateCommand(req *roomv1.AdminUpdateRoomRequest) (command.AdminUpdateRoom, error) { if req == nil || req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetRoomId()) == "" { return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room_id is required") } cmd := command.AdminUpdateRoom{ Base: baseFromMeta(req.GetMeta()), CloseReason: strings.TrimSpace(req.GetCloseReason()), AdminID: req.GetAdminId(), AdminName: strings.TrimSpace(req.GetAdminName()), } if req.Title != nil { value := strings.TrimSpace(req.GetTitle()) if value == "" { return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room title is required") } if utf8.RuneCountInString(value) > maxRoomNameRunes { return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room title is too long") } cmd.Title = &value } if req.CoverUrl != nil { value := strings.TrimSpace(req.GetCoverUrl()) if utf8.RuneCountInString(value) > maxRoomAvatarRunes { return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room cover is too long") } if value == "" { value = defaultRoomAvatar } cmd.CoverURL = &value } if req.Description != nil { value := strings.TrimSpace(req.GetDescription()) if utf8.RuneCountInString(value) > maxRoomDescriptionRunes { return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room description is too long") } cmd.Description = &value } if req.Mode != nil { value := strings.TrimSpace(req.GetMode()) if value == "" || utf8.RuneCountInString(value) > 64 { return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room mode is invalid") } cmd.Mode = &value } if req.Status != nil { value := strings.TrimSpace(req.GetStatus()) switch value { case state.RoomStatusActive, state.RoomStatusClosed: cmd.Status = &value default: return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room status is invalid") } if value != state.RoomStatusActive && strings.TrimSpace(cmd.CloseReason) == "" { return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "close reason is required") } } if req.OwnerCountryCode != nil { value := normalizeRoomCountryCode(req.GetOwnerCountryCode()) if value == "" { // 国家码只服务内部同步和运维 backfill;入口仍按列表筛选同一套规则拒绝非法值。 return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "owner country code is invalid") } cmd.OwnerCountryCode = &value } if req.VisibleRegionId != nil { value := req.GetVisibleRegionId() if value < 0 { return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "visible_region_id is invalid") } cmd.VisibleRegionID = &value } return cmd, nil } func (s *Service) applyAdminRoomMutation(ctx context.Context, logCommand command.Command, cmd command.AdminUpdateRoom) (mutationResult, error) { return s.mutateRoom(ctx, logCommand, 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") } changed := applyAdminRoomPatch(current, cmd) records := make([]outbox.Record, 0, 2) result := mutationResult{ // 房主国家是列表国家筛选和恢复态的一致性字段,内部 backfill 必须立刻把 RoomExt 写进 snapshot。 forceSnapshot: cmd.OwnerCountryCode != nil, } if cmd.Status != nil { switch *cmd.Status { case state.RoomStatusClosed, state.RoomStatusDeleted: kickTargets := closeRoomKickTargets(current) current.OnlineUsers = map[int64]*state.RoomUserState{} for index := range current.MicSeats { current.ClearMicSession(index) } closedEvent, err := outbox.Build(current.RoomID, "RoomClosed", current.Version+1, now, &roomeventsv1.RoomClosed{ ActorUserId: cmd.ActorUserID(), Reason: cmd.CloseReason, }) if err != nil { return mutationResult{}, nil, err } records = append(records, closedEvent) for _, targetUserID := range kickTargets { kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version+1, now, &roomeventsv1.RoomUserKicked{ ActorUserId: cmd.ActorUserID(), TargetUserId: targetUserID, }) if err != nil { return mutationResult{}, nil, err } records = append(records, kickEvent) } result.roomStatus = *cmd.Status result.closeInfo = &RoomCloseInfo{Reason: cmd.CloseReason, AdminID: cmd.AdminID, AdminName: cmd.AdminName, ClosedAtMS: now.UnixMilli()} result.syncEvent = &tencentim.RoomEvent{EventID: closedEvent.EventID, RoomID: current.RoomID, EventType: "room_closed", ActorUserID: cmd.ActorUserID(), RoomVersion: current.Version + 1} case state.RoomStatusActive: result.roomStatus = state.RoomStatusActive result.closeInfo = &RoomCloseInfo{ClearOnOpen: true} } } if cmd.Mode != nil { result.roomMode = cmd.Mode } if cmd.VisibleRegionID != nil { result.visibleRegionID = cmd.VisibleRegionID } if !changed && len(records) == 0 && result.roomMode == nil && result.visibleRegionID == nil && result.roomStatus == "" { result.snapshot = current.ToProto() return result, nil, nil } current.Version++ result.snapshot = current.ToProto() return result, records, nil }) } func applyAdminRoomPatch(current *state.RoomState, cmd command.AdminUpdateRoom) bool { changed := false if current.RoomExt == nil { current.RoomExt = map[string]string{} } if cmd.Title != nil && current.RoomExt[roomExtTitleKey] != *cmd.Title { current.RoomExt[roomExtTitleKey] = *cmd.Title changed = true } if cmd.CoverURL != nil && current.RoomExt[roomExtCoverURLKey] != *cmd.CoverURL { current.RoomExt[roomExtCoverURLKey] = *cmd.CoverURL changed = true } if cmd.Description != nil && current.RoomExt[roomExtDescriptionKey] != *cmd.Description { current.RoomExt[roomExtDescriptionKey] = *cmd.Description changed = true } if cmd.Mode != nil && current.Mode != *cmd.Mode { current.Mode = *cmd.Mode changed = true } if cmd.VisibleRegionID != nil && current.VisibleRegionID != *cmd.VisibleRegionID { current.VisibleRegionID = *cmd.VisibleRegionID changed = true } if cmd.OwnerCountryCode != nil { // 房主国家只作为发现页分类投影字段保存在 RoomExt;后台 HTTP 不开放直改入口,只有 user-service 国家事实同步会写入。 ownerCountryCode := normalizeRoomCountryCode(*cmd.OwnerCountryCode) if current.RoomExt[roomExtOwnerCountryKey] != ownerCountryCode { current.RoomExt[roomExtOwnerCountryKey] = ownerCountryCode changed = true } } if cmd.Status != nil && current.Status != *cmd.Status { current.Status = *cmd.Status changed = true } return changed } func adminRoomListItemProto(item AdminRoomListEntry) *roomv1.AdminRoomListItem { return &roomv1.AdminRoomListItem{ RoomId: item.RoomID, RoomShortId: item.RoomShortID, VisibleRegionId: item.VisibleRegionID, OwnerUserId: item.OwnerUserID, Title: item.Title, CoverUrl: item.CoverURL, Mode: item.Mode, Status: item.Status, CloseReason: item.CloseReason, ClosedByAdminId: item.ClosedByAdminID, ClosedByAdminName: item.ClosedByAdminName, ClosedAtMs: item.ClosedAtMS, Heat: item.Heat, OnlineCount: item.OnlineCount, SeatCount: item.SeatCount, OccupiedSeatCount: item.OccupiedSeatCount, SortScore: item.SortScore, CreatedAtMs: item.CreatedAtMS, UpdatedAtMs: item.UpdatedAtMS, } }