专题: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.ModelSerializer
和serializers.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
这个CreateModelMixin
在create
的时候首先会调用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。
下面一步一步去完成,ShoppingCart
模型中用到了user
、goods
、nums
这几个重要的字段,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主键序列化为紧凑的十六进制表示形式。由于使用的是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
首先引入上方创建好的序列化类,并且指定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)
[外链图片转存失败(img-Y80mxmPa-1563550151566)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191447_50.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191447_50.png”)]
选择某个商品添加到购物车,会返回该商品的id和添加的数量。
[外链图片转存失败(img-2Q2WCa3i-1563550151566)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191440_59.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191440_59.png”)]
再访问 http://127.0.0.1:8000/shoppingcart/ 可以看到购物车已有的商品id及数量。
由于每个用户的购物车商品都是唯一的,所以只需要传递商品的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/ 获取该购物车详情
[外链图片转存失败(img-4XX04zvR-1563550151567)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191429_74.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191429_74.png”)]
在这就可以对商品进行删除,也可以对商品数量进行更新。测试下更新,修改商品数量,然后点击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
,所以直接拿来用不会报错。
[外链图片转存失败(img-RnfjbRn6-1563550151567)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191421_50.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191421_50.png”)]
现在数量新的商品数量后,在点击PUT就不会报错了,数量也能正常更新。
测试删除功能,可以指定商品进行删除,删除没有返回结果。也无须重写删除方法
[外链图片转存失败(img-Nf5RIoKz-1563550151567)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191414_18.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191414_18.png”)]
访问 http://127.0.0.1:8000/docs/#shoppingcart-list ,在文档页面也会出现刚才添加的接口
[外链图片转存失败(img-8YLH1Op0-1563550151568)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191401_74.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191401_74.png”)]
现在购物车中只有商品的id,但是一般还需要显示商品的图片、名称、单价等信息。
而现在ShoppingCartSerializer
中goods=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__"
然后在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/ 可以看到购物车商品列表
[外链图片转存失败(img-2yNhWIQM-1563550151568)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191347_89.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191347_89.png”)]
只有在显示购物车列表时才会用到这个序列化类,显示某个商品详情,或更新就是用之前的类
修改接口,添加购物车的相关接口
// 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 + '/')
};
选择某一商品,点击添加到购物车
[外链图片转存失败(img-Q4qw6vyK-1563550151569)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191339_17.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191339_17.png”)]
访问 http://127.0.0.1:8080/#/app/shoppingcart/cart 购物车可以查看到列表
[外链图片转存失败(img-vWGjsdMm-1563550151569)(https://blog.starmeow.cn/media/blog/images/2019/07/BLOG_20190719_191332_29.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190719_191332_29.png”)]
加入购物车按钮位置
<a class="btn" id="buy_btn" @click="addShoppingCart">
<i class="iconfont">i>
加入购物车
a>
当点击加入购物车按钮时,会调用
// 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.js
:export 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.js
中goods_list.goods_list
const goods_list = {
totalPrice:'',
goods_list:[]
}
经过Vue的处理,显示到顶部
<div class="hd_cart" id="ECS_CARTINFO" @mouseover="overShopCar" @mouseout="outShopCar">
<router-link class="tit" :to="'/app/shoppingcart/cart'" target=_blank>
<b class="iconfont">b>去购物车结算<span><i class="iconfont">i>span>
<em class="num" id="hd_cartnum"
style="visibility: visible;">{{goods_list.goods_list.length}}em>
router-link>
<div class="list" v-show="showShopCar">
<div class="data">
<dl v-for="(item,index) in goods_list.goods_list">
<dt>
<router-link :to="'/app/home/productDetail/'+item.goods.id" target=_blank><img
:src="item.goods.goods_front_image">router-link>
dt>
<dd>
<h4>
<router-link :to="'/app/home/productDetail/'+item.goods.id" target=_blank>
{{item.goods.name}}
router-link>
h4>
<p><span class="red">{{item.goods.shop_price}}span> <i>Xi> {{item.nums}}
p>
<a title="删除" class="iconfont del" @click="deleteGoods(index,item.goods.id)">×a>
dd>
dl>
div>
<div class="count">共<span class="red" id="hd_cart_count">{{goods_list.goods_list.length}}span>件商品哦~
<p>总价:<span class="red"><em id="hd_cart_total">{{goods_list.totalPrice}}em>span>
<router-link class="btn" :to="'/app/shoppingcart/cart'" target=_blank>去结算
router-link>
p>
div>
div>
div>
当进入购物车页面时 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中填充数据
<li class="cle hover" style="border-bottom-style: none;" v-for="(item,index) in goods.goods_list">
<div class="pic">
<a target="_blank"> <img :alt="item.goods.name" :src="item.goods.goods_front_image">a>
div>
<div class="name">
<a target="_blank">{{item.goods.name}}a>
<p>p>
div>
<div class="price-xj">
<p><em>¥{{item.goods.shop_price}}元em>p>
div>
<div class="nums" id="nums">
<span class="minus" title="减少1个数量" @click="reduceCartNum(index, item.goods.id);">-span>
<input type="text" v-model="item.nums">
<span class="add" title="增加1个数量" @click="addCartNum(index, item.goods.id);">+span>
div>
<div class="price-xj"><span>span>
<em id="total_items_3137">¥{{item.goods.shop_price * item.nums}}元em>
div>
<div class="del">
<a class="btn-del" @click="deleteGoods(index, item.goods.id)">删除a>
div>
li>
当点击删除时,执行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中显示的购物车商品列表数据
在购物车中有一个增加数量和减少数量的按钮
<div class="nums" id="nums">
<span class="minus" title="减少1个数量" @click="reduceCartNum(index, item.goods.id);">-span>
<input type="text" v-model="item.nums">
<span class="add" title="增加1个数量" @click="addCartNum(index, item.goods.id);">+span>
div>
点击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,并更新购物车中的数据。
<li v-for="item in addrInfo" :class="{'addressActive':addressActive==item.id}" @click="selectAddr(item.id)">
<p class="item">地址:{{item.province}} {{item.city}} {{item.district}} {{item.address}}p>
<p class="item">电话:{{item.signer_mobile}}p>
<p class="item">姓名:{{item.signer_name}}p>
li>
组件创建时,获取所有的收货地址,遍历显示到页面
// src/views/cart/cart.vue
getAllAddr() { //获得所有配送地址
getAddress().then((response) => {
this.addrInfo = response.data;
}).catch(function (error) {
console.log(error);
});
},