用戶模塊分析已經(jīng)完成,那么接下來(lái)就是具體的代碼實(shí)現(xiàn)了,這一章主要完成用戶模型設(shè)計(jì)和圖形驗(yàn)證碼、用戶名和手機(jī)號(hào)驗(yàn)證的完成。
一、用戶模型設(shè)計(jì)
用戶模型使用django自帶的auth中的AbstractUser來(lái)進(jìn)行改寫(xiě),
- 添加手機(jī)號(hào)字段
- 添加郵箱狀態(tài)字段
具體實(shí)現(xiàn)過(guò)程:在 users app 里面的 models.py 里面進(jìn)行用戶模型的修改
1. 對(duì)創(chuàng)建超級(jí)用戶時(shí)需要輸入email字段
users/models.py
from django.db import models
from django.contrib.auth.models import UserManager as _UserManager
class UserManager(_UserManager):
"""
重寫(xiě)創(chuàng)建超級(jí)用戶時(shí)需要輸入email字段
"""
def create_superuser(self, username, password, email=None, **extra_fields):
return super().create_superuser(username=username, password=password, email=email, **extra_fields)
2. 添加mobile和email_active字段
users/models.py
from django.contrib.auth.models import AbstractUser
class Users(AbstractUser):
"""
重寫(xiě)users模型
"""
objects = UserManager()
mobile = models.CharField(
max_length=11,
help_text="手機(jī)號(hào)",
verbose_name="手機(jī)號(hào)",
error_messages={
"unique": "手機(jī)號(hào)已被注冊(cè)"
})
email_active = models.BooleanField(default=False, help_text="郵箱狀態(tài)")
class Meta:
db_table = "tb_users" # 數(shù)據(jù)庫(kù)名
verbose_name = "用戶" # 指明在admin站點(diǎn)中的名字
verbose_name_plural = verbose_name # 顯示復(fù)數(shù)名稱
def __str__(self):
return self.username
3. 對(duì)Users模型進(jìn)行settings.py配置
自定義的Users模型需要進(jìn)行配置后才能使用。
settings.py
# 配置自定義的用戶模型
AUTH_USER_MODEL = "users.Users"
二、圖形驗(yàn)證碼
在用戶注冊(cè)時(shí)很多字段驗(yàn)證在后續(xù)的其他項(xiàng)目時(shí)還能使用,為了復(fù)用這里重新新建一個(gè)app專門(mén)用來(lái)做用戶注冊(cè)的用戶名、手機(jī)號(hào)、圖形驗(yàn)證碼、短信驗(yàn)證碼的驗(yàn)證。
創(chuàng)建1個(gè)新的app verifications,拉到apps里面去。
1. 路由urls.py設(shè)置
-
項(xiàng)目下面的 urls.py
from django.urls import path, include
urlpatterns = [
path('', include("verifications.urls")),
]
-
應(yīng)用 verifications/urls.py配置
from django.urls import re_path
from verifications import views
app_name = "verifications"
urlpatterns = [
re_path("image_code/<uuid:image_code_id>/", views.ImageCodes.as_view(), name="image_code"),
]
2. JS前端實(shí)現(xiàn)
注冊(cè)功能的js代碼不單獨(dú)放,都統(tǒng)一放在register.js里面
users/register.js
$(function () {
let $img = $('.form-item .captcha-graph-img img'); //獲取圖片信息
let sImageCodeId=''; //定義一個(gè)空字符串用來(lái)放uuid字符串
generateImageCode();
// 當(dāng)點(diǎn)擊驗(yàn)證碼時(shí)刷新驗(yàn)證碼
$img.click(generateImageCode);
// 生成圖片UUID驗(yàn)證碼
function generateUUID() {
let d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c==='x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
//生成驗(yàn)證碼圖片
function generateImageCode() {
sImageCodeId = generateUUID(); //這里不能有l(wèi)et這相當(dāng)于對(duì)前面的sImageCodeId進(jìn)行修改
// console.log("第一個(gè)uuid:"+sImageCodeId);
let imageCodeUrl = '/image_code/'+sImageCodeId+'/'; //拼接圖片驗(yàn)證碼請(qǐng)求路徑
$img.attr('src',imageCodeUrl); //給圖片添加src鏈接 路徑
}
});
3. views.py邏輯實(shí)現(xiàn)
圖形驗(yàn)證碼需要使用圖形驗(yàn)證碼插件,網(wǎng)上有很多現(xiàn)成的代碼,這里可以在獲取captcha圖形驗(yàn)證碼插件。
在前面settings.py配置redis數(shù)據(jù)庫(kù)時(shí)只設(shè)置了一個(gè)為default默認(rèn)0數(shù)據(jù)庫(kù),將圖形驗(yàn)證碼和短信驗(yàn)證碼的放在為verify_code的1數(shù)據(jù)庫(kù)
settings.py中添加驗(yàn)證碼存放的數(shù)據(jù)庫(kù)
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/0',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient'
}
},
# 新添加存放短信驗(yàn)證碼和圖形驗(yàn)證碼
'verify_code': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient'
}
},
}
verifications/views.py
import logging
from django.views import View
from django.http import HttpResponse
from django_redis import get_redis_connection
from verifications import contains
from utils.captcha.captcha import captcha
logger = logging.getLogger("django")
class ImageCodes(View):
"""
create image_code view
"""
def get(self, request, image_code_id): # 從前端獲取到傳過(guò)來(lái)的參數(shù)uuid
verify_text, verify_image = captcha.generate_captcha() # 生成驗(yàn)證碼文本和圖片
# 鏈接redis步驟 將生成的驗(yàn)證碼文本和圖片保存到redis中
# 1.鏈接redis區(qū)
redis_obj = get_redis_connection(alias="verify_code")
# 2. 構(gòu)造鍵值對(duì)
verify_key = "image_{}".format(image_code_id)
# 3. 保存數(shù)據(jù)到redis并設(shè)置有效期
redis_obj.setex(verify_key, contains.IMAGE_CODE_EXPIRE_TIME, verify_text)
logger.info('verify_text:{}'.format(verify_text)) # 后臺(tái)顯示驗(yàn)證碼信息
return HttpResponse(content=verify_image, content_type='image/jpg') # 把驗(yàn)證碼圖片返回到前端
三、用戶名
1. 路由urls.py設(shè)置
verification/urls.py
from django.urls import re_path
from verifications import views
app_name = "verifications"
urlpatterns = [
re_path("username/(?P<username>\w{6,18})/", views.CheckUsernameView.as_view(), name="username"),
]
2. JS前端實(shí)現(xiàn)
users/register.js
$(function () {
let $username = $('#user_name'); //獲取用戶名標(biāo)簽
//鼠標(biāo)移出用戶名輸入框時(shí)執(zhí)行判斷
$username.blur(function () {
fn_check_username();
});
// 用戶名是否存在
function fn_check_username() {
let sUsername = $username.val(); //獲取表單數(shù)據(jù)
let sReturnValue = '';
// 前端校驗(yàn)
if (sUsername === ""){
// alert("用戶名不能為空") ;
// alert('用戶名能為空!'); //這里需要改進(jìn)
return
}else if(!(/\w{6,18}/).test(sUsername)){
// alert('請(qǐng)輸入6~18位字符的用戶名!'); //這里需要改進(jìn)
return
}
//ajax請(qǐng)求判斷數(shù)據(jù)庫(kù)是否存在 后端校驗(yàn)
$.ajax({
url:'/username/' + sUsername +'/',
type:'GET',
dataType:'json',
async: false, // 關(guān)閉異步
success:function (arg) {
if(arg.data.count !== 0){ // 不等于0表示已經(jīng)注冊(cè)了
// alert("用戶名已注冊(cè),請(qǐng)重新輸入")
sReturnValue = "";
}else{
sReturnValue="success";
alert("用戶名可以正常使用")
}
},
error:function(){
alert("用戶名ajax,服務(wù)器超時(shí),請(qǐng)重新輸入");
sReturnValue = "";
}
});
return sReturnValue //返回出該信息
}
});
3. view.py邏輯實(shí)現(xiàn)
verifications/views.py
from django.views import View
from django.http import JsonResponse
from users import models
class CheckUsernameView(View):
"""
create username verify view
# 1. 創(chuàng)建一個(gè)類
request: GET
params: username
"""
# 2. 創(chuàng)建個(gè)get方法來(lái)處理邏輯
def get(self, request, username):
# 3. 從數(shù)據(jù)庫(kù)中查看是否存在該用戶
data = {
"username": username,
"count": models.Users.objects.filter(username=username).count() # 獲取數(shù)據(jù)庫(kù)中有幾條這個(gè)信息:無(wú)則0
}
# 4. 返回到前端
return JsonResponse(data=data)
四、手機(jī)號(hào)
1. 路由urls.py設(shè)置
verification/urls.py
from django.urls import re_path
from verifications import views
app_name = "verifications"
urlpatterns = [
re_path("mobile/(?P<mobile>1[3-9]\d{11})/", views.CheckMobileView.as_view(), name="mobile"),
]
2. JS前端實(shí)現(xiàn)
users/register.js
$(function () {
let $mobile = $("#mobile"); //獲取手機(jī)號(hào)
//鼠標(biāo)移出手機(jī)號(hào)輸入框時(shí)執(zhí)行判斷
$mobile.blur(function() {
fn_check_mobile();
});
// 手機(jī)號(hào)是否存在
function fn_check_mobile(){
let sMobile = $mobile.val();
let sReturnMobileValue = "";
// 前端校驗(yàn)
if (sMobile === ""){
// alert("手機(jī)號(hào)輸入為空");
alert("手機(jī)號(hào)為空");
}else if(!(/^1[3-9]\d{9}/).test(sMobile)){
alert("請(qǐng)輸入正確的手機(jī)號(hào)格式") ;
}
//ajax請(qǐng)求判斷數(shù)據(jù)庫(kù)是否存在 后端校驗(yàn)
$.ajax({
url:"/mobile/"+sMobile+'/',
type:"GET",
dataType: "json",
async:false, // 關(guān)閉異步
success:function (arg) {
if(arg.data.count !== 0){
alert(sMobile+"手機(jī)號(hào)已被注冊(cè),請(qǐng)重新輸入");
sReturnMobileValue = "";
}else{
alert(sMobile+"手機(jī)號(hào)能正常使用");
sReturnMobileValue = "success";
}
},
error:function () {
alert("手機(jī)號(hào)ajax,服務(wù)器超時(shí),請(qǐng)重新輸入");
sReturnMobileValue = "";
}
});
return sReturnMobileValue //返回出該信息
}
});
3. view.py邏輯實(shí)現(xiàn)
verifications/views.py
from django.views import View
from django.http import HttpResponse, JsonResponse
from users import models
class CheckMobileView(View):
"""
create mobile verify view
# 1.創(chuàng)建一個(gè)類
request: GET
params: mobile
"""
# 2. 創(chuàng)建個(gè)get方法來(lái)處理邏輯
def get(self, request, mobile):
# 3. 從數(shù)據(jù)庫(kù)中查看是否存在該用戶
data = {
"mobile": mobile,
"count": models.Users.objects.filter(mobile=mobile).count()
}
# 5. 返回到前端
return JsonResponse(data=data)
五、短信驗(yàn)證碼
1. 路由urls.py設(shè)置
verification/urls.py
from django.urls import re_path
from verifications import views
app_name = "verifications"
urlpatterns = [
re_path("sms_code/", views.SmsCodeView.as_view(), name="sms_code"),
]
2. JS前端實(shí)現(xiàn)
users/register.js
$(function () {
let sImageCodeId=''; //定義一個(gè)空字符串用來(lái)放uuid字符串
let $mobile = $("#mobile"); //獲取手機(jī)號(hào)
let $smsCodeBtn = $(".form-item .sms-captcha"); //發(fā)送短信驗(yàn)證按鈕
let $smsCodeText = $(".form-item .form-captcha"); //圖形驗(yàn)證碼輸入框文本
//短信驗(yàn)證
$smsCodeBtn.click(function(){
// 判定手機(jī)號(hào)是否有輸入
if ( fn_check_mobile() !== "success"){
alert("手機(jī)號(hào)為空");
return
}
// 判斷用戶是否輸入圖形驗(yàn)證碼
let text = $smsCodeText.val();
if (!text){
alert("請(qǐng)輸入圖形驗(yàn)證碼");
return
}
// 判斷UUID
if(!sImageCodeId){
console.log("第2個(gè)"+sImageCodeId);
alert("圖形UUID不對(duì)");
return
}
let dataParams={
'mobile':$mobile.val(),
'image_text':text,
"image_code_id":sImageCodeId,
};
$.ajax({
url:"/sms_code/",
type:"POST",
data:JSON.stringify(dataParams),
success:function(arg){
console.log(arg);
if(arg.errno==="200"){ // errno: "200", errmsg: "短信發(fā)送正常", data: null
alert("短信驗(yàn)證碼發(fā)送成功");
//倒計(jì)時(shí)功能
let num = 60;
// 設(shè)置計(jì)時(shí)器
let t = setInterval(function () {
if (num===1){
clearInterval(t);
$smsCodeBtn.html("重新發(fā)送驗(yàn)證碼")
}else{
num -= 1;
//展示倒計(jì)時(shí)信息
$smsCodeBtn.html(num+"秒");
}
},1000)
}else{
alert(arg.errmsg);
}
},
error:function(){
alert("短信驗(yàn)證ajax,服務(wù)器超時(shí),請(qǐng)重試")
}
})
});
});
3. view.py邏輯實(shí)現(xiàn)
在寫(xiě)代碼時(shí)需要很多錯(cuò)誤代碼和錯(cuò)誤代碼的意思,這里單獨(dú)建一個(gè)文件用來(lái)放這個(gè)錯(cuò)誤代碼,同時(shí)后續(xù)在寫(xiě)代碼時(shí)需要很多時(shí)候傳遞給前端一個(gè)json格式的文件,包含錯(cuò)誤代碼,這樣前端才能做好判斷。
在utlis下面創(chuàng)建一個(gè)res_code文件夾,創(chuàng)建一個(gè) res_code.py文件用來(lái)放錯(cuò)誤代碼, json_function.py用來(lái)放置轉(zhuǎn)化json格式的插件:
utils/res_code/res_code.py
class Code:
OK = "200"
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: "數(shù)據(jù)庫(kù)查詢錯(cuò)誤",
Code.NODATA: "無(wú)數(shù)據(jù)",
Code.DATAEXIST: "數(shù)據(jù)已存在",
Code.DATAERR: "數(shù)據(jù)錯(cuò)誤",
Code.METHERR: "方法錯(cuò)誤",
Code.SMSERROR: "發(fā)送短信驗(yàn)證碼異常",
Code.SMSFAIL: "發(fā)送短信驗(yàn)證碼失敗",
Code.SESSIONERR: "用戶未登錄",
Code.LOGINERR: "用戶登錄失敗",
Code.PARAMERR: "參數(shù)錯(cuò)誤",
Code.USERERR: "用戶不存在或未激活",
Code.ROLEERR: "用戶身份錯(cuò)誤",
Code.PWDERR: "密碼錯(cuò)誤",
Code.SERVERERR: "內(nèi)部錯(cuò)誤",
Code.UNKOWNERR: "未知錯(cuò)誤",
}
utils/res_code/json_funciton.py
from django.http import JsonResponse
from utils.res_code.res_code import Code
# 處理json格式轉(zhuǎn)化,并加入異常碼和異常信息
def to_json_data(errno=Code.OK, errmsg='', data=None, **kwargs):
"""
返回給前端 json數(shù)據(jù)以及錯(cuò)誤信息
:param errno: 錯(cuò)誤代碼
:param errmsg: 錯(cuò)誤信息
:param data: 參數(shù)
:param kwargs: 不定長(zhǎng)數(shù)據(jù)
:return:
"""
json_dict = {'errno': errno, 'errmsg': errmsg, 'data': data}
if kwargs:
json_dict.update(kwargs)
return JsonResponse(json_dict)
verifications/contains.py
# -*- coding: utf-8 -*-
"""
@Time : 2020/2/26 21:29
@Author : 半紙梁
@File : contains.py
"""
IMAGE_CODE_EXPIRE_TIME = 5*60 # 圖形驗(yàn)證碼有效時(shí)間
SMS_CODE_EXPIRE_TIME = 5*60 # 短信驗(yàn)證碼有效時(shí)間
SMS_REPEAT_EXPIRE_TIME = 60 # 可重復(fù)發(fā)送短信的時(shí)間
SMS_TEMPLATE = 1 # 短信發(fā)送模板
SMS_CCP_EXPIRE_TIME = 5 # 云通訊短信有效時(shí)間 以分鐘計(jì)算的
verifications/views.py
import logging
import json
import random
from django.views import View
from django_redis import get_redis_connection
from verifications import contains
from verifications.forms import SmsCodeForm
from utils.yuntongxun.sms import CCP
from utils.res_code.json_function import to_json_data
from utils.res_code.res_code import Code, error_map
logger = logging.getLogger("django")
class SmsCodeView(View):
"""
# 1. 創(chuàng)建一個(gè)SmsCodeView類
param: mobile、image_text、image_code_id
"""
# 2. 創(chuàng)建一個(gè)post方法用來(lái)處理邏輯
def post(self, request):
# 3. 獲取前端傳來(lái)的數(shù)據(jù)
json_data = request.body
# 4. 將數(shù)據(jù)轉(zhuǎn)化為字典
dict_data = json.loads(json_data)
# 5. 將數(shù)據(jù)傳遞給SmsCodeForm表單進(jìn)行校驗(yàn)
form = SmsCodeForm(data=dict_data)
# 6. 校驗(yàn)成功處理方式
if form.is_valid():
# 7. 獲取校驗(yàn)后的數(shù)據(jù)
mobile = form.cleaned_data.get("mobile")
# 8. 生成短信驗(yàn)證碼
sms_text = "%06d" % random.randint(0, 999999)
# 9. 將短信驗(yàn)證碼和和過(guò)期時(shí)間保存到redis中
redis_obj = get_redis_connection("verify_code")
sms_text_key = "sms_code_{}".format(mobile).encode("utf8")
sms_repeat_key = "sms_sixty_{}".format(mobile).encode("utf8")
redis_obj.setex(sms_text_key, contains.SMS_CODE_EXPIRE_TIME, sms_text) # key, expire_time, value
redis_obj.setex(sms_repeat_key, contains.SMS_CODE_EXPIRE_TIME, contains.SMS_REPEAT_EXPIRE_TIME)
# logging.info("發(fā)送短信正常[mobile:%s sms_num:%s]" % (mobile, sms_num)) # 調(diào)試代碼時(shí)候用,在控制臺(tái)顯示
# 9. 使用用通訊插件發(fā)送短信
try:
result = CCP().send_Template_sms(mobile, [sms_text, contains.SMS_CCP_EXPIRE_TIME], contains.SMS_TEMPLATE)
except Exception as e:
logger.error("短信發(fā)送異常[mobile:{},error:{}]".format(mobile, e))
return to_json_data(errno=Code.SMSERROR, errmsg=error_map[Code.SMSERROR]) # 短信發(fā)送異常
else:
if result == 0: # 發(fā)送成功
logger.info("短信發(fā)送成功[mobile:{},sms_code:{}]".format(mobile, sms_text))
return to_json_data(errmsg="短信發(fā)送正常")
else: # 發(fā)送失敗
logger.warning("短信發(fā)送失敗[mobile:{}]".format(mobile))
return to_json_data(errno=Code.SMSFAIL, errmsg=error_map[Code.SMSFAIL])
# 校驗(yàn)未通過(guò)
else:
err_msg_list = []
for item in form.errors.values():
err_msg_list.append(item[0])
err_info = '/'.join(err_msg_list)
return to_json_data(errno=Code.PARAMERR, errmsg=err_info)
verifications/forms.py
# -*- coding: utf-8 -*-
"""
@Time : 2020/2/27 9:49
@Author : 半紙梁
@File : forms.py
"""
from django import forms
from django_redis import get_redis_connection
from django.core.validators import RegexValidator
mobile_reg = RegexValidator(r"^1[3-9]\d{9}","手機(jī)號(hào)格式不正確")
class SmsCodeForm(forms.Form):
"""
check sms_code field
字段一定要和js中傳遞過(guò)來(lái)的保持一樣
"""
mobile = forms.CharField(
max_length=11,
min_length=11,
validators=[mobile_reg],
error_messages={
"max_length": "手機(jī)號(hào)格式不正確",
"min_length": "手機(jī)號(hào)格式不正確",
"required": "手機(jī)號(hào)不能為空"
}
)
image_code_id = forms.UUIDField(error_messages={"required": "UUID不能為空"}) #
image_text = forms.CharField(
max_length=4,
min_length=4,
error_messages={
"max_length": "圖形驗(yàn)證碼格式不正確",
"min_length": "圖像驗(yàn)證碼格式不正確",
"required": "圖形驗(yàn)證碼不能為空"
}
)
def clean(self):
# 1. 獲取清洗后的數(shù)據(jù)
cleaned_data = super().clean()
mobile = cleaned_data.get("mobile")
image_code_uuid = cleaned_data.get("image_code_id")
image_text = cleaned_data.get("image_text")
# 2. 建立redis鏈接
try:
redis_obj = get_redis_connection("verify_code")
except Exception as e:
raise forms.ValidationError("redis數(shù)據(jù)庫(kù)連接錯(cuò)誤")
# 3. 構(gòu)建image_code查詢的鍵
image_code_key = "image_{}".format(image_code_uuid).encode("utf8")
# 4. 獲取數(shù)據(jù)庫(kù)中的image_code的值
redis_byte_image_text = redis_obj.get(image_code_key)
# 5. 刪除redis中該uuid的鍵
redis_obj.delete(image_code_key) #
# 6. 將拿到的值化為utf8字符:redis中拿到的值都是二進(jìn)制
redis_image_text = redis_byte_image_text.decode("utf8") if redis_byte_image_text else None
# 7. 判斷現(xiàn)在的image_text和redis_image_text是否一致
if image_text.upper() != redis_image_text:
raise forms.ValidationError("圖形驗(yàn)證碼校驗(yàn)失敗")
# 8. 判斷60秒內(nèi)是否有重復(fù)發(fā)送短信
redis_sms_repeat_key = "mobile_{}".format(mobile).encode("utf8") # 構(gòu)建短信60秒內(nèi)發(fā)送查詢的鍵
if redis_obj.get(redis_sms_repeat_key):
raise forms.ValidationError("短信發(fā)送太頻繁,請(qǐng)稍后再試")
用戶注冊(cè)功能點(diǎn)擊注冊(cè)放在users app里面進(jìn)行完成。
|