# lufflyapi **Repository Path**: duyupeng36/lufflyapi ## Basic Information - **Project Name**: lufflyapi - **Description**: 路飞项目后端 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-02-12 - **Last Updated**: 2021-04-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 路飞项目后端 该项目模仿路飞城,完成路飞学城首页、登录注册、课程列表、课程详情、 购物车、商品结算、个人中心、视频播放功能。 ## 一、项目创建 创建项目后调整目录结构为如下结构 ``` lufly ├── logs # 保存项目运行的日志文件 ├── lufly │   ├── apps # 保存项目创建的app │   │   └── __init__.py │   ├── asgi.py │   ├── __init__.py │   ├── libs # 保存项目使用的第三方库 │   │   └── __init__.py │   ├── settings │   │   ├── debug.py │   │   ├── __init__.py │   │   └── release.py │   ├── urls.py │   └── wsgi.py ├── manage.py ├── README.md └── scripts # 保存项目运行时的脚本 ``` 对项目中的`manage.py`进行修改(开发阶段时,运行项目) ```python import os # 修改前 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lufflyapi.settings') # 修改后 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lufflyapi.settings.debug') ``` 对项目中`wsgi.py和asgi.py`进行修改(上线和测试时,运行项目) ```python import os # 修改前 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lufflyapi.settings') # 修改后 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lufflyapi.settings.release') ``` ## 二、项目配置文件的修改 ### 2.1 语言修改 找到关于地区的配置,修改为如下代码 ```python # 国际化 LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = False ``` ### 2.2 关于数据库配置 使用mysql数据库作为该项目的数据管理软件。 进入mysql ```shell mysql -uroot -p ``` 创建数据库 ```mysql create database lufly default charset = 'utf8' ``` 为指定数据库配置指定用户 ```mysql -- grant 权限(create, update) on 库.表 to 'username'@'host' identified by 'password'; -- 上述命令会创建用户赋予用户指定的权限 -- 配置任何ip都可连入数据库 grant all privileges on luffly.* to 'luffly'@'%' identified by 'Luffly123?'; -- 配置本地ip连入数据库 grant all privileges on luffly.* to 'username'@'localhost' identified by 'Luffly123?'; -- 刷新权限 flush privileges; ``` * 对于mysql8.0不在支持以上命令,需要先创建用户,在给权限 ```mysql create user 'username'@'host' identified by 'password'; grant all privileges on database_name.* to 'username'@'host'; ``` 在`settings/debug.py`和任意的`__init__.py`中配置数据库 ```python # settings/debug.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': "luffly", 'HOST': '127.0.0.1', 'PORT': 3306, 'USER': 'luffly', 'PASSWORD': 'Luffly123?', 'CHARSET': 'utf8' } } # lufly/__init__.py import pymysql pymysql.install_as_MySQLdb() ``` ### 2.3 日志记录配置 在`settings/debug.py`中添加日志配置 ```python import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent # 真实项目上线后,日志文件打印级别不能过低,因为一次日志记录就是一次文件io操作 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': 'DEBUG', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', 'formatter': 'simple' }, 'file': { # 实际开发建议使用ERROR 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', # 日志位置,日志文件名,日志保存目录必须手动创建,注:这里的文件路径要注意BASE_DIR代表的是小luffyapi 'filename': os.path.join(os.path.dirname(BASE_DIR), "logs", "luffy.logs"), # 日志文件的最大值,这里我们设置300M 'maxBytes': 300 * 1024 * 1024, # 日志文件的数量,设置最大日志数量为10 'backupCount': 10, # 日志格式:详细格式 'formatter': 'verbose', # 文件内容编码 'encoding': 'utf-8' }, }, # 日志对象 'loggers': { 'django': { 'handlers': ['console', 'file'], 'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统 }, } } ``` 在`utils`下新建`loggers.py`,键入 ```python import logging logger = logging.getLogger('django') ``` ## 三、全局对象的封装 ### 3.1 全局Response对象封装 在`luflyapi/utils`下新建一个`response.py`,对键入如下内容 ```python from rest_framework.response import Response class APIResponse(Response): def __init__(self, code=100, msg='成功', result=None, status=None, headers=None, content_type=None, **kwargs): data = { 'code': code, 'msg': msg, } if result: data['result'] = result data.update(kwargs) super(APIResponse, self).__init__(data=data, status=status, headers=headers, content_type=content_type) ``` ### 3.2 全局rest_framework异常封装 ```python from rest_framework.views import exception_handler from utils import APIResponse from utils import logger def common_exception_handler(exc, context): """ 自定义异常处理 """ logger.error(f'view is {context["view"]}, error message is {exc.detail}') # 记录日志 ret = exception_handler(exc, context) # 返回Response对象 if not ret: # rest framework处理不了的异常 return APIResponse(code=101, msg='error', result={'detail': exc.detail}) return APIResponse(code=101, msg='error', result=ret.data) ``` 配置rest framework使用自定义的异常处理函数 ```python # rest_framework的配置 REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'lufflyapi.utils.exceptions.common_exception_handler', } ``` ## 四 用户相关 ### 4.1 用户表字段扩展 项目创建时,就已经创建了关于用户的app user 在`user/models.py`中写 ```python from django.db import models from django.contrib.auth.models import AbstractUser # Create your models here. class User(AbstractUser): telephone = models.CharField(verbose_name='手机号', max_length=12) icon = models.ImageField(verbose_name='头像', upload_to='icon', default='icon/default.png') ``` 由于继承了AbstractUser,要在配置文件中指定 ```python AUTH_USER_MODEL = 'user.User' ``` 指定完成后,进行数据库迁移了。 ## 五、首页相关 ### 5.1 轮播图(获取) 表模型设计 #### 5.1.1 表模型(轮播图表) 轮播图表需要的字段如下 |字段|类型|说明| |:---:|:---:|---| |create_time|DateTimeField(auto_now_add=True)|创建时间| |update_time|DateTimeField(auto_now=True)|最后更新时间| |is_delete|BooleanField(default=False)|是否删除| |is_show|BooleanField(default=True)|是否展示| |default|IntegerField()|展示顺序| |||| |name|CharField(max_length=32)|图片名| |image|ImageField(upload_to='banners')|图片的地址| |link_url|CharField(max_length=255)|图片关联的url| |info|TextField|图片简介| 上一半的字段对于其他表(例如,课程表等)而言也是可以使用的。将这些字段作为公共字段,在`utils`目录下新建`models.py` 定义一个基表。 ```python from django.db import models class BaseModel(models.Model): create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') update_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间') is_delete = models.BooleanField(default=False, verbose_name='是否删除') is_show = models.BooleanField(default=True, verbose_name='是否显示') display_order = models.IntegerField(verbose_name='展示顺序') class Meta: abstract = True # 该表不能在数据库中创建,所以指定为抽象表 ``` 非公共字段在`home/models.py`中编写 ```python from django.db import models from utils import BaseModel class Banner(BaseModel): name = models.CharField(verbose_name='图片名', max_length=32) image = models.ImageField(verbose_name='轮播图', upload_to='banners', help_text='图片大小为:3840 * 800') link_url = models.CharField(max_length=255, verbose_name='轮播图的跳转链接') info = models.TextField(verbose_name='简介') ``` #### 5.1.2 序列化器 要将数据返回给前端,且前端能够使用,则需要将查询出来的数据序列化为json对象, 再将数据返回。 使用`rest_framework`的序列化器完成序列化, 再`home`下新建`serializers.py`文件, 写入如下内容。 ```python from rest_framework import serializers from lufflyapi.apps.home import models class BannerModelSerializer(serializers.ModelSerializer): class Meta: model = models.Banner fields = ('name', 'link_url', 'image') ``` 对于数据库表的序列化,可以使用`MdelSerializer`进行序列化。 #### 5.1.2 视图类 视图再`rest_framework`中有很多方式。视图就是为了返回数据给前端,所以要执行 下面几步 1. 获取数据 2. 序列化,序列化多条数据时,注意`many=True` 3. 返回数据 ##### 5.1.2.1 只使用APIView ```python from utils import APIResponse as Response from rest_framework.views import APIView from lufflyapi.apps.home import models from lufflyapi.apps.home import serializers class BannerView(APIView): """ 单独使用APIView实现获取轮播图接口。 路由配置如下 path('banner/', views.BannerView.as_view()), path('banner//', views.BannerView.as_view()) """ def get(self, request, *args, **kwargs): """ get方法获取数据 """ pk = kwargs.get('pk', None) if pk: banner = models.Banner.objects.filter(pk=pk, is_delete=False, is_show=True) if banner: banner_ser = serializers.BannerModelSerializer(banner.first()) return Response(result=banner_ser.data) else: return Response(conde=101, msg='error', result='指定数据不存在') banner_list = models.Banner.objects.filter(is_delete=False, is_show=True) banner_list_ser = serializers.BannerModelSerializer(banner_list, many=True) return Response(result=banner_list_ser.data) ``` 路由配置如下 ```python from django.urls import path, include, re_path from lufflyapi.apps.home import views app_name = 'home' urlpatterns = [ path('banner/', views.BannerView.as_view()), path('banner//', views.BannerView.as_view()) ] ``` ##### 5.1.2.2 只使用GenericAPIView ```python from utils import APIResponse from rest_framework.generics import GenericAPIView from lufflyapi.apps.home import models from lufflyapi.apps.home import serializers class BannerView(GenericAPIView): queryset = models.Banner.objects.filter(is_delete=False, is_show=True) serializer_class = serializers.BannerModelSerializer def get(self, request, *args, **kwargs): """ get方法获取数据 """ pk = kwargs.get('pk', None) if pk: banner = self.get_object() if banner: banner_ser = self.get_serializer(banner) return APIResponse(result=banner_ser.data) return APIResponse(code=101, msg='error', result='指定数据不存在') banner_list = self.get_queryset() banner_ser = self.get_serializer(banner_list, many=True) return APIResponse(result=banner_ser.data) ``` 路由配置如下 ```python from django.urls import path, include, re_path from lufflyapi.apps.home import views app_name = 'home' urlpatterns = [ path('banner/', views.BannerView.as_view()), path('banner//', views.BannerView.as_view()) ] ``` ##### 5.1.2.3 使用GenericAPIView+ListModelMixin ```python from utils import APIResponse from rest_framework.generics import GenericAPIView from rest_framework.mixins import ListModelMixin from lufflyapi.apps.home import models from lufflyapi.apps.home import serializers class BannerView(GenericAPIView, ListModelMixin): queryset = models.Banner.objects.filter(is_delete=False, is_show=True) serializer_class = serializers.BannerModelSerializer def get(self, request, *args, **kwargs): """ get方法获取数据 """ return self.list(request, *args, **kwargs) def list(self, request, *args, **kwargs): pk = kwargs.get("pk", None) if pk: self.queryset = self.queryset.filter(pk=pk) response = super().list(request, *args, **kwargs) return APIResponse(result=response.data) ``` 路由配置如下 ```python from django.urls import path, include, re_path from lufflyapi.apps.home import views app_name = 'home' urlpatterns = [ path('banner/', views.BannerView.as_view()), path('banner//', views.BannerView.as_view()) ] ``` ##### 5.1.2.4 基于APIView+ViewModelMixin(ViewSet) ```python from utils import APIResponse from rest_framework.viewsets import ViewSet from lufflyapi.apps.home import models from lufflyapi.apps.home import serializers class BannerView(ViewSet): def list(self, request, *args, **kwargs): """ 获取数据 """ pk = kwargs.get('pk', None) if pk: banner = models.Banner.objects.filter(pk=pk, is_delete=False, is_show=True) if banner: banner_ser = serializers.BannerModelSerializer(banner.first()) return APIResponse(result=banner_ser.data) else: return APIResponse(conde=101, msg='error', result='指定数据不存在') banner_list = models.Banner.objects.filter(is_delete=False, is_show=True) banner_list_ser = serializers.BannerModelSerializer(banner_list, many=True) return APIResponse(result=banner_list_ser.data) ``` 路由配置如下 ```python from django.urls import path, include, re_path from lufflyapi.apps.home import views app_name = 'home' urlpatterns = [ path('banner/', views.BannerView.as_view(actions={'get': 'list'})), path('banner//', views.BannerView.as_view(actions={'get': 'list'})) ] ``` ##### 5.1.2.5 基于GenericViewSet ```python from utils import APIResponse from rest_framework.viewsets import GenericViewSet from lufflyapi.apps.home import models from lufflyapi.apps.home import serializers class BannerView(GenericViewSet): queryset = models.Banner.objects.filter(is_delete=False, is_show=True) serializer_class = serializers.BannerModelSerializer def list(self, request, *args, **kwargs): """ get方法获取数据 """ pk = kwargs.get('pk', None) if pk: banner = self.get_object() if banner: banner_ser = self.get_serializer(banner) return APIResponse(result=banner_ser.data) return APIResponse(code=101, msg='error', result='指定数据不存在') banner_list = self.get_queryset() banner_ser = self.get_serializer(banner_list, many=True) return APIResponse(result=banner_ser.data) ``` 路由配置如下 ```python from django.urls import path, include, re_path from lufflyapi.apps.home import views app_name = 'home' urlpatterns = [ path('banner/', views.BannerView.as_view(actions={'get': 'list'})), path('banner//', views.BannerView.as_view(actions={'get': 'list'})) ] ``` ##### 5.1.2.6 基于GenericViewSet+ListModelMixin(本项目采用此方式) ```python from utils import APIResponse from rest_framework.mixins import ListModelMixin from rest_framework.viewsets import GenericViewSet from lufflyapi.apps.home import models from lufflyapi.apps.home import serializers class BannerView(GenericViewSet, ListModelMixin): queryset = models.Banner.objects.filter(is_delete=False, is_show=True) serializer_class = serializers.BannerModelSerializer def list(self, request, *args, **kwargs): pk = kwargs.get('pk', None) if pk: self.queryset = self.queryset.filter(pk=pk) response = super(BannerView, self).list(request, *args, **kwargs) return APIResponse(result=response.data) ``` 路由配置如下 ```python from django.urls import path, include, re_path from lufflyapi.apps.home import views app_name = 'home' urlpatterns = [ path('banner/', views.BannerView.as_view(actions={'get': 'list'})), path('banner//', views.BannerView.as_view(actions={'get': 'list'})) ] ``` 或 ```python from django.urls import path, re_path, include from rest_framework.routers import SimpleRouter from home import views app_name = "home" router = SimpleRouter() router.register('banner', views.BannerView, basename='banner') urlpatterns = [ path('', include(router.urls)) ] ``` **出现报错,将其他文件的models的引入方式改为相对引入** ### 5.2 跨域资源访问(cors) #### 5.2.1 同源策略 同源策略是指请求的url地址,必须与浏览器上的url地址处于同域上,也就是域名,端口,协议相同. **同源策略本质上是浏览器的一个安全策略** #### 5.2.2 跨域资源共享 CORS**需要浏览器和服务器同时支持**。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 整个CORS通信过程,都是**浏览器自动完成**,不需要用户参与。对于开发者来说, CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信 ##### 5.2.2.1 CORS基本流程 浏览器将CORS请求分成两类:**简单请求(simple request)** 和 **非简单请求(not-so-simple request)** * **简单请求**: **只需要在头信息之中增加一个Origin字段** * **非简单请求**: **会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)**, 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。 只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。 ##### 5.2.2.2 CORS两种请求详解 1. 什么是简单请求?满足下面两个条件的请求为简单请求 * 请求方法是以下三种方法之一 * HEAD * GET * POST * HTTP的头信息不超出以下几种字段: * Accept * Accept-Language * Content-Language * Last-Event-ID * Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain 2. **凡是不同时满足上面两个条件,就属于非简单请求** 3. 两种请求的处理方式 * 简单请求和非简单请求的区别? * 简单请求:一次请求 * 非简单请求:两次请求,在发送数据之前会先发一次请求用于做“预检”, 只有“预检”通过后才再发送一次请求用于数据传输。 * 关于“预检” * 请求方式:OPTIONS * “预检”其实做检查,检查如果通过则允许传输数据,检查不通过则不再发送真正想要发送的消息 * 如何“预检” * 如果复杂请求是PUT等请求,则服务端需要设置允许某请求,否则“预检”不通过 Access-Control-Request-Method * 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则“预检”不通过 Access-Control-Request-Headers 4. 简单请求的跨域支持 * 服务器设置响应头:Access-Control-Allow-Origin = '域名' 或 '*' 5. 非简单请求的跨域支持 * “预检”请求时,允许请求方式则需服务器设置响应头:Access-Control-Request-Method * “预检”请求时,允许请求头则需服务器设置响应头:Access-Control-Request-Headers #### 5.2.3 跨域资源共享 ##### 5.2.3.1 自定义实现 在`utils`中新建`corsmiddleware.py`文件,键入如下内容 ```python from django.utils.deprecation import MiddlewareMixin class AccessCorsMiddleware(MiddlewareMixin): def process_response(self, request, response): # 处理简单请求跨域 response['Access-Control-Allow-Origin'] = "*" if request.method == 'OPTIONS': response['Access-Control-Request-Headers'] = 'Content-Type' return response ``` ##### 5.2.3.2 第三方实现(本项目采用) 安装: `pip install django-cors-headers` 注册: ```python INSTALLED_APPS = ( 'corsheaders', ) ``` 添加中间件 ```python MIDDLEWARE = [ # Or MIDDLEWARE_CLASSES on Django < 1.10 ..., 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', ... ] ``` 在settings中配置 ```python CORS_ALLOW_CREDENTIALS = True CORS_ORIGIN_ALLOW_ALL = True # CORS_ORIGIN_WHITELIST = ('*') CORS_ALLOW_METHODS = ( 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'VIEW', ) CORS_ALLOW_HEADERS = ( 'XMLHttpRequest', 'X_FILENAME', 'accept-encoding', 'authorization', 'content-type', 'dnt', 'origin', 'user-agent', 'x-csrftoken', 'x-requested-with', 'Pragma', ) ``` ## 六、版本控制工具 `git`是一个分布式的版本控制软件。 ![](./.img/git.jpg) 与另外一个版本控制软件svn对比,svn是集中式管理代码。 ![](./.img/svn.jpg) ### 6.1 git工作流程 ![](./.img/git工作流程.jpg) 本地git划分了三个区域**工作区、暂存区、版本库** 和 **远程版本库** * 工作区: 被git管理后,用于编写代码的区域。在工作区需要创建、修改、删除文件。 * 工作区回滚: `git checkout .`新建文件动作不能回滚, 只有被版本管理的文件修改、删除才能回滚 * 提交到暂存区: `git add .` * 暂存区: 开发完成后,展示保存代码的地方,此时还没被版本管理 * 提交到版本库: `git commit -m 提交信息` * 暂存区回滚: `git reset HEAD .`,移除暂存区 * 版本库: 本地开发出来的代码(本地代码仓库)。 * 版本回滚: `git reset -- hard 版本号`,回滚到指定版本 * 远程版本库: 远程服务器上的代码仓库 * 提交到远程仓库 * 拉取到本地仓库 ### 6.2 git分支管理 ![](./.img/git分支管理.jpg) 分支: 项目开发的时间轴,一个时间节点代表版本库中的一个版本 **分支开发是独立的,不去影响其他分支**,要将子分支开发的内容同步到主分支时,完成分支合并即可。 ### 6.3 基本命令 1. `git init [目录]`: 初始化仓库 2. `git status`: 查看仓库状态 3. `git add 文件`: 提交文件到暂存区 4. `git commit -m "messag"`: 提交到版本库 * 提交到库时需要有作者信息 ```git git config [--global] user.name "name" git config [--global] user.email "email" ``` * `--global`: 表示全局作者信息,没有则仅对当前仓库有效。 * 会在用户家目录下生成`.gitconfig`文件 5. `git checkout 文件`: 文件回滚到被管理的最新版本 6. `git log`: 查看版本管理日志 7. `git reset --hard 版本号`: 回滚到指定的版本状态 ### 6.4 忽略文件 指定某些文件或文件夹不被git管理,**默认不管理空文件夹** 在`.git`所在的目录下面新建`.gitignore`按如下语法编写需要忽略的文件或文件夹 ```gitignore # 注释 #忽略文件夹 文件夹 #忽略文件 文件 # 忽略文件夹中.py结尾的文件 文件夹/*.py # 当前路径下的文件或文件夹 /文件 /文件夹 ``` ### 6.5 分支操作 1. `git branch`: 查看分支 2. `git branch 分支名`: 创建分支 3. `git branch -d 分支名`: 删除分支 4. `git checkout 分支名`: 切换分支 5. `git checkout -b 分支名`: 创建并切换到创建的分支 6. `git merge 分支名`: 合并分支(当前分支合并指定分支) ### 6.6 冲突 1. 分支合并时出现冲突 2. 不同的开发者修改同一分支同一文件的同一行 ### 6.7 git远程仓库 1. `git remote`: 查看远程仓库地址 2. `git remote add 链接名 地址`: 添加远程仓库 3. `git push 链接名 分支名`: 提交到远程仓库指定的分支 4. `git clone 地址`: 克隆到本地 5. `git pull 链接名 分支名`: 从链接指定的远程仓库的指定分支拉取代码 **提交代码之前,必须先拉取代码(更新)** #### 6.7.1 新建仓库 ```shell mkdir git_learn cd git_learn git init # 初始化仓库 touch README.md # 新建文件 git add README.md ## 添加到暂存区 git commit -m "first commit" # 提交到本仓库 git remote add origin https://gitee.com/duyupeng36/git_learn.git # 添加远程链接 git push -u origin master # 提交 ``` #### 6.7.2 已有长裤 ```shell cd existing_git_repo git remote add origin https://gitee.com/duyupeng36/git_learn.git git push -u origin master ``` ### 6.8 ssh链接远程仓库 #### 6.8.1 介绍非对称加密 非对称加密原理是, 1. 寻找两个质数 $P$ 和 $Q$ 2. 计算 $P$ 和 $Q$ 的最小公倍数 $N$ 3. 计算$φ(N) = (P-1)(Q-1)$, 4. 计算公钥E: 公钥必须满足 $1 < E < φ(N)$ 且 $E$ 与 $φ(N)$ 互为质数, 5. 计算私钥D: 私钥 $E * D % φ(N) = 1$ 6. 加密: $C = M^E mod N$; $C$是加密后的密文,$M$是明文, 7. 解密: $M =C^D mod N$; $C$是密文, $M$解密出来的明文 #### 6.8.2 使用ssh链接远程仓库 1. 创建ssh公钥: `ssh-keygen -t 加密算法 -C "内容"` * 生成到用户家目录下的.ssh文件夹里面 2. 将公钥给远程仓库 ![img.png](./.img/gitee添加ssh公钥.png) 打开家目录下的`.ssh/id_rsa.pub`,中的内容复制添加到上面区 ## 七、用户登录注册 前端设计了两种方式,一种是**账号+密码**进行登录,另一种是**手机号+验证码**进行登录 账号+密码方式: 该方式的账号可以提供用户名、手机号、邮箱,需要在后端提供多方式登录接口 手机号+验证码: 需要提供一个手机号登录接口 要实现登录和注册的接口,后端需要提供如下几个接口 1. 多方登录接口 2. 手机号登录接口 3. 发送验证码接口 4. 校验手机号是否存在的接口 5. 注册接口 ### 7.1 多方式登录 登录的用户使用JWT方式进行验证 #### 7.1.1 JWT认证 用户注册或登录成功后,要保存用户登录状态。已经知道了cookie和session两种状态认证方式,但是它们都有缺点。 * cookie由于信息保存在用户的浏览器之上,用户信息安全不能够保证。 * session是将用户信息保存在服务端,返回给用户一个随机字符串。当用户登录过多时,会造成服务器数据压力大。对于服务器的集群部署支持不好。 ![](https://images.gitee.com/uploads/images/2021/0130/103150_d371cec0_7841459.jpeg "基于session认证.jpg") ![](https://images.gitee.com/uploads/images/2021/0130/103226_9a7d3c49_7841459.jpeg "基于session认证集群部署.jpg") 为了解决上述问题,将用户的特征信息,及特征信息的加密数据存放在浏览器。每当用户访问时,都会携带着这个信息,在服务进行认证。基于JSON数据格式的。将这种认证称为Json Web Token,实际就是token认证。 ![](https://images.gitee.com/uploads/images/2021/0130/103618_b264a0bc_7841459.jpeg "jwt认证.jpg") ![](https://images.gitee.com/uploads/images/2021/0130/103634_2d9c66be_7841459.jpeg "jwt认证集群部署.jpg") ##### 7.1.1.1 JWT的构成和工作原理 ###### JWT的构成 JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样 ```python "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" ``` **第一部分** 我们称它为头部(header): 头部承载两部分信息 1. 声明类型,这里是jwt 2. 声明加密的算法 通常直接使用 `HMACSHA256` 头部信息是像这样的 ```python { 'typ': 'JWT', 'alg': 'HS256' } ``` 然后进行base64编码后的信息 ```python "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" ``` **第二部分** 我们称其为载荷(payload, 类似于飞机上承载的物品): 存放有效信息的地方, 包含三部分内容 1. 标准中注册的声明 * iss: **jwt签发者** * sub: **jwt所面向的用户** * aud: 接收jwt的一方 * exp: **jwt的过期时间,这个过期时间必须要大于签发时间** * nbf: 定义在什么时间之前,该jwt都是不可用的. * iat: jwt的签发时间 * jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避时序攻击。 2. 公共的声明 公共的声明可以添加任何的信息,一般添加 **用户的相关信息或其他业务需要的必要信息** .但不建议添加敏感信息,因为该部分在客户端可解密. 3. 私有的声明 私有声明是 **提供者和消费者** 所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息 定义一个payload: ```python { "sub": "1234567890", "name": "John Doe", "admin": True } ``` 然后将其进行base64编码,得到JWT的第二部分 ```python "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9" ``` **第三部** 分是签证(signature): 是一个签证信息,这个签证信息由三部分组成 1. **header** (base64后的) 2. **payload** (base64后的) 3. **secret** 这个部分需要base64编码后的header和base64编码后的payload使用`.`连接组成的字符串,然后通过header中声明的加密方式进行加盐`secret`组合加密,然后就构成了jwt的第三部分 ```js // javascript var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ ``` 将这三部分用`.`连接成一个完整的字符串,构成了最终的jwt ```python "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" ``` **注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了** ###### JWT原理 jwt分三段式:头.体.签名 (head.payload.sgin) **头和体是可逆加密** ,让服务器可以反解出user对象; **签名是不可逆加密** ,保证整个token的安全性的 头体签名三部分, **都是采用json格式的字符串** ,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法 头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息 ```python { "company": "公司信息", } ``` 体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间 ```python { "user_id": 1, } ``` 签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密 ```python { "head": "头的加密字符串", "payload": "体的加密字符串", "secret_key": "安全码" } ``` **签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token** ```python """ 1)用基本信息存储json字典,采用base64算法加密得到 头字符串 2)用关键信息存储json字典,采用base64算法加密得到 体字符串 3)用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串 账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台 """ ``` **校验:根据客户端带token的请求 反解出 user 对象** ```python """ 1)将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理 2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时同一设备来的 3)再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户 """ ``` **drf项目的jwt认证开发流程** ```python """ 1)用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中 2)校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户 注:登录接口需要做 认证 + 权限 两个局部禁用 """ ``` ##### 7.1.1.2 自定义 在`utils/jsonwebtoken.py`编写如下代码 ```python import base64 import hashlib import json from datetime import datetime from datetime import timedelta HEADER = { 'type': 'jwt', 'alg': 'sha256' } class JsonWebToken: def __init__(self, head=None): self.__head = head if head else HEADER self.__head = self.__json_dumps(self.__head) self.__head = self.__base64_encode(self.__head) self.__payload = None self.__token = None def jwt_payload_handler(self, user): """ 传入用户对象,返回payload, bytes对象 """ payload_dict = { 'id': user.id, # 用户唯一标识 'username': user.username, # 用户名 'iat': datetime.now().timestamp(), # 签发时间 'exp': (datetime.now() + timedelta(days=7)).timestamp() # 过期时间 } return self.__json_dumps(payload_dict) def jwt_encode_handler(self, payload): """ 传入payload,返回token字符串 """ secret = self.__head + b"." + self.__base64_encode(payload) return secret + b'.' + hashlib.sha256(secret).hexdigest().encode() def jwt_decode_handler(self, token): """ 将token字符串传入,返回payload """ if not isinstance(token, bytes): token = token.encode() self.__payload = token.split(b'.')[1] self.__token = token.split(b'.')[-1] return self.__payload def authenticated(self, payload): """ 校验登录是否过期 """ if not isinstance(payload, bytes): payload = payload.encode() payload_string = self.__base64_decode(payload) payload_dict = self.__json_loads(payload_string) exp = payload_dict.get('exp', None) if exp and exp > datetime.now().timestamp(): return True return False @staticmethod def __base64_encode(string): """ 将string的base64编码返回 """ if not isinstance(string, bytes): string = string.encode() return base64.b64encode(string) @staticmethod def __base64_decode(string): """ 将string解码后返回 """ if not isinstance(string, bytes): string = string.encode() return base64.b64decode(string).decode() @staticmethod def __json_dumps(obj): """ 将obj序列化 """ return json.dumps(obj).encode() @staticmethod def __json_loads(string): """ 将string反序列化 """ return json.loads(string) ``` ##### 7.1.1.3 第三方jwt认证(djangorestframework-jwt) 参考[JWT认证](https://gitee.com/duyupeng36/python-learn/blob/master/jwt%E8%AE%A4%E8%AF%81.md) #### 7.1.2 多方式登录接口 ##### 7.1.2.1 序列化器 在`user/serializers.py`编写如下代码 ```python import re from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler from user import models class UserSerializer(serializers.ModelSerializer): username = serializers.CharField() class Meta: model = models.User fields = ['id', 'username', 'password'] extra_kwargs = { 'id': {'read_only': True}, 'password': {'write_only': True} } def validate(self, attrs): """ 全局钩子函数 """ user = self.__get_user(attrs) token = self.__get_token(user) self.context['user'] = user self.context['token'] = token return attrs def __get_user(self, attrs): """ 获取用户 """ username = attrs.get('username') password = attrs.get('password') if re.match(r'1[3-9][0-9]{9}', username): user = models.User.objects.filter(telephone=username).first() elif re.match(r"^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$", username): user = models.User.objects.filter(email=username).first() else: user = models.User.objects.filter(username=username).first() if user: if user.check_password(password): return user else: raise ValidationError('密码错误') else: raise ValidationError(f'{username}不存在') def __get_token(self, user): """ 签发token """ payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) return token ``` ##### 7.1.2.2 视图类 继承ViewSet ```python from rest_framework.viewsets import ViewSet from rest_framework.decorators import action from user import serializers from utils import APIResponse class LoginView(ViewSet): @action(['post'], detail=False) def login(self, request): ser = serializers.UserSerializer(data=request.data) if ser.is_valid(): token = ser.context['token'] username = ser.context['user'].username return APIResponse(result=dict(token=token, username=username)) return APIResponse(code=101, msg='login fail', result=ser.errors) ``` ##### 7.1.2.3 路由配置 使用了ViewSet,可以使用路由组件自动生成 ```python from django.urls import path, include from rest_framework.routers import SimpleRouter from user import views router = SimpleRouter() router.register('', viewset=views.LoginView, basename='login') app_name = 'user' urlpatterns = [ path('', include(router.urls)) ] ``` ### 7.2 发送手机验证码 前端传入手机号,后端向手机号发送验证码,使用腾讯发送短信的接口 #### 7.2.1 腾讯发送手机短信的sdk封装 ```python # -*- coding: utf-8 -*- from tencentcloud.common import credential from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException # 导入 SMS 模块的client models from tencentcloud.sms.v20190711 import sms_client, models # 导入可选配置类 from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile import random import json import os from utils import logger def get_code(): code = '' return code.join([str(random.randint(0, 9)) for i in range(4)]) def send_code(phone, code): try: # 必要步骤: # 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId 和 secretKey # 本示例采用从环境变量读取的方式,需要预先在环境变量中设置这两个值 # 您也可以直接在代码中写入密钥对,但需谨防泄露,不要将代码复制、上传或者分享给他人 # CAM 密钥查询:https://console.cloud.tencent.com/cam/capi from django.conf import settings file = open(settings.BASE_DIR / "libs/data.json", 'r', encoding='utf-8') dic = json.load(file) secretId = dic.get('secretId') secretKey = dic.get('secretKey') cred = credential.Credential(secretId, secretKey) # cred = credential.Credential( # os.environ.get("secretId"), # os.environ.get("secretKey") # ) # 实例化一个 http 选项,可选,无特殊需求时可以跳过 httpProfile = HttpProfile() httpProfile.reqMethod = "POST" # POST 请求(默认为 POST 请求) httpProfile.reqTimeout = 30 # 请求超时时间,单位为秒(默认60秒) httpProfile.endpoint = "sms.tencentcloudapi.com" # 指定接入地域域名(默认就近接入) # 非必要步骤: # 实例化一个客户端配置对象,可以指定超时时间等配置 clientProfile = ClientProfile() clientProfile.signMethod = "TC3-HMAC-SHA256" # 指定签名算法 clientProfile.language = "en-US" clientProfile.httpProfile = httpProfile # 实例化 SMS 的 client 对象 # 第二个参数是地域信息,可以直接填写字符串 ap-guangzhou,或者引用预设的常量 client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile) # 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 # 您可以直接查询 SDK 源码确定 SendSmsRequest 有哪些属性可以设置 # 属性可能是基本类型,也可能引用了另一个数据结构 # 推荐使用 IDE 进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 req = models.SendSmsRequest() # 基本类型的设置: # SDK 采用的是指针风格指定参数,即使对于基本类型也需要用指针来对参数赋值 # SDK 提供对基本类型的指针引用封装函数 # 帮助链接: # 短信控制台:https://console.cloud.tencent.com/smsv2 # sms helper:https://cloud.tencent.com/document/product/382/3773 # 短信应用 ID: 在 [短信控制台] 添加应用后生成的实际 SDKAppID,例如1400006666 req.SmsSdkAppid = dic.get('SmsSdkAppid') # 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,可登录 [短信控制台] 查看签名信息 req.Sign = dic.get('Sign') # 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper] req.ExtendCode = "" # 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 req.SessionContext = "xxx" # 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper] req.SenderId = "" # 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号] # 例如+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 req.PhoneNumberSet = [phone] # 模板 ID: 必须填写已审核通过的模板 ID,可登录 [短信控制台] 查看模板 ID req.TemplateID = dic.get('TemplateID') # 模板参数: 若无模板参数,则设置为空 req.TemplateParamSet = [code] # 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的 resp = client.SendSms(req) # 输出 JSON 格式的字符串回包 dic = json.loads(resp.to_json_string(indent=2)) SendStatusSet = dic.get('SendStatusSet')[0] if SendStatusSet.get('Fee'): return True return False except TencentCloudSDKException as err: logger.error('手机号: %s,验证码发送失败;失败原因: %s' % (phone, str(err))) return False ``` #### 7.2.2 手机验证码接口视图 只需要在LoginView类添加如下代码 ```python from libs.tencent import get_code, send_code from rest_framework.viewsets import ViewSet from utils import APIResponse from rest_framework.decorators import action class LoginView(ViewSet): @action(['get'], detail=False) def send(self, request, *args, **kwargs): """ 发送验证码接口 """ code = get_code() telephone = "+86" + request.query_params.get('telephone') is_success = send_code(telephone, code) if is_success: return APIResponse(result="验证码发送成功") return APIResponse(code=101, msg='error send code', result='验证码发送失败') ``` 路由可以自动生成 #### 7.2.3 短信接口的频率限制 基于BaseThrottle,实现频率限制 ```python import time from rest_framework.throttling import BaseThrottle class IPThrottle(BaseThrottle): VISIT_DIC = {} def __init__(self): self.history_list = [] def allow_request(self, request, view): """ 频率限制的逻辑 :param request: :param view: :return: """ # 1. 取出提交的手机号 # 2. 判断手机号是否在访问字典中,返回True, 表示第一次访问 # 3. 循环判断当前手机的列表,有值,并且当前时间减去的最后一个时间大于60秒,把这种时间移除 # 4. 判断当前IP列表中的个数是否小于某个值,小于则返回True表示通过 # 5. 大于则,限制访问,返回False telephone = request.query_params.get("telephone") ctime = time.time() if telephone not in self.VISIT_DIC: self.VISIT_DIC[telephone] = [ctime] return True self.history_list: list = self.VISIT_DIC[telephone] while True: if ctime - self.history_list[-1] > 60: self.history_list.pop() else: break if len(self.history_list) < 3: self.history_list.insert(0, ctime) return True return False def wait(self): """ 返回限制的剩余时间 :return: """ ctime = time.time() return 60 - (ctime - self.history_list[-1]) ``` SimpleRateThrottle,实现频率限制(本项目采用) ```python from rest_framework.throttling import SimpleRateThrottle class SMSThrottle(SimpleRateThrottle): scope = 'sms' def get_cache_key(self, request, view): """ 返回的字段就是用于频率限制的字段 """ telephone = request.query_params.get('telephone') # 'throttle_%(scope)s_%(ident)s' return self.cache_format % {"scope": self.scope, "ident": telephone} ``` 在配置文件中配置添加如下配置信息 ```python REST_FRAMEWORK = { # Throttling 'DEFAULT_THROTTLE_RATES': { 'sms': '1/m', # 限制短信接口的访问频率为1分钟一次 }, } ``` 重构`views.py`,由于频率限制会限制整个视图类中的所有方法。将发送短信验证码的方法 写在新的视图类中,并局部配置频率限制类 ```python from django.core.cache import cache from rest_framework.viewsets import ViewSet from rest_framework.decorators import action from utils import APIResponse from libs.tencent import get_code, send_code from lufflyapi.apps.user.throttlings import SMSThrottle class SMSSendView(ViewSet): throttle_classes = [SMSThrottle] # 配置频率限制类 @action(['get'], detail=False) # action中也可以设置局部权限认证 def send(self, request, *args, **kwargs): """ 发送验证码接口 """ code = get_code() telephone = "+86" + request.query_params.get('telephone') cache.set('cache_%s_code' % telephone, code, 180) # 缓存 is_success = send_code(telephone, code) if is_success: return APIResponse(result="验证码发送成功") return APIResponse(code=101, msg='error send code', result='验证码发送失败') ``` ### 7.3 手机号是否存在校验接口 将前端发送过来的手机号,在数据库中进行匹配。 匹配成功,返回 ```python { "code": 100, "msg": "成功", "result": "手机号存在" } ``` 匹配失败,返回 ```python { "code": 101, "msg": "error search telephone", "result": "手机号不存在" } ``` ```python from rest_framework.viewsets import ViewSet from utils import APIResponse from rest_framework.decorators import action from user import models class LoginView(ViewSet): @action(['get'], detail=False) def check_telephone(self, request, *args, **kwargs): """ 检查手机号是否存在 """ telephone = request.query_params.get('telephone') user = models.User.objects.filter(telephone=telephone).first() if user: return APIResponse(result="手机号存在") return APIResponse(code=101, msg='error search telephone', result='手机号不存在') ``` ### 7.4 手机号+验证码登录接口 前端发送过来手机号和验证码时,需要判断手机号是否存在,验证码输入是否正确。 只有手机号存在且验证码正确才能登录成功。 登录成功返回 ```python { "code": 100, "msg": "成功", "result": { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2MTMzMTQxMjksImVtYWlsIjoiMjMyMTkzNjQwMkBxcS5jb20ifQ.8dgMD23A7qb7lgW7roFAdprCKQQSIeZYadqfprmWr8U", "username": "root" } } ``` 登录失败返回 ```python { "code": 101, "msg": "error", "result": { "non_field_errors": [ "验证码不正确" ] } } ``` #### 7.4.1 序列化器 ```python import re from django.core.cache import cache from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler from user import models from django.conf import settings class CodeUserModelSerializer(serializers.ModelSerializer): code = serializers.CharField(required=True, write_only=True) telephone = serializers.CharField(max_length=11, min_length=11) # 数据库指定了唯一,重写字段才能校验通过,否在会出现异常 class Meta: model = models.User fields = ['telephone', 'code'] def validate(self, attrs): """ 全局钩子 """ user = self.__get_user(attrs) # 获取用户 token = self.__get_token(user) # 获取token self.context['token'] = token self.context['user'] = user return attrs def __get_user(self, attrs): """ 获取用户 """ telephone = attrs.get('telephone') code = attrs.get('code') # 取出code cache_code = cache.get(settings.CODE_CACHE_KEY % {'telephone': telephone}) if code == cache_code: # 验证码校验通过,进行用户检测 if re.match(r'1[3-9][0-9]{9}', telephone): try: user = models.User.objects.get(telephone=telephone) return user except: raise ValidationError('手机号不存在,请先注册') else: raise ValidationError('手机号不合法') raise ValidationError('验证码不正确') def __get_token(self, user): """ 签发token """ payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) return token ``` #### 7.4.2 视图 只需要在视图类LoginView中添加即可, 路由自动生成 ```python from rest_framework.viewsets import ViewSet from rest_framework.decorators import action from user import serializers from utils import APIResponse class LoginView(ViewSet): @action(methods=['post'], detail=False) def telephone_login(self, request, *args, **kwargs): """ 手机号+验证码的登录接口 """ code_serializer = serializers.CodeUserModelSerializer(data=request.data) if code_serializer.is_valid(): token = code_serializer.context['token'] username = code_serializer.context['user'].username return APIResponse(result={'token': token, 'username': username}) else: return APIResponse(code=101, msg='error', result=code_serializer.errors) ``` ### 7.5 用户注册接口 前端传入手机号、密码、验证码,进行校验注册 注册成功返回 ```python { "code": 100, "msg": "成功", "result": { "telephone": "17342541080", "username": "17342541080" } } ``` 注册失败返回 ```python { "code": 101, "msg": "error", "result": { "telephone": [ "具有 手机号 的 用户 已存在。" ] } } ``` #### 7.5.1 序列化器 基于ModelSerializer ```python import re from django.core.cache import cache from rest_framework import serializers from rest_framework.exceptions import ValidationError from lufflyapi.apps.user import models from django.conf import settings class RegisterUserModelSerializer(serializers.ModelSerializer): code = serializers.CharField(max_length=4, min_length=4, write_only=True) class Meta: model = models.User fields = ['telephone', 'code', 'password', 'username'] extra_kwargs = { 'password': {'write_only': True, 'max_length': 18, 'min_length': 8}, 'username': {'read_only': True} } def validate_password(self, data): """ 校验密码是否合法 1.密码必须由字母、数字、特殊符号组成,区分大小写 2.特殊符号包含(. _ ~ ! @ # $ / ^ & *) 3.密码长度为8-18位 :param data: 校验通过的密码 """ if re.match(r'^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[._~!@#$^&*/])[A-Za-z0-9._~!@#$^&*/]{8,18}$', data): return data raise ValidationError('密码必须由字母(区分大小写)、数字、特殊符号(. _ ~ ! @ # $ / ^ & *),长度必须再8~18之间') def validate(self, attrs): """ 全局钩子, 用于校验code和手机号,并设置用户名为手机号 """ telephone = attrs.get('telephone') code = attrs.get('code') cache_code = cache.get(settings.CODE_CACHE_KEY % {'telephone': telephone}) if re.match(r"^1[3-9][0-9]{9}$", telephone): # 手机号校验通过 if code == cache_code: # 验证码校验通过 attrs.pop('code') attrs['username'] = telephone return attrs else: raise ValidationError('验证码错误') else: raise ValidationError('手机号不合法') def create(self, validated_data): """ 重写create方法, 保存用户 """ user = models.User.objects.create_user(**validated_data) return user ``` #### 7.5.2 视图类 ##### 7.5.2.1 基于ViewSet实现 要先调用`serializer`的`is_valid()`方法,校验数据,通过后,调用其`save()` 方法,完成用户注册. **视图->save->create->数据库** ```python from rest_framework.viewsets import ViewSet from rest_framework.decorators import action from user import serializers from utils import APIResponse class RegisterView(ViewSet): @action(['post'], detail=False) def register(self, request, *args, **kwargs): register_serializer = serializers.RegisterUserModelSerializer(data=request.data) if register_serializer.is_valid(): register_serializer.save() return APIResponse(result='注册成功') return APIResponse(code=101, msg='register fail', result='注册失败') ``` 路由配置 ```python from django.urls import path, include from rest_framework.routers import SimpleRouter from user import views router = SimpleRouter() router.register('', viewset=views.RegisterView, basename='register') app_name = 'user' urlpatterns = [ path('', include(router.urls)) ] # 或者 path('register/', views.RegisterView.as_view(actions={'post': 'register'})) ``` ##### 7.5.2.2 基于GenericViewSet实现 ```python from rest_framework.viewsets import GenericViewSet from rest_framework.decorators import action from user import serializers from user import models from utils import APIResponse class RegisterView(GenericViewSet): queryset = models.User.objects.all() serializer_class = serializers.RegisterUserModelSerializer @action(['post'], detail=False) def register(self, request, *args, **kwargs): register_serializer = self.get_serializer(data=request.data) if register_serializer.is_valid(): register_serializer.save() return APIResponse(result='注册成功') return APIResponse(code=101, msg='register fail', result='注册失败') ``` 路由配置与7.5.2.1一样 ##### 7.5.2.3 基于GenericViewSet + CreateModelMixin实现 ```python from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import CreateModelMixin from user import serializers from user import models class RegisterView(GenericViewSet, CreateModelMixin): queryset = models.User.objects.all() serializer_class = serializers.RegisterUserModelSerializer ``` 视图配置 ```python from django.urls import path, include from rest_framework.routers import SimpleRouter from user import views router = SimpleRouter() router.register('register', viewset=views.RegisterView, basename='register') app_name = 'user' urlpatterns = [ path('', include(router.urls)), ] # 或者 path('register/', views.RegisterView.as_view(actions={'post': 'create'})) ``` 修改视图为(新增一个方法,调用create方法) ```python from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import CreateModelMixin from rest_framework.decorators import action from user import serializers from user import models from utils import APIResponse class RegisterView(GenericViewSet, CreateModelMixin): queryset = models.User.objects.all() serializer_class = serializers.RegisterUserModelSerializer @action(['post'], detail=False) def register(self, request, *args, **kwargs): response = self.create(request, *args, **kwargs) return APIResponse(result=response.data) ``` 路由配置变为 ```python from django.urls import path, include from rest_framework.routers import SimpleRouter from user import views router = SimpleRouter() router.register('', viewset=views.RegisterView, basename='register') app_name = 'user' urlpatterns = [ path('', include(router.urls)), ] # 或者 path('register/', views.RegisterView.as_view(actions={'post': 'register'})) ``` ## 八、redis使用 使用python对redis数据类型的简单操作 ### 8.1 普通链接与连接池 安装操作redis的库`pip install redis` redis-py提供两个类Redis和StrictRedis用于实现Redis的命令, StrictRedis用于实现大部分官方的命令,并使用官方的语法和命令, Redis是StrictRedis的子类,用于向后兼容旧版本的redis-py #### 8.1.1 python操作redis普通链接 ```python import redis connection = redis.Redis() # 链接redis服务端,返回链接对象 connection.set('name', 'dyy') print(connection.get('name')) ``` #### 8.1.2 python操作redis连接池 ```python import redis pool = redis.ConnectionPool(host='127.0.0.1', port=6379) # 必须将这个连接池做成单例,可以继承重写,也可以通过模块导入的形式。 r = redis.Redis(connection_pool=pool) print(r.get('name')) ``` 必须将这个连接池做成单例,可以继承重写,也可以通过模块导入的形式。 ### 8.2 字符串操作(string) redis中的String在在内存中按照一个name对应一个value来存储 ```python import redis pool = redis.ConnectionPool(host='127.0.0.1', port=6379) # 必须将这个连接池做成单例,可以继承重写,也可以通过模块导入的形式。 r = redis.Redis(connection_pool=pool) ``` #### 8.2.1 增加 * `r.set(name, value, ex=None, px=None, nx=False, xx=False)`: 给name键设置value值 1. 以`name: value`形式存放在内存 2. `ex`: 过期时间,单位秒 3. `px`: 过期时间,单位毫秒 4. `nx`: 如果设置为`True`,则只有`name`不存在时,当前`set`操作才执行,值存在,就修改不了,执行没效果 5. `xx`: 如果设置为`True`,则只有`name`存在时,当前`set`操作才执行,值存在才能修改,值不存在,不会设置新值 * `r.setnx(name, value)`: 当`name`不存在时,给`name`设置`value`值 * `r.setex(name, value, ex)`: 给`name`设置`value`值,`ex`时间内有效 1. `ex`: 过期时间(数字秒 或 timedelta对象) * `r.psetex(name, px, value)`: 给`name`设置`value`值,`px`时间内有效 1. `px`: 过期时间(数字毫秒 或 timedelta对象) * `r.mset(*args, **kwargs)`: 批量设置值 1. `r.mset(k1='v1', k2='v2')`或`r.mset({'k1': 'v1', 'k2': 'v2'})` #### 8.2.2 查找 * `r.get(name)`: 获取`name`对应的值 * `r.mget(keys, *args)`: 获取多个name对应的值 * `r.mget('k1', 'k2')`或 `r.mget(['k3', 'k4'])` * `r.getset(name, value)`: 获取原来的值,并设置新值 * `r.getrange(name, start, end)`: 获取`name`对应的字符串的子串 * `[start, end]`表示字节数 * `r.getbit(name, offset)`: 获取`name`对应二进制数据的`offset`位上的数据(只有0和1) * `r.bitcount(key, start, end)`: 获取`name`对应的二进制串中1的个数(登录与非登录用户分类) * `r.strlen(name)`: 返回name对应的字节长度(一个汉族3个字节(utf8)) #### 8.2.3 修改 * `r.setrange(name. offset, value)`: 修该`name`对应字符串`offset`开始后面部分为`value` * `r.setbit(name, offset, value)`: 修改`name`对应二进制串`offset`位置修改位`value` * `r.append(name, value)`: 追加`value`到`name`对应的字符串的尾部 #### 8.2.4 字符串当数字使用 * `r.incr(name, amount=1)`: 将`name`对应`value`进行增值操作, `name`不存在则创建, 并赋值为`amount`(访问量统计) * `r.decr(name, amount=1)`: 将`name`对应`value`进行减值操作, `name`不存在则创建,并赋值为`-amount`(秒杀) ### 8.3 hash操作 redis中的hash结构是值value的结构。 #### 8.3.1 设置 * `r.hset(name, key, value)`: 给`name`设置`key-value`键值对。如果`name`存在,则新增。`key`重复则修改 * `r.hmset(name, mapping)`: 给`name`设置`mapping`中的`key-value`键值对 #### 8.3.2 获取 * `r.hget(name, key)`: 获取`name`对应的`hash`对象中`key`对应的`value`值 * `r.hmget(name, keys, *args)`: 获取`name`对应的`hash`对象中获取多个`key`的值 1. `keys`: 要获取`key`集合,如:`['k1', 'k2', 'k3']` 2. `*args`: 要获取的`key`,如:`k1,k2,k3` * `r.hgetall(name)`: 获取`name`对应的`hash`对象所有值 * `r.hlen(name)`: 获取`name`对应`hash`对象的长度 * `r.hkeys(name)`: 获取`name`对应`hash`对象的`keys` * `r.hvalues(name)`: 获取`name`对应`hash`对象的`values` * `r.hincrby(name, key, amount=1)`: 给`name`对应的`hash`对象的`key`的对应的`value`增加`amount` * `r.hscan(name, cursor=0, match=None, count=None)`: 增量式迭代获取,对于数据大的数据非常有用,hscan可以实现分片的获取数据,并非一次性将数据全部获取完,从而放置内存被撑爆 1. `name`,redis的name 2. `cursor`,游标(基于游标分批取获取数据) 3. `match`,匹配指定`key`,默认`None` 表示所有的`key` 4. `count`,每次分片最少获取个数,默认`None`表示采用Redis的默认分片个数 例如 ```python # 第一次:cursor1, data1 = r.hscan('xx', cursor=0, match=None, count=None) # 第二次:cursor2, data1 = r.hscan('xx', cursor=cursor1, match=None, count=None) # ... # 直到返回值cursor的值为0时,表示数据已经通过分片获取完毕 ``` * `r.hscan_iter(name, match=None, count=None)`: 利用yield封装hscan创建生成器,实现分批去redis中获取数据 #### 8.3.3 判断 * `r.hexists(name, key)`: 判断`name`对应的`hash`对象是否存在`key` #### 8.3.4 删除 * `r.hdel(name, *keys)`: 删除`name`对应的`hash`对象的`key` ### 8.4 列表操作(list) redis中的List在在内存中按照一个name对应一个List来存储 #### 8.4.1 增加 * `r.lpush(name, *values)`: 将`value`按顺序从左边插入到`name`对应的列表中 * `r.lpushx(name, *values)`: 当`name`存在时,将`value`按顺序从左边插入到`name`对应的列表中 * `r.rpush(name, *values)`: 将`value`按顺序从右边插入到`name`对应的列表中 * `r.rpushx(name, *values)`: 当`name`存在时,将`value`按顺序从右边插入到`name`对应的列表中 * `r.insert(name, where, refvalue, value)`: 指定在`refvalue`位置的`where`处插入 1. `where`: `BEFORE` 或 `AFTER` 2. `refvalue`: 标的值,存在多个,则第一个找到的位置为标的 3. `value`: 待插入的数据 #### 8.4.2 获取 * `r.llen(name)`: 查看`name`列表的长度 * `r.lindex(name, index)`: 查看name列表的index位置的值 * `r.lrange(name, start, end)`: 获取`name`列表的`[start, end]`区间内的元素 #### 8.4.3 修改 * `r.lset(name, index, value)`: 给`index`位置从新赋值为`value` * `index`: 索引,从0开始计数 #### 8.4.4 删除 * `r.lrem(name, count, value)`: 删除`value`,`count`表示删除次数 * `count=0`: 删除所用 * `count>0`: 从前到后删除count个 * `count<0`: 从后到前删除count个 * `r.lpop(name)`: 在`name`列表的左侧第一个元素删除并返回 * `r.rpop(name)`: 在`name`列表的右侧第一个元素删除并返回 * `r.ltrim(name, start, end)`: 在`name`列表中移除没有在`[start, end]`之间的值 * `r.rpoplpush(src, dst)`: 将`src`右边的值删除,并插入到`dst`列表的左边 * `r.blpop(keys, timeout=0)`: 从keys指定的列表左边开始删除并返回数,如果不能获取到数据则阻塞 * `r.brpop(keys, timeout=0)`: 从keys指定的列表右边开始删除并返回数,如果不能获取到数据则阻塞 * `r.brpoplpush(src, dst, timeout=0)`: 将`src`右边的值删除,并插入到`dst`列表的左边, 如果不能获得数据则阻塞 #### 8.4.5 自定义增量迭代 redis没有提供列表元素的增量迭代,如果想要循环name对应的列表所有元素, 那么就需要之定义,逻辑代码如下 ```python import redis conn = redis.Redis(host='127.0.0.1', port=6379) conn.lpush('test', *[1, 2, 3, 4, 45, 5, 6, 7, 7, 8, 43, 5, 6, 768, 89, 9, 65, 4, 23, 54, 6757, 8, 68]) # conn.flushall() def scan_list(name, count=10): index = 0 while True: data_list = conn.lrange(name, index, count + index - 1) if not data_list: return index += count for item in data_list: yield item print(conn.lrange('test', 0, 100)) for item in scan_list('test', 5): print('---') print(item) ``` ### 8.5 集合操作(set) #### 8.5.1 添加 * `r.sadd(name,*values)`: 给`name`集合添加指定`value`值 #### 8.5.2 删除 * `r.srem(name,*values)`: 从`name`集合中删除指定的`value` * `r.spop(name)`: 随机删除并返回`name`中的一个元素 * `r.smove(src, dst, value)`: 将`src`中的`value`删除并添加到`dst`集合中 #### 8.5.3 获取 * `r.scard(name)`: 获取`name`集合中的元素个数 * `r.smembers(name)`: 获取`name`集合中的所有元素 * `r.srandmember(name)`: 随机返回集合`name`中的元素,不删除 * `r.srandmember(name, numbers)`: 随机从集合`name`中获取`numbers`个元素 * `r.sscan(name, cursor=0, match=None, count=None)`: 分片获取 * `r.sscan_iter(name, match=None, count=None)`: 分片获取 #### 8.5.4 运算 * `r.sismember(name, value)`: 判断`value`是否是`name`集合的元素 * `r.sinter(keys, *args)`: 求交集 * `r.sinterstore(dest,keys,*args)`: 求交集,并将交集保存在`dest` * `r.sunion(keys.*args)`: 求并集 * `r.sunionstore(dest,keys,*args)`: 求并集,并将并集保存在`dest` * `r.sdiff(key,*args)`: 求差集 * `r.sdiffstore(dset,key,*args)`: 求差集, 并将差集保存在`dest`中, 返回差集个数 ### 8.6 `zset`操作 有序集合,在集合的基础上,为每元素排序; 元素的排序需要根据另外一个值来进行比较, 所以,对于有序集合,每一个元素有两个值, 即:值和分数,分数专门用来做排序。 #### 8.6.1 添加 * `r.zadd(name, **kwargs)`: 向`name`中添加元素, 如果元素已存在, 则更新其score * `r.zincrby(name, amount, value)`: 如果在`name`中已经存在元素`value`,则该元素的`score`增加`amount`,否则向该集合中添加该元素,其`score`的值为`amount` #### 8.6.2 删除 * `r.zrem(name, *values)`: 在`name`中删除`value` * `r.zremrangebyrank(name, min, max)`: 删除`name`中排名在`min`和`max`之间的`value`,并返回删除的元素个数 * `r.zremrangebyscore(name, min, max)`: 删除`name`中`score`在给定区间的元素,并返回删除的元素个数 #### 8.6.3 获取 * `r.zrank(name, value)`: 返回`name`中元素`value`的排名下标(按`score`从小到大) * `r.zrevrank(name,value)`: 返回`name`中元素`value`的排名下标(按`score`从大到小) * `r.zrange(name, start, end, desc=False, withscores=False, score_cast_func=float)`: 按照索引范围获取`name`对应的有序集合的元素, 从小到大排序 1. `name`: `redis`的`name` 2. `start`: 有序集合索引起始位置(非分数) 3. `end`: 有序集合索引结束位置(非分数) 4. `desc`: 排序规则,默认按照分数从小到大排序 5. `withscores`: 是否获取元素的分数,默认只获取元素的值 6. `score_cast_func`: 对分数进行数据转换的函数 * `r.zrevrange(name, start, end, withscores=False, score_cast_func=float)`: 按照索引范围获取`name`对应的有序集合的元素, (从大到小排序) * `r.zrangebyscore(name, min, max, start=None, num=None, withscores=False, score_cast_func=float)`: 按照分数范围获取name对应的有序集合的元素,从小到大排序 * `zrevrangebyscore(name, max, min, start=None, num=None, withscores=False, score_cast_func=float)`: 按照分数范围获取name对应的有序集合的元素,从大到小排序 * `r.zscan(name, cursor=0, match=None, count=None, score_cast_func=float)`: 分片获取 * `r.zscan_iter(name, match=None, count=None,score_cast_func=float)`: 分片获取 ### 8.7 其他操作 * `r.delete(*names)`: 删除redis中的任意数据类型 * `r.exists(name)`: 检测redis的name是否存在 * `r.keys(pattern='*')`: 根据模型获取redis的name * pattern: 正则表达式 * `r.expire(name ,time)`: 为某个redis的某个name设置超时时间 * `r.rename(src, dst)`: 对redis的name重命名为 * `r.move(name, db)`: 将redis的某个值移动到指定的db下 * `r.randomkey()`: 随机获取一个redis的name(不删除) * `r.type(name)`: 获取name对应值的类型 * `r.scan(cursor=0, match=None, count=None)`: 分片获取name * `scan_iter(match=None, count=None)`:分片获取name ### 8.8 管道 `redis-py`默认在执行每次请求都会创建(连接池申请连接)和断开(归还连接池) 一次连接操作,如果想要在一次请求中指定多个命令, 则可以使用`pipline`实现一次请求指定多个命令, 并且默认情况下一次`pipline` 是原子性操作。 ```python import redis pool = redis.ConnectionPool(host='127.0.0.1', port=6379) r = redis.Redis(connection_pool=pool) # pipe = r.pipeline(transaction=False) pipe = r.pipeline(transaction=True) # 开启事务 pipe.multi() pipe.set('name', 'alex') pipe.set('role', 'sb') pipe.execute() # 执行事务 ``` 只有执行了`execute`才能将`redis`中的数据修改 ### 8.9 Django中使用redis #### 8.9.1 通用方式 自己在使用的位置写代码 #### 8.9.2 django-redis `Django`默认不支持`redis`缓存 **直接使用Django的cache使用** 在`settings.py`(配置文件)添加配置 ```python CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100} # "PASSWORD": "123", } } } ``` **使用redis的链接对象** ```python from django_redis import get_redis_connection conn = get_redis_connection('default') ``` ## 九、接口缓存 将首页轮播图的数据缓存到redis中。首页是没有用户访问时都需要访问,如果首页上 的长期不变化的数据不进行缓存,每一次访问都会查询数据库,造成服务端压力过大 ```python from utils import APIResponse from rest_framework.mixins import ListModelMixin from rest_framework.viewsets import GenericViewSet from django.core.cache import cache from django.conf import settings from lufflyapi.apps.home import models from lufflyapi.apps.home import serializers class BannerView(GenericViewSet, ListModelMixin): queryset = models.Banner.objects.filter(is_delete=False, is_show=True) serializer_class = serializers.BannerModelSerializer def list(self, request, *args, **kwargs): pk = kwargs.get('pk', None) if pk: self.queryset = self.queryset.filter(pk=pk) # 1. 先去缓存获取数据, banner_data = cache.get(settings.BANNER_CACHE_KEY) if not banner_data: # 缓存中没有值,去数据库获取值 response = super(BannerView, self).list(request, *args, **kwargs) cache.set(settings.BANNER_CACHE_KEY, response.data, 604800) return APIResponse(result=response.data) return APIResponse(result=banner_data) ``` ## 十 celery介绍和简单使用 当数据库中的数据被删除时,缓存中的数据没有更新。造成展示页面的数据和 保存的数据不匹配。使用celery来实现定时任务,异步任务 ### 10.1 介绍 Celery是一个简单、灵活且可靠的,处理大量消息的分布式系统 专注于实时处理的异步任务队列 同时也支持任务调度 **应用场景** 1. 异步执行: 解决耗时任务,将耗时操作任务提交给Celery去异步执行,比如发送短信/邮件、消息推送、音视频处理等等 2. 延迟执行: 解决延迟任务 3. 定时执行: 解决周期(周期)任务,比如每天数据统计 * **可以不依赖任何服务器,通过自身命令,启动服务(内部支持socket)** * **celery服务为为其他项目服务提供异步解决任务需求的** ### 10.2 celery异步任务架构 Celery的架构由三部分组成, 1. 消息中间件(message broker) 2. 任务执行单元(worker) 3. 任务执行结果存储(task result store) ![](./.img/celery.jpg) * **消息中间件**: Celery本身不提供消息服务,但是可以方便的和第三方提供的消息中间件集成。包括,RabbitMQ, Redis等等 * **任务执行单元**: Worker是Celery提供的任务执行的单元,worker并发的运行在分布式的系统节点中 * **任务结果存储**: Task result store用来存储Worker执行的任务的结果,Celery支持以不同方式存储任务的结果,包括AMQP, redis等 ### 10.3 celery的两种使用结构 1. 如果 `Celery`对象:`Celery(...)` 是放在一个模块下的 * 终端切换到该模块所在文件夹位置:scripts * 执行启动worker的命令:`celery worker -A 模块名 -l info -P eventlet` 2. 如果 Celery对象:Celery(...) 是放在一个包下的 * 必须在这个包下建一个`celery.py`的文件,将`Celery(...)`产生对象的语句放在该文件中 * 执行启动`worker`的命令:`celery worker -A 包名 -l info -P eventlet` * 注:`windows`系统需要`eventlet`支持,`Linux`与`MacOS`直接执行:`celery worker -A 模块名 -l info` ### 10.4 通过包来管理使用Celery--添加异步任务delay 建立一个`celery_task`包,里面必须要有一个`celery.py`文件 在`celery.py`中生成celery的app ```python from celery_task import Celery broker = 'redis://127.0.0.1:6379/1' # 任务中间件 backend = 'redis://127.0.0.1:6379/2' # 结果存放 app = Celery(__name__, broker=broker, backend=backend, include=['celery_task.task']) ``` 在`celery_task`包中写新建任务`task.py`,在里面写任务 ```python from .celery import app @app.task def add(x, y): print(x, y) return x + y ``` 在`app`中包含任务 在任意要添加任务的位置写下列代码 ```python from celery_task.task import add result = add.delay(3, 4) # 返回值为任务的uuid值。用于获取任务结果 print(result) ``` 在任意要获取结果的位置写下列代码 ```python from celery_task.celery import app from celery_task.result import AsyncResult id = '5f27dc65-493e-48d4-9f03-82546a6b8488' if __name__ == '__main__': async_result = AsyncResult(id=id, app=app) if async_result.successful(): result = async_result.get() print(result) elif async_result.failed(): print('任务失败') elif async_result.status == 'PENDING': print('任务等待中被执行') elif async_result.status == 'RETRY': print('任务异常后正在重试') elif async_result.status == 'STARTED': print('任务已经开始被执行') ``` ### 10.5 添加延迟任务apply_async 时间使用`utc`标准时间 ```python from celery_task.task import add # 添加延迟任务 from datetime import datetime, timedelta eta = datetime.utcnow() + timedelta(seconds=10) # 需要utc时间 result = add.apply_async(args=(5, 10), eta=eta) # args是任务需要的参数 print(result) ``` ### 10.6 添加定时任务 在`celery.py`中配置 ```python from celery_task import Celery broker = 'redis://127.0.0.1:6379/1' # 任务中间件 backend = 'redis://127.0.0.1:6379/2' # 结果存放 app = Celery(__name__, broker=broker, backend=backend, include=['celery_task.task']) app.conf.timezone = 'Asia/Shanghai' # 设置时区 app.conf.enable_utc = False # 禁用utc时间 from datetime import timedelta from celery_task.schedules import crontab app.conf.beat_schedule = { "add_task": { 'task': 'celery_task.task.add', # 指定任务 # 'schedule': timedelta(seconds=5), # 时间间隔 'schedule': crontab(hour=8, day_of_week=1), # 每周一早上8点执行任务 'args': (3, 5) # 任务需要的参数 } } ``` 启动一个beat,用于自动添加任务 `celery -A celery_task beat -l info` ## 十一、首页轮播图的定时更新任务 celery框架在django项目工作流程 1. 加载django配置环境 2. 创建Celery框架对象app,配置broker和backend,得到的app就是worker 3. 给worker对应的app添加可处理的任务函数,用include配置给worker的app 4. 完成提供的任务的定时配置app.conf.beat_schedule 5. 启动celery服务,运行worker,执行任务 6. 启动beat服务,运行beat,添加任务 在项目根目录中新建一个管理`celery`的包`celery_tasks`, 里面必须有一个`celery.py`文件 在这个文件中写celery的配置和application,如下 ```python from celery import Celery # 加载django配置 import os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lufflyapi.settings.debug') broker = 'redis://127.0.0.1:6379/1' # 任务中间件 backend = 'redis://127.0.0.1:6379/2' # 结果存放 app = Celery(__name__, broker=broker, backend=backend, include=['celery_tasks.task']) app.conf.timezone = 'Asia/Shanghai' # 设置时区 app.conf.enable_utc = False # 禁用utc时间 from datetime import timedelta from celery.schedules import crontab app.conf.beat_schedule = { "update_task": { 'task': 'celery_tasks.task.update', # 指定任务 # 'schedule': timedelta(seconds=10), # 时间间隔 'schedule': crontab(hour=8, day_of_week=1), # 每周一早上8点执行任务 # 'args': (4, 6) # 任务需要的参数 } } ``` 在`tasks.py`文件中编写任务, 如下 ```python from celery_tasks.celery import app from django.core.cache import cache from django.conf import settings from home import serializers, models @app.task def update(): banner_queryset = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('-display_order') if not settings.BANNER_COUNT > len(banner_queryset): banner_queryset = banner_queryset[:settings.BANNER_COUNT] banner_list = serializers.BannerModelSerializer(instance=banner_queryset, many=True).data for banner in banner_list: banner['image'] = 'http://127.0.0.1:8000%s' % banner['image'] cache.set(settings.BANNER_CACHE_KEY, banner_list, 60*60*24) print(banner_list) return True ``` 在Windows环境下,启动worker: `celery -A celery_tasks worker -l info -P eventlet` 启动beat: `celery -A celery_tasks beat -l info`。 **Django3.0+celery4.0及以上** 执行任务出现`DatabaseWrapper objects created in a thread can only be used in that same thread.` 异常,将启动worker的命令替换为: `celery -A celery_tasks worker -l info --pool=solo` ## 十二 短信发送接口作为异步任务 修改视图为 ```python @action(methods=['get'], detail=False) def send(self, request, *args, **kwargs): """ 发送验证码接口 """ telephone = request.query_params.get('telephone') code = get_code() from celery_tasks import task result = task.send_sms.delay(telephone, code) # 向celery提交异步任务 return APIResponse(result={"message": "验证码发送成功", 'uuid': str(result)}) ``` 新建一个task任务 ```python @app.task def send_sms(phone, code): """ 发送手机验证码 """ cache.set(settings.CODE_CACHE_KEY % {"telephone": phone}, code, 180) phone = settings.TELEPHONE_HEAD + phone return tencent.send_code(phone, code) ``` ## 十三、课程相关