3.2 django项目-新闻博客系统之用户注册功能2

03.1 用户注册功能2

一、前后端分离

使用json数据交互

1、结构设计

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

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

在项目根目录中utils文件夹下创建 utils/response/response_code.py文件,用于定义错误编码,代码如下:

class Code:
    OK = "0"
    DBERR = "4001"
    NODATA = "4002"
    DATAEXIST = "4003"
    DATAERR = "4004"
    METHERR = "4005"
    SMSERROR = "4006"
    SMSFAIL = "4007"

    SESSIONERR = "4101"
    LOGINERR = "4102"
    PARAMERR = "4103"
    USERERR = "4104"
    ROLEERR = "4105"
    PWDERR = "4106"

    SERVERERR = "4500"
    UNKOWNERR = "4501"


error_map = {
    Code.OK: "成功",
    Code.DBERR: "数据库查询错误",
    Code.NODATA: "无数据",
    Code.DATAEXIST: "数据已存在",
    Code.DATAERR: "数据错误",
    Code.METHERR: "方法错误",
    Code.SMSERROR: "发送短信验证码异常",
    Code.SMSFAIL: "发送短信验证码失败",

    Code.SESSIONERR: "用户未登录",
    Code.LOGINERR: "用户登录失败",
    Code.PARAMERR: "参数错误",
    Code.USERERR: "用户不存在或未激活",
    Code.ROLEERR: "用户身份错误",
    Code.PWDERR: "密码错误",

    Code.SERVERERR: "内部错误",
    Code.UNKOWNERR: "未知错误",
}

2、快捷方法

为了方便定义一个快捷方法,在utils目录下创建utils/response/response_json.py文件文件代码如下:

import datetime
from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder
from .response_code import Code


# json编码器
# 自定义序列化器,处理时间字段
class MyJSONEncoder(DjangoJSONEncoder):
    def default(self, o):
        if isinstance(o, datetime.datetime):
            # 序列化数据,将datetime时间转为字符串
            return o.astimezone().strftime('%Y-%m-%d %H:%M:%S')  # 转换为本地时间
        else:
            return super().default(o)


def json_response(errno=Code.OK, errmsg='', data=None, kwargs=None):
    '''
    返回json数据格式
    :param errno:
    :param errmsg:
    :param data:
    :param kwargs:
    :return:
    '''
    json_dict = {
        'errno': errno,
        'errmsg': errmsg,
        'data': data,
    }
    if kwargs and isinstance(kwargs, dict):
        json_dict.update(kwargs)

    return JsonResponse(json_dict, encoder=MyJSONEncoder)

二、用户名检测

1、接口设计

接口说明:

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

参数说明:

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

返回结果:

{
    "errno": "0",
    "errmsg": "OK",
    "data": {
        "username":username,
        "count":1
    }
}

2、后端代码

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

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

别忘了在settings文件中注册app

2.verification/views.py代码

def check_username_view(request, username):
    count = User.objects.filter(username=username).count()
    data = {
        'username': username,
        'count': count,
    }
    return json_response(data=data)

3、路由

re_path('check_username/(?P\D\w{5,19})/', views.check_username_view, name='check_username'),

三、手机号码检测

1、接口设计

接口说明:

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

参数说明:

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

返回结果:

{
    "errno": "0",
    "errmsg": "OK",
    "data": {
        "phone":phone,
        "count":1
    }
}

2、后端代码

verification/views.py代码

def check_phone_view(request, phone):
    count = User.objects.filter(phone=phone).count()
    data = {
        'phone': phone,
        'count': count,
    }

    return json_response(data=data)

路由

re_path('check_phone/(?P1[3-9]\d{9})/', views.check_phone_view, name='check_phone'),

四、获取短信验证码

1.业务流程分析

  • 检查图片验证码是否正确
  • 检查是否在60s内发送记录
  • 生成短信验证码
  • 发送短信
  • 保存短信验证码与发送记录

2.接口设计

接口说明:

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

参数说明:

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

返回结果:

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

3.后端代码

class Check_smscode_view(View):
    '''
    发送短信验证码
    '''
    def post(self, request):
        # 1、图片验证码、手机号码校验
        form = forms.CheckPhoneForm(request.POST, request=request)
        if form.is_valid():
            # 2、生成手机验证码
            # 获取手机号码
            phone = form.cleaned_data.get('phone')
            sms_code = ''.join([str(random.randint(0, 9)) for _ in range(4)])
            # print(phone, sms_code)
            # # 3、发送手机验证码(云通讯)
            # ccp = CCP()
            # try:
            #     res = ccp.send_template_sms(phone, [sms_code, constants.SMS_CODE_EXPIRES_M], "1")
            #     if res == 0:
            #         logger.info('发送短信验证码[正常][mobile: %s sms_code: %s]' % (phone, sms_code))
            #     else:
            #         logger.error('发送短信验证码[失败][moblie: %s sms_code: %s]' % (phone, sms_code))
            #         return json_response(errno=Code.SMSFAIL, errmsg=error_map[Code.SMSFAIL])
            # except Exception as e:
            #     logger.error('发送短信验证码[异常][mobile: %s message: %s]' % (phone, e))
            #     return json_response(errno=Code.SMSERROR, errmsg=error_map[Code.SMSERROR])
            # 4、保存手机验证码到redis数据库
            sms_flag_key = 'sms_flag_{}'.format(phone)
            sms_code_key = 'sms_code_{}'.format(sms_code)
            redis_conn = get_redis_connection(alias='verify_code')
            p1 = redis_conn.pipeline()
            try:
                p1.setex(sms_flag_key, constants.SMS_CODE_INTERVAL, 1)
                p1.setex(sms_code_key, constants.SMS_CODE_EXPIRES_S, sms_code)
                p1.execute()
                logger.info('短信验证码发送成功,{}:{}'.format(phone, sms_code))
                return json_response(errmsg='短信验证码发送成功!')
            except Exception as e:
                logger.error('redis 执行异常:{}'.format(e))
                return json_response(errno=Code.UNKOWNERR, errmsg=error_map[Code.UNKOWNERR])
        else:
            errmsg = form.get_errors()
            return json_response(errno=Code.DATAERR, errmsg=errmsg)

forms.py

from django import forms
from django.core.validators import RegexValidator
from django_redis import get_redis_connection

from user.models import User

# 创建手机号码的正则校验器
phone_validator = RegexValidator(r'^1[3-9]\d{9}$', '手机号码格式不正确')


class CheckPhoneForm(forms.Form):
    '''
    手机号码及图片验证码校验
    '''

    # 将request对象传入校验类
    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request')
        super().__init__(*args, **kwargs)

    captcha = forms.CharField(max_length=4,
                              min_length=4,
                              error_messages={
                                  'max_length': '图片验证码长度有误',
                                  'min_length': '图片验证码长度有误',
                                  'required': '图片验证码不能为空',
                              }
                              )
    phone = forms.CharField(max_length=11,
                            min_length=11,
                            validators=[phone_validator, ],
                            error_messages={
                                'max_length': '手机号码长度有误',
                                'min_length': '手机号码长度有误',
                                'required': '手机号码不能为空',
                            })

    def clean(self):
        clean_data = super().clean()
        phone = clean_data.get('phone')
        captcha = clean_data.get('captcha')
        # 1、 校验手机号码是否已经存在
        if User.objects.filter(phone=phone).count():
            raise forms.ValidationError('手机号码已被注册,请重新输入!')
        # 2、校验图片验证码
        image_code = self.request.session.get('image_code')
        if (not image_code) or (image_code.upper() != captcha.upper()):
            raise forms.ValidationError('图片验证码校验失败')
        # 3、校验是否在60秒内已经发送过短信
        redis_conn = get_redis_connection(alias='verify_code')
        if redis_conn.get('sms_flag_{}'.format(phone)):
            raise forms.ValidationError('获取短信验证码过于频繁')
        return clean_data

    def get_errors(self):
        errors = self.errors.get_json_data()
        err_msg_list = []
        for item in errors.values():
            for ite in item:
                err_msg_list.append(ite.get('message'))
        err_msg_str = '/'.join(err_msg_list)
        return err_msg_str

常量 constants.py

#!/usr/bin/python
# -*- coding: utf-8 -*

# 图片验证码过期时间 单位秒
IMAGE_CODE_EXPIRES = 60 * 5

# 短信验证码长度
SMS_CODE_LENGTH = 4

# 短信验证码发送间隔 秒
SMS_CODE_INTERVAL = 60

# 短信验证码过期时间 分
SMS_CODE_EXPIRES_M = 5

# 短信验证码过期时间 秒
SMS_CODE_EXPIRES_S = 60*SMS_CODE_EXPIRES_M

# 短信发送模板
SMS_CODE_TEMP_ID = 1

# session过期时间,默认7天
USER_SESSION_EXPIRY = 7*24*60*60

4、短信验证码平台-云通讯

本项目中使用的短信验证码平台为云通讯平台,文档参考地址

主要是因为可以免费测试,注册后赠送8元用于测试。

开发参数:

yuntongxun_1.jpg
_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'

设置测试手机号码

yuntongxun_2.jpg

五、注册功能

1、业务流程分析

  1. 判断用户名是否为空,是否已注册
  2. 判断密码是否为空,格式是否正确
  3. 判断两次密码是否一致
  4. 判断手机号码是否为空,格式是否正确
  5. 判断短信验证码是否为空,格式是否正确,是否与真实短信验证码相同

2、接口设计

接口说明:

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

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

参数说明:

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

返回结果:

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

3、后端代码

user/views.py代码:

class User_register(View):
    '''
    用户注册类视图
    '''
    def get(self, request):
        return render(request, 'user/register.html')

    def post(self, request):
        form = Checkregist(request.POST)
        # 校验参数
        if form.is_valid():
            # 校验参数成功
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            phone = form.cleaned_data.get('phone')
            # 创建新用户
            User.objects.create_user(username=username, password=password, phone=phone)
            # 返回成功
            return json_response(errmsg='注册成功,请登录')
        else:
            errmsg = form.get_errors()

            return json_response(errno=Code.DATAERR, errmsg=errmsg)

forms.py

import re
from django import forms
from django.core.validators import RegexValidator
from django_redis import get_redis_connection
from django.db.models import Q
from .models import User
from verification import constants

# 创建手机号码的正则校验器
phone_validator = RegexValidator(r'^1[3-9]\d{9}$', '手机号码格式不正确')
username_validator = RegexValidator(r'^\D\w{5,19}$', '请输入以非数字开头的用户名,6-20个字符')


class Checkregist(forms.Form):
    '''
    注册校验
    '''
    username = forms.CharField(max_length=20,
                               min_length=6,
                               validators=[username_validator, ],
                               error_messages={
                                  'max_length': '用户名长度有误',
                                  'min_length': '用户名长度有误',
                                  'required': '用户名不能为空',
                              }
                              )
    password = forms.CharField(max_length=20,
                               min_length=6,
                               error_messages={
                                   'max_length': '密码长度有误',
                                   'min_length': '密码长度有误',
                                   'required': '密码不能为空',
                               }
                               )
    password_repeat = forms.CharField(max_length=20,
                                      min_length=6,
                                      error_messages={
                                           'max_length': '密码长度有误',
                                           'min_length': '密码长度有误',
                                           'required': '密码不能为空',
                               }
                               )

    phone = forms.CharField(max_length=11,
                            min_length=11,
                            validators=[phone_validator, ],
                            error_messages={
                                'max_length': '手机号码长度有误',
                                'min_length': '手机号码长度有误',
                                'required': '手机号码不能为空',
                            }
                            )

    sms_code = forms.CharField(max_length=constants.SMS_CODE_LENGTH,
                               min_length=constants.SMS_CODE_LENGTH,
                               error_messages={
                                   'max_length': '短信验证码长度有误',
                                   'min_length': '短信验证码长度有误',
                                   'required': '短信验证码不能为空',
                               })

    def clean_username(self):
        '''
        校验用户名
        :return:
        '''
        username = self.cleaned_data.get('username')
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError('用户名已经存在')
        return username

    def clean_phone(self):
        '''
        校验手机号码
        :return:
        '''
        phone = self.cleaned_data.get('phone')
        if User.objects.filter(phone=phone).exists():
            raise forms.ValidationError('手机号码已被注册')
        return phone

    def clean(self):
        '''
        校验密码
        :return:
        '''
        clean_data = super().clean()
        password = self.cleaned_data.get('password')
        password_repeat = self.cleaned_data.get('password_repeat')
        if password != password_repeat:
            raise forms.ValidationError('两次密码不一致')
        # 校验短信验证码
        sms_code = clean_data.get('sms_code')
        redis_conn = get_redis_connection(alias='verify_code')
        p1 = redis_conn.pipeline()
        real_code = redis_conn.get('sms_code_{}'.format(sms_code))
        if (not real_code) or (real_code.decode('utf-8') != sms_code):
            raise forms.ValidationError('短信验证码错误!')
        return clean_data

    def get_errors(self):
        errors = self.errors.get_json_data()
        err_msg_list = []
        for item in errors.values():
            for ite in item:
                err_msg_list.append(ite.get('message'))
        err_msg_str = '/'.join(err_msg_list)
        return err_msg_str

4、前端html

{% extends 'base/base.html' %}
{% load static %}

{% block title %}注册{% endblock title %}

{% block link %}
    
{% endblock link %}

{% block main %}
    

请注册

{% csrf_token %}
{% endblock main %} {#{% block commonjs %}#} {#{% endblock %}#} {% block otherjs %} {# #} {% endblock otherjs %}

5、前端js

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


    $img.click(function () {
        isImagecodeRead = false;
        // $img.attr('src', '/imagecode/?rand=' + Math.random())
        $img.attr('src', '/imagecode/?rand=' + new Date().getTime());
    });

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

    function fnCheckUsername () {
        isUsernameReady = false;
        let sUsername = $username.val();    //获取用户字符串
        if (sUsername === ''){
            message.showError('用户名不能为空!');
            return
        }
        if (!(/^\D\w{5,19}$/).test(sUsername)){
            message.showError('请输入以非数字开头的用户名,6-20个字符');
            return
        }
        $.ajax({
            url: '/check_username/' + sUsername + '/',
            type: 'GET',
            dataType: 'json',
            success: function (data) {
                if(data.data.count !== 0){
                    message.showError(data.data.username + '已经注册,请重新输入!')
                }else {
                    message.showInfo(data.data.username + '可以正常使用!');
                    isUsernameReady = true
                }
            },
            error: function (xhr, msg) {
                message.showError('服务器超时,请重试!')
            }
        });
    }

    // 3.检测密码,检测两次密码是否一致
    let $passwordRepeat = $('input[name="password_repeat"]');
    $passwordRepeat.blur(fnCheckPassword);

    function fnCheckPassword () {
        isPasswordReady = false;
        let password = $('input[name="password"]').val();
        let passwordRepeat = $passwordRepeat.val();
        if (password === '' || passwordRepeat === ''){
            message.showError('密码不能为空');
            return
        }
        if (password !== passwordRepeat){
            message.showError('两次密码输入不一致');
            return
        }
        if (password === passwordRepeat){
            isPasswordReady = true
        }
    }

    // 4.检查手机号码是否可用
    let $phone = $('input[name="phone"]');
    $phone.blur(fnCheckPhone);

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

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

    }

    // 5、点击发送手机验证码
    let $smsButton = $('.sms-captcha');
    $smsButton.click(function () {
        isImagecodeRead = false;
        if ($smsButton.attr('cantclick')==='false'){
        let sCaptcha = $('input[name="captcha_graph"]').val();
        if (sCaptcha === '')
        {
            message.showError('请输入验证码');
            return
        }
        if (sCaptcha.length !== 4)
        {
            message.showError('图形验证码格式错误');
            return
        }
        if (!isPhoneReady)
        {
            fnCheckPhone();
            return
        }
        $
            .ajax({
            url: '/sms_code/',
            type: 'POST',
            data:{
                phone:$phone.val(),
                captcha:sCaptcha
            },
            dataType: 'json'
            })
            .done((data)=>{
                if (data.errno !== '0'){
                    message.showError(data.errmsg);
                }else {
                    isImagecodeRead = true;
                    message.showSuccess(data.errmsg);
                    var num=60;
                    $smsButton.attr('cantclick',true);
                    let t = setInterval(function () {
                        $smsButton.html(num)
                        num--;
                        if(num === 0)
                        {
                            clearInterval(t);
                            $smsButton.attr('cantclick',false);
                            $smsButton.html('获取短信验证码')
                        }
                    },1000)
                }
            })
            .fail(()=>{
                message.showError('服务器超时,请刷新重试!')
            });

    }});

    // 6、提交表单
    let $suumitBnt = $('.register-btn');
    $suumitBnt.click((event)=>{
        // 阻止表单提交
        event.preventDefault();
        if(!isUsernameReady)
        {
            fnCheckUsername();
            return
        }
        if(!isPasswordReady)
        {
            fnCheckPassword();
            return
        }
        if(!isPhoneReady)
        {
            fnCheckPhone();
            return
        }
        if(isImagecodeRead)
        {
        // 短信验证码格式校验
        let sSmscode = $('input[name="sms_captcha"]').val();
        if(sSmscode === '')
        {
            message.showError('短信验证码不能为空!');
            return
        }
        if(!(/^\d{4}$/).test(sSmscode)){
            message.showError('短信验证码格式有误');
            return
        }

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

    }})
});

你可能感兴趣的:(3.2 django项目-新闻博客系统之用户注册功能2)