142 lines
3.5 KiB
Go
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
|
|
}
|