2026-05-16 02:21:43 +08:00

243 lines
7.3 KiB
Go

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.LongBadge = normalizeResource(firstResource(item.LongBadge, item.Badge))
item.Badge = item.LongBadge
item.ShortBadge = normalizeResource(item.ShortBadge)
item.AvatarFrame = normalizeResource(item.AvatarFrame)
item.EntryEffect = normalizeResource(item.EntryEffect)
item.ChatBubble = normalizeResource(item.ChatBubble)
item.FloatPicture = normalizeResource(item.FloatPicture)
item.BackgroundCard = normalizeResource(item.BackgroundCard)
item.EffectImage = normalizeResource(item.EffectImage)
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 firstResource(preferred VipResourcePayload, fallback VipResourcePayload) VipResourcePayload {
if preferred.ResourceID.Int64() > 0 || strings.TrimSpace(preferred.Name) != "" ||
strings.TrimSpace(preferred.URL) != "" || strings.TrimSpace(preferred.SourceURL) != "" ||
strings.TrimSpace(preferred.ResourceURL) != "" || strings.TrimSpace(preferred.Cover) != "" ||
strings.TrimSpace(preferred.CoverURL) != "" {
return preferred
}
return fallback
}
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.LongBadge.ResourceID.Int64()
row.BadgeName = ""
row.BadgeURL = ""
row.ShortBadgeResourceID = item.ShortBadge.ResourceID.Int64()
row.ShortBadgeName = ""
row.ShortBadgeURL = ""
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.BackgroundCardResourceID = item.BackgroundCard.ResourceID.Int64()
row.BackgroundCardName = ""
row.BackgroundCardURL = ""
row.EffectImageResourceID = item.EffectImage.ResourceID.Int64()
row.EffectImageName = ""
row.EffectImageURL = ""
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
}