Files
iotlcm/internal/sms/sms.go
T
MiFaZhan 4fb1016977 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
2026-10-22 14:21:47 +08:00

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
}