chore: 初始开源发布
iotlcm-sms:独立短信 HTTP/gRPC 服务,支持阿里云与腾讯云。 - Go module 迁移至 git.5iot.com/iotlcm-public/iotlcm-sms - 补 Apache-2.0 LICENSE,重写 README(特性/配置/接口/部署/目录/测试) - 配置改为 config.example.yaml + SMS_* 环境变量注入,仓库不含真实凭证 - 清理内部标识(私有镜像仓库 / 公司名 / 客户签名 / 真实模板ID / 真实手机号) - 统一 LF 行尾并加 .gitattributes
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user