Files
fanqie-go/internal/config/config.go
T
chenxin 1eccebedf9 ✨ 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 模板
- 新增 openapi.yaml(OpenAPI 3.1):覆盖全部 24 个路径,含参数、响应结构与鉴权方案,README 增加指引
- 删除死代码 InitDevicePoolTable;新增 rebind_test.go 单测
- 更新 README 与 config.yaml 配置示例,go.mod 新增 go-redis/v9 与 pgx/v5 依赖
2026-09-29 20:33:37 +08:00

275 lines
8.2 KiB
Go

package config
import (
"fmt"
"log"
"sync"
"sync/atomic"
"github.com/spf13/viper"
)
// Config 应用配置结构
type Config struct {
Server ServerConfig `mapstructure:"server"`
Fanqie FanqieConfig `mapstructure:"fanqie"`
Cache CacheConfig `mapstructure:"cache"`
DeviceKeyPool DeviceKeyPoolConfig `mapstructure:"device_key_pool"`
}
// ServerConfig 服务器配置
type ServerConfig struct {
Port string `mapstructure:"port"`
Host string `mapstructure:"host"`
AccessKey string `mapstructure:"access_key"`
}
// FanqieConfig 番茄小说配置
type FanqieConfig struct {
Algorithm string `mapstructure:"algorithm"`
Signature string `mapstructure:"signature"` // 签名方法: sign2 / sign4
Content ContentConfig `mapstructure:"content"`
Manga MangaConfig `mapstructure:"manga"`
}
// ContentConfig 内容处理配置
type ContentConfig struct {
RemoveImages bool `mapstructure:"remove_images"`
RemoveImageDesc bool `mapstructure:"remove_image_desc"`
RemoveBlankLines bool `mapstructure:"remove_blank_lines"`
RemoveHTMLTags bool `mapstructure:"remove_html_tags"`
RemoveChapterTitle bool `mapstructure:"remove_chapter_title"`
AddParagraphIndent bool `mapstructure:"add_paragraph_indent"`
DoubleNewline bool `mapstructure:"double_newline"` // 段落间使用双换行
EnableReview bool `mapstructure:"enable_review"` // 是否启用段评功能
}
// MangaConfig 漫画处理配置
type MangaConfig struct {
OutputMode string `mapstructure:"output_mode"` // 输出模式: data_uri / base64 / url
}
// CacheConfig 缓存配置
type CacheConfig struct {
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 设备密钥池配置
type DeviceKeyPoolConfig struct {
Enabled bool `mapstructure:"enabled"` // 是否启用设备密钥池
InitialSize int `mapstructure:"initial_size"` // 初始化设备数量
MinSize int `mapstructure:"min_size"` // 最小设备数量
MaxSize int `mapstructure:"max_size"` // 最大设备数量
}
// ConfigManager 配置管理器
type ConfigManager struct {
mu sync.RWMutex
configPath string
callbacks []func(*Config)
}
var (
// appConfig 全局配置实例(原子指针,热更新替换时读取侧无数据竞争)
appConfig atomic.Pointer[Config]
manager *ConfigManager
)
// GetAppConfig 获取当前配置,未加载时返回 nil
func GetAppConfig() *Config {
return appConfig.Load()
}
// NewConfigManager 创建配置管理器
func NewConfigManager() *ConfigManager {
return &ConfigManager{
callbacks: make([]func(*Config), 0),
}
}
// LoadConfig 加载配置文件
func LoadConfig(configPath string) error {
// 初始化管理器
if manager == nil {
manager = NewConfigManager()
manager.configPath = configPath
}
// 加载 .env(若存在),敏感配置可由环境变量覆盖
loadDotenv("./.env")
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
// 设置默认值
setDefaults()
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
newConfig := &Config{}
if err := viper.Unmarshal(newConfig); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
applyEnvOverrides(newConfig)
// 更新配置
appConfig.Store(newConfig)
return nil
}
// setDefaults 设置默认值
func setDefaults() {
// 服务器配置
viper.SetDefault("server.port", "40180")
viper.SetDefault("server.host", "0.0.0.0")
// 番茄小说配置
viper.SetDefault("fanqie.algorithm", "gorgon")
viper.SetDefault("fanqie.signature", "sign2")
// 内容处理配置
viper.SetDefault("fanqie.content.remove_images", false)
viper.SetDefault("fanqie.content.remove_image_desc", false)
viper.SetDefault("fanqie.content.remove_blank_lines", true)
viper.SetDefault("fanqie.content.remove_html_tags", false)
viper.SetDefault("fanqie.content.remove_chapter_title", false)
viper.SetDefault("fanqie.content.add_paragraph_indent", false)
viper.SetDefault("fanqie.content.double_newline", true)
viper.SetDefault("fanqie.content.enable_review", true)
// 漫画处理配置
viper.SetDefault("fanqie.manga.output_mode", "url")
// 缓存配置
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)
viper.SetDefault("device_key_pool.min_size", 2)
viper.SetDefault("device_key_pool.max_size", 10)
}
// GetConfig 获取配置实例
func GetConfig() *Config {
if cfg := appConfig.Load(); cfg != nil {
return cfg
}
log.Fatal("配置未初始化,请先调用 LoadConfig")
return nil
}
// ReloadConfig 重新加载配置文件
func ReloadConfig() error {
if manager == nil {
return fmt.Errorf("配置管理器未初始化")
}
manager.mu.RLock()
configPath := manager.configPath
manager.mu.RUnlock()
if configPath == "" {
return fmt.Errorf("配置文件路径为空")
}
// 重新读取配置文件
viper.SetConfigFile(configPath)
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("重新读取配置文件失败: %w", err)
}
newConfig := &Config{}
if err := viper.Unmarshal(newConfig); err != nil {
return fmt.Errorf("重新解析配置文件失败: %w", err)
}
applyEnvOverrides(newConfig)
// 更新配置
manager.mu.Lock()
var oldAlgorithm string
if old := appConfig.Load(); old != nil {
oldAlgorithm = old.Fanqie.Algorithm
}
appConfig.Store(newConfig)
callbacks := make([]func(*Config), len(manager.callbacks))
copy(callbacks, manager.callbacks)
manager.mu.Unlock()
// 通知配置更新
for _, callback := range callbacks {
callback(newConfig)
}
log.Printf("配置重新加载成功: %s", configPath)
if oldAlgorithm != newConfig.Fanqie.Algorithm {
log.Printf("算法已切换: %s -> %s", oldAlgorithm, newConfig.Fanqie.Algorithm)
}
return nil
}
// RegisterConfigCallback 注册配置更新回调
func RegisterConfigCallback(callback func(*Config)) {
if manager == nil {
log.Printf("警告: 配置管理器未初始化,无法注册回调")
return
}
manager.mu.Lock()
defer manager.mu.Unlock()
manager.callbacks = append(manager.callbacks, callback)
}
// GetConfigManager 获取配置管理器实例
func GetConfigManager() *ConfigManager {
return manager
}