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 }