package config import ( "errors" "fmt" "net/url" "os" "strconv" "strings" "time" ) type Config struct { Environment string Address string DataDir string PublicURL string AdminEmail string AdminName string AdminPassword string DeveloperToken string AllowLegacyToken bool AllowedOrigins []string AllowedHosts []string ForceHTTPS bool TrustProxy bool SessionDuration time.Duration MaxUploadBytes int64 RateLimitPerMinute int ReadHeaderTimeout time.Duration ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration ShutdownTimeout time.Duration LogLevel string } func Load() (Config, error) { environment := strings.ToLower(strings.TrimSpace(envOr("SCOUT_ENV", "development"))) if environment == "" { environment = "development" } if environment != "development" && environment != "test" && environment != "production" { return Config{}, errors.New("SCOUT_ENV 只能是 development、test 或 production") } sessionDuration, err := durationEnv("SCOUT_SESSION_DURATION", 24*time.Hour) if err != nil { return Config{}, err } maxUploadBytes, err := bytesEnv("SCOUT_MAX_UPLOAD_BYTES", 512<<20) if err != nil { return Config{}, err } rateLimit, err := intEnv("SCOUT_RATE_LIMIT_PER_MINUTE", 120) if err != nil || rateLimit < 0 { return Config{}, errors.New("SCOUT_RATE_LIMIT_PER_MINUTE 必须是大于等于 0 的整数") } cfg := Config{ Environment: environment, Address: envOr("SCOUT_ADDR", ":8080"), DataDir: envOr("SCOUT_DATA_DIR", "data"), PublicURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SCOUT_PUBLIC_URL")), "/"), AdminEmail: strings.TrimSpace(os.Getenv("SCOUT_ADMIN_EMAIL")), AdminName: strings.TrimSpace(os.Getenv("SCOUT_ADMIN_NAME")), AdminPassword: os.Getenv("SCOUT_ADMIN_PASSWORD"), DeveloperToken: os.Getenv("SCOUT_DEVELOPER_TOKEN"), AllowedOrigins: csvEnv("SCOUT_ALLOWED_ORIGINS"), AllowedHosts: csvEnv("SCOUT_ALLOWED_HOSTS"), ForceHTTPS: boolEnv("SCOUT_FORCE_HTTPS", environment == "production"), TrustProxy: boolEnv("SCOUT_TRUST_PROXY", false), SessionDuration: sessionDuration, MaxUploadBytes: maxUploadBytes, RateLimitPerMinute: rateLimit, ReadHeaderTimeout: durationDefault("SCOUT_READ_HEADER_TIMEOUT", 10*time.Second), ReadTimeout: durationDefault("SCOUT_READ_TIMEOUT", 60*time.Second), WriteTimeout: durationDefault("SCOUT_WRITE_TIMEOUT", 10*time.Minute), IdleTimeout: durationDefault("SCOUT_IDLE_TIMEOUT", 2*time.Minute), ShutdownTimeout: durationDefault("SCOUT_SHUTDOWN_TIMEOUT", 15*time.Second), LogLevel: strings.ToLower(envOr("SCOUT_LOG_LEVEL", "info")), } cfg.AllowLegacyToken = boolEnv("SCOUT_ALLOW_LEGACY_TOKEN", environment != "production") if err := cfg.validate(); err != nil { return Config{}, err } return cfg, nil } func (cfg Config) validate() error { if strings.TrimSpace(cfg.Address) == "" || strings.TrimSpace(cfg.DataDir) == "" { return errors.New("SCOUT_ADDR 和 SCOUT_DATA_DIR 不能为空") } if cfg.SessionDuration <= 0 || cfg.MaxUploadBytes <= 0 { return errors.New("会话时长和上传大小必须大于 0") } if cfg.LogLevel != "debug" && cfg.LogLevel != "info" && cfg.LogLevel != "warn" && cfg.LogLevel != "error" { return errors.New("SCOUT_LOG_LEVEL 只能是 debug、info、warn 或 error") } if cfg.PublicURL != "" { parsed, err := url.Parse(cfg.PublicURL) if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { return errors.New("SCOUT_PUBLIC_URL 必须是完整的 HTTP 或 HTTPS 地址") } if cfg.Environment == "production" && parsed.Scheme != "https" { return errors.New("生产环境的 SCOUT_PUBLIC_URL 必须使用 HTTPS") } } if cfg.Environment != "production" { return nil } if cfg.AdminEmail == "" || cfg.AdminPassword == "" { return errors.New("生产环境必须配置 SCOUT_ADMIN_EMAIL 和 SCOUT_ADMIN_PASSWORD") } if cfg.AdminPassword == "Admin-Test-123" { return errors.New("生产环境不能使用本地测试管理员密码") } if cfg.DeveloperToken != "" && !cfg.AllowLegacyToken { return errors.New("生产环境默认禁止兼容开发者令牌;如确需启用,请设置 SCOUT_ALLOW_LEGACY_TOKEN=true") } if len(cfg.AllowedOrigins) == 0 { return errors.New("生产环境必须配置 SCOUT_ALLOWED_ORIGINS") } if len(cfg.AllowedHosts) == 0 { return errors.New("生产环境必须配置 SCOUT_ALLOWED_HOSTS") } if cfg.PublicURL == "" { return errors.New("生产环境必须配置 SCOUT_PUBLIC_URL") } if os.Getenv("SCOUT_SMTP_HOST") == "" || os.Getenv("SCOUT_SMTP_FROM") == "" { return errors.New("生产环境必须配置 SCOUT_SMTP_HOST 和 SCOUT_SMTP_FROM") } return nil } func envOr(name, fallback string) string { if value := os.Getenv(name); value != "" { return value } return fallback } func csvEnv(name string) []string { values := make([]string, 0) for _, value := range strings.Split(os.Getenv(name), ",") { value = strings.TrimSpace(value) if value != "" { values = append(values, strings.TrimRight(value, "/")) } } return values } func boolEnv(name string, fallback bool) bool { value := strings.TrimSpace(os.Getenv(name)) if value == "" { return fallback } parsed, err := strconv.ParseBool(value) if err != nil { return fallback } return parsed } func intEnv(name string, fallback int) (int, error) { value := strings.TrimSpace(os.Getenv(name)) if value == "" { return fallback, nil } parsed, err := strconv.Atoi(value) if err != nil { return 0, fmt.Errorf("%s 必须是整数", name) } return parsed, nil } func durationEnv(name string, fallback time.Duration) (time.Duration, error) { value := strings.TrimSpace(os.Getenv(name)) if value == "" { return fallback, nil } parsed, err := time.ParseDuration(value) if err != nil || parsed <= 0 { return 0, fmt.Errorf("%s 必须是大于 0 的时间长度,例如 24h 或 30s", name) } return parsed, nil } func durationDefault(name string, fallback time.Duration) time.Duration { value, err := durationEnv(name, fallback) if err != nil { return fallback } return value } func bytesEnv(name string, fallback int64) (int64, error) { value := strings.TrimSpace(os.Getenv(name)) if value == "" { return fallback, nil } multiplier := int64(1) upper := strings.ToUpper(value) switch { case strings.HasSuffix(upper, "KB"): multiplier = 1 << 10 value = strings.TrimSpace(value[:len(value)-2]) case strings.HasSuffix(upper, "MB"): multiplier = 1 << 20 value = strings.TrimSpace(value[:len(value)-2]) case strings.HasSuffix(upper, "GB"): multiplier = 1 << 30 value = strings.TrimSpace(value[:len(value)-2]) } number, err := strconv.ParseInt(value, 10, 64) if err != nil || number <= 0 { return 0, fmt.Errorf("%s 必须是正整数或带 KB、MB、GB 后缀的大小", name) } return number * multiplier, nil }