package config import ( "cmp" "fmt" "os" "strconv" "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"` Gorse GorseConfig `mapstructure:"gorse"` OpenAI OpenAIConfig `mapstructure:"openai"` Email EmailConfig `mapstructure:"email"` ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"` GRPC GRPCConfig `mapstructure:"grpc"` Casbin CasbinConfig `mapstructure:"casbin"` Encryption EncryptionConfig `mapstructure:"encryption"` } // 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/carrot_bbs.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("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("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("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("audit.enabled", false) viper.SetDefault("audit.provider", "local") viper.SetDefault("gorse.enabled", false) viper.SetDefault("gorse.address", "http://localhost:8087") viper.SetDefault("gorse.api_key", "") viper.SetDefault("gorse.dashboard", "http://localhost:8088") viper.SetDefault("gorse.import_password", "") viper.SetDefault("gorse.embedding_api_key", "") viper.SetDefault("gorse.embedding_url", "https://api.littlelan.cn/v1/embeddings") viper.SetDefault("gorse.embedding_model", "BAAI/bge-m3") 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("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", "Carrot BBS") 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", "") // Casbin 默认值 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) 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) } // 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 // 环境变量覆盖(显式处理敏感配置) cfg.JWT.Secret = getEnvOrDefault("APP_JWT_SECRET", cfg.JWT.Secret) cfg.Database.SQLite.Path = getEnvOrDefault("APP_DATABASE_SQLITE_PATH", cfg.Database.SQLite.Path) cfg.Database.Postgres.Host = getEnvOrDefault("APP_DATABASE_POSTGRES_HOST", cfg.Database.Postgres.Host) cfg.Database.Postgres.Port, _ = strconv.Atoi(getEnvOrDefault("APP_DATABASE_POSTGRES_PORT", fmt.Sprintf("%d", cfg.Database.Postgres.Port))) cfg.Database.Postgres.User = getEnvOrDefault("APP_DATABASE_POSTGRES_USER", cfg.Database.Postgres.User) cfg.Database.Postgres.Password = getEnvOrDefault("APP_DATABASE_POSTGRES_PASSWORD", cfg.Database.Postgres.Password) cfg.Database.Postgres.DBName = getEnvOrDefault("APP_DATABASE_POSTGRES_DBNAME", cfg.Database.Postgres.DBName) cfg.Database.LogLevel = getEnvOrDefault("APP_DATABASE_LOG_LEVEL", cfg.Database.LogLevel) cfg.Database.SlowThresholdMs, _ = strconv.Atoi(getEnvOrDefault("APP_DATABASE_SLOW_THRESHOLD_MS", fmt.Sprintf("%d", cfg.Database.SlowThresholdMs))) cfg.Database.IgnoreRecordNotFound, _ = strconv.ParseBool(getEnvOrDefault("APP_DATABASE_IGNORE_RECORD_NOT_FOUND", fmt.Sprintf("%t", cfg.Database.IgnoreRecordNotFound))) cfg.Database.ParameterizedQueries, _ = strconv.ParseBool(getEnvOrDefault("APP_DATABASE_PARAMETERIZED_QUERIES", fmt.Sprintf("%t", cfg.Database.ParameterizedQueries))) cfg.Redis.Redis.Host = getEnvOrDefault("APP_REDIS_REDIS_HOST", cfg.Redis.Redis.Host) cfg.Redis.Redis.Port, _ = strconv.Atoi(getEnvOrDefault("APP_REDIS_REDIS_PORT", fmt.Sprintf("%d", cfg.Redis.Redis.Port))) cfg.Redis.Redis.Password = getEnvOrDefault("APP_REDIS_REDIS_PASSWORD", cfg.Redis.Redis.Password) cfg.Redis.Redis.DB, _ = strconv.Atoi(getEnvOrDefault("APP_REDIS_REDIS_DB", fmt.Sprintf("%d", cfg.Redis.Redis.DB))) cfg.Redis.Miniredis.Host = getEnvOrDefault("APP_REDIS_MINIREDIS_HOST", cfg.Redis.Miniredis.Host) cfg.Redis.Miniredis.Port, _ = strconv.Atoi(getEnvOrDefault("APP_REDIS_MINIREDIS_PORT", fmt.Sprintf("%d", cfg.Redis.Miniredis.Port))) cfg.Redis.Type = getEnvOrDefault("APP_REDIS_TYPE", cfg.Redis.Type) cfg.Cache.KeyPrefix = getEnvOrDefault("APP_CACHE_KEY_PREFIX", cfg.Cache.KeyPrefix) cfg.Cache.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_CACHE_ENABLED", fmt.Sprintf("%t", cfg.Cache.Enabled))) cfg.Cache.DisableFlushDB, _ = strconv.ParseBool(getEnvOrDefault("APP_CACHE_DISABLE_FLUSHDB", fmt.Sprintf("%t", cfg.Cache.DisableFlushDB))) cfg.Cache.DefaultTTL, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_DEFAULT_TTL", fmt.Sprintf("%d", cfg.Cache.DefaultTTL))) cfg.Cache.NullTTL, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_NULL_TTL", fmt.Sprintf("%d", cfg.Cache.NullTTL))) cfg.Cache.JitterRatio, _ = strconv.ParseFloat(getEnvOrDefault("APP_CACHE_JITTER_RATIO", fmt.Sprintf("%.2f", cfg.Cache.JitterRatio)), 64) cfg.Cache.Modules.PostList, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_MODULES_POST_LIST_TTL", fmt.Sprintf("%d", cfg.Cache.Modules.PostList))) cfg.Cache.Modules.Conversation, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_MODULES_CONVERSATION_TTL", fmt.Sprintf("%d", cfg.Cache.Modules.Conversation))) cfg.Cache.Modules.UnreadCount, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_MODULES_UNREAD_COUNT_TTL", fmt.Sprintf("%d", cfg.Cache.Modules.UnreadCount))) cfg.Cache.Modules.GroupMembers, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_MODULES_GROUP_MEMBERS_TTL", fmt.Sprintf("%d", cfg.Cache.Modules.GroupMembers))) cfg.S3.Endpoint = getEnvOrDefault("APP_S3_ENDPOINT", cfg.S3.Endpoint) cfg.S3.AccessKey = getEnvOrDefault("APP_S3_ACCESS_KEY", cfg.S3.AccessKey) cfg.S3.SecretKey = getEnvOrDefault("APP_S3_SECRET_KEY", cfg.S3.SecretKey) cfg.S3.Bucket = getEnvOrDefault("APP_S3_BUCKET", cfg.S3.Bucket) cfg.S3.Domain = getEnvOrDefault("APP_S3_DOMAIN", cfg.S3.Domain) cfg.Server.Host = getEnvOrDefault("APP_SERVER_HOST", cfg.Server.Host) cfg.Server.Port, _ = strconv.Atoi(getEnvOrDefault("APP_SERVER_PORT", fmt.Sprintf("%d", cfg.Server.Port))) cfg.Server.Mode = getEnvOrDefault("APP_SERVER_MODE", cfg.Server.Mode) cfg.Gorse.Address = getEnvOrDefault("APP_GORSE_ADDRESS", cfg.Gorse.Address) cfg.Gorse.APIKey = getEnvOrDefault("APP_GORSE_API_KEY", cfg.Gorse.APIKey) cfg.Gorse.Dashboard = getEnvOrDefault("APP_GORSE_DASHBOARD", cfg.Gorse.Dashboard) cfg.Gorse.ImportPassword = getEnvOrDefault("APP_GORSE_IMPORT_PASSWORD", cfg.Gorse.ImportPassword) cfg.Gorse.EmbeddingAPIKey = getEnvOrDefault("APP_GORSE_EMBEDDING_API_KEY", cfg.Gorse.EmbeddingAPIKey) cfg.Gorse.EmbeddingURL = getEnvOrDefault("APP_GORSE_EMBEDDING_URL", cfg.Gorse.EmbeddingURL) cfg.Gorse.EmbeddingModel = getEnvOrDefault("APP_GORSE_EMBEDDING_MODEL", cfg.Gorse.EmbeddingModel) cfg.OpenAI.BaseURL = getEnvOrDefault("APP_OPENAI_BASE_URL", cfg.OpenAI.BaseURL) cfg.OpenAI.APIKey = getEnvOrDefault("APP_OPENAI_API_KEY", cfg.OpenAI.APIKey) cfg.OpenAI.ModerationModel = getEnvOrDefault("APP_OPENAI_MODERATION_MODEL", cfg.OpenAI.ModerationModel) cfg.OpenAI.ModerationMaxImagesPerRequest, _ = strconv.Atoi(getEnvOrDefault("APP_OPENAI_MODERATION_MAX_IMAGES_PER_REQUEST", fmt.Sprintf("%d", cfg.OpenAI.ModerationMaxImagesPerRequest))) cfg.OpenAI.RequestTimeout, _ = strconv.Atoi(getEnvOrDefault("APP_OPENAI_REQUEST_TIMEOUT", fmt.Sprintf("%d", cfg.OpenAI.RequestTimeout))) cfg.OpenAI.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_OPENAI_ENABLED", fmt.Sprintf("%t", cfg.OpenAI.Enabled))) cfg.OpenAI.StrictModeration, _ = strconv.ParseBool(getEnvOrDefault("APP_OPENAI_STRICT_MODERATION", fmt.Sprintf("%t", cfg.OpenAI.StrictModeration))) cfg.Email.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_ENABLED", fmt.Sprintf("%t", cfg.Email.Enabled))) cfg.Email.Host = getEnvOrDefault("APP_EMAIL_HOST", cfg.Email.Host) cfg.Email.Port, _ = strconv.Atoi(getEnvOrDefault("APP_EMAIL_PORT", fmt.Sprintf("%d", cfg.Email.Port))) cfg.Email.Username = getEnvOrDefault("APP_EMAIL_USERNAME", cfg.Email.Username) cfg.Email.Password = getEnvOrDefault("APP_EMAIL_PASSWORD", cfg.Email.Password) cfg.Email.FromAddress = getEnvOrDefault("APP_EMAIL_FROM_ADDRESS", cfg.Email.FromAddress) cfg.Email.FromName = getEnvOrDefault("APP_EMAIL_FROM_NAME", cfg.Email.FromName) cfg.Email.UseTLS, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_USE_TLS", fmt.Sprintf("%t", cfg.Email.UseTLS))) cfg.Email.InsecureSkipVerify, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_INSECURE_SKIP_VERIFY", fmt.Sprintf("%t", cfg.Email.InsecureSkipVerify))) cfg.Email.Timeout, _ = strconv.Atoi(getEnvOrDefault("APP_EMAIL_TIMEOUT", fmt.Sprintf("%d", cfg.Email.Timeout))) // GRPC 环境变量覆盖 cfg.GRPC.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_GRPC_ENABLED", fmt.Sprintf("%t", cfg.GRPC.Enabled))) cfg.GRPC.Port, _ = strconv.Atoi(getEnvOrDefault("APP_GRPC_PORT", fmt.Sprintf("%d", cfg.GRPC.Port))) cfg.GRPC.TLSEnabled, _ = strconv.ParseBool(getEnvOrDefault("APP_GRPC_TLS_ENABLED", fmt.Sprintf("%t", cfg.GRPC.TLSEnabled))) cfg.GRPC.TLSCertFile = getEnvOrDefault("APP_GRPC_TLS_CERT_FILE", cfg.GRPC.TLSCertFile) cfg.GRPC.TLSKeyFile = getEnvOrDefault("APP_GRPC_TLS_KEY_FILE", cfg.GRPC.TLSKeyFile) // Casbin 环境变量覆盖 cfg.Casbin.ModelPath = getEnvOrDefault("APP_CASBIN_MODEL_PATH", cfg.Casbin.ModelPath) cfg.Casbin.AutoSave, _ = strconv.ParseBool(getEnvOrDefault("APP_CASBIN_AUTO_SAVE", fmt.Sprintf("%t", cfg.Casbin.AutoSave))) cfg.Casbin.AutoLoad, _ = strconv.ParseBool(getEnvOrDefault("APP_CASBIN_AUTO_LOAD", fmt.Sprintf("%t", cfg.Casbin.AutoLoad))) cfg.Casbin.EnableCache, _ = strconv.ParseBool(getEnvOrDefault("APP_CASBIN_ENABLE_CACHE", fmt.Sprintf("%t", cfg.Casbin.EnableCache))) cfg.Casbin.CacheTTL, _ = strconv.Atoi(getEnvOrDefault("APP_CASBIN_CACHE_TTL", fmt.Sprintf("%d", cfg.Casbin.CacheTTL))) // Encryption 环境变量覆盖 cfg.Encryption.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_ENCRYPTION_ENABLED", fmt.Sprintf("%t", cfg.Encryption.Enabled))) cfg.Encryption.Key = getEnvOrDefault("APP_ENCRYPTION_KEY", cfg.Encryption.Key) cfg.Encryption.KeyVersion, _ = strconv.Atoi(getEnvOrDefault("APP_ENCRYPTION_KEY_VERSION", fmt.Sprintf("%d", cfg.Encryption.KeyVersion))) return &cfg, nil } // getEnvOrDefault 获取环境变量值,如果未设置则返回默认值 func getEnvOrDefault(key, defaultValue string) string { return cmp.Or(os.Getenv(key), defaultValue) }