// Package config 提供基于 viper 的应用配置加载,按 默认值 → 配置文件 → 环境变量 的优先级合并。 package config import ( "encoding/hex" "fmt" "strings" "github.com/spf13/viper" ) // Config 是应用全局配置的根结构。 type Config struct { Env string `mapstructure:"env"` DataDir string `mapstructure:"data_dir"` MasterKey []byte // 解码后的 32 字节 HTTP HTTPConfig `mapstructure:"http"` DB DBConfig `mapstructure:"db"` Bootstrap BootstrapConfig `mapstructure:"bootstrap"` } // HTTPConfig 描述 HTTP 服务监听参数。 type HTTPConfig struct { Addr string `mapstructure:"addr"` } // DBConfig 描述数据库连接参数。 type DBConfig struct { DSN string `mapstructure:"dsn"` } // BootstrapConfig 描述首次启动注入的管理员账号。 type BootstrapConfig struct { AdminEmail string `mapstructure:"admin_email"` AdminPassword string `mapstructure:"admin_password"` } // Load 从(按优先级递增)默认值 → 配置文件(如有)→ 环境变量 加载配置。 // configFile 可为空字符串,仅用环境变量。 func Load(configFile string) (*Config, error) { v := viper.New() // 默认值 v.SetDefault("env", "development") v.SetDefault("data_dir", "./data") v.SetDefault("http.addr", ":8080") if configFile != "" { v.SetConfigFile(configFile) if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("read config file: %w", err) } } v.SetEnvPrefix("APP") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() // 显式绑定无默认值的嵌套键,以便 Unmarshal 能从环境变量取值。 // (AutomaticEnv 仅对 v.Get* 直接查询生效;Unmarshal 只看 viper 已知的键集合。) for _, key := range []string{ "db.dsn", "bootstrap.admin_email", "bootstrap.admin_password", "master_key", } { if err := v.BindEnv(key); err != nil { return nil, fmt.Errorf("bind env %s: %w", key, err) } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } // MasterKey 是 hex 字符串,需要单独解码 rawKey := v.GetString("master_key") if rawKey == "" { return nil, fmt.Errorf("APP_MASTER_KEY required") } key, err := hex.DecodeString(rawKey) if err != nil { return nil, fmt.Errorf("APP_MASTER_KEY invalid hex: %w", err) } if len(key) != 32 { return nil, fmt.Errorf("APP_MASTER_KEY must be 32 bytes (got %d)", len(key)) } cfg.MasterKey = key if cfg.DB.DSN == "" { return nil, fmt.Errorf("APP_DB_DSN required") } return &cfg, nil }