Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)

1、短信验证码前端逻辑

Vue绑定短信验证码界面

templates/register.html 前端注册界面

{#  templates/register.html  前端注册界面  #}
{% load static %}

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<title>LG商城-注册title>
	<link rel="stylesheet" type="text/css" href="{% static 'css/reset.css' %}">
	<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}">
head>
<body>

<div id="app">
	<div class="register_con">
		<div class="l_con fl">
			<a href="index.html" class="reg_logo"><img src="{% static 'images/1.png' %}">a>
			<div class="reg_slogan">商品美 · 种类多 · 欢迎光临div>
			<div class="reg_banner">div>
		div>
		<div class="r_con fr">
			<div class="reg_title clearfix">
				<h1>用户注册h1>
				<a href="login.html">登录a>
			div>
			<div class="reg_form clearfix">

				<form method="post" class="register_form" v-cloak @submit="on_submit">
                    {% csrf_token %}
					<ul>
						<li>
							<label>用户名:label>

							<input type="text" name="username" v-model="username" @blur="check_username" id="user_name">

                            <span class="error_tip" v-show="error_name">[[ error_name_message ]]span>
						li>					
						<li>
							<label>密码:label>

							<input type="password" name="password" v-model="password" @blur="check_password" id="pwd">
							<span class="error_tip" v-show="error_password">请输入8-20位的密码span>
						li>
						<li>
							<label>确认密码:label>

							<input type="password" name="password2" v-model="password2" @blur="check_password2" id="cpwd">
							<span class="error_tip" v-show="error_password2">两次输入的密码不一致span>
						li>
						<li>
							<label>手机号:label>
                            
							<input type="text" name="mobile" v-model="mobile" @blur="check_mobile" id="phone">

							<span class="error_tip" v-show="error_mobile">[[ error_mobile_message ]]span>
						li>
						<li>
							<label>图形验证码:label>
{#                             v-model="image_code" @blur="check_image_code"绑定验证码的格式   #}
							<input type="text" name="image_code" id="pic_code" class="msg_input" v-model="image_code" @blur="check_image_code" >
{#                           图形验证码路径进行绑定 :src="image_code_url"  点击变换@click="generate_image_code"   #}
							<img :src="image_code_url" @click="generate_image_code" alt="图形验证码" class="pic_code">
{#							 错误信息显示error_image_code_message     绑定js文件中的错误信息名称v-show="error_image_code" #}
                            <span class="error_tip" v-show="error_image_code">[[ error_image_code_message ]]span>
						li>
						<li>
							<label>短信验证码:label>
{#                            v-model="sms_code"绑定, @blur="check_sms_code"绑定短信验证码的格式    #}
							<input type="text" name="sms_code" id="msg_code" class="msg_input" v-model="sms_code" @blur="check_sms_code">
{#                            点击触发获取短信验证码方法   #}
							<a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]a>
{#                            绑定v-show,用于控制显示错误信息;error_sms_code_message错误信息   #}
							<span class="error_tip" v-show="error_sms_code">[[ error_sms_code_message ]]span>
						li>
						<li class="agreement">

							<input type="checkbox" name="allow" v-model="allow" @change="check_allow"  id="allow">
							<label>同意”LG商城用户使用协议“label>
						
							<span class="error_tip" v-show="error_allow">请勾选用户协议span>

{# 这部分传注册错误信息到前端界面是用的Django自带的,前端验证form表单信息我们采用的是  @submit="on_submit" 中的ajax进行验证,这是两种方式,选择一种就可以 #}
                        {#  将apps/users/views.py文件中的注册错误信息context进行循环   #}
                            <span class="error_tip">
                                {% if form_errors %}
                                    {% for key,error in form_errors.items %}
                                            {{ error }}
                                    {% endfor %}
                                {% endif %}
                            {# 保存用户注册数据失败的信息apps/users/views.py中传递的register_error_message             #}
                                {% if register_error_message %}
                                    {{ register_error_message }}
                                {% endif %} 
                            span>

						li>
						<li class="reg_sub">
							<input type="submit" value="注 册">
						li>
					ul>				
				form>
			div>
		div>
	div>
	<div class="footer no-mp">
		<div class="foot_link">
			<a href="#">关于我们a>
			<span>|span>
			<a href="#">联系我们a>
			<span>|span>
			<a href="#">招聘人才a>
			<span>|span>
			<a href="#">友情链接a>		
		div>
		<p>CopyRight © 2016 北京LG商业股份有限公司 All Rights Reservedp>
		<p>电话:010-****888    京ICP备*******8号p>
	div>
div>
    
    <script src="{% static 'js/vue-2.5.16.js' %}">script>
    <script src="{% static 'js/axios-0.18.0.min.js' %}">script>

    {#   生成uuid的文件 需要先加载common.js文件,再加载注册界面js #}
    <script src="{% static 'js/common.js' %}">script>
    
    <script src="{% static 'js/register.js' %}">script>

body>
html>

static/js/register.js文件,验证注册界面的逻辑

// 实例化Vue的对象   static/js/register.js文件,验证注册界面的逻辑
let vm = new Vue({
    el:"#app",
    // 修改Vue读取变量的语法 {{}}  [[]],Vue中可能不识别Django的{{}}语法形式
    delimiters: ['[[', ']]'],

    data: {
        // v-model绑定名称
        username:"",    //绑定前端界面的username标签
        password:"",
        password2:"",
        mobile:"",
        allow:"",
        image_code_url:"",     // 验证码绑定的路由
        uuid:"",               // 采用common.js文件方法生成的uuid
        image_code:"",         // 检测验证码的格式
        sms_code:"",           // 短信验证码名称
        sms_code_tip:"获取短信验证码",

        // v-show绑定的名称,默认false不显示
        error_name: false,
        error_password: false,
        error_password2: false,
        error_mobile: false,
        error_allow: false,
        error_image_code: false,
        error_sms_code:false,

        // 绑定的错误信息
        error_name_message:"",
        error_mobile_message:"",
        error_image_code_message:"",     // 图形验证码错误信息
        error_sms_code_message:"",       // 短信验证码错误信息
    },

    // Vue的生命周期,页面加载完成后被调用该方法,验证码生成
        mounted(){
            this.generate_image_code();
        },

    methods: {
        // 验证码点击更换方法,且需要在页面加载完成时候,该方法已经执行
        generate_image_code(){
            // 生成uuid,调用common.js方法
            this.uuid = generateUUID()
            // 验证码路由拼接
            this.image_code_url = "/image_codes/"+ this.uuid + "/"
        },

        // 检验短信验证码,和图形验证码的方法逻辑相同
        check_sms_code(){
            if(this.sms_code.length != 6){
                this.error_sms_code_message = '短信验证码长度不正确'
                this.error_sms_code = true      // 前端界面v-show=true,展示错误信息
            }else{
                this.error_sms_code = false     // 不显示信息
            }
        },

        // 定义发送短信验证码方法
        send_sms_code(){
            // 校验数据是否填写,手机号,短信验证码
            this.check_mobile();
            this.check_image_code();
            if (this.error_mobile == true || this.error_image_code == true) {
                return;
            }

            // 'sms_codes/(?P1[3-9]\d{9})/' 需要拼接这样的路由:sms_codes/132xxx/?image_code='xxxx'&uuid='xxxxx'
            let url = 'sms_codes/' + this.mobile + '/?image_code=' + this.image_code + '&uuid=' + this.uuid

            // 路由拼接完成,发送请求
            axios.get(url, {
                responseType: 'json'
            })
            .then(response => {                   // 请求成功
                // 返回数据:JsonResponse({"code": RETCODE.OK, 'errmsg': "发送短信验证码成功"})
                if (response.data.code == '0'){         // 后端返回的状态码为'0',即代表获取短信验证码成功
                    // 60s倒计时显示验证码有效期  需要用到定时器
                    let num = 60
                    let t = setInterval(() => {        // 定时器,匿名函数
                        if(num == 1){
                            // 定时器停止
                            clearInterval(t)
                            this.sms_code_tip = '获取短信验证码'
                            this.generate_image_code();        // 重新生成图形验证码
                        }else{
                            num -= 1                           // 继续读秒
                            this.sms_code_tip = num + '秒'
                        }
                    }, 1000)    // 1000毫秒
                }else{                                   // 后端返回的状态码不为'0',即代表获取短信验证码失败
                    if (response.data.code == '4001'){
                        // 图形验证码输入有误,apps/verifications/views.py中的JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码输入有误!"})
                        this.error_image_code_message = response.data.errmsg
                        this.error_image_code = true          // 显示错误信息
                    }
                }
            })
            .catch(error => {                     // 请求错误
                console.log(error.response)
            })



        },

        // 定义方法  定义标签失去焦点的方法
        // check_username:function () {
        // }

        // @blur="check_username"方法
        check_username(){
            // 正则表达式  5-20位字符数字组成
            let re = /^[a-zA-Z0-9_-]{5,20}$/;
            if(re.test(this.username)){
                // 匹配成功   错误信息不展示
                this.error_name = false
            }else{
                this.error_name = true  // v-show为true,显示信息
                this.error_name_message = "请输入5-20个字符的用户"
            }

            // 使用Vue的ajax进行表单验证
            if(this.error_name == false){        // 用户名正确情况
                // http://127.0.0.1:8000/users/usernames/用户名/count      路由格式
                let url = '/users/usernames/'+ this.username + '/count'    // url拼接

                // Vue发送ajax请求
                axios.get(url, {
                    responseType:'json'
                })
                // 请求成功
                .then(response=> {                // .then(function(response))
                    // 从apps/users/views.py文件返回的JsonResponse({"code": 0, "errmsg": "OK", "count": count})
                    if(response.data.count == 1 ){  // 用户名已经存在   count数据就是在views.py文件中传出的
                        this.error_name_message = '用户名已经存在'
                        this.error_name = true
                    }else{
                        this.error_name = false      //可以继续注册其他的字段
                    }
                })
                // 请求失败
                .catch(error =>{
                    console.log(error.response)         // 前端界面打印error.response
                })
            }
        },

        // @blur="check_password"
        check_password(){
            let re = /^[a-zA-Z0-9]{8,20}$/;
            if(re.test(this.password)){
                this.error_password = false
            }else{
                this.error_password = true
            }
        },

        //  @blur="check_password2"
        check_password2(){
            // 保持一致就行
            if(this.password2 != this.password){
                this.error_password2 = true
            }else{
                this.error_password2 = false
            }
        },

        // @blur="check_mobile"
        check_mobile(){
            let re = /^1[3456789]\d{9}$/;
            if(re.test(this.mobile)){
                this.error_mobile = false
            }else{
                this.error_mobile = true
                this.error_mobile_message = "请输入正确格式手机号!"
            }
        },

        // @change="check_allow"
        check_allow(){                        // checkbox的选中状态
            if(!this.allow){                  // allow是个空的bool类型值
                this.error_allow = true
            }else{
                this.error_allow = false
            }
        },

        // on_submit 表单提交
        on_submit(){
            // 如果表单验证中有true就说明有错误信息,不能提交
            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
            }
        },

        // 检测验证码格式方法
        check_image_code(){
            if(this.image_code.length != 4){
                this.error_image_code_message = '图形验证码长度不正确'
                this.error_image_code = true      // 前端界面v-show=true,展示错误信息
            }else{
                this.error_image_code = false     // 不显示信息
            }
        },

    },
});

apps/verifications/views.py 验证码视图文件

"""
apps/verifications/views.py   验证码视图文件
"""
from django.shortcuts import render
from django.views import View
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from .libs.captcha.captcha import captcha      # 调用实例化对象captcha
from django_redis import get_redis_connection   # 连接redis
import random
from verifications.libs.ronglianyun.ccp_sms import CCP    # 导入发送短信验证码类
from verifications import constants                       # 导入定义软编码文件
from utils.response_code import RETCODE                   # 导入定义状态码文件


# 图形验证码
class ImageCodeView(View):
    """图像验证码"""
    
    def get(self, request, uuid):
        """
        :param uuid:用于标识该验证码属于哪个用户,用户注册时期没有id标识,用uuid不会产生重复的id
        :return:
        """
        #  生成验证码,从生成验证码文件中调用实例化对象的方法
        text, image = captcha.generate_captcha()
        
        # 先连接redis,再保存到redis
        redis_conn = get_redis_connection('verify_code')  # verify_code是dev.py配置文件中的redis配置参数
        # redis_conn.set('img_%s' % uuid, text)   # set(key,value), 这种保存redis数据,无法设置保存时效
        redis_conn.setex('img_%s' % uuid, constants.IMAGE_CODE_REDIS_EXPIRES, text)   # set(key,expries,value),expries是时效,以秒s为单位
        
        # 响应图形验证码
        # return HttpResponse('image/jpg')
        return HttpResponse(image, content_type='image/jpg')


# 手机验证码
class SMSCodeView(View):
    """短信验证码"""
    def get(self, request, mobile):
        """
        :param mobile: 手机号
        :return:   json数据类型
        """
        # 接收参数
        image_code_client = request.GET.get('image_code')        # image_code_client是字符串数据类型
        uuid = request.GET.get('uuid')
        
        # 校验参数   image_code_client, uuid必须都存在
        if not all([image_code_client, uuid]):
            return HttpResponseForbidden('缺少必传参数')
         
        # 创建连接到redis的对象
        # 提取图形验证码
        redis_conn = get_redis_connection('verify_code')
        image_code_server = redis_conn.get('img_%s' % uuid)      # 此时的image_code_server是字节数据类型
        # print(type(image_code_server))
        # print(type(image_code_client))
        
        # 判断图形验证码的有无
        if image_code_server is None:
            return JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码已经失效了"})
        
        # 删除图形验证码,避免恶意测试图形验证码
        redis_conn.delete('img_%s' % uuid)
        
        # 对比图形验证码  .lower()都转成小写
        if image_code_client.lower() != image_code_server.decode().lower():      # decode()方法将字节数据转换成str
            return JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码输入有误!"})   # RETCODE.IMAGECODEERR定义的状态码
        
        # 随机生成短信验证码:生成6位数验证码
        sms_code = "%06d" % random.randint(0, 999999)  # 06d代表前几位可以用0补充
        # print(sms_code)
        # 生成验证码的另一种写法
        # for i in range(6):
        #     sms_code += str(random.randint(0, 9))
        
        # 保存短信验证码
        redis_conn.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)   # 保存300s
        # 发送短信验证码send_message(mobile, datas, tid)中tid是模板,默认为1,datas是数据和保存时效;constants.SMS_CODE_REDIS_EXPIRES//60 双//为了得到整数
        CCP().send_message(mobile, (sms_code, constants.SMS_CODE_REDIS_EXPIRES//60), 1)
        # 响应结果
        return JsonResponse({"code": RETCODE.OK, 'errmsg': "发送短信验证码成功"})   # RETCODE.OK定义状态码

2、避免频繁发送短信验证码

存在的问题:

  • 虽然我们在前端界面做了60秒倒计时功能。
  • 但是恶意用户可以绕过前端界面向后端频繁请求短信验证码。
    解决办法:
  • 在后端也要限制用户请求短信验证码的频率。60秒内只允许一次请求短信验证码。
  • 在Redis数据库中缓存一个数值,有效期设置为60秒。

避免频繁发送短信验证码逻辑分析
Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)_第1张图片
避免频繁发送短信验证码逻辑实现
apps/verifications/views.py 验证码视图文件

"""
apps/verifications/views.py   验证码视图文件
"""
from django.shortcuts import render
from django.views import View
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from .libs.captcha.captcha import captcha      # 调用实例化对象captcha
from django_redis import get_redis_connection   # 连接redis
import random
from verifications.libs.ronglianyun.ccp_sms import CCP    # 导入发送短信验证码类
from verifications import constants                       # 导入定义软编码文件
from utils.response_code import RETCODE                   # 导入定义状态码文件


# 图形验证码
class ImageCodeView(View):
    """图像验证码"""
    
    def get(self, request, uuid):
        """
        :param uuid:用于标识该验证码属于哪个用户,用户注册时期没有id标识,用uuid不会产生重复的id
        :return:
        """
        #  生成验证码,从生成验证码文件中调用实例化对象的方法
        text, image = captcha.generate_captcha()
        
        # 先连接redis,再保存到redis
        redis_conn = get_redis_connection('verify_code')  # verify_code是dev.py配置文件中的redis配置参数
        # redis_conn.set('img_%s' % uuid, text)   # set(key,value), 这种保存redis数据,无法设置保存时效
        redis_conn.setex('img_%s' % uuid, constants.IMAGE_CODE_REDIS_EXPIRES, text)   # set(key,expries,value),expries是时效,以秒s为单位
        
        # 响应图形验证码
        # return HttpResponse('image/jpg')
        return HttpResponse(image, content_type='image/jpg')


# 手机验证码
class SMSCodeView(View):
    """短信验证码"""
    def get(self, request, mobile):
        """
        :param mobile: 手机号
        :return:   json数据类型
        """
        # 接收参数
        image_code_client = request.GET.get('image_code')        # image_code_client是字符串数据类型
        uuid = request.GET.get('uuid')
        
        # 校验参数   image_code_client, uuid必须都存在
        if not all([image_code_client, uuid]):
            return HttpResponseForbidden('缺少必传参数')
         
        # 创建连接到redis的对象
        # 提取图形验证码
        redis_conn = get_redis_connection('verify_code')
        
        # 判断短信验证码是否已经发送
        send_flag = redis_conn.get("send_flag_%s" % mobile)
        if send_flag:
            return JsonResponse({"code":111, 'errmsg': "短信验证码发送频繁"})
        
        image_code_server = redis_conn.get('img_%s' % uuid)      # 此时的image_code_server是字节数据类型
        # print(type(image_code_server))
        # print(type(image_code_client))
        
        # 判断图形验证码的有无
        if image_code_server is None:
            return JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码已经失效了"})
        
        # 删除图形验证码,避免恶意测试图形验证码
        redis_conn.delete('img_%s' % uuid)
        
        # 对比图形验证码  .lower()都转成小写
        if image_code_client.lower() != image_code_server.decode().lower():      # decode()方法将字节数据转换成str
            return JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码输入有误!"})   # RETCODE.IMAGECODEERR定义的状态码
        
        # 随机生成短信验证码:生成6位数验证码
        sms_code = "%06d" % random.randint(0, 999999)  # 06d代表前几位可以用0补充
        # print(sms_code)
        # 生成验证码的另一种写法
        # for i in range(6):
        #     sms_code += str(random.randint(0, 9))
        
        # 保存短信验证码
        redis_conn.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)   # 保存300s
        
        # 保存短信验证码是否已经发送,constants.SEND_SMS_CODE_FLAG保存时效60s   1是value,随意取,"send_flag_%s" % mobile是key
        redis_conn.setex("send_flag_%s" % mobile, constants.SEND_SMS_CODE_FLAG, 1)
        
        # 发送短信验证码send_message(mobile, datas, tid)中tid是模板,默认为1,datas是数据和保存时效;constants.SMS_CODE_REDIS_EXPIRES//60 双//为了得到整数
        CCP().send_message(mobile, (sms_code, constants.SMS_CODE_REDIS_EXPIRES//60), 1)
        # 响应结果
        return JsonResponse({"code": RETCODE.OK, 'errmsg': "发送短信验证码成功"})   # RETCODE.OK定义状态码

static/js/register.js文件,验证注册界面的逻辑

// 实例化Vue的对象   static/js/register.js文件,验证注册界面的逻辑
let vm = new Vue({
    el:"#app",
    // 修改Vue读取变量的语法 {{}}  [[]],Vue中可能不识别Django的{{}}语法形式
    delimiters: ['[[', ']]'],

    data: {
        // v-model绑定名称
        username:"",    //绑定前端界面的username标签
        password:"",
        password2:"",
        mobile:"",
        allow:"",
        image_code_url:"",     // 验证码绑定的路由
        uuid:"",               // 采用common.js文件方法生成的uuid
        image_code:"",         // 检测验证码的格式
        sms_code:"",           // 短信验证码名称
        sms_code_tip:"获取短信验证码",
        send_flag:false,       // 是否发送短信验证码标志

        // v-show绑定的名称,默认false不显示
        error_name: false,
        error_password: false,
        error_password2: false,
        error_mobile: false,
        error_allow: false,
        error_image_code: false,
        error_sms_code:false,

        // 绑定的错误信息
        error_name_message:"",
        error_mobile_message:"",
        error_image_code_message:"",     // 图形验证码错误信息
        error_sms_code_message:"",       // 短信验证码错误信息
    },

    // Vue的生命周期,页面加载完成后被调用该方法,验证码生成
        mounted(){
            this.generate_image_code();
        },

    methods: {
        // 验证码点击更换方法,且需要在页面加载完成时候,该方法已经执行
        generate_image_code(){
            // 生成uuid,调用common.js方法
            this.uuid = generateUUID()
            // 验证码路由拼接
            this.image_code_url = "/image_codes/"+ this.uuid + "/"
        },

        // 检验短信验证码,和图形验证码的方法逻辑相同
        check_sms_code(){
            if(this.sms_code.length != 6){
                this.error_sms_code_message = '短信验证码长度不正确'
                this.error_sms_code = true      // 前端界面v-show=true,展示错误信息
            }else{
                this.error_sms_code = false     // 不显示信息
            }
        },

        // 定义发送短信验证码方法
        send_sms_code(){
            // send_flag:false  是否已经发送短信验证码标志
            if (this.send_flag == true){
                return;                     // 当send_flag = true会跳出方法,不会调用
            }
            this.send_flag = true

            // 校验数据是否填写,手机号,短信验证码
            this.check_mobile();
            this.check_image_code();
            if (this.error_mobile == true || this.error_image_code == true) {
                return;
            }

            // 'sms_codes/(?P1[3-9]\d{9})/' 需要拼接这样的路由:sms_codes/132xxx/?image_code='xxxx'&uuid='xxxxx'
            let url = 'sms_codes/' + this.mobile + '/?image_code=' + this.image_code + '&uuid=' + this.uuid

            // 路由拼接完成,发送请求
            axios.get(url, {
                responseType: 'json'
            })
            .then(response => {                   // 请求成功
                // 返回数据:JsonResponse({"code": RETCODE.OK, 'errmsg': "发送短信验证码成功"})
                if (response.data.code == '0'){         // 后端返回的状态码为'0',即代表获取短信验证码成功
                    // 60s倒计时显示验证码有效期  需要用到定时器
                    let num = 60
                    let t = setInterval(() => {        // 定时器,匿名函数
                        if(num == 1){
                            // 定时器停止
                            clearInterval(t)
                            this.sms_code_tip = '获取短信验证码'
                            this.generate_image_code();        // 重新生成图形验证码
                            this.send_flag = false             // 需要重新发送
                        }else{
                            num -= 1                           // 继续读秒
                            this.sms_code_tip = num + '秒'
                        }
                    }, 1000)    // 1000毫秒
                }else{                                   // 后端返回的状态码不为'0',即代表获取短信验证码失败
                    if (response.data.code == '4001'){
                        // 图形验证码输入有误,apps/verifications/views.py中的JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码输入有误!"})
                        this.error_image_code_message = response.data.errmsg
                        this.error_image_code = true          // 显示错误信息
                    }
                    this.send_flag = false         // 需要重新发送
                }
            })
            .catch(error => {                     // 请求错误
                console.log(error.response)
            })

        },

        // 定义方法  定义标签失去焦点的方法
        // check_username:function () {
        // }

        // @blur="check_username"方法
        check_username(){
            // 正则表达式  5-20位字符数字组成
            let re = /^[a-zA-Z0-9_-]{5,20}$/;
            if(re.test(this.username)){
                // 匹配成功   错误信息不展示
                this.error_name = false
            }else{
                this.error_name = true  // v-show为true,显示信息
                this.error_name_message = "请输入5-20个字符的用户"
            }

            // 使用Vue的ajax进行表单验证
            if(this.error_name == false){        // 用户名正确情况
                // http://127.0.0.1:8000/users/usernames/用户名/count      路由格式
                let url = '/users/usernames/'+ this.username + '/count'    // url拼接

                // Vue发送ajax请求
                axios.get(url, {
                    responseType:'json'
                })
                // 请求成功
                .then(response=> {                // .then(function(response))
                    // 从apps/users/views.py文件返回的JsonResponse({"code": 0, "errmsg": "OK", "count": count})
                    if(response.data.count == 1 ){  // 用户名已经存在   count数据就是在views.py文件中传出的
                        this.error_name_message = '用户名已经存在'
                        this.error_name = true
                    }else{
                        this.error_name = false      //可以继续注册其他的字段
                    }
                })
                // 请求失败
                .catch(error =>{
                    console.log(error.response)         // 前端界面打印error.response
                })
            }
        },

        // @blur="check_password"
        check_password(){
            let re = /^[a-zA-Z0-9]{8,20}$/;
            if(re.test(this.password)){
                this.error_password = false
            }else{
                this.error_password = true
            }
        },

        //  @blur="check_password2"
        check_password2(){
            // 保持一致就行
            if(this.password2 != this.password){
                this.error_password2 = true
            }else{
                this.error_password2 = false
            }
        },

        // @blur="check_mobile"
        check_mobile(){
            let re = /^1[3456789]\d{9}$/;
            if(re.test(this.mobile)){
                this.error_mobile = false
            }else{
                this.error_mobile = true
                this.error_mobile_message = "请输入正确格式手机号!"
            }
        },

        // @change="check_allow"
        check_allow(){                        // checkbox的选中状态
            if(!this.allow){                  // allow是个空的bool类型值
                this.error_allow = true
            }else{
                this.error_allow = false
            }
        },

        // on_submit 表单提交
        on_submit(){
            // 如果表单验证中有true就说明有错误信息,不能提交
            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
            }
        },

        // 检测验证码格式方法
        check_image_code(){
            if(this.image_code.length != 4){
                this.error_image_code_message = '图形验证码长度不正确'
                this.error_image_code = true      // 前端界面v-show=true,展示错误信息
            }else{
                this.error_image_code = false     // 不显示信息
            }
        },

    },
});

定义软编码文件apps/verifications/constants.py

# -*- encoding: utf-8 -*-
"""
@File    : constants.py
@Time    : 2020/8/4 16:44
@Author  : chen

定义软编码文件:apps/verifications/constants.py
"""
# 图形验证码有效期   单位:s秒
IMAGE_CODE_REDIS_EXPIRES = 300

# 短信验证码有效期   单位:s秒
SMS_CODE_REDIS_EXPIRES = 300

# 短信模板
SEND_SMS_TEMPLATE_ID = 1

# 短信验证码是否重复发送
SEND_SMS_CODE_FLAG = 60

3、补充注册时短信验证后端逻辑

补充注册时短信验证后端逻辑

后端表单验证文件apps/users/forms.py

# -*- encoding: utf-8 -*-
"""
@File    : forms.py
@Time    : 2020/7/28 16:16
@Author  : chen

后端表单验证文件:apps/users/forms.py
"""
from django import forms
from .models import User


class RegisterForm(forms.Form):     # 继承自forms.Form是只需要验证个别字段的信息,而继承自model.Form是需要验证所有的字段
    username = forms.CharField(max_length=20, min_length=5, error_messages={"min_length": "用户名长度不对",
                                                                            "max_length": "用户名长度不对"})
    password = forms.CharField(max_length=20, min_length=8, error_messages={"min_length": "密码长度不对",
                                                                            "max_length": "密码长度不对",
                                                                            "required": "密码必须填写"})
    password2 = forms.CharField(max_length=20, min_length=8, error_messages={"min_length": "密码长度不对",
                                                                            "max_length": "密码长度不对",
                                                                            "required": "密码必须填写"})
    allow = forms.BooleanField()
    mobile = forms.CharField(max_length=11, min_length=11, required=True, error_messages={"min_length": "手机号长度不对",
                                                                            "max_length": "手机号长度不对",
                                                                            "required": "手机号必须填写"})
    sms_code = forms.CharField(max_length=6, min_length=6, required=True)
    
    # 检测用户名是否已经在数据库中
    def clean_username(self):
        username = self.cleaned_data.get('username')
        username_exists = User.objects.filter(username=username).exists()      # 检查数据库中username是否存在
        if username_exists:
            raise forms.ValidationError('用户名已经存在')
        return username

    # 检测手机号是否已经在数据库中
    def clean_mobile(self):
        mobile = self.cleaned_data.get('mobile')
        mobile_exists = User.objects.filter(mobile=mobile).exists()  # 检查数据库中手机号是否存在
        if mobile_exists:
            raise forms.ValidationError('手机号已经存在')
        return mobile
    
    # 验证两次密码是否一致
    def clean(self):
        cleaned_data = super().clean()                   # 调用父类的clean方法
        password = cleaned_data.get('password')
        password2 = cleaned_data.get('password2')
        if password != password2:
            raise forms.ValidationError("两次密码不一致")
        return cleaned_data                              # 可以不返回值,建议使用返回值

apps/users/views.py文件,用户后端验证视图文件

"""
视图文件

apps/users/views.py文件,用户后端验证视图文件
"""
from django.shortcuts import render, redirect, reverse
from django.http import HttpResponse, JsonResponse
from django.views import View
from .forms import RegisterForm
from .models import User
from django.contrib.auth import login
from django_redis import get_redis_connection


class RegisterView(View):
    """用户注册"""
    def get(self, request):
        """提供用户的注册界面"""
        return render(request, "register.html")
    
    def post(self, request):
        """提供用户的注册逻辑"""
        # 前端用户提交数据
        form = RegisterForm(request.POST)
        if form.is_valid():
            # 接收参数
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            mobile = form.cleaned_data.get('mobile')
            sms_code_client = request.POST.get('sms_code')         # 验证短信验证码  sms_code是register.html 文件中命名的
            
            # 判断用户输入的短信验证码是否正确
            redis_conn = get_redis_connection('verify_code')       # 链接redis中配置的数据库
            sms_code_server = redis_conn.get('sms_%s' % mobile)    # 根据存储时候的格式写
            if sms_code_server is None:
                return render(request, 'register.html', {'sms_code_errmsg': '短信验证码已经失效'})     # 错误信息渲染到前端界面
            if sms_code_server.decode() != sms_code_client:       # sms_code_server数据类型需要转换
                return render(request, 'register.html', {'sms_code_errmsg': '短信验证码填写错误'})
            
            try:
                # user = User(username=username, password=password, mobile=mobile)
                # 下面的添加数据的方法是封装了加密等功能的函数,更安全
                users = User.objects.create_user(username=username, password=password, mobile=mobile)
            except:    # 如果保存数据失败
                return render(request, 'register.html', {'register_error_message': '注册失败'})
            
            # 保持用户登录的状态
            login(request, users)
            
            # 返回响应
            # return HttpResponse('success')
            return redirect(reverse('contents:index'))           # 注册成功,跳转到首页
        else:
            print(form.errors.get_json_data())
            # return HttpResponse("fail")
            # 返回注册错误信息到前端界面
            context = {
                'form_error': form.errors,
            }
            return render(request, 'register.html', context=context)
    
    
class UsernameExists(View):
    """ 判断用户名是否已经存在"""
    def get(self, request, username):     # username用户名
        count = User.objects.filter(username=username).count()      # 查询数据库中信息
        return JsonResponse({"code": 0, "errmsg": "OK", "count": count})   # 返回给前端界面

templates/register.html 前端注册界面

{#  templates/register.html  前端注册界面  #}
{% load static %}

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<title>LG商城-注册title>
	<link rel="stylesheet" type="text/css" href="{% static 'css/reset.css' %}">
	<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}">
head>
<body>

<div id="app">
	<div class="register_con">
		<div class="l_con fl">
			<a href="index.html" class="reg_logo"><img src="{% static 'images/1.png' %}">a>
			<div class="reg_slogan">商品美 · 种类多 · 欢迎光临div>
			<div class="reg_banner">div>
		div>
		<div class="r_con fr">
			<div class="reg_title clearfix">
				<h1>用户注册h1>
				<a href="login.html">登录a>
			div>
			<div class="reg_form clearfix">

				<form method="post" class="register_form" v-cloak @submit="on_submit">
                    {% csrf_token %}
					<ul>
						<li>
							<label>用户名:label>

							<input type="text" name="username" v-model="username" @blur="check_username" id="user_name">

                            <span class="error_tip" v-show="error_name">[[ error_name_message ]]span>
						li>					
						<li>
							<label>密码:label>

							<input type="password" name="password" v-model="password" @blur="check_password" id="pwd">
							<span class="error_tip" v-show="error_password">请输入8-20位的密码span>
						li>
						<li>
							<label>确认密码:label>

							<input type="password" name="password2" v-model="password2" @blur="check_password2" id="cpwd">
							<span class="error_tip" v-show="error_password2">两次输入的密码不一致span>
						li>
						<li>
							<label>手机号:label>
                            
							<input type="text" name="mobile" v-model="mobile" @blur="check_mobile" id="phone">

							<span class="error_tip" v-show="error_mobile">[[ error_mobile_message ]]span>
						li>
						<li>
							<label>图形验证码:label>
{#                             v-model="image_code" @blur="check_image_code"绑定验证码的格式   #}
							<input type="text" name="image_code" id="pic_code" class="msg_input" v-model="image_code" @blur="check_image_code" >
{#                           图形验证码路径进行绑定 :src="image_code_url"  点击变换@click="generate_image_code"   #}
							<img :src="image_code_url" @click="generate_image_code" alt="图形验证码" class="pic_code">
{#							 错误信息显示error_image_code_message     绑定js文件中的错误信息名称v-show="error_image_code" #}
                            <span class="error_tip" v-show="error_image_code">[[ error_image_code_message ]]span>
						li>
						<li>
							<label>短信验证码:label>
{#                            v-model="sms_code"绑定, @blur="check_sms_code"绑定短信验证码的格式    #}
							<input type="text" name="sms_code" id="msg_code" class="msg_input" v-model="sms_code" @blur="check_sms_code">
{#                            点击触发获取短信验证码方法   #}
							<a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]a>
{#                            绑定v-show,用于控制显示错误信息;error_sms_code_message错误信息   #}
{#							<span class="error_tip" v-show="error_sms_code">[[ error_sms_code_message ]]span>#}

{#                            短信验证码错误信息显示  #}
                            {% if sms_code_errmsg %}
							    <span class="error_tip" >{{ sms_code_errmsg }}span>
                            {% endif %}

						li>
						<li class="agreement">

							<input type="checkbox" name="allow" v-model="allow" @change="check_allow"  id="allow">
							<label>同意”LG商城用户使用协议“label>
						
							<span class="error_tip" v-show="error_allow">请勾选用户协议span>

{# 这部分传注册错误信息到前端界面是用的Django自带的,前端验证form表单信息我们采用的是  @submit="on_submit" 中的ajax进行验证,这是两种方式,选择一种就可以 #}
                        {#  将apps/users/views.py文件中的注册错误信息context进行循环   #}
                            <span class="error_tip">
                                {% if form_errors %}
                                    {% for key,error in form_errors.items %}
                                            {{ error }}
                                    {% endfor %}
                                {% endif %}
                            {# 保存用户注册数据失败的信息apps/users/views.py中传递的register_error_message             #}
                                {% if register_error_message %}
                                    {{ register_error_message }}
                                {% endif %} 
                            span>

						li>
						<li class="reg_sub">
							<input type="submit" value="注 册">
						li>
					ul>				
				form>
			div>
		div>
	div>
	<div class="footer no-mp">
		<div class="foot_link">
			<a href="#">关于我们a>
			<span>|span>
			<a href="#">联系我们a>
			<span>|span>
			<a href="#">招聘人才a>
			<span>|span>
			<a href="#">友情链接a>		
		div>
		<p>CopyRight © 2016 北京商业股份有限公司 All Rights Reservedp>
		<p>电话:010-****888    京ICP备*******8号p>
	div>
div>
    
    <script src="{% static 'js/vue-2.5.16.js' %}">script>
    <script src="{% static 'js/axios-0.18.0.min.js' %}">script>

    {#   生成uuid的文件 需要先加载common.js文件,再加载注册界面js #}
    <script src="{% static 'js/common.js' %}">script>
    
    <script src="{% static 'js/register.js' %}">script>

body>
html>

至此,所有的注册逻辑和界面都完成了,开启数据库MySQL 5.7Redis 3.5.3之后,就可以进行注册了,注册成功后能够自动跳转到首页界面就是成功。

注意:还可以将验证码信息保存在log日志中,apps/verifications/views.py 验证码视图文件

"""
apps/verifications/views.py   验证码视图文件
"""
from django.shortcuts import render
from django.views import View
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from .libs.captcha.captcha import captcha      # 调用实例化对象captcha
from django_redis import get_redis_connection   # 连接redis
import random
from verifications.libs.ronglianyun.ccp_sms import CCP    # 导入发送短信验证码类
from verifications import constants                       # 导入定义软编码文件
from utils.response_code import RETCODE                   # 导入定义状态码文件
import logging

logger = logging.getLogger('django')         # 创建日志,配置文件中名称为django


# 图形验证码
class ImageCodeView(View):
    """图像验证码"""
    
    def get(self, request, uuid):
        """
        :param uuid:用于标识该验证码属于哪个用户,用户注册时期没有id标识,用uuid不会产生重复的id
        :return:
        """
        #  生成验证码,从生成验证码文件中调用实例化对象的方法
        text, image = captcha.generate_captcha()
        
        # 先连接redis,再保存到redis
        redis_conn = get_redis_connection('verify_code')  # verify_code是dev.py配置文件中的redis配置参数
        # redis_conn.set('img_%s' % uuid, text)   # set(key,value), 这种保存redis数据,无法设置保存时效
        redis_conn.setex('img_%s' % uuid, constants.IMAGE_CODE_REDIS_EXPIRES, text)   # set(key,expries,value),expries是时效,以秒s为单位
        
        # 响应图形验证码
        # return HttpResponse('image/jpg')
        return HttpResponse(image, content_type='image/jpg')


# 手机验证码
class SMSCodeView(View):
    """短信验证码"""
    def get(self, request, mobile):
        """
        :param mobile: 手机号
        :return:   json数据类型
        """
        # 接收参数
        image_code_client = request.GET.get('image_code')        # image_code_client是字符串数据类型
        uuid = request.GET.get('uuid')
        
        # 校验参数   image_code_client, uuid必须都存在
        if not all([image_code_client, uuid]):
            return HttpResponseForbidden('缺少必传参数')
         
        # 创建连接到redis的对象
        # 提取图形验证码
        redis_conn = get_redis_connection('verify_code')
        
        # 判断短信验证码是否已经发送
        send_flag = redis_conn.get("send_flag_%s" % mobile)
        if send_flag:
            return JsonResponse({"code":111, 'errmsg': "短信验证码发送频繁"})
        
        image_code_server = redis_conn.get('img_%s' % uuid)      # 此时的image_code_server是字节数据类型
        # print(type(image_code_server))
        # print(type(image_code_client))
        
        # 判断图形验证码的有无
        if image_code_server is None:
            return JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码已经失效了"})
        
        # 删除图形验证码,避免恶意测试图形验证码
        redis_conn.delete('img_%s' % uuid)
        
        # 对比图形验证码  .lower()都转成小写
        if image_code_client.lower() != image_code_server.decode().lower():      # decode()方法将字节数据转换成str
            return JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码输入有误!"})   # RETCODE.IMAGECODEERR定义的状态码
        
        # 随机生成短信验证码:生成6位数验证码
        sms_code = "%06d" % random.randint(0, 999999)  # 06d代表前几位可以用0补充
        # print(sms_code)
        # 生成验证码的另一种写法
        # for i in range(6):
        #     sms_code += str(random.randint(0, 9))
        
        # 验证码保存到log日志中
        logger.info(sms_code)
        
        # 保存短信验证码
        redis_conn.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)   # 保存300s
        
        # 保存短信验证码是否已经发送,constants.SEND_SMS_CODE_FLAG保存时效60s   1是value,随意取,"send_flag_%s" % mobile是key
        redis_conn.setex("send_flag_%s" % mobile, constants.SEND_SMS_CODE_FLAG, 1)
        
        # 发送短信验证码send_message(mobile, datas, tid)中tid是模板,默认为1,datas是数据和保存时效;constants.SMS_CODE_REDIS_EXPIRES//60 双//为了得到整数
        CCP().send_message(mobile, (sms_code, constants.SMS_CODE_REDIS_EXPIRES//60), 1)
        # 响应结果
        return JsonResponse({"code": RETCODE.OK, 'errmsg': "发送短信验证码成功"})   # RETCODE.OK定义状态码

4、pipeline操作Redis数据库

Redis的 C - S 架构:

  • 基于客户端-服务端模型以及请求/响应协议的TCP服务。
  • 客户端向服务端发送一个查询请求,并监听Socket返回。
  • 通常是以阻塞模式,等待服务端响应。
  • 服务端处理命令,并将结果返回给客户端。
    存在的问题:
  • 如果Redis服务端需要同时处理多个请求,加上网络延迟,那么服务端利用率不高,效率降低。
    解决的办法:
  • 管道pipeline
    Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)_第2张图片

pipeline的介绍

管道pipeline

  • 可以一次性发送多条命令并在执行完后一次性将结果返回。
  • pipeline通过减少客户端与Redis的通信次数来实现降低往返延时时间。

实现的原理

  • 实现的原理是队列
  • Client可以将三个命令放到一个tcp报文一起发送。
  • Server则可以将三条命令的处理结果放到一个tcp报文返回。
  • 队列是先进先出,这样就保证数据的顺序性。
    Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)_第3张图片

pipeline操作Redis数据库

实现步骤

  1. 创建Redis管道
  2. 将Redis请求添加到队列
  3. 执行请求

代码实现:apps/verifications/views.py 验证码视图文件

"""
apps/verifications/views.py   验证码视图文件
"""
from django.shortcuts import render
from django.views import View
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from .libs.captcha.captcha import captcha      # 调用实例化对象captcha
from django_redis import get_redis_connection   # 连接redis
import random
from verifications.libs.ronglianyun.ccp_sms import CCP    # 导入发送短信验证码类
from verifications import constants                       # 导入定义软编码文件
from utils.response_code import RETCODE                   # 导入定义状态码文件
import logging

logger = logging.getLogger('django')         # 创建日志,配置文件中名称为django


# 图形验证码
class ImageCodeView(View):
    """图像验证码"""
    
    def get(self, request, uuid):
        """
        :param uuid:用于标识该验证码属于哪个用户,用户注册时期没有id标识,用uuid不会产生重复的id
        :return:
        """
        #  生成验证码,从生成验证码文件中调用实例化对象的方法
        text, image = captcha.generate_captcha()
        
        # 先连接redis,再保存到redis
        redis_conn = get_redis_connection('verify_code')  # verify_code是dev.py配置文件中的redis配置参数
        # redis_conn.set('img_%s' % uuid, text)   # set(key,value), 这种保存redis数据,无法设置保存时效
        redis_conn.setex('img_%s' % uuid, constants.IMAGE_CODE_REDIS_EXPIRES, text)   # set(key,expries,value),expries是时效,以秒s为单位
        
        # 响应图形验证码
        # return HttpResponse('image/jpg')
        return HttpResponse(image, content_type='image/jpg')


# 手机验证码
class SMSCodeView(View):
    """短信验证码"""
    def get(self, request, mobile):
        """
        :param mobile: 手机号
        :return:   json数据类型
        """
        # 接收参数
        image_code_client = request.GET.get('image_code')        # image_code_client是字符串数据类型
        uuid = request.GET.get('uuid')
        
        # 校验参数   image_code_client, uuid必须都存在
        if not all([image_code_client, uuid]):
            return HttpResponseForbidden('缺少必传参数')
         
        # 创建连接到redis的对象
        # 提取图形验证码
        redis_conn = get_redis_connection('verify_code')
        
        # 判断短信验证码是否已经发送
        send_flag = redis_conn.get("send_flag_%s" % mobile)
        if send_flag:
            return JsonResponse({"code":111, 'errmsg': "短信验证码发送频繁"})
        
        image_code_server = redis_conn.get('img_%s' % uuid)      # 此时的image_code_server是字节数据类型
        # print(type(image_code_server))
        # print(type(image_code_client))
        
        # 判断图形验证码的有无
        if image_code_server is None:
            return JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码已经失效了"})
        
        # 删除图形验证码,避免恶意测试图形验证码
        redis_conn.delete('img_%s' % uuid)
        
        # 对比图形验证码  .lower()都转成小写
        if image_code_client.lower() != image_code_server.decode().lower():      # decode()方法将字节数据转换成str
            return JsonResponse({"code": RETCODE.IMAGECODEERR, 'errmsg': "图形验证码输入有误!"})   # RETCODE.IMAGECODEERR定义的状态码
        
        # 随机生成短信验证码:生成6位数验证码
        sms_code = "%06d" % random.randint(0, 999999)  # 06d代表前几位可以用0补充
        # print(sms_code)
        # 生成验证码的另一种写法
        # for i in range(6):
        #     sms_code += str(random.randint(0, 9))
        
        # 验证码保存到log日志中
        logger.info(sms_code)
        """
        # 保存短信验证码
        redis_conn.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)   # 保存300s
        # 保存短信验证码是否已经发送,constants.SEND_SMS_CODE_FLAG保存时效60s   1是value,随意取,"send_flag_%s" % mobile是key
        redis_conn.setex("send_flag_%s" % mobile, constants.SEND_SMS_CODE_FLAG, 1)
        """    # 采用管道pipeline实现redis请求
        # 创建redis管道
        pl = redis_conn.pipeline()
        # 将命令添加到队列中
        pl.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
        pl.setex("send_flag_%s" % mobile, constants.SEND_SMS_CODE_FLAG, 1)
        # 执行
        pl.execute()
        
        # 发送短信验证码send_message(mobile, datas, tid)中tid是模板,
        # 默认为1,datas是数据和保存时效;constants.SMS_CODE_REDIS_EXPIRES//60 双//为了得到整数
        CCP().send_message(mobile, (sms_code, constants.SMS_CODE_REDIS_EXPIRES//60), 1)
        
        # 响应结果
        return JsonResponse({"code": RETCODE.OK, 'errmsg': "发送短信验证码成功"})   # RETCODE.OK定义状态码

5、异步方案Celery

生产者消费者设计模式

问题

  • 我们的代码是自上而下同步执行的。
  • 发送短信是耗时的操作。如果短信被阻塞住,用户响应将会延迟。
  • 响应延迟会造成用户界面的倒计时延迟。
    Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)_第4张图片

解决:

  • 异步发送短信
  • 发送短信和响应分开执行,将发送短信从主业务中解耦出来。
    Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)_第5张图片

生产者消费者设计模式介绍

  • 为了将发送短信从主业务中解耦出来,我们引入生产者消费者设计模式。
  • 它是最常用的解耦方式之一,寻找中间人(broker)搭桥,保证两个业务没有直接关联。
    Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)_第6张图片
    总结:
  • 生产者生成消息,缓存到消息队列中,消费者读取消息队列中的消息并执行
  • 由商城生成发送短信消息,缓存到消息队列中,消费者读取消息队列中的发送短信消息并执行

在这个项目中: 解耦

  • 任务是:发送短信;
  • 生产者:商城;
  • 消费者:Celery;
  • 中间人:消息队列/Redis。

Celery介绍和使用

  • 消费者取到消息之后,要消费掉(执行任务),需要我们去实现。
  • 任务可能出现高并发的情况,需要补充多任务的方式执行。
  • 耗时任务很多种,每种耗时任务编写的生产者和消费者代码有重复。
  • 取到的消息什么时候执行,以什么样的方式执行。

结论:

  • 实际开发中,我们可以借助成熟的工具Celery来完成。
  • 有了Celery,我们在使用生产者消费者模式时,只需要关注任务本身,极大的简化了程序员的开发流程。

Celery介绍

  • 一个简单、灵活且可靠、处理大量消息的分布式系统,可以在一台或者多台机器上运行
  • 单个 Celery 进程每分钟可处理数以百万计的任务
  • 通过消息进行通信,使用消息队列(broker)在客户端和消费者之间进行协调。

安装Celery

虚拟环境中安装
pip install Celery

创建Celery实例并加载配置

celery_tasks.main.py

# celery启动文件
from celery import Celery
# 创建celery实例
celery_app = Celery('meiduo')

加载Celery配置
celery_tasks.config.py

# 指定消息队列的位置
broker_url = "redis://127.0.0.1/10"

celery_tasks.main.py

# celery启动文件
from celery import Celery
# 创建celery实例
celery_app = Celery('meiduo')
# 加载celery配置
celery_app.config_from_object('celery_tasks.config')

定义发送短信任务
注册任务:celery_tasks.main.py

# celery启动文件
from celery import Celery
# 创建celery实例
celery_app = Celery('meiduo')
# 加载celery配置
celery_app.config_from_object('celery_tasks.config')
# 自动注册celery任务
celery_app.autodiscover_tasks(['celery_tasks.sms'])

定义任务:celery_tasks.sms.tasks.py

@celery_app.task(bind=True, name='ccp_send_sms_code', retry_backoff=3)
def ccp_send_sms_code(self, mobile, sms_code):
    """
    发送短信异步任务
    :param mobile: 手机号
    :param sms_code: 短信验证码
    :return: 成功0 或 失败-1
    """
    try:
        send_ret = CCP().send_template_sms(mobile, [sms_code, constants.SMS_CODE_REDIS_EXPIRES // 60], constants.SEND_SMS_TEMPLATE_ID)
    except Exception as e:
        logger.error(e)
        # 有异常自动重试三次
        raise self.retry(exc=e, max_retries=3)
    if send_ret != 0:
        # 有异常自动重试三次
        raise self.retry(exc=Exception('发送短信失败'), max_retries=3)
    return send_ret

启动Celery服务

# Linux系统中
celery -A celery_tasks.main worker -l info

# Windows系统中
celery -A celery_tasks.main worker -l info --pool=solo
  • -A指对应的应用程序, 其参数是项目中 Celery实例的位置。
  • worker指这里要启动的worker。
  • -l指日志等级,比如info等级。

调用发送短信任务

# 发送短信验证码
# CCP().send_template_sms(mobile,[sms_code, constants.SMS_CODE_REDIS_EXPIRES // 60], constants.SEND_SMS_TEMPLATE_ID)
# Celery异步发送短信验证码
ccp_send_sms_code.delay(mobile, sms_code)

实例代码如下:

项目目录如下:
Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)_第7张图片

celery主文件celery_tasks/main.py

# -*- encoding: utf-8 -*-
"""
@File    : main.py
@Time    : 2020/8/7 14:23
@Author  : chen

celery主文件:/celery_tasks/main.py
"""
# celery启动文件
from celery import Celery
# 创建celery实例   'shop'名称
celery_app = Celery('shop')

# 加载配置文件
celery_app.config_from_object('celery_tasks.config')

# 自动注册celery任务
celery_app.autodiscover_tasks(['celery_tasks.sms'])   # 自动去查找tasks.py文件并执行
"""
celery -A celery_tasks.main worker -l info
• -A指对应的应用程序, 其参数是项目中 Celery实例的位置。
• worker指这里要启动的worker。
• -l指日志等级,比如info等级。
"""

定义任务文件shop/celery_tasks/sms/tasks.py

# -*- encoding: utf-8 -*-
"""
@File    : tasks.py
@Time    : 2020/8/7 17:09
@Author  : chen

定义任务文件:shop/celery_tasks/sms/tasks.py
"""
from celery_tasks.sms.ronglianyun.ccp_sms import CCP    # 将容联云的代码复制放到celery_tasks/sms文件内
from celery_tasks.sms import constants
from celery_tasks.main import celery_app


# 使用装饰器,celery识别任务,name是任务名称
@celery_app.task(name="send_sms_code")
def send_sms_code(mobile, sms_code):
    """
    发送短信的异步任务
    :param mobile:收集
    :param sms_code:短信验证码
    :return:成功0;失败-1
    """
    # 发送短信验证码send_message(mobile, datas, tid)中tid是模板,
    # 默认为1,datas是数据和保存时效;constants.SMS_CODE_REDIS_EXPIRES//60 双//为了得到整数
    result = CCP().send_message(mobile, (sms_code, constants.SMS_CODE_REDIS_EXPIRES // 60), 1)

    return result


celery配置文件celery_tasks/config.py

# -*- encoding: utf-8 -*-
"""
@File    : config.py
@Time    : 2020/8/7 17:03
@Author  : chen

celery配置文件:celery_tasks/config.py
"""
# 指定中间人,消息队列,任务队列   redis  指定消息队列的位置
broker_url = "redis://127.0.0.1/10"       # 使用redis第10个数据库

发送短信验证码文件shop\celery_tasks\sms\ronglianyun/ccp_sms.py

# -*- encoding: utf-8 -*-
"""
@File    : ccp_sms.py
@Time    : 2020/8/2 17:23
@Author  : chen


发送短信验证码文件:shop\celery_tasks\sms\ronglianyun/ccp_sms.py
"""
from ronglian_sms_sdk import SmsSDK
import json

accId = '8a216da873a33a500173a407c9bf010c'        # 容联云通讯分配的主账号ID
accToken = '3e0d6a4bbe884ad9889d8d9a16c3747a'     # 容联云通讯分配的主账号TOKEN
appId = '8a216da873a33a500173a407cab00113'        # 容联云的APP ID


# 单例类的实例化能够节省内存空间,无论实例化多少次,内存空间只有一个
class CCP(object):
    # __new__方法是__init__方法之上被调用的,__init__方法会在实例化时候调用,__new__方法是生成类的方法
    def __new__(cls, *args, **kwargs):                              # 这里的cls代表CCP这个类
        if not hasattr(cls, '_instance'):                           # 当不具有_instance这个属性的时候
            cls._instance = super().__new__(cls, *args, **kwargs)
            # print(type(cls._instance))                            # cls._instance相当于CCP这个类
            cls._instance.sdk = SmsSDK(accId, accToken, appId)      # 给CCP这个类添加sdk这个属性,相当于实例化了SmsSDK这个类
        return cls._instance

    # 发送短信验证码
    def send_message(self, mobile, datas, tid):
        resp = self._instance.sdk.sendMessage(tid, mobile, datas)
        # print(type(resp))                        # str  需要转换成字典
        result = json.loads(resp)                # 转换数据类型

        # sdk = SmsSDK(accId, accToken, appId)
        # tid = '容联云通讯创建的模板ID'
        # mobile = '15210438734'
        # datas = ('变量1', '变量2')
        # resp = sdk.sendMessage(tid, mobile, datas)
        # print(resp)
        
        if result['statusCode'] == '000000':      # 当传输状态码为000000时候,代表发送信息成功
            return 0
        else:
            return -1


# if __name__ == '__main__':
#     a = CCP()
#     res = a.send_message('13316551764', ('123456', 5), 1)
#     print(res)

启动Celery:
Django项目实战——4—(短信验证码前端逻辑、避免频繁发送短信验证码、补充注册时短信验证后端逻辑、pipeline操作Redis数据库、异步方案Celery)_第8张图片
测试用户接收短信验证码正常!

你可能感兴趣的:(Django项目实战-商城,django,python,redis,pipeline,Celery)