✨ feat(cache): 新增 Redis 缓存与 PostgreSQL 后端支持

- 新增 cache.redis 配置(独立 host/port/password/db 字段),启用后正文缓存与音频代理缓存走 Redis,键前缀 fq:cache:/fq:audio:,TTL 自动过期
- 新增 cache.backend: sqlite|postgres 切换,PG 连接用 net/url 构造 DSN,密码特殊字符自动转义;SQLite 顺手启用 WAL 与 busy_timeout
- SQL 方言收敛:q() 将 ? 占位符转换为 $n、INSERT OR IGNORE 与查询再插入统一为 ON CONFLICT、LastInsertId 改为 RETURNING id、建表 DDL 双版本分派
- PG 下 VACUUM 走 autovacuum 说明、状态页大小改查 pg_database_size
- 修复 PG 下设备保存失败:is_valid 列为 INTEGER,写入点补 boolToInt 转换
- 新增 .env 支持:敏感配置(密码/主机/访问密钥)由环境变量覆盖 config.yaml,.env 进 gitignore,仓库附 .env.example 模板
- 删除死代码 InitDevicePoolTable;新增 rebind_test.go 单测
- 更新 README 与 config.yaml 配置示例,go.mod 新增 go-redis/v9 与 pgx/v5 依赖
This commit is contained in:
chenxin committed 2026-09-29 20:22:52 +08:00
1 parent d710d382ee
commit cf16c37f32
14 files changed
+671 -210

No files matched your search

+47 -4
View File
@@ -51,10 +51,32 @@ type MangaConfig struct {
// CacheConfig 缓存配置
type CacheConfig struct {
Enabled bool `mapstructure:"enabled"`
SQLitePath string `mapstructure:"sqlite_path"`
CleanupEnabled bool `mapstructure:"cleanup_enabled"` // 是否启用缓存清理
CleanupSeconds int `mapstructure:"cleanup_seconds"` // 缓存清理时间(秒)
Enabled bool `mapstructure:"enabled"`
Backend string `mapstructure:"backend"` // 存储后端: sqlite | postgres
SQLitePath string `mapstructure:"sqlite_path"`
Postgres PostgresConfig `mapstructure:"postgres"`
Redis RedisConfig `mapstructure:"redis"`
CleanupEnabled bool `mapstructure:"cleanup_enabled"` // 是否启用缓存清理
CleanupSeconds int `mapstructure:"cleanup_seconds"` // 缓存清理时间(秒)
}
// PostgresConfig PostgreSQL 连接配置
type PostgresConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
SSLMode string `mapstructure:"sslmode"` // disable | prefer | require
}
// RedisConfig Redis 缓存配置(启用后正文缓存与音频代理缓存走 Redis,统计数据与设备池仍在 SQL 库)
type RedisConfig struct {
Enabled bool `mapstructure:"enabled"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// DeviceKeyPoolConfig 设备密钥池配置
@@ -98,6 +120,9 @@ func LoadConfig(configPath string) error {
manager.configPath = configPath
}
// 加载 .env(若存在),敏感配置可由环境变量覆盖
loadDotenv("./.env")
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
@@ -112,6 +137,7 @@ func LoadConfig(configPath string) error {
if err := viper.Unmarshal(newConfig); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
applyEnvOverrides(newConfig)
// 更新配置
appConfig.Store(newConfig)
@@ -144,10 +170,26 @@ func setDefaults() {
// 缓存配置
viper.SetDefault("cache.enabled", true)
viper.SetDefault("cache.backend", "sqlite")
viper.SetDefault("cache.sqlite_path", "./cache.db")
viper.SetDefault("cache.cleanup_enabled", false)
viper.SetDefault("cache.cleanup_seconds", 86400)
// PostgreSQL 配置
viper.SetDefault("cache.postgres.host", "127.0.0.1")
viper.SetDefault("cache.postgres.port", 5432)
viper.SetDefault("cache.postgres.user", "postgres")
viper.SetDefault("cache.postgres.password", "")
viper.SetDefault("cache.postgres.database", "fanqie")
viper.SetDefault("cache.postgres.sslmode", "disable")
// Redis 配置
viper.SetDefault("cache.redis.enabled", false)
viper.SetDefault("cache.redis.host", "127.0.0.1")
viper.SetDefault("cache.redis.port", 6379)
viper.SetDefault("cache.redis.password", "")
viper.SetDefault("cache.redis.db", 0)
// 设备密钥池配置
viper.SetDefault("device_key_pool.enabled", true)
viper.SetDefault("device_key_pool.initial_size", 3)
@@ -188,6 +230,7 @@ func ReloadConfig() error {
if err := viper.Unmarshal(newConfig); err != nil {
return fmt.Errorf("重新解析配置文件失败: %w", err)
}
applyEnvOverrides(newConfig)
// 更新配置
manager.mu.Lock()
+49
View File
@@ -0,0 +1,49 @@
package config
import (
"os"
"strings"
)
// loadDotenv 加载 .env 文件到环境变量(文件不存在则跳过;不覆盖已有环境变量)
func loadDotenv(path string) {
data, err := os.ReadFile(path)
if err != nil {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
k = strings.TrimSpace(k)
v = strings.Trim(strings.TrimSpace(v), `"'`)
if os.Getenv(k) == "" {
os.Setenv(k, v)
}
}
}
// applyEnvOverrides 用环境变量覆盖敏感配置(配合 .env 使用,真实密码不进 config.yaml)
func applyEnvOverrides(c *Config) {
for _, o := range []struct {
env string
dst *string
}{
{"FQ_SERVER_ACCESS_KEY", &c.Server.AccessKey},
{"FQ_PG_HOST", &c.Cache.Postgres.Host},
{"FQ_PG_USER", &c.Cache.Postgres.User},
{"FQ_PG_PASSWORD", &c.Cache.Postgres.Password},
{"FQ_PG_DATABASE", &c.Cache.Postgres.Database},
{"FQ_REDIS_HOST", &c.Cache.Redis.Host},
{"FQ_REDIS_PASSWORD", &c.Cache.Redis.Password},
} {
if v := os.Getenv(o.env); v != "" {
*o.dst = v
}
}
}