Add Tencent Cloud Text Moderation System (TMS) as a fallback moderation service when OpenAI is unavailable or returns an error. The service now checks OpenAI first, and falls back to Tencent TMS if it fails, providing better reliability for content moderation. Features: - New Tencent TMS configuration in config.yaml with environment variable overrides - New tencent package in internal/pkg/tencent/ with TMS client implementation - PostAIService now uses dual moderation with OpenAI primary and Tencent fallback - Strict mode configuration passed to PostAIService for consistent behavior BREAKING CHANGE: NewPostAIService now requires tencentClient and strictMode parameters - update wire configuration accordingly.
289 lines
18 KiB
Go
289 lines
18 KiB
Go
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"`
|
||
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"`
|
||
WebRTC WebRTCConfig `mapstructure:"webrtc"`
|
||
Report ReportConfig `mapstructure:"report"`
|
||
Sensitive SensitiveConfig `mapstructure:"sensitive"`
|
||
}
|
||
|
||
// 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("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("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", "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)
|
||
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)
|
||
// WebRTC 默认值(Google 公共 STUN 服务器)
|
||
viper.SetDefault("webrtc.ice_servers", []map[string]any{
|
||
{"urls": []string{"stun:stun.l.google.com:19302"}},
|
||
})
|
||
// Report 默认值
|
||
viper.SetDefault("report.auto_hide_threshold", 3)
|
||
|
||
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.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)))
|
||
// TencentTMS 环境变量覆盖
|
||
cfg.TencentTMS.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_TENCENT_TMS_ENABLED", fmt.Sprintf("%t", cfg.TencentTMS.Enabled)))
|
||
cfg.TencentTMS.SecretID = getEnvOrDefault("APP_TENCENT_TMS_SECRET_ID", cfg.TencentTMS.SecretID)
|
||
cfg.TencentTMS.SecretKey = getEnvOrDefault("APP_TENCENT_TMS_SECRET_KEY", cfg.TencentTMS.SecretKey)
|
||
cfg.TencentTMS.Region = getEnvOrDefault("APP_TENCENT_TMS_REGION", cfg.TencentTMS.Region)
|
||
cfg.TencentTMS.BizType = getEnvOrDefault("APP_TENCENT_TMS_BIZ_TYPE", cfg.TencentTMS.BizType)
|
||
cfg.TencentTMS.Timeout, _ = strconv.Atoi(getEnvOrDefault("APP_TENCENT_TMS_TIMEOUT", fmt.Sprintf("%d", cfg.TencentTMS.Timeout)))
|
||
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)))
|
||
// Sensitive 环境变量覆盖
|
||
cfg.Sensitive.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_SENSITIVE_ENABLED", fmt.Sprintf("%t", cfg.Sensitive.Enabled)))
|
||
cfg.Sensitive.ReplaceStr = getEnvOrDefault("APP_SENSITIVE_REPLACE_STR", cfg.Sensitive.ReplaceStr)
|
||
cfg.Sensitive.MinMatchLen, _ = strconv.Atoi(getEnvOrDefault("APP_SENSITIVE_MIN_MATCH_LEN", fmt.Sprintf("%d", cfg.Sensitive.MinMatchLen)))
|
||
cfg.Sensitive.LoadFromDB, _ = strconv.ParseBool(getEnvOrDefault("APP_SENSITIVE_LOAD_FROM_DB", fmt.Sprintf("%t", cfg.Sensitive.LoadFromDB)))
|
||
cfg.Sensitive.LoadFromRedis, _ = strconv.ParseBool(getEnvOrDefault("APP_SENSITIVE_LOAD_FROM_REDIS", fmt.Sprintf("%t", cfg.Sensitive.LoadFromRedis)))
|
||
cfg.Sensitive.RedisKeyPrefix = getEnvOrDefault("APP_SENSITIVE_REDIS_KEY_PREFIX", cfg.Sensitive.RedisKeyPrefix)
|
||
|
||
// 安全检查:生产模式必须设置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
|
||
}
|
||
|
||
// getEnvOrDefault 获取环境变量值,如果未设置则返回默认值
|
||
func getEnvOrDefault(key, defaultValue string) string {
|
||
return cmp.Or(os.Getenv(key), defaultValue)
|
||
}
|