You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/api/sms/send.go

112 lines
2.8 KiB
Go

package sms
import (
"fmt"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/sms_providers"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/contrib/limiter"
"gorm.io/gorm"
)
// SendCodeRequest 发送验证码请求
type SendCodeRequest struct {
Phone string `json:"phone"`
Region string `json:"region"`
Purpose string `json:"purpose"`
TemplateID string `json:"template_id"`
}
// SendCodeResponse 发送验证码响应
type SendCodeResponse struct {
ID uint `json:"id"`
Phone string `json:"phone"`
ExpiresAt time.Time `json:"expires_at"`
Interval int `json:"interval"` // 下次可发送间隔(秒)
}
var _ = Router.Post("/", vigo.SkipBefore, limiter.NewAdvancedRequestLimiter(time.Minute*3, 5, time.Second*30), sendCode)
// SendCode 发送验证码
func sendCode(x *vigo.X) (any, error) {
req := &SendCodeRequest{}
err := x.Parse(req)
if err != nil {
return nil, err
}
// 1. 验证手机号
normalizedPhone := utils.NormalizePhoneNumber(req.Phone)
if !utils.ValidatePhoneNumber(normalizedPhone) {
return nil, fmt.Errorf("invalid phone number: %s", req.Phone)
}
// 2. 检查区域配置
provider, err := sms_providers.GetSMSProvider(req.Region)
if err != nil {
return nil, err
}
// 3. 检查发送频率限制
if err := CheckSendInterval(normalizedPhone, req.Region, req.Purpose); err != nil {
return nil, err
}
// 4. 检查每日发送次数限制
if err := CheckDailyLimit(normalizedPhone, req.Region); err != nil {
return nil, err
}
// 5. 生成验证码
code, err := utils.GenerateCode(cfg.Config.SMS.Global.CodeLength)
if err != nil {
return nil, fmt.Errorf("failed to generate code: %w", err)
}
// 6. 创建验证码记录
smsCode := &models.SMSCode{
Phone: normalizedPhone,
Code: code,
Region: req.Region,
Purpose: req.Purpose,
Status: models.CodeStatusPending,
ExpiresAt: time.Now().Add(cfg.Config.SMS.Global.CodeExpiry),
}
// 7. 发送短信
sendReq := &sms_providers.SendSMSRequest{
Phone: normalizedPhone,
TemplateID: req.TemplateID,
Region: req.Region,
Purpose: req.Purpose,
Params: map[string]string{
"code": code,
},
}
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
err := tx.Create(smsCode).Error
if err != nil {
return err
}
smsID, err := provider.SendSMS(x.Context(), sendReq)
LogSMS(normalizedPhone, req.Region, provider.GetName(), smsID, err)
if err != nil || smsID == "" {
return fmt.Errorf("failed to send sms: %v", err)
}
return nil
})
if err != nil {
return nil, err
}
return &SendCodeResponse{
ID: smsCode.ID,
Phone: normalizedPhone,
ExpiresAt: smsCode.ExpiresAt,
Interval: int(cfg.Config.SMS.Global.SendInterval.Seconds()),
}, nil
}