03.1 用户注册功能2
一、前后端分离
使用json数据交互
1、结构设计
实际项目是多人协同开发,特别是前后端交互,后端返回数据结构要一致。
{"errno": "0", "errmsg": "OK", "data": {...}}
字段 | 类型 | 说明 |
---|---|---|
errno | 字符串 | 错误编码 |
errmsg | 字符串 | 错误信息 |
data | 返回数据 |
在项目根目录中utils文件夹下创建 utils/response/response_code.py文件,用于定义错误编码,代码如下:
class Code:
OK = "0"
DBERR = "4001"
NODATA = "4002"
DATAEXIST = "4003"
DATAERR = "4004"
METHERR = "4005"
SMSERROR = "4006"
SMSFAIL = "4007"
SESSIONERR = "4101"
LOGINERR = "4102"
PARAMERR = "4103"
USERERR = "4104"
ROLEERR = "4105"
PWDERR = "4106"
SERVERERR = "4500"
UNKOWNERR = "4501"
error_map = {
Code.OK: "成功",
Code.DBERR: "数据库查询错误",
Code.NODATA: "无数据",
Code.DATAEXIST: "数据已存在",
Code.DATAERR: "数据错误",
Code.METHERR: "方法错误",
Code.SMSERROR: "发送短信验证码异常",
Code.SMSFAIL: "发送短信验证码失败",
Code.SESSIONERR: "用户未登录",
Code.LOGINERR: "用户登录失败",
Code.PARAMERR: "参数错误",
Code.USERERR: "用户不存在或未激活",
Code.ROLEERR: "用户身份错误",
Code.PWDERR: "密码错误",
Code.SERVERERR: "内部错误",
Code.UNKOWNERR: "未知错误",
}
2、快捷方法
为了方便定义一个快捷方法,在utils目录下创建utils/response/response_json.py文件文件代码如下:
import datetime
from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder
from .response_code import Code
# json编码器
# 自定义序列化器,处理时间字段
class MyJSONEncoder(DjangoJSONEncoder):
def default(self, o):
if isinstance(o, datetime.datetime):
# 序列化数据,将datetime时间转为字符串
return o.astimezone().strftime('%Y-%m-%d %H:%M:%S') # 转换为本地时间
else:
return super().default(o)
def json_response(errno=Code.OK, errmsg='', data=None, kwargs=None):
'''
返回json数据格式
:param errno:
:param errmsg:
:param data:
:param kwargs:
:return:
'''
json_dict = {
'errno': errno,
'errmsg': errmsg,
'data': data,
}
if kwargs and isinstance(kwargs, dict):
json_dict.update(kwargs)
return JsonResponse(json_dict, encoder=MyJSONEncoder)
二、用户名检测
1、接口设计
接口说明:
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /check_username/(?P |
参数格式 | url路径参数 |
参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
username | 字符串 | 是 | 输入的用户名 |
返回结果:
{
"errno": "0",
"errmsg": "OK",
"data": {
"username":username,
"count":1
}
}
2、后端代码
1.创建新的app verification专门用来处理验证
cd ~/code/tztz/apps
python ../manage.py startapp verification
别忘了在settings文件中注册app
2.verification/views.py代码
def check_username_view(request, username):
count = User.objects.filter(username=username).count()
data = {
'username': username,
'count': count,
}
return json_response(data=data)
3、路由
re_path('check_username/(?P\D\w{5,19})/', views.check_username_view, name='check_username'),
三、手机号码检测
1、接口设计
接口说明:
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /check_phone/(?P |
参数格式 | url路径参数 |
参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
phone | 字符串 | 是 | 输入的手机号码 |
返回结果:
{
"errno": "0",
"errmsg": "OK",
"data": {
"phone":phone,
"count":1
}
}
2、后端代码
verification/views.py代码
def check_phone_view(request, phone):
count = User.objects.filter(phone=phone).count()
data = {
'phone': phone,
'count': count,
}
return json_response(data=data)
路由
re_path('check_phone/(?P1[3-9]\d{9})/', views.check_phone_view, name='check_phone'),
四、获取短信验证码
1.业务流程分析
- 检查图片验证码是否正确
- 检查是否在60s内发送记录
- 生成短信验证码
- 发送短信
- 保存短信验证码与发送记录
2.接口设计
接口说明:
类目 | 说明 |
---|---|
请求方法 | POST |
url定义 | /sms_code/ |
参数格式 | 表单 |
参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
phone | 字符串 | 是 | 用户输入的手机号码 |
captcha | 字符串 | 是 | 用户输入的验证码文本 |
返回结果:
{
"errno": "0",
"errmsg": "发送短信验证码成功!",
}
3.后端代码
class Check_smscode_view(View):
'''
发送短信验证码
'''
def post(self, request):
# 1、图片验证码、手机号码校验
form = forms.CheckPhoneForm(request.POST, request=request)
if form.is_valid():
# 2、生成手机验证码
# 获取手机号码
phone = form.cleaned_data.get('phone')
sms_code = ''.join([str(random.randint(0, 9)) for _ in range(4)])
# print(phone, sms_code)
# # 3、发送手机验证码(云通讯)
# ccp = CCP()
# try:
# res = ccp.send_template_sms(phone, [sms_code, constants.SMS_CODE_EXPIRES_M], "1")
# if res == 0:
# logger.info('发送短信验证码[正常][mobile: %s sms_code: %s]' % (phone, sms_code))
# else:
# logger.error('发送短信验证码[失败][moblie: %s sms_code: %s]' % (phone, sms_code))
# return json_response(errno=Code.SMSFAIL, errmsg=error_map[Code.SMSFAIL])
# except Exception as e:
# logger.error('发送短信验证码[异常][mobile: %s message: %s]' % (phone, e))
# return json_response(errno=Code.SMSERROR, errmsg=error_map[Code.SMSERROR])
# 4、保存手机验证码到redis数据库
sms_flag_key = 'sms_flag_{}'.format(phone)
sms_code_key = 'sms_code_{}'.format(sms_code)
redis_conn = get_redis_connection(alias='verify_code')
p1 = redis_conn.pipeline()
try:
p1.setex(sms_flag_key, constants.SMS_CODE_INTERVAL, 1)
p1.setex(sms_code_key, constants.SMS_CODE_EXPIRES_S, sms_code)
p1.execute()
logger.info('短信验证码发送成功,{}:{}'.format(phone, sms_code))
return json_response(errmsg='短信验证码发送成功!')
except Exception as e:
logger.error('redis 执行异常:{}'.format(e))
return json_response(errno=Code.UNKOWNERR, errmsg=error_map[Code.UNKOWNERR])
else:
errmsg = form.get_errors()
return json_response(errno=Code.DATAERR, errmsg=errmsg)
forms.py
from django import forms
from django.core.validators import RegexValidator
from django_redis import get_redis_connection
from user.models import User
# 创建手机号码的正则校验器
phone_validator = RegexValidator(r'^1[3-9]\d{9}$', '手机号码格式不正确')
class CheckPhoneForm(forms.Form):
'''
手机号码及图片验证码校验
'''
# 将request对象传入校验类
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs)
captcha = forms.CharField(max_length=4,
min_length=4,
error_messages={
'max_length': '图片验证码长度有误',
'min_length': '图片验证码长度有误',
'required': '图片验证码不能为空',
}
)
phone = forms.CharField(max_length=11,
min_length=11,
validators=[phone_validator, ],
error_messages={
'max_length': '手机号码长度有误',
'min_length': '手机号码长度有误',
'required': '手机号码不能为空',
})
def clean(self):
clean_data = super().clean()
phone = clean_data.get('phone')
captcha = clean_data.get('captcha')
# 1、 校验手机号码是否已经存在
if User.objects.filter(phone=phone).count():
raise forms.ValidationError('手机号码已被注册,请重新输入!')
# 2、校验图片验证码
image_code = self.request.session.get('image_code')
if (not image_code) or (image_code.upper() != captcha.upper()):
raise forms.ValidationError('图片验证码校验失败')
# 3、校验是否在60秒内已经发送过短信
redis_conn = get_redis_connection(alias='verify_code')
if redis_conn.get('sms_flag_{}'.format(phone)):
raise forms.ValidationError('获取短信验证码过于频繁')
return clean_data
def get_errors(self):
errors = self.errors.get_json_data()
err_msg_list = []
for item in errors.values():
for ite in item:
err_msg_list.append(ite.get('message'))
err_msg_str = '/'.join(err_msg_list)
return err_msg_str
常量 constants.py
#!/usr/bin/python
# -*- coding: utf-8 -*
# 图片验证码过期时间 单位秒
IMAGE_CODE_EXPIRES = 60 * 5
# 短信验证码长度
SMS_CODE_LENGTH = 4
# 短信验证码发送间隔 秒
SMS_CODE_INTERVAL = 60
# 短信验证码过期时间 分
SMS_CODE_EXPIRES_M = 5
# 短信验证码过期时间 秒
SMS_CODE_EXPIRES_S = 60*SMS_CODE_EXPIRES_M
# 短信发送模板
SMS_CODE_TEMP_ID = 1
# session过期时间,默认7天
USER_SESSION_EXPIRY = 7*24*60*60
4、短信验证码平台-云通讯
本项目中使用的短信验证码平台为云通讯平台,文档参考地址
主要是因为可以免费测试,注册后赠送8元用于测试。
开发参数:
_accountSid = '开发者主账号中的ACCOUNT SID'
# 说明:主账号Token,登陆云通讯网站后,可在控制台-应用中看到开发者主账号AUTH TOKEN
_accountToken = '开发者主账号中的AUTH TOKEN'
# 请使用管理控制台首页的APPID或自己创建应用的APPID
_appId = '开发者主账号中的AppID(默认)'
# 说明:请求地址,生产环境配置成app.cloopen.com
_serverIP = 'sandboxapp.cloopen.com'
# 说明:请求端口 ,生产环境为8883
_serverPort = "8883"
# 说明:REST API版本号保持不变
_softVersion = '2013-12-26'
设置测试手机号码
五、注册功能
1、业务流程分析
- 判断用户名是否为空,是否已注册
- 判断密码是否为空,格式是否正确
- 判断两次密码是否一致
- 判断手机号码是否为空,格式是否正确
- 判断短信验证码是否为空,格式是否正确,是否与真实短信验证码相同
2、接口设计
接口说明:
类目 | 说明 |
---|---|
请求方法 | POST |
url定义 | /register/ |
参数格式 | 表单 |
注意:post请求,前端请求要带上csrf token
参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
username | 字符串 | 是 | 用户输入的用户名 |
password | 字符串 | 是 | 用户输入的密码 |
password_repeat | 字符串 | 是 | 用户输入的重复密码 |
mobile | 字符串 | 是 | 用户输入的手机号码 |
sms_code | 字符串 | 是 | 用户输入的短信验证码 |
返回结果:
{
"errno": "0",
"errmsg": "恭喜您,注册成功!",
}
3、后端代码
user/views.py代码:
class User_register(View):
'''
用户注册类视图
'''
def get(self, request):
return render(request, 'user/register.html')
def post(self, request):
form = Checkregist(request.POST)
# 校验参数
if form.is_valid():
# 校验参数成功
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
phone = form.cleaned_data.get('phone')
# 创建新用户
User.objects.create_user(username=username, password=password, phone=phone)
# 返回成功
return json_response(errmsg='注册成功,请登录')
else:
errmsg = form.get_errors()
return json_response(errno=Code.DATAERR, errmsg=errmsg)
forms.py
import re
from django import forms
from django.core.validators import RegexValidator
from django_redis import get_redis_connection
from django.db.models import Q
from .models import User
from verification import constants
# 创建手机号码的正则校验器
phone_validator = RegexValidator(r'^1[3-9]\d{9}$', '手机号码格式不正确')
username_validator = RegexValidator(r'^\D\w{5,19}$', '请输入以非数字开头的用户名,6-20个字符')
class Checkregist(forms.Form):
'''
注册校验
'''
username = forms.CharField(max_length=20,
min_length=6,
validators=[username_validator, ],
error_messages={
'max_length': '用户名长度有误',
'min_length': '用户名长度有误',
'required': '用户名不能为空',
}
)
password = forms.CharField(max_length=20,
min_length=6,
error_messages={
'max_length': '密码长度有误',
'min_length': '密码长度有误',
'required': '密码不能为空',
}
)
password_repeat = forms.CharField(max_length=20,
min_length=6,
error_messages={
'max_length': '密码长度有误',
'min_length': '密码长度有误',
'required': '密码不能为空',
}
)
phone = forms.CharField(max_length=11,
min_length=11,
validators=[phone_validator, ],
error_messages={
'max_length': '手机号码长度有误',
'min_length': '手机号码长度有误',
'required': '手机号码不能为空',
}
)
sms_code = forms.CharField(max_length=constants.SMS_CODE_LENGTH,
min_length=constants.SMS_CODE_LENGTH,
error_messages={
'max_length': '短信验证码长度有误',
'min_length': '短信验证码长度有误',
'required': '短信验证码不能为空',
})
def clean_username(self):
'''
校验用户名
:return:
'''
username = self.cleaned_data.get('username')
if User.objects.filter(username=username).exists():
raise forms.ValidationError('用户名已经存在')
return username
def clean_phone(self):
'''
校验手机号码
:return:
'''
phone = self.cleaned_data.get('phone')
if User.objects.filter(phone=phone).exists():
raise forms.ValidationError('手机号码已被注册')
return phone
def clean(self):
'''
校验密码
:return:
'''
clean_data = super().clean()
password = self.cleaned_data.get('password')
password_repeat = self.cleaned_data.get('password_repeat')
if password != password_repeat:
raise forms.ValidationError('两次密码不一致')
# 校验短信验证码
sms_code = clean_data.get('sms_code')
redis_conn = get_redis_connection(alias='verify_code')
p1 = redis_conn.pipeline()
real_code = redis_conn.get('sms_code_{}'.format(sms_code))
if (not real_code) or (real_code.decode('utf-8') != sms_code):
raise forms.ValidationError('短信验证码错误!')
return clean_data
def get_errors(self):
errors = self.errors.get_json_data()
err_msg_list = []
for item in errors.values():
for ite in item:
err_msg_list.append(ite.get('message'))
err_msg_str = '/'.join(err_msg_list)
return err_msg_str
4、前端html
{% extends 'base/base.html' %}
{% load static %}
{% block title %}注册{% endblock title %}
{% block link %}
{% endblock link %}
{% block main %}
请注册
立即登录 >
{% endblock main %}
{#{% block commonjs %}#}
{#{% endblock %}#}
{% block otherjs %}
{# #}
{% endblock otherjs %}
5、前端js
$(function () {
// 定义状态变量
let isUsernameReady = false,
isPasswordReady = false,
isPhoneReady = false,
isImagecodeRead = false;
// 1.点击刷新图像验证码
let $img = $('.form-contain .form-item .captcha-graph-img img');
$img.click(function () {
isImagecodeRead = false;
// $img.attr('src', '/imagecode/?rand=' + Math.random())
$img.attr('src', '/imagecode/?rand=' + new Date().getTime());
});
// 2.鼠标离开用户名输入框校验用户名
let $username = $('#username');
$username.blur(fnCheckUsername);
function fnCheckUsername () {
isUsernameReady = false;
let sUsername = $username.val(); //获取用户字符串
if (sUsername === ''){
message.showError('用户名不能为空!');
return
}
if (!(/^\D\w{5,19}$/).test(sUsername)){
message.showError('请输入以非数字开头的用户名,6-20个字符');
return
}
$.ajax({
url: '/check_username/' + sUsername + '/',
type: 'GET',
dataType: 'json',
success: function (data) {
if(data.data.count !== 0){
message.showError(data.data.username + '已经注册,请重新输入!')
}else {
message.showInfo(data.data.username + '可以正常使用!');
isUsernameReady = true
}
},
error: function (xhr, msg) {
message.showError('服务器超时,请重试!')
}
});
}
// 3.检测密码,检测两次密码是否一致
let $passwordRepeat = $('input[name="password_repeat"]');
$passwordRepeat.blur(fnCheckPassword);
function fnCheckPassword () {
isPasswordReady = false;
let password = $('input[name="password"]').val();
let passwordRepeat = $passwordRepeat.val();
if (password === '' || passwordRepeat === ''){
message.showError('密码不能为空');
return
}
if (password !== passwordRepeat){
message.showError('两次密码输入不一致');
return
}
if (password === passwordRepeat){
isPasswordReady = true
}
}
// 4.检查手机号码是否可用
let $phone = $('input[name="phone"]');
$phone.blur(fnCheckPhone);
function fnCheckPhone () {
isPhoneReady = false;
let sPhone = $phone.val();
if(sPhone === ''){
message.showError('手机号码不能为空');
return
}
if(!(/^1[3-9]\d{9}$/).test(sPhone)){
message.showError('手机号码格式不正确');
return
}
$.ajax({
url: '/check_phone/' + sPhone + '/',
type: 'GET',
dataType: 'json',
success: function (data) {
if(data.data.count !== 0){
message.showError(data.data.phone + '已经注册,请重新输入!')
}else {
message.showInfo(data.data.phone + '可以正常使用!');
isPhoneReady = true;
}
},
error: function (xhr, msg) {
message.showError('服务器超时,请重试!')
}
});
}
// 5、点击发送手机验证码
let $smsButton = $('.sms-captcha');
$smsButton.click(function () {
isImagecodeRead = false;
if ($smsButton.attr('cantclick')==='false'){
let sCaptcha = $('input[name="captcha_graph"]').val();
if (sCaptcha === '')
{
message.showError('请输入验证码');
return
}
if (sCaptcha.length !== 4)
{
message.showError('图形验证码格式错误');
return
}
if (!isPhoneReady)
{
fnCheckPhone();
return
}
$
.ajax({
url: '/sms_code/',
type: 'POST',
data:{
phone:$phone.val(),
captcha:sCaptcha
},
dataType: 'json'
})
.done((data)=>{
if (data.errno !== '0'){
message.showError(data.errmsg);
}else {
isImagecodeRead = true;
message.showSuccess(data.errmsg);
var num=60;
$smsButton.attr('cantclick',true);
let t = setInterval(function () {
$smsButton.html(num)
num--;
if(num === 0)
{
clearInterval(t);
$smsButton.attr('cantclick',false);
$smsButton.html('获取短信验证码')
}
},1000)
}
})
.fail(()=>{
message.showError('服务器超时,请刷新重试!')
});
}});
// 6、提交表单
let $suumitBnt = $('.register-btn');
$suumitBnt.click((event)=>{
// 阻止表单提交
event.preventDefault();
if(!isUsernameReady)
{
fnCheckUsername();
return
}
if(!isPasswordReady)
{
fnCheckPassword();
return
}
if(!isPhoneReady)
{
fnCheckPhone();
return
}
if(isImagecodeRead)
{
// 短信验证码格式校验
let sSmscode = $('input[name="sms_captcha"]').val();
if(sSmscode === '')
{
message.showError('短信验证码不能为空!');
return
}
if(!(/^\d{4}$/).test(sSmscode)){
message.showError('短信验证码格式有误');
return
}
$
.ajax({
url:'/register/',
type:'POST',
data:{
username:$username.val(),
password:$('input[name="password"]').val(),
password_repeat:$passwordRepeat.val(),
phone:$phone.val(),
sms_code:sSmscode
},
dataType:'json'
})
.done((resp)=> {
if (resp.errno === '0') {
message.showSuccess(resp.errmsg);
setTimeout(function () {
//注册成功后重定向到登录页面
window.location.href = '/login/';
}, 2000)
}
else {
//注册失败
message.showError(resp.errmsg)
}
}
)
.fail(()=>{
message.showError('服务器超时,请重试!')
}
)
}})
});