package config import ( "fmt" "os" "strconv" "strings" "gopkg.in/yaml.v3" ) type AppConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` Version string `yaml:"version"` Debug bool `yaml:"debug"` Timezone string `yaml:"timezone"` Environment string `yaml:"environment"` } type LoggerConfig struct { Level string `yaml:"level"` Format string `yaml:"format"` Output string `yaml:"output"` Filename string `yaml:"filename"` MaxSize int `yaml:"max_size"` MaxAge int `yaml:"max_age"` MaxBackups int `yaml:"max_backups"` Compress bool `yaml:"compress"` } type DatabaseConfig struct { Driver string `yaml:"driver"` DSN string `yaml:"dsn"` Host string `yaml:"host"` Port int `yaml:"port"` Username string `yaml:"username"` Password string `yaml:"password"` DBName string `yaml:"dbname"` SSLMode string `yaml:"sslmode"` SQLitePath string `yaml:"sqlite_path"` MaxIdleConns int `yaml:"max_idle_conns"` MaxOpenConns int `yaml:"max_open_conns"` ConnMaxLifetime int `yaml:"conn_max_lifetime"` } type RedisConfig struct { Enabled bool `yaml:"enabled"` Host string `yaml:"host"` Port int `yaml:"port"` Password string `yaml:"password"` Database int `yaml:"database"` PoolSize int `yaml:"pool_size"` MinIdleConns int `yaml:"min_idle_conns"` MaxRetries int `yaml:"max_retries"` } type JWTConfig struct { Secret string `yaml:"secret"` ExpireHours int `yaml:"expire_hours"` // 兼容旧配置,作为 access token 默认值 AccessExpireMinutes int `yaml:"access_expire_minutes"` RefreshExpireHours int `yaml:"refresh_expire_hours"` Issuer string `yaml:"issuer"` } type ObservabilityConfig struct { MetricsEnabled bool `yaml:"metrics_enabled"` MetricsPath string `yaml:"metrics_path"` TracingEnabled bool `yaml:"tracing_enabled"` TracingService string `yaml:"tracing_service"` TracingEndpoint string `yaml:"tracing_endpoint"` TracingInsecure bool `yaml:"tracing_insecure"` } type Config struct { App AppConfig `yaml:"app"` Logger LoggerConfig `yaml:"logger"` Database DatabaseConfig `yaml:"database"` Redis RedisConfig `yaml:"redis"` JWT JWTConfig `yaml:"jwt"` Observability ObservabilityConfig `yaml:"observability"` } func Load() (*Config, error) { path := os.Getenv("SKELETON_CONFIG") if path == "" { path = "config/app.yaml" } return LoadFrom(path) } func LoadFrom(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("读取配置文件失败: %w", err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("解析配置文件失败: %w", err) } cfg.applyEnvironment() cfg.setDefaults() if err := cfg.validate(); err != nil { return nil, err } return &cfg, nil } func (c *Config) validate() error { switch c.Database.Driver { case "postgres", "mysql", "sqlite": default: return fmt.Errorf("不支持的数据库驱动 %q", c.Database.Driver) } if strings.EqualFold(c.App.Environment, "production") && (c.JWT.Secret == "change-this-secret-key-in-production" || len(c.JWT.Secret) < 32) { return fmt.Errorf("生产环境 JWT_SECRET 必须修改且至少 32 个字符") } return nil } func (c *Config) applyEnvironment() { stringEnv("APP_HOST", &c.App.Host) intEnv("APP_PORT", &c.App.Port) stringEnv("APP_VERSION", &c.App.Version) boolEnv("APP_DEBUG", &c.App.Debug) stringEnv("APP_TIMEZONE", &c.App.Timezone) stringEnv("APP_ENVIRONMENT", &c.App.Environment) stringEnv("LOG_LEVEL", &c.Logger.Level) stringEnv("LOG_FORMAT", &c.Logger.Format) stringEnv("LOG_OUTPUT", &c.Logger.Output) stringEnv("LOG_FILENAME", &c.Logger.Filename) stringEnv("DATABASE_DRIVER", &c.Database.Driver) stringEnv("DATABASE_DSN", &c.Database.DSN) stringEnv("DATABASE_HOST", &c.Database.Host) intEnv("DATABASE_PORT", &c.Database.Port) stringEnv("DATABASE_USERNAME", &c.Database.Username) stringEnv("DATABASE_PASSWORD", &c.Database.Password) stringEnv("DATABASE_NAME", &c.Database.DBName) stringEnv("DATABASE_SSLMODE", &c.Database.SSLMode) stringEnv("DATABASE_SQLITE_PATH", &c.Database.SQLitePath) intEnv("DATABASE_MAX_IDLE_CONNS", &c.Database.MaxIdleConns) intEnv("DATABASE_MAX_OPEN_CONNS", &c.Database.MaxOpenConns) intEnv("DATABASE_CONN_MAX_LIFETIME", &c.Database.ConnMaxLifetime) boolEnv("REDIS_ENABLED", &c.Redis.Enabled) stringEnv("REDIS_HOST", &c.Redis.Host) intEnv("REDIS_PORT", &c.Redis.Port) stringEnv("REDIS_PASSWORD", &c.Redis.Password) intEnv("REDIS_DATABASE", &c.Redis.Database) stringEnv("JWT_SECRET", &c.JWT.Secret) intEnv("JWT_ACCESS_EXPIRE_MINUTES", &c.JWT.AccessExpireMinutes) intEnv("JWT_REFRESH_EXPIRE_HOURS", &c.JWT.RefreshExpireHours) stringEnv("JWT_ISSUER", &c.JWT.Issuer) boolEnv("METRICS_ENABLED", &c.Observability.MetricsEnabled) stringEnv("METRICS_PATH", &c.Observability.MetricsPath) boolEnv("TRACING_ENABLED", &c.Observability.TracingEnabled) stringEnv("OTEL_SERVICE_NAME", &c.Observability.TracingService) stringEnv("OTEL_EXPORTER_OTLP_ENDPOINT", &c.Observability.TracingEndpoint) boolEnv("OTEL_EXPORTER_OTLP_INSECURE", &c.Observability.TracingInsecure) } func stringEnv(key string, target *string) { if value, ok := os.LookupEnv(key); ok { *target = value } } func intEnv(key string, target *int) { if value, ok := os.LookupEnv(key); ok { if parsed, err := strconv.Atoi(value); err == nil { *target = parsed } } } func boolEnv(key string, target *bool) { if value, ok := os.LookupEnv(key); ok { if parsed, err := strconv.ParseBool(value); err == nil { *target = parsed } } } func (c *Config) setDefaults() { if c.App.Host == "" { c.App.Host = "0.0.0.0" } if c.App.Port == 0 { c.App.Port = 8080 } if c.App.Version == "" { c.App.Version = "0.1.0" } if c.App.Timezone == "" { c.App.Timezone = "UTC" } if c.App.Environment == "" { c.App.Environment = "development" } if c.Logger.Level == "" { c.Logger.Level = "info" } if c.Logger.Format == "" { c.Logger.Format = "console" } if c.Logger.Output == "" { c.Logger.Output = "stdout" } if c.Logger.MaxSize == 0 { c.Logger.MaxSize = 100 } if c.Logger.MaxAge == 0 { c.Logger.MaxAge = 30 } if c.Logger.MaxBackups == 0 { c.Logger.MaxBackups = 3 } if c.Database.Driver == "" { c.Database.Driver = "postgres" } c.Database.Driver = strings.ToLower(strings.TrimSpace(c.Database.Driver)) switch c.Database.Driver { case "postgresql": c.Database.Driver = "postgres" case "sqlite3": c.Database.Driver = "sqlite" } if c.Database.Port == 0 { switch c.Database.Driver { case "postgres": c.Database.Port = 5432 case "mysql": c.Database.Port = 3306 } } if c.Database.SSLMode == "" { c.Database.SSLMode = "disable" } if c.Database.SQLitePath == "" { c.Database.SQLitePath = "data/skeleton.db" } if c.Database.MaxIdleConns == 0 { if c.Database.Driver == "sqlite" { c.Database.MaxIdleConns = 1 } else { c.Database.MaxIdleConns = 10 } } if c.Database.MaxOpenConns == 0 { if c.Database.Driver == "sqlite" { c.Database.MaxOpenConns = 1 } else { c.Database.MaxOpenConns = 100 } } if c.Database.ConnMaxLifetime == 0 { c.Database.ConnMaxLifetime = 60 } if c.Redis.Host == "" { c.Redis.Host = "localhost" } if c.Redis.Port == 0 { c.Redis.Port = 6379 } if c.Redis.Database == 0 { c.Redis.Database = 0 } if c.Redis.PoolSize == 0 { c.Redis.PoolSize = 10 } if c.Redis.MinIdleConns == 0 { c.Redis.MinIdleConns = 5 } if c.Redis.MaxRetries == 0 { c.Redis.MaxRetries = 3 } if c.JWT.Secret == "" { c.JWT.Secret = "change-this-secret-key-in-production" } if c.JWT.ExpireHours == 0 { c.JWT.ExpireHours = 24 } if c.JWT.AccessExpireMinutes == 0 { c.JWT.AccessExpireMinutes = c.JWT.ExpireHours * 60 } if c.JWT.RefreshExpireHours == 0 { c.JWT.RefreshExpireHours = 24 * 7 } if c.JWT.Issuer == "" { c.JWT.Issuer = "HeTianXia" } if c.Observability.MetricsPath == "" { c.Observability.MetricsPath = "/metrics" } if c.Observability.TracingService == "" { c.Observability.TracingService = "skeleton" } if c.Observability.TracingEndpoint == "" { c.Observability.TracingEndpoint = "localhost:4318" } } func (c *Config) GetAddr() string { return fmt.Sprintf("%s:%d", c.App.Host, c.App.Port) }