专栏首页若尘的技术专栏【玩转腾讯云】django 开发Bug追踪平台之用户注册篇(基于腾讯云短信 & redis)
原创

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

项目预览 👉 Bug追踪平台【云短信买不起了,可通过 手机号:18203503747 密码:ruochen666 登入体验】

项目gitee地址 👉 saas

本篇教程对应代码为 【注册验证码处理】提交,可通过对应分支查看

用户注册篇

首先,总体的<font color="red">思维导图</font>如下:

1. 前期准备

1.1 腾讯云发送短信

1.2 redis

2. 注册页面展示

2.1 创建app

  • 创建一个名为 web 的app,之后的代码都在这个app里面写```python python manage.py startapp web ```2.2 app注册
  • settings.py 文件中注册app,INSTALLED_APPS 添加自己刚才创建的app【默认应该是已经添加了,没有的话自己添加】一下```python 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 文件夹中,结构如下图
  • 接下来我们要使用的时候就可以直接引入 static 文件夹下的文件2.3.2 母版为什么要用到母版? 前端页面中,注册和登录的页面基本相似,我们可以让这两个页面都继承自母版,做到代码重用
  • web 下创建一个 templates 文件夹,在 templates 文件夹下再创建一个 layout 文件夹放我们的母版文件 basic.html - 结构如下
- `basic.html` 代码如下,其中的导航条样式可以直接从 [bootstrap官网组件](https://v3.bootcss.com/components/#navbar) 拿过来修改一下即可 
	```html
	{% load static %}
	<!DOCTYPE html>
	<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">
	        <!-- Brand and toggle get grouped for better mobile display -->
	        <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 navigation</span>
	                <span class="icon-bar"></span>
	                <span class="icon-bar"></span>
	                <span class="icon-bar"></span>
	            </button>
	            <a class="navbar-brand" href="#">Tracer</a>
	        </div>
	
	        <!-- Collect the nav links, forms, and other content for toggling -->
	        <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="#">Link</a></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="#">Action</a></li>
	                        <li><a href="#">Another action</a></li>
	                        <li><a href="#">Something else here</a></li>
	                        <li role="separator" class="divider"></li>
	                        <li><a href="#">Separated link</a></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】 ```python """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 的路由(视图函数我们下面会写) ```python # -*- 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 类,代码如下 ```python 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 python manage.py makemigrations python manage.py migrate ```2.5 视图函数
  • 将web 下的 views.py 文件删除,创建一个 views 文件夹,方便管理我们的视图,然后在 views 文件夹下创建一个 account.py 文件作为注册视图,代码如下:(RegisterModelFormregister.html后面会写) ```python 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 【验证码】字段 ```python # -*- 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 文件,代码如下: ```python # -*- 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 文件修改为 ```python 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 表单生成的数据即可 ```html {% 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 %}
```

3. 验证码获取

3.1 思路

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

3.2 具体实现

3.2.1 前端代码

  • register.html 中添加js 代码,代码如下 ```html {% 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 文件,代码如下 ```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 中代码如下: ```python 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 文件中添加代码如下 ```python 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 文件中声明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)详细教程
  • local_settings.py 文件配置代码如下
    • 这里的 app_id & app_key 就是文中提到的创建应用后的 AppID & AppKey
    • 短信签名内容就是在创建签名后显示的那个内容,比如我这个是 小小猿若尘 ,如下
    • 短信模板就是创建了模板后对应的 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文件中,代码如下: ```python 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 文件, 如下:
    - sms.py 文件代码如下 ```python # -*- 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](https://blog.csdn.net/qq_29339467/article/details/107920255)
```python
#-*- 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 文件
    - 代码如下 ```python 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/ 页面(登录页面下一篇博文具体介绍)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})
  • web/views/account.py 文件中代码修改如下:(只修改了 register 函数的内容,其余不变)

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

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 转发有礼 | 50篇+云原生系列干货文章汇总,请查收!

    云原生技术干货文章合集,来咯~ ? 2020 年,要说咱们技术圈子里什么最火? 云原生肯定是那 NO.1 ? 截止目前,我们不难看出,K8s 容器、服务...

    腾讯云原生
  • 开工必备!50+篇超实用云原生技术干货合集

    kai 开 gong 工 da 大 ji 吉 新年新气象,更要1G棒 2020年没写完的代码,现在还有思路吗? 2021年开始使用云原生技术了吗? 一开工就遇...

    腾讯云原生
  • 燃烧的“蚂蚁呀嘿”同款,你get了吗?

    ? “蚂蚁呀嘿,蚂蚁呀呼,蚂蚁呀哈哈......”,过去一周,类似的BGM特效变脸视频在朋友圈等社交平台魔性走红。提前体验过的朋友都知道,最开始是由一款俄罗斯...

    腾讯云AI
  • 腾讯云上快速爆发的腾讯会议

    庚子新春,一场突其而来的疫情打乱了中国经济秩序。但经济终要复苏,此时,线上会议服务成为企业远程工作的重要协同工具。

    周小军@运维专家
  • 腾讯云中间件产品月报(2021年第2期)| 文末元宵好礼拼手速

    ? ? 腾讯云中间件 - 微服务团队产品2021年2月简报: 腾讯云微服务引擎 TSE 邀您内测 消息队列 CKafka 支持跨可用区部署;支持实例升配展示进...

    腾讯云中间件团队
  • 产品分享 | 腾讯优图推出人像驱动技术,“蚂蚁呀嘿”AI产品了解一下

    最近,一款以“蚂蚁呀嘿,蚂蚁呀呼,蚂蚁呀哈哈......”为BGM的特效视频,在朋友圈等社交平台魔性走红,搭配五官摇晃的表情动作,让人忍俊不禁。

    优图实验室
  • 鹅厂这波青年用“云”监测云

    引言 “绿水青山,就是金山银山”,随着我国加强立法,大力投入环境治理,大家已经明显感觉到身边的大气环境在不断改善,那么除了国家气象局的城市级监测数据外,我们身...

    腾讯云数据库 TencentDB
  • ​新财报背后:腾讯云的攻势与焦灼

    我们每天都会使用手机QQ、微信发送文件进行办公或社交分享,亦或是进行网络购物、聆听音乐······那么,在我们享受其网络便利的时候,你是否注意到了APP启动界面...

    金融外参
  • 腾讯云发布全新游戏云解决方案,助力游戏开发者高效开发

    ? 9月10日,在2020腾讯全球数字生态大会游戏专场上,腾讯云发布了全新升级的游戏解决方案及游戏服务器引擎GSE、游戏原生数据库TcaplusDB等多款新产...

    腾讯云音视频
  • 短信代收,一个年产数亿的黑产链条

    陈梦 腾讯安全平台部高级安全产品经理 我将会通过一个系列文章来给大家揭开这里的神秘面纱,看看究竟是谁坑走了创业者的钱。今天来看第一篇——年产值过亿的短信代收...

    腾讯研究院
  • 数据中心基于UWB的人员定位&追踪方案实践

    前言 嗨,大家好,我是鹅厂的物联网工程师小Q,又和大家见面啦。相信大家还记得《实验室的光模块都去哪里了?》那篇文章里,我利用云化RFID资产管理系统,解决了实...

    鹅厂网事
  • 给小程序挑选物美价廉的正版音乐,用它

    每一个由开发者辛辛苦苦码出来的小程序,都值得拥有配得上它的背景音乐。 这是理想状态。 现实情况是,当你选好了一首“意中音乐”,它却可能会害你收到来自法务的问...

    正版内容直通车
  • 【玩转腾讯云】征文活动获奖名单公布

    由云+社区联合腾讯云免费体验馆及各产品团队举办【玩转腾讯云】征文活动,吸引入驻作者积极参加,非常感谢各位作者的参与。经过评委老师从产品创新性、实用性、可借鉴性、...

    云加社区
  • 【云+社区年度征文】2020征文活动获奖名单公布

    由腾讯云+社区主办的云+社区 2020 年度征文活动在2020年12月31号圆满的落下帷幕。年度征文活动自2020年11月发布后,吸引了众多社区内的小伙伴。经过...

    云加社区
  • 响铃:连续三季翻番,在收入问题上腾讯云如何体现发展质量优势?

    互联网大佬发财报了总会惹来无数双眼睛的关注,腾讯三季度24%的超预期同比收入增长,免不了一边被大众媒体感慨腾讯“你大爷还是你大爷”,一边又被一贯不看好的好事者挖...

    曾响铃
  • 小程序开发指南

    用户1263954
  • WeTest五周年 | “领航者”的破浪之路:汇聚

    ? “WeTest把自己的积累萃取精华共享给行业,希望以此可以提高手游市场品质门槛” ——QM项目中心总监魏学峰 ? 一、汇聚技术能力   2016年,智能...

    WeTest质量开放平台团队
  • 手搓一个分布式大气监测系统(一)系统功能与架构概述

    “绿水青山,就是金山银山”,随着我国加强立法,大力投入环境治理,大家已经明显感觉到身边的大气环境在不断改善,那么除了国家气象局的城市级监测数据外,我们身边的微环...

    高树磊
  • 腾讯云物联网开发平台 IoT Explorer 全面指引

    腾讯云 IoT Explorer 是腾讯云主推的一站式物联网开发平台,所谓一站式,就是包含了设备侧到应用侧安卓iOS app,甚至小程序的全链条开发支持。

    twowinter

扫码关注云+社区

领取腾讯云代金券