Files
backend/internal/config/config.go
lafay 5b83f98fb8
All checks were successful
Build Backend / build (push) Successful in 2m21s
Build Backend / build-docker (push) Successful in 1m16s
refactor(config): enable environment variable binding for database replica settings
Viper's AutomaticEnv does not bind nested keys when sections are commented out
in YAML. Add explicit BindEnv calls for APP_DATABASE_REPLICA_* variables so
they work when the replica section is not defined in config.yaml. Also bind
optional connection pool parameters (MAX_IDLE_CONNS, MAX_OPEN_CONNS).
2026-06-18 02:01:57 +08:00

325 lines
16 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.
package config
import (
"fmt"
"strings"
"time"
"github.com/spf13/viper"
)
// Config 主配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Cache CacheConfig `mapstructure:"cache"`
S3 S3Config `mapstructure:"s3"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
Upload UploadConfig `mapstructure:"upload"`
FileCleanup FileCleanupConfig `mapstructure:"file_cleanup"`
OpenAI OpenAIConfig `mapstructure:"openai"`
TencentTMS TencentTMSConfig `mapstructure:"tencent_tms"`
Email EmailConfig `mapstructure:"email"`
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
GRPC GRPCConfig `mapstructure:"grpc"`
Casbin CasbinConfig `mapstructure:"casbin"`
Encryption EncryptionConfig `mapstructure:"encryption"`
HotRank HotRankConfig `mapstructure:"hot_rank"`
LiveKit LiveKitConfig `mapstructure:"livekit"`
Report ReportConfig `mapstructure:"report"`
Sensitive SensitiveConfig `mapstructure:"sensitive"`
JPush JPushConfig `mapstructure:"jpush"`
SetupSecret string `mapstructure:"setup_secret"`
WebSocket WSConfig `mapstructure:"websocket"`
Runner RunnerConfig `mapstructure:"runner"`
SeqBuffer SeqBufferConfig `mapstructure:"seq_buffer"`
VersionLog VersionLogConfig `mapstructure:"version_log"`
PushWorker PushWorkerConfig `mapstructure:"push_worker"`
}
// Load 加载配置文件
func Load(configPath string) (*Config, error) {
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
// 启用环境变量支持
viper.SetEnvPrefix("APP")
viper.AutomaticEnv()
// 允许环境变量使用下划线或连字符
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
// Set default values
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.mode", "debug")
viper.SetDefault("server.host", "0.0.0.0")
viper.SetDefault("database.type", "sqlite")
viper.SetDefault("database.sqlite.path", "./data/with_you.db")
viper.SetDefault("database.max_idle_conns", 10)
viper.SetDefault("database.max_open_conns", 100)
viper.SetDefault("database.log_level", "warn")
viper.SetDefault("database.slow_threshold_ms", 200)
viper.SetDefault("database.ignore_record_not_found", true)
viper.SetDefault("database.parameterized_queries", true)
viper.SetDefault("database.replica_policy", "random")
// 读写分离:单副本字段显式 BindEnv。
// AutomaticEnv 对 YAML 中未出现的嵌套 keyreplica 段在 config.yaml 中默认被注释)
// 不会反向推导,必须逐字段 BindEnv 才能让 APP_DATABASE_REPLICA_HOST 等环境变量生效。
// 与 jpush.channel.* 处理方式一致(见下方 BindEnv 块)。
viper.BindEnv("database.replica.host", "APP_DATABASE_REPLICA_HOST")
viper.BindEnv("database.replica.port", "APP_DATABASE_REPLICA_PORT")
viper.BindEnv("database.replica.user", "APP_DATABASE_REPLICA_USER")
viper.BindEnv("database.replica.password", "APP_DATABASE_REPLICA_PASSWORD")
viper.BindEnv("database.replica.dbname", "APP_DATABASE_REPLICA_DBNAME")
viper.BindEnv("database.replica.sslmode", "APP_DATABASE_REPLICA_SSLMODE")
// 副本连接池参数(可选,留空回退到主库连接池配置,见 database.go
viper.BindEnv("database.replica_max_idle_conns", "APP_DATABASE_REPLICA_MAX_IDLE_CONNS")
viper.BindEnv("database.replica_max_open_conns", "APP_DATABASE_REPLICA_MAX_OPEN_CONNS")
viper.SetDefault("redis.type", "miniredis")
viper.SetDefault("redis.redis.host", "localhost")
viper.SetDefault("redis.redis.port", 6379)
viper.SetDefault("redis.redis.password", "")
viper.SetDefault("redis.redis.db", 0)
viper.SetDefault("redis.miniredis.host", "localhost")
viper.SetDefault("redis.miniredis.port", 6379)
viper.SetDefault("redis.pool_size", 10)
viper.SetDefault("cache.enabled", true)
viper.SetDefault("cache.key_prefix", "")
viper.SetDefault("cache.default_ttl", 30)
viper.SetDefault("cache.null_ttl", 5)
viper.SetDefault("cache.jitter_ratio", 0.1)
viper.SetDefault("cache.disable_flushdb", true)
viper.SetDefault("cache.modules.post_list_ttl", 30)
viper.SetDefault("cache.modules.conversation_ttl", 60)
viper.SetDefault("cache.modules.unread_count_ttl", 30)
viper.SetDefault("cache.modules.group_members_ttl", 120)
viper.SetDefault("cache.local.enabled", true)
viper.SetDefault("cache.local.size", 10000)
viper.SetDefault("cache.local.buckets", 0)
viper.SetDefault("cache.local.default_ttl", 300)
viper.SetDefault("jwt.secret", "your-jwt-secret-key-change-in-production")
viper.SetDefault("jwt.access_token_expire", 86400)
viper.SetDefault("jwt.refresh_token_expire", 604800)
viper.SetDefault("log.level", "info")
viper.SetDefault("log.encoding", "json")
viper.SetDefault("log.output_paths", []string{"stdout", "./logs/app.log"})
viper.SetDefault("rate_limit.enabled", true)
viper.SetDefault("rate_limit.requests_per_minute", 60)
viper.SetDefault("upload.max_file_size", 10485760)
viper.SetDefault("upload.allowed_types", []string{"image/jpeg", "image/png", "image/gif", "image/webp"})
// 文件过期清理默认值
viper.SetDefault("file_cleanup.enabled", true)
viper.SetDefault("file_cleanup.retention_days", 7)
viper.SetDefault("file_cleanup.interval_minutes", 360)
viper.SetDefault("file_cleanup.batch_size", 100)
viper.SetDefault("s3.endpoint", "")
viper.SetDefault("s3.access_key", "")
viper.SetDefault("s3.secret_key", "")
viper.SetDefault("s3.bucket", "")
viper.SetDefault("s3.use_ssl", true)
viper.SetDefault("s3.region", "us-east-1")
viper.SetDefault("s3.domain", "")
viper.SetDefault("sensitive.enabled", true)
viper.SetDefault("sensitive.replace_str", "***")
viper.SetDefault("sensitive.min_match_len", 2)
viper.SetDefault("sensitive.load_from_db", true)
viper.SetDefault("sensitive.load_from_redis", false)
viper.SetDefault("sensitive.redis_key_prefix", "sensitive_words")
viper.SetDefault("audit.enabled", false)
viper.SetDefault("audit.provider", "local")
viper.SetDefault("openai.enabled", true)
viper.SetDefault("openai.base_url", "https://api.littlelan.cn/")
viper.SetDefault("openai.api_key", "")
viper.SetDefault("openai.moderation_model", "qwen3.5-122b")
viper.SetDefault("openai.moderation_max_images_per_request", 1)
viper.SetDefault("openai.request_timeout", 30)
viper.SetDefault("openai.strict_moderation", false)
viper.SetDefault("tencent_tms.enabled", false)
viper.SetDefault("tencent_tms.secret_id", "")
viper.SetDefault("tencent_tms.secret_key", "")
viper.SetDefault("tencent_tms.region", "ap-guangzhou")
viper.SetDefault("tencent_tms.biz_type", "")
viper.SetDefault("tencent_tms.timeout", 30)
viper.SetDefault("email.enabled", false)
viper.SetDefault("email.host", "")
viper.SetDefault("email.port", 587)
viper.SetDefault("email.username", "")
viper.SetDefault("email.password", "")
viper.SetDefault("email.from_address", "")
viper.SetDefault("email.from_name", "WithYou")
viper.SetDefault("email.use_tls", true)
viper.SetDefault("email.insecure_skip_verify", false)
viper.SetDefault("email.timeout", 15)
// ConversationCache 默认值
viper.SetDefault("conversation_cache.detail_ttl", "5m")
viper.SetDefault("conversation_cache.list_ttl", "60s")
viper.SetDefault("conversation_cache.participant_ttl", "5m")
viper.SetDefault("conversation_cache.unread_ttl", "30s")
viper.SetDefault("conversation_cache.message_detail_ttl", "30m")
viper.SetDefault("conversation_cache.message_list_ttl", "5m")
viper.SetDefault("conversation_cache.message_index_ttl", "30m")
viper.SetDefault("conversation_cache.message_count_ttl", "30m")
viper.SetDefault("conversation_cache.batch_interval", "5s")
viper.SetDefault("conversation_cache.batch_threshold", 100)
viper.SetDefault("conversation_cache.batch_max_size", 500)
viper.SetDefault("conversation_cache.buffer_max_size", 10000)
// GRPC 默认值
viper.SetDefault("grpc.enabled", true)
viper.SetDefault("grpc.port", 50051)
viper.SetDefault("grpc.tls_enabled", false)
viper.SetDefault("grpc.tls_cert_file", "")
viper.SetDefault("grpc.tls_key_file", "")
viper.SetDefault("casbin.model_path", "./configs/casbin/model.conf")
viper.SetDefault("casbin.auto_save", true)
viper.SetDefault("casbin.auto_load", true)
viper.SetDefault("casbin.enable_cache", true)
viper.SetDefault("casbin.cache_ttl", 300)
viper.SetDefault("casbin.skip_routes", []string{"/health", "/api/v1/auth/login", "/api/v1/auth/register"})
// Encryption 默认值
viper.SetDefault("encryption.enabled", false)
viper.SetDefault("encryption.key", "")
viper.SetDefault("encryption.key_version", 1)
viper.SetDefault("hot_rank.enabled", true)
viper.SetDefault("hot_rank.refresh_interval_seconds", 300)
viper.SetDefault("hot_rank.top_n", 100)
viper.SetDefault("hot_rank.recent_window_hours", 168)
viper.SetDefault("hot_rank.recent_fetch_limit", 500)
viper.SetDefault("hot_rank.candidate_cap", 800)
// Report 默认值
viper.SetDefault("report.auto_hide_threshold", 3)
viper.SetDefault("jpush.enabled", false)
viper.SetDefault("jpush.app_key", "")
viper.SetDefault("jpush.master_secret", "")
viper.SetDefault("jpush.production", false)
// JPush 厂商通道 channel_id 默认值
// 各厂商 channel_id 通过环境变量配置(见下方 BindEnv
viper.SetDefault("jpush.channel.system", "")
viper.SetDefault("jpush.channel.chat", "")
viper.SetDefault("jpush.channel.vendor.xiaomi.system", "")
viper.SetDefault("jpush.channel.vendor.xiaomi.chat", "")
viper.SetDefault("jpush.channel.vendor.huawei.system", "")
viper.SetDefault("jpush.channel.vendor.huawei.chat", "")
viper.SetDefault("jpush.channel.vendor.oppo.system", "")
viper.SetDefault("jpush.channel.vendor.oppo.chat", "")
viper.SetDefault("jpush.channel.vendor.vivo.system", "")
viper.SetDefault("jpush.channel.vendor.vivo.chat", "")
viper.SetDefault("jpush.channel.vendor.meizu.system", "")
viper.SetDefault("jpush.channel.vendor.meizu.chat", "")
viper.SetDefault("jpush.channel.vendor.honor.system", "")
viper.SetDefault("jpush.channel.vendor.honor.chat", "")
viper.SetDefault("jpush.channel.vendor.fcm.system", "")
viper.SetDefault("jpush.channel.vendor.fcm.chat", "")
// 小米/OPPO 模板 id 默认值
viper.SetDefault("jpush.channel.vendor.xiaomi.mi_system_template_id", "")
viper.SetDefault("jpush.channel.vendor.xiaomi.mi_chat_template_id", "")
viper.SetDefault("jpush.channel.vendor.oppo.oppo_system_private_template_id", "")
viper.SetDefault("jpush.channel.vendor.oppo.oppo_chat_private_template_id", "")
// iOS 通知分组 thread-id 默认值
viper.SetDefault("jpush.channel.ios.chat_thread_id", "")
viper.SetDefault("jpush.channel.ios.system_thread_id", "")
// JPush 厂商 channel_id 环境变量显式绑定
// AutomaticEnv 对深层嵌套 key 不可靠,故逐个 BindEnv
// 命名规则APP_JPUSH_CHANNEL_{VENDOR}_{SYSTEM|CHAT}
viper.BindEnv("jpush.channel.system", "APP_JPUSH_CHANNEL_SYSTEM")
viper.BindEnv("jpush.channel.chat", "APP_JPUSH_CHANNEL_CHAT")
viper.BindEnv("jpush.channel.vendor.xiaomi.system", "APP_JPUSH_CHANNEL_XIAOMI_SYSTEM")
viper.BindEnv("jpush.channel.vendor.xiaomi.chat", "APP_JPUSH_CHANNEL_XIAOMI_CHAT")
viper.BindEnv("jpush.channel.vendor.huawei.system", "APP_JPUSH_CHANNEL_HUAWEI_SYSTEM")
viper.BindEnv("jpush.channel.vendor.huawei.chat", "APP_JPUSH_CHANNEL_HUAWEI_CHAT")
viper.BindEnv("jpush.channel.vendor.oppo.system", "APP_JPUSH_CHANNEL_OPPO_SYSTEM")
viper.BindEnv("jpush.channel.vendor.oppo.chat", "APP_JPUSH_CHANNEL_OPPO_CHAT")
viper.BindEnv("jpush.channel.vendor.vivo.system", "APP_JPUSH_CHANNEL_VIVO_SYSTEM")
viper.BindEnv("jpush.channel.vendor.vivo.chat", "APP_JPUSH_CHANNEL_VIVO_CHAT")
viper.BindEnv("jpush.channel.vendor.meizu.system", "APP_JPUSH_CHANNEL_MEIZU_SYSTEM")
viper.BindEnv("jpush.channel.vendor.meizu.chat", "APP_JPUSH_CHANNEL_MEIZU_CHAT")
viper.BindEnv("jpush.channel.vendor.honor.system", "APP_JPUSH_CHANNEL_HONOR_SYSTEM")
viper.BindEnv("jpush.channel.vendor.honor.chat", "APP_JPUSH_CHANNEL_HONOR_CHAT")
viper.BindEnv("jpush.channel.vendor.fcm.system", "APP_JPUSH_CHANNEL_FCM_SYSTEM")
viper.BindEnv("jpush.channel.vendor.fcm.chat", "APP_JPUSH_CHANNEL_FCM_CHAT")
// 小米/OPPO 模板 id 环境变量绑定
viper.BindEnv("jpush.channel.vendor.xiaomi.mi_system_template_id", "APP_JPUSH_CHANNEL_XIAOMI_MI_SYSTEM_TEMPLATE_ID")
viper.BindEnv("jpush.channel.vendor.xiaomi.mi_chat_template_id", "APP_JPUSH_CHANNEL_XIAOMI_MI_CHAT_TEMPLATE_ID")
viper.BindEnv("jpush.channel.vendor.oppo.oppo_system_private_template_id", "APP_JPUSH_CHANNEL_OPPO_SYSTEM_PRIVATE_TEMPLATE_ID")
viper.BindEnv("jpush.channel.vendor.oppo.oppo_chat_private_template_id", "APP_JPUSH_CHANNEL_OPPO_CHAT_PRIVATE_TEMPLATE_ID")
// iOS thread-id 环境变量绑定
viper.BindEnv("jpush.channel.ios.chat_thread_id", "APP_JPUSH_CHANNEL_IOS_CHAT_THREAD_ID")
viper.BindEnv("jpush.channel.ios.system_thread_id", "APP_JPUSH_CHANNEL_IOS_SYSTEM_THREAD_ID")
viper.SetDefault("setup_secret", "")
// WebSocket 默认值
viper.SetDefault("websocket.mode", "standalone")
viper.SetDefault("websocket.cluster.instance_id", "")
viper.SetDefault("websocket.cluster.msg_channel", "ws:msg")
viper.SetDefault("websocket.cluster.online_ttl", 60)
viper.SetDefault("websocket.cluster.heartbeat_interval", 20)
// Runner 集群默认值mode 和 instance_id 空则跟随 websocket
viper.SetDefault("runner.mode", "")
viper.SetDefault("runner.cluster.instance_id", "")
viper.SetDefault("runner.cluster.task_channel", "runner:task")
viper.SetDefault("runner.cluster.result_channel", "runner:result")
viper.SetDefault("runner.cluster.registry_prefix", "runner:reg:")
viper.SetDefault("runner.cluster.cap_prefix", "runner:cap:")
viper.SetDefault("runner.cluster.registry_ttl", 90)
viper.SetDefault("runner.cluster.heartbeat_interval", 30)
// SeqBuffer 默认值
viper.SetDefault("seq_buffer.enabled", false)
viper.SetDefault("seq_buffer.private_buffer_size", 50)
viper.SetDefault("seq_buffer.group_buffer_size", 200)
viper.SetDefault("seq_buffer.lock_timeout_ms", 5000)
viper.SetDefault("seq_buffer.max_retries", 3)
// VersionLog 默认值
viper.SetDefault("version_log.enabled", false)
viper.SetDefault("version_log.sync_limit", 100)
viper.SetDefault("version_log.max_sync_gap", 1000)
// PushWorker 默认值
viper.SetDefault("push_worker.enabled", false)
viper.SetDefault("push_worker.stream", "msg_push")
viper.SetDefault("push_worker.group", "push_worker")
viper.SetDefault("push_worker.batch_size", 50)
viper.SetDefault("push_worker.poll_timeout_ms", 2000)
viper.SetDefault("push_worker.max_retries", 3)
viper.SetDefault("push_worker.max_stream_len", 100000)
viper.SetDefault("push_worker.idle_timeout_ms", 30000)
// LiveKit 默认值
viper.SetDefault("livekit.enabled", false)
viper.SetDefault("livekit.url", "http://localhost:7880")
viper.SetDefault("livekit.api_key", "devkey")
viper.SetDefault("livekit.api_secret", "")
viper.SetDefault("livekit.token_ttl", 600)
viper.SetDefault("livekit.webhook_secret", "")
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Runner mode 和 instance_id 空则跟随 WebSocket 配置
cfg.Runner.ApplyWSDefaults(cfg.WebSocket)
// Convert seconds to duration
cfg.JWT.AccessTokenExpire = time.Duration(viper.GetInt("jwt.access_token_expire")) * time.Second
cfg.JWT.RefreshTokenExpire = time.Duration(viper.GetInt("jwt.refresh_token_expire")) * time.Second
// Viper AutomaticEnv 已自动处理所有 APP_ 前缀的环境变量
// 安全检查生产模式必须设置JWT密钥
if cfg.Server.Mode != "debug" {
if cfg.JWT.Secret == "" || cfg.JWT.Secret == "your-jwt-secret-key-change-in-production" {
return nil, fmt.Errorf("SECURITY ERROR: JWT secret must be set via APP_JWT_SECRET environment variable in production mode")
}
if len(cfg.JWT.Secret) < 32 {
return nil, fmt.Errorf("SECURITY ERROR: JWT secret must be at least 32 characters long in production mode")
}
}
return &cfg, nil
}