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 }