专题:Vue+Django REST framework前后端分离生鲜电商
Vue+Django REST framework 打造前后端分离的生鲜电商项目(慕课网视频)。
Github地址:https://github.com/xyliurui/DjangoOnlineFreshSupermarket ;
Django版本:2.2、djangorestframework:3.9.2。
更多内容请点击 我的博客 查看,欢迎来访。
先看下之前的模型
class OrderInfo(models.Model):
"""
订单
"""
ORDER_STATUS = (
('success', '成功'),
('cancel', '取消'),
('topaid', '待支付')
)
user = models.ForeignKey(User, verbose_name='用户', help_text='用户', on_delete=models.CASCADE, related_name='order_infos')
order_sn = models.CharField(max_length=30, unique=True, verbose_name='订单号', help_text='订单号')
trade_no = models.CharField(max_length=100, unique=True, null=True, blank=True, verbose_name='支付')
pay_status = models.CharField(choices=ORDER_STATUS, max_length=20, verbose_name='订单状态', help_text='订单状态')
post_script = models.CharField(max_length=50, blank=True, null=True, verbose_name='订单留言', help_text='订单留言')
order_amount = models.FloatField(default=0.0, verbose_name='订单金额', help_text='订单金额')
pay_time = models.DateTimeField(null=True, blank=True, verbose_name='支付时间', help_text='支付时间')
# 用户信息
address = models.CharField(max_length=200, default='', verbose_name='收货地址', help_text='收货地址')
signer_name = models.CharField(max_length=20, default='', verbose_name='签收人', help_text='签收人')
signer_mobile = models.CharField(max_length=11, verbose_name='联系电话', help_text='联系电话')
add_time = models.DateTimeField(auto_now_add=True, verbose_name='添加时间')
class Meta:
verbose_name_plural = verbose_name = '订单'
def __str__(self):
return "{}".format(self.order_sn)
分析,order_sn
字段不能为空,这是不行的,订单号需要由后台生成。但用户点击去结算时,后台需要生成一个订单号,但是如果我们用到CreateModelMixin
,这个会对该字段进行验证,在用户界面是不可能post一个订单号到后台,从而导致验证失败,所以该字段增加设置blank=True, null=True
pay_status
可以设置一个默认值,为待支付状态default='topaid'
,提交订单时,用户也不用指定该字段了。
address
不直接使用外键的原因是:用户如果修改了个人中心的收货地址,订单中地址就会跟着变化,显然是不对的,所以订单中的地址就保存一个当时下单的地址组合的字符串。
修改后
# apps/trade/models.py
class OrderInfo(models.Model):
"""
订单
"""
ORDER_STATUS = (
('success', '成功'),
('cancel', '取消'),
('topaid', '待支付')
)
user = models.ForeignKey(User, verbose_name='用户', help_text='用户', on_delete=models.CASCADE, related_name='order_infos')
order_sn = models.CharField(max_length=30, unique=True, blank=True, null=True, verbose_name='订单号', help_text='订单号')
trade_no = models.CharField(max_length=100, unique=True, null=True, blank=True, verbose_name='支付交易号', help_text='支付交易号')
pay_status = models.CharField(choices=ORDER_STATUS, default='topaid', max_length=20, verbose_name='订单状态', help_text='订单状态')
post_script = models.CharField(max_length=50, blank=True, null=True, verbose_name='订单留言', help_text='订单留言')
order_amount = models.FloatField(default=0.0, verbose_name='订单金额', help_text='订单金额')
pay_time = models.DateTimeField(null=True, blank=True, verbose_name='支付时间', help_text='支付时间')
# 用户信息
address = models.CharField(max_length=200, default='', verbose_name='收货地址', help_text='收货地址')
signer_name = models.CharField(max_length=20, default='', verbose_name='签收人', help_text='签收人')
signer_mobile = models.CharField(max_length=11, verbose_name='联系电话', help_text='联系电话')
add_time = models.DateTimeField(auto_now_add=True, verbose_name='添加时间')
class Meta:
verbose_name_plural = verbose_name = '订单'
def __str__(self):
return "{}".format(self.order_sn)
对于一个订单,有多个商品,所以需要设置另外一个model,用外键关联。
序列化中用户也是需要不可见的
# apps/trade/serializers.py
from trade.models import ShoppingCart, OrderInfo
class OrderInfoSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault() # 表示user为隐藏字段,默认为获取当前登录用户
)
class Meta:
model = OrderInfo
fields = "__all__"
不使用viewsets.ModelViewSet
原因是,订单一般不允许修改的,所有要自定义继承
# apps/trade/views.py
from .serializers import ShoppingCartSerializer, ShoppingCartListSerializer, OrderInfoSerializer
from .models import ShoppingCart, OrderInfo
class OrderInfoViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin,mixins.DestroyModelMixin, viewsets.GenericViewSet):
"""
订单管理
list:
获取个人订单
create:
新建订单
delete:
删除订单
detail:
订单详情
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly) # 用户必须登录才能访问
authentication_classes = (JWTAuthentication, SessionAuthentication) # 配置登录认证:支持JWT认证和DRF基本认证
queryset = OrderInfo.objects.all()
serializer_class = OrderInfoSerializer # 添加序列化
def get_queryset(self):
return self.queryset.filter(user=self.request.user)
# DjangoOnlineFreshSupermarket/urls.py
from trade.views import ShoppingCartViewSet, OrderInfoViewSet
router.register(r'orderinfo', OrderInfoViewSet, base_name='orderinfo') # 订单管理
现在访问 http://127.0.0.1:8000/orderinfo/ 可以看到DRF生成的页面
对于订单提交,会告诉后台创建一个订单,在这提交订单是一个比较简单的逻辑,即将购物车ShoppingCart
中的该用户对应的单品和数量,添加到订单商品OrderGoods
中,然后清空该用户的购物车。
可以在OrderInfoViewSet
中重载perform_create(self, serializer)
方法(这个在mixins.CreateModelMixin
类中)
def perform_create(self, serializer):
serializer.save()
在.save()
之前,需要生成一个订单号,所以修改序列化类,在数据验证通过后生成一个订单号,修改OrderInfoSerializer
,重载validate(self, attrs)
方法
# apps/trade/serializers.py
class OrderInfoSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault() # 表示user为隐藏字段,默认为获取当前登录用户
)
def generate_order_sn(self):
# 当前时间+userid+随机数
import time
from random import randint
order_sn = '{time_str}{user_id}{random_str}'.format(time_str=time.strftime('%Y%m%d%H%M%S'), user_id=self.context['request'].user.id, random_str=randint(10, 99))
return order_sn
def validate(self, attrs):
# 数据验证成功后,生成一个订单号
attrs['order_id'] = self.generate_order_sn()
return attrs
class Meta:
model = OrderInfo
fields = "__all__"
之后修改OrderInfoViewSet
,重载perform_create(self, serializer)
方法
class OrderInfoViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet):
"""
订单管理
list:
获取个人订单
create:
新建订单
delete:
删除订单
detail:
订单详情
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly) # 用户必须登录才能访问
authentication_classes = (JWTAuthentication, SessionAuthentication) # 配置登录认证:支持JWT认证和DRF基本认证
queryset = OrderInfo.objects.all()
serializer_class = OrderInfoSerializer # 添加序列化
def get_queryset(self):
return self.queryset.filter(user=self.request.user)
def perform_create(self, serializer):
# 完成创建后保存到数据库,可以拿到保存的OrderInfo对象
order = serializer.save()
shopping_carts = ShoppingCart.objects.filter(user=self.request.user)
# 将该用户购物车所有商品都取出来放在订单商品中
for shopping_cart in shopping_carts:
OrderGoods.objects.create(
order=order,
goods=shopping_cart.goods,
goods_nums=shopping_cart.nums
)
# 然后清空该用户购物车
shopping_carts.delete()
在上面的图中,订单号、支付交易号、订单状态、支付日期(需要支付后才有时间)信息是不能显示给用户的,只能后台去修改
# apps/trade/serializers.py
class OrderInfoSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault() # 表示user为隐藏字段,默认为获取当前登录用户
)
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 generate_order_sn(self):
# 当前时间+userid+随机数
import time
from random import randint
order_sn = '{time_str}{user_id}{random_str}'.format(time_str=time.strftime('%Y%m%d%H%M%S'), user_id=self.context['request'].user.id, random_str=randint(10, 99))
return order_sn
def validate(self, attrs):
# 数据验证成功后,生成一个订单号
attrs['order_sn'] = self.generate_order_sn()
return attrs
class Meta:
model = OrderInfo
fields = "__all__"
return attrs
打上断点调试,在DRF中 http://127.0.0.1:8000/orderinfo/ 填入一些信息后POST,可以在后端看到attrs
的内容
然后进入OrderInfoViewSet
完成创建perform_create
处理,将购物车的商品添加到订单商品中,并删除购物车商品
访问 http://127.0.0.1:8000/orderinfo/1/ 可以看到有订单删除
该功能可以将订单关联的商品一起删除。
访问 http://127.0.0.1:8080/#/app/home/member/order 可以查看我的订单
由于刚才订单删除了,所以为空。在Vue中,添加商品到购物车,然后选择配送地址,点击去结算
当用户准备创建订单时,会点击去结算
<p class="sumup"><a class="btn" @click="balanceCount">去结算a>p>
点击之后进入
// src/views/cart/cart.vue
balanceCount() { // 结算
if (this.addrInfo.length == 0) {
alert("请选择收货地址")
} else {
createOrder(
{
post_script: this.post_script,
address: this.address,
signer_name: this.signer_name,
signer_mobile: this.signer_mobile,
order_amount: this.totalPrice
}
).then((response) => {
alert('订单创建成功');
window.location.href = response.data.alipay_url;
}).catch(function (error) {
console.log(error);
});
}
},
当没有选择地址弹框提示,接下来请求createOrder()
也就是调用 api.js 中的接口
//添加订单
export const createOrder = params => {
return axios.post(`${local_host}/orderinfo/`, params)
};
传递用户留言、收货地址、签收人、电话、订单总金额这个信息到后台,订单创建成功后会跳转到 http://127.0.0.1:8080/undefined 因为给出支付的地址,所以地址有误。
用户中心可以查看订单列表 http://127.0.0.1:8080/#/app/home/member/order
<tr v-for="item in orders">
<td align="center" bgcolor="#ffffff"><a class="f6" @click="goDetail(item.id)">{{item.order_sn}}a>td>
<td align="center" bgcolor="#ffffff">{{item.add_time}}td>
<td align="right" bgcolor="#ffffff">¥{{item.order_amount}}元td>
<td v-if="item.pay_status == 'topaid' " align="center" bgcolor="#ffffff">待支付td>
<td v-if="item.pay_status == 'success' " align="center" bgcolor="#ffffff">已支付td>
<td align="center" bgcolor="#ffffff"><font class="f6"><a @click="cancelOrder(item.id)">取消订单a>font>td>
tr>
当组件创建时,调用
// src/views/member/order.vue
created () {
this.getOrder();
},
即
// src/views/member/order.vue
getOrder () {
getOrders().then((response)=> {
this.orders = response.data;
}).catch(function (error) {
console.log(error);
});
},
该函数会请求api获取订单数据
// src/api/api.js
//获取订单
export const getOrders = () => {
return axios.get(`${local_host}/orderinfo/`)
};
将结果遍历显示到表格中。
这是一个待支付状态,另外还有一个取消订单操作,用户点击取消订单,将该订单id传递给cancelOrder (id)
函数
//src/views/member/order.vue
cancelOrder (id) {
alert('您确认要取消该订单吗?取消后此订单将视为无效订单');
delOrder(id).then((response)=> {
alert('订单删除成功')
}).catch(function (error) {
console.log(error);
});
},
请求api对订单进行删除
//src/api/api.js
//删除订单
export const delOrder = orderId => {
return axios.delete(`${local_host}/orderinfo/` + orderId + '/')
};
点击订单号可以进入订单详情
但是订单中的商品列表没有正常显示出来,所以要动态使用序列化,在显示订单详情时,使用另一个Serializer,并返回商品详情信息
添加一个新的序列化类,用于显示订单详情
# apps/trade/serializers.py
from trade.models import ShoppingCart, OrderInfo, OrderGoods
from goods.serializers import GoodsSerializer
# 订单中的商品序列化
class OrderGoodsSerializer(serializers.ModelSerializer):
goods = GoodsSerializer() # 直接用goods.serializers商品详情的序列化
class Meta:
model = OrderGoods
fields = "__all__"
# 订单详情序列化
class OrderInfoDetailSerializer(serializers.ModelSerializer):
order_goods = OrderGoodsSerializer(many=True) # 为OrderGoods中外键关联名称
class Meta:
model = OrderInfo
fields = "__all__"
当访问订单详情就会使用专用的Serializer
# apps/trade/views.py
class OrderInfoViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet):
"""
订单管理
list:
获取个人订单
create:
新建订单
delete:
删除订单
detail:
订单详情
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly) # 用户必须登录才能访问
authentication_classes = (JWTAuthentication, SessionAuthentication) # 配置登录认证:支持JWT认证和DRF基本认证
queryset = OrderInfo.objects.all()
# serializer_class = OrderInfoSerializer # 添加序列化
def get_queryset(self):
return self.queryset.filter(user=self.request.user)
def get_serializer_class(self): # 动态序列化,当显示订单详情,用另一个Serializer
if self.action == 'retrieve':
return OrderInfoDetailSerializer
else:
return OrderInfoSerializer
def perform_create(self, serializer):
# 完成创建后保存到数据库,可以拿到保存的值
order = serializer.save()
shopping_carts = ShoppingCart.objects.filter(user=self.request.user)
# 将该用户购物车所有商品都取出来放在订单商品中
for shopping_cart in shopping_carts:
OrderGoods.objects.create(
order=order,
goods=shopping_cart.goods,
goods_nums=shopping_cart.nums
)
# 然后清空该用户购物车
shopping_carts.delete()
访问 http://127.0.0.1:8000/orderinfo/3/?format=json 可以看到订单详情序列化的内容,格式化为json,变化收起
访问 http://127.0.0.1:8080/#/app/home/member/orderDetail/3 可以进入该订单详情页
组件创建时,获取url的订单id,然后请求订单详情
// src/views/member/orderDetail.vue
created() {
this.orderId = this.$route.params.orderId;
this.getOrderInfo();
//this.getReceiveByOrderId();
},
获取订单详情
// src/views/member/orderDetail.vue
getOrderInfo() { //获取订单信息
getOrderDetail(this.orderId).then((response) => {
this.orderInfo = response.data;
var totalPrice = 0;
//console.log(response.data.order_goods);
response.data.order_goods.forEach(function (entry) {
totalPrice += entry.goods_nums * entry.goods.shop_price
});
this.totalPrice = totalPrice
}).catch(function (error) {
console.log(error);
});
},
这里面请求api
// src/api/api.js
//获取订单详情
export const getOrderDetail = orderId => {
return axios.get(`${local_host}/orderinfo/` + orderId + '/')
};
请求成功后,在页面中显示
<div class="userCenterBox boxCenterList clearfix" style="_height:1%;">
<h5><span>订单状态span>h5>
<div class="blank">div>
<table width="100%" border="0" cellpadding="5" cellspacing="1" bgcolor="#dddddd">
<tbody>
<tr>
<td width="15%" align="right" bgcolor="#ffffff">订单号:td>
<td align="left" bgcolor="#ffffff">{{orderInfo.order_sn}}
td>
tr>
<tr>
<td align="right" bgcolor="#ffffff">订单状态:td>
<td v-if="orderInfo.pay_status == 'topaid' " align="left" bgcolor="#ffffff">待支付 <div style="text-align:center"><a :href="orderInfo.alipay_url"><input type="button" onclick="" value="立即使用支付宝支付">a>div>
td>
<td v-if="orderInfo.pay_status == 'success' " align="left" bgcolor="#ffffff">已支付td>
tr>
tbody>
table>
<table>table>
<div class="blank">div>
<h5>
<span>商品列表span>
h5>
<div class="blank">div>
<table width="100%" border="0" cellpadding="5" cellspacing="1" bgcolor="#dddddd">
<tbody>
<tr>
<th width="30%" align="center" bgcolor="#ffffff">商品名称th>
<th width="19%" align="center" bgcolor="#ffffff">商品价格th>
<th width="9%" align="center" bgcolor="#ffffff">购买数量th>
<th width="20%" align="center" bgcolor="#ffffff">小计th>
tr>
<tr v-for="item in orderInfo.order_goods">
<td bgcolor="#ffffff">
<router-link :to="'/app/home/productDetail/'+item.id" class="f6">{{item.goods.name}}router-link>
td>
<td align="center" bgcolor="#ffffff">¥{{item.goods.shop_price}}元td>
<td align="center" bgcolor="#ffffff">{{item.goods_nums}}td>
<td align="center" bgcolor="#ffffff">¥{{item.goods.shop_price*item.goods_nums}}元td>
tr>
<tr>
<td colspan="8" bgcolor="#ffffff" align="right">
商品总价: ¥{{totalPrice}}元
td>
tr>
tbody>
table>
<div class="blank">div>
<div class="blank">div>
<h5><span>收货人信息span>h5>
<div class="blank">div>
<form name="formAddress" id="formAddress">
<table width="100%" border="0" cellpadding="5" cellspacing="1" bgcolor="#dddddd">
<tbody>
<tr>
<td width="15%" align="right" bgcolor="#ffffff">收货人姓名:td>
<td width="35%" align="left" bgcolor="#ffffff"><input name="consignee" type="text" class="inputBg" v-model="orderInfo.signer_name" size="25">
td>
<td width="15%" align="right" bgcolor="#ffffff">收货地址:td>
<td width="35%" align="left" bgcolor="#ffffff"><input name="email" type="text" class="inputBg" v-model="orderInfo.address" size="25">
td>
tr>
<tr>
<td align="right" bgcolor="#ffffff">电话:td>
<td align="left" bgcolor="#ffffff"><input name="address" type="text" class="inputBg" v-model="orderInfo.signer_mobile" size="25">td>
tr>
tbody>
table>
form>
<div class="blank">div>
div>
也就是上图中的效果。