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