1 Star 0 Fork 7

猪奋斗/django-rest-framework

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
MIT

django-rest-framework

Vue+Django REST framwork 学习笔记

Django 官方文档

Django 1.8.2 中文文档

Django 1.11.6 中文文档

Django rest framework 官方文档

Django rest framework jwt 官方文档

导航

  1. 课程介绍

  2. 开发环境搭建

  3. model 设计和资源导入

  4. vue的结构和restful api 介绍

  5. 商品列表页

    1. 5.1 django的view实现商品列表页
    2. 5.2 Django 的 serializer 序列化 model
    3. 5.3-5.4 apiview方式实现商品列表页
    4. 5.5.1 drf-serializer
    5. 5.5.2 ModelSerializers
    6. 5.6.1 GenericView 方式实现商品列表页
    7. 5.6.2 商品列表页的分页功能
    8. 5.7 viewsets 和 router 完成商品列表页
    9. 5.8 APIView\GenericView\Viewset\router分析
    10. 5.9 drf 的 request 和 response
    11. 5.10 drf 的过滤
    12. 5.11 drf 的搜索和排序
  6. 商品类别数据和vue展示

    1. 6.1-6.2 商品类别数据列表页和详情页
    2. 6.3 vue 展示商品分类数据
  7. 用户登录和手机注册

    1. 7.1 drf 的 token 登录和原理 - 1
    2. 7.2 drf 的 token 登录和原理 - 2
    3. 7.3 viewsets 配置认证类
    4. 7.4 json web token 的原理
    5. 7.5 json web token 方式完成用户认证
    6. 7.6 vue 和 jwt 接口调试
    7. 7.7 云片网发送短信验证码
    8. 7.8-7.9 drf 实现发送短信验证码接口
    9. 7.10 user serializer 和 validator 验证 - 1
    10. 7.11 user serializer 和 validator 验证 - 2
    11. 7.12 django 信号量实现用户密码修改
    12. 7.13 vue 和注册功能调试
  8. 商品详情页功能

    1. drf 权限验证
  9. 个人中心功能开发

    1. 9.1 drf 的api 文档自动生成和功能详解
    2. 9.2 动态设置 serializer 和 permission 获取用户信息
    3. 9.3 vue 和 用户接口信息联调
    4. 9.4 用户个人信息修改
    5. 9.5 用户收藏功能
    6. 9.6 用户留言功能
    7. 9.7 用户收货地址列表页接口开发
  10. 购物车、订单管理和支付功能

    1. 10.1 购物车功能需求分析和加入到购物车实现
    2. 10.2 修改购物车数量
    3. 10.3 vue 和 购物车接口联调
    4. 10.4-10.5 订单管理接口 -1
    5. 10.6 vue 个人中心订单接口调试
    6. 10.7-10.8 pycharm 远程代码调试 -1
    7. 10.9 支付宝公钥、私钥和沙箱环境的配置
    8. 10.10 支付宝开发文档解读
    9. 10.11 支付宝支付源码解读
    10. 10.12 支付宝通知接口验证
    11. 10.13-10.14 django 集成支付宝 notify_url 和 return_url 接口
    12. 10.15 支付宝接口和 vue 联调 -1
    13. 10.16 支付宝接口和 vue 联调 -2
  11. 首页、商品数量、缓存、限速功能开发

  12. 第三方登录

    1. 12.1 第三方登录开发模式以及 Oauth2.0 简介
    2. 12.2 Oauth2.0 获取微博的access_token
    3. 12.3 social_django 集成第三方登录
  13. sentry实现错误日志监控

    1. 13.1 sentry 的介绍和通过 docker 搭建 sentry
    2. 13.2 sentry 的功能
    3. 13.3 sentry 集成到 django rest framework中

3-1 项目初始化

这个项目是 python3.6 环境,要先新建 虚拟环境

conda info --envs    # 查看当前所有的虚拟环境 

conda create --name VueShop python=3.6

django-rest-framework

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:

新建 python package 文件夹,apps 来放置所有的app,把 users 移入

新建 extra_apps 放置第三方的包

directory:

media 上传文件图片

db_tools python 脚本文件,数据库初始化等等,有用的外部脚本设置

大概先设置成这个样子,以后根据需要做相应的调整

apps 和 extra_apps 设置

将 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'))

3-2 user models 设计

通过需求分析,然后设计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 里放入这句话才会替换系统用户

第4章 vue 的结构和 restful api 介绍

4.1 前后端分离优缺点

为什么要前后端分离

  1. pc, app, pad 多端适应
  2. SPA 开发模式开始流行(单页面应用)
  3. 前后端开发职责不清(template的书写)
  4. 开发效率问题,前后端互相等待
  5. 前端一直配合着后端,能力受限
  6. 后端开发语言和模版高度耦合,导致开发语言依赖严重

前后端分离缺点

  1. 前后端学习门槛增加
  2. 数据依赖导致文档重要性增加
  3. 前端工作量加大
  4. SEO 的难度加大(搜索引擎优化的排名)
  5. 后端开发模式迁移增加成本

restful api

restful api 目前是前后端分离最佳实践 (标准)

  1. 轻量,直接通过http, 不需要额外的协议, post/get/put/delete等操作
  2. 面向资源,一目了然,具有自解释性
  3. 数据描述简单,一般通过 json 或者 xml 做数据通信

restful api 重要概念

  1. 概念
  2. restful 实践

总结一下什么是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 - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。   

4.2 vue 的基本概念介绍

几个概念

  1. 前端工程化
  2. 数据双向绑定
  3. 组件化开发
  4. webpack
  5. vue, vuex, vue-router, axios
  6. ES6, babel

第5章 商品列表页

5-1 django的view实现商品列表页

通过学习 Django-rest-framework 来让我们快速的搭建 restful api 本章是所有的起点,很重要的章节

首先通过 django 实现一个 api 或者 json 的返回

cbv 基于 class base view - 官方推荐编码方式,代码可重用性高一些 fbv 基于 function base view

1. 配置 url

url设计规范最好是名词复数 goods

from django.conf.urls import url
# from django.contrib import admin

urlpatterns = [
    # url(r'^admin/', admin.site.urls),

    # 商品列表页
    url(r'goods/$', )
]

2. 书写 view

为了区分和 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"

3. 配置到 url

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 样式

5.2 Django 的 serializer 序列化 model

    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)

5.3 - 5.4 apiview方式实现商品列表页

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 文件

serializer 的书写

# -*- 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"),

5.5 drf 的 modelserializer 实现商品列表页功能

5.5.1 serializer

上面介绍了 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 来验证

接收前端post的数据,然后保存到数据库中

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

代码变动

5.5.2 ModelSerializer

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 就是外键的实例化来覆盖默认的

这样就可以非常的简单的完成它的序列化,代码非常的少,序列化的嵌套也可以很好的完成

代码变动

5.6 GenericView 方式实现商品列表页和分页功能详解

5.6.1 商品列表页

本节课不在使用 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

在github上查看

5.6.2 商品列表页的分页功能

分页所以要取所有商品

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,
# }

分页代码的实现

5.7 viewsets 和 router 完成商品列表页

最重要的 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

查看官方文档 ViewSets & Routers

现在 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")),
]

viewsets 和 router 的使用

5.8 drf 的 APIView、GenericView、Viewset 和 router 的原理分析

要会使用 ViewSet,而不至于很混乱,搞清楚他们之间的关系

很好的决定使用哪一个 view 或者 组合他们使用

GenericViewSet (viewset) drf

  • 继承自 GenericAPIView drf
  • 继承自 APIView 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

差别

  1. get list 的绑定 和 ViewSetMixin

本身写在代码中的绑定,可以在 url 中实现动态的绑定,或者通过 router 配置

  1. initialize_request - 给 request 中绑定了很多 action,动态 serializer 就会有很大好处

5.9 drf 的 request 和 response

request - 浏览器发送请求过来之后,drf会对它做一定的封装

request 官方文档

request parsing 用户发送数据过来之后,drf 会对它做一定的解析

    request.data = request.POST + request.FILES

它包括所有解析的内容,文件和非文件的 input,解析了http 方法的内容

    .query_params - get 请求参数

    .parsers - 解析器 解析传递过来各种类型的数据

content negotiation

authentication

    .user 获取到当前的用户

Responses - 根据前端返回的请求,返回 html 或者 json

5.10 drf 的过滤

通过 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

查看文档 API Guide 中的 Filtering

列表页常有的功能

过滤 - 精确字段过滤 - 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 地址,查看官方文档

这里只关注一下 drf 的继承

我们新建一个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

filter 代码修改

5.11 drf 的搜索和排序

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' 就是完全匹配

进一步做商品的搜索:

查看 drf 的 SearchFilter 文档

首先引入 rest_framework 的 filters

from rest_framework import filters

然后在 view 函数里面配置

    filter_backends = (DjangoFilterBackend, filters.SearchFilter)
    search_fields = ('name', 'goods_brief', 'goods_desc')

这样就可以直接使用了

Ordering 排序

首先配置 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')

这短短的几行代码完成了,列表页、分页、搜索、过滤、排序

搜索、排序代码

第6章 商品类别数据和 vue 展示

6.1-6.2 商品类别数据列表页和详情页

先完善商品类别的功能

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 就可以了

获取商品分类列表和详情代码

6.3 vue 展示商品分类数据

将商品分类列表数据和前端 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 数据,显示到前端 导航栏和 全部商品分类栏 中

setttings.py 代码的变动

第7章 用户登录和手机注册

7.1 drf 的 token 登录和原理

前后分离的系统,不需要做 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 插件,调试

settings.py代码修改

7.2 drf 的 token 登录和原理 - 2

返回的是 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,统统调用一遍

django从请求到返回都经历了什么

token 认证模式在前后端分离中使用比较常见的,但是 drf token 有很大的问题

  1. token 是保存到服务器当中的,如果是分布式系统的话,或者说有两套系统想用同一套认证系统的话 需要用户同步,实际上是比较麻烦的

  2. 这个token 是永久有效的,它没有过期时间,一旦泄露了,别人可以一直拿来用

7.3 viewsets 配置认证类

上面介绍了 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,)

修改代码

7.4 json web token 的原理

前后端分离之JWT用户认证

7.5 json web token 方式完成用户认证

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 配置 和 settings 配置 变动代码

7.6 vue 和 jwt 接口调试

后台和前端保持一致,把后台 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',
}

jwt 的设置和自定义用户验证代码修改

7.7 云片网发送短信验证码

实现 drf 用户手机注册的功能

用户注册的功能大量用到 serializer 高级用法

首先通过前端页面分析,手机注册需要提供哪些接口

免费获取验证码 - 专门发送短信的接口

用户在回填短信验证码之后,表单提交的验证

验证失败的提示

验证成功的跳转

首先实现手机号码发送验证码的功能 - 使用第三方服务 云片网

这个 APIKEY 后面要用到,很重要

发送短信首先要申请签名

签名需要审核的,还需要新增模版

单条发送接口 API 文档

现在新建一个 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 地址设置进来

云片网发送短信验证码代码

7.8-7.9 drf 实现发送短信验证码接口

因为发送短信验证码是用户操作所以在 users viws.py 里添加代码:

实际上我们之前在 models 里,设计了 VerifyCode 这张表,

发送短信验证码我们实际可以看作是对这张表进行操作 - 所以是 create 操作

from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets

...

class SmsCodeViewSet(CreateModelMixin, viewsets.GenericViewSet):
    """ 发送短信验证码 """

再继续写下面的逻辑之前,先理清需求

用户传递过来电话号码,需要对它做验证:

  1. 是否是合法的手机号码
  2. 手机号码有没有被注册过

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}$"

settings.py 代码的变动

# -*- 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 代码的变动

现在通过 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)

view 代码的变动

设置 url

from users.views import SmsCodeViewSet

router.register(r'codes', SmsCodeViewSet, base_name="codes")

url代码的变动

手机短信发送的一个接口 发送短信成功

7.10 user serializer 和 validator 验证 - 1

完成注册功能

首先看下注册页面的分析,要输入手机号码、验证码、密码 三个字段

需要这三个字段编写后台的 注册 接口,之前说过 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")

首先想下验证码错误有多少种情况

  1. 输入错误,不存在 - 从数据库取
  2. 长度不是4位
  3. 填写验证码超过10分钟或者1分钟是否过期呢,怎么提醒
  4. 连续两个验证码,只取最后一个,还是都可以验证通过
    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)
  1. 随机发送验证码,有可能会发送两天相同的验证码给同一个人,所以这时会有2个记录,会错误
  1. 如果不匹配,可能不存在这条记录的

这两种情况 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 就更加灵活

代码改动记录

7.11 user serializer 和 validator 验证 - 2

虽然在 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

serializer代码修改查看

看这里的逻辑就比较明确了 - 这里做统一的处理

这样 UserRegSerializer 就完成了,接下来,继续完成 UserViewSet

首先要 import 进来 from .serializer import UserRegSerializer

个人中心的时候,用户做个人资料修改的时候,需要用到不同的 Serializer

class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
    """ 用户 """
    serializer_class = UserRegSerializer

view代码变动

现在,可以写 url 配置

from users.views import UserViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', UserViewSet, base_name="users")

url代码变动查看

这样就可以在 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 参数

7.12 django 信号量实现用户密码修改

上面对 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 官方文档

django 1.8 中文文档

drf 1.11.6 中文文档

drf 文档里之前也看到了 post_save()

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 它帮我们发送的,一定要注意这点,内置的信号他会在适当的时候给我们发送,

但是自己设置信号,信号的发送和接收就得自己去写

7.13 vue 和注册功能调试

前端的 代码

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

这样就可以将数据定制化

代码变动

第8章 商品详情页功能

首先在列表页中任意点击一个商品,进入详情页,分析详情页功能,

商品轮播图、商品详情-名称、描述、是否免运费、市场价、促销价

商品售量,库存量,加入购物车、收藏

商品详情页富文本描述

右侧 热卖商品

所以这里我们只需要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 必须能构成唯一集合."
    ]
}

UniqueTogetherValidator

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 的权限验证

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 去搜索的,所以这个是可以配置的

配置方法在 drf 文档里也有

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

所以可以直接来搜索这个字段,现在可以测试下看看是不是根据这个字段来找的

用户收藏功能通过goods_id查询实现

第9章 个人中心功能开发

9.1 drf 的api 文档自动生成和功能详解

Documenting your API

添加 Description 的方法:

  1. 在 models.py 的字段中添加 help_text 属性
goods = models.ForeignKey(Goods, verbose_name="商品", help_text="商品id")

  1. 把 help_text 加到 serializer 上

  1. filters.py 中加 help_text

代码变动

9.2 动态设置 serializer 和 permission 获取用户信息

要完成的第一个功能 - 用户个人信息的修改

姓名:

出生日期:

性别:

电子邮箱:

手机: - 需要单独页面提交

需要一个接口,可以让用户请求 用户信息 - 之前有个 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
  1. 重写 get_object() 方法,这个 get_object() 实际上是来控制 RetrieveModelMixin/Delete的
    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

这样接口就算完成了代码变动对比

9.3 vue 和 用户接口信息联调

9.4 用户个人信息修改

UserViewSet 配置 mixins.UpdateModelMixin

9.5 用户收藏功能

之前遗留的问题:在个人中心获取收藏记录的时候,我们不仅想要 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 序列化代码片段

这里又涉及到动态 serializer 问题

    def get_serializer_class(self):
        if self.action == 'list':
            return UserDetailSerializer
        elif self.action == 'create':
            return UserFavSerializer

        return UserFavSerializer

用户收藏功能 view 的动态序列化代码片段

9.6 用户留言功能

用户留言功能 - 删除、获取留言、添加留言(可以上传文件)

首先写后台接口 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="文档功能")),
]

url代码修改

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)

用户留言 view 代码片段

来看下文档:

删除功能需要服务器返回给前端 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")

最后完整的留言功能的 serializer 序列化代码片段

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

留言功能的代码片段

9.7 用户收货地址列表页接口开发

收货地址功能

获取所有的收货地址、修改某一个收货地址、删除某一个收货地址

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 是否合法啊

收货地址功能开发的代码片段

第10章 购物车、订单管理和支付功能

10.1 购物车功能需求分析和加入到购物车实现

需求分析:

  1. 购物车如果没有就添加一条记录,
  2. 购物车如果有 记录就加一
  3. 在购物车页面 数量 +1 -1 操作 - 更新它的数量
  4. 删除购物车商品

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 配置

购物车添加商品和更新商品数量功能代码片段

10.2 修改购物车数量

上面介绍了加入购物车功能,下面介绍购物车其他的功能,首先是列表页

重写 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() 是不需要重写的

购物车列表功能,更新功能,删除功能 代码片段

10.3 vue 和 购物车接口联调

购物车页面的商品的详情: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 前端联调

10.4 订单管理接口 -1

首先理解下购物车和订单之间的关系,实际上用户在购买商品的时候,都会放很多商品进入我们的购物车,然后去购物车去进行结算,这里我们采用的是比较简单的做法是将购物车里所有的商品进行结算,实际上比如淘宝,实际上是可以只对购物车里某一个商品进行结算的

我们看 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__"

现在分析下逻辑,首先用户提交的时候,创建一个订单,提交订单的时候,就不会提交每一个商品了,因为现在逻辑是比较简单的逻辑,就是直接清空购物车

首先我们获取到用户要创建订单这个需求之后,我们要将购物车里所有商品信息拿出来

  1. 到我们 OrderGoods 里添加记录,意思就是说把我们购物车里的数据,添加到 OrderGoods 里来

  2. 要将购物车里的记录删除

实际上在 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 操作代码片段

10.6 vue 个人中心订单接口调试

在 个人中心 我的订单 中,有订单状态、商品列表、收货人信息,

这里发现后台接口并没有完善好,查看订单详情的时候,应该将订单的商品列表给列出来,

序列化的时候应该获取到订单的商品列表,现在完善后台接口

在 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)

修改的代码记录

10.7 pycharm 远程代码调试 -1

关于后面第三方支付,和第三方登录的时候,我们都是有个 回调 的 url ,

这个 url 一般指向的是服务器的 ip 地址

为了便于调试,所以我们首先需要能够完成在本地 pycharm 去调试远端的服务器

这样我们在做回调的时候,就可以调试代码了

首先要将代码上传到服务器中

代码同步了,必须在远程服务器上建立虚拟环境

安装 python 3.6

安装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

安装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

安装mysql

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;

如果外部无法连接服务器端口,查看防火墙设置:

10.9 支付宝公钥、私钥和沙箱环境的配置

首先看对接支付宝支付接口,需要做哪些准备?

  1. 进入蚂蚁金服开放平台管理中心
  2. 电脑网站支付-开放平台文档
  3. 生成RSA密钥

  1. 把密钥保存到 trade 文件夹下新建 keys,但是注意要在前后加上
-----BEGIN PRIVATE KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzBqHhrDaCyqtdEn+LUeLL83EmnyUVNSQxk6AKMROoJMGszl01Ly3UmKQLnSPIJLF+wGFjDvCs0Cjoi0KOJSmGFecVZ14kS9ZHCj1MdotUkpDj28sViaQXPW7LlLdRuaxSen6sQBwOyqmHV50a7ummrX9EHeQqToShS0lm1brbegkcmtoVrBOv+ehVQDyB76pyukn9N7K8P0SRgPtF7m4zyloJM9ZXnGMxWApj0jRK1uYfWDNXySgtOtyJEhUjufcmtu2Cq/konP+qR3ZZSZG4++dKXQQS5npwsUoywLWsSo6Vf+qjHhKj/v/oOMF1PAKyCvJVIU2599rawA6kROgXwIDAQAB
-----END PRIVATE KEY-----
  1. 复制支付宝公钥

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5h8p1Ufg8Je8DiEPlHNFX828QvWJdzZ6A3HapmNvbK8OcllOD+uJidaiPHDoi2mtCOH7busI1IALv6XaPPWsT5B2FQbABIqncvpH9gUbonWff2/PsWitVay6xcEkHIW4WlUm6DXbN/hTu8dxIC6yJhLKFKG5TZHwOkQgk+IaU4xkJb1D8YZPBy8AojA+0hmtLrn5QGyQH5TyphyZ9DdjvvI5fCu68pRRdHMl3JJeoevuivzWVV33PZW11lOPcznP26cLeI/aAB3UO7C1a9+n7gPxmU/meK4RGxyXNqbmpOoX8ayFGe/h5u8p1QTXJf94D4KDnXXWTmg5PqUC9Ok80QIDAQAB

查询订单状态的时候会用到这个 支付宝公钥

代码变动

10.10 支付宝开发文档解读

上面为了支付宝支付做了准备工作,下面编码实现支付宝的支付接口

首先为了完成支付的接口,将 pycharm 的代码设置Project Interpreter为本地,先在本地调试

然后看如何对接支付宝的支付接口,先了解下支付宝支付的官方文档

电脑网站支付产品介绍 API 列表 统一收单下单并支付页面接口

代码变动

10.11 支付宝支付源码解读

10.12 支付宝通知接口验证

10.13 django 集成支付宝 notify_url 和 return_url 接口 -1

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 没有通过,就不返回了,因为是别人在攻击,不做处理

支付宝 view 代码片段

10.15 支付宝接口和 vue 联调 -1

与前端的联调,完成整个支付的流程

登录-> 购买商品 -> 去结算 -> 配送地址

接口在 trade -> OrderViewSet -> perform_create()

在这个逻辑里面,首先将购物车清空,创建我们的order_goods

这里涉及到支付了,支付是需要生成一个支付的页面,如何在这里生成 url

  1. 重写 CreateModelMixin 里的 create() 方法 把支付宝支付url 放在 serializer 里面
  2. serializer 的另一个功能 SerializerMethodField
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

生成支付页面 代码片段

10.16 支付宝接口和 vue 联调 -2

现在介绍一下如何将 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 页面

代码修改

第11章 首页、商品数量、缓存、限速功能开发

现在开发轮播图,我们之前开发与支付相关的功能为了调试,所以放在服务器运行

现在可以在本地进行调试了,本地调试要将 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

对商品销量数量的修改的代码片段

drf 的缓存设置

一般为了加速网站的访问,会将一些数据,放入缓存中,然后取数据,首先从缓存中取

取完了再从数据库中取,这样会加速网站的反应

先看 Django 的缓存机制 查看文档

Django 1.11.6 文档 Django的缓存框架

这里主要介绍的是 drf 封装的缓存 drf extensions

安装 pip install drf-extensions

CacheResponseMixin

他是在 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
}

drf 的缓存设置 代码

drf 配置 redis 缓存

后台存储的时候存储到 redis 当中,如何设置 redis 作为 backend 来存储数据

redis 除了能够保存在内存中,以及它能够做数据化以外,它还有一个好处,redis 给我们提供了

client,可以在这里观察一下,每次请求页面的时候,它是不是会给我们新建一个 key,

对数据接口来说并不是把数据保存到内存中就可以了,它实际上会考虑很多细节问题,

比如说在请求列表的时候,请求 html 内容还是请求的是 json 格式,

将数据保存到 redis 当中的时候,它是保存 html?还是保存 json格式呢?

drf 是兼容这两种格式的,要做 redis 缓存的话,必须要考虑这两种格式的

再比如,商品列表页加上不同参数之后,比如过滤器之后,

不同的人请求的 goods list 加了不同过滤器

意味着,缓存还应该与请求参数挂钩,

介绍 redis 的配置,需要使用第三方库 django-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

配置 redis 代码

drf 的 throttle 设置 api 访问速率

防止无限制的爬虫,给服务器造成的压力

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 的访问速率 代码片段

第12章 第三方登录

12.1 第三方登录开发模式以及 Oauth2.0 简介

第三方登录的开发模式,接入微博登录,页面跳转到微博登录页面,

登录之后的回调

微博开放平台 、 腾讯开放平台、微信开放平台

微博开放平台,创建我的应用,在测试信息里添加测试账号,

在基本信息里收集 App Key 和 App Secret

查看文档-Oauth2.0授权认证

详细讲解了 Oauth2.0 认证过程

前两个接口很重要

12.2 Oauth2.0 获取微博的access_token

在 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 是不能直接登录的,只是知道确实有这个账号存在

拿到用户这些基本信息,还需要在我们系统中给用户直接注册一个账号

所以不管他们是用什么登录,最终到系统当中,都是要对应我们的用户的

如果没有就给他新建一个,有的话,就要进行绑定

微博登录源码的理解

12.3 social_django 集成第三方登录

上面介绍了如果通过接口的方式,去完成 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
![](http://ovc37dj11.bkt.clouddn.com/15088933221367.jpg)

还要设置 [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/

![](http://ovc37dj11.bkt.clouddn.com/15088936831383.jpg)

如果登录之后,跳转到首页,就需要配置 

```SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/logged-in/'```

登录之后,查看表 

![](http://ovc37dj11.bkt.clouddn.com/15088938891324.jpg)

如果第二次登录的话,它就会来这张表来找通过 uid 找 user_id

这里 user_id 对应的就是我们的 user_profile 这张表的 id

这里登录之后,跳转到首页还是 请登录 

![](http://ovc37dj11.bkt.clouddn.com/15088943260867.jpg)

是因为 django 和 drf 的登录机制是不同的,drf 是在 cookie 里设置 token 的

Django 是在 cookie 设置 session id 的,现在登录整个登录模式都变了

得改造源码所以把 social_core 文件夹,拷贝到 extra_apps 下面

![](http://ovc37dj11.bkt.clouddn.com/15088980881696.jpg)

在 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中
The MIT License (MIT) Copyright (c) 2017 custer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

简介

coding.imooc.com/learn/list/131.html 展开 收起
Python
MIT
取消

发行版

暂无发行版

贡献者 (2)

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Python
1
https://gitee.com/wjbhydj/django-rest-framework.git
git@gitee.com:wjbhydj/django-rest-framework.git
wjbhydj
django-rest-framework
django-rest-framework
master

搜索帮助