Django 从 0 到 1 打造完整电商平台:用户注册与手机号/邮箱验证
上一篇我们把静态文件、模板骨架全部搞定,项目已经可以呈现出漂亮的页面。从今天开始,我们正式踏入业务逻辑开发的第一站——用户注册。用户注册看似简单,但在电商项目里,它涉及表单验证、手机号唯一性校验、验证码发送、邮箱激活、密码加密存储等一大堆细节。今天我会带着大家一步一步写出完整的注册流程,并且让手机验证码和邮箱激活都跑通。今天我们实现了用户注册的核心功能,涵盖:✅自定义注册表单,支持手机号或邮箱双通
作者: 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_phone和clean_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 手机号注册测试
-
访问
http://127.0.0.1:8000/users/register/ -
输入手机号
13800138000,此时验证码输入框会出现 -
点击"获取验证码",查看终端输出:
[20/May/2026 14:35:22] "POST /users/send_sms/ HTTP/1.1" 200 27
========================================
【模拟短信】验证码:384729,发送至手机号:13800138000
========================================
-
输入收到的验证码(如
384729),设置密码,提交注册 -
注册成功,跳转到首页并显示提示
验证数据库:
SELECT id, username, phone, is_active FROM tb_users;
结果示例:
1|admin|13800138001|1 ← 超级管理员(之前创建)
2|13800138000|13800138000|1 ← 刚注册的用户
8.2 邮箱注册测试
-
填写邮箱
test@example.com,密码,确认密码,提交 -
终端会输出激活邮件内容:
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/
-
复制终端中的链接(注意你的 token 和用户 ID 可能不同),用浏览器打开
-
提示"邮箱激活成功!",用户
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策士,未经授权禁止转载。
更多推荐

所有评论(0)