355 lines
11 KiB
Go
355 lines
11 KiB
Go
package utils
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"license-manager/internal/config"
|
|
"license-manager/pkg/cache"
|
|
|
|
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
|
dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v5/client"
|
|
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
|
"github.com/alibabacloud-go/tea/tea"
|
|
)
|
|
|
|
// 短信相关错误定义
|
|
var (
|
|
ErrSMSCodeNotExist = errors.New("sms_code_not_exist")
|
|
ErrSMSCodeExpired = errors.New("sms_code_expired")
|
|
ErrSMSRateLimited = errors.New("sms_rate_limited")
|
|
ErrSMSSendFailed = errors.New("sms_send_failed")
|
|
ErrSMSCodeIncorrect = errors.New("sms_code_incorrect")
|
|
)
|
|
|
|
// 默认短信模板常量
|
|
const (
|
|
DefaultTemplateRegister = "SMS_330275014"
|
|
DefaultTemplateResetPwd = "SMS_330275015"
|
|
DefaultTemplateLogin = "SMS_330275016"
|
|
DefaultTemplateCurrentPhone = "SMS_330275017" // 当前手机号验证码
|
|
DefaultTemplateNewPhone = "SMS_330275018" // 新手机号验证码
|
|
)
|
|
|
|
// 缓存键常量
|
|
const (
|
|
SMSCodeKeyPrefix = "sms:code"
|
|
SMSLimitKeyPrefix = "sms:limit"
|
|
SMSCodeExpireTime = 5 * time.Minute
|
|
SMSLimitTimeWindow = time.Hour
|
|
SMSLimitMaxCount = 25
|
|
)
|
|
|
|
// SMSService 短信服务接口
|
|
type SMSService interface {
|
|
SendVerificationCode(ctx context.Context, phone, phoneCountryCode, templateCode string) error
|
|
VerifyCode(ctx context.Context, phone, phoneCountryCode, code string) (bool, error)
|
|
IsRateLimited(ctx context.Context, phone, phoneCountryCode string) bool
|
|
SendRegisterCode(ctx context.Context, phone, phoneCountryCode string) error
|
|
SendResetPwdCode(ctx context.Context, phone, phoneCountryCode string) error
|
|
SendLoginCode(ctx context.Context, phone, phoneCountryCode string) error
|
|
SendCurrentPhoneCode(ctx context.Context, phone, phoneCountryCode string) error
|
|
SendNewPhoneCode(ctx context.Context, phone, phoneCountryCode string) error
|
|
}
|
|
|
|
// smsService 短信服务实现
|
|
type smsService struct {
|
|
config *config.SMSConfig
|
|
templates *config.SMSTemplates
|
|
cache cache.Cache
|
|
logger interface{} // 支持不同类型的logger
|
|
}
|
|
|
|
// NewSMSService 创建SMS服务实例
|
|
func NewSMSService(cfg *config.SMSConfig, cache cache.Cache, logger interface{}) (SMSService, error) {
|
|
if !cfg.Enabled {
|
|
return &disabledSMSService{}, nil
|
|
}
|
|
|
|
// 验证配置有效性
|
|
if cfg.AccessKeyID == "" || cfg.AccessKeySecret == "" {
|
|
return nil, fmt.Errorf("SMS service configuration is invalid: AccessKeyID and AccessKeySecret are required")
|
|
}
|
|
|
|
if cfg.SignName == "" {
|
|
return nil, fmt.Errorf("SMS service configuration is invalid: SignName is required")
|
|
}
|
|
|
|
return &smsService{
|
|
config: cfg,
|
|
templates: &cfg.Templates,
|
|
cache: cache,
|
|
logger: logger,
|
|
}, nil
|
|
}
|
|
|
|
// disabledSMSService 禁用的SMS服务
|
|
type disabledSMSService struct{}
|
|
|
|
func (s *disabledSMSService) SendVerificationCode(ctx context.Context, phone, phoneCountryCode, templateCode string) error {
|
|
return fmt.Errorf("SMS service is disabled")
|
|
}
|
|
|
|
func (s *disabledSMSService) VerifyCode(ctx context.Context, phone, phoneCountryCode, code string) (bool, error) {
|
|
return false, fmt.Errorf("SMS service is disabled")
|
|
}
|
|
|
|
func (s *disabledSMSService) IsRateLimited(ctx context.Context, phone, phoneCountryCode string) bool {
|
|
return true
|
|
}
|
|
|
|
func (s *disabledSMSService) SendRegisterCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
return fmt.Errorf("SMS service is disabled")
|
|
}
|
|
|
|
func (s *disabledSMSService) SendResetPwdCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
return fmt.Errorf("SMS service is disabled")
|
|
}
|
|
|
|
func (s *disabledSMSService) SendLoginCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
return fmt.Errorf("SMS service is disabled")
|
|
}
|
|
|
|
func (s *disabledSMSService) SendCurrentPhoneCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
return fmt.Errorf("SMS service is disabled")
|
|
}
|
|
|
|
func (s *disabledSMSService) SendNewPhoneCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
return fmt.Errorf("SMS service is disabled")
|
|
}
|
|
|
|
// createSMSClient 创建阿里云短信客户端
|
|
// TODO: 实现阿里云SMS客户端创建,需要添加依赖:
|
|
// github.com/alibabacloud-go/dysmsapi-20170525/v5/client
|
|
// github.com/alibabacloud-go/darabonba-openapi/v2/client
|
|
// github.com/alibabacloud-go/tea-utils/v2/service
|
|
// github.com/alibabacloud-go/tea/tea
|
|
func createSMSClient(accessKeyID, accessKeySecret, regionID, endpoint string) (interface{}, error) {
|
|
// 暂时返回nil,等待依赖添加
|
|
return nil, fmt.Errorf("阿里云SMS客户端未实现,请添加相关依赖")
|
|
}
|
|
|
|
// SendVerificationCode 发送验证码
|
|
func (s *smsService) SendVerificationCode(ctx context.Context, phone, phoneCountryCode, templateCode string) error {
|
|
// 检查频率限制
|
|
if s.IsRateLimited(ctx, phone, phoneCountryCode) {
|
|
return ErrSMSRateLimited
|
|
}
|
|
|
|
// 生成6位随机验证码
|
|
code := generateVerificationCode()
|
|
|
|
// 构造完整手机号
|
|
fullPhone := phoneCountryCode + phone
|
|
if phoneCountryCode == "" {
|
|
fullPhone = "+86" + phone
|
|
}
|
|
|
|
// 送短信 - 需要阿里云SMS SDK
|
|
err := s.sendSMS(fullPhone, templateCode, code)
|
|
if err != nil {
|
|
fmt.Printf("Failed to send SMS to %s: %v", fullPhone, err)
|
|
// 返回包含详细错误信息的错误,这样上层可以获取具体错误原因
|
|
return fmt.Errorf("SMS send failed: %w", err)
|
|
}
|
|
|
|
// 缓存验证码
|
|
cacheKey := fmt.Sprintf("%s:%s:%s", SMSCodeKeyPrefix, phoneCountryCode, phone)
|
|
err = s.cache.Set(ctx, cacheKey, code, SMSCodeExpireTime)
|
|
if err != nil {
|
|
fmt.Printf("Failed to cache verification code: %v", err)
|
|
}
|
|
|
|
// 记录发送日志
|
|
fmt.Printf("SMS sent successfully to %s with template %s, code: %s", fullPhone, templateCode, code)
|
|
|
|
return nil
|
|
}
|
|
|
|
// sendSMS 调用阿里云API发送短信
|
|
func (s *smsService) sendSMS(phone, templateCode, code string) error {
|
|
// 创建阿里云SMS客户端
|
|
client, err := s.createSMSClient()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SMS client: %w", err)
|
|
}
|
|
|
|
// 构建发送短信请求
|
|
sendSmsRequest := &dysmsapi20170525.SendSmsRequest{
|
|
SignName: tea.String(s.config.SignName),
|
|
TemplateCode: tea.String(templateCode),
|
|
PhoneNumbers: tea.String(phone),
|
|
TemplateParam: tea.String(fmt.Sprintf(`{"code":"%s"}`, code)),
|
|
}
|
|
|
|
// 创建运行时选项
|
|
runtime := &util.RuntimeOptions{}
|
|
|
|
// 发送短信
|
|
resp, err := client.SendSmsWithOptions(sendSmsRequest, runtime)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send SMS: %w", err)
|
|
}
|
|
|
|
// 检查响应结果
|
|
if resp.Body == nil {
|
|
return fmt.Errorf("SMS response body is nil")
|
|
}
|
|
|
|
// 检查返回码
|
|
if tea.StringValue(resp.Body.Code) != "OK" {
|
|
return fmt.Errorf("SMS send failed, code: %s, message: %s",
|
|
tea.StringValue(resp.Body.Code),
|
|
tea.StringValue(resp.Body.Message))
|
|
}
|
|
|
|
// 记录成功发送的日志
|
|
fmt.Printf("SMS sent successfully, BizId: %s, Code: %s\n",
|
|
tea.StringValue(resp.Body.BizId),
|
|
tea.StringValue(resp.Body.Code))
|
|
|
|
return nil
|
|
}
|
|
|
|
// createSMSClient 创建阿里云SMS客户端
|
|
func (s *smsService) createSMSClient() (*dysmsapi20170525.Client, error) {
|
|
config := &openapi.Config{
|
|
AccessKeyId: tea.String(s.config.AccessKeyID),
|
|
AccessKeySecret: tea.String(s.config.AccessKeySecret),
|
|
}
|
|
|
|
// 设置地域和端点
|
|
if s.config.RegionID != "" {
|
|
config.RegionId = tea.String(s.config.RegionID)
|
|
} else {
|
|
config.RegionId = tea.String("cn-hangzhou")
|
|
}
|
|
|
|
if s.config.Endpoint != "" {
|
|
config.Endpoint = tea.String(s.config.Endpoint)
|
|
} else {
|
|
config.Endpoint = tea.String("dysmsapi.aliyuncs.com")
|
|
}
|
|
|
|
client, err := dysmsapi20170525.NewClient(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// VerifyCode 验证验证码
|
|
func (s *smsService) VerifyCode(ctx context.Context, phone, phoneCountryCode, code string) (bool, error) {
|
|
cacheKey := fmt.Sprintf("%s:%s:%s", SMSCodeKeyPrefix, phoneCountryCode, phone)
|
|
|
|
storedCode, err := s.cache.Get(ctx, cacheKey)
|
|
if err != nil {
|
|
if err == cache.ErrCacheMiss {
|
|
return false, ErrSMSCodeNotExist
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
// 验证成功后删除验证码(一次性使用)
|
|
if storedCode == code {
|
|
s.cache.Del(ctx, cacheKey)
|
|
return true, nil
|
|
}
|
|
|
|
return false, ErrSMSCodeIncorrect
|
|
}
|
|
|
|
// IsRateLimited 检查是否被频率限制
|
|
func (s *smsService) IsRateLimited(ctx context.Context, phone, phoneCountryCode string) bool {
|
|
limitKey := fmt.Sprintf("%s:%s:%s", SMSLimitKeyPrefix, phoneCountryCode, phone)
|
|
|
|
// 使用Incr实现简单的频率限制
|
|
count, err := s.cache.Incr(ctx, limitKey)
|
|
if err != nil {
|
|
// 缓存错误时允许请求通过(降级策略)
|
|
return false
|
|
}
|
|
|
|
// 首次发送,设置过期时间
|
|
if count == 1 {
|
|
s.cache.Expire(ctx, limitKey, SMSLimitTimeWindow)
|
|
}
|
|
|
|
// 每小时最多5次
|
|
return count > SMSLimitMaxCount
|
|
}
|
|
|
|
// generateVerificationCode 生成6位随机验证码
|
|
func generateVerificationCode() string {
|
|
rand.Seed(time.Now().UnixNano())
|
|
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
|
return code
|
|
}
|
|
|
|
// GetTemplateCode 获取模板代码:优先使用配置,否则使用默认值
|
|
func (s *smsService) GetTemplateCode(templateType string) string {
|
|
switch templateType {
|
|
case "register":
|
|
if s.templates.Register != "" {
|
|
return s.templates.Register
|
|
}
|
|
return DefaultTemplateRegister
|
|
case "reset_pwd":
|
|
if s.templates.ResetPwd != "" {
|
|
return s.templates.ResetPwd
|
|
}
|
|
return DefaultTemplateResetPwd
|
|
case "login":
|
|
if s.templates.Login != "" {
|
|
return s.templates.Login
|
|
}
|
|
return DefaultTemplateLogin
|
|
case "current_phone":
|
|
if s.templates.CurrentPhone != "" {
|
|
return s.templates.CurrentPhone
|
|
}
|
|
return DefaultTemplateCurrentPhone
|
|
case "new_phone":
|
|
if s.templates.NewPhone != "" {
|
|
return s.templates.NewPhone
|
|
}
|
|
return DefaultTemplateNewPhone
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// SendRegisterCode 发送注册验证码
|
|
func (s *smsService) SendRegisterCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
templateCode := s.GetTemplateCode("register")
|
|
return s.SendVerificationCode(ctx, phone, phoneCountryCode, templateCode)
|
|
}
|
|
|
|
// SendResetPwdCode 发送重置密码验证码
|
|
func (s *smsService) SendResetPwdCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
templateCode := s.GetTemplateCode("reset_pwd")
|
|
return s.SendVerificationCode(ctx, phone, phoneCountryCode, templateCode)
|
|
}
|
|
|
|
// SendLoginCode 发送登录验证码
|
|
func (s *smsService) SendLoginCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
templateCode := s.GetTemplateCode("login")
|
|
return s.SendVerificationCode(ctx, phone, phoneCountryCode, templateCode)
|
|
}
|
|
|
|
// SendCurrentPhoneCode 发送当前手机号验证码
|
|
func (s *smsService) SendCurrentPhoneCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
templateCode := s.GetTemplateCode("current_phone")
|
|
return s.SendVerificationCode(ctx, phone, phoneCountryCode, templateCode)
|
|
}
|
|
|
|
// SendNewPhoneCode 发送新手机号验证码
|
|
func (s *smsService) SendNewPhoneCode(ctx context.Context, phone, phoneCountryCode string) error {
|
|
templateCode := s.GetTemplateCode("new_phone")
|
|
return s.SendVerificationCode(ctx, phone, phoneCountryCode, templateCode)
|
|
}
|