package sms import ( "context" "encoding/json" "errors" "fmt" "strings" "sync" ) const ( ServiceAliyun = "1" ServiceTencent = "2" ) var ( ErrProviderNotFound = errors.New("未选择有效的短信发送供应商") ErrInvalidRequest = errors.New("短信请求参数不正确") ErrSceneNotConfigured = errors.New("短信场景未配置或已停用") ) // Config contains the provider credentials and template configuration. // serviceType: 1=Aliyun, 2=Tencent Cloud. // // Scene 为可选业务场景码(如 bind_phone)。当 Scene 非空时,短信服务会按该场景从数据库 // 配置表读取模板ID/签名等配置并回填到本次请求,调用方无需关心具体模板。 type Config struct { Scene string `json:"scene" yaml:"scene"` 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 scenes SceneProvider } // NewRegistry 创建一个无默认配置的注册表。 func NewRegistry() *Registry { return &Registry{providers: make(map[string]Provider)} } // NewRegistryWithDefault 创建一个带默认配置的注册表。调用方请求中未显式提供的字段 // 将回退到 defaultConfig(例如供应商凭证与签名由短信服务自身环境变量注入)。 // scenes 为可选的场景配置提供方(按 scene 从数据库读取模板配置),可为 nil。 func NewRegistryWithDefault(dc Config, scenes SceneProvider) *Registry { return &Registry{providers: make(map[string]Provider), defaultConfig: dc, scenes: scenes} } 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) { // 按业务场景加载模板配置(来自数据库),实现“改配置不改代码、不重启服务”。 if scene := strings.TrimSpace(config.Scene); scene != "" { sceneCfg, err := r.loadSceneConfig(ctx, scene) if err != nil { if errors.Is(err, ErrSceneNotConfigured) { return Result{Code: 400, Message: err.Error()}, err } return Result{Code: 500, Message: "查询短信场景配置失败: " + err.Error()}, err } config = mergeSceneConfig(sceneCfg, config) message.MsgContent = renderSceneMsgContent(sceneCfg.MsgContentTemplate, message.MsgContent) } // 回退默认值:请求中未提供的字段使用短信服务自身的默认配置。 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) } // loadSceneConfig 按场景码加载模板配置,并校验场景已启用。 func (r *Registry) loadSceneConfig(ctx context.Context, scene string) (SceneConfig, error) { if r.scenes == nil { return SceneConfig{}, fmt.Errorf("%w: %s", ErrSceneNotConfigured, scene) } cfg, err := r.scenes.Lookup(ctx, scene) if err != nil { return SceneConfig{}, err } if cfg == nil || cfg.Status != sceneStatusEnabled { return SceneConfig{}, fmt.Errorf("%w: %s", ErrSceneNotConfigured, scene) } return *cfg, nil } // mergeSceneConfig 用场景配置填充请求中未显式提供的字段,调用方显式值优先。 func mergeSceneConfig(scene SceneConfig, config Config) Config { if strings.TrimSpace(config.ServiceType) == "" { config.ServiceType = scene.ServiceType } if strings.TrimSpace(config.SignName) == "" { config.SignName = scene.SignName } if strings.TrimSpace(config.TemplateID) == "" { config.TemplateID = scene.TemplateID } return config } // renderSceneMsgContent 按场景内容模板渲染短信参数;模板为空时直接使用调用方内容。 // 支持 {content}(原样)与 {contentJson}(JSON 转义)两种占位符。 func renderSceneMsgContent(template string, content string) string { template = strings.TrimSpace(template) if template == "" { return content } contentJSON, _ := json.Marshal(content) replacer := strings.NewReplacer( "{content}", content, "{contentJson}", string(contentJSON), ) return replacer.Replace(template) } 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 }