voucher/internal/pkg/sms/aliyun.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, err := s.client.SendSms(request)
if err != nil {
lastError = fmt.Errorf("发送短信失败 (尝试 %d/%d): %w", i+1, s.retryTime+1, err)
continue
}
if response.Code != "OK" {
lastError = fmt.Errorf("发送短信失败 (尝试 %d/%d): 代码=%s, 消息=%s",
i+1, s.retryTime+1, response.Code, response.Message)
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})
}