Files
backend/internal/config/config.go

244 lines
15 KiB
Go
Raw Normal View History

package config
import (
"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"})
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)))
return &cfg, nil
}
// getEnvOrDefault 获取环境变量值,如果未设置则返回默认值
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}