package service import ( "context" "crypto/rand" "fmt" "math/big" "time" "carrotskin/pkg/config" "carrotskin/pkg/email" "carrotskin/pkg/redis" ) const ( // 验证码类型 VerificationTypeRegister = "register" VerificationTypeResetPassword = "reset_password" VerificationTypeChangeEmail = "change_email" // 验证码配置 CodeLength = 6 // 验证码长度 CodeExpiration = 10 * time.Minute // 验证码有效期 CodeRateLimit = 1 * time.Minute // 发送频率限制 ) // verificationService VerificationService的实现 type verificationService struct { redis *redis.Client emailService *email.Service } // NewVerificationService 创建VerificationService实例 func NewVerificationService( redisClient *redis.Client, emailService *email.Service, ) VerificationService { return &verificationService{ redis: redisClient, emailService: emailService, } } // SendCode 发送验证码 func (s *verificationService) SendCode(ctx context.Context, email, codeType string) error { // 测试环境下直接跳过,不存储也不发送 cfg, err := config.GetConfig() if err == nil && cfg.IsTestEnvironment() { return nil } // 检查发送频率限制 rateLimitKey := fmt.Sprintf("verification:rate_limit:%s:%s", codeType, email) exists, err := s.redis.Exists(ctx, rateLimitKey) if err != nil { return fmt.Errorf("检查发送频率失败: %w", err) } if exists > 0 { return fmt.Errorf("发送过于频繁,请稍后再试") } // 生成验证码 code, err := s.generateCode() if err != nil { return fmt.Errorf("生成验证码失败: %w", err) } // 存储验证码到Redis codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email) if err := s.redis.Set(ctx, codeKey, code, CodeExpiration); err != nil { return fmt.Errorf("存储验证码失败: %w", err) } // 设置发送频率限制 if err := s.redis.Set(ctx, rateLimitKey, "1", CodeRateLimit); err != nil { return fmt.Errorf("设置发送频率限制失败: %w", err) } // 发送邮件 if err := s.sendEmail(email, code, codeType); err != nil { // 发送失败,删除验证码 _ = s.redis.Del(ctx, codeKey) return fmt.Errorf("发送邮件失败: %w", err) } return nil } // VerifyCode 验证验证码 func (s *verificationService) VerifyCode(ctx context.Context, email, code, codeType string) error { // 测试环境下直接通过验证 cfg, err := config.GetConfig() if err == nil && cfg.IsTestEnvironment() { return nil } // 检查是否被锁定 locked, ttl, err := CheckVerifyLocked(ctx, s.redis, email, codeType) if err == nil && locked { return fmt.Errorf("验证码错误次数过多,请在 %d 分钟后重试", int(ttl.Minutes())+1) } codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email) // 从Redis获取验证码 storedCode, err := s.redis.Get(ctx, codeKey) if err != nil { // 记录失败尝试并检查是否触发锁定 count, _ := RecordVerifyFailure(ctx, s.redis, email, codeType) if count >= MaxVerifyAttempts { return fmt.Errorf("验证码错误次数过多,账号已被锁定 %d 分钟", int(VerifyLockDuration.Minutes())) } remaining := MaxVerifyAttempts - count if remaining > 0 { return fmt.Errorf("验证码已过期或不存在,还剩 %d 次尝试机会", remaining) } return fmt.Errorf("验证码已过期或不存在") } // 验证验证码 if storedCode != code { // 记录失败尝试并检查是否触发锁定 count, _ := RecordVerifyFailure(ctx, s.redis, email, codeType) if count >= MaxVerifyAttempts { return fmt.Errorf("验证码错误次数过多,账号已被锁定 %d 分钟", int(VerifyLockDuration.Minutes())) } remaining := MaxVerifyAttempts - count if remaining > 0 { return fmt.Errorf("验证码错误,还剩 %d 次尝试机会", remaining) } return fmt.Errorf("验证码错误") } // 验证成功,删除验证码和失败计数 _ = s.redis.Del(ctx, codeKey) _ = ClearVerifyAttempts(ctx, s.redis, email, codeType) return nil } // generateCode 生成6位数字验证码 func (s *verificationService) generateCode() (string, error) { const digits = "0123456789" code := make([]byte, CodeLength) 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 } // sendEmail 根据类型发送邮件 func (s *verificationService) sendEmail(to, code, codeType string) error { switch codeType { case VerificationTypeRegister: return s.emailService.SendEmailVerification(to, code) case VerificationTypeResetPassword: return s.emailService.SendResetPassword(to, code) case VerificationTypeChangeEmail: return s.emailService.SendChangeEmail(to, code) default: return s.emailService.SendVerificationCode(to, code, codeType) } } // DeleteVerificationCode 删除验证码(工具函数,保持向后兼容) func DeleteVerificationCode(ctx context.Context, redisClient *redis.Client, email, codeType string) error { codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email) return redisClient.Del(ctx, codeKey) }