package sms import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // 默认配置文件名。容器内固定挂载到 /app/config/config.yaml。 const defaultConfigFileName = "config.yaml" // ConfigSource 描述默认配置的实际来源,供启动日志排查。 type ConfigSource struct { File string // 实际加载的配置文件路径,空字符串表示未加载到文件 Env []string // 生效的环境变量名(覆盖了文件值或单独提供) } // Describe 返回可读的来源说明。 func (s ConfigSource) Describe() string { file := s.File if file == "" { file = "(无配置文件)" } env := "(无)" if len(s.Env) > 0 { env = strings.Join(s.Env, ",") } return fmt.Sprintf("file=%s env=%s", file, env) } // configFileCandidates 返回配置文件候选路径。 // SMS_CONFIG_FILE 显式指定时只使用该路径;否则依次尝试工作目录、config/ 子目录、容器固定挂载点。 func configFileCandidates() (paths []string, explicit bool) { if p := strings.TrimSpace(os.Getenv("SMS_CONFIG_FILE")); p != "" { return []string{p}, true } return []string{ defaultConfigFileName, filepath.Join("config", defaultConfigFileName), filepath.Join("/app/config", defaultConfigFileName), }, false } // LoadDefaultConfigFromFile 读取默认配置文件。 // 返回的第二个值为实际命中的路径;未命中时路径为空。 // 显式指定 SMS_CONFIG_FILE 但读取或解析失败时返回错误,避免静默使用空凭证。 func LoadDefaultConfigFromFile() (Config, string, error) { paths, explicit := configFileCandidates() for _, path := range paths { info, statErr := os.Stat(path) if statErr != nil { if explicit { return Config{}, "", fmt.Errorf("读取配置文件 %s 失败: %w", path, statErr) } continue } // Docker 在挂载源不存在时会自动创建同名目录;必须显式报错, // 否则会静默回退到空凭证,表现为短信发送全部失败。 if info.IsDir() { return Config{}, "", fmt.Errorf( "配置文件 %s 是目录而非文件:宿主凭证文件可能不存在,Docker 自动创建了目录。"+ "请在服务器创建该文件后重新挂载", path) } data, err := os.ReadFile(path) if err != nil { if explicit { return Config{}, "", fmt.Errorf("读取配置文件 %s 失败: %w", path, err) } continue } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return Config{}, "", fmt.Errorf("解析配置文件 %s 失败: %w", path, err) } return cfg, path, nil } if explicit { return Config{}, "", fmt.Errorf("指定的配置文件不存在: %s", paths[0]) } return Config{}, "", nil } // envOverrides 定义环境变量到配置字段的映射,环境变量优先于配置文件。 var envOverrides = []struct { name string apply func(*Config, string) }{ {"SMS_SERVICE_TYPE", func(c *Config, v string) { c.ServiceType = v }}, {"SMS_REGION_ID", func(c *Config, v string) { c.RegionID = v }}, {"SMS_ACCESS_KEY_ID", func(c *Config, v string) { c.AccessKeyID = v }}, {"SMS_ACCESS_KEY_SECRET", func(c *Config, v string) { c.AccessKeySecret = v }}, {"SMS_SDK_APP_ID", func(c *Config, v string) { c.SDKAppID = v }}, {"SMS_SIGN_NAME", func(c *Config, v string) { c.SignName = v }}, {"SMS_TEMPLATE_ID", func(c *Config, v string) { c.TemplateID = v }}, } // applyEnvOverrides 用环境变量覆盖配置文件中的值,返回实际生效的变量名。 func applyEnvOverrides(cfg *Config) []string { applied := make([]string, 0, len(envOverrides)) for _, item := range envOverrides { if v := strings.TrimSpace(os.Getenv(item.name)); v != "" { item.apply(cfg, v) applied = append(applied, item.name) } } return applied } // LoadDefaultConfig 加载默认供应商配置:配置文件提供默认值,环境变量覆盖。 // 短信服务自身持有全局凭证与签名,调用方请求中未显式提供的字段回退到这里的值。 func LoadDefaultConfig() (Config, ConfigSource, error) { cfg, path, err := LoadDefaultConfigFromFile() if err != nil { return Config{}, ConfigSource{}, err } envApplied := applyEnvOverrides(&cfg) if strings.TrimSpace(cfg.RegionID) == "" { cfg.RegionID = "cn-hangzhou" } return cfg, ConfigSource{File: path, Env: envApplied}, nil }