luffy项目
luffy项目
luffy介绍
暂不介绍
项目搭建
1.虚拟环境
虚拟环境的创建和使用可以参考 项目搭建之虚拟环境,其内部所写的是python3.3版本后内置的venv功能搭建虚拟环境,而如果需要搭建python2的虚拟环境则不得不用virtualenv第三方模块。
virtualenv安装和初始化
安装virtualenv:
pip3 install virtualenv # 虚拟环境是它
pip3 install virtualenvwrapper-win # 对虚拟环境的加强,以后只需要简单的命令就可以使用和创建虚拟环境
配置虚拟环境管理器的工作目录:
-
在环境变量中新建:WORKON_HOME: D:\Virtualenvs
-
在D盘创建该文件夹,通过命令创建的虚拟环境,都会在这个文件夹下
-
在python解释器安装目录的scripts目录中找到virtualenvwrapper.bat执行一下
virtualenv创建和使用虚拟环境
创建并自动进入虚拟环境:
ps:mkvirtualenv.bat已经在python的scripts中,因为配置过环境变量,所以随意位置可执行
mkvirtualenv -p python luffy # 创建了一个以python解释器为准的纯净环境,环境名称luffy
退出虚拟环境
ps:deactive.bat在虚拟环境的scripts文件夹中,在虚拟环境中可以执行
deactive
查看和进入虚拟环境
ps:workon.bat在python的scripts中在win-cmd中任意位置可执行
workon # 查看所有虚拟环境名
workon luffy # 进入虚拟环境luffy
删除虚拟环境
rmvirtualenv
2.后端创建
在虚拟环境创建好后,可以引用虚拟环境创建django项目,注意因为虚拟环境初始是纯净的,没有按照各模块,所以需要记得提前在虚拟环境中安装django,并选择当前流行版本如3.2.2。
基于django项目调整目录
├── luffy_api # 项目根目录
├── logs/ # 项目运行时/开发时日志目录 - 包
├── manage.py # 脚本文件
├── luffy_api/ # 项目主应用,开发时的代码保存 - 包
├── apps/ # 开发者的代码保存目录,以模块[子应用]为目录保存 - 包
├── libs/ # 第三方类库的保存目录[第三方组件、模块] - 包
├── settings/ # 配置目录 - 包
├── dev.py # 项目开发时的本地配置
└── prod.py # 项目上线时的运行配置
├── urls.py # 总路由
└── utils/ # 多个模块[子应用]的公共函数类库[自己开发的组件]
└── scripts/ # 保存项目运行时的脚本文件,小的测试脚本 - 文件夹,不提交到git上
相较于django本身的项目目录,主要的变化有:
- 项目同名文件夹被作为项目主应用,所有可改动的代码都往里塞
- 配置文件修改成settings文件夹,存放不同应用环境的配置文件(测试环境和上线环境等)
- utils.py修改成utils包进一步细分写公共函数类库
- 注册软件从与主文件夹同级调整至主文件夹下的apps中
- 软件通过manage工具创建,注册时可能遇到注册名的问题,所以为了方便,需要在settings中配置下环境变量:将BASE_DIR和BASE_DIR'apps'的字符串格式加入sys.path
django配置文件
settings下的dev文件一般表示开发环境下的配置,以下是对django的一些配置的解读:
点击查看代码
from pathlib import Path
import os
import sys
# 项目根路径
BASE_DIR = Path(__file__).resolve().parent.parent
# 把 apps 路径加入到环境变量
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
# 把BASE_DIR也加入到环境变量,以后直接从小路飞开始导起即可
sys.path.insert(0, str(BASE_DIR))
# 从大路飞开始导起(执行文件所在文件夹)
# 或者小路飞开始导起或者apps开始导起都可以(添加到环境变量)
# 秘钥,涉及到加密的django中
SECRET_KEY = 'django-insecure-!g(8l%fw_#t$pz$x4jdf#e3$b4+c%xzqyq@3zki08vj&i)z4k-'
# 项目是以debug模式运行,还是非debug模式运行
# 项目上线,要改成false
# 调试模式下,对开发者更友好:可以列出所有路径.报了错,前端能看到,但不适合给用户看到
DEBUG = False
# 允许我的项目部署在哪个ip地址上,* 表示允许部署在所有地址上
ALLOWED_HOSTS = ['*']
# 当DEBUG=False时必须指定ALLOWED_HOSTS
# django 是多个app组成的,里面配置app,默认带的app,django内置的app
# django 是一个大而全的框架,有很多内置app:
"""
admin后台管理,
auth权限管理,
contenttypes表中存app也表的关系,
sessions session表,django的session相关
messages:消息框架,flask讲闪现,是一样的东西
staticfiles:静态资源的
"""
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
# 因为添加了apps环境变量,所以可以这样注册
'home',
'user'
]
# 中间件
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全相关中间件
'django.contrib.sessions.middleware.SessionMiddleware', # session相关中间件
'django.middleware.common.CommonMiddleware', # 带不带 / 问题
'django.middleware.csrf.CsrfViewMiddleware', # csrf 认证,生成csrf串
'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户认证
'django.contrib.messages.middleware.MessageMiddleware', #消息框架相关
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# 根路由
ROOT_URLCONF = 'luffy_api.urls'
# 模板文件(前后端分离的项目用不上)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages',
],
},
},
]
# 项目运行的配置---》项目上线运行,使用uwsgi 运行 application()
WSGI_APPLICATION = 'luffy_api.wsgi.application'
# 存在环境变量中,代码中不能直接看到
name = os.environ.get('LUFFY_NAME', 'luffy')
password = os.environ.get('LUFFY_PASSWORD', 'Luffy123?')
# 拓展:有的公司,直接有个配置中心---》服务--》只用来存放配置文件
# 数据库配置,mysql 主从搭建完,读写分离
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'luffy',
'USER': name,
'PASSWORD': password,
'HOST': '127.0.0.1',
'PORT': 3306
},
}
# 忽略掉
AUTH_PASSWORD_VALIDATORS = [...]
# 国际化
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# 静态资源
STATIC_URL = '/static/'
# 建表自动都会创建一个自增字段
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
开启media访问
django项目中有个相对固定的配置方式
配置文件:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
路由:
from django.views.static import serve
# settings.MEDIA_ROOT虽然是在settings/dev.py中配置的
# 但是所有的配置都会被最终合并到conf的settings中
from django.conf import settings
path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT})
后台日志封装
这里采取logging模块的日志记录方式,在工作环境中,也许会有专门管理日志的服务去统一管理日志
如:sentry,是由django写的服务,目前开源。
日志组成可以参考 内置模块之logging模块--日志模块。
日志settings配置:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
},
},
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': {
# 实际开发建议使用WARNING
'level': 'INFO',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'file': {
# 实际开发建议使用ERROR
'level': 'WARNING',
'class': 'logging.handlers.RotatingFileHandler',
# 日志位置,日志文件名,日志保存目录必须手动创建,
# 注:这里的文件路径要注意BASE_DIR代表的是小luffyapi
# 小luffyapi的dirname是大luffyapi
'filename': os.path.join(os.path.dirname(BASE_DIR), "logs", "luffy.log"),
# 日志文件的最大值,这里我们设置300M
'maxBytes': 300 * 1024 * 1024,
# 日志文件的数量,设置最大日志数量为10
'backupCount': 10,
# 日志格式:详细格式
'formatter': 'verbose',
# 文件内容编码
'encoding': 'utf-8'
},
},
# 日志对象
'loggers': {
'django': {
'handlers': ['console', 'file'],
'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统
},
}
}
产生日志记录者:
# 在utils新建 common_logger.py ,得到日志对象
import logging
logger = logging.getLogger('django')
使用日志:
from utils import logger # utils包需要在__init__中导入logger以便这样调用
logger.info('info级别的日志')
logger.error('error级别的日志')
全局异常处理封装
在drf中,在APIView的视图类中,会进行三大认证+视图的全局异常捕获,我们可以对全局异常处理的函数进行改写和配置。
原处理异常函数可以处理drf异常,而我们在此基础上进行扩展,使他能处理更多的异常,并且在处理异常时,可以记录日志。
# 按照以下位置写函数和配置
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'utils.common_exceptions.exception_handler',
}
处理函数异常的参考:
from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.response import Response
from utils import logger
def exception_handler(exc, context):
request = context.get('request')
view = context.get('view')
response = drf_exception_handler(exc, context)
if response is None:
# 说明drf没能处理异常
logger.error(f'用户【{request.user}】|ip【{request.META.get("REMOTE_ADDR")}】'
f'|视图【{view}】|错误【{exc}】')
return Response({'code': 888, 'msg': '服务器错误'})
logger.info(f'用户【{request.user}】|ip【{request.META.get("REMOTE_ADDR")}】'
f'|视图【{view}】|错误【{exc}】')
return response
响应格式封装
Response没有地方可以替换,但是我们可以继承它的Response类再写一个响应类,来帮助更方便的产生固定格式的响应类。
我们希望达到如下的效果:
-
默认有{code:100,msg:成功}这两个字段打头
-
如果有其他状态码则按code,msg传即可
-
原本data={返回对象}会被组织成{code:100,msg:成功,data:{..}}
-
如APIResponse(name='leethon')传参,则前端会收到
{code:100,msg:成功,name:leethon}的响应体。
from rest_framework.response import Response
class APIResponse(Response):
# 原Response有什么,我们就初始化什么
def __init__(self, code=100, msg='成功', status=None,
template_name=None, headers=None,
exception=False, content_type=None, **kwargs):
data = {'code': code, 'msg': msg}
# 如果有多余的参数,一律塞到data字典中
if kwargs:
data.update(kwargs)
# 派生老Response的初始化
super().__init__(data=data, status=status,
template_name=template_name, headers=headers,
exception=exception, content_type=content_type)
3.后端数据库建立
创建luffy数据库
对于一般程序员来说,开发人员有专门的用户,用户对于数据库的权限是有限的,那么我们可以在创建库后,再创建一个用户来模拟这种情况:
root登录mysql:
# 创建数据库
create database luffy;
# 查看mysql用户信息
select user,host from mysql.user;
# 将库的权限给一个新的mysql用户
grant 权限(create, update) on 库.表 to '账号'@'host' identified by '密码'
# 将库的所有权限给一个所有地址登录的luffy用户密码为luffy123?
grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123?';
# 刷新权限到硬盘
flush privileges;
以上:就创建了一个用户只对luffy库有增删改查所有权限
用户:luffy
密码:Luffy123?
项目连接库
这里后端是用django写的,原本Python2中可以使用mysqlDB连接mysql,在使用python3后,mysqlDB不能使用了,但django仍然默认连接mysqlDB,需要一个猴子补丁将其替换成可以使用的程序。则需要下载mysqlclient模块。
pip install mysqlclient
修改settings配置:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'luffy',
'USER': 'luffy',
'PASSWORD': 'Luffy123?',
'HOST': '127.0.0.1',
'PORT': 3306
}
}
这里的user和password配置仅为测试环境dev会用到的配置,而实际上线的库可能都是不一样的,所以暂不必担心代码泄露密码问题。
不过我们也可以在系统环境变量中存放用户名和密码等信息,用os模块获取来模拟这种情况:
name = os.environ.get('LUFFY_NAME', 'luffy')
password = os.environ.get('LUFFY_PASSWORD', 'Luffy123?')
创建用户表
class User(AbstractUser):
# 扩写手机号和头像字段
mobile = models.CharField(max_length=11, unique=True)
# 需要pillow包的支持
icon = models.ImageField(upload_to='icon', default='icon/default.png')
class Meta:
# 数据库中的表名
db_table = 'luffy_user'
verbose_name = '用户表'
verbose_name_plural = verbose_name
def __str__(self):
return self.username
4.前端搭建
全局css和全局配置文件
asserts文件夹就是用来存放一些项目所用的img、js、css的
-
全局css
/*global.css*/ /* 声明全局样式和项目的初始化样式 */ body, h1, h2, h3, h4, h5, h6, p, table, tr, td, ul, li, a, form, input, select, option, textarea { margin: 0; padding: 0; font-size: 15px; } a { text-decoration: none; color: #333; } ul { list-style: none; } table { border-collapse: collapse; /* 合并边框 */ }
用于取消页面标签各种默认的样式,如ul的小点,a标签的下划线。
-
全局js
书写一些配置,并导出
// settings.js export default { BASE_URL: 'http://127.0.0.1:8001/api/v1' }
在main.js中导入调用设置为Vue的全局变量:
import settings from "@/assets/js/settings"; Vue.prototype.$settings = settings
安装一些前端模块依赖
一些前端模块十分的常用,很多组件都会用到,可以将其添加到全局变量中。
-
axios模块
cnpm i axios -S
import axios from "axios"; Vue.prototype.$axios = axios
-
vue-cookies模块
cnpm i vue-cookies -S
import cookies from "vue-cookies" Vue.prototype.$cookies = cookies
-
elementui
cnpm install element-ui -S
import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI)
项目启动
1.主页设计
由原型图分析接口
在上图中我们首先可以看到轮播图,这个轮播图是需要一个接口提供的,所以后端需要开一个轮播图接口。
由接口分析表模型
轮播图接口,主要就是查看所有list。
而数据方面需要提供轮播图数据,轮播图的特征有:
-
图片资源
-
排序优先级(这个很多地方可能用到,通过不同方式排序)
所以我们可以建一个父表模型在utils中以便继承调用。
- 创建时间
- 更新时间
- 是否删除
- 是否上架
- 优先级
-
跳转链接
-
图的标题和详情
建立表模型:
# utils.common_models.py
from django.db import models
class BaseBanner(models.Model):
created_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
updated_time = models.DateTimeField(verbose_name='更新时间', auto_now=True)
is_delete = models.BooleanField(verbose_name='是否删除')
is_show = models.BooleanField(verbose_name='是否上架')
order_rank = models.IntegerField(verbose_name='优先级')
class Meta:
# 不建表,专门用于给其他类继承
abstract = True
# home.models.py
from django.db import models
from utils import BaseBanner
class Banner(BaseBanner):
title = models.CharField(verbose_name='标题', max_length=16)
detail = models.TextField(verbose_name='详情')
link = models.CharField(verbose_name='链接', max_length=64)
image = models.ImageField(verbose_name='图片', upload_to='banner')
接口单独列项目的场景:
有时一个项目有限时活动,需要开设一些后端接口来配合这个应用,但是不一定要在原本的项目中开设接口,可以重新开一个小项目(足够轻量化可以使用flask),当活动开启时启动小项目,活动结束时关闭小项目即可。
这里仅是分享一个小的思路,即后端的接口可以部署在多个项目上。
接口视图编写思路
针对轮播图接口,目前已经建立好了表,并使用django-admin后台管理添加了一些数据,我们只需要按照一定顺序排序,将轮播图的数据序列化返回出去即可。
# 视图函数
class BannerView(GenericViewSet, ListModelMixin):
# 数据是通过筛选的未删除并上架按照优先级排序的queryset
queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('order_rank')
serializer_class = BannerSerializers
# 查看所有接口,将响应转为统一格式的响应
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
return APIResponse(results=response.data)
# 序列化类
class BannerSerializers(serializers.ModelSerializer):
class Meta:
model = Banner
fields = ['id', 'title', 'detail', 'link', 'image']
一般情况下的视图可拓展思路:
-
get_serializer(self)
-
get_queryset(self)
-
perform_update(self, serializer)
-
perform_destroy(self, instance)
-
perform_create(self, serializer)
-
utils封装常见工具,如:
可以封装基于APIResponse的五个视图扩展类
# 这里封装一个演示 class MyListMixin(mixins.ListModelMixin): def list(self, request, *args, **kwargs): response = super(MyListMixin, self).list(self, request, *args, **kwargs) return APIResponse(results=response.data)
2.前端实现轮播图
前端首页界面除了轮播图还有一些其他界面,只不过由于轮播图接口写好了,所以可以结合后端写。
首页还有页头页脚等其他组件,我们都可以认为这些事页面内组件,放入components文件夹,而首页组件属于页面组件,归为view文件夹更为合适。
页头组件
<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
</div>
<div class="nav">
<ul class="left-part">
<li class="logo">
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
</li>
<li class="ele">
<span @click="goPage('/free-course')" :class="{active: route === '/free-course'}">免费课</span>
</li>
<li class="ele">
<span @click="goPage('/actual-course')" :class="{active: route === '/actual-course'}">实战课</span>
</li>
<li class="ele">
<span @click="goPage('/light-course')" :class="{active: route === '/light-course'}">轻课</span>
</li>
</ul>
<div class="right-part">
<div>
<span>登录</span>
<span class="line">|</span>
<span>注册</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Header",
data() {
return {
route: '',
}
},
methods: {
goPage(route) {
this.route = route
this.$router.push(route)
}
}
}
</script>
<style scoped>
.header {
background-color: white;
box-shadow: 0 0 5px 0 #aaa;
}
.header:after {
content: "";
display: block;
clear: both;
}
.slogan {
background-color: #eee;
height: 40px;
}
.slogan p {
width: 1200px;
margin: 0 auto;
color: #aaa;
font-size: 13px;
line-height: 40px;
}
.nav {
background-color: white;
user-select: none;
width: 1200px;
margin: 0 auto;
}
.nav ul {
padding: 15px 0;
float: left;
}
.nav ul:after {
clear: both;
content: '';
display: block;
}
.nav ul li {
float: left;
}
.logo {
margin-right: 20px;
}
.ele {
margin: 0 20px;
}
.ele span {
display: block;
font: 15px/36px '微软雅黑';
border-bottom: 2px solid transparent;
cursor: pointer;
}
.ele span:hover {
border-bottom-color: orange;
}
.ele span.active {
color: orange;
border-bottom-color: orange;
}
.right-part {
float: right;
}
.right-part .line {
margin: 0 10px;
}
.right-part span {
line-height: 68px;
cursor: pointer;
}
</style>
页尾组件
<template>
<div class="footer">
<ul>
<li>关于我们</li>
<li>联系我们</li>
<li>商务合作</li>
<li>帮助中心</li>
<li>意见反馈</li>
<li>新手指南</li>
</ul>
<p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p>
</div>
</template>
<script>
export default {
name: "Foot"
}
</script>
<style scoped>
.footer {
width: 100%;
height: 128px;
background: #25292e;
color: #fff;
}
.footer ul {
margin: 0 auto 16px;
padding-top: 38px;
width: 810px;
}
.footer ul li {
float: left;
width: 112px;
margin: 0 10px;
text-align: center;
font-size: 14px;
}
.footer ul::after {
content: "";
display: block;
clear: both;
}
.footer p {
text-align: center;
font-size: 12px;
}
</style>
组件统合至页面组件
<template>
<div class="home">
<Header></Header>
<Banner></Banner>
<Foot></Foot>
</div>
</template>
<script>
import Header from "@/components/Header";
import Banner from "@/components/Banner";
import Foot from "@/components/Foot";
export default {
name: 'HomeView',
components: {Header, Banner, Foot}
}
</script>
<style scoped>
</style>
- 导入组件
- 注册组件(components中)
- 模板中引用组件(标签形式)
3.登录注册接口
效果图及需求分析
有两种登录方式
- 密码+账号的登录,账号可以是用户名、手机号、邮箱-->多方式登录接口
- 短信验证码的登录,账号必须输入手机号-->短信登录接口
- 发送验证码-->发送验证码的接口(需要借助第三方发短信)
注册只要求手机号:
- 注册账号接口
- 发送验证码接口
- 手机号可能不存在-->校验手机号是否存在的接口(可能要借助第三方,也可以简单的正则匹配)
思考:登录接口是否需要先认证手机号是否存在,大概过一下逻辑,如果手机号不存在,那么注册时就不应该存入用户表,所以不需要。
多种方式登录接口
多种方式登录接口分析:
- 需要提交数据,post请求
- 数据校验,校验成功返回user和token给前端,校验失败返回错误信息
- 校验逻辑可以写在序列化类中,让is_valid去完成
# GenericViewSet
@action(methods=['POST'], detail=False)
def login_mul(self, request):
ser = LoginMulSerializer(data=request.data)
ser.is_valid(raise_exception=True) # 校验错误直接报错交由异常捕获处理
return APIResponse(
token=ser.context.get('token'),
refresh=ser.context.get('refresh')
)
# Serializer
class LoginMulSerializer(BaseLoginSerializer):
input_user = serializers.CharField()
password = serializers.CharField()
def validate(self, attrs):
# 全局钩子的校验步骤分步进行,在后续会有扩展性改动
self._get_user(attrs)
self._get_token()
return attrs
def _get_user(self, attrs):
input_user = attrs.get('input_user')
password = attrs.get('password')
user = User.objects.filter(Q(username=input_user) |
Q(email=input_user) | Q(mobile=input_user)).first()
if user and user.check_password(password):
self.context['user'] = user
else:
raise MsgException('用户名或者密码输入错误')
def _get_token(self):
refresh = RefreshToken.for_user(self.context.get('user'))
self.context['refresh'] = str(refresh)
self.context['token'] = str(refresh.access_token)
ps:在序列化类中,context属性会被存到序列化对象中,这个可以用于多个方法中的通信,也可以用于序列化类与视图类方法之间的通信
发送验证码接口
我们需要借助第三方或者公司内部的发短信接口,这里使用了腾讯云的SDK短信接口。
逻辑上,我们需要做:
-
判断手机用户是否存在于User表(登录验证码才需要,注册的发送验证码不需要)
-
发送短信验证码
-
验证码存储到缓存以便验证
def send_auth_code(self, request):
mobile = request.query_params.get('mobile')
user = self.get_queryset().filter(mobile=mobile).first()
if not user:
return APIResponse(code=444, msg='手机号未在本系统注册')
# 发送验证码和存储缓存的逻辑都写在了验证码类中
code_obj = AuthCode(mobile=mobile, code_digit=4, expired_min=1)
send_results = code_obj.send_code()
return APIResponse(expired_time='1分钟', sendStatusSet=send_results.get('SendStatusSet'))
# 存储到缓存,django自带缓存
from django.core.cache import cache
cache.set(f"auth_code{self.mobile}", code)
短信验证码登录功能
逻辑上,两种登录功能只有校验逻辑上的细微差别,而签发token的过程一致(通过user签发token)
那么分叉就出现了,在get_user上。
于是我们可以按以下书写序列化类:
class BaseLoginSerializer(serializers.Serializer):
def validate(self, attrs):
self._get_user(attrs)
self._get_token()
return attrs
def _get_user(self, attrs):
raise Exception('你必须重写_get_user方法')
def _get_token(self):
refresh = RefreshToken.for_user(self.context.get('user'))
self.context['refresh'] = str(refresh)
self.context['token'] = str(refresh.access_token)
class LoginMulSerializer(BaseLoginSerializer):
input_user = serializers.CharField()
password = serializers.CharField()
def _get_user(self, attrs):
input_user = attrs.get('input_user')
password = attrs.get('password')
user = User.objects.filter(Q(username=input_user) | Q(email=input_user) | Q(mobile=input_user)).first()
if user and user.check_password(password):
self.context['user'] = user
else:
raise MsgException('用户名或者密码输入错误')
class LoginSacSerializer(BaseLoginSerializer):
code = serializers.CharField()
mobile = serializers.CharField()
def _get_user(self, attrs):
code = attrs.get('code')
mobile = attrs.get('mobile')
auth_code = cache.get(f"auth_code{mobile}", None)
if not code == auth_code:
raise MsgException('验证码无效')
user = User.objects.filter(mobile=mobile).first()
# if not user:
# raise MsgException('你小子,跟刚才输入的号码不一样哦')
self.context['user'] = user
将两个序列化类的validate
方法和_get_token
方法抽象出来,得到一个父类BaseLoginSerializer
,而其中又必须配合一个_get_user
方法,需要在子类中书写形成一个拥有完整校验逻辑的序列化类。
而视图函数,也可以进行一定程度的合并:
@action(methods=['POST'], detail=False)
def login_sac(self, request):
self.serializer_class = LoginSacSerializer
return self._login(request)
@action(methods=['POST'], detail=False)
def login_mul(self, request):
# 在函数体中分别配置序列化类,最终会被get_serializer捕捉到
self.serializer_class = LoginMulSerializer
return self._login(request)
"""原视图代码↓↓"""
# ser = LoginMulSerializer(data=request.data)
# ser.is_valid(raise_exception=True) # 校验错误直接报错交由异常捕获处理
# return APIResponse(
# token=ser.context.get('token'),
# refresh=ser.context.get('refresh')
# )
def _login(self, request):
"""两登录接口的代码一致,只是校验的序列化类不一样"""
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True) # 校验错误直接报错交由异常捕获处理
return APIResponse(
token=ser.context.get('token'),
refresh=ser.context.get('refresh')
)
短信验证码注册接口
注册接口也需要验证码校验,需要发送验证码和校验验证码,校验的逻辑与短信登录接口类似,但是不同的是,注册接口在校验后需要反序列化写入数据库。
对于User表的写入要注意,密码字段要通过密文的形式写入,所以用create_user的方法。
class RegisterSerializer(serializers.ModelSerializer):
code = serializers.CharField(write_only=True) # 为什么code要有write_only属性
class Meta:
model = User
fields = ['mobile', 'password', 'code']
extra_kwargs = {
"password": {"write_only": True}
}
def validate(self, attrs):
code = attrs.pop('code') # 获取code进行校验,同时从attrs中剔除
mobile = attrs.get('mobile')
auth_code = cache.get(f"auth_code{mobile}", None)
if not (code == auth_code or code == "8888"):
raise MsgException('验证码无效')
attrs['username'] = '新用户' + str(uuid4())
# attrs最终传入以下的validated_data中,与最初的request.data相比,多了username,少了code
return attrs
def create(self, validated_data):
# 写入表格,创建用户,username是必填且unique字段,用uuid生成了一个用户名
user = User.objects.create_user(**validated_data) # 创建的是密文
return user
为什么code要有write_only属性?
在createModelMixin重写的create方法中,我们除了执行了反序列化校验,和序列化.save之外,还将最终创建的数据对象序列化返回出去了(restful规范),那么在序列化类时,code如果被计入,就会因为数据对象中没有code字段而导致报错,所以用write_only取消code字段的序列化即可。