Files
iotlcm/internal/sms/config.go
T
2026-10-29 14:17:14 +08:00

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
}