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,151 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user