Files
ONE/backend/internal/config/resolver.go
T
Sakurasan e68400b389 系统设置迁入后台:站点地址 / OAuth 凭据 / R2 存储 / 管理员账号,保存即生效无需重启
- 生效规则统一为「后台填了用后台的,没填回落环境变量」,老部署不改 env 照常跑
- config.Resolver:短缓存解析有效配置,存储后端按配置签名热重建;后台保存主动失效
- 秘密项(client secret / bot token / R2 密钥 / 管理员密码)接口永不回显明文,
  只报「是否已配置、来自哪里」;留空保存 = 保持现值
- 管理员密码 bcrypt 入库,DB 哈希优先、显式设置的 env 密码作解锁后路;
  后台改过密码后 admin/admin 开发模式立即失效
- 设置页新增「登录与存储」标签,基础信息加站点地址;秘密项带来源提示
- 监听地址 / 数据库 / 目录 / ONE_SECRET / Passkey 仍留环境变量(启动期依赖)
2026-10-01 12:14:10 +08:00

144 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 系统设置的运行时解析层。
//
// 迁移自环境变量的设置(站点地址、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)