- Updated main.go to initialize email service and include it in the dependency injection container. - Refactored handlers to utilize context in service method calls, improving consistency and error handling. - Introduced new service options for upload, security, and captcha services, enhancing modularity and testability. - Removed unused repository implementations to streamline the codebase. This commit continues the effort to improve the architecture by ensuring all services are properly injected and utilized across the application.
174 lines
5.1 KiB
Go
174 lines
5.1 KiB
Go
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)
|
|
}
|