package config import ( "fmt" "net" "os" "path/filepath" "runtime" "strconv" "strings" "time" "cloudnest/internal/pkg/logger" "github.com/spf13/viper" ) // Config holds all application configuration. // Config 包含所有应用程序配置。 // // Configuration loading order (later overrides earlier): // 1. config.yaml (default values) // 2. config.{env}.yaml (environment-specific values) // 3. Environment variables (highest priority, auto-mapped) // // 配置加载顺序(后面的覆盖前面的): // 1. config.yaml(默认值) // 2. config.{env}.yaml(环境特定值) // 3. 环境变量(最高优先级,自动映射) type Config struct { App AppConfig `mapstructure:"app"` Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` Cache CacheConfig `mapstructure:"cache"` Storage StorageConfig `mapstructure:"storage"` JWT JWTConfig `mapstructure:"jwt"` Email EmailConfig `mapstructure:"email"` } // AppConfig holds application-level configuration. // AppConfig 包含应用程序级别的配置。 type AppConfig struct { Name string `mapstructure:"name"` Version string `mapstructure:"version"` Env string `mapstructure:"env"` LogLevel string `mapstructure:"log_level"` FileServiceURL string `mapstructure:"file_service_url"` } // ServerConfig holds HTTP server configuration. // ServerConfig 包含 HTTP 服务器配置。 type ServerConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` GracefulShutdownTimeout int `mapstructure:"graceful_shutdown_timeout"` } // DatabaseConfig holds database connection configuration. // DatabaseConfig 包含数据库连接配置。 type DatabaseConfig struct { Driver string `mapstructure:"driver"` Host string `mapstructure:"host"` Port int `mapstructure:"port"` User string `mapstructure:"user"` Password string `mapstructure:"password"` DBName string `mapstructure:"dbname"` Charset string `mapstructure:"charset"` ParseTime bool `mapstructure:"parse_time"` Loc string `mapstructure:"loc"` MaxIdleConns int `mapstructure:"max_idle_conns"` MaxOpenConns int `mapstructure:"max_open_conns"` ConnMaxLifetime int `mapstructure:"conn_max_lifetime"` } // CacheConfig holds Redis cache configuration. // CacheConfig 包含 Redis 缓存配置。 type CacheConfig struct { Driver string `mapstructure:"driver"` Host string `mapstructure:"host"` Port int `mapstructure:"port"` Password string `mapstructure:"password"` DB int `mapstructure:"db"` PoolSize int `mapstructure:"pool_size"` MinIdleConns int `mapstructure:"min_idle_conns"` } // StorageConfig holds MinIO object storage configuration. // StorageConfig 包含 MinIO 对象存储配置。 type StorageConfig struct { Driver string `mapstructure:"driver"` Endpoint string `mapstructure:"endpoint"` AccessKeyID string `mapstructure:"access_key_id"` SecretAccessKey string `mapstructure:"secret_access_key"` BucketName string `mapstructure:"bucket_name"` UseSSL bool `mapstructure:"use_ssl"` PresignDuration int `mapstructure:"presign_duration"` MaxFileSize int `mapstructure:"max_file_size"` RecycleBinExpireDays int `mapstructure:"recycle_bin_expire_days"` } // JWTConfig holds JWT authentication configuration. // JWTConfig 包含 JWT 认证配置。 type JWTConfig struct { Secret string `mapstructure:"secret"` ExpiresIn int `mapstructure:"expires_in"` Issuer string `mapstructure:"issuer"` } // EmailConfig holds SMTP email configuration. // EmailConfig 包含 SMTP 邮件配置。 type EmailConfig struct { SMTPHost string `mapstructure:"smtp_host"` SMTPPort int `mapstructure:"smtp_port"` SMTPUsername string `mapstructure:"smtp_username"` SMTPPassword string `mapstructure:"smtp_password"` FromName string `mapstructure:"from_name"` } // DSN builds MySQL DSN string from config fields. // DSN 从配置字段构建 MySQL DSN 字符串。 // // Returns: // - string: The formatted DSN string. // // 返回值: // - string: 格式化后的 DSN 字符串。 func (c *DatabaseConfig) DSN() string { return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=%t&loc=%s", c.User, c.Password, c.Host, c.Port, c.DBName, c.Charset, c.ParseTime, c.Loc, ) } // Addr builds Redis connection address. // Addr 构建 Redis 连接地址。 // // Returns: // - string: The formatted "host:port" address. // // 返回值: // - string: 格式化的 "host:port" 地址。 func (c *CacheConfig) Addr() string { return fmt.Sprintf("%s:%d", c.Host, c.Port) } // PresignDuration builds time.Duration from hours config. // PresignDuration 从小时配置构建 time.Duration。 // // Returns: // - time.Duration: The presign duration. // // 返回值: // - time.Duration: 预签名有效期。 func (c *StorageConfig) PresignDurationTime() time.Duration { return time.Duration(c.PresignDuration) * time.Hour } // ExpiresIn builds time.Duration from hours config. // ExpiresIn 从小时配置构建 time.Duration。 // // Returns: // - time.Duration: The JWT expiration duration. // // 返回值: // - time.Duration: JWT 过期时间。 func (c *JWTConfig) ExpiresInDuration() time.Duration { return time.Duration(c.ExpiresIn) * time.Hour } // getEnv returns the current environment name from CLOUDNEST_ENV or defaults to "dev". // getEnv 从 CLOUDNEST_ENV 获取当前环境名称,默认为 "dev"。 func getEnv() string { env := strings.ToLower(os.Getenv("CLOUDNEST_ENV")) if env == "" { env = "dev" } return env } // getConfigDir returns the absolute path to the configs directory. // getConfigDir 返回 configs 目录的绝对路径。 // // Note: // - Tries to find configs/ relative to the executable or current working directory. // - Falls back to current working directory if not found. // // 注意: // - 尝试从可执行文件或当前工作目录相对位置找到 configs/。 // - 如果找不到则回退到当前工作目录。 func getConfigDir() string { // Try to get the directory of the current file // 尝试获取当前文件的目录 _, filename, _, ok := runtime.Caller(0) if ok { dir := filepath.Join(filepath.Dir(filename), "..", "..", "configs") if _, err := os.Stat(dir); err == nil { return dir } } // Try executable directory / 尝试可执行文件目录 ex, err := os.Executable() if err == nil { dir := filepath.Join(filepath.Dir(ex), "configs") if _, err := os.Stat(dir); err == nil { return dir } } // Try current working directory / 尝试当前工作目录 cwd, err := os.Getwd() if err == nil { dir := filepath.Join(cwd, "configs") if _, err := os.Stat(dir); err == nil { return dir } } return "configs" } // Load reads and parses configuration from YAML files and environment variables. // Load 从 YAML 文件和环境变量中读取并解析配置。 // // Loading order (later overrides earlier): // 1. configs/config.yaml (base defaults) // 2. configs/config.{env}.yaml (env-specific overrides) // 3. Environment variables (highest priority) // // Environment selection via CLOUDNEST_ENV: // - "dev" -> configs/config.dev.yaml // - "test" -> configs/config.test.yaml // - "prod" -> configs/config.prod.yaml // - default -> configs/config.dev.yaml // // Environment variables are automatically mapped with the following prefixes: // - APP_ -> app // - SERVER_ -> server // - DB_ -> database // - REDIS_ -> cache // - MINIO_ -> storage // - JWT_ -> jwt // - EMAIL_ -> email // // Returns: // - *Config: A pointer to the parsed configuration struct. // // 配置加载顺序(后面的覆盖前面的): // 1. configs/config.yaml(基础默认值) // 2. configs/config.{env}.yaml(环境特定覆盖) // 3. 环境变量(最高优先级) // // 通过 CLOUDNEST_ENV 选择环境: // - "dev" -> configs/config.dev.yaml // - "test" -> configs/config.test.yaml // - "prod" -> configs/config.prod.yaml // - 默认 -> configs/config.dev.yaml // // 环境变量自动映射的前缀: // - APP_ -> app // - SERVER_ -> server // - DB_ -> database // - REDIS_ -> cache // - MINIO_ -> storage // - JWT_ -> jwt // - EMAIL_ -> email // // 返回值: // - *Config: 解析后的配置结构体指针。 func Load() *Config { env := getEnv() configDir := getConfigDir() v := viper.New() v.SetConfigType("yaml") // 1. Load base config / 加载基础配置 baseConfig := filepath.Join(configDir, "config.yaml") v.SetConfigFile(baseConfig) if err := v.ReadInConfig(); err != nil { logger.Warn("failed to read base config, using defaults", "error", err, "path", baseConfig) } else { logger.Info("base config loaded", "path", baseConfig) } // 2. Load environment-specific config / 加载环境特定配置 envConfig := filepath.Join(configDir, fmt.Sprintf("config.%s.yaml", env)) if _, err := os.Stat(envConfig); err == nil { envViper := viper.New() envViper.SetConfigFile(envConfig) if err := envViper.ReadInConfig(); err == nil { // Merge environment config into base config // 将环境配置合并到基础配置中 for _, key := range envViper.AllKeys() { v.Set(key, envViper.Get(key)) } logger.Info("env config loaded", "env", env, "path", envConfig) } else { logger.Warn("failed to read env config", "env", env, "error", err) } } else { logger.Info("env config not found, using base config", "env", env, "path", envConfig) } // 3. Enable environment variable overrides / 启用环境变量覆盖 v.AutomaticEnv() v.SetEnvPrefix("") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // Map environment variables to nested config keys // 将环境变量映射到嵌套配置键 envMappings := map[string]string{ "APP_NAME": "app.name", "APP_VERSION": "app.version", "APP_ENV": "app.env", "APP_LOG_LEVEL": "app.log_level", "APP_FILE_SERVICE_URL": "app.file_service_url", "SERVER_HOST": "server.host", "SERVER_PORT": "server.port", "SERVER_GRACEFUL_SHUTDOWN_TIMEOUT": "server.graceful_shutdown_timeout", "DB_DRIVER": "database.driver", "DB_HOST": "database.host", "DB_PORT": "database.port", "DB_USER": "database.user", "DB_PASSWORD": "database.password", "DB_NAME": "database.dbname", "DB_CHARSET": "database.charset", "DB_PARSE_TIME": "database.parse_time", "DB_LOC": "database.loc", "DB_MAX_IDLE_CONNS": "database.max_idle_conns", "DB_MAX_OPEN_CONNS": "database.max_open_conns", "DB_CONN_MAX_LIFETIME": "database.conn_max_lifetime", "REDIS_HOST": "cache.host", "REDIS_PORT": "cache.port", "REDIS_PASSWORD": "cache.password", "REDIS_DB": "cache.db", "REDIS_POOL_SIZE": "cache.pool_size", "REDIS_MIN_IDLE_CONNS": "cache.min_idle_conns", "MINIO_ENDPOINT": "storage.endpoint", "MINIO_ACCESS_KEY": "storage.access_key_id", "MINIO_SECRET_KEY": "storage.secret_access_key", "MINIO_BUCKET": "storage.bucket_name", "MINIO_USE_SSL": "storage.use_ssl", "MINIO_PRESIGN_DURATION": "storage.presign_duration", "MINIO_MAX_FILE_SIZE": "storage.max_file_size", "MINIO_RECYCLE_BIN_EXPIRE_DAYS": "storage.recycle_bin_expire_days", "JWT_SECRET": "jwt.secret", "JWT_EXPIRES_IN": "jwt.expires_in", "JWT_ISSUER": "jwt.issuer", "EMAIL_SMTP_HOST": "email.smtp_host", "EMAIL_SMTP_PORT": "email.smtp_port", "EMAIL_SMTP_USERNAME": "email.smtp_username", "EMAIL_SMTP_PASSWORD": "email.smtp_password", "EMAIL_FROM_NAME": "email.from_name", } for envKey, configKey := range envMappings { if val := os.Getenv(envKey); val != "" { // Try to parse as int/bool first, fallback to string // 先尝试解析为 int/bool,否则回退为 string if intVal, err := strconv.Atoi(val); err == nil { v.Set(configKey, intVal) } else if boolVal, err := strconv.ParseBool(val); err == nil { v.Set(configKey, boolVal) } else { v.Set(configKey, val) } } } // 4. Unmarshal into struct / 反序列化到结构体 var cfg Config if err := v.Unmarshal(&cfg); err != nil { logger.Fatal("failed to unmarshal config", "error", err) } logger.Info("config loaded", "app", fmt.Sprintf("%s:%s", cfg.App.Name, cfg.App.Version), "env", cfg.App.Env, "config_dir", configDir, ) return &cfg } // IsDev returns true if running in development environment. // IsDev 如果运行在开发环境则返回 true。 func (c *Config) IsDev() bool { return strings.ToLower(c.App.Env) == "development" || strings.ToLower(c.App.Env) == "dev" } // IsProd returns true if running in production environment. // IsProd 如果运行在生产环境则返回 true。 func (c *Config) IsProd() bool { return strings.ToLower(c.App.Env) == "production" || strings.ToLower(c.App.Env) == "prod" } // IsTest returns true if running in test environment. // IsTest 如果运行在测试环境则返回 true。 func (c *Config) IsTest() bool { return strings.ToLower(c.App.Env) == "test" } // FindAvailablePort checks if the configured port is available, returns next available if occupied. // FindAvailablePort 检查配置的端口是否可用,如果被占用则返回下一个可用端口。 // // Parameters: // - preferredPort: The preferred port to use. // - maxAttempts: Maximum number of ports to try. // // Returns: // - int: An available port number. // - error: An error if no port is available after max attempts. // // Note: // - Only searches in development mode. In production, it returns the configured port directly. // - Search range: preferredPort to preferredPort + maxAttempts - 1. // // 参数: // - preferredPort: 首选端口。 // - maxAttempts: 最大尝试次数。 // // 返回值: // - int: 可用端口号。 // - error: 如果超过最大尝试次数仍未找到可用端口则返回错误。 // // 注意: // - 仅在开发模式下搜索。生产模式下直接返回配置端口。 // - 搜索范围: preferredPort 到 preferredPort + maxAttempts - 1。 func FindAvailablePort(preferredPort int, maxAttempts int) (int, error) { for i := 0; i < maxAttempts; i++ { port := preferredPort + i addr := fmt.Sprintf(":%d", port) listener, err := net.Listen("tcp", addr) if err != nil { logger.Warn("port occupied, trying next", "port", port, "error", err) continue } listener.Close() return port, nil } return 0, fmt.Errorf("no available port found in range %d-%d", preferredPort, preferredPort+maxAttempts-1) }