feat(M4): 生命周期 + 审计 — 到期锁定/回收 cron、审计查询/CSV 导出/每日归档、settings 动态策略

This commit is contained in:
2026-08-30 10:17:22 +08:00
parent 1ea18490e0
commit c0e2ff975a
18 changed files with 1088 additions and 75 deletions
+129
View File
@@ -0,0 +1,129 @@
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"}
}