package verification import ( "context" "crypto/rand" "fmt" "math/big" "github.com/break/junhong_cmp_fiber/pkg/config" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/sms" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) // Service 验证码服务 type Service struct { redisClient *redis.Client smsClient *sms.Client logger *zap.Logger } // NewService 创建验证码服务实例 func NewService(redisClient *redis.Client, smsClient *sms.Client, logger *zap.Logger) *Service { return &Service{ redisClient: redisClient, smsClient: smsClient, logger: logger, } } // SendCode 发送验证码 func (s *Service) SendCode(ctx context.Context, phone string) error { // 检查发送频率限制 limitKey := constants.RedisVerificationCodeLimitKey(phone) exists, err := s.redisClient.Exists(ctx, limitKey).Result() if err != nil { s.logger.Error("检查验证码发送频率限制失败", zap.String("phone", phone), zap.Error(err), ) return fmt.Errorf("检查验证码发送频率限制失败: %w", err) } if exists > 0 { s.logger.Warn("验证码发送过于频繁", zap.String("phone", phone), ) return fmt.Errorf("验证码发送过于频繁,请稍后再试") } // 生成随机验证码 code, err := s.generateCode() if err != nil { s.logger.Error("生成验证码失败", zap.String("phone", phone), zap.Error(err), ) return fmt.Errorf("生成验证码失败: %w", err) } // 构造短信内容 cfg := config.Get() content := fmt.Sprintf("您的验证码是%s,%d分钟内有效", code, int(constants.VerificationCodeExpiration.Minutes())) // 发送短信 _, err = s.smsClient.SendMessage(ctx, content, []string{phone}) if err != nil { s.logger.Error("发送验证码短信失败", zap.String("phone", phone), zap.Error(err), ) return fmt.Errorf("发送验证码短信失败: %w", err) } // 存储验证码到 Redis codeKey := constants.RedisVerificationCodeKey(phone) err = s.redisClient.Set(ctx, codeKey, code, constants.VerificationCodeExpiration).Err() if err != nil { s.logger.Error("存储验证码失败", zap.String("phone", phone), zap.Error(err), ) return fmt.Errorf("存储验证码失败: %w", err) } // 设置发送频率限制 err = s.redisClient.Set(ctx, limitKey, "1", constants.VerificationCodeRateLimit).Err() if err != nil { s.logger.Error("设置验证码发送频率限制失败", zap.String("phone", phone), zap.Error(err), ) // 这个错误不影响主流程,只记录日志 } s.logger.Info("验证码发送成功", zap.String("phone", phone), ) // 避免在日志中暴露验证码(仅在开发环境下记录) if cfg.Logging.Development { s.logger.Debug("验证码内容(仅开发环境)", zap.String("phone", phone), zap.String("code", code), ) } return nil } // VerifyCode 验证验证码 func (s *Service) VerifyCode(ctx context.Context, phone string, code string) error { codeKey := constants.RedisVerificationCodeKey(phone) // 从 Redis 获取验证码 storedCode, err := s.redisClient.Get(ctx, codeKey).Result() if err == redis.Nil { s.logger.Warn("验证码不存在或已过期", zap.String("phone", phone), ) return fmt.Errorf("验证码不存在或已过期") } if err != nil { s.logger.Error("获取验证码失败", zap.String("phone", phone), zap.Error(err), ) return fmt.Errorf("获取验证码失败: %w", err) } // 验证码比对 if storedCode != code { s.logger.Warn("验证码错误", zap.String("phone", phone), ) return fmt.Errorf("验证码错误") } // 验证成功,删除验证码(防止重复使用) err = s.redisClient.Del(ctx, codeKey).Err() if err != nil { s.logger.Error("删除验证码失败", zap.String("phone", phone), zap.Error(err), ) // 这个错误不影响主流程,只记录日志 } s.logger.Info("验证码验证成功", zap.String("phone", phone), ) return nil } // generateCode 生成随机验证码 func (s *Service) generateCode() (string, error) { // 生成 6 位数字验证码 const digits = "0123456789" code := make([]byte, constants.VerificationCodeLength) for i := range code { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits)))) if err != nil { return "", err } code[i] = digits[num.Int64()] } return string(code), nil }