package config import ( "fmt" "os" "github.com/spf13/viper" ) type Config struct { Server ServerConfig MySQL MySQLConfig Redis RedisConfig Telegram TelegramConfig LLM LLMConfig Serper SerperConfig GitHub GitHubConfig Security SecurityConfig App AppConfig } type ServerConfig struct { Port int CORSOrigins []string `mapstructure:"cors_origins"` // allowed origins, empty = * MaxUploadMB int `mapstructure:"max_upload_mb"` // max upload size in MB, default 10 } type SecurityConfig struct { JWTSecret string `mapstructure:"jwt_secret"` PasswordMinLen int `mapstructure:"password_min_len"` // default 8 WebhookAllowHTTP bool `mapstructure:"webhook_allow_http"` // allow http:// webhooks } type AppConfig struct { Name string // display name, default "商户采集系统" } type MySQLConfig struct { Host string Port int User string Password string Database string } type RedisConfig struct { Host string Port int Password string DB int } type TelegramConfig struct { AppID int `mapstructure:"app_id"` AppHash string `mapstructure:"app_hash"` Accounts []TGAccount SessionsDir string `mapstructure:"sessions_dir"` // absolute path, e.g. /app/sessions SecretKey string `mapstructure:"secret_key"` // base64 32-byte, or literal ${TG_SECRET_KEY} } type TGAccount struct { Phone string SessionFile string `mapstructure:"session_file"` } type LLMConfig struct { Provider string BaseURL string `mapstructure:"base_url"` APIKey string `mapstructure:"api_key"` Model string Timeout string } type SerperConfig struct { APIKey string `mapstructure:"api_key"` ResultsPerPage int `mapstructure:"results_per_page"` MaxPages int `mapstructure:"max_pages"` } type GitHubConfig struct { Token string } var global *Config func Load(path string) (*Config, error) { v := viper.New() v.SetConfigFile(path) v.SetConfigType("yaml") if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("read config file: %w", err) } cfg := &Config{} if err := v.Unmarshal(cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } // Apply defaults if cfg.Security.JWTSecret == "" { cfg.Security.JWTSecret = "spider-jwt-secret-2026" } if cfg.Security.PasswordMinLen < 6 { cfg.Security.PasswordMinLen = 8 } if cfg.Server.MaxUploadMB <= 0 { cfg.Server.MaxUploadMB = 10 } if cfg.App.Name == "" { cfg.App.Name = "商户采集系统" } // Resolve ${ENV_VAR} placeholders for secret fields. cfg.Telegram.SecretKey = expandEnvPlaceholder(cfg.Telegram.SecretKey) cfg.Security.JWTSecret = expandEnvPlaceholder(cfg.Security.JWTSecret) if cfg.Telegram.SessionsDir == "" { cfg.Telegram.SessionsDir = "/app/sessions" } global = cfg return cfg, nil } func Get() *Config { return global } // expandEnvPlaceholder replaces a value of the form "${VAR}" with os.Getenv("VAR"). // A non-placeholder value is returned unchanged. func expandEnvPlaceholder(v string) string { if len(v) > 3 && v[0] == '$' && v[1] == '{' && v[len(v)-1] == '}' { return os.Getenv(v[2 : len(v)-1]) } return v }