package roomadmin import ( "context" "fmt" "strconv" "strings" "time" "hyapp-admin-server/internal/integration/roomclient" ) const ( roomPinStatusActive = "active" roomPinStatusCancelled = "cancelled" roomPinStatusExpired = "expired" roomPinStatusAll = "all" roomPinTypeRegion = "region" roomPinTypeGlobal = "global" roomPinTypeCountry = "country" defaultRoomPinDays = int64(30) maxRoomPinDays = int64(3650) roomPinDayMillis = int64(24 * 60 * 60 * 1000) ) type RoomPinRoom struct { CoverURL string `json:"coverUrl"` RegionName string `json:"regionName"` RoomID string `json:"roomId"` RoomShortID string `json:"roomShortId"` Status string `json:"status"` Title string `json:"title"` VisibleRegionID int64 `json:"visibleRegionId"` } type RoomPin struct { CancelledAtMS int64 `json:"cancelledAtMs"` CancelledByAdminID uint `json:"cancelledByAdminId"` CreatedAtMS int64 `json:"createdAtMs"` CreatedByAdminID uint `json:"createdByAdminId"` ExpiresAtMS int64 `json:"expiresAtMs"` ID int64 `json:"id"` PinType string `json:"pinType"` PinnedAtMS int64 `json:"pinnedAtMs"` RemainingMS int64 `json:"remainingMs"` Room RoomPinRoom `json:"room"` Status string `json:"status"` UpdatedAtMS int64 `json:"updatedAtMs"` User RoomOwner `json:"user"` Weight int64 `json:"weight"` } func (s *Service) ListRoomPins(ctx context.Context, query roomPinListQuery) ([]RoomPin, int64, error) { if s.roomClient == nil { return nil, 0, fmt.Errorf("room service client is not configured") } query = normalizeRoomPinListQuery(query) result, err := s.roomClient.ListRoomPins(ctx, roomclient.ListRoomPinsRequest{ Page: query.Page, PageSize: query.PageSize, Keyword: query.Keyword, Status: query.Status, RegionID: query.RegionID, PinType: query.PinType, }) if err != nil { return nil, 0, err } items := roomPinsFromClient(result.Pins) if err := s.fillRoomPinDetails(ctx, items); err != nil { return nil, 0, err } return items, result.Total, nil } func (s *Service) CreateRoomPin(ctx context.Context, req createRoomPinRequest, actorID uint) (RoomPin, error) { if s.roomClient == nil { return RoomPin{}, fmt.Errorf("room service client is not configured") } input, err := normalizeCreateRoomPinRequest(req) if err != nil { return RoomPin{}, err } pin, err := s.roomClient.CreateRoomPin(ctx, roomclient.CreateRoomPinRequest{ RoomID: input.RoomID, PinType: input.PinType, Weight: input.Weight, DurationDays: input.DurationDays, PinnedAtMS: input.PinnedAtMS, ExpiresAtMS: input.ExpiresAtMS, AdminID: uint64(actorID), }) if err != nil { return RoomPin{}, mapRoomClientError(err) } item := roomPinFromClient(pin) items := []RoomPin{item} if err := s.fillRoomPinDetails(ctx, items); err != nil { return RoomPin{}, err } return items[0], nil } func (s *Service) CancelRoomPin(ctx context.Context, pinID int64, actorID uint) (RoomPin, error) { if s.roomClient == nil { return RoomPin{}, fmt.Errorf("room service client is not configured") } if pinID <= 0 { return RoomPin{}, fmt.Errorf("%w: 置顶 ID 不正确", ErrInvalidArgument) } pin, err := s.roomClient.CancelRoomPin(ctx, roomclient.CancelRoomPinRequest{PinID: pinID, AdminID: uint64(actorID)}) if err != nil { return RoomPin{}, mapRoomClientError(err) } item := roomPinFromClient(pin) items := []RoomPin{item} if err := s.fillRoomPinDetails(ctx, items); err != nil { return RoomPin{}, err } return items[0], nil } func normalizeRoomPinListQuery(query roomPinListQuery) roomPinListQuery { if query.Page < 1 { query.Page = 1 } if query.PageSize < 1 { query.PageSize = 20 } if query.PageSize > 100 { query.PageSize = 100 } query.Keyword = strings.TrimSpace(query.Keyword) query.Status = normalizeRoomPinStatus(query.Status) query.PinType = normalizeRoomPinType(query.PinType) if query.RegionID < 0 { query.RegionID = 0 } return query } func normalizeRoomPinType(pinType string) string { switch strings.ToLower(strings.TrimSpace(pinType)) { case roomPinTypeGlobal: return roomPinTypeGlobal case roomPinTypeCountry: return roomPinTypeCountry case roomPinTypeRegion: return roomPinTypeRegion default: return "" } } func normalizeRoomPinStatus(status string) string { switch strings.ToLower(strings.TrimSpace(status)) { case "", roomPinStatusActive: return roomPinStatusActive case roomPinStatusExpired: return roomPinStatusExpired case roomPinStatusCancelled: return roomPinStatusCancelled case roomPinStatusAll: return roomPinStatusAll default: return roomPinStatusActive } } func normalizeCreateRoomPinRequest(req createRoomPinRequest) (createRoomPinRequest, error) { req.RoomID = strings.TrimSpace(req.RoomID) if req.RoomID == "" || len(req.RoomID) > 64 { return req, fmt.Errorf("%w: 请选择房间", ErrInvalidArgument) } if req.Weight < 0 { return req, fmt.Errorf("%w: 权重不能小于 0", ErrInvalidArgument) } req.PinType = normalizeRoomPinType(req.PinType) if req.PinType == "" { req.PinType = roomPinTypeRegion } if req.PinnedAtMS <= 0 { req.PinnedAtMS = time.Now().UTC().UnixMilli() } if req.ExpiresAtMS <= 0 && req.DurationDays == 0 { req.DurationDays = defaultRoomPinDays } if req.ExpiresAtMS <= 0 { req.ExpiresAtMS = req.PinnedAtMS + req.DurationDays*roomPinDayMillis } if req.ExpiresAtMS <= req.PinnedAtMS { return req, fmt.Errorf("%w: 置顶时间区间不正确", ErrInvalidArgument) } durationDays := (req.ExpiresAtMS - req.PinnedAtMS + roomPinDayMillis - 1) / roomPinDayMillis if durationDays < 1 || durationDays > maxRoomPinDays { return req, fmt.Errorf("%w: 置顶时间必须在 1 到 3650 天之间", ErrInvalidArgument) } req.DurationDays = durationDays return req, nil } func roomPinsFromClient(input []roomclient.RoomPin) []RoomPin { out := make([]RoomPin, 0, len(input)) for _, pin := range input { out = append(out, roomPinFromClient(pin)) } return out } func roomPinFromClient(input roomclient.RoomPin) RoomPin { status := input.Room.Status if status != "active" { status = "closed" } ownerUserID := strconv.FormatInt(input.Room.OwnerUserID, 10) remainingMS := input.ExpiresAtMS - time.Now().UTC().UnixMilli() if remainingMS < 0 { remainingMS = 0 } return RoomPin{ ID: input.ID, PinType: firstNonEmpty(input.PinType, roomPinTypeRegion), Weight: input.Weight, Status: input.Status, PinnedAtMS: input.PinnedAtMS, ExpiresAtMS: input.ExpiresAtMS, CancelledAtMS: input.CancelledAtMS, CreatedByAdminID: uint(input.CreatedByAdminID), CancelledByAdminID: uint(input.CancelledByAdminID), CreatedAtMS: input.CreatedAtMS, UpdatedAtMS: input.UpdatedAtMS, RemainingMS: remainingMS, User: RoomOwner{UserID: ownerUserID}, Room: RoomPinRoom{ RoomID: input.Room.RoomID, RoomShortID: input.Room.RoomShortID, VisibleRegionID: input.Room.VisibleRegionID, Title: input.Room.Title, CoverURL: input.Room.CoverURL, Status: status, }, } } type roomPinScanner interface { Scan(dest ...any) error } func scanRoomPin(row roomPinScanner) (RoomPin, error) { var item RoomPin var ownerUserID int64 err := row.Scan( &item.ID, &item.Room.VisibleRegionID, &item.Room.RoomID, &item.Weight, &item.Status, &item.PinnedAtMS, &item.ExpiresAtMS, &item.CancelledAtMS, &item.CreatedByAdminID, &item.CancelledByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.Room.RoomShortID, &item.Room.Title, &item.Room.CoverURL, &ownerUserID, &item.Room.Status, ) item.User = RoomOwner{UserID: strconv.FormatInt(ownerUserID, 10)} item.RemainingMS = item.ExpiresAtMS - time.Now().UTC().UnixMilli() if item.RemainingMS < 0 { item.RemainingMS = 0 } if item.Room.Status != "active" { item.Room.Status = "closed" } return item, err } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func (s *Service) fillRoomPinDetails(ctx context.Context, items []RoomPin) error { if s.userDB == nil || len(items) == 0 { return nil } ownerIDs := make([]int64, 0, len(items)) regionIDs := make([]int64, 0, len(items)) for _, item := range items { if ownerID, err := strconv.ParseInt(item.User.UserID, 10, 64); err == nil && ownerID > 0 { ownerIDs = append(ownerIDs, ownerID) } if item.Room.VisibleRegionID > 0 { regionIDs = append(regionIDs, item.Room.VisibleRegionID) } } owners, err := s.queryRoomOwners(ctx, uniqueInt64s(ownerIDs)) if err != nil { return err } regions, err := s.queryRegionNames(ctx, uniqueInt64s(regionIDs)) if err != nil { return err } for index := range items { if ownerID, err := strconv.ParseInt(items[index].User.UserID, 10, 64); err == nil { if owner, ok := owners[ownerID]; ok { items[index].User = owner } } if name, ok := regions[items[index].Room.VisibleRegionID]; ok { items[index].Room.RegionName = name } } return nil }