You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django邮箱验证OTP无效问题排查及代码优化需求

问题排查(OTP无效错误)

核心原因:类型不匹配

你打印的verify.otp是整数,但serializer.data["otp"]大概率是字符串(前端传参或序列化器未做类型转换),直接用!=比较会返回True,触发无效提示。比如数据库存的是123456,而你拿到的otp是"123456",两者类型不同导致不相等。

其他潜在问题

  • 时区不一致:邮件函数里用datetime.now()生成时间,视图里用timezone.now(),如果项目开启了时区支持,两者会存在偏差,可能导致OTP被误判为过期或有效时间计算错误。
  • 重复请求OTP报错:Verify模型的email字段设为unique=True,用户重复请求验证邮件时,Verify.objects.create()会抛出唯一约束异常,导致新OTP无法存储,用户只能用旧的OTP。
  • 变量名冲突:邮件函数里otp先存随机数,后又赋值为Verify对象,后续代码如果用到这个变量会出问题,逻辑混淆。

代码优化方案

1. 修复OTP类型不匹配问题

修改VerifySerializerBase

确保序列化器将otp字段转为整数:

class VerifySerializerBase(serializers.Serializer):
    email = serializers.EmailField()
    otp = serializers.IntegerField(min_value=100000, max_value=999999)

视图中强制类型转换(备选)

如果序列化器无法修改,在视图里手动转换:

otp = int(serializer.data["otp"])

2. 优化Verify模型与邮件发送函数

模型调整

  • 去掉created_at的手动赋值,因为已经设置auto_now_add=True
  • 可选:如果需要允许用户重复请求OTP并覆盖旧记录,保留unique_together约束;如果要保留历史验证记录,去掉该约束
class Verify(models.Model):
    email = models.EmailField(_("Email"), max_length=254)
    otp = IntegerRangeField(min_value=100000, max_value=999999)
    created_at = models.DateTimeField(_("created at"), auto_now_add=True)
    expires_at = models.DateTimeField()

    def __str__(self) -> str:
        return self.email

    class Meta:
        unique_together = ('email',)

邮件函数优化

  • 修复变量名冲突
  • 处理重复请求OTP的情况(用get_or_create更新)
  • 统一使用时区时间
from django.utils import timezone

def send_opt_email(email, firstname, lastname):
    otp_code = random.randint(100000, 999999)
    subject = "Email verification"
    message = "Email verification"
    html_message = loader.render_to_string(
        "email_verify.html",
        {
            "firstname": firstname,
            "lastname": lastname,
            "otp": otp_code,
        },
    )
    email_from = settings.EMAIL_HOST_USER
    send_mail(
        subject,
        message,
        email_from,
        [email],
        fail_silently=True,
        html_message=html_message,
    )
    # 处理重复请求:更新现有记录或创建新记录
    verify_obj, created = Verify.objects.get_or_create(
        email=email,
        defaults={
            'otp': otp_code,
            'expires_at': timezone.now() + timedelta(minutes=30)
        }
    )
    if not created:
        # 如果记录已存在,更新OTP和过期时间
        verify_obj.otp = otp_code
        verify_obj.expires_at = timezone.now() + timedelta(minutes=30)
        verify_obj.save()

3. 重构Verify_Email视图(简化逻辑+异常处理)

from django.utils import timezone
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

class Verify_Email(APIView):
    """Verify registered emails"""

    def post(self, request):
        serializer = VerifySerializerBase(data=request.data)
        if not serializer.is_valid():
            return Response(
                {"message": "Invalid input data", "errors": serializer.errors},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        email = serializer.validated_data["email"]
        otp = serializer.validated_data["otp"]

        # 检查用户是否存在
        try:
            user = User.objects.get(email=email)
        except User.DoesNotExist:
            return Response(
                {"message": "User was not found"},
                status=status.HTTP_404_NOT_FOUND
            )
        
        # 检查用户是否已激活
        if user.is_active:
            return Response(
                {"message": "This email has already been verified"},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        # 检查验证记录是否存在
        try:
            verify = Verify.objects.get(email=email)
        except Verify.DoesNotExist:
            return Response(
                {"message": "No verification record found, please request OTP again"},
                status=status.HTTP_404_NOT_FOUND
            )
        
        # 验证OTP和过期时间
        now = timezone.now()
        if verify.otp != otp:
            return Response(
                {"message": "This OTP is invalid"},
                status=status.HTTP_400_BAD_REQUEST
            )
        if verify.expires_at < now:
            verify.delete()
            return Response(
                {"message": "This OTP has expired, please request another one"},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        # 验证通过,激活用户并删除验证记录
        verify.delete()
        user.is_active = True
        user.save()
        return Response(
            {"message": "Email has been verified"},
            status=status.HTTP_200_OK
        )

视图优化点

  • 简化嵌套try-except结构,按逻辑顺序依次检查,代码更易读
  • 直接使用serializer.validated_data替代serializer.data,数据更安全
  • 单独处理User.DoesNotExist和Verify.DoesNotExist,返回明确提示
  • 验证过期后自动删除无效记录
  • 返回序列化错误信息,方便调试

内容的提问来源于stack exchange,提问作者rediet yosef

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 17:45:40