【Vue+DRF生鲜电商】25.商品添加购物车接口功能,Vue和购物车联调

专题:Vue+Django REST framework前后端分离生鲜电商

Vue+Django REST framework 打造前后端分离的生鲜电商项目(慕课网视频)。
Github地址:https://github.com/xyliurui/DjangoOnlineFreshSupermarket ;
Django版本:2.2、djangorestframework:3.9.2。

更多内容请点击 我的博客 查看,欢迎来访。

商品详情页点击加入购物车,就可以把商品加入到数据库【创建】。

对于购物车已存在的商品,如果将商品重复加入购物车,或者减少商品数量,就将它的数量进行加减【更新】。

购物车也支持对商品进行删除【删除】,以及列表展示【查询】。

综上,购物车就需要用到mixins中的所有功能。

商品添加到购物车功能实现

实现购物车视图,在 apps/trade/views.py 添加代码

from rest_framework import mixins, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import SessionAuthentication

from rest_framework_simplejwt.authentication import JWTAuthentication

from utils.permissions import IsOwnerOrReadOnly


class ShoppingCartViewSet(viewsets.ModelViewSet):
    """
    购物车功能实现
    list:
        获取购物车列表
    create:
        添加商品到购物车
    update:
        更新购物车商品数量
    delete:
        从购物车中删除商品
    """
    # 权限问题:购物车和用户权限关联,这儿和用户操作差不多
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)  # 用户必须登录才能访问
    authentication_classes = (JWTAuthentication, SessionAuthentication)  # 配置登录认证:支持JWT认证和DRF基本认证

以上就是购物车的权限和认证相关的问题,接下来开始完成Serializer的编写。

对比需要继承的serializers.ModelSerializerserializers.Serializer,根据项目逻辑需求,这儿用serializers.Serializer比较合适,因为它比较灵活。

如果用户对某个商品添加到购物车,之后再对该商品进行重复添加,那么是需要将购物车中的数据进行修改,也就是当前用户的购物车中的每一种商品都是唯一的,那么修改一下models,为其增加联合唯一的限定。

# apps/trade/models.py

class ShoppingCart(models.Model):
    """
    购物车
    """
    user = models.ForeignKey(User, verbose_name='用户', help_text='用户', on_delete=models.CASCADE, related_name='shopping_carts')
    goods = models.ForeignKey(Goods, verbose_name='商品', help_text='商品', on_delete=models.CASCADE)
    nums = models.IntegerField(default=0, verbose_name='购买数量', help_text='购买数量')
    add_time = models.DateTimeField(auto_now_add=True, verbose_name='添加时间')

    class Meta:
        verbose_name_plural = verbose_name = '购物车'
        unique_together = ['user', 'goods']  # 用户和商品联合唯一

    def __str__(self):
        return "{}({})".format(self.goods.name, self.nums)

那么试想下,如果 user 和 goods 已经存在了,再向购物车添加同一个商品,不希望得到验证说这个商品已经存在,而是希望在当前数量上加1。

如果使用的是serializers.ModelSerializer,模型中定义了unique_together = ['user', 'goods'],进行validate时就会抛出异常,就算是重写def create(self, validated_data)方法也是无效的,因为在进行该方法前,就已经进行了validate。继续解释下,ShoppingCartViewSet继承的是viewsets.ModelViewSet,在这里面有个mixins.CreateModelMixin

BLOG_20190719_191509_29

这个CreateModelMixincreate的时候首先会调用serializer.is_valid(raise_exception=True),接下来会调用self.perform_create(serializer)执行数据保存serializer.save(),在验证的时候就已经报错了,就不会调用create方法了,所以是无法保存成功的。

当然,我们也可以重写CreateModelMixin中的create的方法,就可以控制该方法的所有步骤。

如果不用serializer,那么它的验证功能就享受不到了,不用serializer做,就需要在views中去完成;另外生成文档的时候,字段是从serializer中取得,如果不用那么文档的功能就缺失了。

用了serializer,但用的是serializers.ModelSerializer,那么刚才的验证是通不过的。所以这儿用底层的serializers.Serializer,自己来做validate

购物车序列化ShoppingCartSerializer

下面一步一步去完成,ShoppingCart模型中用到了usergoodsnums这几个重要的字段,user和之前用户操作中一样,直接复制过来即可。

# apps/trade/serializers.py

from rest_framework import serializers
from goods.models import Goods


class ShoppingCartSerializer(serializers.Serializer):
    user = serializers.HiddenField(
        default=serializers.CurrentUserDefault()  # 表示user为隐藏字段,默认为获取当前登录用户
    )
    nums = serializers.IntegerField(required=True, min_value=1, label='商品数量', help_text='商品数量',
                                    error_messages={
                                        'min_value': '商品数量不能小于1',
                                        'required': '请选择购买数量'
                                    })
    goods = serializers.PrimaryKeyRelatedField(queryset=Goods.objects.all(), required=True, label='商品')

分析:访问 https://www.django-rest-framework.org/api-guide/fields/ 可以看到DRF提供的Serializer字段;而像外键这个关系型的,可以查看 https://www.django-rest-framework.org/api-guide/relations/ , PrimaryKeyRelatedField就是需要在这goods用到的

参数说明:

  • queryset:在验证字段输入时用于模型实例查找的queryset。关系必须显式设置queryset,或者设置read_only=True
  • many:如果应用于多对多关系,则应将此参数设置为True
  • allow_null:如果设置为True,该字段将接受None值或空字符串,用于可空关系。默认值为False
  • pk_field:设置为一个字段来控制主键值的序列化/反序列化。例如,pk_field=UUIDField(format='hex')将把UUID主键序列化为紧凑的十六进制表示形式。

ShoppingCartSerializer添加商品重写create()方法

由于使用的是serializers.Serializer,没有定义create()方法,需要自己去重写。而ModelSerializer是写好了该方法。

用户在添加购物车时,也就是在数据库中添加一条记录。还要分为两种情况,当购物车数据库中没有这个商品时,执行添加;当已存在该商品时,执行数量更新。

validated_data也就是上方定义的字段传过来之前已经处理好的数据,例如,nums如果数量为负数,那么该数据到不了create()方法。

# apps/trade/serializers.py

from rest_framework import serializers
from goods.models import Goods
from trade.models import ShoppingCart


class ShoppingCartSerializer(serializers.Serializer):
    user = serializers.HiddenField(
        default=serializers.CurrentUserDefault()  # 表示user为隐藏字段,默认为获取当前登录用户
    )
    nums = serializers.IntegerField(required=True, min_value=1, label='商品数量', help_text='商品数量',
                                    error_messages={
                                        'min_value': '商品数量不能小于1',
                                        'required': '请选择购买数量'
                                    })
    goods = serializers.PrimaryKeyRelatedField(queryset=Goods.objects.all(), required=True, label='商品')

    def create(self, validated_data):
        user = self.context['request'].user  # serializer中获取当前用户,而views是直接从request中获取
        nums = validated_data['nums']
        goods = validated_data['goods']

        # 查询记录是否存在,存在,则进行数量加,不存在则新创建
        shopping_cart = ShoppingCart.objects.filter(user=user, goods=goods)
        if shopping_cart:
            shopping_cart = shopping_cart.first()
            shopping_cart.nums += nums
            shopping_cart.save()
        else:
            shopping_cart = ShoppingCart.objects.create(**validated_data)
        # 最后要返回创建后的结果
        return shopping_cart

ShoppingCartViewSet中获取当前登录用户购物车

首先引入上方创建好的序列化类,并且指定queryset,只能显示当前登录用户的购物车列表

# apps/trade/views.py

from .serializers import ShoppingCartSerializer
from .models import ShoppingCart


class ShoppingCartViewSet(viewsets.ModelViewSet):
    """
    购物车功能实现
    list:
        获取购物车列表
    create:
        添加商品到购物车
    update:
        更新购物车商品数量
    delete:
        从购物车中删除商品
    """
    # 权限问题:购物车和用户权限关联,这儿和用户操作差不多
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)  # 用户必须登录才能访问
    authentication_classes = (JWTAuthentication, SessionAuthentication)  # 配置登录认证:支持JWT认证和DRF基本认证
    serializer_class = ShoppingCartSerializer
    queryset = ShoppingCart.objects.all()

    def get_queryset(self):
        # 只能显示当前用户的购物车列表
        return self.queryset.filter(user=self.request.user)

BLOG_20190719_191447_50

选择某个商品添加到购物车,会返回该商品的id和添加的数量。

BLOG_20190719_191440_59

再访问 http://127.0.0.1:8000/shoppingcart/ 可以看到购物车已有的商品id及数量。

修改购物车中商品数量

ShoppingCartSerializer指定商品更新重写update()方法

由于每个用户的购物车商品都是唯一的,所以只需要传递商品的id,就可以获取到该记录。

# apps/trade/serializers.py

class ShoppingCartViewSet(viewsets.ModelViewSet):
    """
    购物车功能实现
    list:
        获取购物车列表
    create:
        添加商品到购物车
    update:
        更新购物车商品数量
    delete:
        从购物车中删除商品
    """
    # 权限问题:购物车和用户权限关联,这儿和用户操作差不多
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)  # 用户必须登录才能访问
    authentication_classes = (JWTAuthentication, SessionAuthentication)  # 配置登录认证:支持JWT认证和DRF基本认证
    serializer_class = ShoppingCartSerializer
    queryset = ShoppingCart.objects.all()
    lookup_field = 'goods'

    def get_queryset(self):
        # 只能显示当前用户的购物车列表
        return self.queryset.filter(user=self.request.user)

比如上方商品id为105的购物车,可以直接访问 http://127.0.0.1:8000/shoppingcart/105/ 获取该购物车详情

BLOG_20190719_191429_74

在这就可以对商品进行删除,也可以对商品数量进行更新。测试下更新,修改商品数量,然后点击PUT,就报错了

    raise NotImplementedError('`update()` must be implemented.')
NotImplementedError: `update()` must be implemented.

意思就是没有update()方法。

可以查看rest的源码 rest_framework.serializers.Serializer,这个类继承的BaseSerializer有这样一个方法:

# 源码:rest_framework.serializers.BaseSerializer#update
    def update(self, instance, validated_data):
        raise NotImplementedError('`update()` must be implemented.')

Serializer没有重写update()方法,使用时会抛出上面的异常。而ModelSerializer是有该方法的rest_framework.serializers.ModelSerializer#update,所以直接拿来用不会报错。

BLOG_20190719_191421_50

现在数量新的商品数量后,在点击PUT就不会报错了,数量也能正常更新。

测试指定商品删除

测试删除功能,可以指定商品进行删除,删除没有返回结果。也无须重写删除方法

BLOG_20190719_191414_18

BLOG_20190719_191409_62

访问 http://127.0.0.1:8000/docs/#shoppingcart-list ,在文档页面也会出现刚才添加的接口

BLOG_20190719_191401_74

Vue和购物车接口联调

ShoppingCartListSerializer购物车显示商品列表

现在购物车中只有商品的id,但是一般还需要显示商品的图片、名称、单价等信息。

而现在ShoppingCartSerializergoods=serializers.PrimaryKeyRelatedField(queryset=Goods.objects.all(), required=True, label='商品')关联商品的主键id,所以要动态设置Serializer,这是个ModelSerializer

新建一个Serializer

# apps/trade/serializers.py

from goods.serializers import GoodsSerializer


class ShoppingCartListSerializer(serializers.ModelSerializer):
    goods = GoodsSerializer(many=False)  # 一个购物车的记录只会对应一个商品,默认many=False,也就是可以不写

    class Meta:
        model = ShoppingCart
        fields = "__all__"

ShoppingCartViewSet购物车动态使用序列化类

然后在ViewSet中动态获取要使用的Serializer

# apps/trade/views.py

from .serializers import ShoppingCartSerializer, ShoppingCartListSerializer

class ShoppingCartViewSet(viewsets.ModelViewSet):
    """
    购物车功能实现
    list:
        获取购物车列表
    create:
        添加商品到购物车
    update:
        更新购物车商品数量
    delete:
        从购物车中删除商品
    """
    # 权限问题:购物车和用户权限关联,这儿和用户操作差不多
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)  # 用户必须登录才能访问
    authentication_classes = (JWTAuthentication, SessionAuthentication)  # 配置登录认证:支持JWT认证和DRF基本认证
    # serializer_class = ShoppingCartSerializer  # 使用get_serializer_class(),这个就不需要了
    queryset = ShoppingCart.objects.all()
    lookup_field = 'goods'

    def get_serializer_class(self):
        if self.action == 'list':
            # 当获取购物车列表时,使用ModelSerializer,可以显示购物车商品详情
            return ShoppingCartListSerializer
        else:
            return ShoppingCartSerializer

    def get_queryset(self):
        # 只能显示当前用户的购物车列表
        return self.queryset.filter(user=self.request.user)

现在访问 http://127.0.0.1:8000/shoppingcart/ 可以看到购物车商品列表

BLOG_20190719_191347_89

只有在显示购物车列表时才会用到这个序列化类,显示某个商品详情,或更新就是用之前的类

Vue中配置购物车接口

修改接口,添加购物车的相关接口

// src/api/api.js

//获取购物车商品
export const getShopCarts = params => {
    return axios.get(`${local_host}/shoppingcart/`)
};
// 添加商品到购物车
export const addShopCart = params => {
    return axios.post(`${local_host}/shoppingcart/`, params)
};
//更新购物车商品信息
export const updateShopCart = (goodsId, params) => {
    return axios.patch(`${local_host}/shoppingcart/` + goodsId + '/', params)
};
//删除某个商品的购物记录
export const deleteShopCart = goodsId => {
    return axios.delete(`${local_host}/shoppingcart/` + goodsId + '/')
};

选择某一商品,点击添加到购物车

BLOG_20190719_191339_17

访问 http://127.0.0.1:8080/#/app/shoppingcart/cart 购物车可以查看到列表

BLOG_20190719_191332_29

Vue加入购物车逻辑分析

加入购物车按钮位置




    
    加入购物车

当点击加入购物车按钮时,会调用

// src/views/productDetail/productDetail.vue

addShoppingCart() { //加入购物车
    addShopCart({
        goods: this.productId, // 商品id
        nums: this.buyNum, // 购买数量
    }).then((response) => {
        this.$refs.model.setShow();
        // 更新store数据
        this.$store.dispatch('setShopList');

    }).catch(function (error) {
        console.log(error);
    });
},

这里面会请求addShopCart传递商品的id和购买数量,这个数量和输入框的值一样,直接请求api中创建购物车的接口

// src/api/api.js

// 添加商品到购物车
export const addShopCart = params => {
    return axios.post(`${local_host}/shoppingcart/`, params)
};

当提交成功后,会请示this.$refs.model.setShow()显示添加成功的弹框。

页面顶部显示购物车简单列表

然后this.$store.dispatch('setShopList');更新vuex,点击setShopList可以访问到src/store/actions.jsexport const setShopList = makeAction(types.SET_SHOPLIST);,实际上就是src/store/mutations.js

// src/store/mutations.js

[types.SET_SHOPLIST](state) { //设置购物车数据
    // token = cookie.getCookie('token')
    if (cookie.getCookie('token') != null) {
        getShopCarts().then((response) => {
            // 更新store数据
            state.goods_list.goods_list = response.data;
            //console.log(response.data);
            var totalPrice = 0;
            response.data.forEach(function (entry) {
                totalPrice += entry.goods.shop_price * entry.nums
            });
            state.goods_list.totalPrice = totalPrice;

        }).catch(function (error) {
            console.log(error);
        });
    }
},

这一段的意思就是调用getShopCarts()函数,获取到购物车的商品,将这些商品的数据填充到state中,更新src/store/store.jsgoods_list.goods_list

const goods_list =  {
       totalPrice:'',
        goods_list:[]

    }

经过Vue的处理,显示到顶部

BLOG_20190719_191321_46



去购物车结算 {{goods_list.goods_list.length}}

{{item.goods.name}}

{{item.goods.shop_price}} X {{item.nums}}

×
{{goods_list.goods_list.length}}件商品哦~

总价:{{goods_list.totalPrice}} 去结算

购物车列表显示功能

当进入购物车页面时 http://127.0.0.1:8080/#/app/shoppingcart/cart 组件创建

//src/views/cart/cart.vue

created() {
    // 请求购物车商品
    getShopCarts().then((response) => {
        //console.log(response.data);
        // 更新store数据
        //this.goods_list = response.data;
        var totalPrice = 0;
        this.goods.goods_list = response.data;
        response.data.forEach(function (entry) {
            totalPrice += entry.goods.shop_price * entry.nums
            //console.log(entry.goods.shop_price);
        });

        this.goods.totalPrice = totalPrice
        this.totalPrice = totalPrice
    }).catch(function (error) {
    });
    this.getAllAddr()
},

然后向html中填充数据



  • ¥{{item.goods.shop_price}}元

    - +
    ¥{{item.goods.shop_price * item.nums}}元
  • 购物车中删除商品功能

    当点击删除时,执行deleteGoods(index, item.goods.id),传递商品的id

    // src/views/cart/cart.vue
    
    deleteGoods(index, id) { //移除购物车
        alert('您确定把该商品移除购物车吗');
        deleteShopCart(id).then((response) => {
            //console.log(response.data);
            this.goods.goods_list.splice(index, 1);
    
            // 更新store数据
            this.$store.dispatch('setShopList');
    
        }).catch(function (error) {
            console.log(error);
        });
    },
    

    接下俩调用删除deleteShopCart(id)的api

    // src/api/api.js
    
    //删除某个商品的购物记录
    export const deleteShopCart = goodsId => {
        return axios.delete(`${local_host}/shoppingcart/` + goodsId + '/')
    };
    

    删除成功后更新Vue中显示的购物车商品列表数据

    购物车更新商品数量功能

    在购物车中有一个增加数量和减少数量的按钮

    
    
    
    - +

    点击reduceCartNum(index, item.goods.id),减少一个数量

    // src/views/cart/cart.vue
    
    reduceCartNum(index, id) { //删除数量
        if (this.goods.goods_list[index].nums <= 1) {
            this.deleteGoods(index, id)
        } else {
            updateShopCart(id, {
                nums: this.goods.goods_list[index].nums - 1
            }).then((response) => {
                this.goods.goods_list[index].nums = this.goods.goods_list[index].nums - 1;
                // 更新store数据
                this.$store.dispatch('setShopList');
                //更新总价
                this.setTotalPrice();
    
            }).catch(function (error) {
                console.log(error);
            });
        }
    },
    

    当数量减小到0,就从购物车删除该商品。

    更新购物车updateShopCart()指定商品的id,传递数量(当前的数量-1)提交到api

    // src/api/api.js
    
    //更新购物车商品信息
    export const updateShopCart = (goodsId, params) => {
        return axios.patch(`${local_host}/shoppingcart/` + goodsId + '/', params)
    };
    

    提交成功后更新购物车商品数据。

    当点击addCartNum(index, item.goods.id)增加1个数量时,调用

    // src/views/cart/cart.vue
    
    addCartNum(index, id) { //添加数量
        updateShopCart(id, {
            nums: this.goods.goods_list[index].nums + 1
        }).then((response) => {
            this.goods.goods_list[index].nums = this.goods.goods_list[index].nums + 1;
            // 更新store数据
            this.$store.dispatch('setShopList');
            //更新总价
            this.setTotalPrice();
    
        }).catch(function (error) {
            console.log(error);
        });
    },
    

    请求updateShopCart()指定商品的id,传递数量(当前的数量+1)提交到api,提交成功后Vue中将当前商品数量+1,并更新购物车中的数据。

    购物车收货地址获取

  • 地址:{{item.province}} {{item.city}} {{item.district}} {{item.address}}

    电话:{{item.signer_mobile}}

    姓名:{{item.signer_name}}

  • 组件创建时,获取所有的收货地址,遍历显示到页面

    // src/views/cart/cart.vue
    
    getAllAddr() { //获得所有配送地址
        getAddress().then((response) => {
            this.addrInfo = response.data;
        }).catch(function (error) {
            console.log(error);
        });
    },
    

    你可能感兴趣的:(【Vue+DRF生鲜电商】25.商品添加购物车接口功能,Vue和购物车联调)