// Package settings 提供数据库 settings KV 的运行时读写: // env(FCB_*)提供基线,DB KV 覆盖可变项;管理端修改后立即生效。 package settings import ( "context" "encoding/json" "errors" "log" "sync" "time" "gorm.io/gorm" "filecodebox/internal/config" "filecodebox/internal/model" ) // settingsKey 数据库中的配置键(对齐参考实现)。 const settingsKey = "settings" // Manager 设置管理器:线程安全,缓存 KV 覆盖到内存。 type Manager struct { db *gorm.DB cfg *config.Config mu sync.RWMutex secret string // jwt_secret(频繁使用,单独缓存) initPwd string // admin_token 哈希(频繁使用,单独缓存) } // NewManager 构造设置管理器并加载 DB KV。 // ensure 默认配置行(首次启动时写入 settings 键)。 func NewManager(ctx context.Context, db *gorm.DB, cfg *config.Config) (*Manager, error) { m := &Manager{db: db, cfg: cfg} if err := m.ensureSettingsRow(ctx); err != nil { return nil, err } if err := m.Reload(ctx); err != nil { return nil, err } return m, nil } // ensureSettingsRow 首次启动时把默认安全配置写入 KV(对齐 ensure_settings_row)。 func (m *Manager) ensureSettingsRow(ctx context.Context) error { var row model.KeyValue err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error if err == nil { return nil } // 双方言:必须用 errors.Is 判定(sqlite 驱动错误链与字符串消息与 postgres 不同) if !errors.Is(err, gorm.ErrRecordNotFound) { return err } // 不存在:写入初始配置(不含 admin_token/jwt_secret,保持未初始化状态) initial := map[string]any{} raw, _ := json.Marshal(initial) row = model.KeyValue{Key: settingsKey, Value: strPtr(string(raw))} if err := m.db.WithContext(ctx).Create(&row).Error; err != nil { return err } log.Println("[settings] 系统尚未初始化,请在浏览器中打开站点并完成管理员密码设置") return nil } // Reload 从数据库加载 settings KV 并覆盖到运行时配置。 func (m *Manager) Reload(ctx context.Context) error { var row model.KeyValue err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error if err != nil { // 行不存在时保持现有覆盖(双方言:errors.Is 判定) if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return err } kv := map[string]any{} if row.Value != nil && *row.Value != "" { if err := json.Unmarshal([]byte(*row.Value), &kv); err != nil { log.Printf("[settings] settings KV 解析失败: %v", err) } } // 内部键不允许通过 KV 覆盖(_ 开头) safe := map[string]any{} for k, v := range kv { if len(k) > 0 && k[0] == '_' { continue } safe[k] = v } m.mu.Lock() m.cfg.ApplyKV(safe) m.secret, _ = safe["jwt_secret"].(string) m.initPwd, _ = safe["admin_token"].(string) m.mu.Unlock() return nil } // Get 返回当前配置(只读使用;不要修改返回值)。 func (m *Manager) Get() *config.Config { return m.cfg } // SecretProvider 返回 jwt_secret 读取函数(JWT 中间件用)。 func (m *Manager) SecretProvider() func() string { return func() string { m.mu.RLock() defer m.mu.RUnlock() return m.secret } } // IsInitialized 系统是否已完成初始化(管理员密码已设置且非默认密码)。 func (m *Manager) IsInitialized() bool { m.mu.RLock() defer m.mu.RUnlock() if m.initPwd == "" { return false } // 旧版默认密码视为未初始化(对齐 LEGACY_DEFAULT_ADMIN_TOKEN 检查) return !verifyLegacyDefault(m.initPwd) } // legacyDefaultToken 参考实现的旧默认管理员密码。 const legacyDefaultToken = "FileCodeBox2023" // verifyLegacyDefault 检查哈希是否对应旧默认密码。 func verifyLegacyDefault(hashed string) bool { if hashed == "" { return false } return VerifyPassword(legacyDefaultToken, hashed) } // UpdateKV 合并更新 settings KV(管理端保存配置)。 func (m *Manager) UpdateKV(ctx context.Context, patch map[string]any) error { m.mu.Lock() defer m.mu.Unlock() // 读现有值 var row model.KeyValue err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error kv := map[string]any{} if err == nil && row.Value != nil { _ = json.Unmarshal([]byte(*row.Value), &kv) } for k, v := range patch { if len(k) > 0 && k[0] == '_' { continue } kv[k] = v } raw, err := json.Marshal(kv) if err != nil { return err } if err == nil && row.ID > 0 { row.Value = strPtr(string(raw)) return m.db.WithContext(ctx).Model(&row).Update("value", row.Value).Error } row = model.KeyValue{Key: settingsKey, Value: strPtr(string(raw))} return m.db.WithContext(ctx).Create(&row).Error } // SystemStart 记录系统启动时间(对齐 sys_start 键)。 func (m *Manager) SystemStart(ctx context.Context) { now := time.Now().UnixMilli() raw, _ := json.Marshal(now) _ = m.db.WithContext(ctx).Where(model.KeyValue{Key: "sys_start"}). Assign(model.KeyValue{Value: strPtr(string(raw))}). FirstOrCreate(&model.KeyValue{Key: "sys_start"}).Error } func strPtr(s string) *string { return &s }