182 lines
6.3 KiB
Go
182 lines
6.3 KiB
Go
package sms
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"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) {
|
|
data, path, err := readDefaultConfigFile()
|
|
if err != nil {
|
|
return Config{}, "", err
|
|
}
|
|
if path == "" {
|
|
return Config{}, "", nil
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return Config{}, "", fmt.Errorf("解析配置文件 %s 失败: %w", path, err)
|
|
}
|
|
return cfg, path, nil
|
|
}
|
|
|
|
// readDefaultConfigFile 按候选路径查找并读取配置文件,返回文件内容与命中路径。
|
|
// 未命中任何候选文件时返回空内容与空路径(不视为错误);
|
|
// 显式指定 SMS_CONFIG_FILE 时任何失败都返回错误,避免静默回退。
|
|
func readDefaultConfigFile() ([]byte, string, error) {
|
|
paths, explicit := configFileCandidates()
|
|
for _, path := range paths {
|
|
info, statErr := os.Stat(path)
|
|
if statErr != nil {
|
|
if explicit {
|
|
return nil, "", fmt.Errorf("读取配置文件 %s 失败: %w", path, statErr)
|
|
}
|
|
continue
|
|
}
|
|
// Docker 在挂载源不存在时会自动创建同名目录;必须显式报错,
|
|
// 否则会静默回退到空凭证,表现为短信发送全部失败。
|
|
if info.IsDir() {
|
|
return nil, "", fmt.Errorf(
|
|
"配置文件 %s 是目录而非文件:宿主凭证文件可能不存在,Docker 自动创建了目录。"+
|
|
"请在服务器创建该文件后重新挂载", path)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if explicit {
|
|
return nil, "", fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
|
|
}
|
|
continue
|
|
}
|
|
return data, path, nil
|
|
}
|
|
if explicit {
|
|
return nil, "", fmt.Errorf("指定的配置文件不存在: %s", paths[0])
|
|
}
|
|
return nil, "", 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
|
|
}
|
|
|
|
// SceneDbConfig 场景模板配置库(表 t_sms_template_config)连接参数。
|
|
// DSN 为空表示未启用场景库能力:Send 收到带 scene 的请求时返回 ErrSceneNotConfigured。
|
|
type SceneDbConfig struct {
|
|
DSN string `yaml:"sceneDbDsn"`
|
|
CacheTTL string `yaml:"sceneDbCacheTtl"`
|
|
}
|
|
|
|
// DefaultSceneDbCacheTTL 场景配置缓存默认时长。
|
|
const DefaultSceneDbCacheTTL = 60 * time.Second
|
|
|
|
// TTL 返回解析后的缓存时长;非法或非正值回退默认值。
|
|
func (c SceneDbConfig) TTL() time.Duration {
|
|
if v, err := time.ParseDuration(strings.TrimSpace(c.CacheTTL)); err == nil && v > 0 {
|
|
return v
|
|
}
|
|
return DefaultSceneDbCacheTTL
|
|
}
|
|
|
|
// LoadSceneDbConfig 从默认配置文件读取场景库参数(与凭证同一份 config.yaml),环境变量可覆盖。
|
|
// 未命中配置文件不报错(场景库能力可选);解析失败或显式指定的文件不存在才返回错误。
|
|
func LoadSceneDbConfig() (SceneDbConfig, ConfigSource, error) {
|
|
data, path, err := readDefaultConfigFile()
|
|
if err != nil {
|
|
return SceneDbConfig{}, ConfigSource{}, err
|
|
}
|
|
sc := SceneDbConfig{}
|
|
if path != "" {
|
|
if err := yaml.Unmarshal(data, &sc); err != nil {
|
|
return SceneDbConfig{}, ConfigSource{}, fmt.Errorf("解析配置文件 %s 失败: %w", path, err)
|
|
}
|
|
}
|
|
envs := make([]string, 0, 2)
|
|
if v := strings.TrimSpace(os.Getenv("SMS_DB_DSN")); v != "" {
|
|
sc.DSN = v
|
|
envs = append(envs, "SMS_DB_DSN")
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("SMS_DB_CONFIG_CACHE_TTL")); v != "" {
|
|
sc.CacheTTL = v
|
|
envs = append(envs, "SMS_DB_CONFIG_CACHE_TTL")
|
|
}
|
|
return sc, ConfigSource{File: path, Env: envs}, nil
|
|
}
|