Django项目: 3.用户注册功能

本章内容的补充知识点

导入库的良好顺序:

1.系统库 2.django库 3.自己定义的库(第三方库)

 

redis缓存数据库的数据调用速度快,但是不利于长时间保存。

mysql用于长时间存储,但是调用比较慢。

session会话存储的内容(以字典的方式存放)放在redis缓存里面,要设置过期时间

 

用户注册功能

一、用户模型设计

1. 用户表字段分析

  • 用户名

  • 密码

  • 手机号

  • 邮箱

2.用户模型设计

django的强大之处在于开发效率高,内置了权限模块之类的很多常用功能。在开始一个新的django项目时,如果权限模块中的User模型不满足项目要求,我们需要扩展或者自定义User模型。

扩展User模型有两种方法

  1. 如果你不需要改变数据库存储内容,只是改变行为,那么可以建立有一个基于User模型的代理模型。

  2. 如果想存储与User模型关联的信息,可以使用OneToOneField到包含其他信息字段的模型。这种one-to-one模型经常被称作Profile模型,因为它可能存储站点用户的非身份验证的相关信息。例如:

    from django.contrib.auth.models import User
    
    class Employee(models.Model):
        user = models.OneToOneField(User, on_delete=models.CASCADE)
        department = models.CharField(max_length=100)

自定义User模型

如果不想使用django内置的权限系统,当然你需要自定义用户模型,这种情况不讨论。当然也不建议这么做,django内置权限系统有大的自定义功能扩展,而不是重复造轮子。

开启一个新项目,官方强烈推荐用户自定义用户模型,即是默认的用户模型目前已经足够,但是未来可能会要扩展。

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    pass

注意:不要忘记在settings.py中设置AUTH_USER_MODEL指向它。

一旦已经创立数据库表之后再去修改AUTH_USER_MODEL,会困难很多,因为它会影响外键和多对多关系。这个改动并不能自动完成,需要手动修复(巨坑)。

官方文档

3.用户模型代码

根据上面的分析我们的用户模型代码如下:

from django.db import models
from django.contrib.auth.models import AbstractUser, UserManager as _UserManager


class UserManager(_UserManager):
    """
    自定义 user manager 修改在使用`python manage.py createsuperuser`命令时
    可以提供email
    """
    def create_superuser(self, username, password, email=None, **extra_fields):
        return super().create_superuser(username=username, password=password, email=email, **extra_fields)


class User(AbstractUser):
    """
    add mobile, email_active fields to Django user model.
    """
    mobile = models.CharField('手机号', max_length=11, unique=True, help_text='手机号', error_messages={'unique': '此手机号码已注册'})

    email_active = models.BooleanField('邮箱状态', default=False)

    class Meta:
        db_table = 'tb_user'    # 指定数据库表名
        verbose_name = '用户'     # 在admin站点中显示名称
        verbose_name_plural = verbose_name  # 显示复数

    def __str__(self):
        return self.username

    # A list of the field names that will be prompted for
    # when create a user via createsuperuser management command.
    REQUIRED_FIELDS = ['mobile']
    # specify manager
    objects = UserManager()

在settings.py文件中添加如下配置:

# 自定义用户模型
AUTH_USER_MODEL = 'user.User'

然后运行命令进行数据库迁移:

# 1. 相当于 在该app下建立 migrations目录,并记录下你所有的关于modes.py的改动,比如0001_initial.py, 但是这个改动还没有作用到数据库文件你可以手动打开这个文件,看看里面是什么
python manage.py makemigrations

# 2. 将该改动作用到数据库文件,比如产生table之类
python manage.py migrate

再创建一个管理用户

(tzproject) ~/code/tztz$ python manage.py createsuperuser
用户名: admin
手机号: 158xxxxxxxx
Password: 
Password (again): 
密码长度太短。密码必须包含至少 8 个字符。
这个密码太常见了。
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

1.设计接口思路

  • 分析业务逻辑,明确在这个业务中需要涉及到几个相关子业务,将每个子业务党组欧一个接口来设计

  • 分析接口的功能任务,明确接口的访问方式与返回数据:

    • 接口的请求方式,如GET,POST,PUT等

    • 接口的URL路径定义

    • 需要接受的参数及参数格式(如路径参数,查询字符串,请求表单,JSON等)

    • 返回的数据及数据格式

2.注册功能分析

  1. 流程图

 

 

   

  1. 功能

根据流程图总结注册业务包含如下功能

  • 注册页面

  • 图片验证码

  • 用户名检测是否注册

  • 手机号检测是否注册

  • 短信验证码

  • 注册保存用户数据

因为图片验证码,短信验证码考虑到后续可能会在其他业务中用到,因此将验证码功能独立出来,创建一个新应用verification

三、图形验证码功能实现

1.接口设计

接口说明:

类目
说明
请求方法
GET
url定义 /image_code/
参数格式 查询参数

参数说明:

参数名 类型 是否必须 描述
rand 字符串
输入的用户名

返回结果:验证码图片

2.后端代码

  1. 将验证码生成模块复制到根目录utils文件夹下

  2. 创建新的app verification专门用来处理验证

    cd ~/code/tztz/apps/
    python ../manage.py startapp verification

别忘了在settings文件中注册app

必须在这里要先安装pillow才能用chptcha(pip install pillow)

  1. constants.py(用于设置常量的文件) 和 verification/views.py代码如下:

    # constants.py 文件
    # 保存设置常量,单位秒
    IMAGE_CODE_EXPIRES = 300
    # views.py 文件
    import logging
    
    from django.http import HttpResponse
    
    from utils.captcha.captcha import captcha
    from . import constants  # 先要在本app中创建constants.py文件
    # 日志器
    logger = logging.getLogger('django')
    
    
    def image_code_view(request):
        """
        生成图片验证码
        url:/image_code/
        :param request:
        :return:
        """
        text, image = captcha.generate_captcha()
        request.session['image_code'] = text
        # 将验证码存入session中
        request.session.set_expiry(constants.IMAGE_CODE_EXPIRES)
        logger.info('Image code:{}'.format(text))
    
        return HttpResponse(content=image, content_type='image/jpg')

     

  2. verification/urls.py代码如下:

    from django.urls import path
    from . import views
    # url的命名空间
    app_name = 'verification'
    
    urlpatterns = [
        path('image_code/', views.image_code_view, name='image_code'),
    ]
  3. 根urls.py代码如下:
    from django.urls import path, include
    
    urlpatterns = [
        path('', include('news.urls')),
        path('', include('verification.urls')),
    ]

    验证码的走向图:

     

     

     

四、注册页面

1.接口设计

  1. 接口说明:

    类目
    说明
    请求方法
    GET
    url定义
      /user/register/
    参数格式  
  2. 返回结果: 注册页面

2.后端代码

  1. user/views.py

    from django.shortcuts import render
    from django.views import View
    
    
    class RegisterView(View):
        def get(self, request):
            return render(request, 'user/register.html')

     

  2. user/urls.py
    from django.urls import path, include
    from . import views
    app_name = 'user'
    
    urlpatterns = [
        path('register/', views.RegisterView.as_view(), name='register')
    ]

     

  3. 根urls.py
    from django.urls import path, include
    
    urlpatterns = [
        path('', include('news.urls')),
        path('user/', include('user.urls'))
    ]

     

3.前端页面代码

  1. user/register.html代码如下:

    {% extends 'base/base.html' %}
    {% load static %}
    {% block title %}注册{% endblock title %}
    
    {% block link %}
    <link rel="stylesheet" href="{% static 'css/user/auth.css' %}">
    {% endblock link %}
    
    {% block main_start %}
        <main id="container">
      <div class="register-contain">
        <div class="top-contain">
          <h4 class="please-register">请注册</h4>
          <a href="javascript:void(0);" class="login">立即登录 &gt;</a>
        </div>
        <form action="" method="post" class="form-contain">
    
    
          <div class="form-item">
            <input type="text" placeholder="请输入用户名" name="username" class="form-control" autocomplete="off">
          </div>
          <div class="form-item">
            <input type="password" placeholder="请输入密码" name="password" class="form-control">
          </div>
          <div class="form-item">
            <input type="password" placeholder="请输入确认密码" name="password_repeat" class="form-control">
          </div>
          <div class="form-item">
            <input type="tel" placeholder="请输入手机号" name="telephone" class="form-control" autocomplete="off" autofocus>
          </div>
          <div class="form-item">
            <input type="text" placeholder="请输入图形验证码" name="captcha_graph" class="form-captcha">
            <a href="javascript:void(0);" class="captcha-graph-img">
              <img src="{% url 'verification:image_code' %}" alt="验证码" title="点击刷新">
            </a>
          </div>
          <div class="form-item">
            <input type="text" placeholder="请输入短信验证码" name="sms_captcha" class="form-captcha" autocomplete="off">
            <a href="javascript:void(0);" class="sms-captcha" title="发送验证码">获取短信验证码</a>
          </div>
          <div class="form-item">
            <input type="submit" value="立即注册" class="register-btn">
          </div>
        </form>
      </div>
    </main>
    {% endblock main_start %}
    {% block script %}
        <script src="{% static 'js/user/auth.js' %}"></script>
    {% endblock script %}

     

  2. js代码

    点击验证码图片刷新的js代码如下:

    $(function () {
        let $img = $('.form-contain .form-item .captcha-graph-img img');
        
        // 1.点击刷新图像验证码
        $img.click(function () {
            $img.attr('src', '/image_code/?rand=' + Math.random())
        });
    });

     

五、json响应数据结构设计

1.结构设计

实际项目是多人协同开发,特别是前后端交互,后端返回数据结构要一致。

{"errno": "0", "errmsg": "OK", "data": {...}}
字段 类型 说明
errno 字符串
错误编码
errmsg 字符串 错误信息
data   返回数据

在项目根目录中utils文件夹下创建res_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目录下创建json_res.py文件(也可以直接在res_code文件中直接加上)代码如下:

from django.http import JsonResponse

# 在res_code文件中时不用加下面这个导入
from .res_code import Code


def json_response(errno=Code.OK, errmsg='', data=None, kwargs=None):
json_dict = {
        'errno': errno,
        'errmsg': errmsg,
        'data': data
    }
    # 额外的字段的扩展
    if kwargs and isinstance(kwargs, dict) :
        json_dict.update(kwargs)
    
    return JsonResponse(json_dict)

六、判断用户是否注册功能实现

1.接口设计

接口说明:

类目说明
请求方法 GET
url定义 /username/(?P<username>\w{5,20})/
参数格式 url路径参数

参数说明:

参数名类型是否必须描述
username 字符串 输入的用户名

返回结果:

{
    "errno": "0", 
     "errmsg": "OK",                     # 错误信息
     "data": {
        "username": "username",            # 查询的用户名
        "count": 1                        # 用户名查询的数量
    }
}

2.后端代码

  1. 创建新的app verification专门用来处理验证(上面图形验证码中已经创建,所以这个是多余的)

    cd ~/code/tztz/apps/
    python ../manage.py startapp verification
  2. verification/views.py代码

    from user.models import User
    from utils.json_res import json_response
    
    
    def check_username_view(request, username):
        """
        校验用户名
        url: /username/(?p<username>\w{5,20})/
        """
        data = {
            'username': username,
            'count': User.objects.filter(username=username).count()
        }
    
        return json_response(data=data)

     

  3. verification/urls.py代码
    from django.urls import path, re_path
    from . import views
    # url的命名空间
    app_name = 'verification'
    
    urlpatterns = [
        path('image_code/', views.image_code_view, name='image_code'),
        re_path('username/(?P<username>\w{5,20})/', views.check_username_view, name='check_username'),
    ]

     

3.前端页面代码

user/register.html代码如下:

{% extends 'base/base.html' %}
{% load static %}
{% block title %}注册{% endblock title %}

{% block link %}
<link rel="stylesheet" href="{% static 'css/user/auth.css' %}">
{% endblock link %}

{% block main_start %}
    <main id="container">
  <div class="register-contain">
    <div class="top-contain">
      <h4 class="please-register">请注册</h4>
      <a href="javascript:void(0);" class="login">立即登录 &gt;</a>
    </div>
    <form action="" method="post" class="form-contain">


      <div class="form-item">
        <input type="text" placeholder="请输入用户名" id="username" name="username" class="form-control" >
      </div>
      <div class="form-item">
        <input type="password" placeholder="请输入密码" name="password" class="form-control">
      </div>
      <div class="form-item">
        <input type="password" placeholder="请输入确认密码" name="password_repeat" class="form-control">
      </div>
      <div class="form-item">
        <input type="tel" placeholder="请输入手机号" name="telephone" class="form-control" autocomplete="off">
      </div>
      <div class="form-item">
        <input type="text" placeholder="请输入图形验证码" name="captcha_graph" class="form-captcha">
        <a href="javascript:void(0);" class="captcha-graph-img">
          <img src="{% url 'verification:image_code' %}" alt="验证码" title="点击刷新">
        </a>
      </div>
      <div class="form-item">
        <input type="text" placeholder="请输入短信验证码" name="sms_captcha" class="form-captcha" autocomplete="off">
        <a href="javascript:void(0);" class="sms-captcha" title="发送验证码">获取短信验证码</a>
      </div>
      <div class="form-item">
        <input type="submit" value="立即注册" class="register-btn">
      </div>
    </form>
  </div>
</main>
{% endblock main_start %}

{% block script %}
    <script src="{% static 'js/user/auth.js' %}"></script>
{% endblock script %}

4.前端js代码

user/auth.js代码:

$(function () {
    // 定义状态变量
    let isUsernameReady = false,
        isPasswordReady = false,
        isMobileReady = false,
        isSmsCodeReady = false;
    // 1.点击刷新图像验证码
    let $img = $('.form-contain .form-item .captcha-graph-img img');
    

    $img.click(function () {
        $img.attr('src', '/image_code/?rand=' + Math.random())
    });

    // 2.鼠标离开用户名输入框校验用户名
    let $username = $('#username');
    $username.blur(fnCheckUsername);
    
    function fnCheckUsername () {
        isUsernameReady = false;
        let sUsername = $username.val();    //获取用户字符串
        if (sUsername === ''){
            message.showError('用户名不能为空!');
            return
        }
        if (!(/^\w{5,20}$/).test(sUsername)){
            message.showError('请输入5-20个字符的用户名');
            return
        }
        $.ajax({
            url: '/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
        }
    }

 

七、判断手机号码是否注册功能

1.接口设计

接口说明:

类目说明
请求方法 GET
url定义 /mobile/(?P<mobile>\1[3-9]\d{9})/
参数格式 url路径参数

参数说明:

参数名类型是否必须描述
moblie 字符串 输入的手机号码

返回结果:

{
    "errno": "0", 
     "errmsg": "OK", 
     "data": {
        "mobile": "13xxxxxxxxx",            # 查询的手机号
        "count": 1                        # 手机号查询的数量
    }
}

2.后端代码

  1. verification/views.py代码

    # ····
    def check_mobile_view(request, mobile):
        """
        校验手机号是否存在
        url:/moblie/(?P<moblie>1[3-9]\d{9})/
        :param request:
        :param username:
        :return:
        """
        data = {
            'mobile': mobile,
            'count': User.objects.filter(mobile=mobile).count()
        }
    
        return json_response(data=data)

     

  2. verification/urls.py
    from django.urls import path, re_path
    from . import views
    # url的命名空间
    app_name = 'verification'
    
    urlpatterns = [
        path('image_code/', views.image_code_view, name='image_code'),
        re_path('username/(?P<username>\w{5,20})/', views.check_username_view, name='check_username'),
        re_path('mobile/(?P<mobile>1[3-9]\d{9})/', views.check_mobile_view, name='check_mobile'),
    ]

     

  3. 前端js代码:
    $(function () {
        // 定义状态变量
        let isUsernameReady = false,
            isPasswordReady = false,
            isMobileReady = false,
            isSmsCodeReady = false;
        // 1.点击刷新图像验证码
        let $img = $('.form-contain .form-item .captcha-graph-img img');
        
    
        $img.click(function () {
            $img.attr('src', '/image_code/?rand=' + Math.random())
        });
    
        // 2.鼠标离开用户名输入框校验用户名
        let $username = $('#username');
        $username.blur(fnCheckUsername);
    
        function fnCheckUsername () {
            isUsernameReady = false;
            let sUsername = $username.val();    //获取用户字符串
            if (sUsername === ''){
                message.showError('用户名不能为空!');
                return
            }
            if (!(/^\w{5,20}$/).test(sUsername)){
                message.showError('请输入5-20个字符的用户名');
                return
            }
            $.ajax({
                url: '/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 $mobile = $('input[name="mobile"]');
        $mobile.blur(fnCheckMobile);
    
        function fnCheckMobile () {
            isMobileReady = true;
            let sMobile = $mobile.val();
            if(sMobile === ''){
                message.showError('手机号码不能为空');
                return
            }
            if(!(/^1[3-9]\d{9}$/).test(sMobile)){
                message.showError('手机号码格式不正确');
                return
            }
    
            $.ajax({
                url: '/mobile/' + sMobile + '/',
                type: 'GET',
                dataType: 'json',
                success: function (data) {
                    if(data.data.count !== 0){
                        message.showError(data.data.mobile + '已经注册,请重新输入!')
                    }else {
                        message.showInfo(data.data.mobile + '可以正常使用!');
                        isMobileReady = true
                    }
                },
                error: function (xhr, msg) {
                    message.showError('服务器超时,请重试!')
                }
            });
    
        }
    });

     

八、获取短信验证码功能

1.业务流程分析

  • 校验手机号码

  • 检查图片验证码是否正确

  • 检查是否在60s内发送记录

  • 生成短信验证码

  • 发送短信

  • 保存短信验证码与发送记录

2.接口设计

接口说明:

类目说明
请求方法 POST
url定义 /sms_code/
参数格式 表单

参数说明:

参数名类型是否必须描述
moblie 字符串 用户输入的手机号码
captcha 字符串 用户输入的验证码文本

返回结果:

{
    "errno": "0", 
     "errmsg": "发送短信验证码成功!", 
}

3.后端代码

  1. verification/views.py代码如下:

    import logging
    import random
    
    from django.http import HttpResponse
    from django.views import View
    from django_redis import get_redis_connection
    
    from user.models import User
    from utils.json_res import json_response
    from utils.res_code import Code, error_map
    from utils.captcha.captcha import captcha
    from utils.yuntongxun.sms import CCP
    
    from . import constants
    from .forms import CheckImagForm
    
    # 日志器
    logger = logging.getLogger('django')
    
    
    def image_code_view(request):
        """
        生成图片验证码
        url:/image_code/
        :param request:
        :return:
        """
        text, image = captcha.generate_captcha()
        request.session['image_code'] = text
        # 将验证码存入session中
        request.session.set_expiry(constants.IMAGE_CODE_EXPIRES)
        logger.info('Image code:{}'.format(text))
    
        return HttpResponse(content=image, content_type='image/jpg')
    
    
    def check_username_view(request, username):
        """
        校验用户名是否存在
        url:/username/(?P<username>\w{5,20})/
        :param request:
        :param username:
        :return:
        """
        data = {
            'username': username,
            'count': User.objects.filter(username=username).count()
        }
    
        return json_response(data=data)
    
    
    def check_mobile_view(request, mobile):
        """
        校验手机号是否存在
        url:/moblie/(?P<moblie>1[3-9]\d{9})/
        :param request:
        :param username:
        :return:
        """
        data = {
            'mobile': mobile,
            'count': User.objects.filter(mobile=mobile).count()
        }
    
        return json_response(data=data)
    
    
    class SmsCodeView(View):
        """
        发送短信验证码
        POST /sms_codes/
        """
        def post(self, request):
            # 1.校验参数
    
            form = CheckImagForm(request.POST, request=request)
            if form.is_valid():
                # 2.获取手机
                mobile = form.cleaned_data.get('mobile')
                # 3.生成手机验证码
                sms_code = ''.join([random.choice('0123456789') for _ in range(constants.SMS_CODE_LENGTH)])
                # 4.发送手机验证码
                ccp = CCP()
                try:
                    res = ccp.send_template_sms(mobile, [sms_code, constants.SMS_CODE_EXPIRES], "1")
                    if res == 0:
                        logger.info('发送短信验证码[正常][mobile: %s sms_code: %s]' % (mobile, sms_code))
                    else:
                        logger.error('发送短信验证码[失败][moblie: %s sms_code: %s]' % (mobile, sms_code))
                        return json_response(errno=Code.SMSFAIL, errmsg=error_map[Code.SMSFAIL])
                except Exception as e:
                    logger.error('发送短信验证码[异常][mobile: %s message: %s]' % (mobile, e))
                    return json_response(errno=Code.SMSERROR, errmsg=error_map[Code.SMSERROR])
    
    
                # 5.保存到redis数据库
                # 创建短信验证码发送记录
                sms_flag_key = 'sms_flag_{}'.format(mobile)
                # 创建短信验证码内容记录
                sms_text_key = 'sms_text_{}'.format(mobile)
    
                redis_conn = get_redis_connection(alias='verify_code')
                pl = redis_conn.pipeline()
    
                try:
                    pl.setex(sms_flag_key, constants.SMS_CODE_INTERVAL, 1)
                    pl.setex(sms_text_key, constants.SMS_CODE_EXPIRES*60, sms_code)
                    # 让管道通知redis执行命令
                    pl.execute()
                    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:
                # 将表单的报错信息进行拼接
                err_msg_list = []
                for item in form.errors.get_json_data().values():
                    err_msg_list.append(item[0].get('message'))
                    # print(item[0].get('message'))   # for test
                err_msg_str = '/'.join(err_msg_list)  # 拼接错误信息为一个字符串
    
                return json_response(errno=Code.PARAMERR, errmsg=err_msg_str)
  2. verification/forms.py文件代码如下:
    from django import forms
    from django.core.validators import RegexValidator
    from django_redis import get_redis_connection
    
    from user.models import User
    
    # 创建手机号的正则校验器
    mobile_validator = RegexValidator(r'^1[3-9]\d{9}$', '手机号码格式不正确')
    
    
    class CheckImagForm(forms.Form):
        """
        check image code
        """
    
        def __init__(self, *args, **kwargs):
            self.request = kwargs.pop('request')
            super().__init__(*args, **kwargs)
    
        mobile = forms.CharField(max_length=11, min_length=11, validators=[mobile_validator, ],
                                 error_messages={
                                     'max_length': '手机长度有误',
                                     'min_length': '手机长度有误',
                                     'required': '手机号不能为空'
                                 })
    
        captcha = forms.CharField(max_length=4, min_length=4,
                                  error_messages={
                                      'max_length': '验证码长度有误',
                                      'min_length': '图片验证码长度有误',
                                      'required': '图片验证码不能为空'
                                  })
    
        def clean(self):
            clean_data = super().clean()
            mobile = clean_data.get('mobile')
            captcha = clean_data.get('captcha')
        # 1.校验图片验证码
            image_code = self.request.session.get('image_code')
            if (not image_code) or (image_code.upper() != captcha.upper()):
                raise forms.ValidationError('图片验证码校验失败!')
    
        # 2.校验是否在60秒内已发送过短信
            redis_conn = get_redis_connection(alias='verify_code')
            if redis_conn.get('sms_flag_{}'.format(mobile)):
                raise forms.ValidationError('获取短信验证码过于频繁')
    
        # 3.校验手机号码是否已注册
            if User.objects.filter(mobile=mobile).count():
                raise forms.ValidationError('手机号已注册,请重新输入')

     

  3. verification/constants.py代码如下:
    # 图片验证码过期时间 单位秒
    IMAGE_CODE_EXPIRES = 300
    
    # 短信验证码长度
    SMS_CODE_LENGTH = 4
    
    # 短信验证码发送间隔 秒
    SMS_CODE_INTERVAL = 60
    
    # 短信验证码过期时间 分
    SMS_CODE_EXPIRES = 5
    
    # 短信发送模板
    SMS_CODE_TEMP_ID = 1

     

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'

设置测试手机号码

 

 

 

5.前端js代码

user/auth.js代码:

$(function () {
    // 定义状态变量
    let isUsernameReady = false,
        isPasswordReady = false,
        isMobileReady = false,
        isSmsCodeReady = false;
    // 1.点击刷新图像验证码
    let $img = $('.form-contain .form-item .captcha-graph-img img');
    

    $img.click(function () {
        $img.attr('src', '/image_code/?rand=' + Math.random())
    });

    // 2.鼠标离开用户名输入框校验用户名
    let $username = $('#username');
    $username.blur(fnCheckUsername);

    function fnCheckUsername () {
        isUsernameReady = false;
        let sUsername = $username.val();    //获取用户字符串
        if (sUsername === ''){
            message.showError('用户名不能为空!');
            return
        }
        if (!(/^\w{5,20}$/).test(sUsername)){
            message.showError('请输入5-20个字符的用户名');
            return
        }
        $.ajax({
            url: '/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 $mobile = $('input[name="mobile"]');
    $mobile.blur(fnCheckMobile);

    function fnCheckMobile () {
        isMobileReady = true;
        let sMobile = $mobile.val();
        if(sMobile === ''){
            message.showError('手机号码不能为空');
            return
        }
        if(!(/^1[3-9]\d{9}$/).test(sMobile)){
            message.showError('手机号码格式不正确');
            return
        }

        $.ajax({
            url: '/mobile/' + sMobile + '/',
            type: 'GET',
            dataType: 'json',
            success: function (data) {
                if(data.data.count !== 0){
                    message.showError(data.data.mobile + '已经注册,请重新输入!')
                }else {
                    message.showInfo(data.data.mobile + '可以正常使用!');
                    isMobileReady = true
                }
            },
            error: function (xhr, msg) {
                message.showError('服务器超时,请重试!')
            }
        });

    }

    // 5.发送手机验证码
    let $smsButton = $('.sms-captcha');
    $smsButton.click(function () {
        let sCaptcha = $('input[name="captcha_graph"]').val();
        if(sCaptcha === ''){
            message.showError('请输入验证码');
            return
        }
        if(!isMobileReady){
            fnCheckMobile();
            return
        }

        $.ajax({
            url: '/sms_code/',
            type: 'POST',
            data: {
                mobile: $mobile.val(),
                captcha: sCaptcha
            },
            dataType: 'json',
            success: function (data) {
                if(data.errno !== '0'){
                    message.showError(data.errmsg)
                }else {
                    message.showSuccess(data.errmsg);
                    let num = 60;
                    //设置计时器
                    let t = setInterval(function () {
                        if(num===1){
                            clearInterval(t)
                        }
                    })
                }
            },
            error: function (xhr, msg) {
                message.showError('服务器超时,请重试!')
            }
        });

    });
});

因为用到了post方法,django默认带有csrf防护,所以在base/common.js中添加如下代码:

$(()=>{
    let $navLi = $('#header .nav .menu li');
    $navLi.click(function(){
        $(this).addClass('active').siblings('li').removeClass('active')
    });

    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie !== '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = cookies[i].trim();
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }

    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
            }
        }
    });
});

详解django文档

九、注册功能

1.业务流程分析

  1. 判断用户名是否为空,是否已注册

  2. 判断密码是否为空,格式是否正确

  3. 判断两次密码是否一致

  4. 判断手机号码是否为空,格式是否正确

  5. 判断短信验证码是否为空,格式是否正确,是否与真实短信验证码相同

2.接口设计

接口说明:

类目说明
请求方法 POST
url定义 /user/register/
参数格式 表单

注意:post请求,前端请求要带上csrf token

参数说明:

参数名类型是否必须描述
username 字符串 用户输入的用户名
password 字符串 用户输入的密码
password_repeat 字符串 用户输入的重复密码
mobile 字符串 用户输入的手机号码
sms_code 字符串 用户输入的短信验证码

返回结果:

{
    "errno": "0", 
     "errmsg": "恭喜您,注册成功!", 
}

3.后端代码

  1. user/views.py代码:

    from django.shortcuts import render
    from django.views import View
    
    from .forms import RegisterForm
    from .models import User
    from utils.json_res import json_response
    from utils.res_code import Code, error_map
    
    
    class RegisterView(View):
        def get(self, request):
            return render(request, 'user/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')
                User.objects.create_user(username=username, password=password, mobile=mobile)
                return json_response(errmsg='恭喜你,注册成功!')
            else:
                # 定义一个错误信息列表
                err_msg_list = []
                for item in form.errors.values():
                    err_msg_list.append(item[0])
                err_msg_str = '/'.join(err_msg_list)
    
                return json_response(errno=Code.PARAMERR, errmsg=err_msg_str)

     

  2. user/forms.py代码:

    import re
    
    from django import forms
    from django_redis import get_redis_connection
    
    from .models import User
    from verification.constants import SMS_CODE_LENGTH
    
    
    class RegisterForm(forms.Form):
        username = forms.CharField(label='用户名', max_length=20, min_length=5,
                                   error_messages={
                                       'max_length': '用户名长度要小于20',
                                       'min_length': '用户名长度要大于4',
                                       'required': '用户名不能为空'
                                   })
        password = forms.CharField(label='密码', max_length=20, min_length=6,
                                   error_messages={
                                       'max_length': '密码长度要小于20',
                                       'min_length': '密码长度要大于5',
                                       'required': '用户名不能为空'
                                   })
        password_repeat = forms.CharField(label='确认密码', max_length=20, min_length=6,
                                          error_messages={
                                              'max_length': '密码长度要小于20',
                                              'min_length': '密码长度要大于5',
                                              'required': '用户名不能为空'
                                          })
        mobile = forms.CharField(label='手机号码', max_length=11, min_length=11,
                                 error_messages={
                                     'max_length': '手机号码长度有误',
                                     'min_length': '手机号码长度有误',
                                     'required': '手机号码不能为空'
                                 })
        sms_code = forms.CharField(label='短信验证码', max_length=SMS_CODE_LENGTH, min_length=SMS_CODE_LENGTH,
                                   error_messages={
                                       'max_length': '短信验证码长度有误',
                                       'min_length': '短信验证码长度有误长度有误',
                                       'required': '短信验证码不能为空'
                                   })
    ### clean_username这种是单独校验,写多少就校验多少,上面的错了,下面的同方照样执行
    ### clean一般用于多个字段联合校验,但是只要上面错了下面就不会校验了
        def clean_username(self):
            """
            校验用户名
            :return:
            """
            username = self.cleaned_data.get('username')
            if User.objects.filter(username=username).exists():
                return forms.ValidationError('用户名已存在!')
            return username
    
        def clean_mobile(self):
            """
            校验手机号
            :return:
            """
            mobile = self.cleaned_data.get('mobile')
            if not re.match(r'^1[3-9]\d{9}$', mobile):
                raise forms.ValidationError('手机号码格式不正确')
    
            if User.objects.filter(mobile=mobile).exists():
                raise forms.ValidationError('手机号码已注册!')
    
            return mobile
    
        def clean(self):
            """
            校验,密码,和短信验证码
            :return:
            """
            clean_data = super().clean()
            # 校验密码是否一致
            password = clean_data.get('password')
            password_repeat = clean_data.get('password_repeat')
            if password != password_repeat:
                raise forms.ValidationError('两次密码不一致!')
    
            # 校验短信验证码
            sms_code = clean_data.get('sms_code')
            moblie = clean_data.get('mobile')
    
            redis_conn = get_redis_connection(alias='verify_code')
            real_code = redis_conn.get('sms_text_{}'.format(moblie))
            if (not real_code) or (real_code.decode('utf-8') != sms_code):
                raise forms.ValidationError('短信验证码错误!')

clean_username和clean的差别用法:

 

 4.前端js代码

$(function () {
    // 定义状态变量
    let isUsernameReady = false,
        isPasswordReady = false,
        isMobileReady = false;

    // 1.点击刷新图像验证码
    let $img = $('.form-contain .form-item .captcha-graph-img img');
    

    $img.click(function () {
        $img.attr('src', '/image_code/?rand=' + Math.random())
    });

    // 2.鼠标离开用户名输入框校验用户名
    let $username = $('#username');
    $username.blur(fnCheckUsername);

    function fnCheckUsername () {
        isUsernameReady = false;
        let sUsername = $username.val();    //获取用户字符串
        if (sUsername === ''){
            message.showError('用户名不能为空!');
            return
        }
        if (!(/^\w{5,20}$/).test(sUsername)){
            message.showError('请输入5-20个字符的用户名');
            return
        }
        $.ajax({
            url: '/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 $mobile = $('input[name="mobile"]');
    $mobile.blur(fnCheckMobile);

    function fnCheckMobile () {
        isMobileReady = true;
        let sMobile = $mobile.val();
        if(sMobile === ''){
            message.showError('手机号码不能为空');
            return
        }
        if(!(/^1[3-9]\d{9}$/).test(sMobile)){
            message.showError('手机号码格式不正确');
            return
        }

        $.ajax({
            url: '/mobile/' + sMobile + '/',
            type: 'GET',
            dataType: 'json',
            success: function (data) {
                if(data.data.count !== 0){
                    message.showError(data.data.mobile + '已经注册,请重新输入!')
                }else {
                    message.showInfo(data.data.mobile + '可以正常使用!');
                    isMobileReady = true
                }
            },
            error: function (xhr, msg) {
                message.showError('服务器超时,请重试!')
            }
        });

    }

    // 5.发送手机验证码
    let $smsButton = $('.sms-captcha');
    $smsButton.click(function () {
        let sCaptcha = $('input[name="captcha_graph"]').val();
        if(sCaptcha === ''){
            message.showError('请输入验证码');
            return
        }
        if(!isMobileReady){
            fnCheckMobile();
            return
        }

        $.ajax({
            url: '/sms_code/',
            type: 'POST',
            data: {
                mobile: $mobile.val(),
                captcha: sCaptcha
            },
            dataType: 'json',
            success: function (data) {
                if(data.errno !== '0'){
                    message.showError(data.errmsg)
                }else {
                    message.showSuccess(data.errmsg);
                    let num = 60;
                    //设置计时器
                    let t = setInterval(function () {
                        if(num===1){
                            clearInterval(t)
                        }
                    })
                }
            },
            error: function (xhr, msg) {
                message.showError('服务器超时,请重试!')
            }
        });

    });

    // 6.注册
    let $submitBtn = $('.register-btn');
    $submitBtn.click(function (e) {
        //阻止默认提交
        e.preventDefault();
        // 1.检查用户名
        if(!isUsernameReady){
            fnCheckUsername();
            return
        }
        // 2.检查密码
        if(!isPasswordReady){
            fnCheckPassword();
            return
        }
        // 3.检查电话号码
        if(!isMobileReady){
            fnCheckMobile();
            return
        }
        // 4.检查短信验证码
        let sSmsCode = $('input[name="sms_captcha"]').val();
        if(sSmsCode === ''){
            message.showError('短信验证码不能为空!');
            return
        }
        if(!(/^\d{4}$/).test(sSmsCode)){
            message.showError('短信验证码长度不正确,必须是4位数字!');
            return
        }

        $.ajax({
            url: '/user/register/',
            type: 'POST',
            data:{
                username: $username.val(),
                password: $('input[name="password"]').val(),
                password_repeat: $passwordRepeat.val(),
                mobile: $mobile.val(),
                sms_code: sSmsCode
            },
            dataType: 'json',
            success: function (res) {
                if(res.errno === '0'){
                    message.showSuccess('恭喜您,注册成功!');
                    setTimeout(function () {
                        //注册成功后重定向到登录页面
                        window.location.href = '/user/login/'
                    }, 3000)
                }else{
                    //注册失败
                    message.showError(res.errmsg)
                }
            },
            error: function () {
                message.showError('服务器超时,请重试!')
            }
        })

    });
});

 

posted @ 2019-09-16 20:56  Tmclri  阅读(853)  评论(0编辑  收藏  举报