Django 从 0 到 1 打造完整电商平台:用户注册与手机号/邮箱验证

Django 从 0 到 1 打造完整电商平台:用户注册与手机号/邮箱验证 作者IT策士系列《Django 从 0 到 1 打造完整电商平台》第 6 篇标签Django, 用户认证, 短信验证码, 邮箱激活, 电商开发前言上一篇我们把静态文件、模板骨架全部搞定项目已经可以呈现出漂亮的页面。从今天开始我们正式踏入业务逻辑开发的第一站——用户注册。用户注册看似简单但在电商项目里它涉及表单验证、手机号唯一性校验、验证码发送、邮箱激活、密码加密存储等一大堆细节。今天我会带着大家一步一步写出完整的注册流程并且让手机验证码和邮箱激活都跑通。一、需求分析我们的注册功能需要支持两种方式注册方式流程手机号注册输入手机号 → 获取短信验证码 → 填写验证码 密码 → 完成注册邮箱注册输入邮箱 密码 → 注册成功 → 发送激活邮件 → 点击链接激活账号开发环境说明由于没有真实短信通道我们采用控制台模拟发送短信验证码生产环境换成阿里云/腾讯云 SDK 即可。邮件方面Django 提供了console.EmailBackend激活邮件会直接打印在终端里非常方便调试。二、配置邮件后端开发环境在django_ecommerce/settings.py中添加邮件配置# 开发环境将邮件打印到控制台EMAIL_BACKENDdjango.core.mail.backends.console.EmailBackend# 生产环境才需要下面的真实 SMTP 配置现在注释掉# EMAIL_HOST smtp.example.com# EMAIL_PORT 587# EMAIL_HOST_USER your_emailexample.com# EMAIL_HOST_PASSWORD your_password# EMAIL_USE_TLS True# DEFAULT_FROM_EMAIL 电商平台 noreplyexample.com这样所有发出的邮件都会显示在runserver的终端输出中注册后去终端复制激活链接即可。三、编写注册表单Django 的 Form 组件能帮我们处理前端数据校验。我们在apps/users/forms.py中创建自定义注册表单支持手机号或邮箱两种方式from djangoimportforms from django.core.validatorsimportRegexValidator from .modelsimportUser class RegisterForm(forms.Form):# 手机号可选如果用邮箱注册则留空phoneforms.CharField(max_length11,min_length11,requiredFalse,validators[RegexValidator(r^1[3-9]\d{9}$,message请输入有效的手机号)],widgetforms.TextInput(attrs{class:form-control,placeholder:手机号选填}))# 邮箱emailforms.EmailField(requiredFalse,widgetforms.EmailInput(attrs{class:form-control,placeholder:邮箱选填}))# 密码passwordforms.CharField(min_length6,max_length20,widgetforms.PasswordInput(attrs{class:form-control,placeholder:密码至少6位}))password2forms.CharField(label确认密码,widgetforms.PasswordInput(attrs{class:form-control,placeholder:再次输入密码}))# 手机验证码如果用手机注册时必填sms_codeforms.CharField(max_length6,requiredFalse,widgetforms.TextInput(attrs{class:form-control,placeholder:短信验证码}))def clean(self): cleaned_datasuper().clean()phonecleaned_data.get(phone)emailcleaned_data.get(email)# 必须提供手机号或邮箱之一ifnot phone and not email: raise forms.ValidationError(请至少填写手机号或邮箱)returncleaned_data def clean_phone(self): phoneself.cleaned_data.get(phone)ifphone and User.objects.filter(phonephone).exists(): raise forms.ValidationError(该手机号已被注册)returnphone def clean_email(self): emailself.cleaned_data.get(email)ifemail and User.objects.filter(emailemail).exists(): raise forms.ValidationError(该邮箱已被注册)returnemail def clean_password2(self):pwdself.cleaned_data.get(password)pwd2self.cleaned_data.get(password2)ifpwdand pwd2 andpwd!pwd2: raise forms.ValidationError(两次密码不一致)returnpwd2设计要点手机号和邮箱均设为requiredFalse但通过clean()方法确保至少填一个自定义手机号正则校验^1[3-9]\d{9}$匹配中国大陆手机号格式唯一性校验在clean_phone和clean_email中检查是否已被注册密码一致性校验在clean_password2中比对两次输入四、编写注册视图在apps/users/views.py中实现注册逻辑importrandom from django.shortcutsimportrender, redirect from django.contribimportmessages from django.core.mailimportsend_mail from django.confimportsettings from django.httpimportJsonResponse from django.views.decorators.httpimportrequire_POST from .formsimportRegisterForm from .modelsimportUser def register(request):ifrequest.methodPOST:formRegisterForm(request.POST)ifform.is_valid(): phoneform.cleaned_data.get(phone)emailform.cleaned_data.get(email)passwordform.cleaned_data.get(password)# 验证手机验证码如果使用手机注册ifphone: sms_code_inputform.cleaned_data.get(sms_code)sms_code_sessionrequest.session.get(sms_code)ifnot sms_code_session or sms_code_input!sms_code_session: form.add_error(sms_code,验证码错误或已过期)returnrender(request,users/register.html,{form:form})# 清空 session 中的验证码request.session.pop(sms_code, None)# 创建用户userUser.objects.create_user(usernamephone or email.split()[0],# 用手机号或邮箱前缀当用户名passwordpassword,phonephoneifphoneelseNone,emailemailifemailelseNone,)# 如果用邮箱注册发送激活邮件ifemail:# 生成简单的 token生产环境建议用 itsdangeroustokenstr(random.randint(100000,999999))request.session[femail_token_{user.id}]token activate_urlrequest.build_absolute_uri(f/users/activate/{user.id}/{token}/)send_mail(subject激活你的电商账号,messagef点击链接激活账号{activate_url},from_emailnoreplyexample.com,recipient_list[email],)messages.success(request,注册成功激活邮件已发送请前往邮箱查收查看终端输出。)else: messages.success(request,注册成功您现在可以登录了。)returnredirect(home)# 后续改为登录页else: formRegisterForm()returnrender(request,users/register.html,{form:form})require_POST def send_sms_code(request):发送短信验证码模拟 phonerequest.POST.get(phone)ifnot phone:returnJsonResponse({ok:False,msg:手机号不能为空},status400)# 生成 6 位随机验证码codestr(random.randint(100000,999999))# 存入 sessionrequest.session[sms_code]code request.session.set_expiry(300)# 5分钟有效# 控制台模拟发送print(f\n{*40})print(f【模拟短信】验证码{code}发送至手机号{phone})print(f{*40}\n)returnJsonResponse({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.pyfrom django.urlsimportpath from.importviews app_nameusersurlpatterns[path(register/, views.register,nameregister), path(send_sms/, views.send_sms_code,namesend_sms),]5.2 项目级路由django_ecommerce/urls.pyfrom django.contribimportadmin from django.urlsimportpath, include from django.confimportsettings from django.conf.urls.staticimportstatic from django.views.genericimportTemplateView urlpatterns[path(admin/, admin.site.urls), path(, TemplateView.as_view(template_namehome.html),namehome), path(users/, include(apps.users.urls)),# 注意路径前缀]ifsettings.DEBUG: urlpatternsstatic(settings.MEDIA_URL,document_rootsettings.MEDIA_ROOT)六、注册页面模板创建apps/users/templates/users/register.html{% extendsbase.html%}{% block title %}用户注册{% endblock %}{% block content %}divclassrow justify-content-centerdivclasscol-md-6 col-lg-5divclasscard shadow-smdivclasscard-body p-4h3classtext-center mb-4 创建账号/h3formmethodpostnovalidate{% csrf_token %}!-- 手机号 --divclassmb-3labelclassform-label手机号/label{{form.phone}}{%ifform.phone.errors %}divclasstext-danger small{{form.phone.errors.0}}/div{% endif %}/div!-- 验证码仅手机注册时显示 --divclassmb-3idsms-code-groupstyledisplay:none;labelclassform-label验证码/labeldivclassinput-group{{form.sms_code}}buttonclassbtn btn-outline-secondarytypebuttonidget-sms-btn获取验证码/button/div{%ifform.sms_code.errors %}divclasstext-danger small{{form.sms_code.errors.0}}/div{% endif %}/div!-- 邮箱 --divclassmb-3labelclassform-label邮箱/label{{form.email}}{%ifform.email.errors %}divclasstext-danger small{{form.email.errors.0}}/div{% endif %}/div!-- 密码 --divclassmb-3labelclassform-label密码/label{{form.password}}{%ifform.password.errors %}divclasstext-danger small{{form.password.errors.0}}/div{% endif %}/divdivclassmb-3labelclassform-label确认密码/label{{form.password2}}{%ifform.password2.errors %}divclasstext-danger small{{form.password2.errors.0}}/div{% endif %}/div!-- 全局错误如未填手机号或邮箱 --{%ifform.non_field_errors %}divclassalert alert-danger{{form.non_field_errors.0}}/div{% endif %}buttontypesubmitclassbtn btn-primary w-100注册/button/formpclasstext-center mt-3已有账号ahref#立即登录/a/p/div/div/div/div{% endblock %}{% block extra_js %}scriptconst phoneInputdocument.querySelector(#id_phone);const emailInputdocument.querySelector(#id_email);const smsGroupdocument.querySelector(#sms-code-group);const getSmsBtndocument.querySelector(#get-sms-btn);// 根据手机号输入框是否有内容来显示/隐藏验证码区域functiontoggleSmsGroup(){if(phoneInput.value.trim().length0){smsGroup.style.displayblock;}else{smsGroup.style.displaynone;}}phoneInput.addEventListener(input, toggleSmsGroup);// 页面初始化 toggleSmsGroup();// 获取验证码 getSmsBtn.addEventListener(click,function(){const phonephoneInput.value.trim();if(!phone){alert(请先输入手机号);return;}// 简单的前端倒计时letcountdown60;getSmsBtn.disabledtrue;getSmsBtn.textContentcountdown 秒后重试;const timersetInterval((){ countdown--;getSmsBtn.textContentcountdown秒后重试;if(countdown0){ clearInterval(timer);getSmsBtn.disabledfalse;getSmsBtn.textContent获取验证码;} },1000);//发送请求 fetch({%url users:send_sms%},{ method:POST,headers:{ X-CSRFToken:document.querySelector([namecsrfmiddlewaretoken]).value,Content-Type:application/x-www-form-urlencoded,},body:phoneencodeURIComponent(phone)}).then(responseresponse.json()).then(data{if(!data.ok){alert(data.msg);}});});/script{% endblock %}前端交互亮点动态显示验证码区域监听手机号输入框有内容时自动显示验证码输入框60 秒倒计时防重复点击点击获取验证码后按钮禁用并倒计时AJAX 发送验证码使用fetch()发送 POST 请求自动携带 CSRF Token七、邮箱激活功能为了完成完整的邮箱注册流程我们再添加一个激活视图。7.1 激活视图apps/users/views.pyfrom django.httpimportHttp404 from django.shortcutsimportget_object_or_404 def activate_email(request, user_id, token): userget_object_or_404(User,iduser_id)session_tokenrequest.session.get(femail_token_{user.id})ifnot session_token or session_token!token: raise Http404(激活链接无效或已过期)user.email_activeTrue user.save(update_fields[email_active])# 激活成功后清理 tokenrequest.session.pop(femail_token_{user.id}, None)messages.success(request,邮箱激活成功现在可以登录了。)returnredirect(home)# 后续改为登录页7.2 添加激活路由apps/users/urls.pypath(activate/int:user_id/str:token/, views.activate_email,nameactivate_email),八、测试完整流程启动开发服务器python manage.py runserver8.1 手机号注册测试访问http://127.0.0.1:8000/users/register/输入手机号13800138000此时验证码输入框会出现点击获取验证码查看终端输出[20/May/202614:35:22]POST /users/send_sms/ HTTP/1.120027【模拟短信】验证码384729发送至手机号13800138000输入收到的验证码如384729设置密码提交注册注册成功跳转到首页并显示提示验证数据库SELECT id, username, phone, is_active FROM tb_users;结果示例1|admin|13800138001|1← 超级管理员之前创建2|13800138000|13800138000|1← 刚注册的用户8.2 邮箱注册测试填写邮箱testexample.com密码确认密码提交终端会输出激活邮件内容Content-Type: text/plain;charsetutf-8MIME-Version:1.0Content-Transfer-Encoding: 7bit Subject: 激活你的电商账号 From: noreplyexample.com To: testexample.com Date: Wed,20May202614:40:00-0000Message-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策士未经授权禁止转载。