4fb1016977
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
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
package sms
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
ServiceAliyun = "1"
|
|
ServiceTencent = "2"
|
|
)
|
|
|
|
var (
|
|
ErrProviderNotFound = errors.New("未选择有效的短信发送供应商")
|
|
ErrInvalidRequest = errors.New("短信请求参数不正确")
|
|
)
|
|
|
|
// Config contains the provider credentials and template configuration.
|
|
// serviceType: 1=Aliyun, 2=Tencent Cloud.
|
|
type Config struct {
|
|
ServiceType string `json:"serviceType" yaml:"serviceType"`
|
|
RegionID string `json:"regionId" yaml:"regionId"`
|
|
AccessKeyID string `json:"accessKeyId" yaml:"accessKeyId"`
|
|
AccessKeySecret string `json:"accessKeySecret" yaml:"accessKeySecret"`
|
|
SDKAppID string `json:"sdkAppId" yaml:"sdkAppId"`
|
|
SignName string `json:"signName" yaml:"signName"`
|
|
TemplateID string `json:"templateId" yaml:"templateId"`
|
|
}
|
|
|
|
type Message struct {
|
|
PhoneNumber string `json:"phoneNumber"`
|
|
MsgContent string `json:"msgContent"`
|
|
}
|
|
|
|
type Result struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data map[string]any `json:"data,omitempty"`
|
|
}
|
|
|
|
type Provider interface {
|
|
Send(context.Context, Config, Message) (Result, error)
|
|
}
|
|
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
providers map[string]Provider
|
|
defaultConfig Config
|
|
}
|
|
|
|
// NewRegistry 创建一个无默认配置的注册表。
|
|
func NewRegistry() *Registry {
|
|
return &Registry{providers: make(map[string]Provider)}
|
|
}
|
|
|
|
// NewRegistryWithDefault 创建一个带默认配置的注册表。调用方请求中未显式提供的字段
|
|
// 将回退到 defaultConfig(例如供应商凭证与签名由短信服务自身环境变量注入)。
|
|
func NewRegistryWithDefault(dc Config) *Registry {
|
|
return &Registry{providers: make(map[string]Provider), defaultConfig: dc}
|
|
}
|
|
|
|
func (r *Registry) Register(serviceType string, provider Provider) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.providers[serviceType] = provider
|
|
}
|
|
|
|
func (r *Registry) Send(ctx context.Context, config Config, message Message) (Result, error) {
|
|
// 回退默认值:请求中未提供的字段使用短信服务自身的默认配置。
|
|
config = mergeConfig(r.defaultConfig, config)
|
|
if err := Validate(config, message); err != nil {
|
|
return Result{Code: 400, Message: err.Error()}, err
|
|
}
|
|
|
|
r.mu.RLock()
|
|
provider, ok := r.providers[config.ServiceType]
|
|
r.mu.RUnlock()
|
|
if !ok {
|
|
return Result{Code: 400, Message: ErrProviderNotFound.Error()}, ErrProviderNotFound
|
|
}
|
|
return provider.Send(ctx, config, message)
|
|
}
|
|
|
|
func Validate(config Config, message Message) error {
|
|
missing := make([]string, 0, 6)
|
|
if strings.TrimSpace(config.ServiceType) == "" {
|
|
missing = append(missing, "config.serviceType")
|
|
}
|
|
if strings.TrimSpace(config.AccessKeyID) == "" {
|
|
missing = append(missing, "config.accessKeyId")
|
|
}
|
|
if strings.TrimSpace(config.AccessKeySecret) == "" {
|
|
missing = append(missing, "config.accessKeySecret")
|
|
}
|
|
if strings.TrimSpace(config.SignName) == "" {
|
|
missing = append(missing, "config.signName")
|
|
}
|
|
if strings.TrimSpace(config.TemplateID) == "" {
|
|
missing = append(missing, "config.templateId")
|
|
}
|
|
if strings.TrimSpace(message.PhoneNumber) == "" {
|
|
missing = append(missing, "params.phoneNumber")
|
|
}
|
|
if config.ServiceType == ServiceTencent && strings.TrimSpace(config.SDKAppID) == "" {
|
|
missing = append(missing, "config.sdkAppId")
|
|
}
|
|
if len(missing) != 0 {
|
|
return fmt.Errorf("%w: 缺少 %s", ErrInvalidRequest, strings.Join(missing, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mergeConfig 用 defaultConfig 填充 config 中的空字段,调用方显式值优先。
|
|
func mergeConfig(defaultConfig, config Config) Config {
|
|
if config.ServiceType == "" {
|
|
config.ServiceType = defaultConfig.ServiceType
|
|
}
|
|
if config.RegionID == "" {
|
|
config.RegionID = defaultConfig.RegionID
|
|
}
|
|
if config.AccessKeyID == "" {
|
|
config.AccessKeyID = defaultConfig.AccessKeyID
|
|
}
|
|
if config.AccessKeySecret == "" {
|
|
config.AccessKeySecret = defaultConfig.AccessKeySecret
|
|
}
|
|
if config.SDKAppID == "" {
|
|
config.SDKAppID = defaultConfig.SDKAppID
|
|
}
|
|
if config.SignName == "" {
|
|
config.SignName = defaultConfig.SignName
|
|
}
|
|
if config.TemplateID == "" {
|
|
config.TemplateID = defaultConfig.TemplateID
|
|
}
|
|
return config
|
|
}
|
|
|
|
func SplitAndTrim(value, separator string) []string {
|
|
parts := strings.Split(value, separator)
|
|
result := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
if item := strings.TrimSpace(part); item != "" {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|