package config 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" ) // Config 应用配置 type Config struct { 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" 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" 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" 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 func DefaultConfig() *Config { // Use home dir for default paths homeDir, _ := os.UserHomeDir() nexDir := filepath.Join(homeDir, ".nex") return &Config{ Server: ServerConfig{ Port: 9826, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, }, Database: DatabaseConfig{ Path: filepath.Join(nexDir, "config.db"), MaxIdleConns: 10, MaxOpenConns: 100, ConnMaxLifetime: 1 * time.Hour, }, Log: LogConfig{ Level: "info", Path: filepath.Join(nexDir, "log"), MaxSize: 100, MaxBackups: 10, MaxAge: 30, Compress: true, }, } } // GetConfigDir 获取配置目录路径(~/.nex/) func GetConfigDir() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { return "", err } configDir := filepath.Join(homeDir, ".nex") if err := os.MkdirAll(configDir, 0755); err != nil { return "", err } return configDir, nil } // GetDBPath 获取数据库文件路径 func GetDBPath() (string, error) { configDir, err := GetConfigDir() if err != nil { return "", err } return filepath.Join(configDir, "config.db"), nil } // GetConfigPath 获取配置文件路径 func GetConfigPath() (string, error) { configDir, err := GetConfigDir() if err != nil { return "", err } 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) } // LoadConfigFromPath 从指定路径加载配置 func LoadConfigFromPath(configPath string) (*Config, error) { // 1. 创建 Viper 实例 v := viper.New() // 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) } // 9. 验证配置 if err := cfg.Validate(); err != nil { return nil, err } return cfg, nil } // SaveConfig saves config to YAML file func SaveConfig(cfg *Config) error { configPath, err := GetConfigPath() if err != nil { return appErrors.Wrap(appErrors.ErrInternal, err) } data, err := yaml.Marshal(cfg) if err != nil { return appErrors.Wrap(appErrors.ErrInternal, err) } // Ensure directory exists dir := filepath.Dir(configPath) if err := os.MkdirAll(dir, 0755); err != nil { return appErrors.Wrap(appErrors.ErrInternal, err) } return os.WriteFile(configPath, data, 0644) } // Validate validates the config func (c *Config) Validate() error { validate := validator.New() if err := validate.Struct(c); err != nil { return appErrors.WithMessage(appErrors.ErrInvalidRequest, fmt.Sprintf("配置验证失败: %v", err)) } 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() }