455 lines
15 KiB
Go
455 lines
15 KiB
Go
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)
|
|
}
|