package sms import ( "errors" "fmt" "strings" "yunyan/lego/utils/mapstructure" ) // 短信服务商。Provider 留空按腾讯云处理(兼容加入阿里云之前已落库/落 yaml 的配置)。 const ( ProviderTencent = "tencent" ProviderAliyun = "aliyun" ) // 阿里云默认值:地域与模板参数名。 const ( defaultAliRegionId = "cn-hangzhou" defaultAliTemplateParam = "code" ) type Option func(*Options) type Options struct { Provider string // tencent(默认) | aliyun // 腾讯云 AppId string SecretId string SecretKey string // 阿里云 AccessKeyId string AccessKeySecret string RegionId string // 默认 cn-hangzhou;国际站按需 ap-southeast-1 TemplateParamName string // 模板变量名,默认 code(对应模板里的 ${code}) // 两家共用 SignName string Template1 string // 国内模板(腾讯 TemplateId / 阿里 TemplateCode) Template2 string // 国际/港澳台模板 } func SetProvider(v string) Option { return func(o *Options) { o.Provider = v } } func SetAppId(v string) Option { return func(o *Options) { o.AppId = v } } func SetSecretId(v string) Option { return func(o *Options) { o.SecretId = v } } func KeySecretKey(v string) Option { return func(o *Options) { o.SecretKey = v } } func SetAccessKeyId(v string) Option { return func(o *Options) { o.AccessKeyId = v } } func SetAccessKeySecret(v string) Option { return func(o *Options) { o.AccessKeySecret = v } } func SetRegionId(v string) Option { return func(o *Options) { o.RegionId = v } } func SetTemplateParamName(v string) Option { return func(o *Options) { o.TemplateParamName = v } } func SetSignName(v string) Option { return func(o *Options) { o.SignName = v } } func SetTemplate1(v string) Option { return func(o *Options) { o.Template1 = v } } func SetTemplate2(v string) Option { return func(o *Options) { o.Template2 = v } } func newOptionsByOption(opts ...Option) (Options, error) { options := Options{} for _, o := range opts { o(&options) } options.normalize() return options, options.validate() } // provider 归一化后的服务商(空 → 腾讯云)。 func (o Options) provider() string { if p := strings.ToLower(strings.TrimSpace(o.Provider)); p != "" { return p } return ProviderTencent } // normalize 填充可留空字段的默认值,并把服务商归一化成小写。 func (o *Options) normalize() { o.Provider = o.provider() if o.Provider == ProviderAliyun { if o.RegionId == "" { o.RegionId = defaultAliRegionId } if o.TemplateParamName == "" { o.TemplateParamName = defaultAliTemplateParam } } } // validate 校验发短信必需字段,按服务商分别校验。 func (o Options) validate() error { switch o.provider() { case ProviderTencent: if o.SecretId == "" { return errors.New("sms 配置缺失: SecretId 为空(腾讯云)") } if o.SecretKey == "" { return errors.New("sms 配置缺失: SecretKey 为空(腾讯云)") } case ProviderAliyun: if o.AccessKeyId == "" { return errors.New("sms 配置缺失: AccessKeyId 为空(阿里云)") } if o.AccessKeySecret == "" { return errors.New("sms 配置缺失: AccessKeySecret 为空(阿里云)") } if o.SignName == "" { return errors.New("sms 配置缺失: SignName 为空(阿里云签名必填)") } default: return fmt.Errorf("sms 配置错误: 未知服务商 Provider=%q(可选 %s / %s)", o.Provider, ProviderTencent, ProviderAliyun) } return nil } // newOptionsChecked 解析 map 配置并校验,不合法时返回 error 而非 panic。 // 启动(OnInit)与运行时热重载(Reload)共用:配置缺失只应让本子系统降级/本次重载失败,不该打挂进程。 func newOptionsChecked(config map[string]interface{}, opts ...Option) (Options, error) { options := Options{} if config != nil { mapstructure.Decode(config, &options) } for _, o := range opts { o(&options) } options.normalize() return options, options.validate() }