django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)

  • 用户注册篇
    • 1. 前期准备
      • 1.1 腾讯云发送短信
      • 1.2 redis
    • 2. 注册页面展示
      • 2.1 创建app
      • 2.2 app注册
      • 2.3 母版准备
        • 2.3.1 插件引入
        • 2.3.2 母版
      • 2.4 URL准备
      • 2.5 模型准备【models.py】
      • 2.5 视图函数
      • 2.6 ModelForm【简单校验 & 样式添加】
      • 2.7 前端页面
    • 3. 验证码获取
      • 3.1 思路
      • 3.2 具体实现
        • 3.2.1 前端代码
        • 3.2.2 后端代码
          • 3.2.2.1 URL
          • 3.2.2.2 视图函数
          • 3.2.2.3 配置文件
            • 3.2.2.3.1 腾讯云短信配置文件
            • 3.2.2.3.2 redis 配置文件
          • 3.2.2.4 SendSmsForm
    • 4. 点击注册
      • 4.1 前端: 获取数据 & 发送ajax请求
      • 4.2 后端
        • 4.2.1 数据校验
        • 4.2.2 写入数据库

上一篇文章请点击 django 开发Bug追踪平台之环境搭建篇

用户注册篇

首先,总体的思维导图如下:
django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)_第1张图片

1. 前期准备

1.1 腾讯云发送短信

  • 项目中的登录 & 注册功能,需要使用手机号进行短信的接收,这里用到了腾讯云短信
  • python 操作腾讯云短信详细教程可查看此篇博文:Python 操作腾讯云短信(sms)详细教程

1.2 redis

  • redis 的下载安装以及python 操作redis 可查看此篇博文: redis 下载安装 & python 操作redis & django 连接redis

2. 注册页面展示

2.1 创建app

  • 创建一个名为 web 的app,之后的代码都在这个app里面写
    python manage.py startapp web
    

2.2 app注册

  • settings.py 文件中注册app,INSTALLED_APPS 添加自己刚才创建的app【默认应该是已经添加了,没有的话自己添加】一下
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'app01.apps.App01Config',
        'web.apps.WebConfig',
    ]
    

2.3 母版准备

2.3.1 插件引入

  • 在创建母版之前我们需要先引入 bootstrap、js 等插件,可以使用 cdn,也可以下载离线使用,这里我们将其下载下来,放置到 static 文件夹中,方便我们以后使用

离线文件可以自己去官网下载,也可以拿我这里已经下载好的,我将其放在网盘中,需要可自行下载,其中包含: js、bootstrap、font-awesome【图标】
链接:https://pan.baidu.com/s/1gQRN57XgYcD9y3-Dz8_oaA
提取码:mnjl
解压密码: ruochen666

  • web 下创建一个用于存放静态文件的 static 文件夹,然后再创建一个 plugin 文件夹,用于存放工具类文件,然后将下载好的 js、bootstrap、font-awesome 放置到 static 文件夹中,结构如下图
    django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)_第2张图片
  • 接下来我们要使用的时候就可以直接引入 static 文件夹下的文件

2.3.2 母版

为什么要用到母版?
前端页面中,注册和登录的页面基本相似,我们可以让这两个页面都继承自母版,做到代码重用

  • web 下创建一个 templates 文件夹,在 templates 文件夹下再创建一个 layout 文件夹放我们的母版文件 basic.html
    • 结构如下
      django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)_第3张图片

    • basic.html 代码如下,其中的导航条样式可以直接从 bootstrap官网组件 拿过来修改一下即可

      {% load static %}
      
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>{% block title %}{% endblock %}title>
          <link rel="stylesheet" href="{% static '/plugin/bootstrap/css/bootstrap.min.css' %}">
          <link rel="stylesheet" href="{% static '/plugin/font-awesome/css/font-awesome.min.css' %}">
          <style>
              .navbar-default{
                  border-radius: 0;
              }
          style>
          {% block css %}{% endblock %}
      head>
      <body>
      <nav class="navbar navbar-default">
          <div class="container">
              
              <div class="navbar-header">
                  <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                          data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                      <span class="sr-only">Toggle navigationspan>
                      <span class="icon-bar">span>
                      <span class="icon-bar">span>
                      <span class="icon-bar">span>
                  button>
                  <a class="navbar-brand" href="#">Tracera>
              div>
      
              
              <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                  <ul class="nav navbar-nav">
                      <li><a href="#">产品功能a>li>
                      <li><a href="#">企业方案a>li>
                      <li><a href="#">帮助文档a>li>
                      <li><a href="#">价格a>li>
                  ul>
                  <ul class="nav navbar-nav navbar-right">
                      <li><a href="#">Linka>li>
                      <li class="dropdown">
                          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
                             aria-expanded="false">Dropdown <span class="caret">span>a>
                          <ul class="dropdown-menu">
                              <li><a href="#">Actiona>li>
                              <li><a href="#">Another actiona>li>
                              <li><a href="#">Something else herea>li>
                              <li role="separator" class="divider">li>
                              <li><a href="#">Separated linka>li>
                          ul>
                      li>
                  ul>
              div>
          div>
      nav>
      
      {% block content %}{% endblock %}
      
      <script src="{% static 'js/jquery-3.4.1.min.js' %}">script>
      <script src="{% static 'plugin/bootstrap/css/bootstrap.min.css' %}">script>
      {% block js %}{% endblock %}
      body>
      html>
      
      

2.4 URL准备

  • MyDjango/MyDjango/urls.py 【我的项目名称为 MyDjango
    """MyDjango URL Configuration
    
    The `urlpatterns` list routes URLs to views. For more information please see:
        https://docs.djangoproject.com/en/1.11/topics/http/urls/
    Examples:
    Function views
        1. Add an import:  from my_app import views
        2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
    Class-based views
        1. Add an import:  from other_app.views import Home
        2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
    Including another URLconf
        1. Import the include() function: from django.conf.urls import url, include
        2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
    """
    from django.conf.urls import url, include
    from django.contrib import admin
    
    urlpatterns = [
        url(r'^admin/', admin.site.urls),
        url(r'^/', include('web.urls')),
    ]
    
  • 在 web 文件夹下创建 urls.py 文件,用于管理该app 的路由(视图函数我们下面会写)
    # -*- coding: UTF-8 -*-
    '''=================================================
    @Project -> File   :MyDjango -> urls
    @IDE    :PyCharm
    @Author :ruochen
    @Date   :2020/7/2 1:18
    @Desc   :
    =================================================='''
    from django.conf.urls import url
    from web.views import account
    
    urlpatterns = [
        url(r'^register/$', account.register, name='register'),  # register
    ]
    

2.5 模型准备【models.py】

  • 用户注册时,要填写的数据有
    • 用户名
    • 邮箱
    • 手机号
    • 密码
  • web/models.py 文件中创建一个 UserInfo 类,代码如下
    from django.db import models
    
    class UserInfo(models.Model):
        username = models.CharField(verbose_name='用户名', max_length=32)
        email = models.EmailField(verbose_name='邮箱', max_length=32)
        mobile_phone = models.CharField(verbose_name='手机号', max_length=32)
        password = models.CharField(verbose_name='密码', max_length=32)
    
        def __str__(self):
            return self.username
    
  • 迁移数据库
    python manage.py makemigrations
    python manage.py migrate
    

2.5 视图函数

  • 将web 下的 views.py 文件删除,创建一个 views 文件夹,方便管理我们的视图,然后在 views 文件夹下创建一个 account.py 文件作为注册视图,代码如下:(RegisterModelFormregister.html后面会写)
    from django.shortcuts import render
    from web.forms.account import RegisterModelForm
    
    def register(request):
        form = RegisterModelForm()
        return render(request, 'register.html', {'form': form})
    

2.6 ModelForm【简单校验 & 样式添加】

  • 前端页面要通过Form 表单循环生成数据,但是直接生成的话有点丑,而且数据也要先做一些基本的校验【例如手机号,钩子函数在后面校验表单时用到,这里先通过正则简单的校验一下手机号】
  • web 文件夹下创建一个 forms 文件夹,forms 文件夹中创建 account.py 文件,代码如下
    • 一: 对字段进行处理,例如手机号进行校验,密码为 PasswordInput 形式等
    • 二:给每个字段添加 form-control 样式,前端页面显示比较美观一点
    • 三:添加 code 【验证码】字段
    # -*- coding: UTF-8 -*-
    '''=================================================
    @Project -> File   :MyDjango -> account
    @IDE    :PyCharm
    @Author :ruochen
    @Date   :2020/7/2 12:37
    @Desc   :
    =================================================='''
    from django import forms
    from django.core.validators import RegexValidator
    
    from web import models
    
    class RegisterModelForm(forms.ModelForm):
        mobile_phone = forms.CharField(
            label='手机号',
            validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ])
        password = forms.CharField(
            label='密码', widget=forms.PasswordInput())
    
        confirm_password = forms.CharField(
            label='重复密码',
            widget=forms.PasswordInput())
        code = forms.CharField(
            label='验证码',
            widget=forms.TextInput())
    
        class Meta:
            model = models.UserInfo
            fields = ['username', 'email', 'password', 'confirm_password', 'mobile_phone', 'code']
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            for name, field in self.fields.items():
                field.widget.attrs['class'] = 'form-control'
                field.widget.attrs['placeholder'] = '请输入{}'.format(field.label,)
    
    
  • 使用上述代码是没有任何问题的,但是,对于添加 form-control 属性,之后的代码中其他字段都要用到,每次使用for 循环添加很显然有些赘余,我们可以将其封装在一个类中,这样,需要添加样式的时候直接继承这个类就可以了。
  • 修改如下,在 web/forms 下创建一个 bootstrap.py 文件,代码如下:
    # -*- coding: UTF-8 -*-
    '''=================================================
    @Project -> File   :MyDjango -> bootstrap
    @IDE    :PyCharm
    @Author :ruochen
    @Date   :2020/7/3 16:25
    @Desc   :
    =================================================='''
    class BootStrapForm(object):
    
        bootstrap_class_exclude = []
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            for name, field in self.fields.items():
                if name in self.bootstrap_class_exclude:
                    continue
                old_class = field.widget.attrs.get('class', '')
                field.widget.attrs['class'] = '{} form-control'.format(old_class)
                field.widget.attrs['placeholder'] = '请输入{}'.format(field.label,)
    
  • 然后 forms/account.py 文件修改为
    from django import forms
    from django.core.validators import RegexValidator
    
    from web import models
    from web.forms.bootstrap import BootStrapForm
    
    class RegisterModelForm(BootStrapForm, forms.ModelForm):
        password = forms.CharField(
            label='密码',
            min_length=8,
            max_length=64,
            error_messages={
                'min_length': "密码长度不能小于8个字符",
                'max_length': "密码长度不能大于64个字符"
            },
            widget=forms.PasswordInput())
    
        confirm_password = forms.CharField(
            label='重复密码',
            min_length=8,
            max_length=64,
            error_messages={
                'min_length': "重复密码长度不能小于8个字符",
                'max_length': "重复密码长度不能大于64个字符"
            },
            widget=forms.PasswordInput()
        )
    
        mobile_phone = forms.CharField(
            label='手机号',
            validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ])
    
        code = forms.CharField(
            label='验证码',
            widget=forms.TextInput())
    
        class Meta:
            model = models.UserInfo
            fields = ['username', 'email', 'password', 'confirm_password', 'mobile_phone', 'code']
    

2.7 前端页面

  • templates 文件夹下创建 register.html 文件夹,让其继承自 basic.html
  • 前端页面对于字段的展示,直接循环展示 form 表单生成的数据即可
    {% extends 'layout/basic.html' %}
    {% load static %}
    
    {% block title %} 用户注册 {% endblock %}
    
    {% block css %}
        <link rel="stylesheet" href="{% static 'css/account.css' %}">
    {% endblock %}
    
    
    {% block content %}
        <div class="account">
            <div class="title">用户注册div>
            <form id="form" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    {% if field.name == 'code' %}
                        <div class="form-group">
                            <label for="{{ field.id_for_label }}">{{ field.label }}label>
                            <div class="row">
                                <div class="col-xs-7">
                                    {{ field }}
                                    <span class="error-msg">{{ field.errors.0 }}span>
                                div>
                                <div class="col-xs-5">
                                    <input id="smsBtn" type="button" class="btn btn-default" value="点击获取验证码"/>
                                div>
                            div>
                        div>
                    {% else %}
                        <div class="form-group">
                            <label for="{{ field.id_for_label }}">{{ field.lable }}label>
                            {{ field }}
                            <span class="error-msg">{{ field.errors.0 }}span>
                        div>
                    {% endif %}
                {% endfor %}
    
                <div class="row">
                    <div class="col-xs-3">
                        <input id="submit" type="button" class="btn btn-primary" value="注  册"/>
                    div>
                div>
            form>
        div>
    
    {% endblock %}
    
    
    {% block js %}
    
    {% endblock %}
    
    

django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)_第4张图片

3. 验证码获取

3.1 思路

  • 给获取验证码按钮绑定事件,在前端页面中,用户点击获取验证码后,通过腾讯云短信向用户手机号发送验证码,并且在页面上显示60s倒计时,向后端发送ajax请求
  • 后端进行手机号校验(判断手机号是否已经注册过)和短信模板的验证(腾讯云短信的一些凭证)

3.2 具体实现

3.2.1 前端代码

  • register.html 中添加js 代码,代码如下
    {% extends 'layout/basic.html' %}
    {% load static %}
    
    {% block title %} 用户注册 {% endblock %}
    
    {% block css %}
        <link rel="stylesheet" href="{% static 'css/account.css' %}">
        <style>
            .error-msg {
                color: red;
                position: absolute;
                font-size: 13px;
            }
        style>
    {% endblock %}
    
    
    {% block content %}
        <div class="account">
            <div class="title">用户注册div>
            <form id="form" method="POST" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    {% if field.name == 'code' %}
                        <div class="form-group">
                            <label for="{{ field.id_for_label }}">{{ field.label }}label>
                            <div class="row">
                                <div class="col-xs-7">
                                    {{ field }}
                                    <span class="error-msg">span>
                                div>
                                <div class="col-xs-5">
                                    <input id="btnSms" type="button" class="btn btn-default" value="点击获取验证码">
                                div>
                            div>
                        div>
                    {% else %}
                        <div class="form-group">
                            <label for="{{ field.id_for_label }}">{{ field.label }}label>
                            {{ field }}
                            <span class="error-msg">span>
                        div>
                    {% endif %}
                {% endfor %}
    
                <div class="row">
                    <div class="col-xs-3">
                        <input id="submit" type="button" class="btn btn-primary" value="注  册"/>
                    div>
                div>
            form>
        div>
    {% endblock %}
    
    
    {% block js %}
        <script>
            // 页面框架加载完成之后自动执行函数
            $(function () {
    
                bindClickBtnSms();
            });
    
            /*
            点击获取验证码的按钮绑定事件
             */
            function bindClickBtnSms() {
                $('#btnSms').click(function () {
    
                    $('.error-msg').empty();
    
                    // 获取用户输入的手机号
                    // 找到输入框的ID,根据ID获取值,如何找到手机号的ID?
                    // Django ModelForm 默认生成字段ID为 “id_ + 字段名”
                    var mobilePhone = $('#id_mobile_phone').val();
    
                    // 发送ajax 请求,把手机号发送过去
                    $.ajax({
                        url: "{% url 'send_sms' %}",  // 等价于 /send/sms/
                        type: "GET",
                        data: {mobile_phone: mobilePhone, tpl: "register"},  // 手机号和注册的模板
                        dataType: "JSON",  // 将服务端返回的数据反序列化为字典
                        success: function (res) {
                            // ajax请求发送成功之后,自动执行的函数: res就是后端返回的值
                            if(res.status) {
                                sendSmsRemind();
                            } else {
                                // 错误信息
                                // console.log(res);  // {status: False, error: { mobile_phone: ["错误信息", ] }
                                $.each(res.error, function (key, value) {
                                    $("#id_" + key).next().text(value[0]);
                                })
                            }
    
                        }
                    })
                })
            }
    
            /*
            倒计时
             */
            function sendSmsRemind() {
                var $smsBtn = $('#btnSms');
                // 将按钮变为不可点击
                $smsBtn.prop('disabled', true);
                var time = 60;
                var remind = setInterval(function () {
                    $smsBtn.val(time + '秒重新发送');
                    time = time - 1;
                    if (time < 1) {
                        clearInterval(remind);
                        $smsBtn.val('点击获取验证码').prop('disabled', false);
                    }
                }, 1000)
            }
        script>
    
    {% endblock %}
    
    

前端页面60s倒计时用到了定时器功能,如下

var obj = setInterval(function(){  // 创建定时器,此处就相当于每1秒执行一次function函数
   console.log(123);
}, 1000)

clearInterval(obj);  // 关闭定时器

那么,对于60s的倒计时功能,我们就可以使用如下代码实现

var time = 60;
var obj = setInterval(function(){
   time = time - 1;
   if(time < 1) {
       clearInterval(obj); 
   }
}, 1000)
  • 其中有个 account.css 是自己写的css 样式,在 web/static 文件夹下新建一个 css 文件夹用于存放自己写的css 样式,然后新建一个 account.css 文件,代码如下
    .account {
        width: 400px;
        margin-top: 30px;
        margin-left: auto;
        margin-right: auto;
        border: 1px solid #f0f0f0;
        padding: 10px 30px 30px 30px;
        -webkit-box-shadow: 5px 10px 10px rgba(0, 0, 0, .05);
        box-shadow: 5px 10px 10px rgba(0, 0, 0, .05);
    }
    
    .account .title {
        font-size: 25px;
        font-weight: bold;
        text-align: center;
    }
    
    .account .form-group {
        margin-bottom: 20px;
    }
    

3.2.2 后端代码

3.2.2.1 URL
  • 首先,上面写到前端页面通过js 向后端发送ajax 请求,请求地址为 /send/sms/,那么我们首先要添加一个 urlweb/urls.py 中代码如下:
    from django.conf.urls import url
    from web.views import account
    
    urlpatterns = [
        url(r'^register/$', account.register, name='register'),  # register
        url(r'^send/sms/$', account.send_sms, name='send_sms'),  # register
    ]
    
3.2.2.2 视图函数
  • 上面添加了路由,接下来是视图函数,在 web/views/account.py 文件中添加代码如下
    from django.shortcuts import render, HttpResponse
    from django.http import JsonResponse
    from web.forms.account import RegisterModelForm, SendSmsForm
    
    def register(request):
        """ 注册 """
        form = RegisterModelForm()
        return render(request, 'register.html', {'form': form})
    
    def send_sms(request):
        """ 发送短信 """
        form = SendSmsForm(request, data=request.GET)
        # 只是校验手机号:不能为空、格式是否正确
        if form.is_valid():
            return JsonResponse({'status': True})
    
        return JsonResponse({'status': False, 'error': form.errors})
    
3.2.2.3 配置文件
3.2.2.3.1 腾讯云短信配置文件
  • 对于腾讯云短信的配置文件,我们应该将其放置在 local_settings.py 文件中( local_settings.py 文件的作用,我在上一篇文章中提到过),同时要在 settings.py 文件中声明
  • local_settings.py 文件配置代码如下
import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# --------- sms -----------
# 腾讯云短信应用的 app_id
TENCENT_SMS_APP_ID = '自己的app_id'
# 腾讯云短信应用的 app_key
TENCENT_SMS_APP_KEY = '自己的app_key'
# 腾讯云短信签名内容
TENCENT_SMS_SIGN = 'xxxx'

# 短信模板
TENCENT_SMS_TEMPLATE = {
    'register': 'xxxx',
    'login': 'xxxx',
}

关于腾讯云短信的配置,可查看此篇文章:Python 操作腾讯云短信(sms)详细教程

  • 这里的 app_id & app_key 就是文中提到的创建应用后的 AppID & AppKey
  • 短信签名内容就是在创建签名后显示的那个内容,比如我这个是 小小猿若尘 ,如下
    django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)_第5张图片
  • 短信模板就是创建了模板后对应的 ID
  • settings.py 文件中也要声明如下(settings.py文件最后添加下面代码,赋值随便填,因为我们在最后导入了 local_settings.py 文件,项目实际上使用的是 local_settings.py 文件中的配置,这里写只是为了声明一下,因为我们的 local_settings.py 文件是不会给别人的)
# --------- sms -----------
# 腾讯云短信应用的 app_id
TENCENT_SMS_APP_ID = 6666
# 腾讯云短信应用的 app_key
TENCENT_SMS_APP_KEY = '6666'
# 腾讯云短信签名内容
TENCENT_SMS_SIGN = 'xxxx'
# 短信模板
TENCENT_SMS_TEMPLATE = {
    'register': 666666,
    'login': 666666,
}
3.2.2.3.2 redis 配置文件
  • redis的具体操作可查看此篇文章: redis 下载安装 & python 操作redis & django 连接redis,这里用到的就是 django-redis 模块【记得安装】
  • redis的配置放在 local_settings.py文件中,代码如下:
    CACHES = {
        "default": {
            "BACKEND": "django_redis.cache.RedisCache",
            "LOCATION": "redis://192.168.1.6:6379",  # 在终端中通过 [ipconfig] 命令查看
            "OPTIONS": {
                "CLIENT_CLASS": "django_redis.client.DefaultClient",
                "CONNECTION_POOL_KWARGS": {
                    "max_connections": 1000,
                    "encoding": 'utf-8'
                },
                "PASSWORD": "root"  # 密码,上述文章有具体说明
            }
        }
    
3.2.2.4 SendSmsForm
  • 在上面视图函数中我们通过 SendSmsForm 进行了校验,web/forms/account.py 文件中代码如下
    • 一:对手机号、短信模板进行了校验
    • 二:利用 腾讯云短信 向用户发送短信
      • 在项目目录下创建 utils 文件夹,存放我们的工具类,再创建一个 tencent 文件夹,在文件夹下创建 sms.py 文件, 如下:
        django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)_第6张图片
      • sms.py 文件代码如下
        # -*- coding: UTF-8 -*-
        '''=================================================
        @Project -> File   :MyDjango -> sms
        @IDE    :PyCharm
        @Author :ruochen
        @Date   :2020/6/21 15:57
        @Desc   :
        =================================================='''
        
        import ssl
        
        # ssl._create_default_https_context = ssl._create_unverified_context
        from qcloudsms_py import SmsMultiSender, SmsSingleSender
        from qcloudsms_py.httpclient import HTTPError
        from django.conf import settings
        
        def send_sms_single(phone_num, template_id, template_param_list):
            """
            单条发送短信
            :param phone_num: 手机号
            :param template_id: 腾讯云短信模板ID
            :param template_param_list: 短信模板所需参数列表,例如:【验证码:{1},描述:{2}】,则传递参数 [888,666]按顺序去格式化模板
            :return:
            """
            appid = settings.TENCENT_SMS_APP_ID  # 自己应用ID
            appkey = settings.TENCENT_SMS_APP_KEY  # 自己应用Key
            sms_sign = settings.TENCENT_SMS_SIGN  # 自己腾讯云创建签名时填写的签名内容(使用公众号的话这个值一般是公众号全称或简称)
            sender = SmsSingleSender(appid, appkey)
            try:
                response = sender.send_with_param(86, phone_num, template_id, template_param_list, sign=sms_sign)
            except HTTPError as e:
                response = {'result': 1000, 'errmsg': "网络异常发送失败"}
            return response
        
        def send_sms_multi(phone_num_list, template_id, param_list):
            """
            批量发送短信
            :param phone_num_list:手机号列表
            :param template_id:腾讯云短信模板ID
            :param param_list:短信模板所需参数列表,例如:【验证码:{1},描述:{2}】,则传递参数 [888,666]按顺序去格式化模板
            :return:
            """
            appid = settings.TENCENT_SMS_APP_ID  # 自己应用ID
            appkey = settings.TENCENT_SMS_APP_KEY  # 自己应用Key
            sms_sign = settings.TENCENT_SMS_SIGN  # 自己腾讯云创建签名时填写的签名内容(使用公众号的话这个值一般是公众号全称或简称)
            sender = SmsMultiSender(appid, appkey)
            try:
                response = sender.send_with_param(86, phone_num_list, template_id, param_list, sign=sms_sign)
            except HTTPError as e:
                response = {'result': 1000, 'errmsg': "网络异常发送失败"}
            return response
        
    • 三: 将验证码存入redis数据库中,且超时时间为 60s【即60s后自动消失】,这样我们在提交表单的时候,就可以将用户输入的验证码同redis中存的进行比较,且如果时间超过 60s,用户就要重新获取验证码
      • redis的操作可以查看此篇文章: redis 下载安装 & python 操作redis & django 连接redis
    #-*- coding: UTF-8 -*-
    '''=================================================
    @Project -> File   :MyDjango -> account
    @IDE    :PyCharm
    @Author :ruochen
    @Date   :2020/7/2 12:37
    @Desc   :
    =================================================='''
    import random
    from django import forms
    from django.core.validators import RegexValidator
    from django.core.exceptions import ValidationError
    from django.conf import settings
    
    from django_redis import get_redis_connection
    
    from web import models
    from web.forms.bootstrap import BootStrapForm
    from utils.tencent.sms import send_sms_single
    
    class RegisterModelForm(BootStrapForm, forms.ModelForm):
        password = forms.CharField(
            label='密码',
            min_length=8,
            max_length=64,
            error_messages={
                'min_length': "密码长度不能小于8个字符",
                'max_length': "密码长度不能大于64个字符"
            },
            widget=forms.PasswordInput())
    
        confirm_password = forms.CharField(
            label='重复密码',
            min_length=8,
            max_length=64,
            error_messages={
                'min_length': "重复密码长度不能小于8个字符",
                'max_length': "重复密码长度不能大于64个字符"
            },
            widget=forms.PasswordInput()
        )
    
        mobile_phone = forms.CharField(
            label='手机号',
            validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ])
    
        code = forms.CharField(
            label='验证码',
            widget=forms.TextInput())
    
        class Meta:
            model = models.UserInfo
            fields = ['username', 'email', 'password', 'confirm_password', 'mobile_phone', 'code']
    
    class SendSmsForm(forms.Form):
        mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ])
    
        def __init__(self, request, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.request = request
    
        def clean_mobile_phone(self):
            """ 手机号校验的钩子 """
            mobile_phone = self.cleaned_data['mobile_phone']
    
            # 判断短信模板是否有问题
            tpl = self.request.GET.get('tpl')
            template_id = settings.TENCENT_SMS_TEMPLATE.get(tpl)
            if not template_id:
                # self.add_error('mobile_phone', '短信模板错误')
                raise ValidationError('短信模板错误')
    
            # 检验数据库中是否已有手机号
            exists = models.UserInfo.objects.filter(mobile_phone=mobile_phone).exists()
            if exists:
                raise ValidationError('手机号已存在')
    
            # 发短信
            code = random.randrange(1000, 9999)
    
            # 发送短信
            sms = send_sms_single(mobile_phone, template_id, [code, ])
            if sms['result'] != 0:
                raise ValidationError('短信发送失败,{}'.format(sms['errmsg']))
    
            # 验证码写入redis(django-redis)
            conn = get_redis_connection()
            conn.set(mobile_phone, code, ex=60)
    
            return mobile_phone
    

4. 点击注册

4.1 前端: 获取数据 & 发送ajax请求

  • 收集表单中的数据(找到每一个字段)
  • 数据通过ajax发送到后台【POST请求】
  • register.html 文件中js 部分添加点击注册事件函数,代码如下(前面代码部分同上,只是在js 中添加了 bindClickSubmit 函数,并让其在页面框架加载完成后自动执行)

ajax请求这里我没有再写一个URL,而是复用了 /register/,只需要判断用户发的是哪种请求就可以

  • 用户反正地址时发送的是 GET 请求,这时我们直接让其跳转到注册页面即可
  • 用户点击注册时,发送的是 POST 请求,这时我们进行表单验证 & 写入数据库等操作即可
{% block js %}
    <script>
        // 页面框架加载完成之后自动执行函数
        $(function () {
            bindClickBtnSms();
            bindClickSubmit();
        });

        /*
        点击提交(注册)
        */
        function bindClickSubmit() {
            $('#btnSubmit').click(function () {
                $('.error-msg').empty();

                // 收集表单中的数据(找到每一个字段)
                // 数据通过ajax发送到后台
                $.ajax({
                    url: "{% url 'register' %}",
                    type: "POST",
                    data: $('#regForm').serialize(),  // 获取表单中所有的键值, 包含所有字段的数据 + csrf token
                    dataType: "JSON",
                    success: function (res) {
                        if (res.status) {
                            location.href = res.data;
                        } else {
                            $.each(res.error, function (key, value) {
                                $("#id_" + key).next().text(value[0]);
                            })
                        }
                    }
                })
            })
        }

        /*
        点击获取验证码的按钮绑定事件
         */
        function bindClickBtnSms() {
            $('#btnSms').click(function () {
                $('.error-msg').empty();

                // 获取用户输入的手机号
                // 找到输入框的ID,根据ID获取值,如何找到手机号的ID?
                // Django ModelForm 默认生成字段ID为 “id_ + 字段名”
                var mobilePhone = $('#id_mobile_phone').val();

                // 发送ajax 请求,把手机号发送过去
                $.ajax({
                    url: "{% url 'send_sms' %}",  // 等价于 /send/sms/
                    type: "GET",
                    data: {mobile_phone: mobilePhone, tpl: "register"},  // 手机号和注册的模板
                    dataType: "JSON",  // 将服务端返回的数据反序列化为字典
                    success: function (res) {
                        // ajax请求发送成功之后,自动执行的函数: res就是后端返回的值
                        if (res.status) {
                            sendSmsRemind();
                        } else {
                            // 错误信息
                            // console.log(res);  // {status: False, error: { mobile_phone: ["错误信息", ] }
                            $.each(res.error, function (key, value) {
                                $("#id_" + key).next().text(value[0]);
                            })
                        }

                    }
                })
            })
        }

        /*
        倒计时
         */
        function sendSmsRemind() {
            var $smsBtn = $('#btnSms');
            $smsBtn.prop('disabled', true);
            var time = 60;
            var remind = setInterval(function () {
                $smsBtn.val(time + '秒重新发送');
                time = time - 1;
                if (time < 1) {
                    clearInterval(remind);
                    $smsBtn.val('点击获取验证码').prop('disabled', false);
                }
            }, 1000)
        }
    script>
{% endblock %}

4.2 后端

4.2.1 数据校验

  • 校验如下:
    • 用户名、邮箱、手机号在钩子函数中验证
    • 密码通过md5加密后返回
      • md5 加密 单独封装起来,在utils文件夹中添加 encrypt.py 文件
        django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)_第7张图片
      • 代码如下
        import uuid
        import hashlib
        
        from django.conf import settings
        
        def md5(string):
            """ MD5加密 """
            hash_object = hashlib.md5(settings.SECRET_KEY.encode('utf-8'))
            hash_object.update(string.encode('utf-8'))
            return hash_object.hexdigest()
        
        def uid(string):
            data = "{}-{}".format(str(uuid.uuid4()), string)
            return md5(data)
        
    • 验证码通过redis 根据手机号(键)获取值与用户输入的进行比较【注意存在过期时间】
  • 修改 forms/account.py 文件如下:(只修改了 RegisterModelForm 类的代码,其余不变)
from utils import encrypt

class RegisterModelForm(BootStrapForm, forms.ModelForm):
    password = forms.CharField(
        label='密码',
        min_length=8,
        max_length=64,
        error_messages={
            'min_length': "密码长度不能小于8个字符",
            'max_length': "密码长度不能大于64个字符"
        },
        widget=forms.PasswordInput())

    confirm_password = forms.CharField(
        label='重复密码',
        min_length=8,
        max_length=64,
        error_messages={
            'min_length': "重复密码长度不能小于8个字符",
            'max_length': "重复密码长度不能大于64个字符"
        },
        widget=forms.PasswordInput()
    )

    mobile_phone = forms.CharField(
        label='手机号',
        validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ])

    code = forms.CharField(
        label='验证码',
        widget=forms.TextInput())

    class Meta:
        model = models.UserInfo
        fields = ['username', 'email', 'password', 'confirm_password', 'mobile_phone', 'code']

    def clean_username(self):
        username = self.cleaned_data['username']

        exists = models.UserInfo.objects.filter(username=username).exists()
        if exists:
            raise ValidationError('用户名已存在')
            # self.add_error('username', '用户名已存在')
        return username

    def clean_email(self):
        email = self.cleaned_data['email']

        exists = models.UserInfo.objects.filter(email=email).exists()
        if exists:
            raise ValidationError('邮箱已存在')
        return email

    def clean_password(self):
        pwd = self.cleaned_data['password']
        # 加密 & 返回
        return encrypt.md5(pwd)

    def clean_confirm_password(self):
        # pwd = self.cleaned_data['password']
        pwd = self.cleaned_data.get('password')

        confirm_pwd = encrypt.md5(self.cleaned_data['confirm_password'])

        if pwd != confirm_pwd:
            raise ValidationError('两次密码不一致')
        return confirm_pwd

    def clean_mobile_phone(self):
        mobile_phone = self.cleaned_data['mobile_phone']
        exists = models.UserInfo.objects.filter(mobile_phone=mobile_phone).exists()
        if exists:
            raise ValidationError('手机号已注册')
        return mobile_phone

    def clean_code(self):
        code = self.cleaned_data['code']
        # mobile_phone = self.cleaned_data['mobile_phone']

        mobile_phone = self.cleaned_data.get('mobile_phone')
        if not mobile_phone:
            return code

        conn = get_redis_connection()
        redis_code = conn.get(mobile_phone)
        if not redis_code:
            raise ValidationError('验证码失效或未发送,请重新发送')

        redis_str_code = redis_code.decode('utf-8')

        if code.strip() != redis_str_code:
            raise ValidationError('验证码错误,请重新输入')

        return code

4.2.2 写入数据库

  • 数据校验成功后,即可将在数据库中创建一条记录,跳转到 /login/ 页面(登录页面下一篇博文具体介绍)
  • web/views/account.py 文件中代码修改如下:(只修改了 register 函数的内容,其余不变)
def register(request):
    """ 注册 """
    if request.method == 'GET':
        form = RegisterModelForm()
        return render(request, 'register.html', {'form': form})

    form = RegisterModelForm(data=request.POST)
    if form.is_valid():
        # 验证通过,写入数据库(密码要是密文)
        # data = form.cleaned_data
        # data.pop('code')
        # data.pop('confirm_password')
        # instance = models.UserInfo.objects.create(**data)
        # save() 等同于上述代码,会自动剔除数据库中没有的数据
        # 用户表中新建了一条数据(注册)
       	form.save()

        return JsonResponse({'status': True, 'data': '/login/'})

    return JsonResponse({'status': False, 'error': form.errors})

持续更新中,欢迎大家关注博主

你可能感兴趣的:(#,django,django开发,bug追踪平台,用户注册,腾讯云短信,redis)