235 lines
8.5 KiB
Go
235 lines
8.5 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log/slog"
|
||
"strings"
|
||
|
||
roomv1 "hyapp.local/api/proto/room/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/logx"
|
||
"hyapp/pkg/xerr"
|
||
)
|
||
|
||
var adminSeatCandidates = []int32{10, 15, 20, 25, 30}
|
||
|
||
const (
|
||
roomPinTypeRegion = "region"
|
||
roomPinTypeGlobal = "global"
|
||
roomPinTypeCountry = "country"
|
||
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: normalizeRoomPinListType(req.GetPinType()),
|
||
NowMS: nowMS,
|
||
})
|
||
if err != nil {
|
||
// gRPC 传输层会把普通 repository error 统一隐藏成 INTERNAL_ERROR;这里保留原始错误和筛选边界,
|
||
// 让线上排障能直接区分 SQL/schema 问题、上下文租户问题和输入条件问题。
|
||
logx.Error(ctx, "admin_list_room_pins_failed", err,
|
||
slog.Int("page", int(req.GetPage())),
|
||
slog.Int("page_size", int(req.GetPageSize())),
|
||
slog.String("status", strings.TrimSpace(req.GetStatus())),
|
||
slog.Int64("visible_region_id", req.GetVisibleRegionId()),
|
||
slog.String("pin_type", normalizeRoomPinListType(req.GetPinType())),
|
||
slog.Bool("has_keyword", strings.TrimSpace(req.GetKeyword()) != ""),
|
||
)
|
||
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")
|
||
}
|
||
s.projectRoomPinCacheBestEffort(ctx, roomListPinEntryFromAdmin(pin))
|
||
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")
|
||
}
|
||
s.projectRoomPinCacheBestEffort(ctx, roomListPinEntryFromAdmin(pin))
|
||
return &roomv1.AdminCancelRoomPinResponse{Pin: roomPinToAdminProto(pin), ServerTimeMs: nowMS}, nil
|
||
}
|
||
|
||
func (s *Service) projectRoomPinCacheBestEffort(ctx context.Context, pin RoomListPinEntry) {
|
||
if s == nil || s.roomListCache == nil {
|
||
return
|
||
}
|
||
if err := s.roomListCache.UpsertRoomPin(ctx, pin); err != nil {
|
||
// 后台置顶 owner 已经写入 MySQL;缓存失败只影响短暂展示顺序,后续 refresh 会从 owner 表重建。
|
||
logx.Warn(ctx, "room_pin_cache_project_failed", slog.Int64("pin_id", pin.ID), slog.String("room_id", pin.RoomID), slog.String("pin_type", pin.PinType), slog.String("error", err.Error()))
|
||
}
|
||
}
|
||
|
||
func roomListPinEntryFromAdmin(pin AdminRoomPinEntry) RoomListPinEntry {
|
||
return RoomListPinEntry{
|
||
ID: pin.ID,
|
||
AppCode: pin.AppCode,
|
||
VisibleRegionID: pin.VisibleRegionID,
|
||
PinType: pin.PinType,
|
||
RoomID: pin.RoomID,
|
||
Weight: pin.Weight,
|
||
Status: pin.Status,
|
||
PinnedAtMS: pin.PinnedAtMS,
|
||
ExpiresAtMS: pin.ExpiresAtMS,
|
||
}
|
||
}
|
||
|
||
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
|
||
case roomPinTypeCountry:
|
||
return roomPinTypeCountry
|
||
default:
|
||
// 空值和未知值都回落到区域置顶,兼容旧后台只传 room_id/duration_days 的请求。
|
||
return roomPinTypeRegion
|
||
}
|
||
}
|
||
|
||
func normalizeRoomPinListType(pinType string) string {
|
||
switch strings.ToLower(strings.TrimSpace(pinType)) {
|
||
case roomPinTypeRegion:
|
||
return roomPinTypeRegion
|
||
case roomPinTypeGlobal:
|
||
return roomPinTypeGlobal
|
||
case roomPinTypeCountry:
|
||
return roomPinTypeCountry
|
||
default:
|
||
// 列表空类型代表“不按置顶类型筛选”;不能复用创建默认 region,否则国家置顶会从后台总列表消失。
|
||
return ""
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|