package sms import ( "context" "encoding/json" "fmt" "strings" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/utils" dysms "github.com/alibabacloud-go/dysmsapi-20170525/v5/client" "github.com/alibabacloud-go/tea/dara" "go.uber.org/zap" ) type AliyunConfig struct { AccessKeyID string AccessKeySecret string Endpoint string SignName string LoginTemplateCode string } type AliyunProvider struct { client *dysms.Client signName string loginTemplateCode string log *zap.Logger } func NewAliyunProvider(cfg AliyunConfig, log *zap.Logger) (*AliyunProvider, error) { cfg.Endpoint = strings.TrimSpace(cfg.Endpoint) if cfg.Endpoint == "" { cfg.Endpoint = "dysmsapi.aliyuncs.com" } if strings.TrimSpace(cfg.AccessKeyID) == "" || strings.TrimSpace(cfg.AccessKeySecret) == "" || strings.TrimSpace(cfg.SignName) == "" || strings.TrimSpace(cfg.LoginTemplateCode) == "" { return nil, ErrProviderConfigInvalid } client, err := dysms.NewClient(&openapi.Config{ AccessKeyId: dara.String(strings.TrimSpace(cfg.AccessKeyID)), AccessKeySecret: dara.String(strings.TrimSpace(cfg.AccessKeySecret)), Endpoint: dara.String(cfg.Endpoint), }) if err != nil { return nil, err } return &AliyunProvider{ client: client, signName: strings.TrimSpace(cfg.SignName), loginTemplateCode: strings.TrimSpace(cfg.LoginTemplateCode), log: log, }, nil } func (p *AliyunProvider) SendLoginCode(ctx context.Context, phone string, code string) error { return p.SendTemplate(ctx, TemplateRequest{ Phone: phone, TemplateCode: p.loginTemplateCode, Params: map[string]string{"code": code}, }) } // SendTemplate 发送已报备的模板短信。req.SignName 为空时回退到 Provider 默认签名。 // 模板无变量时按阿里云要求传空 JSON,避免拼接文案导致模板不匹配。 func (p *AliyunProvider) SendTemplate(ctx context.Context, req TemplateRequest) error { phone := strings.TrimSpace(req.Phone) if phone == "" { return ErrProviderConfigInvalid } signName := strings.TrimSpace(req.SignName) if signName == "" { signName = p.signName } templateCode := strings.TrimSpace(req.TemplateCode) if templateCode == "" { return ErrProviderConfigInvalid } params := "{}" if len(req.Params) > 0 { raw, err := json.Marshal(req.Params) if err != nil { return err } params = string(raw) } apiReq := &dysms.SendSmsRequest{} apiReq.SetPhoneNumbers(phone) apiReq.SetSignName(signName) apiReq.SetTemplateCode(templateCode) apiReq.SetTemplateParam(params) resp, err := p.client.SendSmsWithContext(ctx, apiReq, &dara.RuntimeOptions{}) if err != nil { return fmt.Errorf("aliyun sms send failed: %w", err) } if resp == nil || resp.Body == nil { return fmt.Errorf("aliyun sms empty response") } resultCode := dara.StringValue(resp.Body.Code) if resultCode != "OK" { resultMessage := dara.StringValue(resp.Body.Message) providerErr := &ProviderError{Code: resultCode, Message: resultMessage} if resultCode == "isv.BUSINESS_LIMIT_CONTROL" { providerErr.Err = ErrProviderRateLimited } return providerErr } if p.log != nil { p.log.Debug( "短信发送成功", zap.String("phone", maskPhone(phone)), zap.String("template_code", templateCode), zap.String("provider_request_id", dara.StringValue(resp.Body.RequestId)), ) } return nil } func maskPhone(phone string) string { if len(phone) < 7 { return phone } return phone[:3] + "****" + phone[len(phone)-4:] }