作者: IT策士
系列: 《Django 从 0 到 1 打造完整电商平台》第 6 篇
标签: Django, 用户认证, 短信验证码, 邮箱激活, 电商开发


前言

上一篇我们把静态文件、模板骨架全部搞定,项目已经可以呈现出漂亮的页面。从今天开始,我们正式踏入业务逻辑开发的第一站——用户注册

用户注册看似简单,但在电商项目里,它涉及表单验证、手机号唯一性校验、验证码发送、邮箱激活、密码加密存储等一大堆细节。今天我会带着大家一步一步写出完整的注册流程,并且让手机验证码和邮箱激活都跑通。


一、需求分析

我们的注册功能需要支持两种方式:

注册方式 流程
手机号注册 输入手机号 → 获取短信验证码 → 填写验证码 + 密码 → 完成注册
邮箱注册 输入邮箱 + 密码 → 注册成功 → 发送激活邮件 → 点击链接激活账号

开发环境说明: 由于没有真实短信通道,我们采用控制台模拟发送短信验证码(生产环境换成阿里云/腾讯云 SDK 即可)。邮件方面,Django 提供了 console.EmailBackend,激活邮件会直接打印在终端里,非常方便调试。


二、配置邮件后端(开发环境)

django_ecommerce/settings.py 中添加邮件配置:

# 开发环境:将邮件打印到控制台
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# 生产环境才需要下面的真实 SMTP 配置,现在注释掉
# EMAIL_HOST = 'smtp.example.com'
# EMAIL_PORT = 587
# EMAIL_HOST_USER = 'your_email@example.com'
# EMAIL_HOST_PASSWORD = 'your_password'
# EMAIL_USE_TLS = True
# DEFAULT_FROM_EMAIL = '电商平台 <noreply@example.com>'

这样所有发出的邮件都会显示在 runserver 的终端输出中,注册后去终端复制激活链接即可。


三、编写注册表单

Django 的 Form 组件能帮我们处理前端数据校验。我们在 apps/users/forms.py 中创建自定义注册表单,支持手机号或邮箱两种方式:

from django import forms
from django.core.validators import RegexValidator
from .models import User


class RegisterForm(forms.Form):
    # 手机号(可选,如果用邮箱注册则留空)
    phone = forms.CharField(
        max_length=11,
        min_length=11,
        required=False,
        validators=[
            RegexValidator(r'^1[3-9]\d{9}$', message='请输入有效的手机号')
        ],
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': '手机号(选填)'
        })
    )

    # 邮箱
    email = forms.EmailField(
        required=False,
        widget=forms.EmailInput(attrs={
            'class': 'form-control',
            'placeholder': '邮箱(选填)'
        })
    )

    # 密码
    password = forms.CharField(
        min_length=6,
        max_length=20,
        widget=forms.PasswordInput(attrs={
            'class': 'form-control',
            'placeholder': '密码(至少6位)'
        })
    )

    password2 = forms.CharField(
        label='确认密码',
        widget=forms.PasswordInput(attrs={
            'class': 'form-control',
            'placeholder': '再次输入密码'
        })
    )

    # 手机验证码(如果用手机注册时必填)
    sms_code = forms.CharField(
        max_length=6,
        required=False,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': '短信验证码'
        })
    )

    def clean(self):
        cleaned_data = super().clean()
        phone = cleaned_data.get('phone')
        email = cleaned_data.get('email')

        # 必须提供手机号或邮箱之一
        if not phone and not email:
            raise forms.ValidationError('请至少填写手机号或邮箱')

        return cleaned_data

    def clean_phone(self):
        phone = self.cleaned_data.get('phone')
        if phone and User.objects.filter(phone=phone).exists():
            raise forms.ValidationError('该手机号已被注册')
        return phone

    def clean_email(self):
        email = self.cleaned_data.get('email')
        if email and User.objects.filter(email=email).exists():
            raise forms.ValidationError('该邮箱已被注册')
        return email

    def clean_password2(self):
        pwd = self.cleaned_data.get('password')
        pwd2 = self.cleaned_data.get('password2')
        if pwd and pwd2 and pwd != pwd2:
            raise forms.ValidationError('两次密码不一致')
        return pwd2

设计要点

  • 手机号和邮箱均设为 required=False,但通过 clean() 方法确保至少填一个

  • 自定义手机号正则校验^1[3-9]\d{9}$ 匹配中国大陆手机号格式

  • 唯一性校验:在 clean_phoneclean_email 中检查是否已被注册

  • 密码一致性校验:在 clean_password2 中比对两次输入


四、编写注册视图

apps/users/views.py 中实现注册逻辑:

import random
from django.shortcuts import render, redirect
from django.contrib import messages
from django.core.mail import send_mail
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .forms import RegisterForm
from .models import User


def register(request):
    if request.method == 'POST':
        form = RegisterForm(request.POST)
        if form.is_valid():
            phone = form.cleaned_data.get('phone')
            email = form.cleaned_data.get('email')
            password = form.cleaned_data.get('password')

            # 验证手机验证码(如果使用手机注册)
            if phone:
                sms_code_input = form.cleaned_data.get('sms_code')
                sms_code_session = request.session.get('sms_code')
                if not sms_code_session or sms_code_input != sms_code_session:
                    form.add_error('sms_code', '验证码错误或已过期')
                    return render(request, 'users/register.html', {'form': form})
                # 清空 session 中的验证码
                request.session.pop('sms_code', None)

            # 创建用户
            user = User.objects.create_user(
                username=phone or email.split('@')[0],  # 用手机号或邮箱前缀当用户名
                password=password,
                phone=phone if phone else None,
                email=email if email else None,
            )

            # 如果用邮箱注册,发送激活邮件
            if email:
                # 生成简单的 token(生产环境建议用 itsdangerous)
                token = str(random.randint(100000, 999999))
                request.session[f'email_token_{user.id}'] = token
                activate_url = request.build_absolute_uri(
                    f'/users/activate/{user.id}/{token}/'
                )
                send_mail(
                    subject='激活你的电商账号',
                    message=f'点击链接激活账号:{activate_url}',
                    from_email='noreply@example.com',
                    recipient_list=[email],
                )
                messages.success(request, '注册成功!激活邮件已发送,请前往邮箱查收(查看终端输出)。')
            else:
                messages.success(request, '注册成功!您现在可以登录了。')

            return redirect('home')  # 后续改为登录页
    else:
        form = RegisterForm()

    return render(request, 'users/register.html', {'form': form})


@require_POST
def send_sms_code(request):
    """发送短信验证码(模拟)"""
    phone = request.POST.get('phone')
    if not phone:
        return JsonResponse({'ok': False, 'msg': '手机号不能为空'}, status=400)

    # 生成 6 位随机验证码
    code = str(random.randint(100000, 999999))
    # 存入 session
    request.session['sms_code'] = code
    request.session.set_expiry(300)  # 5分钟有效

    # 控制台模拟发送
    print(f"\n{'='*40}")
    print(f"【模拟短信】验证码:{code},发送至手机号:{phone}")
    print(f"{'='*40}\n")

    return JsonResponse({'ok': True, 'msg': '验证码已发送'})

关键点说明

功能 实现方式
验证码校验 手机注册时,比对用户输入与 request.session['sms_code'],通过后立即清空
用户创建 使用 User.objects.create_user(),自动处理密码加密;用户名取手机号或邮箱前缀
邮箱激活 生成 6 位随机 token 存入 session,构建激活链接并通过 send_mail() 发送
短信模拟 send_sms_code 是独立 AJAX 视图,验证码存 session 并设置 5 分钟过期,同时在控制台打印

五、URL 路由配置

5.1 应用级路由:apps/users/urls.py

from django.urls import path
from . import views

app_name = 'users'

urlpatterns = [
    path('register/', views.register, name='register'),
    path('send_sms/', views.send_sms_code, name='send_sms'),
]

5.2 项目级路由:django_ecommerce/urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.views.generic import TemplateView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
    path('users/', include('apps.users.urls')),  # 注意路径前缀
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

六、注册页面模板

创建 apps/users/templates/users/register.html

{% extends 'base.html' %}
{% block title %}用户注册{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-6 col-lg-5">
        <div class="card shadow-sm">
            <div class="card-body p-4">
                <h3 class="text-center mb-4">📝 创建账号</h3>

                <form method="post" novalidate>
                    {% csrf_token %}

                    <!-- 手机号 -->
                    <div class="mb-3">
                        <label class="form-label">手机号</label>
                        {{ form.phone }}
                        {% if form.phone.errors %}
                            <div class="text-danger small">{{ form.phone.errors.0 }}</div>
                        {% endif %}
                    </div>

                    <!-- 验证码(仅手机注册时显示) -->
                    <div class="mb-3" id="sms-code-group" style="display:none;">
                        <label class="form-label">验证码</label>
                        <div class="input-group">
                            {{ form.sms_code }}
                            <button class="btn btn-outline-secondary" type="button" id="get-sms-btn">
                                获取验证码
                            </button>
                        </div>
                        {% if form.sms_code.errors %}
                            <div class="text-danger small">{{ form.sms_code.errors.0 }}</div>
                        {% endif %}
                    </div>

                    <!-- 邮箱 -->
                    <div class="mb-3">
                        <label class="form-label">邮箱</label>
                        {{ form.email }}
                        {% if form.email.errors %}
                            <div class="text-danger small">{{ form.email.errors.0 }}</div>
                        {% endif %}
                    </div>

                    <!-- 密码 -->
                    <div class="mb-3">
                        <label class="form-label">密码</label>
                        {{ form.password }}
                        {% if form.password.errors %}
                            <div class="text-danger small">{{ form.password.errors.0 }}</div>
                        {% endif %}
                    </div>

                    <div class="mb-3">
                        <label class="form-label">确认密码</label>
                        {{ form.password2 }}
                        {% if form.password2.errors %}
                            <div class="text-danger small">{{ form.password2.errors.0 }}</div>
                        {% endif %}
                    </div>

                    <!-- 全局错误(如未填手机号或邮箱) -->
                    {% if form.non_field_errors %}
                        <div class="alert alert-danger">
                            {{ form.non_field_errors.0 }}
                        </div>
                    {% endif %}

                    <button type="submit" class="btn btn-primary w-100">注册</button>
                </form>

                <p class="text-center mt-3">
                    已有账号?<<a href="#">立即登录</a>
                </p>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
    const phoneInput = document.querySelector('#id_phone');
    const emailInput = document.querySelector('#id_email');
    const smsGroup = document.querySelector('#sms-code-group');
    const getSmsBtn = document.querySelector('#get-sms-btn');

    // 根据手机号输入框是否有内容来显示/隐藏验证码区域
    function toggleSmsGroup() {
        if (phoneInput.value.trim().length > 0) {
            smsGroup.style.display = 'block';
        } else {
            smsGroup.style.display = 'none';
        }
    }

    phoneInput.addEventListener('input', toggleSmsGroup);
    // 页面初始化
    toggleSmsGroup();

    // 获取验证码
    getSmsBtn.addEventListener('click', function() {
        const phone = phoneInput.value.trim();
        if (!phone) {
            alert('请先输入手机号');
            return;
        }
        // 简单的前端倒计时
        let countdown = 60;
        getSmsBtn.disabled = true;
        getSmsBtn.textContent = countdown + '秒后重试';
        const timer = setInterval(() => {
            countdown--;
            getSmsBtn.textContent = countdown + '秒后重试';
            if (countdown <= 0) {
                clearInterval(timer);
                getSmsBtn.disabled = false;
                getSmsBtn.textContent = '获取验证码';
            }
        }, 1000);

        // 发送请求
        fetch('{% url "users:send_sms" %}', {
            method: 'POST',
            headers: {
                'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: 'phone=' + encodeURIComponent(phone)
        })
        .then(response => response.json())
        .then(data => {
            if (!data.ok) {
                alert(data.msg);
            }
        });
    });
</script>
{% endblock %}

前端交互亮点

  • 动态显示验证码区域:监听手机号输入框,有内容时自动显示验证码输入框

  • 60 秒倒计时防重复点击:点击"获取验证码"后按钮禁用并倒计时

  • AJAX 发送验证码:使用 fetch() 发送 POST 请求,自动携带 CSRF Token


七、邮箱激活功能

为了完成完整的邮箱注册流程,我们再添加一个激活视图。

7.1 激活视图:apps/users/views.py

from django.http import Http404
from django.shortcuts import get_object_or_404

def activate_email(request, user_id, token):
    user = get_object_or_404(User, id=user_id)
    session_token = request.session.get(f'email_token_{user.id}')

    if not session_token or session_token != token:
        raise Http404('激活链接无效或已过期')

    user.email_active = True
    user.save(update_fields=['email_active'])
    # 激活成功后清理 token
    request.session.pop(f'email_token_{user.id}', None)

    messages.success(request, '邮箱激活成功!现在可以登录了。')
    return redirect('home')  # 后续改为登录页

7.2 添加激活路由:apps/users/urls.py

path('activate/<int:user_id>/<str:token>/', views.activate_email, name='activate_email'),

八、测试完整流程

启动开发服务器:

python manage.py runserver

8.1 手机号注册测试

  1. 访问 http://127.0.0.1:8000/users/register/

  2. 输入手机号 13800138000,此时验证码输入框会出现

  3. 点击"获取验证码",查看终端输出:

[20/May/2026 14:35:22] "POST /users/send_sms/ HTTP/1.1" 200 27

========================================
【模拟短信】验证码:384729,发送至手机号:13800138000
========================================
  1. 输入收到的验证码(如 384729),设置密码,提交注册

  2. 注册成功,跳转到首页并显示提示

验证数据库:

SELECT id, username, phone, is_active FROM tb_users;

结果示例:

1|admin|13800138001|1        ← 超级管理员(之前创建)
2|13800138000|13800138000|1  ← 刚注册的用户

8.2 邮箱注册测试

  1. 填写邮箱 test@example.com,密码,确认密码,提交

  2. 终端会输出激活邮件内容:

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: 激活你的电商账号
From: noreply@example.com
To: test@example.com
Date: Wed, 20 May 2026 14:40:00 -0000
Message-ID: <...>
X-Mailer: Django Mailer

点击链接激活账号:http://127.0.0.1:8000/users/activate/3/123456/
  1. 复制终端中的链接(注意你的 token 和用户 ID 可能不同),用浏览器打开

  2. 提示"邮箱激活成功!",用户 email_active 变为 True


九、当前注册的不足与改进方向

问题 现状 改进方案
手机验证码防刷 没有对同一手机号频繁发送做限制 后续使用 Redis 缓存记录发送频率
激活 Token 安全性 使用 6 位随机数,易被暴力破解 生产环境使用 itsdangerous 或 Django 自带的 PasswordResetTokenGenerator 生成有时效的签名 token
用户名冲突 邮箱前缀可能重复 后续在 clean 中增加唯一性校验,或改用 UUID 作为 username

这些优化会在后续篇(第 24~26 篇)中逐步完善。


十、总结与下集预告

今天我们实现了用户注册的核心功能,涵盖:

自定义注册表单,支持手机号或邮箱双通道注册
模拟短信验证码的发送与验证,使用 session 存储验证码
邮箱注册的激活流程,通过控制台邮件后端完成激活
前端动态显示验证码输入区域,AJAX 请求发送验证码

注册搞定了,登录自然是下一步。 第 7 篇,我将带大家实现登录与登出功能,包括 Django 内置认证系统的使用、登录装饰器、记住我功能,以及登录后导航栏的状态变化。


📢 想了解更多? 去公众号、今日头条搜索「IT策士」,一起升级 IT 思维!
本文为《Django 从 0 到 1 打造完整电商平台》系列第 6 篇,作者:IT策士,未经授权禁止转载。


Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐