第11次:用户注册(完整版)
第一步:定义用户模型类
class User(AbstractUser):mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')class Meta:db_table = 'tb_user'verbose_name = '用户'verbose_name_plural = verbose_namedef __str__(self):return self.username
第二步:在配置文件中配置自定义User模型类,用自定义的User模型类代替Django自带的User模型类
#dev.py
# 指定本项目用户模型类,让Django自带的User类不起作用
AUTH_USER_MODEL = 'users.User'
第三步:生成数据表
python .\manage.py makemigrations
python .\manage.py migrate
第四步:定义视图类,处理注册时请求
import refrom django.db import DatabaseError
from django.http import HttpResponseForbidden
from django.shortcuts import render, redirect
from django.urls import reverse
from django.views import View# from xiaoyu_mall.apps.users.models import User 应用目录变更后不要使用这种方式来导包
# 推荐使用如下的相对路径来导包
from .models import Userclass RegisterView(View):# get请求,def get(self, request):return render(request, 'register.html')def post(self, request):# 第一步:接收请求参数username = request.POST.get('username')password = request.POST.get('password')password2 = request.POST.get('password2')mobile = request.POST.get('mobile')allow = request.POST.get('allow')# 第二步:判断参数是否完整if not all([username, password, mobile, allow]):return HttpResponseForbidden('缺少必要参数')# 用户名长度校验if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', username):return HttpResponseForbidden('请输入5-20个字符的用户名')# 检验密码格式if not re.match(r'^[0-9A-Za-z]{8,20}$', password):return HttpResponseForbidden('请输入8-20位的密码')# 检验两次密码一致if password != password2:return HttpResponseForbidden('两次密码不一致')# 校验手机号if not re.match(r'^1[3-9]{9}$', mobile):return HttpResponseForbidden('请输入正确的手机号')# 提醒勾选协议if allow != 'on':return HttpResponseForbidden('请勾选协议')# 第三步:保存注册数据try:User.objects.create_user(username=username, password=password, mobile=mobile)except DatabaseError:return render(request, 'register.html', {'register_errmsg': '注册失败'})# 返回注册结果return redirect(reverse('contents:index'))
第五步:定义根路由和子路由
#根路由
from django.contrib import admin
from django.urls import path, includeurlpatterns = [path('admin/', admin.site.urls),# 导入path('', include('users.urls', namespace='users')),
]
from django.urls import path
# 变更了应用所在目录之后,不要使用这种方式导包
# from xiaoyu_mall.apps.users import views
# 推荐使用如下的相对路径来导包
from . import views
# 设置命名空间
app_name = 'users'
urlpatterns = [path('register/', views.RegisterView.as_view(), name='register'),
]
第六步:注册过程中的前端校验与后端校验
- 前端校验使用vue-2.5.16.js实现,预先校验表单数据的合法性,定义在register.js中
- 用户名与手机号先要经过前端正则校验后,再向后端发送查询请求,确定数据库中没有重复数据之后再保存。
- 密码要在先在前端正则检验
- 要校验确认密码与密码是否一致
- 要校验手机号的格式
- 要校验是否勾选协议
在static/js/目录下创建register.js
let vm = new Vue({el: '#app',// 修改Vue读取变量的语法delimiters: ['[[', ']]'],data: {username: '', // 用户名password: '', // 密码password2: '', // 确认密码mobile: '', // 手机号allow: '', // 同意协议uuid: '',image_code_url: '', // 图形验证码image_code: '',error_name: false,error_password: false,error_password2: false,error_mobile: false,error_allow: false,error_image_code: false,error_name_message: '', // 用户名错误提示error_mobile_message: '', // 手机错误提示error_image_code_message: '',},mounted(){// 界面获取图形验证码this.generate_image_code();},methods: {// 生成图形验证码generate_image_code(){// 生成UUID。generateUUID() : 封装在common.js文件中,需要提前引入this.uuid = generateUUID();// 拼接图形验证码请求地址this.image_code_url = "/image_codes/" + this.uuid + "/";},// 校验用户名check_username(){// 准备正则表达式let re = /^[a-zA-Z0-9_-]{5,20}$/;// 正则表达式匹配用户名if (re.test(this.username)) {this.error_name = false;} else {this.error_name_message = '请输入5-20个字符的用户名';this.error_name = true;}// 检查用户名是否重名注册if (this.error_name == false) {let url = '/usernames/' + this.username + '/count/';axios.get(url,{responseType: 'json'}).then(response => {if (response.data.count == 1) {this.error_name_message = '用户名已存在';this.error_name = true;} else {this.error_name = false;}}).catch(error => {console.log(error.response);})}},// 校验密码check_password(){let re = /^[0-9A-Za-z]{8,20}$/;if (re.test(this.password)) {this.error_password = false;} else {this.error_password = true;}},// 校验确认密码check_password2(){// 判断两次密码是否一致if(this.password != this.password2) {this.error_password2 = true;} else {this.error_password2 = false;}},// 校验手机号check_mobile(){let re = /^1[3-9]\d{9}$/;if(re.test(this.mobile)) {this.error_mobile = false;} else {this.error_mobile_message = '您输入的手机号格式不正确';this.error_mobile = true;}// 检查手机号是否重复注册if (this.error_mobile == false) {let url = '/mobiles/'+ this.mobile + '/count/';axios.get(url, {responseType: 'json'}).then(response => {if (response.data.count == 1) {this.error_mobile_message = '手机号已存在';this.error_mobile = true;} else {this.error_mobile = false;}}).catch(error => {console.log(error.response);})}},// 检查图形验证码check_image_code(){if(this.image_code.length != 4) {this.error_image_code_message = '请填写图片验证码';this.error_image_code = true;} else {this.error_image_code = false;}},// 校验是否勾选协议check_allow(){if(!this.allow) {this.error_allow = true;} else {this.error_allow = false;}},// 监听表单提交事件on_submit(){this.check_username();this.check_password();this.check_password2();this.check_mobile();this.check_allow();if(this.error_name == true || this.error_password == true || this.error_password2 == true|| this.error_mobile == true || this.error_allow == true) {// 禁用表单的提交window.event.returnValue = false;}},}
});
common.js
// 获取cookie
function getCookie(name) {let r = document.cookie.match("\\b" + name + "=([^;]*)\\b");return r ? r[1] : undefined;
}// 提取地址栏中的查询字符串
function get_query_string(name) {let reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');let r = window.location.search.substr(1).match(reg);if (r != null) {return decodeURI(r[2]);}return null;
}// 生成uuid
function generateUUID() {let d = new Date().getTime();if(window.performance && typeof window.performance.now === "function"){d += performance.now(); //use high-precision timer if available}let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {let r = (d + Math.random()*16)%16 | 0;d = Math.floor(d/16);return (c=='x' ? r : (r&0x3|0x8)).toString(16);});return uuid;
}
- 后端校验通过视图来实现,通过查询数据库校验用户名、手机号在系统中的是否唯一。
users应用下views.py
from xiaoyu_mall.utils.response_code import RETCODEclass MobileCountView(View):"""手机号唯一性校验"""def get(self, request, mobile):count = User.objects.filter(mobile=mobile).count()return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'count': count})class UsernameCountView(View):"""判断用户名是否重复注册"""def get(self, request, username):count = User.objects.filter(username=username).count()return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'count': count})
上面代码用的状态码定义在xiaoyu_mall/xiaoyu_mall/utils/response_code.py中
class RETCODE:OK = "0"IMAGECODEERR = "4001"THROTTLINGERR = "4002"NECESSARYPARAMERR = "4003"USERERR = "4004"PWDERR = "4005"CPWDERR = "4006"MOBILEERR = "4007"SMSCODERR = "4008"ALLOWERR = "4009"SESSIONERR = "4101"DBERR = "5000"EMAILERR = "5001"TELERR = "5002"NODATAERR = "5003"NEWPWDERR = "5004"OPENIDERR = "5005"PARAMERR = "5006"STOCKERR = "5007"err_msg = {RETCODE.OK : "成功",RETCODE.IMAGECODEERR : "图形验证码错误",RETCODE.THROTTLINGERR : "访问过于频繁",RETCODE.NECESSARYPARAMERR : "缺少必传参数",RETCODE.USERERR : "用户名错误",RETCODE.PWDERR : "密码错误",RETCODE.CPWDERR : "密码不一致",RETCODE.MOBILEERR : "手机号错误",RETCODE.SMSCODERR : "短信验证码有误",RETCODE.ALLOWERR : "未勾选协议",RETCODE.SESSIONERR : "用户未登录",RETCODE.DBERR : "数据错误",RETCODE.EMAILERR : "邮箱错误",RETCODE.TELERR : "固定电话错误",RETCODE.NODATAERR : "无数据",RETCODE.NEWPWDERR : "新密码数据",RETCODE.OPENIDERR : "无效的openid",RETCODE.PARAMERR : "参数错误",RETCODE.STOCKERR : "库存不足",
}
- 路由配置,在users应用下urls.py中,
from django.urls import path, re_path
# 变更了应用所在目录之后,不要使用这种方式导包
# from xiaoyu_mall.apps.users import views
# 推荐使用如下的相对路径来导包
from . import views# 设置命名空间
app_name = 'users'
urlpatterns = [path('register/', views.RegisterView.as_view(), name='register'),re_path('usernames/(?P<username>[a-zA-Z0-9_-]{5,20})/count/', views.UsernameCountView.as_view()),re_path(r'mobiles/(?P<mobile>1[3-9]\d{9})/count/', views.MobileCountView.as_view()),
]
第七步:图形验证
安装依赖库
pip install pillow
pip install captcha
图形验证码要存在redis数据库中,在xiaoyu_mall/dev.py配置使用redis的2号库存图形验证码
CACHES = {"default": { # 默认"BACKEND": "django_redis.cache.RedisCache","LOCATION": "redis://127.0.0.1:6379/0","OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient",}},"session": { # session"BACKEND": "django_redis.cache.RedisCache","LOCATION": "redis://127.0.0.1:6379/1","OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient",}},"verify_code": { # 保存验证码"BACKEND": "django_redis.cache.RedisCache","LOCATION": "redis://127.0.0.1:6379/2", # 选择redis2号库"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient",}},
}
在verifications应用中创建constants.py,用于设置图形验证码有效期的变量
# 图形验证码有效期,单位:秒
IMAGE_CODE_REDIS_EXPIRES = 300
# 短信验证码有效期,单位:秒
SMS_CODE_REDIS_EXPIRES = 300
在verifications应用中创建verify_pic.py
from captcha.image import ImageCaptcha
import io
import random
import string# 生成随机验证码
def generate_captcha_text():captcha_text = ''.join(random.choices(string.ascii_letters +string.digits, k=4))return captcha_text# 生成验证码图片
def generate_captcha_image(text):image = ImageCaptcha()data = image.generate_image(text)img_byte_array = io.BytesIO()data.save(img_byte_array, format='PNG')binary_image = img_byte_array.getvalue()return binary_image
在verifications应用下views.py定义生成图形验证码的类视图
from django.views import View
from .verify_pic import generate_captcha_text,generate_captcha_image
from django_redis import get_redis_connection
from . import constants
from django.http import HttpResponse
import logging# 日志记录器
logger = logging.getLogger('django')class ImageCodeView(View):def get(self, request, uuid):text = generate_captcha_text()image = generate_captcha_image(text)# print(text) # 输出生成的验证码redis_conn = get_redis_connection('verify_code') # 保存图形验证码# setex 保存到redis中 并设置生存时间redis_conn.setex('img_%s' % uuid,constants.IMAGE_CODE_REDIS_EXPIRES, text)# 响应图形验证码return HttpResponse(image, content_type='image/jpg')
配置根路由
from django.contrib import admin
from django.urls import path, includefrom xiaoyu_mall.apps import verificationsurlpatterns = [path('admin/', admin.site.urls),# 导入path('', include('users.urls', namespace='users')),path('', include('contents.urls', namespace='contents')),path('',include('verifications.urls')),
]
配置verifications应用下子路由
from django.urls import path, re_path
# 变更了应用所在目录之后,不要使用这种方式导包
# from xiaoyu_mall.apps.users import views
# 推荐使用如下的相对路径来导包
from . import viewsurlpatterns = [path('image_codes/<uuid:uuid>', views.ImageCodeView.as_view(), name='register'),
]