package service import ( "context" "encoding/json" "fmt" "strconv" "time" "github.com/redis/go-redis/v9" "gorm.io/gorm" "spider/internal/model" ) const settingsCacheKey = "spider:cache:settings" const settingsCacheTTL = 5 * time.Minute // SettingsService provides hot-reloadable access to managed_settings. type SettingsService struct { db *gorm.DB redis *redis.Client } // NewSettingsService creates a new SettingsService. func NewSettingsService(db *gorm.DB, rdb *redis.Client) *SettingsService { return &SettingsService{db: db, redis: rdb} } // Load 从数据库加载所有设置到 Redis 缓存 func (s *SettingsService) Load(ctx context.Context) error { var settings []model.ManagedSetting if err := s.db.WithContext(ctx).Find(&settings).Error; err != nil { return fmt.Errorf("load settings from db: %w", err) } if len(settings) == 0 { // Nothing to cache; ensure any stale cache is cleared. return s.redis.Del(ctx, settingsCacheKey).Err() } fields := make([]interface{}, 0, len(settings)*2) for _, setting := range settings { fields = append(fields, setting.KeyName, setting.Value) } pipe := s.redis.Pipeline() pipe.HSet(ctx, settingsCacheKey, fields...) pipe.Expire(ctx, settingsCacheKey, settingsCacheTTL) _, err := pipe.Exec(ctx) if err != nil { return fmt.Errorf("cache settings to redis: %w", err) } return nil } // Get 获取设置值(先读 Redis 缓存,缓存不存在则读 DB 并回填) func (s *SettingsService) Get(ctx context.Context, key string) (string, error) { // Try cache first. val, err := s.redis.HGet(ctx, settingsCacheKey, key).Result() if err == nil { return val, nil } // Cache miss or Redis error — fall back to DB. var setting model.ManagedSetting if err := s.db.WithContext(ctx).Where("key_name = ?", key).First(&setting).Error; err != nil { return "", fmt.Errorf("setting %q not found: %w", key, err) } // Back-fill the cache entry. pipe := s.redis.Pipeline() pipe.HSet(ctx, settingsCacheKey, key, setting.Value) pipe.Expire(ctx, settingsCacheKey, settingsCacheTTL) pipe.Exec(ctx) //nolint:errcheck — best-effort return setting.Value, nil } // GetInt 获取整数类型设置 func (s *SettingsService) GetInt(ctx context.Context, key string, defaultVal int) int { raw, err := s.Get(ctx, key) if err != nil { return defaultVal } v, err := strconv.Atoi(raw) if err != nil { return defaultVal } return v } // GetFloat 获取浮点类型设置 func (s *SettingsService) GetFloat(ctx context.Context, key string, defaultVal float64) float64 { raw, err := s.Get(ctx, key) if err != nil { return defaultVal } v, err := strconv.ParseFloat(raw, 64) if err != nil { return defaultVal } return v } // GetBool 获取布尔类型设置 func (s *SettingsService) GetBool(ctx context.Context, key string, defaultVal bool) bool { raw, err := s.Get(ctx, key) if err != nil { return defaultVal } v, err := strconv.ParseBool(raw) if err != nil { return defaultVal } return v } // GetJSON 获取 JSON 类型设置,解析到 target func (s *SettingsService) GetJSON(ctx context.Context, key string, target interface{}) error { raw, err := s.Get(ctx, key) if err != nil { return err } return json.Unmarshal([]byte(raw), target) } // Set 更新设置(更新 DB + 清除缓存) func (s *SettingsService) Set(ctx context.Context, key, value string) error { result := s.db.WithContext(ctx).Model(&model.ManagedSetting{}). Where("key_name = ?", key). Update("value", value) if result.Error != nil { return fmt.Errorf("update setting %q in db: %w", key, result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("setting %q not found", key) } // Invalidate cache so next read reloads from DB. return s.Invalidate(ctx) } // Invalidate 清除缓存,下次读取时从 DB 加载 func (s *SettingsService) Invalidate(ctx context.Context) error { return s.redis.Del(ctx, settingsCacheKey).Err() }