1
0

feat: 引入 Viper 实现多层配置管理

引入 Viper 配置管理框架,支持 CLI 参数、环境变量、配置文件和默认值四种配置方式。

主要变更:
- 引入 Viper、pflag、validator、mapstructure 依赖
- 实现配置优先级:CLI > ENV > File > Default
- 所有 13 个配置项支持 CLI 参数和环境变量
- 规范化命名:server.port → NEX_SERVER_PORT → --server-port
- 使用结构体验证器进行配置验证
- 添加配置摘要输出功能

新增能力:
- cli-config: 命令行参数配置支持
- env-config: 环境变量配置支持(符合 12-Factor App)
- config-priority: 配置优先级管理

修改能力:
- config-management: 扩展为多层配置源支持

使用示例:
  ./server --server-port 9000 --log-level debug
  export NEX_SERVER_PORT=9000 && ./server
  ./server --config /path/to/custom.yaml
This commit is contained in:
2026-04-20 18:04:42 +08:00
parent aea360bce8
commit cfb0edf802
15 changed files with 468 additions and 577 deletions

View File

@@ -4,8 +4,13 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-playground/validator/v10"
"github.com/mitchellh/mapstructure"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
appErrors "nex/backend/pkg/errors"
@@ -13,34 +18,34 @@ import (
// Config 应用配置
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Log LogConfig `yaml:"log"`
Server ServerConfig `yaml:"server" mapstructure:"server" validate:"required"`
Database DatabaseConfig `yaml:"database" mapstructure:"database" validate:"required"`
Log LogConfig `yaml:"log" mapstructure:"log" validate:"required"`
}
// ServerConfig 服务器配置
type ServerConfig struct {
Port int `yaml:"port"`
ReadTimeout time.Duration `yaml:"read_timeout"`
WriteTimeout time.Duration `yaml:"write_timeout"`
Port int `yaml:"port" mapstructure:"port" validate:"required,min=1,max=65535"`
ReadTimeout time.Duration `yaml:"read_timeout" mapstructure:"read_timeout" validate:"required"`
WriteTimeout time.Duration `yaml:"write_timeout" mapstructure:"write_timeout" validate:"required"`
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Path string `yaml:"path"`
MaxIdleConns int `yaml:"max_idle_conns"`
MaxOpenConns int `yaml:"max_open_conns"`
ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime"`
Path string `yaml:"path" mapstructure:"path" validate:"required"`
MaxIdleConns int `yaml:"max_idle_conns" mapstructure:"max_idle_conns" validate:"required,min=1"`
MaxOpenConns int `yaml:"max_open_conns" mapstructure:"max_open_conns" validate:"required,min=1"`
ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime" mapstructure:"conn_max_lifetime" validate:"required"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `yaml:"level"`
Path string `yaml:"path"`
MaxSize int `yaml:"max_size"`
MaxBackups int `yaml:"max_backups"`
MaxAge int `yaml:"max_age"`
Compress bool `yaml:"compress"`
Level string `yaml:"level" mapstructure:"level" validate:"required,oneof=debug info warn error"`
Path string `yaml:"path" mapstructure:"path" validate:"required"`
MaxSize int `yaml:"max_size" mapstructure:"max_size" validate:"required,min=1"`
MaxBackups int `yaml:"max_backups" mapstructure:"max_backups" validate:"required,min=0"`
MaxAge int `yaml:"max_age" mapstructure:"max_age" validate:"required,min=0"`
Compress bool `yaml:"compress" mapstructure:"compress"`
}
// DefaultConfig returns default config values
@@ -103,29 +108,143 @@ func GetConfigPath() (string, error) {
return filepath.Join(configDir, "config.yaml"), nil
}
// setupDefaults 设置默认配置值
func setupDefaults(v *viper.Viper) {
homeDir, _ := os.UserHomeDir()
nexDir := filepath.Join(homeDir, ".nex")
v.SetDefault("server.port", 9826)
v.SetDefault("server.read_timeout", "30s")
v.SetDefault("server.write_timeout", "30s")
v.SetDefault("database.path", filepath.Join(nexDir, "config.db"))
v.SetDefault("database.max_idle_conns", 10)
v.SetDefault("database.max_open_conns", 100)
v.SetDefault("database.conn_max_lifetime", "1h")
v.SetDefault("log.level", "info")
v.SetDefault("log.path", filepath.Join(nexDir, "log"))
v.SetDefault("log.max_size", 100)
v.SetDefault("log.max_backups", 10)
v.SetDefault("log.max_age", 30)
v.SetDefault("log.compress", true)
}
// setupFlags 定义和绑定 CLI 参数
func setupFlags(v *viper.Viper, flagSet *pflag.FlagSet) {
// 定义所有配置项的 CLI 参数
// 注意:这里不设置默认值,让 viper 的默认值生效
flagSet.Int("server-port", 0, "服务器端口")
flagSet.Duration("server-read-timeout", 0, "读超时")
flagSet.Duration("server-write-timeout", 0, "写超时")
flagSet.String("database-path", "", "数据库文件路径")
flagSet.Int("database-max-idle-conns", 0, "最大空闲连接数")
flagSet.Int("database-max-open-conns", 0, "最大打开连接数")
flagSet.Duration("database-conn-max-lifetime", 0, "连接最大生命周期")
flagSet.String("log-level", "", "日志级别debug/info/warn/error")
flagSet.String("log-path", "", "日志文件目录")
flagSet.Int("log-max-size", 0, "单个日志文件最大大小 MB")
flagSet.Int("log-max-backups", 0, "保留的旧日志文件最大数量")
flagSet.Int("log-max-age", 0, "保留旧日志文件的最大天数")
flagSet.Bool("log-compress", false, "是否压缩旧日志文件")
// 绑定所有 flag 到 viper
// 注意:必须在设置默认值之后绑定
v.BindPFlag("server.port", flagSet.Lookup("server-port"))
v.BindPFlag("server.read_timeout", flagSet.Lookup("server-read-timeout"))
v.BindPFlag("server.write_timeout", flagSet.Lookup("server-write-timeout"))
v.BindPFlag("database.path", flagSet.Lookup("database-path"))
v.BindPFlag("database.max_idle_conns", flagSet.Lookup("database-max-idle-conns"))
v.BindPFlag("database.max_open_conns", flagSet.Lookup("database-max-open-conns"))
v.BindPFlag("database.conn_max_lifetime", flagSet.Lookup("database-conn-max-lifetime"))
v.BindPFlag("log.level", flagSet.Lookup("log-level"))
v.BindPFlag("log.path", flagSet.Lookup("log-path"))
v.BindPFlag("log.max_size", flagSet.Lookup("log-max-size"))
v.BindPFlag("log.max_backups", flagSet.Lookup("log-max-backups"))
v.BindPFlag("log.max_age", flagSet.Lookup("log-max-age"))
v.BindPFlag("log.compress", flagSet.Lookup("log-compress"))
}
// setupEnv 绑定环境变量
func setupEnv(v *viper.Viper) {
v.SetEnvPrefix("NEX")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
}
// setupConfigFile 读取配置文件
func setupConfigFile(v *viper.Viper, configPath string) error {
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
// 尝试读取配置文件,如果不存在则忽略
if err := v.ReadInConfig(); err != nil {
if !os.IsNotExist(err) {
return appErrors.Wrap(appErrors.ErrInternal, err)
}
// 配置文件不存在,创建默认配置文件
if err := v.SafeWriteConfig(); err != nil {
// 忽略写入错误(可能目录已存在等)
return nil
}
}
return nil
}
// LoadConfig loads config from YAML file, creates default if not exists
func LoadConfig() (*Config, error) {
configPath, err := GetConfigPath()
if err != nil {
return nil, appErrors.Wrap(appErrors.ErrInternal, err)
}
return LoadConfigFromPath(configPath)
}
cfg := DefaultConfig()
// LoadConfigFromPath 从指定路径加载配置
func LoadConfigFromPath(configPath string) (*Config, error) {
// 1. 创建 Viper 实例
v := viper.New()
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
// Create default config file
if saveErr := SaveConfig(cfg); saveErr != nil {
return nil, appErrors.WithMessage(appErrors.ErrInternal, "创建默认配置失败")
}
return cfg, nil
}
// 2. 定义 CLI 参数
flagSet := pflag.NewFlagSet("config", pflag.ContinueOnError)
flagSet.String("config", configPath, "配置文件路径")
setupFlags(v, flagSet)
// 3. 解析 CLI 参数(忽略错误,因为可能没有参数)
flagSet.Parse(os.Args[1:])
// 4. 获取配置文件路径(可能被 --config 参数覆盖)
if configPathFlag, err := flagSet.GetString("config"); err == nil && configPathFlag != "" {
configPath = configPathFlag
}
// 5. 设置默认值
setupDefaults(v)
// 6. 绑定环境变量
setupEnv(v)
// 7. 读取配置文件
if err := setupConfigFile(v, configPath); err != nil {
return nil, err
}
// 8. 反序列化到结构体
cfg := &Config{}
if err := v.Unmarshal(cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
))); err != nil {
return nil, appErrors.Wrap(appErrors.ErrInternal, err)
}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, appErrors.Wrap(appErrors.ErrInternal, err)
// 9. 验证配置
if err := cfg.Validate(); err != nil {
return nil, err
}
return cfg, nil
@@ -154,18 +273,24 @@ func SaveConfig(cfg *Config) error {
// Validate validates the config
func (c *Config) Validate() error {
if c.Server.Port < 1 || c.Server.Port > 65535 {
return appErrors.WithMessage(appErrors.ErrInvalidRequest, fmt.Sprintf("无效的端口号: %d", c.Server.Port))
validate := validator.New()
if err := validate.Struct(c); err != nil {
return appErrors.WithMessage(appErrors.ErrInvalidRequest, fmt.Sprintf("配置验证失败: %v", err))
}
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
if !validLevels[c.Log.Level] {
return appErrors.WithMessage(appErrors.ErrInvalidRequest, fmt.Sprintf("无效的日志级别: %s", c.Log.Level))
}
if c.Database.Path == "" {
return appErrors.WithMessage(appErrors.ErrInvalidRequest, "数据库路径不能为空")
}
return nil
}
// PrintSummary 打印配置摘要
func (c *Config) PrintSummary() {
fmt.Println("\nAI Gateway 启动配置")
fmt.Println("==================")
fmt.Printf("服务器端口: %d\n", c.Server.Port)
fmt.Printf("数据库路径: %s\n", c.Database.Path)
fmt.Printf("日志级别: %s\n", c.Log.Level)
fmt.Println("\n配置来源:")
configPath, _ := GetConfigPath()
fmt.Printf(" 配置文件: %s\n", configPath)
fmt.Println(" 环境变量: 待统计")
fmt.Println(" CLI 参数: 待统计")
fmt.Println()
}