146 lines
3.7 KiB
Go
146 lines
3.7 KiB
Go
package sms
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
|
|
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
|
|
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
|
)
|
|
|
|
// Config 阿里云短信配置
|
|
type Config struct {
|
|
AccessKeyID string `json:"access_key_id" yaml:"access_key_id"`
|
|
AccessKeySecret string `json:"access_key_secret" yaml:"access_key_secret"`
|
|
Endpoint string `json:"endpoint" yaml:"endpoint"`
|
|
SignName string `json:"sign_name" yaml:"sign_name"`
|
|
RetryTimes int `json:"retry_times" yaml:"retry_times"`
|
|
Timeout int `json:"timeout" yaml:"timeout"`
|
|
}
|
|
|
|
// Service 短信服务接口
|
|
type Service interface {
|
|
SendSMS(ctx context.Context, phoneNumbers []string, templateCode string, params map[string]string) error
|
|
SendVerificationCode(ctx context.Context, phoneNumber, templateCode, code string) error
|
|
}
|
|
|
|
// AliYunService 阿里云短信服务实现
|
|
type AliYunService struct {
|
|
config Config
|
|
client *dysmsapi.Client
|
|
retryTime int
|
|
timeout time.Duration
|
|
}
|
|
|
|
// NewService 创建阿里云短信服务实例
|
|
func NewService(config Config) (Service, error) {
|
|
// 创建配置
|
|
cfg := sdk.NewConfig()
|
|
//cfg.HttpTransport.MaxIdleConns = 100
|
|
//cfg.HttpTransport.IdleConnTimeout = 90 * time.Second
|
|
//cfg.HttpTransport.TLSHandshakeTimeout = 10 * time.Second
|
|
|
|
// 默认超时时间
|
|
if config.Timeout <= 0 {
|
|
config.Timeout = 10
|
|
}
|
|
|
|
// 默认重试次数
|
|
if config.RetryTimes <= 0 {
|
|
config.RetryTimes = 3
|
|
}
|
|
|
|
// 创建凭证
|
|
credential := credentials.NewAccessKeyCredential(
|
|
config.AccessKeyID,
|
|
config.AccessKeySecret,
|
|
)
|
|
|
|
// 创建客户端
|
|
client, err := dysmsapi.NewClientWithOptions(
|
|
config.Endpoint,
|
|
cfg,
|
|
credential,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("创建阿里云客户端失败: %w", err)
|
|
}
|
|
|
|
return &AliYunService{
|
|
config: config,
|
|
client: client,
|
|
retryTime: config.RetryTimes,
|
|
timeout: time.Duration(config.Timeout) * time.Second,
|
|
}, nil
|
|
}
|
|
|
|
// SendSMS 发送短信
|
|
func (s *AliYunService) SendSMS(ctx context.Context, phoneNumbers []string, templateCode string, params map[string]string) error {
|
|
|
|
if len(phoneNumbers) == 0 {
|
|
return fmt.Errorf("电话号码不能为空")
|
|
}
|
|
|
|
if templateCode == "" {
|
|
return fmt.Errorf("模板代码不能为空")
|
|
}
|
|
|
|
// 转换参数为JSON
|
|
paramJSON, err := json.Marshal(params)
|
|
if err != nil {
|
|
return fmt.Errorf("参数序列化失败: %w", err)
|
|
}
|
|
|
|
// 创建请求
|
|
request := dysmsapi.CreateSendSmsRequest()
|
|
request.PhoneNumbers = strings.Join(phoneNumbers, ",")
|
|
request.SignName = s.config.SignName
|
|
request.TemplateCode = templateCode
|
|
request.TemplateParam = string(paramJSON)
|
|
|
|
// 设置超时
|
|
ctx, cancel := context.WithTimeout(ctx, s.timeout)
|
|
defer cancel()
|
|
|
|
// 执行发送(带重试)
|
|
var lastError error
|
|
for i := 0; i <= s.retryTime; i++ {
|
|
|
|
if i > 0 {
|
|
// 指数退避重试
|
|
backoff := time.Duration(200*i) * time.Millisecond
|
|
select {
|
|
case <-time.After(backoff):
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
response, err2 := s.client.SendSms(request)
|
|
if err != nil {
|
|
lastError = fmt.Errorf("发送短信失败 (尝试 %d/%d): %w", i+1, s.retryTime+1, err2)
|
|
continue
|
|
}
|
|
|
|
if response.Code != "OK" {
|
|
lastError = fmt.Errorf("发送短信失败 (尝试 %d/%d): 代码=%s, 消息=%s,请求=%s",
|
|
i+1, s.retryTime+1, response.Code, response.Message, request.TemplateParam)
|
|
continue
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return lastError
|
|
}
|
|
|
|
// SendVerificationCode 发送验证码
|
|
func (s *AliYunService) SendVerificationCode(ctx context.Context, phoneNumber, templateCode, code string) error {
|
|
return s.SendSMS(ctx, []string{phoneNumber}, templateCode, map[string]string{"code": code})
|
|
}
|