Files
backend/internal/service/verification_service.go

174 lines
5.1 KiB
Go
Raw Normal View History

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)
}