package vip import ( "context" "errors" "net/http" "sort" "strings" "time" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "gorm.io/gorm" ) // GetConfig returns the operator-managed VIP configuration for one sysOrigin. func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*ConfigResponse, error) { sysOrigin = normalizeSysOrigin(sysOrigin) if sysOrigin == "" { return nil, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required") } rows, err := s.loadConfigRows(ctx, sysOrigin) if err != nil { return nil, err } resources, err := s.loadResourceLookup(ctx, resourceIDsFromConfigRows(rows)) if err != nil { return nil, err } levels := buildDefaultLevelPayloads() configured := len(rows) > 0 var updateTime time.Time for _, row := range rows { if row.Level < 1 || row.Level > levelCount { continue } levels[row.Level-1] = payloadFromConfigWithResources(row, resources) if row.UpdateTime.After(updateTime) { updateTime = row.UpdateTime } } return &ConfigResponse{ Configured: configured, SysOrigin: sysOrigin, Levels: levels, UpdateTime: formatTime(updateTime), }, nil } // SaveConfig saves all five VIP levels for one sysOrigin. func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) { sysOrigin := normalizeSysOrigin(req.SysOrigin) if sysOrigin == "" { return nil, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required") } levels, err := normalizeSaveLevels(req.Levels) if err != nil { return nil, err } now := time.Now() if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { for _, item := range levels { var row model.VipLevelConfig err := tx.Where("sys_origin = ? AND level = ?", sysOrigin, item.Level).First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { nextID, idErr := utils.NextID() if idErr != nil { return idErr } row = model.VipLevelConfig{ ID: nextID, SysOrigin: sysOrigin, Level: item.Level, CreateTime: now, } } else if err != nil { return err } applyLevelPayloadToConfig(&row, item, now) if err := tx.Save(&row).Error; err != nil { return err } } return nil }); err != nil { return nil, err } return s.GetConfig(ctx, sysOrigin) } func normalizeSaveLevels(levels []VipLevelPayload) ([]VipLevelPayload, error) { if len(levels) != levelCount { return nil, NewAppError(http.StatusBadRequest, "invalid_levels", "levels must contain exactly 5 items") } seen := make(map[int]struct{}, levelCount) normalized := make([]VipLevelPayload, 0, levelCount) for _, item := range levels { if err := validateLevel(item.Level); err != nil { return nil, err } if _, exists := seen[item.Level]; exists { return nil, NewAppError(http.StatusBadRequest, "duplicate_level", "level must be unique") } seen[item.Level] = struct{}{} if item.PriceGold < 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_price", "priceGold must be greater than or equal to 0") } item.LevelCode = levelCode(item.Level) item.DisplayName = strings.TrimSpace(item.DisplayName) if item.DisplayName == "" { item.DisplayName = levelDisplayName(item.Level) } item.DurationDays = vipDurationDay item.Badge = normalizeResource(item.Badge) item.AvatarFrame = normalizeResource(item.AvatarFrame) item.EntryEffect = normalizeResource(item.EntryEffect) item.ChatBubble = normalizeResource(item.ChatBubble) item.FloatPicture = normalizeResource(item.FloatPicture) normalized = append(normalized, item) } sort.Slice(normalized, func(i, j int) bool { return normalized[i].Level < normalized[j].Level }) return normalized, nil } func normalizeResource(resource VipResourcePayload) VipResourcePayload { if resource.ResourceID < 0 { resource.ResourceID = 0 } resource.Name = strings.TrimSpace(resource.Name) resource.URL = strings.TrimSpace(resource.URL) resource.SourceURL = strings.TrimSpace(resource.SourceURL) resource.ResourceURL = strings.TrimSpace(resource.ResourceURL) resource.Cover = strings.TrimSpace(resource.Cover) resource.CoverURL = strings.TrimSpace(resource.CoverURL) if resource.URL == "" { resource.URL = resource.SourceURL } if resource.URL == "" { resource.URL = resource.ResourceURL } if resource.Cover == "" { resource.Cover = resource.CoverURL } resource.SourceURL = resource.URL resource.ResourceURL = resource.URL resource.CoverURL = resource.Cover return resource } func applyLevelPayloadToConfig(row *model.VipLevelConfig, item VipLevelPayload, now time.Time) { row.Level = item.Level row.LevelCode = levelCode(item.Level) row.DisplayName = strings.TrimSpace(item.DisplayName) row.Enabled = item.Enabled row.DurationDays = vipDurationDay row.PriceGold = item.PriceGold row.BadgeResourceID = item.Badge.ResourceID.Int64() row.BadgeName = "" row.BadgeURL = "" row.AvatarFrameResourceID = item.AvatarFrame.ResourceID.Int64() row.AvatarFrameName = "" row.AvatarFrameURL = "" row.EntryEffectResourceID = item.EntryEffect.ResourceID.Int64() row.EntryEffectName = "" row.EntryEffectURL = "" row.ChatBubbleResourceID = item.ChatBubble.ResourceID.Int64() row.ChatBubbleName = "" row.ChatBubbleURL = "" row.FloatPictureResourceID = item.FloatPicture.ResourceID.Int64() row.FloatPictureName = "" row.FloatPictureURL = "" row.UpdateTime = now if row.CreateTime.IsZero() { row.CreateTime = now } } func (s *Service) loadConfigRows(ctx context.Context, sysOrigin string) ([]model.VipLevelConfig, error) { var rows []model.VipLevelConfig if err := s.db.WithContext(ctx). Where("sys_origin = ?", normalizeSysOrigin(sysOrigin)). Order("level asc"). Find(&rows).Error; err != nil { return nil, err } return rows, nil } func (s *Service) loadConfigMap(ctx context.Context, sysOrigin string) (map[int]model.VipLevelConfig, error) { rows, err := s.loadConfigRows(ctx, sysOrigin) if err != nil { return nil, err } configs := make(map[int]model.VipLevelConfig, len(rows)) for _, row := range rows { configs[row.Level] = row } return configs, nil } func buildDefaultLevelPayloads() []VipLevelPayload { levels := make([]VipLevelPayload, 0, levelCount) for level := 1; level <= levelCount; level++ { levels = append(levels, defaultLevelPayload(level)) } return levels }