Files
lone-services/services/user/internal/logic/sendSmsLogic.go
T
2026-09-03 18:23:18 +08:00

142 lines
3.5 KiB
Go

package logic
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"strconv"
"time"
"lone-services/pkg/redis"
"lone-services/pkg/sms"
"lone-services/pkg/utils"
"lone-services/pkg/validate"
user "lone-services/rpc/user/pb"
"lone-services/services/user/internal/dao"
"lone-services/services/user/internal/svc"
"lone-services/services/user/validator"
"github.com/zeromicro/go-zero/core/logx"
)
type SendSmsLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLogic {
return &SendSmsLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *SendSmsLogic) SendSms(in *user.SendSmsReq) (*user.Response, error) {
var req validator.SendSmsValidator
if msg := validate.ValidateFromProto(in, &req); msg != utils.StringEmpty {
return outResponse(utils.ErrorParams, msg), nil
}
if l.svcCtx.SMS == nil {
return outResponse(utils.Fail, "短信服务未配置"), nil
}
kind, ok := smsSceneKind(req.Scene)
if !ok {
return outResponse(utils.ErrorParams, "短信场景不正确"), nil
}
freqKey := smsFreqRedisKey(req.Scene, req.Mobile)
okSet, err := redis.Client.SetNX(l.ctx, freqKey, "1", time.Duration(dao.SmsRateLimitSeconds)*time.Second).Result()
if err != nil {
l.Errorf("sms freq setnx: %v", err)
return failResponse(utils.Fail), nil
}
if !okSet {
return outResponse(utils.ErrorParams, "发送过于频繁,请稍后再试"), nil
}
code, err := genDigitCode(dao.SmsCodeLength)
if err != nil {
_ = redis.Client.Del(l.ctx, freqKey).Err()
l.Errorf("sms gen code: %v", err)
return failResponse(utils.Fail), nil
}
if err := l.svcCtx.SMS.SendByKind(req.Mobile, kind, map[string]string{"code": code}); err != nil {
_ = redis.Client.Del(l.ctx, freqKey).Err()
l.Errorf("sms send: %v", err)
return outResponse(utils.Fail, "短信发送失败"), nil
}
ttl := l.svcCtx.SMS.Timeout()
if ttl <= utils.NumberZero {
ttl = 600
}
codeKey := smsCodeRedisKey(req.Scene, req.Mobile)
if err := redis.Client.Set(l.ctx, codeKey, code, time.Duration(ttl)*time.Second).Err(); err != nil {
l.Errorf("sms cache code: %v", err)
return failResponse(utils.Fail), nil
}
return okResponse(nil), nil
}
func smsSceneKind(scene int32) (sms.TemplateKind, bool) {
switch scene {
case dao.SmsSceneLogin:
return sms.TemplateCode, true
case dao.SmsSceneResetPassword:
return sms.TemplatePassword, true
default:
return "", false
}
}
func smsCodeRedisKey(scene int32, mobile string) string {
return utils.CodeKey + strconv.Itoa(int(scene)) + ":" + mobile
}
func smsFreqRedisKey(scene int32, mobile string) string {
return "sms:freq:" + strconv.Itoa(int(scene)) + ":" + mobile
}
func verifyAndConsumeSmsCode(ctx context.Context, scene int32, mobile, code string) (bool, error) {
if code == utils.StringEmpty {
return false, nil
}
codeKey := smsCodeRedisKey(scene, mobile)
cached, err := redis.Client.Get(ctx, codeKey).Result()
if err == redis.Nil {
return false, nil
}
if err != nil {
return false, err
}
if cached != code {
return false, nil
}
if delErr := redis.Client.Del(ctx, codeKey).Err(); delErr != nil {
return false, delErr
}
return true, nil
}
func genDigitCode(length int) (string, error) {
if length <= utils.NumberZero {
return utils.StringEmpty, fmt.Errorf("invalid code length")
}
out := make([]byte, length)
for i := range out {
n, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return utils.StringEmpty, err
}
out[i] = byte('0' + n.Int64())
}
return string(out), nil
}