// 系统设置的运行时解析层。 // // 迁移自环境变量的设置(站点地址、OAuth 凭据、R2 存储、管理员账号)存进 // settings KV 表后,生效规则统一为「DB 有值用 DB,空值回落环境变量」: // 老部署不改 env 也能照常跑,后台改过的值随时可以赢回来。 // // Resolver 在 env Config 之上叠加 DB 值,给两个 API 提供按需解析的有效配置; // 配置带短缓存(设置保存时主动失效),存储后端按配置签名惰性重建—— // 后台改完 R2 凭据不用重启,下一次上传就走新桶。 package config import ( "strings" "sync" "time" "oneblog/internal/model" "oneblog/internal/storage" "oneblog/internal/store" ) // Overlay 返回 c 的副本:model.Settings 里非空的系统设置项覆盖 env 值。 // 每个字段独立回落——比如 client_id 在后台改了、secret 还是 env 的, // 两个拼起来照样能用,不必逼站主一次填齐。 func (c *Config) Overlay(st model.Settings) *Config { e := *c pick := func(dst *string, v string) { if t := strings.TrimSpace(v); t != "" { *dst = t } } pick(&e.SiteURL, st.SiteURL) pick(&e.GitHubClientID, st.GitHubClientID) pick(&e.GitHubClientSecret, st.GitHubClientSecret) pick(&e.GoogleClientID, st.GoogleClientID) pick(&e.GoogleClientSecret, st.GoogleClientSecret) pick(&e.TelegramBot, st.TelegramBot) pick(&e.TelegramToken, st.TelegramToken) pick(&e.AdminUser, st.AdminUsername) pick(&e.UploadsPublicBase, st.UploadsPublicBase) pick(&e.S3Endpoint, st.S3Endpoint) pick(&e.R2Bucket, st.R2Bucket) pick(&e.R2AccessKey, st.R2AccessKey) pick(&e.R2SecretKey, st.R2SecretKey) // 存储端按「凭据是否齐全」重新判定,与 config.Load 的规则一致 if e.S3Endpoint != "" && e.R2Bucket != "" && e.R2AccessKey != "" && e.R2SecretKey != "" { e.StorageDriver = "r2" } else { e.StorageDriver = "local" } return &e } // SettingsSource 是 Resolver 对 store 的最小依赖(测试可注入假实现)。 type SettingsSource interface { GetSettings() (model.Settings, error) } // Resolver 提供带缓存的有效配置与按需重建的存储后端。零值不可用, // 用 NewResolver 构造;API 拿不到 Resolver 时(旧测试)直接用 env 值。 type Resolver struct { env *Config st SettingsSource mu sync.Mutex cfg *Config cfgAt time.Time blobs storage.BlobStore blobsKV string } // cacheTTL 只兜「绕过后台直接改库」这种少数派路径;正常保存走 // Invalidate() 立即生效,所以 TTL 可以放宽到不影响体验的量级。 const cacheTTL = 10 * time.Second func NewResolver(env *Config, st SettingsSource) *Resolver { return &Resolver{env: env, st: st} } // Config 返回叠加了 DB 设置的有效配置。读库失败时回落 env—— // 设置页不能因为 DB 抖动把整站带挂。 func (r *Resolver) Config() *Config { if r == nil { return r.env } r.mu.Lock() defer r.mu.Unlock() if r.cfg != nil && time.Since(r.cfgAt) < cacheTTL { return r.cfg } st, err := r.st.GetSettings() if err != nil { if r.cfg == nil { return r.env } return r.cfg // 沿用上次缓存,别让一次查询失败打断请求 } r.cfg = r.env.Overlay(st) r.cfgAt = time.Now() return r.cfg } // Invalidate 丢弃缓存:后台保存设置后调用,改完立即生效。 func (r *Resolver) Invalidate() { if r == nil { return } r.mu.Lock() r.cfg, r.cfgAt = nil, time.Time{} r.mu.Unlock() } // Blobs 返回当前有效配置对应的存储后端,配置变了自动重建。 // 构造失败(R2 凭据不对等)返回 nil,调用方按「无存储」处理。 func (r *Resolver) Blobs() storage.BlobStore { if r == nil { return nil } c := r.Config() kv := c.StorageDriver + "|" + c.S3Endpoint + "|" + c.R2Bucket + "|" + c.R2AccessKey + "|" + c.R2SecretKey r.mu.Lock() defer r.mu.Unlock() if r.blobs != nil && kv == r.blobsKV { return r.blobs } var b storage.BlobStore switch c.StorageDriver { case "r2": s, err := storage.NewR2(c.S3Endpoint, c.R2Bucket, c.R2AccessKey, c.R2SecretKey) if err != nil { return r.blobs // 重建失败沿用旧后端,至少不把上传/读取打断 } b = s default: b = storage.NewLocal(c.DataDir + "/uploads") } r.blobs, r.blobsKV = b, kv return b } // 静态断言:*store.Store 满足 SettingsSource(GetSettings 在 store 包里)。 var _ SettingsSource = (*store.Store)(nil)