package service import ( "context" "errors" "fmt" "strings" "time" "gorm.io/gorm" "ws_usernode/internal/config" "ws_usernode/internal/model" ) // 系统设置错误。 var ErrSettingKeyUnknown = errors.New("service: 未知设置项") var ErrSettingValueInvalid = errors.New("service: 设置值不合法") // 设置项 key 白名单(settings 表可覆盖 config 的策略字段,PLAN F7)。 // 其余设置(SMTP、会话时长等)为启动时配置,不支持动态覆盖。 var settingKeys = map[string]bool{ "policy.default_ttl": true, // 新账号默认有效期(时长) "policy.recycle_period": true, // 到期后回收期(时长) "policy.audit_retention": true, // 审计保留时长(时长) } // SettingItem 一个设置项的生效值。 type SettingItem struct { Key string `json:"key"` Value string `json:"value"` // 生效值(settings 覆盖优先,否则 config 默认) Overridden bool `json:"overridden"` // 是否被 settings 表覆盖 } // SettingService 系统设置读写(settings 表,config 提供默认值)。 type SettingService struct { db *gorm.DB cfg *config.Config } // NewSettingService 创建设置服务。 func NewSettingService(db *gorm.DB, cfg *config.Config) *SettingService { return &SettingService{db: db, cfg: cfg} } // GetAll 返回全部设置项的生效值(未覆盖的展示 config 默认值)。 func (s *SettingService) GetAll(ctx context.Context) ([]SettingItem, error) { var rows []model.Setting if err := s.db.WithContext(ctx).Find(&rows).Error; err != nil { return nil, err } overridden := make(map[string]string, len(rows)) for _, r := range rows { if settingKeys[r.Key] { overridden[r.Key] = r.Value } } items := make([]SettingItem, 0, len(settingKeys)) for _, key := range sortedSettingKeys() { if v, ok := overridden[key]; ok { items = append(items, SettingItem{Key: key, Value: v, Overridden: true}) } else { items = append(items, SettingItem{Key: key, Value: s.defaultValue(key), Overridden: false}) } } return items, nil } // Set 更新一个设置项:校验 key 白名单与值格式(均为时长)。全部校验通过才写入。 func (s *SettingService) Set(ctx context.Context, key, value string) error { if !settingKeys[key] { return fmt.Errorf("%w: %q", ErrSettingKeyUnknown, key) } value = strings.TrimSpace(value) if _, err := time.ParseDuration(value); err != nil { return fmt.Errorf("%w: %q(需为时长,如 2160h)", ErrSettingValueInvalid, value) } return s.db.WithContext(ctx).Save(&model.Setting{Key: key, Value: value}).Error } // Duration 读取策略时长设置:settings 覆盖优先,无覆盖或解析失败用 fallback。 func (s *SettingService) Duration(ctx context.Context, key string, fallback time.Duration) time.Duration { if !settingKeys[key] { return fallback } var row model.Setting err := s.db.WithContext(ctx).First(&row, "key = ?", key).Error if err != nil { return fallback } d, err := time.ParseDuration(row.Value) if err != nil { return fallback } return d } // DefaultTTL 新账号默认有效期(settings 覆盖 config.policy.default_ttl)。 func (s *SettingService) DefaultTTL(ctx context.Context) time.Duration { return s.Duration(ctx, "policy.default_ttl", s.cfg.Policy.DefaultTTL) } // RecyclePeriod 到期后回收期。 func (s *SettingService) RecyclePeriod(ctx context.Context) time.Duration { return s.Duration(ctx, "policy.recycle_period", s.cfg.Policy.RecyclePeriod) } // AuditRetention 审计保留时长。 func (s *SettingService) AuditRetention(ctx context.Context) time.Duration { return s.Duration(ctx, "policy.audit_retention", s.cfg.Policy.AuditRetention) } // defaultValue 返回设置项的 config 默认值(保证遍历顺序稳定)。 func (s *SettingService) defaultValue(key string) string { switch key { case "policy.default_ttl": return s.cfg.Policy.DefaultTTL.String() case "policy.recycle_period": return s.cfg.Policy.RecyclePeriod.String() case "policy.audit_retention": return s.cfg.Policy.AuditRetention.String() } return "" } func sortedSettingKeys() []string { // 固定顺序展示,避免 map 遍历随机 return []string{"policy.default_ttl", "policy.recycle_period", "policy.audit_retention"} }