# django-rest-framework
**Repository Path**: wjbhydj/django-rest-framework
## Basic Information
- **Project Name**: django-rest-framework
- **Description**: coding.imooc.com/learn/list/131.html
- **Primary Language**: Python
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 6
- **Created**: 2023-06-01
- **Last Updated**: 2023-06-01
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# django-rest-framework
[Vue+Django REST framwork 学习笔记](http://coding.imooc.com/learn/list/131.html)
[Django 官方文档](https://docs.djangoproject.com/en/dev/)
[Django 1.8.2 中文文档](http://python.usyiyi.cn/translate/django_182/index.html)
[Django 1.11.6 中文文档](http://python.usyiyi.cn/translate/Django_111/index.html)
[Django rest framework 官方文档](http://www.django-rest-framework.org)
[Django rest framework jwt 官方文档](http://getblimp.github.io/django-rest-framework-jwt/)
### 导航
1. [课程介绍](#intro)
2. [开发环境搭建](#env)
3. [model 设计和资源导入](#model)
4. [vue的结构和restful api 介绍](#vue-restful-api)
5. [商品列表页](#shop-list)
1. [5.1 django的view实现商品列表页](#shop-list)
2. [5.2 Django 的 serializer 序列化 model](#serializer)
3. [5.3-5.4 apiview方式实现商品列表页](#apiview)
4. [5.5.1 drf-serializer](#drf-serializer)
5. [5.5.2 ModelSerializers](#modelserializers)
6. [5.6.1 GenericView 方式实现商品列表页](#mixins)
7. [5.6.2 商品列表页的分页功能](#pagination)
8. [5.7 viewsets 和 router 完成商品列表页](#viewsets)
9. [5.8 APIView\GenericView\Viewset\router分析](#analyze)
10. [5.9 drf 的 request 和 response](#request)
11. [5.10 drf 的过滤](#filtering)
12. [5.11 drf 的搜索和排序](#search)
6. [商品类别数据和vue展示](#shop-category)
1. [6.1-6.2 商品类别数据列表页和详情页](#shop-category)
2. [6.3 vue 展示商品分类数据](#vue-category)
7. [用户登录和手机注册](#login)
1. [7.1 drf 的 token 登录和原理 - 1](#login)
2. [7.2 drf 的 token 登录和原理 - 2](#drf-token)
3. [7.3 viewsets 配置认证类](#global-token)
4. [7.4 json web token 的原理](#jwt-theo)
5. [7.5 json web token 方式完成用户认证](#jwt)
6. [7.6 vue 和 jwt 接口调试](#setting-jwt)
7. [7.7 云片网发送短信验证码](#yunpian)
8. [7.8-7.9 drf 实现发送短信验证码接口](#drf-sms)
9. [7.10 user serializer 和 validator 验证 - 1](#userviewset)
10. [7.11 user serializer 和 validator 验证 - 2](#user-serializer)
11. [7.12 django 信号量实现用户密码修改](#signal)
12. [7.13 vue 和注册功能调试](#vue-login)
8. [商品详情页功能](#shop-detail)
1. [drf 权限验证](#drf-permission)
9. [个人中心功能开发](#center)
1. [9.1 drf 的api 文档自动生成和功能详解](#center)
2. [9.2 动态设置 serializer 和 permission 获取用户信息](#serializer-permission)
3. [9.3 vue 和 用户接口信息联调](#vue-center)
4. [9.4 用户个人信息修改](#user-info)
5. [9.5 用户收藏功能](#user-fav)
6. [9.6 用户留言功能](#user-sms)
7. [9.7 用户收货地址列表页接口开发](#user-address)
10. [购物车、订单管理和支付功能](#shopping)
1. [10.1 购物车功能需求分析和加入到购物车实现](#shopping)
2. [10.2 修改购物车数量](#shop-car)
3. [10.3 vue 和 购物车接口联调](#vue-car)
4. [10.4-10.5 订单管理接口 -1](#order1)
5. [10.6 vue 个人中心订单接口调试](#vue-center-order)
6. [10.7-10.8 pycharm 远程代码调试 -1](#pycharm1)
7. [10.9 支付宝公钥、私钥和沙箱环境的配置](#alipay)
8. [10.10 支付宝开发文档解读](#alipay-doc)
9. [10.11 支付宝支付源码解读](#alipay-code)
10. [10.12 支付宝通知接口验证](#alipay-notify)
11. [10.13-10.14 django 集成支付宝 notify_url 和 return_url 接口](#django-alipay1)
12. [10.15 支付宝接口和 vue 联调 -1](#vue-alipay1)
13. [10.16 支付宝接口和 vue 联调 -2 ](#vue-alipay2)
11. [首页、商品数量、缓存、限速功能开发](#home)
12. [第三方登录](#part-login)
1. [12.1 第三方登录开发模式以及 Oauth2.0 简介](#part-login)
2. [12.2 Oauth2.0 获取微博的access_token](#oauth)
3. [12.3 social_django 集成第三方登录](#social_django)
13. [sentry实现错误日志监控](#sentry)
1. [13.1 sentry 的介绍和通过 docker 搭建 sentry](#sentry_docker)
2. [13.2 sentry 的功能](#sentry_func)
3. [13.3 sentry 集成到 django rest framework中](#sentry_drf)
### 3-1 项目初始化
这个项目是 python3.6 环境,要先新建 虚拟环境
```
conda info --envs # 查看当前所有的虚拟环境
conda create --name VueShop python=3.6
```
[django-rest-framework](http://www.django-rest-framwork.org)
```
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. [概念](http://www.ruanyifeng.com/blog/2011/09/restful.html)
2. [restful 实践](http://www.ruanyifeng.com/blog/2014/05/restful_api.html)
总结一下什么是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 中书写
结合 [官方文档](http://www.django-rest-framework.org) 和代码实例
```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](http://www.django-rest-framework.org/tutorial/3-class-based-views/)
这个例子介绍 简单快速的来写一个 list
序列化类,我们可以自己定义序列化类,这个序列化类和form、modelform
modelform可以直接将字段转成html
drf 的 serializer 实际上取代 django 的 form 开发的
form 开发是针对 html 的。 serializer 是针对 json 的
serializer 的定义和之前 form 一样,新建一个 serializers.py 文件
[serializer 的书写](http://www.django-rest-framework.org/tutorial/1-serialization/)
```
# -*- 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 拿到字段之后也是可以做保存的,把字段保存到数据库当中
[查看官方文档](http://www.django-rest-framework.org/tutorial/1-serialization/#creating-a-serializer-class)
在 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的数据,然后保存到数据库中](http://www.django-rest-framework.org/tutorial/3-class-based-views/#rewriting-our-api-using-class-based-views)
```
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
```
[代码变动](https://gitee.com/custer_git/django-rest-framework/commit/a1d3218947e6a6b782c9a60b0530f47665135406#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_481_621)
#### 5.5.2 ModelSerializer
ModelSerializer 和 ModelForm 是一样的
让我们省去了所有字段的添加
查看官方文档[Using Modelserializers](http://www.django-rest-framework.org/tutorial/1-serialization/#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 就是外键的实例化来覆盖默认的
这样就可以非常的简单的完成它的序列化,代码非常的少,序列化的嵌套也可以很好的完成
[代码变动](https://gitee.com/custer_git/django-rest-framework/commit/d08512bca8d7703cca9258f7753c3919f29fcfb9#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_621_718)
### 5.6 GenericView 方式实现商品列表页和分页功能详解
#### 5.6.1 商品列表页
本节课不在使用 APIView 来实现,使用更加上层的 View
[使用 mixins 和 GenericAPIView 来使代码变的更加简洁](http://www.django-rest-framework.org/tutorial/3-class-based-views/#using-mixins)
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)
```
[查看修改的代码](https://gitee.com/custer_git/django-rest-framework/commit/3a7d2c4ba996366c9405d11b4da1803dc3d7c11f#6d4a72fc2f1d8464c6074128401996024d6a3c02_18_18)
不重载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](http://www.django-rest-framework.org/api-guide/pagination/#setting-the-pagination-style)
```
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,
# }
```
[分页代码的实现](https://gitee.com/custer_git/django-rest-framework/commit/3c348d30f8ce07910a4bf4e5b71e3a199418cd61#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_826_883)
### 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](http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/#binding-viewsets-to-urls-explicitly)
现在 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 的使用](https://gitee.com/custer_git/django-rest-framework/commit/3008012672cbcf5e34c26ee383c2fe7489a9ba39#072362b84bccca705ae7ff97bfc981c164ccf5c1_512_541)
### 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 配置
2. initialize_request - 给 request 中绑定了很多 action,动态 serializer 就会有很大好处
### 5.9 drf 的 request 和 response
request - 浏览器发送请求过来之后,drf会对它做一定的封装
[request 官方文档](http://www.django-rest-framework.org/api-guide/requests/)
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](http://www.django-rest-framework.org/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 地址](https://github.com/carltongibson/django-filter),查看[官方文档](https://django-filter.readthedocs.io/en/latest)
[这里只关注一下 drf 的继承](http://django-filter.readthedocs.io/en/latest/guide/rest_framework.html#adding-a-filterset-with-filter-class)
我们新建一个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 代码修改](https://gitee.com/custer_git/django-rest-framework/commit/e6a9904daa5fd32f4fa2288c414caf547e5d430e#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_1092_1220)
### 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 文档](http://www.django-rest-framework.org/api-guide/filtering/#searchfilter)
首先引入 rest_framework 的 filters
```
from rest_framework import filters
```
然后在 view 函数里面配置
```
filter_backends = (DjangoFilterBackend, filters.SearchFilter)
search_fields = ('name', 'goods_brief', 'goods_desc')
```
这样就可以直接使用了

[Ordering 排序](http://www.django-rest-framework.org/api-guide/filtering/#specifying-which-fields-may-be-ordered-against)
首先配置 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")
```
[获取所有分类数据](https://gitee.com/custer_git/django-rest-framework/commit/95016ae904673a56cbe4b7ba0fdc90089cd24c10#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_1299_1335)
修改商品分类数据层次结构
获取第一类 商品分类
``` 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 就可以了
[获取商品分类列表和详情代码](https://gitee.com/custer_git/django-rest-framework/commit/55403f88f3820389b864bb5002608df66f212328#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_1335_1402)
### 6.3 vue 展示商品分类数据
将商品分类列表数据和前端 view 做调试
调试之前要解决跨域的问题,跨域问题在前后端分离开发当中,非常的常见
前端通过npm 设置 proxy 代理也可以解决跨域问题
这里重点讲解服务器解决跨域的方法,搜索 [github django cors headers](https://github.com/ottoyiu/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 代码的变动](https://gitee.com/custer_git/django-rest-framework/commit/6f95dcfa8d59093cfdf47ba68625031730e0733d#072362b84bccca705ae7ff97bfc981c164ccf5c1_533_532)
## 第7章 用户登录和手机注册
### 7.1 drf 的 token 登录和原理
前后分离的系统,不需要做 csrf 验证,实际上已经跨域了
查看 [drf 官方文档 API Guide Authentication](http://www.django-rest-framework.org/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](http://www.django-rest-framework.org/api-guide/authentication/#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代码修改](https://gitee.com/custer_git/django-rest-framework/commit/313ae1f24f7031653e119d244c9e7d5b0f7c6c63#072362b84bccca705ae7ff97bfc981c164ccf5c1_533_555)
### 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从请求到返回都经历了什么](http://projectsedu.com/archives/)
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,)
```
[修改代码](https://gitee.com/custer_git/django-rest-framework/commit/8b180373d0ae3225ab2c095d202b310e4d0991a3#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_1631_1669)
### 7.4 json web token 的原理
**[前后端分离之JWT用户认证](http://www.jianshu.com/p/180a870a308a)**
### 7.5 json web token 方式完成用户认证
github 搜索 [django rest framework jwt](https://github.com/GetBlimp/django-rest-framework-jwt) 可以查看它的[官方文档](http://getblimp.github.io/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 配置 变动代码](https://gitee.com/custer_git/django-rest-framework/commit/de0101eb96357b53c295de3685b27eb567afb147#072362b84bccca705ae7ff97bfc981c164ccf5c1_597_591)**
### 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 的设置和自定义用户验证代码修改](https://gitee.com/custer_git/django-rest-framework/commit/daa9574e0fbf7ec99ebc92fe0fd7007810877da3#072362b84bccca705ae7ff97bfc981c164ccf5c1_595_619)
### 7.7 云片网发送短信验证码
实现 drf 用户手机注册的功能
用户注册的功能大量用到 serializer 高级用法
首先通过前端页面分析,手机注册需要提供哪些接口
免费获取验证码 - 专门发送短信的接口
用户在回填短信验证码之后,表单提交的验证
验证失败的提示
验证成功的跳转
首先实现手机号码发送验证码的功能 - 使用第三方服务 [云片网](https://www.yunpian.com)

这个 APIKEY 后面要用到,很重要
发送短信首先要申请签名

签名需要审核的,还需要新增模版
[单条发送接口 API 文档](https://www.yunpian.com/api2.0/api-domestic/single_send.html)
现在新建一个 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 地址设置进来
[云片网发送短信验证码代码](https://gitee.com/custer_git/django-rest-framework/commit/3676881561b8230803a5e3449b80e3c0170b9c4c#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_1786_1851)
### 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 代码的变动](https://gitee.com/custer_git/django-rest-framework/commit/312105e52e4e57f88e58e7c124062d4c406527f3#072362b84bccca705ae7ff97bfc981c164ccf5c1_641_692)
```
# -*- 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 代码的变动](https://gitee.com/custer_git/django-rest-framework/commit/312105e52e4e57f88e58e7c124062d4c406527f3#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_1858_2030)
现在通过 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 代码的变动](https://gitee.com/custer_git/django-rest-framework/commit/312105e52e4e57f88e58e7c124062d4c406527f3#e89b5777e4188067198f9dc19140480164a91dbb_2_2)
设置 url
```
from users.views import SmsCodeViewSet
router.register(r'codes', SmsCodeViewSet, base_name="codes")
```
[url代码的变动](https://gitee.com/custer_git/django-rest-framework/commit/312105e52e4e57f88e58e7c124062d4c406527f3#deeb9fc362b3435bde09df3d1119ce07e0902508_170_176)
手机短信发送的一个接口 发送短信成功

### 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个记录,会错误
> 2. 如果不匹配,可能不存在这条记录的
这两种情况 get 都会抛异常
```
try:
verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
except VerifyCode.DoesNotExist as e:
pass
except VerifyCode.MultipleObjectsReturned as e:
pass
```
所以这两种异常都要扑获
filter 就不一样了,如果没有数据,会返回空数组,如果有两个数据,就返回两个
直接对数据判断,就简单多了
过期时间,用 get 也不太容易做,filter 就更加灵活
[代码改动记录](https://gitee.com/custer_git/django-rest-framework/commit/412bd378dd1c2d14d94621e5382d72eb1997674b#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_2028_2220)
### 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代码修改查看](https://gitee.com/custer_git/django-rest-framework/commit/f9aebb7b1527c5fe82e71e1e8e9880de971d4a02#c6cab6ad40ac560ee0842555c9657e85efbf51fa_0_25)
看这里的逻辑就比较明确了 - 这里做统一的处理
这样 UserRegSerializer 就完成了,接下来,继续完成 UserViewSet
首先要 import 进来 ``` from .serializer import UserRegSerializer ```
个人中心的时候,用户做个人资料修改的时候,需要用到不同的 Serializer
```
class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
""" 用户 """
serializer_class = UserRegSerializer
```
[view代码变动](https://gitee.com/custer_git/django-rest-framework/commit/f9aebb7b1527c5fe82e71e1e8e9880de971d4a02#1feddba0c7668208855f99226313a5b9ae0b0187_32_33)
现在,可以写 url 配置
```
from users.views import UserViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', UserViewSet, base_name="users")
```
[url代码变动查看](https://gitee.com/custer_git/django-rest-framework/commit/f9aebb7b1527c5fe82e71e1e8e9880de971d4a02#072362b84bccca705ae7ff97bfc981c164ccf5c1_685_721)
这样就可以在 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](http://www.django-rest-framework.org/api-guide/validators/#uniquevalidator)
```
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](http://www.django-rest-framework.org/api-guide/fields/#core-arguments)
**⚠️注意⚠️** Core arguments write_only = True 设置 True 序列化就不会 序列化这个字段了
这样就可以解决这个错误了

密码是明文的,刚才的文档 有个 [style参数](http://www.django-rest-framework.org/api-guide/fields/#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 官方文档 ](https://docs.djangoproject.com/en/dev/ref/signals/)
[django 1.8 中文文档](http://python.usyiyi.cn/translate/django_182/topics/signals.html)
[drf 1.11.6 中文文档](http://python.usyiyi.cn/translate/Django_111/topics/signals.html)

[drf 文档里之前也看到了 post_save()](http://www.django-rest-framework.org/api-guide/authentication/#generating-tokens)
```
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```
这样就可以将数据定制化
[代码变动](https://gitee.com/custer_git/django-rest-framework/commit/9a9ab1a5f4f53afef5a64b85942c8338c4dce2b5#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_2503_2648)
## 第8章 商品详情页功能
首先在列表页中任意点击一个商品,进入详情页,分析详情页功能,
商品轮播图、商品详情-名称、描述、是否免运费、市场价、促销价
商品售量,库存量,加入购物车、收藏
商品详情页富文本描述
右侧 热卖商品
所以这里我们只需要``` mixins.RetrieveModelMixin, ```在列表 view 里加上这一句就可以了
然后我们就来看这里的 serializer,因为商品的轮播图我们之前设置了的外键的,
所以在序列化的时候,我们要将它关联的表, 嵌套序列化就可以了
```
goods = models.ForeignKey(Goods, verbose_name="商品", related_name="images")
```
注意 related_name 、(many=True) 、fields = ("image",)
[代码修改记录](https://gitee.com/custer_git/django-rest-framework/commit/17e9302dc81d89714683eff4469bb1c166b89bdf#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_2650_2675)
热卖商品之前在 model 字段有一个 is_hot,只要在 过滤器 filters.py 中加一下就可以了
[热卖商品详情页代码修改](https://gitee.com/custer_git/django-rest-framework/commit/6b042852de332d9988d2c86c94d32b276004cbdf#072362b84bccca705ae7ff97bfc981c164ccf5c1_786_796)
### 用户收藏接口实现
首先这个是用户操作的功能,所以在 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](http://www.django-rest-framework.org/api-guide/validators/#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](http://www.django-rest-framework.org/api-guide/validators/#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 ``` 这不是某个字段出错,前端接收这个信息,显示到整个表单下面显示错误信息
[查看代码片段](https://gitee.com/custer_git/django-rest-framework/commit/4d3a655868d97a9f8b91868359b01f6d0809f93d#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_2679_2841)
### drf 的权限验证
drf 官方文档 Authentication 和 [Permissions 用户验证 和 权限判断](http://www.django-rest-framework.org/api-guide/permissions/#allowany)
AllowAny - 不管有没有登录的用户都可以请求
IsAuthenticated - 判断是否已经登录的 - 初步判定是否登录
IsAdminUser - 判断用户是否是 admin
```
from rest_framework.permissions import IsAuthenticated
permission_classes = (IsAuthenticated,)
```
用户未登录会返回401 Unauthorized
删除的时候验证权限,删除的记录的用户是否是当前 request 里的用户
[官方文档的例子](http://www.django-rest-framework.org/api-guide/permissions/#examples)
在 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里
[这里权限就完成了,比较重要的是认证模式](https://gitee.com/custer_git/django-rest-framework/commit/487bafc16294c7767f00c2ede0bb4adfb2818baa#79e9ae0daae846438802ec768f759a3da71d5513_67_66)
前端只传递 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 文档里也有 ](http://www.django-rest-framework.org/api-guide/generic-views/#api-reference)

```
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](http://www.django-rest-framework.org/topics/documenting-your-api/)

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

2. 把 help_text 加到 serializer 上


3. filters.py 中加 help_text

[代码变动](https://gitee.com/custer_git/django-rest-framework/commit/55e3c3ef596c0629972be85307aec2667053ef7c#072362b84bccca705ae7ff97bfc981c164ccf5c1_761_769)
### 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
```
2. 重写 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
```
这样接口就算完成了[代码变动对比](https://gitee.com/custer_git/django-rest-framework/commit/fe96e310766aa9ed05fdd12eaef542b9bf0fa5c3#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_3025_3265)
### 9.3 vue 和 用户接口信息联调
### 9.4 用户个人信息修改
[UserViewSet 配置 mixins.UpdateModelMixin](https://gitee.com/custer_git/django-rest-framework/commit/ac195153d5fa2da8267b77a608d01b730a33897b#e89b5777e4188067198f9dc19140480164a91dbb_70_69)
### 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 序列化代码片段](https://gitee.com/custer_git/django-rest-framework/commit/ac195153d5fa2da8267b77a608d01b730a33897b#79e9ae0daae846438802ec768f759a3da71d5513_43_43)
这里又涉及到动态 serializer 问题
```
def get_serializer_class(self):
if self.action == 'list':
return UserDetailSerializer
elif self.action == 'create':
return UserFavSerializer
return UserFavSerializer
```
[用户收藏功能 view 的动态序列化代码片段](https://gitee.com/custer_git/django-rest-framework/commit/ac195153d5fa2da8267b77a608d01b730a33897b#1c82e6622c1fdc380268e52c55cd5deb8f05dea7_17_19)
### 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代码修改](https://gitee.com/custer_git/django-rest-framework/commit/ac195153d5fa2da8267b77a608d01b730a33897b#072362b84bccca705ae7ff97bfc981c164ccf5c1_736_721)
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 代码片段](https://gitee.com/custer_git/django-rest-framework/commit/ac195153d5fa2da8267b77a608d01b730a33897b#1c82e6622c1fdc380268e52c55cd5deb8f05dea7_27_45)
来看下文档:


删除功能需要服务器返回给前端 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 序列化代码片段](https://gitee.com/custer_git/django-rest-framework/commit/ac195153d5fa2da8267b77a608d01b730a33897b#79e9ae0daae846438802ec768f759a3da71d5513_43_43)
read_only 就是这个值只返回,不提交
这里修改 models.py file 的 upload_to=
```file = models.FileField(upload_to="message/images/", verbose_name="上传的文件", help_text="上传的文件")```
[apps/user_operation/models.py代码修改](https://gitee.com/custer_git/django-rest-framework/commit/ac195153d5fa2da8267b77a608d01b730a33897b#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_3228_3431)
不添加 upload_to = 它默认保存在该项目的根路径下,为什么这里的 file upload 这么简单,是因为
[drf 中 api-guide-parsers-multipartparser](http://www.django-rest-framework.org/api-guide/parsers/#formparser)
**multipart/form-data**
[留言功能的代码片段](https://gitee.com/custer_git/django-rest-framework/commit/ac195153d5fa2da8267b77a608d01b730a33897b#92c995507c77a281556fb695cacda40590d7230d_23_42)
### 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 是否合法啊
[收货地址功能开发的代码片段](https://gitee.com/custer_git/django-rest-framework/commit/6c8e74d3d45b1de8e2175eb31c1938ab864249bb#17037b5583ab625ab38d06919252362fbaa5a39f_0_43)
## 第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 构成联合唯一的索引](https://gitee.com/custer_git/django-rest-framework/commit/d6bcc94540e1bbc7340147ecdff00377970ec541#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_3572_3779)
而我们不希望第二次添加商品到购物车,添加失败,而是购买数量 +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](http://www.django-rest-framework.org/api-guide/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](#userviewset)介绍过
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购物车字段验证](https://gitee.com/custer_git/django-rest-framework/commit/d6bcc94540e1bbc7340147ecdff00377970ec541#2f0575bd776cdfd33144ee4e9e22f72b07e1754b_24_25)
再回过来完善 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 新增代码变动](https://gitee.com/custer_git/django-rest-framework/commit/d6bcc94540e1bbc7340147ecdff00377970ec541#072362b84bccca705ae7ff97bfc981c164ccf5c1_716_725)
```
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 配置**
[购物车添加商品和更新商品数量功能代码片段](https://gitee.com/custer_git/django-rest-framework/commit/d6bcc94540e1bbc7340147ecdff00377970ec541#16f894ca8363aa4e2c7a41da08ad7c6d8aaf40fa_0_33)
### 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() 是不需要重写的
[购物车列表功能,更新功能,删除功能 代码片段](https://gitee.com/custer_git/django-rest-framework/commit/45b1714d21466655b9711c500ce3f7a5756cd664#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_3781_3819)
### 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)
```
有了这个之后,[列表页显示就算完成了(代码片段)](https://gitee.com/custer_git/django-rest-framework/commit/ad127cecfbe5a35382c2771db102484094cb0d44#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_3821_3870),接下来和 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 操作代码片段](https://gitee.com/custer_git/django-rest-framework/commit/5cc676f61217e617010477f1ff255ceb14b68df3#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_3873_4066)
### 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)```
[修改的代码记录](https://gitee.com/custer_git/django-rest-framework/commit/cf85af9da546a768d4edaeaca7d0af2f9f0f8a81#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4069_4121)
### 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 权限里配置 连接
```GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root1234' WITH GRANT OPTION;
FLUSH PRIVILEGES;
```
这样就可以在外部通过 navicate 进行连接
也可以通过命令行新建数据库:
```CREATE DATABASE IF NOT EXISTS yourdbname DEFAULT CHARSET utf8 COLLATE utf8_general_ci;```

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

### 10.9 支付宝公钥、私钥和沙箱环境的配置
首先看对接支付宝支付接口,需要做哪些准备?
1. [进入蚂蚁金服开放平台管理中心](https://open.alipay.com/platform/manageHome.htm)
2. [电脑网站支付-开放平台文档](https://docs.open.alipay.com/270)
3. [生成RSA密钥](https://docs.open.alipay.com/291/105971/)

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

```
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5h8p1Ufg8Je8DiEPlHNFX828QvWJdzZ6A3HapmNvbK8OcllOD+uJidaiPHDoi2mtCOH7busI1IALv6XaPPWsT5B2FQbABIqncvpH9gUbonWff2/PsWitVay6xcEkHIW4WlUm6DXbN/hTu8dxIC6yJhLKFKG5TZHwOkQgk+IaU4xkJb1D8YZPBy8AojA+0hmtLrn5QGyQH5TyphyZ9DdjvvI5fCu68pRRdHMl3JJeoevuivzWVV33PZW11lOPcznP26cLeI/aAB3UO7C1a9+n7gPxmU/meK4RGxyXNqbmpOoX8ayFGe/h5u8p1QTXJf94D4KDnXXWTmg5PqUC9Ok80QIDAQAB
```
查询订单状态的时候会用到这个 支付宝公钥
[代码变动](https://gitee.com/custer_git/django-rest-framework/commit/3e2407ae3242ab207a1337277208e57f64d845e7#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4261_4286)
### 10.10 支付宝开发文档解读
上面为了支付宝支付做了准备工作,下面编码实现支付宝的支付接口
首先为了完成支付的接口,将 pycharm 的代码设置Project Interpreter为本地,先在本地调试

然后看如何对接支付宝的支付接口,先了解下支付宝支付的官方文档
[电脑网站支付产品介绍 API 列表 统一收单下单并支付页面接口](https://docs.open.alipay.com/270/105900/)
[代码变动](https://gitee.com/custer_git/django-rest-framework/commit/e19d94ebd3569e2016ef052119cda53622dc5d5e#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4288_4298)
### 10.11 支付宝支付源码解读
### 10.12 支付宝通知接口验证
### 10.13 django 集成支付宝 notify_url 和 return_url 接口 -1
return url 需要修改订单状态,和支付宝进行交互,同步返回地址
notify_url 支付宝服务器主动通知商户服务器里指定的页面 - 异步的接口,
只要订单在支付宝平台创建成功,可以在支付宝账单里支付,也可以在手机支付,
只要支付了账单,都会像 notify_url 发送请求,发起这个请求,只要实现了url
然后在里面修改订单的状态就可以了
[电脑网站支付结果异步通知](https://docs.open.alipay.com/270/105902/)
> 对于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
注意及时把代码上传到服务器
[这里要注意支付宝交易状态](https://docs.open.alipay.com/270/105902/)
```
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 代码片段](https://gitee.com/custer_git/django-rest-framework/commit/5ea8f9be1fbba4640590e7062a26f39d8a7e89f9#8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4303_4461)
### 10.15 支付宝接口和 vue 联调 -1
与前端的联调,完成整个支付的流程
登录-> 购买商品 -> 去结算 -> 配送地址
接口在 trade -> OrderViewSet -> perform_create()
在这个逻辑里面,首先将购物车清空,创建我们的order_goods
这里涉及到支付了,支付是需要生成一个支付的页面,如何在这里生成 url
1. 重写 CreateModelMixin 里的 create() 方法 把支付宝支付url 放在 serializer 里面
2. serializer 的另一个功能 [SerializerMethodField](http://www.django-rest-framework.org/api-guide/fields/#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
[生成支付页面 代码片段](https://gitee.com/custer_git/django-rest-framework/commit/a553f895f5ecdbedb5dd7c22f881e7bfa550c51a#7a498cbfffb414da29f10b61f6c250d3c38f497b_22_22)
### 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 里的路径要修改
```