package service import ( "context" "errors" "strings" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" ) var adminSeatCandidates = []int32{10, 15, 20, 25, 30} const ( roomPinTypeRegion = "region" roomPinTypeGlobal = "global" roomPinDayMillis = int64(24 * 60 * 60 * 1000) ) func (s *Service) AdminGetRoomSeatConfig(ctx context.Context, _ *roomv1.AdminGetRoomSeatConfigRequest) (*roomv1.AdminGetRoomSeatConfigResponse, error) { config, err := s.currentRoomSeatConfig(ctx) if err != nil { return nil, err } return &roomv1.AdminGetRoomSeatConfigResponse{Config: seatConfigToAdminProto(config), ServerTimeMs: s.clock.Now().UnixMilli()}, nil } func (s *Service) AdminUpdateRoomSeatConfig(ctx context.Context, req *roomv1.AdminUpdateRoomSeatConfigRequest) (*roomv1.AdminUpdateRoomSeatConfigResponse, error) { if req == nil { return nil, xerr.New(xerr.InvalidArgument, "room seat config is required") } config, err := normalizeRoomSeatConfig(RoomSeatConfig{ AppCode: appcode.FromContext(ctx), AllowedSeatCounts: req.GetAllowedSeatCounts(), DefaultSeatCount: req.GetDefaultSeatCount(), UpdatedAtMS: s.clock.Now().UTC().UnixMilli(), }) if err != nil { return nil, xerr.New(xerr.InvalidArgument, err.Error()) } if err := s.repository.UpsertRoomSeatConfig(ctx, config); err != nil { return nil, err } return &roomv1.AdminUpdateRoomSeatConfigResponse{Config: seatConfigToAdminProto(config), ServerTimeMs: config.UpdatedAtMS}, nil } func (s *Service) AdminListRoomPins(ctx context.Context, req *roomv1.AdminListRoomPinsRequest) (*roomv1.AdminListRoomPinsResponse, error) { nowMS := s.clock.Now().UTC().UnixMilli() pins, total, err := s.repository.AdminListRoomPins(ctx, AdminRoomPinQuery{ Page: int(req.GetPage()), PageSize: int(req.GetPageSize()), Keyword: strings.TrimSpace(req.GetKeyword()), Status: strings.TrimSpace(req.GetStatus()), RegionID: req.GetVisibleRegionId(), PinType: normalizeRoomPinType(req.GetPinType()), NowMS: nowMS, }) if err != nil { return nil, err } out := make([]*roomv1.AdminRoomPin, 0, len(pins)) for _, pin := range pins { if pin.Status == "active" && pin.ExpiresAtMS <= nowMS { // 过期不需要回写房间状态;对后台读模型呈现 expired,公共列表查询也会自动把它排除出置顶 join。 pin.Status = "expired" } out = append(out, roomPinToAdminProto(pin)) } return &roomv1.AdminListRoomPinsResponse{Pins: out, Total: total, ServerTimeMs: nowMS}, nil } func (s *Service) AdminCreateRoomPin(ctx context.Context, req *roomv1.AdminCreateRoomPinRequest) (*roomv1.AdminCreateRoomPinResponse, error) { if req == nil || strings.TrimSpace(req.GetRoomId()) == "" { return nil, xerr.New(xerr.InvalidArgument, "room id is required") } if req.GetWeight() < 0 { return nil, xerr.New(xerr.InvalidArgument, "room pin weight is invalid") } nowMS := s.clock.Now().UTC().UnixMilli() pinnedAtMS, expiresAtMS, err := normalizeAdminRoomPinInterval(req.GetPinnedAtMs(), req.GetExpiresAtMs(), req.GetDurationDays(), nowMS) if err != nil { return nil, xerr.New(xerr.InvalidArgument, err.Error()) } pin, ok, err := s.repository.AdminCreateRoomPin(ctx, AdminCreateRoomPinInput{ RoomID: strings.TrimSpace(req.GetRoomId()), PinType: normalizeRoomPinType(req.GetPinType()), Weight: req.GetWeight(), DurationDays: req.GetDurationDays(), PinnedAtMS: pinnedAtMS, ExpiresAtMS: expiresAtMS, AdminID: req.GetAdminId(), NowMS: nowMS, }) if err != nil { return nil, err } if !ok { return nil, xerr.New(xerr.Conflict, "room is not active or not found") } return &roomv1.AdminCreateRoomPinResponse{Pin: roomPinToAdminProto(pin), ServerTimeMs: nowMS}, nil } func (s *Service) AdminCancelRoomPin(ctx context.Context, req *roomv1.AdminCancelRoomPinRequest) (*roomv1.AdminCancelRoomPinResponse, error) { if req == nil || req.GetPinId() <= 0 { return nil, xerr.New(xerr.InvalidArgument, "room pin id is invalid") } nowMS := s.clock.Now().UTC().UnixMilli() pin, ok, err := s.repository.AdminCancelRoomPin(ctx, req.GetPinId(), req.GetAdminId(), nowMS) if err != nil { return nil, err } if !ok { return nil, xerr.New(xerr.NotFound, "room pin not found") } return &roomv1.AdminCancelRoomPinResponse{Pin: roomPinToAdminProto(pin), ServerTimeMs: nowMS}, nil } func seatConfigToAdminProto(config RoomSeatConfig) *roomv1.AdminRoomSeatConfig { return &roomv1.AdminRoomSeatConfig{ CandidateSeatCounts: append([]int32(nil), adminSeatCandidates...), AllowedSeatCounts: append([]int32(nil), config.AllowedSeatCounts...), DefaultSeatCount: config.DefaultSeatCount, UpdatedAtMs: config.UpdatedAtMS, } } func roomPinToAdminProto(pin AdminRoomPinEntry) *roomv1.AdminRoomPin { return &roomv1.AdminRoomPin{ Id: pin.ID, VisibleRegionId: pin.VisibleRegionID, RoomId: pin.RoomID, Weight: pin.Weight, Status: pin.Status, PinType: pin.PinType, PinnedAtMs: pin.PinnedAtMS, ExpiresAtMs: pin.ExpiresAtMS, CancelledAtMs: pin.CancelledAtMS, CreatedByAdminId: pin.CreatedByAdminID, CancelledByAdminId: pin.CancelledByAdminID, CreatedAtMs: pin.CreatedAtMS, UpdatedAtMs: pin.UpdatedAtMS, Room: &roomv1.AdminRoomPinRoom{ RoomId: pin.RoomID, RoomShortId: pin.RoomShortID, VisibleRegionId: pin.RoomVisibleRegionID, OwnerUserId: pin.OwnerUserID, Title: pin.Title, CoverUrl: pin.CoverURL, Status: pin.RoomStatus, }, } } func normalizeRoomPinType(pinType string) string { switch strings.ToLower(strings.TrimSpace(pinType)) { case roomPinTypeGlobal: return roomPinTypeGlobal default: // 空值和未知值都回落到区域置顶,兼容旧后台只传 room_id/duration_days 的请求。 return roomPinTypeRegion } } func normalizeAdminRoomPinInterval(pinnedAtMS int64, expiresAtMS int64, durationDays int64, nowMS int64) (int64, int64, error) { if pinnedAtMS <= 0 { pinnedAtMS = nowMS } if expiresAtMS <= 0 && durationDays > 0 { expiresAtMS = pinnedAtMS + durationDays*roomPinDayMillis } if expiresAtMS <= pinnedAtMS { return 0, 0, errors.New("room pin time range is invalid") } if expiresAtMS-pinnedAtMS > 3650*roomPinDayMillis { return 0, 0, errors.New("room pin time range is too long") } return pinnedAtMS, expiresAtMS, nil }