DRF 项目笔记

2020 年 06 月 04 日 • 阅读数: 135

Django Rest Framework 项目笔记

项目简介

本次项目是一个前后端分离的项目,前端使用Vue框架,后端将采用Django REST Framework框架

该框架基于Django,项目中涉及到Django部分不会详细介绍

项目初始化

新建一个项目目录,切换到该目录新建一个虚拟环境

pipenv install

修改 pipenv 的下载源,将 pip 官方的下载源替换成 "https://pypi.tuna.tsinghua.edu.cn/simple"

安装相关依赖

pipenv install django
pipenv install djangorestframework
pipenv install django-filter
pipenv install django-crispy-forms
pipenv install django-guardian
pipenv install coreapi
pipenv install markdown
pipenv install mysqlclient

创建Django项目,使用该虚拟环境

在项目的根目录下新建5个文件 apps extra_apps media static db_tools

apps 中新建应用 goods trade user_operation users

创建应用可以通过 pycharm 中的 Tools 中的 Run manage.py Task 来完成,指令是 startapp app_name

extra_apps 中添加第三方的应用源码,这里我们使用了 xadminDjangoUeditor

修改配置文件 settings.py 中的数据库配置

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': '数据库名称',
        'USER': '用户名称',
        'PASSWORD': '用户密码',
        'HOST': 'localhost',
        'PORT': '3306',
        'OPTIONS': {'init_command': 'SET default_storage_engine=INNODB;'}
    }
}

appsextra_apps 加入到python的根搜索路径下

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

配置静态文件和媒体文件路径

STATIC_URL = '/static/'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

数据库设计

扩展Django的 User

通过基础 AbstractUser 来实现扩展Django的 User 表的目的

from django.db import models
from django.contrib.auth.models import AbstractUser

class UserProfile(AbstractUser):
    """
    用户信息表
    """
    GENDER_TYPE = (
        ("male", "男"),
        ("female", "女")
    )

    name = models.CharField(max_length=30, null=True, blank=True, verbose_name="姓名")
    birthday = models.DateField(null=True, blank=True, verbose_name="出生年月")
    gender = models.CharField(max_length=6, choices=GENDER_TYPE, default="male",
                              verbose_name="性别")
    mobile = models.CharField(null=True, blank=True, max_length=11, verbose_name="电话")
    email = models.EmailField(max_length=100, null=True, blank=True, verbose_name="邮箱")

    class Meta:
        verbose_name = "用户"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

model的设计就不一一介绍了,具体设计请参考源码,这里简单介绍下下面的model中 parent_category 字段中的 related_name="sub_cat" 这个属性,在后面的序列化的时候会用到

class GoodsCategory(models.Model):
    """
    商品类型
    """

    CATEGORY_TYPE = (
        (1, "一级类目"),
        (2, "二级类目"),
        (3, "三级类目"),
    )

    name = models.CharField(default="", max_length=30, verbose_name="类别名", help_text="类别名")
    code = models.CharField(default="", max_length=30, verbose_name="类别code",
                            help_text="类别code")
    desc = models.TextField(default="", verbose_name="类别描述", help_text="类别描述")
    category_type = models.IntegerField(choices=CATEGORY_TYPE, verbose_name="类目级别",
                                        help_text="类目级别")
    parent_category = models.ForeignKey("self", null=True, blank=True, verbose_name="父类别",
                                        related_name="sub_cat", on_delete=models.CASCADE)
    is_tab = models.BooleanField(default=False, verbose_name="是否导航", help_text="是否导航")
    add_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")

    class Meta:
        verbose_name = "商品类别"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name

将我们创建的应用都添加到 INSTALLED_APPS 中,当然也包括我们使用到的第三方的应用

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'DjangoUeditor',
    'crispy_forms',
    'rest_framework',
    'django_filters',
    'corsheaders',
    'xadmin',
    'users',
    'goods',
    'trade',
    'user_operation',
]

Run manage.py Task 中,指向指令 makemigrationsmigrate ,进行数据库迁移和同步

后台的配置

在上面我们已经将 xadmin 添加到 INSTALLED_APPS 中了,也做好了数据库同步

那么我们就可以直接使用 xadmin

在根URL中我们添加两条路由,一条是 xadmin 的路由,一个是媒体文件的路由

import xadmin
from django.urls import path
from VueShop.settings import MEDIA_ROOT
from django.views.static import serve

urlpatterns = [
    path('admin/', xadmin.site.urls),
    path('media/<path:path>', serve, {'document_root': MEDIA_ROOT}),

在各个应用中新建python文件命名为 adminx.py Django就会自动识别

具体使用方式就是给model绑定一个Admin的类,在类中可以定义它在网页上的一些显示方式

下面代码中第一个和第二个是对页面整体的一个配置,第三个是映射的一个model类

import xadmin
from xadmin import views
from .models import VerifyCode

class BaseSetting(object):
    enable_themes = True
    use_bootswatch = True

class GlobalSetting(object):
    site_title = "VueShop后台管理系统"
    site_footer = "VueShop在线"
    menu_style = "accordion"

class VerifyCodeAdmin(object):
    pass

xadmin.site.register(views.BaseAdminView, BaseSetting)
xadmin.site.register(views.CommAdminView, GlobalSetting)
xadmin.site.register(VerifyCode, VerifyCodeAdmin)

**注:**其他后台管理配置请参考源码

数据的导入

db_tools 中放入我们自己准备好的数据,然后编写脚本将数据导入到数据库中

脚本的编写方式如下

import os
import sys
import django

# 将该文件加入到python的根搜索路径下
pwd = os.path.dirname(os.path.realpath(__file__))
sys.path.append(pwd+'../')
# 在Django外部使用model需要的配置(这个配置实际是从manage.py中复制过来的)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "VueShop.settings")

django.setup()

if __name__ == '__main__':
    from goods.models import Goods, GoodsCategory, GoodsImage
    from db_tools.data.product_data import row_data

    for goods_detail in row_data:
        goods = Goods()
        goods.name = goods_detail["name"]
        goods.market_price = float(
            int(goods_detail["market_price"].replace("¥", "").replace("元", "")))
        goods.shop_price = float(
            int(goods_detail["sale_price"].replace("¥", "").replace("元", "")))
        goods.goods_brief = goods_detail["desc"] if goods_detail["desc"] is not None else ""
        goods.goods_desc = goods_detail["goods_desc"]
        								if goods_detail["goods_desc"] is not None else ""
        goods.goods_front_image = goods_detail["images"][0] if goods_detail["images"] else ""

        category_name = goods_detail["categorys"][-1]
        category = GoodsCategory.objects.filter(name=category_name)
        if category:
            goods.category = category[0]
        goods.save()

        for goods_image in goods_detail["images"]:
            goods_image_instance = GoodsImage()
            goods_image_instance.image = goods_image
            goods_image_instance.goods = goods
            goods_image_instance.save()

**注:**从Django中引入model一定要在 django.setup() 之后

API的设计

商品分类的实现

我们的类别一共分为3层结构,当我们点击任意一层的时候,其下的数据都要获取出来,具体实现如下:

Serializer

这里使用嵌套的序列化,将 GoodsCategory 的所有字段都做了序列化

from rest_framework import serializers
from .models import GoodsCategory


class CategorySerializer(serializers.ModelSerializer):

    class SubCategorySerializer(serializers.ModelSerializer):

        class SubCategorySerializer(serializers.ModelSerializer):

            class Meta:
                model = GoodsCategory
                fields = '__all__'

        class Meta:
            model = GoodsCategory
            fields = '__all__'

        sub_cat = SubCategorySerializer(many=True)

    class Meta:
        model = GoodsCategory
        fields = '__all__'

    sub_cat = SubCategorySerializer(many=True)

ViewSet

这里继承了 mixinListModelMixinRetrieveModelMixin 说明可以返回一个列表,也可以返回一个详情

除此之外没有做更多的过滤的功能

from rest_framework import mixins, viewsets

from .serializers import CategorySerializer
from .models import GoodsCategory

class CategoryViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    """
    list: 商品分类列表数据
    """
    queryset = GoodsCategory.objects.filter(category_type=1)
    serializer_class = CategorySerializer

Router

声明一个 DefaultRouter() 将视图注册到 router 中,再配置一条路由,就完成了一个API的编写

from django.urls import path
from rest_framework.routers import DefaultRouter
from goods import views as good_views

router = DefaultRouter()
 
router.register('categorys', good_views.CategoryViewSet, base_name='categorys')

urlpatterns = [
        path('', include(router.urls)),
]

商品接口的实现

在列表页,我们可以通过点击分类进行筛选,也可以进行价格区间的筛选,还可以根据一定的字段排序,具体实现如下:

Serializer

这里 GoodsSerializer 的实现用到了前面的 CategorySerializer()

from rest_framework import serializers
from .models import Goods, GoodsCategory, GoodsImage


class GoodsImageSerializer(serializers.ModelSerializer):

    class Meta:
        model = GoodsImage
        fields = ('image', )

class GoodsSerializer(serializers.ModelSerializer):
    category = CategorySerializer()
    images = GoodsImageSerializer(many=True)

    class Meta:
        model = Goods
        fields = '__all__'

ViewSet

这里应用 ListModelMixinRetrieveModelMixin 可以返回列表和详情,还添加了分页,过滤,搜索和排序

我们自定义了一个分页类,每一页12个数据

我们也自定义了一个Filter(在后面说明)

排序方式,可以指定按照 sold_numshop_price 来排序

搜索方式,我们可以从三个字段中匹配你输入的搜索内容

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import mixins, viewsets
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.pagination import PageNumberPagination

from .filters import GoodsFilter
from .serializers import GoodsSerializer
from .models import Goods


class GoodsPagination(PageNumberPagination):
    page_size = 12
    page_size_query_param = 'page_size'
    page_query_param = 'page'
    max_page_size = 10
    

class GoodListViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin,
                      viewsets.GenericViewSet):
    """
    list: 商品列表数据
    retrieve: 商品详情
    """
    queryset = Goods.objects.all()
    serializer_class = GoodsSerializer
    pagination_class = GoodsPagination
    filter_backends = (DjangoFilterBackend, OrderingFilter, SearchFilter)
    filter_class = GoodsFilter
    ordering_fields = ('sold_num', 'shop_price',)
    search_fields = ('name', 'goods_brief', 'goods_desc')

Filter

自定义的Filter需要在上面的视图中声明

而在Filter的定义中,我们定义了一个最小价格和最大价格,还有一个所属分类,最后一个是本来就有的字段,意思是是否为热门商品

import django_filters
from django.db.models import Q
from .models import Goods


class GoodsFilter(django_filters.rest_framework.FilterSet):
    """
    商品的过滤器
    """
    pricemin = django_filters.NumberFilter(field_name='shop_price', lookup_expr='gte')
    pricemax = django_filters.NumberFilter(field_name='shop_price', lookup_expr='lte')
    top_category = django_filters.NumberFilter(method='top_category_filter')

    def top_category_filter(self, queryset, name, value):
        queryset = queryset.filter(Q(category_id=value) |
                                   Q(category__parent_category_id=value) |
                                   Q(category__parent_category__parent_category_id=value))
        return queryset

    class Meta:
        model = Goods
        fields = ['pricemin', 'pricemax', 'top_category', 'is_hot']

Router

前面我们已经实例化了一个 router 现在只需要将视图注册进入就可以了

from goods import views as good_views


# 商品数据API
router.register('goods', good_views.GoodListViewSet, base_name='goods')

注册与登录的实现

在用户注册的时候,我们需要给用户发送短信验证码,当用户填写了正确的验证码后,我们需要将用户的信息保存到数据库,并且做页面的跳转,同时给用户一个 Token ,用户下一次携带 Token 发过来的请求就可以直接通过我们的用户认证,Token需要有一个过期时间,当 Token 过期后,用户需要重新登录,具体实现如下:

发送短信验证码

在用户注册页面,当用户输入了手机号码,点击获取验证码的时候,在后台我们要获取用户输入的手机号码,并验证是否合法,然后调用接口,为用户发送短信验证码

Serializer的实现

发送短信的Serializer实际只是用来做验证功能的,在这里我们检测了用户是否存在,手机号是否合法,还有延迟时间必需大于60秒,验证通过后,我们返回手机号

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_minutes_ago = datetime.datetime.now() - datetime.timedelta(
            hours=0, minutes=1, seconds=0)
        if VerifyCode.objects.filter(add_time__gt=one_minutes_ago, mobile=mobile):
            raise serializers.ValidationError('距离上次发送不足60秒')

        return mobile
ViewSet的实现

SmsCodeViewSet 里面我们只实现了 CreateModelMixin 意味着我们可以添加数据,在下面我们重写了 create() 方法,然在在里面加入了发送短信的逻辑,可以看到如果失败我们返回一个 HTTP_400_BAD_REQUEST 如果成功,我们向数据库保存一条记录,并且返回 HTTP_201_CREATED

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

    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 = yun_pian.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:
            VerifyCode.objects.create(code=code, mobile=mobile)
            return Response({'mobile': mobile}, status=status.HTTP_201_CREATED)
短信发送的实现

短信发送实际上我们是调用的 云片网 的接口实现的,逻辑如下,至于验证码,我们是通过 random 模块实现的

class YunPian(object):
    def __init__(self, api_key):
        self.api_key = api_key
        self.single_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}。如非本人操作,请忽略本短信'.format(code=code)
        }

        response = requests.post(self.single_url, data=parmas)
        return json.loads(response.text)

    @staticmethod
    def generate_code():
        seeds = '1234567890'
        random_str = []
        for i in range(6):
            random_str.append(random.choice(seeds))
        return ''.join(random_str)
添加接口路由
router.register('code', user_views.SmsCodeViewSet, base_name='code')

用户注册

当用户输入了验证码,将手机号与验证码还有密码,同时提交过来的时候,我们需要验证手机号和验证码是否匹配

Serializer的实现

在下面的代码中,我们对验证码做了验证,将用户的手机号设置为接收到的数据(前端传过来的是username)并将 code 字段删除,因为在保存数据库的时候,我们不需要该字段

class UserRegisterSerializer(serializers.ModelSerializer):
    code = serializers.CharField(max_length=6, min_length=6, write_only=True,
                                 label='验证码',help_text='验证码',
                                 error_messages={
                                     'blank': '请输入验证码',
                                     'required': '请输入验证码',
                                     'max_length': '验证码格式错误',
                                     'min_length': '验证码格式错误'
                                 })
    password = serializers.CharField(style={'input_type': 'password'},
                                     write_only=True, label='密码', help_text='密码')

    username = serializers.CharField(required=True, allow_blank=False,
                                     label='用户名', help_text='用户名',
                                     validators=
                                     [UniqueValidator(queryset=User.objects.all())])

    def validate_code(self, code):
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by('-add_time')
        if verify_records:
            last_record = verify_records[0]
            five_minutes_ago = datetime.datetime.now() - datetime.timedelta(hours=0, minutes=5, seconds=0)
            five_minutes_ago = five_minutes_ago.replace(tzinfo=pytz.timezone('UTC'))
            if five_minutes_ago > last_record.add_time:
                raise serializers.ValidationError('验证码过期')

            if last_record.code != code:
                raise serializers.ValidationError('验证码错误')

        else:
            raise serializers.ValidationError('验证码错误')

    def validate(self, attrs):
        attrs['mobile'] = attrs['username']
        del attrs['code']
        return attrs

    class Meta:
        model = User
        fields = ('username', 'code', 'password')
View的实现

UserViewSet 中继承了 CreateModelMixin 所以我们可以添加数据,并且这里我们重写了 create() 方法

这是因为我们在给用户返回数据的时候,需要额外传递一个 token 参数(这里使用的JWT)

class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
    """
    create: 用户注册
    retrieve: 用户详情
    """
    serializer_class = UserRegisterSerializer
    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)
        re_dict['name'] = user.name if user.name else user.username

        headers = self.get_success_headers(serializer.data)
        return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        return serializer.save()
通过信号加密密码

在我们保存数据库的时候,其实没有对密码进行加密,这里加密的方式很多,我们可以重写Serializer的Create() 方法,也可以用下面的信号量的方式

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model

__author__ = '骆杨'

User = get_user_model()


@receiver(post_save, sender=User)
def create_auth(sender, instance=None, created=False, **kwargs):
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()

app.py 中定义信号

    def ready(self):
        import users.signals

自定义验证类

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:
            print(e)
            return None

用户注册

router.register('users', user_views.UserViewSet, base_name='users')

用户登录

urlpatterns = [
    ...
    # JWT验证API
    path('login/', obtain_jwt_token),
]

个人中心的实现

在个人中心里面,我们需要提供几个数据接口,主要是用户个人信息,用户收藏列表,用户留言,用户配送地址,还有一个用户订单

用户信息

我们可以查看并修改个人信息,手机号除外

用户详情的Serializer
class UserDetailSerializer(serializers.ModelSerializer):
    """
    用户详情序列化
    """
    class Meta:
        model = User
        fields = ('name', 'gender', 'birthday', 'email', 'mobile')
ViewSet的实现

可以看到,我们依然在以前的逻辑上修改,因为用户信息和注册时返回的数据不同,我们需要动态配置Serializer

通过重写 get_serializer_class() 方法,然后判断请求的类型,根据不同的类型调用不同的Serializer即可

验证也是同样的道理,用户注册的时候,我们不需要验证用户,但查看个人中心,必须要是用户登录状态

class UserViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    """
    create: 用户注册
    retrieve: 用户详情
    """
    queryset = User.objects.all()
    authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)

    # 动态配置Serializer
    def get_serializer_class(self):
        if self.action == 'retrieve':
            return UserDetailSerializer
        elif self.action == 'create':
            return UserRegisterSerializer
        return UserDetailSerializer

    # 动态设置验证类型
    def get_permissions(self):
        if self.action == 'retrieve':
            return [permissions.IsAuthenticated()]
        elif self.action == 'create':
            return []
        return []
    
    ...

    def get_object(self):
        return self.request.user
Router配置

路由配置其实没变

# 用户信息
router.register('users', user_views.UserViewSet, base_name='users')

用户收藏

在个人中心我们需要显示用户收藏列表,并且可以删除,在商品详情中也有收藏按钮,可以收藏和取消收藏

Serializer

用户字段指定为当前登录用户

并且给用户和商品加上一个联合唯一约束,表示一个商品一个用户只能收藏一次

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


class UserFavDetailSerializer(serializers.ModelSerializer):
    goods = GoodsSerializer()

    class Meta:
        model = UserFav
        fields = ('goods', 'id')

ViewSet的实现

同样的使用动态的Serializer,并且加上权限,只允许登录用户访问

class UserFavViewSet(mixins.CreateModelMixin, mixins.ListModelMixin,
                     mixins.DestroyModelMixin, mixins.RetrieveModelMixin,
                     viewsets.GenericViewSet):
    """
    list: 用户收藏列表
    create: 用户添加收藏
    delete: 用户取消收藏
    retrieve: 用户收藏详情
    """
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
    authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
    lookup_field = 'goods_id'

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

    def get_queryset(self):
        return UserFav.objects.filter(user=self.request.user)
Router
# 用户收藏
router.register('userfavs', operation_views.UserFavViewSet, base_name='userfavs')
自定义验证类

可以发现,上面我们使用了 IsOwnerOrReadOnly 这个验证类,该类的定义如下

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.user == request.user

用户留言

可以添加,删除和查看留言列表

Serializer

同样用户设为当前用户,处理一下返回的时间格式

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 = '__all__'
ViewSet

添加权限和验证

返回对应的查询集

class LeavingMessageViewSet(mixins.ListModelMixin, mixins.DestroyModelMixin,
                            mixins.CreateModelMixin, viewsets.GenericViewSet):
    """
    list: 获取用户留言
    create: 添加用户留言
    delete: 删除用户留言
    """
    serializer_class = LeavingMessageSerializer
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
    authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)

    def get_queryset(self):
        return UserLeavingMessage.objects.filter(user=self.request.user)
Router
# 用户留言
router.register('messages', operation_views.LeavingMessageViewSet,
                base_name='messages')

用户地址

可以添加,修改和删除用户收货地址

Serializer

这里同样设置用户为当前登录用户,并对手机号码进行验证

class AddressSerializer(serializers.ModelSerializer):
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    def validate_signer_mobile(self, signer_mobile):

        if not re.match(REGEX_MOBILE, signer_mobile):
            raise serializers.ValidationError('手机验证非法')
        return signer_mobile

    class Meta:
        model = UserAddress
        fields = '__all__'

ViewSet

我们直接继承 ModelViewSet ,它实际上实现了所以的 mixin 方法

class AddressViewSet(viewsets.ModelViewSet):
    """
    list: 获取收货地址列表
    create: 添加收货地址
    update: 修改收货地址
    delete: 删除收货地址
    """
    serializer_class = AddressSerializer
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
    authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)

    def get_queryset(self):
        return UserAddress.objects.filter(user=self.request.user)
Router
# 用户地址
router.register('address', operation_views.AddressViewSet, base_name='address')

交易功能的实现

购物车功能

Serializer的实现

因为需要重写 ShopCartSerializercreate() 方法和 update() 方法,所以这里我们没有使用 ModelSerializer 而是直接使用 Serializer

当我们将一个商品添加到购物车的时候,我们需要判断,当购物车内不存在该商品时,创建该商品,当已经存在时,直接在原来的基础上添加数量,当我们更新商品的时候,将新的数量进行更改然后保存

class ShopCartSerializer(serializers.Serializer):
    """
    购物车
    """
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())
    nums = serializers.IntegerField(required=True, min_value=1, label='数量',
                                    error_messages={
                                        'min_value': '商品数量不能小于1',
                                        'required': '请选择购买数量'
                                    })
    goods = serializers.PrimaryKeyRelatedField(required=True, label='商品',
                                               queryset=Goods.objects.all())

    def update(self, instance, validated_data):
        instance.nums = validated_data['nums']
        instance.save()
        return instance

    def create(self, validated_data):
        user = self.context['request'].user
        nums = validated_data['nums']
        goods = validated_data['goods']

        existed = ShoppingCart.objects.filter(user=user, goods=goods)

        if existed:
            existed = existed[0]
            existed.nums += nums
            existed.save()
        else:
            existed = ShoppingCart.objects.create(**validated_data)

        return existed

    
class ShopCartDetailSerializer(serializers.ModelSerializer):

    goods = GoodsSerializer(many=False)

    class Meta:
        model = ShoppingCart
        fields = '__all__'
ViewSet的实现

这里直接使用 ModelViewSet

添加验证类和权限

动态定义 serializer_class

返回与用户相对应的 queryset

查询参数设为商品ID

class ShoppingCartViewSet(viewsets.ModelViewSet):
    """
    list: 获取购物车列表
    create: 加入购物车
    delete: 删除购物记录
    update: 更新购物车
    """
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
    authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)

    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)

    lookup_field = 'goods_id'
Router
# 购物车
router.register('shopcarts', trade_views.ShoppingCartViewSet, base_name='shopcarts')

订单功能的实现

Serializer的实现

订单的 Serializer 也有2个,一个用于列表,一个用于详情

需要注意的是,订单号的实现,通过单独定义一个函数,然后再验证的时候调用该函数,生成订单号,但该函数需要用户ID作为参数,这时候不能直接通过 request 获取到用户ID,而需要使用 context['request'].user.id

订单商品单独作为一个 Serializer ,在订单详情中将订单商品包含进去即可

class OrderSerializer(serializers.ModelSerializer):

    user = serializers.HiddenField(default=serializers.CurrentUserDefault())
    order_sn = serializers.CharField(read_only=True)
    trade_no = serializers.CharField(read_only=True)
    pay_status = serializers.CharField(read_only=True)
    pay_time = serializers.DateTimeField(read_only=True)

    def validate(self, attrs):
        attrs['order_sn'] = get_order_sn(self.context['request'].user.id)
        return attrs

    class Meta:
        model = OrderInfo
        fields = '__all__'


class OrderGoodsSerializer(serializers.ModelSerializer):

    goods = GoodsSerializer(many=False)

    class Meta:
        model = OrderGoods
        fields = '__all__'


class OrderDetailSerializer(OrderSerializer):
    goods = OrderGoodsSerializer(many=True)
ViewSet的实现

和上面类似,添加验证类和权限

动态配置 serializer_class

返回用户对应的订单查询集

重写 perform_create() 方法,该方法是 CreateModelMixin 中定义的,它调用了 Serializersave() 方法,我们需要在 save 之前完成一些逻辑:创建订单商品,清空购物车

class OrderViewSet(mixins.ListModelMixin, mixins.CreateModelMixin,
                   mixins.DestroyModelMixin, mixins.RetrieveModelMixin,
                   viewsets.GenericViewSet):
    """
    list: 获取个人订单列表
    create: 新增订单
    delete: 删除订单
    """
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
    authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)

    def get_serializer_class(self):
        if self.action == 'retrieve':
            return OrderDetailSerializer
        else:
            return OrderSerializer

    def get_queryset(self):
        return OrderInfo.objects.filter(user=self.request.user)

    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.order = order
            order_goods.goods = shop_cart.goods
            order_goods.goods_num = shop_cart.nums
            order_goods.save()

            shop_cart.delete()
        return order
Router
# 订单
router.register('orders', trade_views.OrderViewSet, base_name='orders')

Pycharm远程调试

上传代码到服务器

在Pycharm中点击上方的 Tools 中的 Deployment 下的 Configuration

选择 SFTP 填写 Ip 和端口,导入密钥或填写密码

配置好本地路径和对应的服务器路径

完成后,选中根目录,上传文件

服务器环境的配置

应为本项目使用的 pipenv 管理工具,在上传文件的时候,同时上传依赖的 Pipfile 文件,在文件根目录下直接执行以下命令(前提安装好了Python3的环境,以及 pipenv

pipenv install

**注:**注意Django的版本(本项目使用的是Django2.0)

服务器数据库的同步

要在本地访问服务器的数据库,并进行数据上传,需要一些配置,如下

  • 进入服务器端MySql
mysql -u root -p
  • 开放远程连接权限
grant all privileges on *.* to 'root'@'%' identified by 'YOUR_PASSWORD' with grant option;
  • 刷新权限
flush privileges;
  • 修改配置文件
vi /etc/mysql/mysql.conf.d
选中mysqld.cnf
  • 将接受地址
bind-address       = 127.0.0.1
  • 修改为
bind-address       = 0.0.0.0
  • 重启服务
service mysql restart

之后就可以通过 navicat 连接到服务器数据库了,然后使用工具中的数据传输,完成数据库的上传同步

调试远程代码

配置远程环境:在Pycharm中选这Python环境的地方,新建远程环境,选者SSH,在下面选者我们第一次创建的Configuration ,然后下一步选中服务器上的虚拟Python环境

例如这里的环境路径为 /root/.virtualenvs/VueShop-mLXVca9O/bin/python3

配置代码的运行环境 Edit Configurations

Host 设为 0.0.0.0

支付宝接口对接

先从接口对接开始解释,我们现在需要给每一个订单绑定一个支付宝的链接,通过该链接可以跳转到支付宝的支付页面,在支付页面中完成支付后,支付宝会给我们发起一个异步的请求,我们需要解析该请求并且修改订单状态

当然中间过程涉及到一些加密解密的过程,还有数据的一个封装过程,会在后面解释

重新定义订单的Serializer

添加一个字段

这里使用了 SerializerMethodField 它允许我们自定义一个 get 方法(固定写法 get_fields_name

在该方法中我们实例化了一个 AliPay 对象,然后调用该对象的 direct_pay() 方法生成支付宝链接

    alipay_url = serializers.SerializerMethodField(read_only=True)

    def get_alipay_url(self, obj):
        alipay = AliPay(
            app_id='2016091700529381',
            app_notify_url='http://47.98.207.4:8080/alipay/return/',
            app_private_key_path=PRIVATE_KEY_PATH,
            alipay_public_key_path=ALI_PUB_KEY_PATH,
            debug=True,
            return_url='http://47.98.207.4:8080/alipay/return/'
        )

        url = alipay.direct_pay(
            subject=obj.order_sn,
            out_trade_no=obj.order_sn,
            total_amount=obj.order_mount,
            return_url='http://47.98.207.4:8080/alipay/return/'
        )
        return 'https://openapi.alipaydev.com/gateway.do?{data}'.format(data=url)
处理支付宝异步返回

**注:**这里只介绍异步返回,当然支付宝也提供了同步返回的接口

支付宝的异步返回是一个 post 请求,我们先获取到所有返回的参数,保存到一个字典中(除去 sign),实例化一个 AliPay 对象,通过该对象的 verify() 方法验证数据,验证通过后我们就需要修改订单了

实际上,这里的逻辑也不复杂,具体的返回参数可以参考 蚂蚁金服的开发文档 ,注意最后要返回 success

class AliPayView(APIView):
    """
    支付宝接口
    """
    def post(self, request):
        # 处理支付宝的notify_url
        processed_dict = {}
        for key, value in request.POST.items():
            processed_dict[key] = value

        sign = processed_dict.pop('sign', None)

        alipay = AliPay(
            app_id='2016091700529381',
            app_notify_url='http://47.98.207.4:8080/alipay/return/',
            app_private_key_path=PRIVATE_KEY_PATH,
            alipay_public_key_path=ALI_PUB_KEY_PATH,
            debug=True,
            return_url='http://47.98.207.4:8080/alipay/return/'
        )

        verify_re = alipay.verify(processed_dict, sign)

        if verify_re 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)

            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')
添加路由

这里我们在Views中直接使用的 APIView 所以不能也没必要使用 Router 直接配置一个路由就行

# 支付宝接口
path('alipay/return/', trade_views.AliPayView.as_view(), name='alipay'),

支付宝接口详解

前面我们已经提到了,支付宝接口有2大核心内容,一个是生成一个支付宝链接,另一个是验证支付宝的返回结果

先来看看构造函数,在构造函数中,我们添加了一些公共参数,这些参数,都是 蚂蚁金服接口文档 规定的

其中必填参数包括:

  • app_id :开发者应用ID
  • method :接口名称(默认)
  • charset :编码格式
  • sign_type :签名算法类型
  • sign :签名字符串
  • timestamp :时间戳
  • version :版本(固定1.0)
  • biz_content :请求参数的集合
生成支付链接

在构造函数中,我们添加了 app_id 密钥,支付宝公钥和请求地址(该地址可以通过设置debug参数来切换)

import json
import base64
from datetime import datetime
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from urllib.parse import quote_plus


class AliPay(object):
    """
    支付宝
    """
    def __init__(self, **kwargs):
        self.app_id = kwargs['app_id']
        self.app_notify_url = kwargs['app_notify_url']
        self.app_private_key_path = kwargs['app_private_key_path']
        self.return_url = kwargs['return_url']
        with open(self.app_private_key_path) as fp:
            self.app_private_key = RSA.importKey(fp.read())

        self.alipay_public_key_path = kwargs['alipay_public_key_path']
        with open(self.alipay_public_key_path) as fp:
            self.alipay_public_key = RSA.import_key(fp.read())

        if kwargs['debug'] is True:
            self.__gateway = 'https://openapi.alipaydev.com/gateway.do'
        else:
            self.__gateway = 'https://openapi.alipay.com/gateway.do'

direct_pay() 函数作为我们生成链接的入口,我们继续传入一些参数,这些参数主要是我们自己的请求参数,如主题,本地订单号,订单金额,然后通过这些参数,构造请求体

    def direct_pay(self, subject, out_trade_no, total_amount,
                   return_url=None, **kwargs):
        biz_content = {
            'subject': subject,
            'out_trade_no': out_trade_no,
            'total_amount': total_amount,
            'product_code': 'FAST_INSTANT_TRADE_PAY',
        }
        biz_content.update(kwargs)
        data = self.build_body('alipay.trade.page.pay', biz_content, return_url)
        return self.sign_data(data)

这里的参数就是所有的必填参数,除了 sign

notify_urlreturn_url 是支付宝给我们返回数据的时候它请求的我们的URL(注意这里需要公网地址)

    def build_body(self, method, biz_content, return_url=None):
        data = {
            'app_id': self.app_id,
            'method': method,
            'charset': 'utf-8',
            'sign_type': 'RSA2',
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'version': '1.0',
            'biz_content': biz_content
        }

        if return_url is not None:
            data['notify_url'] = self.app_notify_url
            data['return_url'] = self.return_url

        return data

前面说了必填请求参数还缺少一个签名,然而支付宝对签名的数据格式有一个要求,必须是排好序的数据,所以我们还需要为参数进行一次排序,排完序之后需要用 & 符号将所有参数链接起来转换成字符串,然后进行签名,最后构造支付宝链接,注意最后还需要加上签名

    def sign_data(self, data):
        data.pop('sign', None)
        unsigned_items = self.ordered_data(data)
        unsigned_string = '&'.join('{0}={1}'.format(k, v) for k, v in unsigned_items)
        sign = self.sign(unsigned_string.encode('utf-8'))
        quoted_string = '&'.join('{0}={1}'.format(k, quote_plus(v))
                                 for k, v in unsigned_items)

        signed_string = quoted_string + '&sign=' + quote_plus(sign)
        return signed_string

参数排序

    def ordered_data(self, data):
        complex_keys = []
        for key, value in data.items():
            if isinstance(value, dict):
                complex_keys.append(key)

        for key in complex_keys:
            data[key] = json.dumps(data[key], separators=(',', ':'))

        return sorted([(k, v) for k, v in data.items()])

签名需要用到前面配置好的本地密钥,最后还要转换成 base64 的编码格式

    def sign(self, unsigned_string):
        key = self.app_private_key
        signer = PKCS1_v1_5.new(key)
        signature = signer.sign(SHA256.new(unsigned_string))
        sign = base64.encodebytes(signature).decode('utf8').replace('\n', '')
        return sign
验证支付宝返回

无论支付宝通过什么方式返回的数据,我们都需要验证它的数据合法性,这里的验证需要使用到支付宝提供的公钥,前面我们已经配置了公钥

要解密同样需要将数据先转换成字符串通过 & 连接

    def verify(self, data, signature):
        if "sign_type" in data:
            data.pop("sign_type")
        # 排序后的字符串
        unsigned_items = self.ordered_data(data)
        message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)
        return self._verify(message, signature)

注:sign_typesign 需要先从数据中弹出,不能包含在字符串内,并且 sign 是支付宝返回的签名,我们需要使用该签名来验证数据,所以我们在函数中的参数 signature 就是提前提取出来的 sign

使用支付宝提供的公钥和支付宝返回的签名,来对数据进行验证

    def _verify(self, raw_content, signature):
        key = self.alipay_public_key
        signer = PKCS1_v1_5.new(key)
        digest = SHA256.new()
        digest.update(raw_content.encode('utf8'))
        if signer.verify(digest, base64.decodebytes(signature.encode('utf8'))):
            return True
        return False
返回请求URL

当支付宝给支付成功后,它会给我们返回数据,而返回数据的URL可以通过前面提到的 notify_urlreturn_url 区别如下:

notify_url 是一个异步的请求,支付一旦完成它就会给我们返回数据,该请求也是 post 请求

return_url 是一个同步的请求,支付完成后,它会向该URL做跳转,所以该请求是 get 请求

**注:**为了方便调试,我们最好在公网的环境下启动服务器

首页与缓存的实现

首页轮播图

没啥说的,非常简单

Serializer
class BannerSerializer(serializers.ModelSerializer):

    class Meta:
        model = Banner
        fields = '__all__'
ViewSet
class BannerViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    """
    list: 获取轮播图
    """
    queryset = Banner.objects.all()
    serializer_class = BannerSerializer
Router
# 获取轮播图
router.register('banners', good_views.BannerViewSet, base_name='banners')

首页新品

Serializer

我们在GoodsModel中有一个 is_new 字段,通过该字段我们就可以指定新品,然后再商品的过滤器中添加一个过滤选项即可

首页商品

Serializer
class BrandSerializer(serializers.ModelSerializer):

    class Meta:
        model = GoodsCategoryBrand
        fields = '__all__'


class IndexGoodsSerializer(serializers.ModelSerializer):

    class SubCategorySerializer(serializers.ModelSerializer):
        class Meta:
            model = GoodsCategory
            fields = '__all__'

    brands = BrandSerializer(many=True)
    goods = serializers.SerializerMethodField()
    sub_cat = SubCategorySerializer(many=True)
    ad_goods = serializers.SerializerMethodField()

    def get_goods(self, obj):
        all_goods = Goods.objects.filter(
            Q(category_id=obj.id) |
            Q(category__parent_category_id=obj.id) |
            Q(category__parent_category__parent_category_id=obj.id))
        goods_serializer = GoodsSerializer(all_goods, many=True)
        return goods_serializer.data

    def get_ad_goods(self, obj):
        goods_json = {}
        ad_goods = IndexAd.objects.filter(category_id=obj.id)
        if ad_goods:
            good_ins = ad_goods[0].goods
            goods_json = GoodsSerializer(
                good_ins, many=False,
                context={'request': self.context['request']}).data
        return goods_json

    class Meta:
        model = GoodsCategory
        fields = '__all__'
ViewSet
class IndexGoodsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    """
    list: 首页商品
    """
    queryset = GoodsCategory.objects.filter(is_tab=True)[0:4]
    serializer_class = IndexGoodsSerializer
Router
# 首页商品
router.register('indexgoods', good_views.IndexGoodsViewSet, base_name='indexgoods')

缓存的实现

安装第三方库 drf-extensionsdjango-redis

在需要做缓存的视图中继承 CacheResponseMixin

如下:

from rest_framework_extensions.cache.mixins import CacheResponseMixin


class GoodsViewSet(CacheResponseMixin,
                   mixins.ListModelMixin, mixins.RetrieveModelMixin,
                   viewsets.GenericViewSet):
    """
    list: 商品列表数据
    retrieve: 商品详情
    """
    ...

在配置文件中将缓存配置到 redis 数据库

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

API限速的实现

在配置文件中加入以下配置

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ),
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}

在视图函数中应用相应的限速类

from rest_framework.throttling import AnonRateThrottle, UserRateThrottle

class GoodsViewSet(CacheResponseMixin, mixins.ListModelMixin,
                   mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    """
    list: 商品列表数据
    retrieve: 商品详情
    """
    ...
    throttle_classes = (AnonRateThrottle, UserRateThrottle)
标签: PythonDRFAPI
添加评论
评论列表
没有更多内容