同步操作将从 custer/django-rest-framework 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
Django rest framework jwt 官方文档
这个项目是 python3.6 环境,要先新建 虚拟环境
conda info --envs # 查看当前所有的虚拟环境
conda create --name VueShop python=3.6
source activate VueShop
pip install -i https://pypi.douban.com/simple django
pip install djangorestframework
pip install markdown # markdown support for the browsable API
pip install django-filter
使用 pycharm 新建 django 项目
修改 settings.py 中的数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'vue_shop',
'USER': 'root',
'PASSWORD': 'root1234',
'HOST': '127.0.0.1',
'OPTIONS': {'init_command': 'SET default_storage_engine=INNODB;'}
}
}
这里 OPTIONS 是为了第三方登录
新建数据库,使用 navicate
此时运行 pycharm 报错
以后我们使用 mysqlclient 而不是 MySQL-python,因此 mysqlclient 可以很好的替换 mysql-python(只支持python2.7)
pip install -i https://pypi.douban.com/simple mysqlclient
pip install -i https://pypi.douban.com/simple pillow
到目前为止,基本需要的包都安装完了,现在整理 项目结构
新建 python package 文件夹,apps 来放置所有的app,把 users 移入
新建 extra_apps 放置第三方的包
media 上传文件图片
db_tools python 脚本文件,数据库初始化等等,有用的外部脚本设置
大概先设置成这个样子,以后根据需要做相应的调整
将 apps 和 extra_apps Mark Directory as Sources Root. import时候可以带来很多方便
将 apps 和 extra_apps 加入到根搜索路径之下 setting.py 修改
import os
import sys
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE_DIR)
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
sys.path.insert(0, os.path.join(BASE_DIR, 'extra_apps'))
通过需求分析,然后设计app 数据表
app 设计思想:归类 - 商品类别信息、购物车管理、订单信息、交易管理、用户,用户操作
然后把 goods 移入到 apps 里面
根据每个 app 设计 model
第一步要设计的 model 是哪一个 app 的
用户的 model 扩展字段,继承 AbstractUser
from datetime import datetime
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class UserProfile(AbstractUser):
""" 用户 """
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="姓名")
birthday = models.DateField(null=True, blank=True, verbose_name="出生年月")
mobile = models.CharField(max_length=6, choices=(("male", "男"), ("female", "女")), default="male", verbose_name="性别")
gender = models.CharField(max_length=11, verbose_name="电话")
email = models.CharField(max_length=100, null=True, blank=True, verbose_name="邮箱")
class Meta:
verbose_name = "用户"
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class VerifyCode(models.Model):
""" 短信验证码 """
code = models.CharField(max_length=20, verbose_name="验证码")
mobile = models.CharField(max_length=11, verbose_name="电话")
send_time = models.DateTimeField(default=datetime.now, verbose_name="发送时间")
class Meta:
verbose_name = "短信验证码"
verbose_name_plural = verbose_name
def __str__(self):
return self.code
setting.py 里放入这句话才会替换系统用户
为什么要前后端分离
前后端分离缺点
restful api
restful api 目前是前后端分离最佳实践 (标准)
restful api 重要概念
总结一下什么是RESTful架构:
(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务) 204 NO CONTENT - [DELETE]:用户删除数据成功。 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
几个概念
通过学习 Django-rest-framework 来让我们快速的搭建 restful api 本章是所有的起点,很重要的章节
首先通过 django 实现一个 api 或者 json 的返回
cbv 基于 class base view - 官方推荐编码方式,代码可重用性高一些 fbv 基于 function base view
url设计规范最好是名词复数 goods
from django.conf.urls import url
# from django.contrib import admin
urlpatterns = [
# url(r'^admin/', admin.site.urls),
# 商品列表页
url(r'goods/$', )
]
为了区分和 django-rest-framework 的区别,新建 views_base.py 文件
这里书写通过 django 的 view 方式,来完成 json 的返回
from django.views.generic.base import View
django cbv 方式的最常见的最底层的 View
django 的进阶 - 看官方文档
# -*- coding: utf-8 -*-
from django.views.generic.base import View
# from django.views.generic import ListView
from .models import Goods
class GoodListView(View):
def get(self, request):
"""
通过 django 的 view 实现商品列表页
:param request:
:return:
"""
json_list = []
goods = Goods.objects.all()[:10]
for good in goods:
json_dict = {}
json_dict["name"] = good.name
json_dict["category"] = good.category.name
json_dict["market_price"] = goods.market_price
json_list.append(json_dict)
from django.http import HttpResponse
import json
return HttpResponse(json.dumps(json_list), content_type="application/json")
json_list 通过 json.dumps 做序列化,要返回json必须要指明 content_type="application/json"
from django.conf.urls import url
# from django.contrib import admin
from goods.views_base import GoodsListView
urlpatterns = [
# url(r'^admin/', admin.site.urls),
# 商品列表页
url(r'goods/$', GoodsListView.as_view(), name="goods-list")
]
配置完成之后就可以来启动啦 浏览器显示 json 排版可以使用 JSONView 插件
这就通过 django 的 view 简单的完成了商品列表页,自己序列化数据,然后返回一个 json 样式
from django.forms.models import model_to_dict
for good in goods:
json_dict = model_to_dict(good)
json_list.append(json_dict)
ImageFieldFile DateTimeField 等等是不能做序列化的
如何才能将这些不同类型的字段给做序列化呢
另外一种方法
import json
from django.core import serializers
json_data = serializers("json", goods)
json_data = json.loads(json_data)
from django.http import HttpResponse
return HttpResponse(json.dumps(json_data), content_type="application/json")
这就是 django 提供的 serializer 序列化
上面 json.load() 和 json.dumps() 是相反的操作,修改代码
import json
from django.core import serializers
json_data = serializers("json", goods)
from django.http import HttpResponse
return HttpResponse(json_data, content_type="application/json")
也可以直接用 JsonResponse
import json
from django.core import serializers
json_data = serializers("json", goods)
json_data = json.loads(json_data) # 这里转换成 dict
from django.http import JsonResponse # json.dumps() 转换成 字符串
return JsonResponse(json_data, safe=False)
django-rest-framework 基础知识,现在在 views.py 中书写
结合 官方文档 和代码实例
pip install coreapi django-guardian
coreapi 支持 rest 文档的第三方包
django-guardian 对象级别的权限
coreapi 引入之后就可以使用 drf 提供的文档功能
在 url 中配置
from rest_framework.documentation import include_docs_urls
url(r'docs/', include_docs_urls(title="牧学生鲜"))
from django.conf.urls import url
from rest_framework.documentation import include_docs_urls
from goods.views_base import GoodListView
urlpatterns = [
# 商品列表页
url(r'goods/$', GoodListView.as_view(), name="goods-list"),
url(r'docs/', include_docs_urls(title="牧学生鲜")),
]
现在打开官方文档,根据官方文档来写 views.py
按照文档配置了 setting.py 和 urls.py 之后,看文档Tutorial 3: Class-based Views
这个例子介绍 简单快速的来写一个 list
序列化类,我们可以自己定义序列化类,这个序列化类和form、modelform
modelform可以直接将字段转成html
drf 的 serializer 实际上取代 django 的 form 开发的
form 开发是针对 html 的。 serializer 是针对 json 的
serializer 的定义和之前 form 一样,新建一个 serializers.py 文件
# -*- coding: utf-8 -*-
from rest_framework import serializers
class GoodsSerializer(serializers.Serializer):
"""
新建一个序列化对象来映射每一个字段,当返回数据或者post数据的时候
可以直接通过 serializer 保存到数据库中
和 form 的功能相类似,专门用于 json 中的
"""
name = serializers.CharField(required=True, max_length=100)
click_num = serializers.IntegerField(default=0)
然后在返回 views.py
from .serializers import GoodsSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Goods
class GoodstListView(APIView):
""" List all goods """
def get(self, request, format=None):
goods = Goods.objects.all()[:10]
goods_serializer = GoodsSerializer(goods, many=True)
return Response(goods_serializer.data)
然后配置 urls.py
from goods.views import GoodsListView
url(r'goods/$', GoodsListView.as_view(), name="goods-list"),
上面介绍了 drf 的 APIView 方式来实现商品列表页的返回,这里已经引出了两个非常重要的概念
一个是 serializer 序列化,一个是 APIView
现在来继续完善我们的代码
# -*- coding: utf-8 -*-
from rest_framework import serializers
class GoodsSerializer(serializers.Serializer):
"""
新建一个序列化对象来映射每一个字段,当返回数据或者post数据的时候
可以直接通过 serializer 保存到数据库中
和 form 的功能相类似,专门用于 json 中的
"""
name = serializers.CharField(required=True, max_length=100)
click_num = serializers.IntegerField(default=0)
goods_front_image = serializers.ImageField()
serializer 拿到字段之后也是可以做保存的,把字段保存到数据库当中
在 serializer 中覆盖 create 方法
from goods.models import Goods
....
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.
"""
return Goods.objects.create(**validated_data)
这个 validated_data, 他会将上面的 name\click_num\goods_front_image这些字段全部放进来
Goods 是 django 的 model 对象,objects 实际上是 django 的一个管理器,它有 create 函数
直接通过 **validated_data 这种方式,直接 create Goods 这个对象
这里重载了 create 函数,通过 Goods.objects.create(**validated_data) 可以将前端传递的json数据,通过 serializer 来验证
from rest_framework import status
def post(self, request, format=None):
serializer = GoodsSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
上面的 request 是 drf 封装的 request 不是 django 的 request 了
if serializer.is_valid():
这是根据 serializer 定义的字段和属性来验证的
serializer = GoodsSerializer(data=request.data)
django 的 request 是没有 data 这个属性的,drf 的封装
serializer.save()
save() 调用的是 serializer 中的重载的 create 方法
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
HTTP_201_CREATED = 201
HTTP_202_ACCEPTED = 202
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206
HTTP_207_MULTI_STATUS = 207
HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302
HTTP_303_SEE_OTHER = 303
HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404
HTTP_405_METHOD_NOT_ALLOWED = 405
HTTP_406_NOT_ACCEPTABLE = 406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
HTTP_408_REQUEST_TIMEOUT = 408
HTTP_409_CONFLICT = 409
HTTP_410_GONE = 410
HTTP_411_LENGTH_REQUIRED = 411
HTTP_412_PRECONDITION_FAILED = 412
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_414_REQUEST_URI_TOO_LONG = 414
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
HTTP_417_EXPECTATION_FAILED = 417
HTTP_422_UNPROCESSABLE_ENTITY = 422
HTTP_423_LOCKED = 423
HTTP_424_FAILED_DEPENDENCY = 424
HTTP_428_PRECONDITION_REQUIRED = 428
HTTP_429_TOO_MANY_REQUESTS = 429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_501_NOT_IMPLEMENTED = 501
HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
HTTP_507_INSUFFICIENT_STORAGE = 507
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
ModelSerializer 和 ModelForm 是一样的
让我们省去了所有字段的添加
查看官方文档Using Modelserializers
使用 ModelSerializer 之前
# -*- coding: utf-8 -*-
from rest_framework import serializers
from goods.models import Goods
class GoodsSerializer(serializers.Serializer):
"""
新建一个序列化对象来映射每一个字段,当返回数据或者post数据的时候
可以直接通过 serializer 保存到数据库中
和 form 的功能相类似,专门用于 json 中的
"""
name = serializers.CharField(required=True, max_length=100)
click_num = serializers.IntegerField(default=0)
goods_front_image = serializers.ImageField()
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.
"""
return Goods.objects.create(**validated_data)
使用 ModelSerializer 之后
# -*- coding: utf-8 -*-
from rest_framework import serializers
from goods.models import Goods
class GoodsSerializer(serializers.ModelSerializer):
class Meta:
model = Goods
fields = ('name', 'click_num', 'market_price', 'add_time')
逻辑更加简单,因为它是通过 model 直接实现的映射
views.py 就是之前简单的代码,就可以实现了序列化
from .serializers import GoodsSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Goods
class GoodsListView(APIView):
""" List all goods """
def get(self, request, format=None):
goods = Goods.objects.all()[:10]
goods_serializer = GoodsSerializer(goods, many=True)
return Response(goods_serializer.data)
Goods 里面很多字段,使用 "all" 可以直接取出所有字段
from rest_framework import serializers
from goods.models import Goods
class GoodsSerializer(serializers.ModelSerializer):
class Meta:
model = Goods
fields = "__all__"
不管什么类型的字段序列化成字符串都不会出错,如果想获得完整的外键信息
得再实现外键的 serializer,然后嵌套 序列化就可以了
from rest_framework import serializers
from goods.models import Goods, GoodsCategory
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = GoodsCategory
fields = "__all__"
class GoodsSerializer(serializers.ModelSerializer):
category = CategorySerializer()
class Meta:
model = Goods
fields = "__all__"
用 category 就是外键的实例化来覆盖默认的
这样就可以非常的简单的完成它的序列化,代码非常的少,序列化的嵌套也可以很好的完成
本节课不在使用 APIView 来实现,使用更加上层的 View
使用 mixins 和 GenericAPIView 来使代码变的更加简洁
GenericAPIView 是非常重要的和使用相当多的 View,继承自APIView 封装了 分页等许多功能
from .serializers import GoodsSerializer
from rest_framework import mixins
from rest_framework import generics
from .models import Goods
class GoodsListView(mixins.ListModelMixin, generics.GenericAPIView):
""" 商品列表页 """
queryset = Goods.objects.all()[:10]
serializer_class = GoodsSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
不重载get ,前端会返回 GET 不被允许
不管是继承 GenericAPIView 还是继承 APIView, 不去重写 get、post、delete、...、
View会默认不接收这种请求,它会返回方法错误
查看源码步骤:
可以查看到所有我们可以继承的 APIView
和我们上面的代码完全相同,所以我们可以直接继承它
ListAPIView, 可以看到它的实现很简单,帮我们继承两个类,实现一个get函数
from .serializers import GoodsSerializer
from rest_framework import generics
from .models import Goods
class GoodsListView(generics.ListAPIView):
""" 商品列表页 """
queryset = Goods.objects.all()[:10]
serializer_class = GoodsSerializer
代码已经这么少了
这里是实现商品列表页的代码
serializers.py
# -*- coding: utf-8 -*-
from rest_framework import serializers
from goods.models import Goods, GoodsCategory
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = GoodsCategory
fields = "__all__"
class GoodsSerializer(serializers.ModelSerializer):
category = CategorySerializer()
class Meta:
model = Goods
fields = "__all__"
views.py
from .serializers import GoodsSerializer
from rest_framework import generics
from .models import Goods
class GoodsListView(generics.ListAPIView):
""" 商品列表页 """
queryset = Goods.objects.all()[:10]
serializer_class = GoodsSerializer
分页所以要取所有商品
queryset = Goods.objects.all()
REST_FRAMEWORK 有一个总的配置文件
查找源代码
在我们的配置文件 settings.py, 最后添加全局配置,就可以了
REST_FRAMEWORK = {
'PAGE_SIZE': 10,
}
如何定制分页,查看官方文档 api guide pagniation
from .serializers import GoodsSerializer
from rest_framework import generics
from rest_framework.pagination import PageNumberPagination
from .models import Goods
class GoodsPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
page_query_param = 'p'
max_page_size = 100
class GoodsListView(generics.ListAPIView):
""" 商品列表页 """
queryset = Goods.objects.all()[:10]
serializer_class = GoodsSerializer
pageination_class = GoodsPagination
设置之后 settings.py 中的设置就可以注释了
# REST_FRAMEWORK = {
# 'PAGE_SIZE': 10,
# }
最重要的 APIView
from rest_framework import viewsets
ViewSet GenericViewSet ModelViewSet
看下 GenericViewSet 它继承两个 ViewSetMixin, 和 GenericAPIView
ViewSetMixin 重写了 as_view 方法 - 使得注册 url 变得更加简单
initialize_request - 在view 上设置很多 action 属性
动态设置 serializer 有很大的好处
GenericViewSet 继承了 GenericAPIView , GenericAPIView 本身没有定义 get post 等方法
所以还需要用到之前的 mixins
现在 urls.py 就需要另外一种配置方法了
from goods.views import GoodsListViewSet
goods_list = GoodsListViewSet.as_view({
'get': 'list'
})
get 请求绑定到 list 方法,和之前的代码相类似
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
之前重载 get 请求,转到 list 方法,现在直接通过 as_view() 函数配置就可以了
from django.conf.urls import url, include
from rest_framework.documentation import include_docs_urls
from goods.views import GoodsListViewSet
goods_list = GoodsListViewSet.as_view({
'get': 'list'
})
urlpatterns = [
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'goods/$', goods_list, name="goods-list"),
url(r'docs/', include_docs_urls(title="b")),
]
这个是 get 绑定到 list ,但是有了 router 配置 URL 就会更加的简单
from django.conf.urls import url, include
# from django.contrib import admin
from rest_framework.documentation import include_docs_urls
from rest_framework.routers import DefaultRouter
from goods.views import GoodsListViewSet
router = DefaultRouter()
# 配置 goods 的 url
router.register(r'goods', GoodsListViewSet)
urlpatterns = [
# url(r'^admin/', admin.site.urls),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^', include(router.urls)),
url(r'docs/', include_docs_urls(title="b")),
]
要会使用 ViewSet,而不至于很混乱,搞清楚他们之间的关系
很好的决定使用哪一个 view 或者 组合他们使用
GenericViewSet (viewset) drf
继承自 View django
差异的核心点在于 mixins
CreateModelMixin
ListModelMixin
RetrieveModelMixin - 获取具体的信息,get_object() 详情
UpdateModelMixin - 部分更新还是全部更新
DestroyModelMixin - 连接delete 方法
之前说过如果不继承 ListModelMixin 的话,就无法将 get 和 list 连接起来
class ListModelMixin(object):
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
查看 GenericAPIView 源码,封装实现的功能
filter_backends - 过滤功能 pagination_class - 分页 serializer_class - 序列化
GenericAPIView 与 各种 mixins 的组合 :
generics.py
class ListAPIView(mixins.ListModelMixin,
GenericAPIView):
"""
Concrete view for listing a queryset.
"""
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
viewsets.py
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
"""
The GenericViewSet class does not provide any actions by default,
but does include the base set of generic view behavior, such as
the `get_object` and `get_queryset` methods.
"""
pass
差别
本身写在代码中的绑定,可以在 url 中实现动态的绑定,或者通过 router 配置
request - 浏览器发送请求过来之后,drf会对它做一定的封装
request parsing 用户发送数据过来之后,drf 会对它做一定的解析
request.data = request.POST + request.FILES
它包括所有解析的内容,文件和非文件的 input,解析了http 方法的内容
.query_params - get 请求参数
.parsers - 解析器 解析传递过来各种类型的数据
content negotiation
authentication
.user 获取到当前的用户
Responses - 根据前端返回的请求,返回 html 或者 json
通过 drf 提供的过滤的功能,简单快速的完成我们的过滤
drf 中 APIView 提供了get_queryset() 方法,允许我们对 queryset() 返回加一定的逻辑
class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
""" 商品列表页 """
serializer_class = GoodsSerializer
pageination_class = GoodsPagination
def get_queryset(self):
return Goods.objects.filter(shop_price__gt=100)
class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
""" 商品列表页 """
serializer_class = GoodsSerializer
pageination_class = GoodsPagination
def get_queryset(self):
queryset = Goods.objects.all()
price_min = self.request.query_params.get("price_min", 0)
if price_min:
queryset = queryset.filter(shop_price__gt=int(price_min))
return queryset
列表页常有的功能
过滤 - 精确字段过滤 - DjangoFilterBackend
搜索 - SearchFilter
排序 - OrderingFilter
pip install django-filter
django-filter 要加入到 settings.py 的 INSTALLED_APPS 中
然后 import 进来
from django_filters.rest_framework import DjangoFilterBackend
在 view 中完成 filter_backends 的设置
filter_backends = (DjangoFilterBackend,)
配置 filter_fields
filter_fields = ('name', 'shop_price')
这些代码就完成了我们的过滤了
from .serializers import GoodsSerializer
from rest_framework import mixins
from rest_framework.pagination import PageNumberPagination
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from .models import Goods
class GoodsPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
page_query_param = 'p'
max_page_size = 100
class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
""" 商品列表页 """
queryset = Goods.objects.all()
serializer_class = GoodsSerializer
pageination_class = GoodsPagination
filter_backends = (DjangoFilterBackend, )
filter_fields = ('name', 'shop_price')
f5 刷新页面,页面的变化
过滤的搜索字段要完全相同,不能进行模糊搜索、搜索区间等等
要完成这些功能,首先查看 django_filter 的 github 地址,查看官方文档
我们新建一个filters.py
import django_filters
from .models import Goods
class GoodsFilter(django_filters.rest_framework.FilterSet):
""" 商品的过滤类 """
min_price = django_filters.NumberFilter(name="shop_price", lookup_expr='gte')
max_price = django_filters.NumberFilter(name="shop_price", lookup_expr='lte')
class Meta:
model = Goods
fields = ['min_price', 'max_price']
gte 大于等于 >=
lte 小于等于 <=
然后 修改 filter_fields 为 filter_class
from .filters import GoodsFilter
...
filter_class = GoodsFilter
name 的模糊查询 -
name = django_filters.NumberFilter(name='name', lookup_expr='contains')
忽略大小写的话,前面加一个 i - icontains
然后把 name 配置进 fields 里面
import django_filters
from .models import Goods
class GoodsFilter(django_filters.rest_framework.FilterSet):
""" 商品的过滤类 """
min_price = django_filters.NumberFilter(name="shop_price", lookup_expr='gte')
max_price = django_filters.NumberFilter(name="shop_price", lookup_expr='lte')
name = django_filters.CharFilter(name="name", lookup_expr='icontains')
class Meta:
model = Goods
fields = ['min_price', 'max_price', 'name']
不指定 lookup_expr='contains' 就是完全匹配
进一步做商品的搜索:
首先引入 rest_framework 的 filters
from rest_framework import filters
然后在 view 函数里面配置
filter_backends = (DjangoFilterBackend, filters.SearchFilter)
search_fields = ('name', 'goods_brief', 'goods_desc')
这样就可以直接使用了
首先配置 filters.OrderingFilter
然后配置 ordering_fields = ('username', 'email')
class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
""" 商品列表页,分页,搜索,过滤,排序 """
queryset = Goods.objects.all()
serializer_class = GoodsSerializer
pageination_class = GoodsPagination
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
# filter_fields = ('name', 'shop_price')
filter_class = GoodsFilter
search_fields = ('name', 'goods_brief', 'goods_desc')
ordering_fields = ('sold_num', 'add_time')
这短短的几行代码完成了,列表页、分页、搜索、过滤、排序
先完善商品类别的功能
serializers.py
class CategorySerializer(serializers.ModelSerializer):
""" 商品类别序列化 """
class Meta:
model = GoodsCategory
fields = "__all__"
views.py
class CategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
list:
商品分类列表
"""
# queryset = GoodsCategory.objects.all()
queryset = GoodsCategory.objects.filter(category_type=1)
serializer_class = CategorySerializer
urls.py
# 配置 categorys 的 url
router.register(r'categorys', CategoryViewSet, base_name="categorys")
修改商品分类数据层次结构
获取第一类 商品分类
queryset = GoodsCategory.object.filter(category_type=1)
怎么将第二类数据添加到一类数据里面呢,需要重写 category serializer
通过一类拿到二类的数据,
通过 models.py 中定义的字段 parent_category 设置的 related_name="sub_cat" 属性
class CategorySerializer2(serializers.ModelSerializer):
""" 商品类别序列化 """
class Meta:
model = GoodsCategory
fields = "__all__"
class CategorySerializer(serializers.ModelSerializer):
""" 商品类别序列化 """
sub_cat = CategorySerializer2(many=True)
class Meta:
model = GoodsCategory
fields = "__all__"
CategorySerializer 是一类
Categoryserializer2 是二类, 把二类嵌套到一类里面,一定要设置 many=True
三类的话,就再嵌套一层
class CategorySerializer3(serializers.ModelSerializer):
""" 商品类别三层序列化 """
class Meta:
model = GoodsCategory
fields = "__all__"
class CategorySerializer2(serializers.ModelSerializer):
sub_cat = CategorySerializer3(many=True)
""" 商品类二层别序列化 """
class Meta:
model = GoodsCategory
fields = "__all__"
class CategorySerializer(serializers.ModelSerializer):
""" 商品类别序列化 """
sub_cat = CategorySerializer2(many=True)
class Meta:
model = GoodsCategory
fields = "__all__"
三层的序列化
获取某一个商品的详情,只需要要继承 mixins.RetrieveModelMixin 就可以了
将商品分类列表数据和前端 view 做调试
调试之前要解决跨域的问题,跨域问题在前后端分离开发当中,非常的常见
前端通过npm 设置 proxy 代理也可以解决跨域问题
这里重点讲解服务器解决跨域的方法,搜索 github django cors headers
安装包
pip install django-cors-headers
完成之后,根据文档做相应的配置
INSTALLED_APPS = (
...
'corsheaders',
...
)
MIDDLEWARE = [ # Or MIDDLEWARE_CLASSES on Django < 1.10
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
CorsMiddleware should be placed as high as possible, especially before any middleware that can generate responses such as Django's CommonMiddleware or Whitenoise's WhiteNoiseMiddleware. If it is not before, it will not be able to add the CORS headers to these responses.
Also if you are using CORS_REPLACE_HTTPS_REFERER it should be placed before Django's CsrfViewMiddleware (see more below).
尽量放在 CsrfViewMiddleware 之前,我们把它简单的放在第一个
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
CORS_ORIGIN_ALLOW_ALL = True
然后设置 CORS_ORIGIN_ALLOW_ALL = True
这样就可以将后台传递过来的 category 数据,显示到前端 导航栏和 全部商品分类栏 中
前后分离的系统,不需要做 csrf 验证,实际上已经跨域了
查看 drf 官方文档 API Guide Authentication
在 settings.py 配置 REST_FRAMEWORK 变量
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}
看上面两个 middleware ,django 默认配置里面 也有两个
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
django.contrib.sessions.middleware.SessionMiddleware',
django.contrib.auth.middleware.AuthenticationMiddleware',
这两个的 middleware 的作用是每当有 request 的时候,
这两个 middleware 就会把 request 里的 cookie、 session id 转换成我们的 user
查看 drf 的 SessionAuthentication 源码:
重点核心在 TokenAuthentication
首先配置 installed_apps
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)
authtoken 会给我们建一张表的,所以首先要放到 installed_app 里面来
否者 makemigration 的时候。不会给我们生成表的
python manage.py makemigrations
python manage.py migrate
需要为 user 创建 token - token 和 user 应该是 一一对应的
token 需要我们自己创建
首页要 先 添加 url 设置
from rest_framework.authtoken import views
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token)
]
这是使用 firefox 附加组件中的 httprequester 插件,调试
返回的是 user = {AnonymouseUser} 匿名用户,就是说没有取到用户
按照官方文档,已经都做了,还是没有取到用户,默认设置就是这两种 middleware
现在我们用到了 token 的认证方式,所有我们要把 token 认证 放进来
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
)
}
一定要把这个 'rest_framework.authentication.TokenAuthentication',
TokenAuthentication 放进来,要理解到 Authentication_Class 的作用,
这里设置 authentication_class 和 django 的 middleware 一样
在将我们的request 映射到view 之前,Django 和 drf 都会调用 这个类
优先调用这个类里的方法
这个方法会讲 user 放入到我们的 request 当中去
django 从请求到响应的整个逻辑,查看 Django 的源码
这个 process_request 和 process_response 需要注意
在 settings.py 注册的 middleware 都可以重载 process_request 和 process_response
在我们将用户的request 提交给后台 view 之前会调用 所有在 settings.py 配置的 middleware 找
process_request,统统调用一遍
token 认证模式在前后端分离中使用比较常见的,但是 drf token 有很大的问题
token 是保存到服务器当中的,如果是分布式系统的话,或者说有两套系统想用同一套认证系统的话 需要用户同步,实际上是比较麻烦的
这个token 是永久有效的,它没有过期时间,一旦泄露了,别人可以一直拿来用
上面介绍了 drf token 登录机制,
下面介绍不管 token 认证还是其他认证都容易出现的 坑
还是这里配置了全局的 TokenAuthentication
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
)
}
这个 TokenAuthentication 会对 token 进行验证,如果验证失败的话,它是会抛异常的
认证失败 返回的是 401 Unauthorized 认证令牌无效
比如说 商品列表页,这样公开的数据,不用用户一定要登录
我们可以不配置全局的token 认证,可以在 view 里来做认证
把 'rest_framework.authentication.TokenAuthentication',
删除
在 views.py 中做认证,如果一个 view 接口需要做认证的话,放到 view 里
from rest_framework.authentication import TokenAuthentication
...
authentication_classes = (TokenAuthentication,)
github 搜索 django rest framework jwt 可以查看它的官方文档
首先要进行安装 pip install djangorestframework-jwt
然后进行配置
In your settings.py, add JSONWebTokenAuthentication to Django REST framework's DEFAULT_AUTHENTICATION_CLASSES.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
JSONWebTokenAuthentication 将用户 POST 过来 token 进行验证,通过的话将 user 取出来
需要在 URL 中进行配置
In your urls.py add the following URL route to enable obtaining a token via a POST included the user's username and password.
from rest_framework_jwt.views import obtain_jwt_token
#...
urlpatterns = [
'',
# ...
url(r'^api-token-auth/', obtain_jwt_token),
]
后台和前端保持一致,把后台 url 改成 login
# jwt 的认证接口
url(r'^login/', obtain_jwt_token),
obtain_jwt_token 它继承的是Django 的 auth 认证的方法,默认是用户名 密码登录的
是不支持手机号登录的,所以我们呀自定义 django 的用户认证函数
首先在 settings.py 设置 AUTHENTICATION_BACKENDS 变量
AUTHENTICATION_BACKENDS = (
'',
)
这里定义的类可以写到 users apps 里的 views.py
from django.shortcuts import render
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q
User = get_user_model()
class CustomBackend(ModelBackend):
""" 自定义用户验证 """
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username)|Q(email=username))
if user.check_password(password):
return user
except Exception as e:
return None
自定义用户验证一定要继承 ModelBackend,然后重写 authenticate 函数
查询用户使用 username email 或者 mobile
然后配置到 authentication_backend
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',
)
JWT 有很多设置,如何设置?,这里主要讲两个一个是
JWT_EXPIRATION_DELTA 过期时间
首先设置一个全局变量 JWT_AUTH
第二个是 JWT_AUTH_HEADER_PREFIX 使用默认设置 JWT 就可以了
import datetime
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
实现 drf 用户手机注册的功能
用户注册的功能大量用到 serializer 高级用法
首先通过前端页面分析,手机注册需要提供哪些接口
免费获取验证码 - 专门发送短信的接口
用户在回填短信验证码之后,表单提交的验证
验证失败的提示
验证成功的跳转
首先实现手机号码发送验证码的功能 - 使用第三方服务 云片网
这个 APIKEY 后面要用到,很重要
发送短信首先要申请签名
签名需要审核的,还需要新增模版
现在新建一个 python package 文件夹 utils
然后新建一个 python 文件 yunpian.py
# -*- coding: utf-8 -*-
import requests
class YunPian(object):
def __init__(self, api_key):
self.api_key = api_key
self.single_send_url = "https://sms.yunpian.com/v2/sms/single_send.json"
def send_sms(self, code, mobile):
parmas = {
"apikey": self.api_key,
"mobile": mobile,
"text": "【少儿可视化编程】{code}(#YaK#手机验证码,请完成验证),如非本人操作,请忽略本短信".format(code=code)
}
response = requests.post(self.single_send_url, data=parmas)
import json
re_dict = json.loads(response.text)
print(re_dict)
if __name__ == "__main__":
yunpian = YunPian("103b5a7cece5fe72d9f888fb36abc0fe")
yunpian.send_sms("2017", "18801796642")
在设置 系统设置 IP 白名单, 一定要把本地 IP 地址,或者服务器 IP 地址设置进来
因为发送短信验证码是用户操作所以在 users viws.py 里添加代码:
实际上我们之前在 models 里,设计了 VerifyCode 这张表,
发送短信验证码我们实际可以看作是对这张表进行操作 - 所以是 create 操作
from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
...
class SmsCodeViewSet(CreateModelMixin, viewsets.GenericViewSet):
""" 发送短信验证码 """
再继续写下面的逻辑之前,先理清需求
用户传递过来电话号码,需要对它做验证:
serializer 实际上和 django 的 form 或 ModelForm 是一样的
所以验证就放到 serializer 里面做,先新建一个 serializers.py
这里我们用 serializers.Serializer 而不是用 serializers.ModelSerializer 去和 VerifyCode 这张表做关联呢? 因为发送验证码只需要提供一个手机号码就可以了,而 VerifyCode 这张表 code 是必填字段
# 手机号码正则表达式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"
# -*- coding: utf-8 -*-
import re
from datetime import datetime
from datetime import timedelta
from rest_framework import serializers
from django.contrib.auth import get_user_model
from MxShop.settings import REGEX_MOBILE
from .models import VerifyCode
User = get_user_model()
class SmsSerializer(serializers.Serializer):
mobile = serializers.CharField(max_length=11)
def validate_mobile(self, mobile):
""" 验证手机号码 """
# 手机是否注册
if User.objects.filter(mobile=mobile).count():
raise serializers.ValidationError("用户已经存在")
# 验证手机号码是否合法
if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手机号码非法")
# 验证发送频率
one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
if VerifyCode.objects.filter(send_time__gt=one_mintes_ago, mobile=mobile).count():
raise serializers.ValidationError("距离上次发送未超过60秒")
return mobile
现在通过 serializer 的验证,可以继续写 view 逻辑了
from .serializers import SmsSerializer
...
class SmsCodeViewSet(CreateModelMixin, viewsets.GenericViewSet):
""" 发送短信验证码 """
serializer_class = SmsSerializer
导入 SmsSerializer 之后,开始重写 Create 方法
from rest_framework.response import Response
from rest_framework import status
...
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
注意 serializer.is_valid(raise_exception=True)
如果 serializer 调用失败的话,就不会继续走下去了,直接抛异常,drf 捕捉到 400
from django.shortcuts import render
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q
from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from random import choice
from .serializers import SmsSerializer
from utils.yunpian import YunPian
from MxShop.settings import APIKEY
from .models import VerifyCode
# Create your views here.
User = get_user_model()
class CustomBackend(ModelBackend):
""" 自定义用户验证 """
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username) | Q(mobile=username))
if user.check_password(password):
return user
except Exception as e:
return None
class SmsCodeViewSet(CreateModelMixin, viewsets.GenericViewSet):
""" 发送短信验证码 """
serializer_class = SmsSerializer
def generate_code(self):
""" 生成四位数字的验证码 """
seeds = "1234567890"
random_str = []
for i in range(4):
random_str.append(choice(seeds))
return "".join(random_str)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
mobile = serializer.validated_data["mobile"]
yun_pian = YunPian(APIKEY)
code = self.generate_code()
sms_status = yun_pian.send_sms(code=code, mobile=mobile)
if sms_status["code"]!=0:
return Response({
"mobile": sms_status["msg"]
}, status=status.HTTP_400_BAD_REQUEST)
else:
code_record = VerifyCode(code=code, mobile=mobile)
code_record.save()
return Response({
"mobile": mobile
}, status=status.HTTP_201_CREATED)
设置 url
from users.views import SmsCodeViewSet
router.register(r'codes', SmsCodeViewSet, base_name="codes")
手机短信发送的一个接口 发送短信成功
完成注册功能
首先看下注册页面的分析,要输入手机号码、验证码、密码 三个字段
需要这三个字段编写后台的 注册 接口,之前说过 Django 的 form 和 ModelForm 是用来验证用户提交字段的合法性的,所以这里我们要首先写一个 viewset
restful api 规范 url 实际上对应的是对资源的操作,那现在注册的资源是什么?是用户
可以理解成 post 一个用户到后台数据库里,所以是对 User 的操作
所以新建一个 UserViewSet 类
class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
""" 用户 """
现在写 用户提交的验证 serialzier
class UserRegSerializer(serializers.ModelSerializer):
为什么这里又使用了 ModelSerializer 呢?
之前 SmsSerializer 发送短信验证码的时候没有使用 ModelSerializer 是因为 VerifyCode 里面的 code 是必填字段,而前端并没有给我们传递,所以不太适合用 ModelSerializer
而 UserRegSerializer 前端给我们 POST 了一个 额外的 code 字段,所以它现在又多了一个字段,那我们为什么还能用 ModelSerializer 呢?
处理的时候注意观察下,虽然 ModelSerializer 有很多限制,但是我们可以使用很多技巧,来突破它的一些限制,就可以随机应变,这样既能享受到 ModelSerializer 给我们带来的好处,然后我们又能突破它的一些限制
class UserRegSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("username", )
这里注意下 User 是我们延伸的 UserProfile 字段,它是继承 Django 自带的 user
所以 username 是必填的字段,所以一定要将 username 给保存过来
第二个就是我们的 code,⚠️注意⚠️ code 在我们 UserPrifile 里面并没有定义这个字段
所以 code 是我们自己添加的字段
class UserRegSerializer(serializers.ModelSerializer):
code = serializers.CharField(required=True, max_length=4, min_length=4)
class Meta:
model = User
fields = ("username", "code", )
为了更好的操作 ModelSerializer,我们修改 mobile 字段,把它更改为不是必填字段,可以为空
我们将它传过来的 username ,自己给它放到 mobile 里面,这只是为了掩饰,
比较好的习惯是用户将 username 和 mobile 都 POST 过来,这样后台操作起来就很简单
class UserProfile(AbstractUser):
""" 用户 """
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="姓名")
birthday = models.DateField(null=True, blank=True, verbose_name="出生年月")
mobile = models.CharField(null=True, blank=True, max_length=6, choices=(("male", "男"), ("female", "女")), default="male", verbose_name="性别")
gender = models.CharField(max_length=11, verbose_name="电话")
email = models.CharField(max_length=100, null=True, blank=True, verbose_name="邮箱")
class Meta:
verbose_name = "用户"
verbose_name_plural = verbose_name
def __str__(self):
return self.username
这样整个 serializer 就设置完成了
class UserRegSerializer(serializers.ModelSerializer):
code = serializers.CharField(required=True, max_length=4, min_length=4)
class Meta:
model = User
fields = ("username", "code", "mobile")
有了 ModelSerializer 我们就可以继续来验证里面的某些字段, serializer - 验证字段
首先需要验证的是 code
class UserRegSerializer(serializers.ModelSerializer):
code = serializers.CharField(required=True, max_length=4, min_length=4)
def validate_code(self, code):
class Meta:
model = User
fields = ("username", "code", "mobile")
首先想下验证码错误有多少种情况
def validate_code(self, code):
verify_records = VerifyCode.objects.filter()
注意这个 filter 首先要获取这个短信,code 和 username(mobile) 是需要绑定起来的
在 ModelSerializer 里有一个 initial_data,这一个值就是前端传递过来的值,就是用户POST过来的值
直接用 self.initial_data["username"] 来取 ,注意一定要按照时间排序
VerifyCode.objects.fileter(mobile=self.initial_data["username"]).order_by("-add_time")
注意一定要按照时间排序,因为我们从最后一条开始验证
if verify_records:
last_records = verify_records[0]
这样就可以取最近一条开始验证,完整的验证代码
class UserRegSerializer(serializers.ModelSerializer):
code = serializers.CharField(required=True, max_length=4, min_length=4)
def validate_code(self, code):
verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-send_time")
if verify_records:
last_records = verify_records[0] # 最近的一个验证码
five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) # 有效期为5分钟
if five_mintes_ago > last_records.send_time:
raise serializers.ValidationError("验证码过期")
if last_records.code != code:
raise serializers.ValidationError("验证码错误") # 验证码输入错误
# return code # 这个code 只是做验证的,没必要保存
else:
raise serializers.ValidationError("验证码错误") # 记录都不存在
class Meta:
model = User
fields = ("username", "code", "mobile")
⚠️注意⚠️ 为什么不直接使用
verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"],code=code)
- 随机发送验证码,有可能会发送两天相同的验证码给同一个人,所以这时会有2个记录,会错误
- 如果不匹配,可能不存在这条记录的
这两种情况 get 都会抛异常
try:
verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
except VerifyCode.DoesNotExist as e:
pass
except VerifyCode.MultipleObjectsReturned as e:
pass
所以这两种异常都要扑获
filter 就不一样了,如果没有数据,会返回空数组,如果有两个数据,就返回两个
直接对数据判断,就简单多了
过期时间,用 get 也不太容易做,filter 就更加灵活
虽然在 validate_code 里不返回 code,实际上在 ModelSerializer 之后的数据里,code 会被置为 null
def validate(self, attrs):
在后面再写一个 validate(self, attrs),它作用于所有的 serializer 之上,
不是作用于单个的字段之上,在这里做一个全盘的设置
** attrs - 每个字段 validate 之后返回的总的 dict **
def validate(self, attrs):
attrs["mobile"] = attrs["username"]
del attrs["code"]
return attrs
看这里的逻辑就比较明确了 - 这里做统一的处理
这样 UserRegSerializer 就完成了,接下来,继续完成 UserViewSet
首先要 import 进来 from .serializer import UserRegSerializer
个人中心的时候,用户做个人资料修改的时候,需要用到不同的 Serializer
class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
""" 用户 """
serializer_class = UserRegSerializer
现在,可以写 url 配置
from users.views import UserViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', UserViewSet, base_name="users")
这样就可以在 UserViewSet 的 CreateModelMixin 打断点调试程序
在前端访问页面 http://127.0.0.1:8000/users/ 看下 UserRegSerializer 有没有问题
这三个字段是通过 UserViewSet 中 serializer_class = UserRegSerializer
==> 通过 UserRegSerializer 中 fields = ("username", "code", "mobile")
看现在 code 是英文,修改这个新增的字段
code = serializers.CharField(required=True, max_length=4, min_length=4, help_text="验证码")
修改错误提示,针对每一个做错误提示
code = serializers.CharField(required=True, max_length=4, min_length=4,
error_messages={
"blank": "请输入验证码",
"required": "请输入验证码",
"max_length": "验证码格式错误",
"min_length": "验证码格式错误",
}, help_text="验证码")
还可以验证 username(手机号码) 是否存在,要使用 drf 的 Validators
from rest_framework.validators import UniqueValidator
...
username = serializers.CharField(required=True, allow_blank=False, validators=[UniqueValidator(queryset=User.objects.all(),message="用户已经存在")])
验证失败怎么做一个友好的提示呢?
直接使用message 参数
上面对 Validator 和 Serializer 做了完整的介绍
下面完成用户注册时逻辑功能的编码
我们之前用的 UserRegSerializer 是 ModelSerializer 所以,几乎不用加功能的了, 只需要书写最基本的 queryset
这个是个很经典的错误
看数据库,其实已经添加进来了
为什么已经保存了,但是还是报错?
首先查看源码 CreateModelMixin
class CreateModelMixin(object):
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': data[api_settings.URL_FIELD_NAME]}
except (TypeError, KeyError):
return {}
首先 serializer = self.get_serializer(data=request.data)
拿到 serializer_class,就是调用我们配置的 serializer
serializer 做一个验证 serializer.is_valid(raise_exception=True)
没有 poasswrod 首先在 view 里面设置一下
返回调用的 Response(serializer.data,
他会给data 做序列化
拿到 fields = ("username", "code", "mobile", "password")
而 code 之前已经被删除了 del attrs["code"]
这里我们查看文档 drf serializer fields
⚠️注意⚠️ Core arguments write_only = True 设置 True 序列化就不会 序列化这个字段了
这样就可以解决这个错误了
密码是明文的,刚才的文档 有个 style参数
password = serializers.CharField(
style={'input_type': 'password'}
)
这样就可以设置成密文,现在 修改数据库 验证码时间 重新 POST 数据
可以看到 password 被返回回来了,这是不合理的,所以要设置 password 为 write_only=True
就不会被返回回来
username = serializers.CharField(required=True, allow_blank=False, label="用户名",validators=[UniqueValidator(queryset=User.objects.all(),message="用户已经存在")])
password = serializers.CharField(
style={'input_type': 'password'}, label="密码", write_only=True,
)
但查看数据库还有一个问题:
数据库存储的 password 是一个明文,django 里面的密码应该是不能反解的密文
因为 ModelSerializer 拿到这个字段直接保存的,密码在保存的过程中,应该对密码进行单独的设置
可以重载 serializer 的 create 方法,在方法里面加入自己的逻辑
def create(self, validated_data):
user = super(UserRegSerializer, self).create(validated_data=validated_data)
user.set_password(validated_data["password"])
user.save()
return user
这样登录注册的功能就已经完成,虽然代码很少,但是上面的代码可以不写或者分离开 serializer
这里就要提到 Django 信号量的机制
百度搜索 django post_save() django 官方文档
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
这里用户一旦创建的时候,我们可以给他创建一个 token ,我们也可以试下,用户一旦创建修改它的密码
我们在 users 文件夹下新建一个 signals.py
# -*- coding: utf-8 -*-
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
from django.contrib.auth import get_user_model
User = get_user_model()
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
password = instance.password
instance.set_password(password)
instance.save()
# Token.objects.create(user=instance)
我们使用jwt 方式,这里就不用创建 token 了
注意代码:
@receiver
Django 里的一个装饰器,里面包括一个 post_save
和 sender(就是我们的 Model)
注意它接收 Model 传递过来的,它会告诉你这是不是新建的
create=False
, 因为在 update 的时候也会传递过来一个
最后还要做一个配置 在 apps.py 中要重载一个函数
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
verbose_name = "用户管理"
def ready(self):
import users.signals
signals 用来接收信号的,信号里面来完成自己的逻辑,好处就是代码的分离性比较好
可以查看官方文档,自己也可以定义信号,但是自己定义信号的时候,一定要注意发送出去
如果用Django 自定义的信号量,比如说 Model Signals 的 post_save() 等等,
实际上是 Model 它帮我们发送的,一定要注意这点,内置的信号他会在适当的时候给我们发送,
但是自己设置信号,信号的发送和接收就得自己去写
前端的 代码
isRegister(){
var that = this;
register({
password:that.password,
username:that.mobile ,
code:that.code,
}).then((response)=> {
cookie.setCookie('name',response.data.username,7);
cookie.setCookie('token',response.data.token,7)
//存储在store
// 更新store数据
that.$store.dispatch('setInfo');
//跳转到首页页面
this.$router.push({ name: 'index'})
})
注册完成之后,自动登录,再跳转到首页
后端并没有写 token 的接口,我们并没有返回 jwt 的 token
如果需要注册完成之后帮它登录的话,我们就需要完善 views.py 把 token 给返回回来
所以我们就需要重载 CreateModelMixin 中的 create 函数
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
注意这个函数 perform_create
只是调用了 serializer.save()
所以我们也要将这个函数重载,因为我们要生成用户 token 的时候,必须要拿到 user
这个 perform_create 实际上只是调用了 perform_create() 这个函数,它并没有返回 user
所以修改成
class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
""" 用户 """
serializer_class = UserRegSerializer
queryset = User.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
return serializer.save()
我们之前都是直接使用的 jwt 框架,那么 jwt 是怎么生成 token 的呢,我们追踪下源码,
因为后面做第三方登录的时候还会用到这个逻辑
从 url 点击去 obtain_jwt_token
# jwt 的认证接口
url(r'^login/', obtain_jwt_token),
token = serializer.object.get('token')
这个 serializer 直接获取 token 了,所以逻辑应该在 serializer 里面
所以 payload 和 token 是关联的
我们在 users/views.py 中引入
from rest_framework_jwt.serializers import jwt_encode_handler, jwt_payload_handler
拿到这两个我们才能生成 payload 和 token
class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
""" 用户 """
serializer_class = UserRegSerializer
queryset = User.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
re_dict = serializer.data
payload = jwt_payload_handler(user)
re_dict["token"] = jwt_encode_handler(payload)
headers = self.get_success_headers(serializer.data)
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
return serializer.save()
这样我们就完成了token 和 payload 的生成,然后前端就可以拿到token 保存到 cookie
这样我们就完成了我们数据 token 的定制化,这些技巧一定要掌握,后期想自己在这里添加任何东西都可以
比如说针对前端添加 name
re_dict["name"] = user.name if user.name else user.username
这样就可以将数据定制化
首先在列表页中任意点击一个商品,进入详情页,分析详情页功能,
商品轮播图、商品详情-名称、描述、是否免运费、市场价、促销价
商品售量,库存量,加入购物车、收藏
商品详情页富文本描述
右侧 热卖商品
所以这里我们只需要mixins.RetrieveModelMixin,
在列表 view 里加上这一句就可以了
然后我们就来看这里的 serializer,因为商品的轮播图我们之前设置了的外键的,
所以在序列化的时候,我们要将它关联的表, 嵌套序列化就可以了
goods = models.ForeignKey(Goods, verbose_name="商品", related_name="images")
注意 related_name 、(many=True) 、fields = ("image",)
热卖商品之前在 model 字段有一个 is_hot,只要在 过滤器 filters.py 中加一下就可以了
首先这个是用户操作的功能,所以在 user_operation 下的 views.py 编写代码
from rest_framework import viewsets, mixins
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet):
""" 用户收藏 """
然后写 serializers.py 文件
from rest_framework import serializers
from .models import UserFav
class UserFavSerializer(serializers.ModelSerializer):
class Meta:
model = UserFav
fields = ("user", "goods")
然后完善 views.py
from rest_framework import viewsets, mixins
from .models import UserFav
from .serializers import UserFavSerializer
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet):
""" 用户收藏功能 """
queryset = UserFav.objects.all()
serializer_class = UserFavSerializer
添加 url 配置
from user_operation.views import UserFavViewSet
router.register(r'userfavs', UserFavViewSet, base_name="userfavs")
一般添加收藏,不会是选择用户,所以我们希望 fields = ("user", "goods")
user 是获取当前登录用户的 user
这里查看文档 validators -> Advanced field defaults -> CurrentUserDefault
from rest_framework import serializers
from .models import UserFav
class UserFavSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
class Meta:
model = UserFav
fields = ("user", "goods")
这样就能获取当前 用户
刷新页面,看到就只有商品了,就不会给我们显示用户了
这样就完成了收藏的功能,如果也要添加删除的功能,就要将 id 也返回回来
from rest_framework import serializers
from .models import UserFav
class UserFavSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
class Meta:
model = UserFav
fields = ("user", "goods", "id")
有了这个 id 后面做删除功能(取消收藏功能)就简单了
获取收藏列表的功能 添加 mixins.ListModelMixin,
在个人中心获取收藏记录的时候,我们不仅想要 goods ID,我们还希望获取 goods 的基本字段
如何获取商品详情,以后在个人中心的时候再来完善
如果用户反复收藏都一个东西,比如这个东西收藏过,使用 django** unique_together **
unique_together=("user","goods")
联合唯一的验证
class UserFav(models.Model):
""" 用户收藏 """
user = models.ForeignKey(User, verbose_name="用户")
goods = models.ForeignKey(Goods, verbose_name="商品")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")
class Meta:
verbose_name = "用户收藏"
verbose_name_plural = verbose_name
unique_together = ("user", "goods")
def __str__(self):
return self.user.name
映射到数据库里的一些功能,这里设置之后数据库给我们完成的,数据库会给我们抛出异常
{
"non_field_errors": [
"字段 user, goods 必须能构成唯一集合."
]
}
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from .models import UserFav
class UserFavSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
class Meta:
model = UserFav
validators = [
UniqueTogetherValidator(
queryset=UserFav.objects.all(),
fields=('user', 'goods'),
message="已经收藏"
)
]
fields = ("user", "goods", "id")
non_field_errors
这不是某个字段出错,前端接收这个信息,显示到整个表单下面显示错误信息
drf 官方文档 Authentication 和 Permissions 用户验证 和 权限判断
AllowAny - 不管有没有登录的用户都可以请求
IsAuthenticated - 判断是否已经登录的 - 初步判定是否登录
IsAdminUser - 判断用户是否是 admin
from rest_framework.permissions import IsAuthenticated
permission_classes = (IsAuthenticated,)
用户未登录会返回401 Unauthorized
删除的时候验证权限,删除的记录的用户是否是当前 request 里的用户
在 utils.py 文件里新建一个 permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Instance must have an attribute named `owner`.
return obj.user == request.user
from utils.permissions import IsOwnerOrReadOnly
...
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
这样就可以确认删除的权限
不能获取所有 UserFav,只能获得当前用户的 UserFav,所以要重载 get_queryset() 方法
from rest_framework import viewsets, mixins
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.authentication import SessionAuthentication
from .models import UserFav
from utils.permissions import IsOwnerOrReadOnly
from .serializers import UserFavSerializer
# Create your views here.
class UserFavViewSet(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet):
""" 用户收藏功能 """
# queryset = UserFav.objects.all()
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
serializer_class = UserFavSerializer
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
def get_queryset(self):
return UserFav.objects.filter(user=self.request.user)
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
配置到 view里
前端只传递 userfavs/goodsid,后端能否满足根据goodsid 判断这个商品这个用户是否被收藏?
我们配置 mixins.RetrieveModelMixin, 他会自动给我们生成一个详情的 url
但是不知道数据库保存的id 是什么?所以我们希望这个url 生成的时候,传递进来的不再是 id
希望传递进来的是 goods_id
来了解一下 RetrieveModelMixin 原理,来看下源码:
class RetrieveModelMixin(object):
"""
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
关键是
instance = self.get_object()
获取某一个具体的详情,所以实际上它是调用了 get_object() 函数
这个函数是在 GenericViewSet -> GenericAPIView 里
这个函数,他会根据传递过来的id ,去搜索数据库
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
根据 lookup_field 去搜索的,所以这个是可以配置的
class UserFavViewSet(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet):
""" 用户收藏功能 """
# queryset = UserFav.objects.all()
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
serializer_class = UserFavSerializer
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
lookup_field = "goods_id"
def get_queryset(self):
return UserFav.objects.filter(user=self.request.user)
goods 是外键,保存到数据库中是 goods_id
所以可以直接来搜索这个字段,现在可以测试下看看是不是根据这个字段来找的
添加 Description 的方法:
goods = models.ForeignKey(Goods, verbose_name="商品", help_text="商品id")
要完成的第一个功能 - 用户个人信息的修改
姓名:
出生日期:
性别:
电子邮箱:
手机: - 需要单独页面提交
需要一个接口,可以让用户请求 用户信息 - 之前有个 UserViewSet 重载了 create 方法实现用户注册
from rest_framework import mixins
mixins.RetrieveModelMixin,
viewsets 我们在使用 router 注册的时候,它会给 RetrieveModelMixin 注册一个详情的 url
所以我们在使用 Retrieve 这个方法,在获取用户详情的时候 url 是这样的 users/id
实际上用户在进入个人中心的时候,并不知道用户的id,因为我们 return 了 token 和 name
如何来解决这个问题呢?
两种方法: 1. 给用户返回 id
re_dict["token"] = jwt_encode_handler(payload)
re_dict["name"] = user.name if user.name else user.username
def get_object(self):
return self.request.user
不管 url 传递什么,我们都只返回 self.request.user,所以URL可以随意传递数字进来 /usrs/123
restful api 概念就是对资源的操作
用户注册 - 对用户的 POST 请求
获取用户信息 - GET 请求
修改用户信息 - UPDATE 请求
现在回过头来,思考权限的问题,它能够获取当前用户,所以必须是登录的状态
from rest_framework import permissions
...
permission_classes = (permissions.IsAuthenticated, )
这样在访问 UserViewSet 里的方法时都需要必须要用户登录,另外一个问题:
create()方法实现的是用户注册,用户信息获取都放在 UserViewSet,
所以用户注册肯定不能用 IsAuthenticated,所以我们希望 permissions 以一种动态的方式呈现
如果是 注册 permissions.AllowAny
为了动态设置 permissions 所以 这种配置是不可取的
permission_classes = (permissions.IsAuthenticated, )
来研究下源码:
viewsets.GenericViewSet -> generics.GenericAPIView -> views.APIView
get_permissions() 它会去我们之前配置的 permission_classes 里去遍历,
返回 permission() 实例,然后放在一个数组里面,所以实际上我们可以重写这个get_permission()函数
现在我们知道了通过 get_permission() 就可以动态设置用户的 permission
关键是 这里的 action 对应的是什么?post 的时候,或者 get 数据的时候 action对应的是什么?
实际上是和函数名称保持一致的,返回 permissions 的实例 的数组
# permission_classes = (permissions.IsAuthenticated, )
def get_permissions(self):
if self.action == 'retrieve':
return [permissions.IsAuthenticated()]
elif self.action == 'create':
return []
return [] # 返回默认值为空一定要加,否则会出错的
action 放入 self 里面只有使用 viewsets.GenericViewSet 才可以
调试,这里还需要用户认证,其实还缺少一个功能,就是用户的认证
弹出框就是 BasicAuthentication 的认证模式
from rest_framework import authentication
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
...
authentication_classes = (JSONWebTokenAuthentication,
authentication.SessionAuthentication)
现在配置了 JSONWebTokenAuthentication,和 SessionAuthentication 模式,就不会有弹出框了
实际上 这两个是不需要用户输入用户名密码的,
它是需要在浏览器里面添加 session,或者在 header 添加 token
现在问题是 返回给用户的数据只有 username 和 mobile 是因为之前在 UserRegSerializer 设置的
fields = ("username", "code", "mobile", "password")
code 和 password 都是 write_only
现在返回给用户的时候我们希望用另外一个 serializer 来序列化,比如来定义一个
UserDetailSerializer
有了这个就可以指明需要哪些字段
class UserDetailSerializer(serializers.ModelSerializer):
""" 用户详情序列化类 """
class Meta:
module = User
fields = ("name", "gender", "birthday", "email", "mobile")
现在如何像 get_permisssions() 一样动态获取 serializer ?
像之前一样,进一步了解源码
viewsets.GenericViewSet -> generics.GenericAPIView
所以我们来重载 get_serializer_class() 函数
def get_serializer_class(self):
if self.action == 'retrieve':
return UserDetailSerializer
elif self.action == 'create':
return UserRegSerializer
return UserDetailSerializer
这样接口就算完成了代码变动对比
UserViewSet 配置 mixins.UpdateModelMixin
之前遗留的问题:在个人中心获取收藏记录的时候,我们不仅想要 goods ID,
我们还希望获取 goods 的基本字段,如何获取商品详情,现在来完善
在使用mixins.ListModelMixin 返回 userfavs 列表的时候,
用户收藏列表显示,实际上要做另一个 serializer
from goods.serializers import GoodsSerializer
class UserDetailSerializer(serializers.ModelSerializer):
goods = GoodsSerializer(many=True)
class Meta:
model = UserFav
这里又涉及到动态 serializer 问题
def get_serializer_class(self):
if self.action == 'list':
return UserDetailSerializer
elif self.action == 'create':
return UserFavSerializer
return UserFavSerializer
用户留言功能 - 删除、获取留言、添加留言(可以上传文件)
首先写后台接口 view
class LeavingMessageViewSet(mixins.ListModelMixin, mixins.DestroyModelMixin, mixins.CreateModelMixin,
viewsets.GenericViewSet):
"""
list:
获取用户留言
create:
添加留言
delete:
删除留言
"""
然后写 serializer
class LeavingMessageSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
class Meta:
model = UserLeavingMessage
fields = ("user", "msg_type", "subject", "message", "file")
完善 view 代码:
class LeavingMessageViewSet(mixins.ListModelMixin, mixins.DestroyModelMixin, mixins.CreateModelMixin,
viewsets.GenericViewSet):
"""
list:
获取用户留言
create:
添加留言
delete:
删除留言
"""
serializer_class = LeavingMessageSerializer
def get_queryset(self):
return UserLeavingMessage.objects.filter(user=self.request.user)
最后配置 URL 代码:
from django.conf.urls import url, include
# from django.contrib import admin
from rest_framework.documentation import include_docs_urls
from rest_framework.routers import DefaultRouter
from rest_framework.authtoken import views
from rest_framework_jwt.views import obtain_jwt_token
router = DefaultRouter()
# from goods.views_base import GoodListView
from goods.views import GoodsListViewSet, CategoryViewSet
from users.views import SmsCodeViewSet, UserViewSet
from user_operation.views import UserFavViewSet, LeavingMessageViewSet
# 配置 goods 的 url
router.register(r'goods', GoodsListViewSet, base_name="goods") # 商品
router.register(r'categorys', CategoryViewSet, base_name="categorys") # 商品分类
router.register(r'codes', SmsCodeViewSet, base_name="codes") # 验证码
router.register(r'users', UserViewSet, base_name="users") # 用户
router.register(r'userfavs', UserFavViewSet, base_name="userfavs") # 收藏
router.register(r'messages', LeavingMessageViewSet, base_name="messages") # 留言
# goods_list = GoodsListViewSet.as_view({
# 'get': 'list'
# })
urlpatterns = [
# url(r'^admin/', admin.site.urls),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
# drf 自带的 token 认证模式
url(r'^api-token-auth/', views.obtain_auth_token),
# jwt 的认证接口
url(r'^login/', obtain_jwt_token),
# 商品列表页
# url(r'goods/$', GoodsListView.as_view(), name="goods-list"),
# url(r'goods/$', goods_list, name="goods-list"),
url(r'^', include(router.urls)),
url(r'docs/', include_docs_urls(title="文档功能")),
]
view 代码还没有完善
必须登录
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWenTokenAuthentication, SessionAuthentication)
class LeavingMessageViewSet(mixins.ListModelMixin, mixins.DestroyModelMixin, mixins.CreateModelMixin,
viewsets.GenericViewSet):
"""
list:
获取用户留言
create:
添加留言
delete:
删除留言
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class = LeavingMessageSerializer
def get_queryset(self):
return UserLeavingMessage.objects.filter(user=self.request.user)
来看下文档:
删除功能需要服务器返回给前端 id, 前端通过 id ,删除文件,所以
fields = ("user", "msg_type", "subject", "message", "file", "id")
刷新下页面就可以看见 id 了
我们希望把 add_time 加进来,但是我们不想填写时间
fields=("user", "msg_type", "subject", "message", "file", "id", "add_time")
这里就需要我们另一个配置 add_time = serializers.DateTimeField(read_only=True)
DateTimeField 可以通过 format 做格式化
class LeavingMessageSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
add_time = serializers.DateTimeField(read_only=True, format='%Y-%m-%d %H:%M')
class Meta:
model = UserLeavingMessage
fields = ("user", "msg_type", "subject", "message", "file", "id", "add_time")
read_only 就是这个值只返回,不提交
这里修改 models.py file 的 upload_to=
file = models.FileField(upload_to="message/images/", verbose_name="上传的文件", help_text="上传的文件")
apps/user_operation/models.py代码修改
不添加 upload_to = 它默认保存在该项目的根路径下,为什么这里的 file upload 这么简单,是因为
drf 中 api-guide-parsers-multipartparser
multipart/form-data
收货地址功能
获取所有的收货地址、修改某一个收货地址、删除某一个收货地址
class AddressViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin,
viewsets.GenericViewSet):
这里涉及到了增、删、改、查,实际上有个 Model 给我们封装到一起了** viewsets.ModelViewSet **
看下源码:
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
class AddressViewSet(viewsets.ModelViewSet):
"""
收货地址管理
list:
获取收货地址
create:
添加收货地址
update:
更新收货地址
delete:
删除收货地址
"""
然后来写 serializer :
class AddressSerializer(serializers.ModelSerializer):
class Meta:
model = UserAddress
fields = ("user", )
为了和前端的 收货地址区域 格式相同,我们在这里修改 models,也可以不修改,在前端使用的时候划分,这里在 models 里修改
class UserAddress(models.Model):
""" 用户收货地址 """
user = models.ForeignKey(User, verbose_name="用户")
province = models.CharField(max_length=100, default="", verbose_name="省份")
city = models.CharField(max_length=100, default="", verbose_name="城市")
district = models.CharField(max_length=100, default="", verbose_name="区域")
address = models.CharField(max_length=100, default="", verbose_name="详细地址")
singer_name = models.CharField(max_length=100, default="", verbose_name="签收人")
singer_mobile = models.CharField(max_length=11, default="", verbose_name="电话")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")
class Meta:
verbose_name = "收货地址"
verbose_name_plural = verbose_name
def __str__(self):
return self.address
因为修改了字段,要及时做 makemigrations 和 migrate
现在来写 serializer
class AddressSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
add_time = serializers.DateTimeField(read_only=True, format='%Y-%m-%d %H:%M')
class Meta:
model = UserAddress
fields = ("user", "province", "city", "district", "address", "singer_name", "singer_mobile", "id", "add_time")
把 AddressSerializer 配置到 viewset 中
class AddressViewSet(viewsets.ModelViewSet):
"""
收货地址管理
list:
获取收货地址
create:
添加收货地址
update:
更新收货地址
delete:
删除收货地址
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class = AddressSerializer
def get_queryset(self):
return UserAddress.objects.filter(user=self.request.user)
这样它的增删改查就完成了,来配置 url
router.register(r'address', AddressViewSet, base_name="address") # 收货地址
配置完成之后就可以在 文档 中测试一下
这里填写了这么多字段,可以做一个 validator 验证,是否为空啊,mobile 是否合法啊
需求分析:
list列表页、create、update、destory
views.py
from rest_framework import viewsets
class ShoppingCartViewSet(viewsets.ModelViewSet):
"""
购物车功能
list:
获取购物车详情
create:
加入购物车
delete:
删除购物记录
update:
更新购物商品数量
"""
写 serializer 代码:
from rest_framework import serializers
class ShopCartSerializer(serializers.Serializer):
这里使用的是 Serializer 而不是 ModelSerializer,因为 Serializer 灵活性更高
首先看下我们的需求,先看下我们的 models.py
如果用户对某一个商品添加过一次,如果再对商品添加一次,我们就直接将它的商品数量 +1
所以这里就需要 unique_together 将 user 和 goods 构成联合唯一的索引
而我们不希望第二次添加商品到购物车,添加失败,而是购买数量 +1 的操作
如果我们用 ModelSerialzier,那 serializer valdated 的时候就会抛异常
进入不了我们的逻辑。就算重载 create() 方法也是无效的
因为 view 继承的是 ModelViewSet -> mixins.CreateModelMixin ->
serializer.is_valid(raise_exception=True)
验证的时候就抛异常,进入不了create方法
那我们这里用到底层的 serializers.Serializer,我们自己来做中间的 序列化 验证 过程
首先把 models.py 里的字段给映射出来
goods 是外键,那serializer 有没有外键字段呢? 查看 drf 官方文档 Serializer relations 的 primarykeyrelatedfield
官方文档示例:
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
这个 Demo 是 ModelSerializer,而我们的是 Serializer 所以我们要指明 queryset=
goods = serializers.PrimaryKeyRelatedField(required=True, queryset=Goods.objects.all())
有了这个外键设置之后,我们就可以来重写 create() 方法了,
serializer 必须重写 create()方法,因为它本身没有提供 save 功能
这个功能很重要了,首先用户在创建的时候,用户对某一个商品加入购物车,
是有两种状态的 : 1. 购物车本身没有这个记录 2. 有这个记录
要获取购物车记录,判断记录存在不存在
但是在获取购物车记录之前,必须要拿到这里的数据,在调用 create 的时候,
validated_data 数据实际上是每一个变量已经做过 validate 之后处理过的数据
比如说 goods_num 是 int ,实际上它已经把它处理成 int 了,我们就不要对它做字符串的转换
在 7.10 user serializer 和 validator 验证 - 1介绍过
initial_data 是前端传递过来的值,就是用户POST过来的值,没有 validated_data 之前的数据
首先获取当前的用户,user = self.context["request"].user
context 上下文 里面有个 request
⚠️注意⚠️ request 不是放到 self. 里的,在 view 中可以直接用 self.request 在 serialzier 中应该要从 context 中取
goods = validated_data["goods"]
goods 实际上已经是 goods 的对象了,
外键做反序列化的时候,首先会序列化成 goods 对象
有了这三个字段就可以查询数据库,看记录是否存在
existed = ShoppingCart.objects.filter(user=user, goods=goods)
filter 返回的是数组,如果没有找到就是空数组,有的话就都取出来,我们获取第一条数据
existed = existed[0]
如果已经存在,就更新购物车
如果不存在,就直接调用 create ,获取返回结果,做反序列化,需要返回给前端的
这样就完成了,重写 create() 方法,加入自己的逻辑,整个过程都可以控制
def create(self, validated_data):
user = self.context["request"].user
goods_num = validated_data["nums"]
goods = validated_data["goods"]
existed = ShoppingCart.objects.filter(user=user, goods=goods)
if existed:
existed = existed[0]
existed.goods_num += goods_num
existed.save()
else:
existed = ShoppingCart.objects.create(**validated_data)
return existed
新增serializers.py 代码片段,序列化ShopCartSerializer购物车字段验证
再回过来完善 view.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.authentication import SessionAuthentication
from utils.permissions import IsOwnerOrReadOnly
from .serializers import ShopCartSerializer
class ShoppingCartViewSet(viewsets.ModelViewSet):
"""
购物车功能
list:
获取购物车详情
create:
加入购物车
delete:
删除购物记录
update:
更新购物商品数量
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class = ShopCartSerializer
然后再来配置 url url 新增代码变动
from trade.views import ShoppingCartViewSet
router.register(r'shopcats', ShoppingCartViewSet, base_name="shopcats") # 购物车
然后继续 完善 views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.authentication import SessionAuthentication
from utils.permissions import IsOwnerOrReadOnly
from .serializers import ShopCartSerializer
from .models import ShoppingCart
class ShoppingCartViewSet(viewsets.ModelViewSet):
"""
购物车功能
list:
获取购物车详情
create:
加入购物车
delete:
删除购物记录
update:
更新购物商品数量
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class = ShopCartSerializer
queryset = ShoppingCart.objects.all()
特别注意 不要 忽略 queryset 配置
上面介绍了加入购物车功能,下面介绍购物车其他的功能,首先是列表页
重写 get_queryset(self) 方法,只返回当前用户列表
def get_queryset(self):
return ShoppingCart.objects.filter(user=self.request.user)
还有一个细节,和之前收藏一样,我们希望只传递 goods_id 过来,而不是传递记录本身的ID给前端
现在传递商品的 id 过来,做一个更新的操作,
** 如果我们继承 Serializer 就得重写 update() 方法,以及 create() 方法
我们来查看 ModelSerializer,已经重写了 create() update() 方法
所以继承 ModelSerializer 代码量就很少,继承 Serialzier 自己就需要重写用到的方法
现在来重写 update 方法
def update(self, instance, validated_data):
# 修改商品数量
instance.goods_num = validated_data["goods_num"]
instance.save()
return instance
删除 delete() 是不需要重写的
购物车页面的商品的详情:ShopCartDetailSerializer
from goods.serializers import GoodsSerializer
class ShopCartDetailSerializer(serializers.ModelSerializer):
goods = GoodsSerializer(many=False)
class Meta:
model = ShoppingCart
fields = "__all__"
一个 shopcartdetail 对应一个 goods ,所以 many=False
在 views.py 中重载 get_serializer_class 方法实现动态 serializer
class ShoppingCartViewSet(viewsets.ModelViewSet):
"""
购物车功能
list:
获取购物车详情
create:
加入购物车
delete:
删除购物记录
update:
更新购物商品数量
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class = ShopCartSerializer
# queryset = ShoppingCart.objects.all()
lookup_field = "goods_id"
def get_serializer_class(self):
if self.action == 'list':
return ShopCartDetailSerializer
else:
return ShopCartSerializer
def get_queryset(self):
return ShoppingCart.objects.filter(user=self.request.user)
有了这个之后,列表页显示就算完成了(代码片段),接下来和 vue 前端联调
首先理解下购物车和订单之间的关系,实际上用户在购买商品的时候,都会放很多商品进入我们的购物车,然后去购物车去进行结算,这里我们采用的是比较简单的做法是将购物车里所有的商品进行结算,实际上比如淘宝,实际上是可以只对购物车里某一个商品进行结算的
我们看 models.py 里有一个字段 order_sn,这个字段它是不能为空的,
order_sn=models.CharField(max_length=30, unique=True, verbose_name="订单号")
当用户点击里去结算之后,我们给它生成一个订单(订单号),然后让用户去支付页面去支付
这个订单号 order_sn 我们给设置的是不能为 null,但是我们之前做过,
用到 viewset 里面的 createmixin,实际上会来对这种 order_sn 字段进行验证的,
所以实际上用来在开始不可能 POST 一个 order_sn 的,所以有个问题就是这个 order_sn 订单号
实际上订单号它是后台生成的,所以说用户在前端 POST 过来,它是没有 order_sn 的,
但是 order_sn 又不能为空,所以使用 createmixin 就会有问题的,所以我们在这里可以简单的设置为空
这样在验证字段的时候就没有关系了
order_sn = models.CharField(max_length=30, null=True, blank=True, unique=True, verbose_name="订单号")
trade_no 是支付宝给我们返回的交易号,
pay_status 我们可以给它设置一个默认值,待支付
address 配送地址,我们为什么不直接指向一个外键呢?而是要把它值取出来,保存到订单的信息里面呢
使用外键,用户在修改个人中心里的配置地址,就会影响到这里,如果查看以前的某个商品的配送地址
如果使用外键,就无法正确查看之前的配送地址了
这里都是订单的基本详情,订单还有最重要的是 OrderGoods 订单的商品详情,是一对多的关系
一个订单里有多个商品,所以我们给它设置了 OrderGoods这个Model,让它有个外键指向order和goods
有了 这个model 就可以写后台的逻辑了:(views.py)
class OrderViewSet(viewsets.GenericViewSet):
这里我们为什么只用 GenericViewSet 而不是用 ModelViewSet 呢?
分析下,订单一般是不允许修改的,所以就没有 update 的操作,所以我们就不适合使用 ModelViewSet
class OrderViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin,
viewsets.GenericViewSet):
"""
订单管理
list:
获取个人订单
delete:
删除订单
create:
新增订单
retrieve:
订单详情
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class =
def get_queryset(self):
return OrderInfo.objects.filter(user=self.request.user)
返回当前用户的订单
然后现在写 serializers.py
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = OrderInfo
fields = "__all__"
现在分析下逻辑,首先用户提交的时候,创建一个订单,提交订单的时候,就不会提交每一个商品了,因为现在逻辑是比较简单的逻辑,就是直接清空购物车
首先我们获取到用户要创建订单这个需求之后,我们要将购物车里所有商品信息拿出来
到我们 OrderGoods 里添加记录,意思就是说把我们购物车里的数据,添加到 OrderGoods 里来
要将购物车里的记录删除
实际上在 create 订单之后还多了这两步,如何来完成这两步呢?
1: def perform_create(self, serialzier): 这里完成逻辑,
这个调用的是 serialzier.save(),拿到 order= serializer.save()
2: 订单号它是必有的,在 order=serializer.save()之前生成一个 order_sn 订单号
def generate_order_sn (self):
保证信息比较完整,也不会冲突
常用做法是当前时间+userid+随机数
import time
def generate_order_sn(self):
# 当前时间+userid+随机数
from random import Random
random_ins = Random()
order_sn = "{time_str}{userid}{ranstr}".format(time_str=time.strftime("%Y%m%d%H%M%S"), userid=self.context["request"].user.id, ranstr=random_ins.randint(10, 99))
return order_sn
time.strftime("%Y%m%d%H%M%S")
获取当前时间,把它格式化成字符串
self.request.user.id
Random()实例化 randint(10,99)
生成 10,99 之间的两位随机数字
如果在 view 中验证,mobile = serializer.validated_data["mobile"]
最好是写在 serialzier 中
⚠️注意⚠️ 在 serialzier 中取 user 的方法 self.context["request"].user.id
使用 def validate(self, attrs)
作用于所有的 serializer 字段,作为全局设置
def validate(self, attrs):
attrs["order_sn"] = self.generate_order_sn()
return attrs
然后在 views.py 做一些后续的操作
def perform_create(self, serializer):
order = serializer.save()
shop_carts = ShoppingCart.objects.filter(user=self.request.user)
for shop_cart in shop_carts:
order_goods = OrderGoods()
order_goods.goods = shop_cart.goods
order_goods.goods_num = shop_cart.goods_num
order_goods.order = order
order_goods.save()
shop_cart.delete()
return order
把购物车里的数据,添加到 OrderGoods 数据表中,
首先获取到当前用户购物车里的商品
shop_carts = ShoppingCart.objects.filter(user=self.request.user)
然后生成 OrderGoods 表
for shop_cart in shop_carts:
order_goods = OrderGoods()
order_goods.goods = shop_cart.goods
order_goods.goods_num = shop_cart.goods_num
order_goods.order = order
order_goods.save()
然后 清空购物车 shop_cart.delete()
然后配置 url router.register(r'orders', OrderViewSet, base_name="orders")
查看浏览器 api 接口
用户给列出来了,所以要修改为
user=serializer.HiddenField(default=serializers.CurrentUserDefault())
这里订单状态默认的是成功,所以一定不能让用户提交这个参数,要不然,用户修改了状态直接提交
pay_status = serializers.CharField(read_only=True)
trade_no = serializers.CharField(read_only=True)
order_sn = serializers.CharField(read_only=True)
pay_time = serializers.DateTimeField(read_only=True)
订单状态、订单号,支付订单号,都要修改为只能读,不能写
加上支付时间也不能填写,因为支付时间,是支付宝支付之后的生成的
测试 delete 功能,他将 ShoppingCart 数据删除之后,也会将 OrderGoods 数据删除
这样订单的相关功能就介绍完成
ShoppingCart 和 OrderGoods 操作代码片段
在 个人中心 我的订单 中,有订单状态、商品列表、收货人信息,
这里发现后台接口并没有完善好,查看订单详情的时候,应该将订单的商品列表给列出来,
序列化的时候应该获取到订单的商品列表,现在完善后台接口
在 views.py 中动态获取 serializer_class
def get_serializer_class(self):
if self.action == "retrieve":
return OrderDetailSerializer
return OrderSerializer
这里就要定义一个新的 OrderDetailSerializer
涉及到
class OrderGoods(models.Model):
""" 订单的商品详情 """
order = models.ForeignKey(OrderInfo, verbose_name="订单信息", related_name="goods")
class OrderGoodsSerializer(serializers.ModelSerializer):
goods = GoodsSerializer(many=False)
class Meta:
model = OrderGoods
fields = "__all__"
class OrderDetailSerializer(serializers.ModelSerializer):
goods = OrderGoodsSerializer(many=True)
class Meta:
model = OrderInfo
fields = "__all__"
OrderInfo 和 OrderGoods 的关联
order = models.ForeignKey(OrderInfo, verbose_name="订单信息", related_name="goods")
serializer 序列化的时候实际上应该序列化 OrderGoods 这个类
goods = GoodsSerializer(many=False)
关于后面第三方支付,和第三方登录的时候,我们都是有个 回调 的 url ,
这个 url 一般指向的是服务器的 ip 地址
为了便于调试,所以我们首先需要能够完成在本地 pycharm 去调试远端的服务器
这样我们在做回调的时候,就可以调试代码了
代码同步了,必须在远程服务器上建立虚拟环境
安装python3.6
1. 获取
wget https://www.python.org/ftp/python/3.6.2/Python-3.6.2.tgz
tar -xzvf Python-3.6.2.tgz -C /tmp
cd /tmp/Python-3.6.2/
2. 把Python3.6安装到 /usr/local 目录
./configure --prefix=/usr/local
make
make altinstall
3. 更改/usr/bin/python链接
ln -s /usr/local/bin/python3.6 /usr/bin/python3
sudo apt-get install python-virtualenv
pip install virtualenvwrapper
sudo find / -name virtualenvwrapper.sh
在根目录下寻找 virtualenvwrapper.sh
找到路径 拷贝下来,配置的环境变量
vim ~/.bashrc
export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh
source ~/.bashrc
mkvirtualenv —python=/usr/bin/python3 py3envname
如果出现这个错误
pip install --upgrade virtualenv
可以解决这个问题
继续安装虚拟环境:
pip install -i https://pypi.douban.com/simple django
pip install -i https://pypi.douban.com/simple pillow
pip install mysqlclient
安装 mysqlclient 可能会出现错误,mysqlclient 是python3 替代python2 的 mysql-python
解决方法:sudo apt-get install libmysqlclient-dev
安装好了依赖包之后,再继续安装 pip install -r requirements.txt
查看所有虚拟环境 workon
进入虚拟环境 workon py3
退出虚拟环境 deactivate
sudo apt-get install mysql-server
mysql 连接配置 ip bind IP绑定
vim /etc/mysql/mysql.conf.d/mysqld.cnf
vim /etc/mysql/my.cnf
修改 bind-address = 127.0.0.1
bind-address = 0.0.0.0
这样才能通过 ip 地址访问 mysql,方便在其他系统其他电脑用 navicate 连接进行操作
修改之后重启 mysql
sudo service mysql restart
在 mysql 权限里配置 连接
FLUSH PRIVILEGES;
这样就可以在外部通过 navicate 进行连接
也可以通过命令行新建数据库:
CREATE DATABASE IF NOT EXISTS yourdbname DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
如果外部无法连接服务器端口,查看防火墙设置:
首先看对接支付宝支付接口,需要做哪些准备?
-----BEGIN PRIVATE KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzBqHhrDaCyqtdEn+LUeLL83EmnyUVNSQxk6AKMROoJMGszl01Ly3UmKQLnSPIJLF+wGFjDvCs0Cjoi0KOJSmGFecVZ14kS9ZHCj1MdotUkpDj28sViaQXPW7LlLdRuaxSen6sQBwOyqmHV50a7ummrX9EHeQqToShS0lm1brbegkcmtoVrBOv+ehVQDyB76pyukn9N7K8P0SRgPtF7m4zyloJM9ZXnGMxWApj0jRK1uYfWDNXySgtOtyJEhUjufcmtu2Cq/konP+qR3ZZSZG4++dKXQQS5npwsUoywLWsSo6Vf+qjHhKj/v/oOMF1PAKyCvJVIU2599rawA6kROgXwIDAQAB
-----END PRIVATE KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5h8p1Ufg8Je8DiEPlHNFX828QvWJdzZ6A3HapmNvbK8OcllOD+uJidaiPHDoi2mtCOH7busI1IALv6XaPPWsT5B2FQbABIqncvpH9gUbonWff2/PsWitVay6xcEkHIW4WlUm6DXbN/hTu8dxIC6yJhLKFKG5TZHwOkQgk+IaU4xkJb1D8YZPBy8AojA+0hmtLrn5QGyQH5TyphyZ9DdjvvI5fCu68pRRdHMl3JJeoevuivzWVV33PZW11lOPcznP26cLeI/aAB3UO7C1a9+n7gPxmU/meK4RGxyXNqbmpOoX8ayFGe/h5u8p1QTXJf94D4KDnXXWTmg5PqUC9Ok80QIDAQAB
查询订单状态的时候会用到这个 支付宝公钥
上面为了支付宝支付做了准备工作,下面编码实现支付宝的支付接口
首先为了完成支付的接口,将 pycharm 的代码设置Project Interpreter为本地,先在本地调试
然后看如何对接支付宝的支付接口,先了解下支付宝支付的官方文档
电脑网站支付产品介绍 API 列表 统一收单下单并支付页面接口
return url 需要修改订单状态,和支付宝进行交互,同步返回地址
notify_url 支付宝服务器主动通知商户服务器里指定的页面 - 异步的接口,
只要订单在支付宝平台创建成功,可以在支付宝账单里支付,也可以在手机支付,
只要支付了账单,都会像 notify_url 发送请求,发起这个请求,只要实现了url
然后在里面修改订单的状态就可以了
对于PC网站支付的交易,在用户支付完成之后,支付宝会根据API中商户传入的notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统。
页面回跳参数
对于PC网站支付的交易,在用户支付完成之后,支付宝会根据API中商户传入的return_url参数,通过GET请求的形式将部分支付结果参数通知到商户系统。
分析了上面,只需要写一个 view,既可以处理 POST 又可以处理 GET 请求
这样的话,只需要配置一个 url 就可以完成支付宝两种形式的返回
因为这个 view 是和支付宝相关的,没有 model 所以我们可以直接使用底层的 APIView
from rest_framework.views import APIView
class AliPayView(APIView):
def get(self, request):
""" 处理支付宝 return_url 返回"""
pass
def post(self, request):
""" 处理支付宝 notify_url """
pass
首先处理 POST 请求,方便调试,先把 URL 配置好
url(r'^alipay/return/', AliPayView.as_view(), name="alipay")
先不花精力写里面的业务逻辑,先确定能进入这个函数里面
首先生成一个支付订单,生成这个订单的时候,
一定要将 app_notify_url、return_url 改为刚配置的url
把代码上传到服务器,使用服务器调试,记得修改 project interpreter
alipay = AliPay(
appid="2016081600258982",
app_notify_url="http://106.14.156.160:8000/alipay/return/",
app_private_key_path="../trade/keys/private_2048.txt", # 个人私钥
alipay_public_key_path="../trade/keys/alipay_key_2048.txt", # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://106.14.156.160:8000/alipay/return/"
)
app_notify_url 和 return_url 修改为服务器 ip
注意及时把代码上传到服务器
WAIT_BUYER_PAY 交易创建,等待买家付款
TRADE_CLOSED 未付款交易超时关闭,或支付完成后全额退款
TRADE_SUCCESS 交易支付成功
TRADE_FINISHED 交易结束,不可退款
把这四种定义到 model 里去
class OrderInfo(models.Model):
""" 订单基本信息 """
ORDER_STATUS = (
("TRADE_SUCCESS", "交易支付成功"),
("TRADE_CLOSED", "未付款交易超时关闭,或支付完成后全额退款"),
("WAIT_BUYER_PAY", "交易创建,等待买家付款"),
("TRADE_FINISHED", "交易结束,不可退款"),
("paying", "待支付"),
)
接下来处理我们的逻辑,首先从 request 里获取数据
processed_dict = {}
for key, value in request.POST.items():
processed_dict[key] = value
django 的 request.POST 调试,可以查看到是字符串格式,可以直接使用
sign = processed_dict.pop("sign", None)
很关键,一定要将 sign pop出来
然后把 alipay 的实例代码拷贝过来,注意文件的路径
相对路径比较容易出现问题,而决定路径,在本地调试,和服务器调试是需要修改的
所以把路径配置到 settings.py 中去
# 支付宝相关配置
private_key_path = os.path.join(BASE_DIR, 'apps/trade/keys/private_2048.txt')
ali_pub_key_path = os.path.join(BASE_DIR, 'apps/trade/keys/alipay_key_2048.txt')
alipay = AliPay(
appid="2016081600258982",
app_notify_url="http://106.14.156.160:8000/alipay/return/",
app_private_key_path=private_key_path, # 个人私钥
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://106.14.156.160:8000/alipay/return/"
)
然后验证verify_result = alipay.verify(processed_dict, sign)
如果 verify_result 返回为 True,然后去获取数据库里的记录
if verify_result is True:
order_sn = processed_dict.get("out_trade_no", None)
trade_no = processed_dict.get("trade_no", None)
trade_status = processed_dict.get("trade_status", None)
out_trade_no 商品网站唯一订单号
trade_no 支付宝交易凭证号
trade_status 交易目前所处的状态
existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
for existed_order in existed_orders:
existed_order.pay_status = trade_status
existed_order.trade_no = trade_no
existed_order.pay_time = datetime.now()
existed_order.save()
将记录保存到数据库中,直接 return Response("success")
返回一个 success 给支付宝,如果不返回 success 给支付宝的话,支付宝会不停的向这个接口发消息
会多次发消息说支付成功,只要我们返回了 success 就不会重发了
验证 False 没有通过,就不返回了,因为是别人在攻击,不做处理
与前端的联调,完成整个支付的流程
登录-> 购买商品 -> 去结算 -> 配送地址
接口在 trade -> OrderViewSet -> perform_create()
在这个逻辑里面,首先将购物车清空,创建我们的order_goods
这里涉及到支付了,支付是需要生成一个支付的页面,如何在这里生成 url
from django.contrib.auth.models import User
from django.utils.timezone import now
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
days_since_joined = serializers.SerializerMethodField()
class Meta:
model = User
def get_days_since_joined(self, obj):
return (now() - obj.date_joined).days
很灵活的字段,它可以让我们自己去写函数,不用依赖与数据表中的某一个字段了
class OrderSerializer(serializers.ModelSerializer):
....
alipay_url = serializers.SerializerMethodField(read_only=True)
read_only 不能让用户自己来提交,服务器端生成返回给用户的
写这个函数的规则是 get_alipay_url 前面加一个 get_
自动会找和它对应的函数,会传递一个 obj - object 就是 serializer 对象
def get_alipay_url(self, obj):
alipay = AliPay(
appid="2016081600258982",
app_notify_url="http://106.14.156.160:8000/alipay/return/",
app_private_key_path=private_key_path, # 个人私钥
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://106.14.156.160:8000/alipay/return/"
)
url = alipay.direct_pay(
subject=obj.order_sn,
out_trade_no=obj.order_sn,
total_amount=obj.order_mount,
)
re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
return re_url # 序列化的时候生成支付宝的支付url
服务器联调要将它上传到服务器,然后使用 browser api 尝试下
生成支付 url 的逻辑没有问题,可以获取到支付宝的 url
alipay_url = serializers.SerializerMethodField(read_only=True)
def get_alipay_url(self, obj):
alipay = AliPay(
appid="2016081600258982",
app_notify_url="http://106.14.156.160:8000/alipay/return/",
app_private_key_path=private_key_path, # 个人私钥
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://106.14.156.160:8000/alipay/return/"
)
url = alipay.direct_pay(
subject=obj.order_sn,
out_trade_no=obj.order_sn,
total_amount=obj.order_mount,
)
re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
return re_url # 序列化的时候生成支付宝的支付url
要把这个逻辑拷贝到 OrderDetailSerializer 里,因为个人中心 我的订单里
也需要 立即使用支付宝支付
这里 appid、return_url 倒可以配置到 settings.py 中,这样代码配置性就比较高
这里支付功能就完成了,怎么成功付款之后跳转返回url
现在介绍一下如何将 vue 前端纳入到 django 里面来,这内容,直接关系到后面的部署方式,
如果采用 django 代理页面的话,一定要认真安装下面的每一个步骤
首先来 vue 项目里面,了解 vue 有两种开发 模式 dev 和 build
npm run build
直接帮我们生成静态文件
这个静态文件,直接放到 django template 里就可以访问了
生成了三个文件
首先把 index.html 拷贝到 templates 文件夹下
在 MxShop 目录之下,新建一个 static 目录
然后将我们的 index.entry.js 拷贝到 static 目录下,还有 static 目录下的所有东西,都拷贝
有了这些步骤之后,设置 settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "static"),
)
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
注意一定要加上 逗号
修改的文件,都需要上传到服务器
index.html 里的路径要修改
<script type="text/javascript" src="/static/index.entry.js"></script></body>
然后直接用 Django 原生的from django.view.generic import TemplateView
配置 url
url(r'^index/', TemplateView.as_view(template_name="index.html"), name="index"),
配置好了 url 之后,实际用户支付成功 return
# return Response("success")
from django.shortcuts import redirect
response = redirect("index")
response.set_cookie("nextPath", "pay", max_age=2)
max_age = 2 设置为 2 秒时间短些,取一次就会过期,
调转到这个页面的时候,希望直接跳转到 pay 页面
现在开发轮播图,我们之前开发与支付相关的功能为了调试,所以放在服务器运行
现在可以在本地进行调试了,本地调试要将 interpreter 切换为本地虚拟环境
配置本地数据库
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'mxshop',
'USER': 'root',
'PASSWORD': 'root1234',
'HOST': '127.0.0.1',
'OPTIONS': {'init_command': 'SET default_storage_engine=INNODB;'}
}
}
class BannerViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
""" 获取轮播图列表"""
serializers.py
class BannerSerializer(serializers.ModelSerializer):
class Meta:
model = Banner
fields = "__all__"
完善 view
class BannerViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
""" 获取轮播图列表"""
queryset = Banner.objects.all().order_by("index")
serializer_class = BannerSerializer
配置 url
router.register(r'banners', BannerViewSet, base_name="banners") # 轮播图
这样就完成了轮播图的开发轮播图+热搜代码片段
这里稍微复杂些,也会涉及到一些细节
分析下复杂的地方:
一对多的关系比较多 大的分类 生鲜食品 里面有很多 商品品牌 还有很多小类 精品肉类、海鲜水产 等
大的分类里面对应的还有商品,这就是三个一对多的关系
有了这个关系就知道 serializer 是嵌套关系:
category = models.ForeignKey(GoodsCategory, null=True, blank=True, verbose_name="商品类别名称", related_name="brands")
设置 related_name 可以让 GoodsCategory 反向来取 GoodsCategoryBrand 比较方便
class BrandSerializer(serializers.ModelSerializer):
class Meta:
model = GoodsCategoryBrand
fields = "__all__"
class IndexCategorySerializer(serializers.ModelSerializer):
brands = BrandSerializer(many=True)
goods = serializers.SerializerMethodField()
sub_cat = CategorySerializer2(many=True)
def get_goods(self, obj):
all_goods = Goods.objects.filter(Q(category_id=obj.id) | Q(category_parent_category_id=obj.id) | Q(
category_parent_category_parent_category_id=obj.id))
goods_serializer = GoodsSerializer(all_goods, many=True)
return goods_serializer.data
class Meta:
model = GoodsCategory
fields = "__all__"
书写 view
class IndexCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
""" 首页商品分类数据 """
queryset = GoodsCategory.objects.filter(is_tab=True)
serializer_class = IndexCategorySerializer
配置url - 首页商品系列数据
router.register(r'indexgoods', IndexCategoryViewSet, base_name="indexgoods")
在 GoodsListViewSet 继承了 RetrieveModelMixin
可以重载 def retrieve() 方法,加入自己的逻辑,实现商品点击数
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
instance.click_num += 1
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data)
收藏数 重载 def perform_create() 方法
当用户创建一个收藏的时候,我们要找到这个 goods ,把它收藏数 +1
def perform_create(self, serializer):
instance = serializer.save()
goods = instance.goods
goods.fav_num += 1
goods.save()
也可以用信号量来解决
delete 和 save Django 都会发送信号量,信号量代码分离性比较好,不需要在业务里写逻辑
from django.db.models.signals import post_save
from django.dispatch import receiver
from user_operation.models import UserFav
@receiver(post_save, sender=UserFav)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
goods = instance.goods
goods.fav_num += 1
goods.save()
用户取消操作
@receiver(post_delete, sender=UserFav)
def delete_userfav(sender, instance=None, created=False, **kwargs):
goods = instance.goods
goods.fav_num -= 1
goods.save()
通过业务分析哪些行为会引起商品库存的变化
新增商品到购物车
修改购物车数量
删除购物车记录
这里做的比较简单,通过用户对购物车的操作,来完成商品库存数的修改
新增商品是 create 操作 重载 def perform_create() 方法
这里就不用信号量来完成了,直接覆盖方法来完成,因为这样更加灵活
class ShoppingCartViewSet(viewsets.ModelViewSet):
......
def perform_create(self, serializer):
shop_cart = serializer.save()
goods = shop_cart.goods
goods.goods_num -= shop_cart.goods_num
goods.save()
这样就完成了商品库存数的修改 - 新增商品到购物车
删除购物车数量
def perform_destroy(self, instance):
goods = instance.goods
goods.goods_num += instance.nums
goods.save()
instance.delete()
修改购物车数量,有可能增加,多买一个,减少,少买一个
def perform_update(self, serializer):
# 先取到保存之前的数据,和现在数据进行比对
existed_record = ShoppingCart.objects.get(id=serializer.instance.id)
existed_nums = existed_record.nums # 之前的数量
saved_record = serializer.save() # 保存之后的数量
nums = saved_record.nums - existed_nums # 修改后的数量-修改前的数量
goods = saved_record.goods
goods.goods_num -= nums
goods.save()
serializer.instance.id
ModelForm 对应的 Model 实例是放在 serializer.instance 里面的
一般是在支付成功之后,商品销量 +1
existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
for existed_order in existed_orders:
通过 OrderInfo 订单基本信息 取 OrderGoods 订单的商品详情
OrderGoods 的外键有一个是 OrderInfo
class OrderGoods(models.Model):
""" 订单的商品详情 """
order = models.ForeignKey(OrderInfo, verbose_name="订单信息", related_name="goods")
这里我们只要使用 related_name 反向取 OrderGoods 就可以了
order_goods = existed_order.goods.all()
for existed_order in existed_orders:
order_goods = existed_order.goods.all()
for order_good in order_goods:
goods = order_good.goods
goods.sold_num += order_good.goods_num
goods.save()
支付成功之后通过 OrderInfo 取到 order 下的goods
一般为了加速网站的访问,会将一些数据,放入缓存中,然后取数据,首先从缓存中取
取完了再从数据库中取,这样会加速网站的反应
先看 Django 的缓存机制 查看文档
这里主要介绍的是 drf 封装的缓存 drf extensions
安装 pip install drf-extensions
他是在 retrieve 和 list 方法之上需要 缓存,一般是获取数据的时候,才需要 cash
做 POST 或者 UPDATE 的时候,肯定是不需要做 cash 的,一般做数据获取 GET 才需要
基本的缓存的设置
from rest_framework_extensions.cache.mixins import CacheResponseMixin
class GoodsListViewSet(CacheResponseMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
还需要一些定制的需求,设置过期时间
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 5
}
后台存储的时候存储到 redis 当中,如何设置 redis 作为 backend 来存储数据
redis 除了能够保存在内存中,以及它能够做数据化以外,它还有一个好处,redis 给我们提供了
client,可以在这里观察一下,每次请求页面的时候,它是不是会给我们新建一个 key,
对数据接口来说并不是把数据保存到内存中就可以了,它实际上会考虑很多细节问题,
比如说在请求列表的时候,请求 html 内容还是请求的是 json 格式,
将数据保存到 redis 当中的时候,它是保存 html?还是保存 json格式呢?
drf 是兼容这两种格式的,要做 redis 缓存的话,必须要考虑这两种格式的
再比如,商品列表页加上不同参数之后,比如过滤器之后,
不同的人请求的 goods list 加了不同过滤器
意味着,缓存还应该与请求参数挂钩,
介绍 redis 的配置,需要使用第三方库 django-redis
pip install django-redis
作为 cache backend 使用配置
为了使用 django-redis , 你应该将你的 django cache setting 改成这样:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
如果设置了密码 直接 redis://:root1234@127.0.0.1:6379/1
/1 - 指的是数据库,也可以不用加
ubuntu 下安装 redis
sudo apt-get install redis-server
启动 redis
redis-server
查看redis 是否启动
redis-cli
redis 127.0.0.1:6379>
127.0.0.1 是本机 ip, 6379是 redis 服务端口, 输入 ping
返回 PONG
说明我们已经成功安装了 redis
防止无限制的爬虫,给服务器造成的压力
drf 自带的限速功能,不需要安装第三方应用,查看官方文档 API Guide Throtting
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
# 'rest_framework.authentication.TokenAuthentication',
# 'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
),
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
}
}
DEFAULT_THROTTLE_RATES 限速的规则 100次数,day 时间区间,一天
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
限速的类 Anon 用户没有登录的情况下 - 通过 ip 地址判断的
Usr 登录的用户 - 通过 token session 判断的
然后在 view 中添加配置
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
class GoodsListViewSet(CacheResponseMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
""" 商品列表页,分页,搜索,过滤,排序 + 详情"""
throttle_classes = (UserRateThrottle, AnonRateThrottle)
请求超过限制,返回
HTTP 429 Too Many Requests
"detail": "请求超过了限速。"
drf thtottle 设置 api 的访问速率 代码片段
第三方登录的开发模式,接入微博登录,页面跳转到微博登录页面,
登录之后的回调
微博开放平台 、 腾讯开放平台、微信开放平台
微博开放平台,创建我的应用,在测试信息里添加测试账号,
在基本信息里收集 App Key 和 App Secret
详细讲解了 Oauth2.0 认证过程
前两个接口很重要
在 utils.py 中,新建一个 weibo_login.py 文件
根据Oauth2/authorize 文档来写接口
回调地址 首先要把这个填写,
client_id 从这里获取到
def get_auth_url():
weibo_auth_url = "https://api.weibo.com/oauth2/authorize"
redirect_url = "http://106.14.156.160:8000/complete/weibo/"
auth_url = weibo_auth_url + "?client_id={client_id}&redirect_uri={re_url}".format(client_id=2710259933,
re_url=redirect_url)
print(auth_url)
if __name__ == "__main__":
get_auth_url()
运行 这个脚本文件,生成一个 URL 地址
https://api.weibo.com/oauth2/authorize?client_id=2710259933&redirect_uri=http://106.14.156.160:8000/complete/weibo/
点击授权,就会跳转到
http://106.14.156.160:8000/complete/weibo/?code=73c564b127cec835989d69d082b07f99
这个 code 很重要,对应的是 OAuth2/access_token 获取授权过的 Access Token
这个 api 才是真正获取到 access token
def get_access_token(code="4c624ebcce5806face8af798ad33f969"):
access_token_url = "https://api.weibo.com/oauth2/access_token"
import requests
re_dict = requests.post(access_token_url, data={
"client_id": 2710259933,
"client_secret": "f01a2549b57a7eb8b8cad60b67b105ff",
"grant_type": "authorization_code",
"code": code,
"redirect_uri": "http://106.14.156.160:8000/complete/weibo/"
})
pass
这里登录成功了,可以取到 token 和 uid,
'{"access_token":"2.00I3jrgCt6y6xC19cdd6a754MXX2FD","remind_in":"157679999","expires_in":157679999,"uid":"2465677512","isRealName":"true"}'
直接使用 api 比如users/show根据用户ID获取用户信息
def get_user_info(access_token="", uid=""):
user_url = "https://api.weibo.com/2/users/show.json?access_token={token}&uid={uid}".format(token=access_token, uid=uid)
print(user_url)
get_user_info(access_token="2.00I3jrgCt6y6xC19cdd6a754MXX2FD", uid="2465677512")
这样就完成了用接口登录,拿到 access_token 和 uid ,再使用他们连接 api 获取相应的信息
爬虫爬取微博的基本思想,
我们不会用这些函数做微博的登录,因为使用第三方集成到 Django 的框架
理解他们登录的原理,爬虫爬取微博的思路
拿到 access_token 是不能直接登录的,只是知道确实有这个账号存在
拿到用户这些基本信息,还需要在我们系统中给用户直接注册一个账号
所以不管他们是用什么登录,最终到系统当中,都是要对应我们的用户的
如果没有就给他新建一个,有的话,就要进行绑定
上面介绍了如果通过接口的方式,去完成 Oauth2.0 的完整过程
下面介绍如何将开源的第三方登录接入到 django rest framework 当中
相当完善的第三方登录解决方案 1. 过程完善 2. 基于 django
安装 pip install social-auth-app-django
查看文档如何使用 configuration - django framework
INSTALLED_APPS = (
...
'social_django',
...
)
第三方登录的时候,这个框架额外的提供了一些数据表的,所以我们需要先做 migrate
生成表,同步到数据库当中去,查看源码在social_django:
它已经把 migrations 文件给生成好了,所以可以直接做 migrate
然后在 settings.py 中加上
AUTHENTICATION_BACKENDS = (
'social_core.backends.open_id.OpenIdAuth',
'social_core.backends.google.GoogleOpenId',
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.google.GoogleOAuth',
'social_core.backends.twitter.TwitterOAuth',
'social_core.backends.yahoo.YahooOpenId',
...
'django.contrib.auth.backends.ModelBackend',
)
一开始我们设置过自定义的 backends,去查询用户的username 或者 mobile
现在在这里加入一些新的
'django.contrib.auth.backends.ModelBackend',
django modelbackend 先加入进来
看上面的都是 google\twitter\yahoo 登录,我们需要的是微博 QQ,
我们先在源码里看下 在 social_core 里 backends 文件夹下
基本包含了,世界上主流的第三方登录 weibo weixin QQ 都是有的
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',
'social_core.backends.weibo.WeiboOAuth2',
'social_core.backends.qq.QQOAuth2',
'social_core.backends.weixin.WeixinOAuth2',
'django.contrib.auth.backends.ModelBackend',
)
接下来配置 URL
url('', include('social_django.urls', namespace='social'))
这里的 url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth, name='begin'),
相当于上面我们在实现微博登录的 get_auth_url()
name='complete'), ```
这个相当于 get_access_token 通过第三方登录返回的 code 拿到 access_token
然后 后续操作,如果当前用户登录了,就绑定用户,如果没有就新建用户
继续配置Template Context Processors
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(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', 'social_django.context_processors.backends', 'social_django.context_processors.login_redirect', ], }, }, ]
要把 OAuth2.0 授权回调设置为本机 ip

还要设置 [Keys and secrets](http://python-social-auth.readthedocs.io/en/latest/configuration/settings.html)
SOCIAL_AUTH_WEIBO_KEY = '2710259933' SOCIAL_AUTH_WEIBO_SECRET = 'f01a2549b57a7eb8b8cad60b67b105ff' SOCIAL_AUTH_QQ_KEY = 'foobar' SOCIAL_AUTH_QQ_SECRET = 'bazqux' SOCIAL_AUTH_WEIXIN_KEY = 'foobar' SOCIAL_AUTH_WEIXIN_SECRET = 'bazqux'
这是在浏览器输入 127.0.0.1:8000/login/weibo/

如果登录之后,跳转到首页,就需要配置
```SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/logged-in/'```
登录之后,查看表

如果第二次登录的话,它就会来这张表来找通过 uid 找 user_id
这里 user_id 对应的就是我们的 user_profile 这张表的 id
这里登录之后,跳转到首页还是 请登录

是因为 django 和 drf 的登录机制是不同的,drf 是在 cookie 里设置 token 的
Django 是在 cookie 设置 session id 的,现在登录整个登录模式都变了
得改造源码所以把 social_core 文件夹,拷贝到 extra_apps 下面

在 do_complete 这个函数里面 会重定向 URL ,这个 url 是 settings.py 配置的
```return backend.strategy.redirect(url)```
所以这里可以做修改
response = backend.strategy.redirect(url)
response.set_cookie("name", )
response.set_cookie("token", )
这里注意下,它做 redirect 的时候,实际上 user 已经拿到了
实际上能拿到 user_prifile 这个对象的,所以可以通过 response 里的 user 生成 token
token 是如何生成的,注册的时候,已经用到过 在 apps -> users -> views.py
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
re_dict = serializer.data
payload = jwt_payload_handler(user)
re_dict["token"] = jwt_encode_handler(payload)
re_dict["name"] = user.name if user.name else user.username
headers = self.get_success_headers(serializer.data)
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)
继续修改 actions.py
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
...
response = backend.strategy.redirect(url)
payload = jwt_payload_handler(user)
response.set_cookie("name", user.name if user.name else user.username, max_age=24*3600)
response.set_cookie("token", jwt_encode_handler(payload))
return response
设置 max_age 过期时间 很重要 24小时 * 3600 秒,一天
[第三方登录,所有的源码修改]()
## <a name="sentry"></a>第13章 sentry 实现错误日志监控
### <a name="sentry_docker"></a>13.1 sentry 的介绍和通过 docker 搭建 sentry
### <a name="sentry_func"></a>13.2 sentry 的功能
### <a name="sentry_drf"></a>13.3 sentry 集成到 django rest framework中
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。