DRF实操学习——购物车及订单生成
目标:实现购物车功能及订单生成功能
分析:
技术点:事务控制
模型:
购物车表
订单表
订单商品表
1.基础配置
1. 创建模型
在shopping的models文件下创建模型
# 购物车表
class ShoppingCart(DateTimeModelMixin):
number = models.IntegerField(default=1,verbose_name='数量')
community = models.ForeignKey(Commodity,on_delete=models.CASCADE,verbose_name='商品')
user = models.ForeignKey(User,on_delete=models.CASCADE,verbose_name='用户')
class Meta:
ordering=['-update_time']
db_table = 'shopping_cart'
verbose_name = '购物车'
verbose_name_plural = verbose_name
# 订单表
class Order(DateTimeModelMixin):
PAY_METHOD_CHOICES = (
(1,'支付宝'),
(2,'货到付款'),
)
STATUS_CHOICES = (
(1,'待支付'),
(2,'待发货'),
(3,'待收货'),
(4,'待评价'),
(5,'已完成'),
(6,'已取消'),
(7,'申请退款'),
(8,'退款成功'),
)
# 将订单号定义为主键字段
order_id = models.CharField(max_length=64,primary_key=True,verbose_name='订单号')
total_count = models.IntegerField(default=1,verbose_name='商品数')
total_amount = models.DecimalField(max_digits=10,decimal_places=2,verbose_name='商品总金额')
pay_method = models.IntegerField(verbose_name='支付方式',default=1,choices=PAY_METHOD_CHOICES)
status = models.IntegerField(verbose_name='订单状态',default=1,choices=STATUS_CHOICES)
address = models.TextField(verbose_name='收货地址')
user = models.ForeignKey(User,on_delete=models.PROTECT,verbose_name='用户')
class Meta:
ordering=['-create_time']
db_table = 'order'
verbose_name = '订单表'
verbose_name_plural = verbose_name
# 订单商品表
class OrderGoods(DateTimeModelMixin):
SCORE_CHOICES = (
(0,'零星'),
(1,'一星'),
(2,'二星'),
(3,'三星'),
(4,'四星'),
(5,'五星'),
)
number = models.IntegerField(default=1,verbose_name='商品数')
price = models.DecimalField(max_digits=10,decimal_places=2,verbose_name='单价')
comment = models.TextField(verbose_name='评价',null=True,blank=True)
score = models.IntegerField(null=True,blank=True,verbose_name='评分',choices=SCORE_CHOICES)
is_anonymous = models.BooleanField(default=False,verbose_name='匿名评价')
order = models.ForeignKey(Order,on_delete=models.CASCADE,verbose_name='订单')
commodity = models.ForeignKey(Commodity,on_delete=models.SET_NULL,null=True,blank=True,verbose_name='商品')
class Meta:
db_table = 'order_goods'
verbose_name = '订单商品'
verbose_name_plural = verbose_name
2. 创建序列化器
class ShoppingCartSerializer(ModelSerializer):
# 调用get_commodityDetail
commodityDetail = SerializerMethodField()
class Meta:
model = ShoppingCart
fields = '__all__'
# 使用对象级别的额外校验:校验加入购物车的商品数量不能大于商品库存数量
def validate(self,data):
if data['number'] <=0:
raise serializers.ValidationError('要购买的商品数量错误')
elif data['number'] > data['commodity'].stock:
raise serializers.ValidationError('商品数量超出库存')
else:
return data
def get_commodityDetail(self,shoppingcart):
return CommodityImgSerializer(shoppingcart.community).data
class OrderCartSerializer(ModelSerializer):
class Meta:
model = Order
fields = '__all__'
3. 创建视图
class ShoppingCartViewSet(ModelViewSet):
serializer_class = ShoppingCartSerializer
permission_classes = [IsAuthenticated]
# 返回当前登录用户的购物车数据
def get_queryset(self):
# 使用管理器:源模型类小写_set
return self.request.user.shoppingcart_set.all()
@auto_user #自动将当前登录的用户作为创建者
def create(self, request, *args, **kwargs):
return ModelViewSet.create(self, request, *args, **kwargs)
@update_auto_user
def update(self, request, *args, **kwargs):
return ModelViewSet.create(self, request, *args, **kwargs)
@destory_auto_user
def destroy(self, request, *args, **kwargs):
return ModelViewSet.create(self, request, *args, **kwargs)
class OrderCartViewSet(ReadOnlyModelViewSet,CreateModelMixin):
serializer_class = OrderCartSerializer
# 返回当前登录用户的数据
def get_queryset(self):
# 使用管理器:源模型类小写_set
return self.request.user.order_set.all()
4. 创建路由
from django.urls import path
from rest_framework.routers import DefaultRouter
from rest_framework_jwt.views import obtain_jwt_token
from .views import *
urlpatterns = []
router = DefaultRouter()
router.register('classifications', ClassificationViewSet)
router.register('commoditys', CommodityViewSet)
router.register('commodity_img', CommodityImgViewSet)
router.register('cart', ShoppingCartViewSet,basename='shoppingcart')
router.register('order', OrderCartViewSet,basename='order')
urlpatterns += router.urls
2. 接口的编写
知识补充:事务
使用场景,保证代码在一个事务中执行,比如购买商品时进行结算,代码执行到一半时突然断电或宕机,所有的事务都要撤销,不能让金额或购物信息出现错误,保证数据的一致性。
1. 在事务开始时创建保存点
2. 在事务出现问题时回滚到保存点
3. 执行成功,提交从保存点到当前的所有事务操作
使用方法
1. 装饰器用法
##### 2. with语句用法
1. 重写订单创建接口:事务的实操
在序列化器重写创建,完成自定义的创建功能
class OrderCartSerializer(ModelSerializer):
# 得到前端传入过来要结算的购物车商品id列表
cart = serializers.ListField(write_only=True)
class Meta:
model = Order
fields = '__all__'
# 定义只读字段,不允许前端传入,前端传入有直接通过页面检查修改的风险
read_only_fields = ['order_id','total_count','total_amount','status','user']
#在序列化器重写创建,完成自定义的创建功能
# validated_data保存前端传入的所有数据
def create(self,validated_data):
"""
自定义创建订单功能
"""
#生成订单编号:唯一性,标识性。 日期+时间+六位数的用户id+两位随机数 22位
#在视图集中self.request参数保存请求对象,在序列化器中是self.context['request']
user = self.context['request'].user
# '%06d%02d'格式化 d表示整数,6表示6位,不够补0
order_id = datetime.now().strftime('%Y%m%d%H%M%S') + '%06d%02d'%(user.id,randint(1,99))
# 获取前端传入的支付方式
pay_method = validated_data.get('pay_method')
# 如果没有传入id,只传入了地址,说明传入的是假地址
try:
address = Address.objects.get(id=validated_data.get('address'))
except Address.DoesNotExist:
raise serializers.ValidationError('address error')
# 格式化收货地址信息
adress_s = f'{address.province.name}{address.city.name}{address.district.name}{address.place}[{address.receive} 收]{address.mobile}'
with transaction.atomic():
#开启事务,在with缩进里面的代码执行完毕之后自动关闭事务,with上下文协议
# 里面的代码都是在一个原子,事务中
save_point = transaction.savepoint()#创建保存点
try:
#创建订单信息
order = Order.objects.create(
order_id = order_id,
total_count=0,
total_amount=Decimal('0.00'),
pay_method = pay_method,
# 支付宝支付,状态待支付。货到付款,状态为代发货 三目运算
status = 1 if pay_method == 1 else 2 ,
user =user,
address =adress_s,
)
#获取购物车中要结算的商品
carts = validated_data.get('cart') #要结算的商品id列表
for cart_id in carts:
try:
cart = ShoppingCart.objects.get(id=cart_id)
except:
transaction.savepoint_rollback(save_point) #回滚到指定保存点
raise serializers.ValidationError('购物信息不存在')
# 得到购物车中的商品信息,库存数量,销量
commodity = cart.commodity #关联查询
# 原本的库存与销量
origin_stock = commodity.stock
origin_sales = commodity.sales
#核算 购买的数量是否超过库存
if cart.number>origin_stock:
raise serializers.ValidationError('库存不足')
# 库存操作:减少库存,销量增加
new_stock = origin_stock-cart.number
new_sales = origin_sales+cart.number
# 更新商品信息:模型对象是修改属性调用save方法保存,查询集才是用update方法做修改
# 所以这里不用get,用filter
Commodity.objects.filter(id=commodity.id).update(stock=new_stock,sales=new_sales)
# 创建订单对应的商品数据
OrderGoods.objects.create(
number=cart.number,
price = commodity.price,
order=order,
commodity=commodity
)
# 更新订单表
order.total_count += cart.number
order.total_amount +=(cart.number * commodity.price)
#保存订单信息
order.save()
#清除购物车
# id__in=carts 查询出id在carts中的数据
ShoppingCart.objects.filter(id__in=carts).delete()
except Exception as e:
transaction.savepoint_rollback(save_point) #回滚到指定保存点
raise serializers.ValidationError(e) #结束方法 响应给客户端错误信息
else:
transaction.savepoint_commit(save_point) #提交保存点的所有信息
return order
知识补充:悲观锁和乐观锁
并发问题:资源抢夺问题
问题的描述:A商品库存为6,张三加入购物车A商品为6,李四加入购物车A商品为6,两人同时结算支付,因为购买的数量小于等于库存数量,所以都可以支付成功
解决办法: 加锁、队列
1. 悲观锁
悲观锁是mysql级别的锁,需要配合事务进行使用。
BEGIN:
SELECT ‘stock’ FROM ‘commodity’ where ‘id’=1 for UPDATE;
UPDATE ‘commodity’ SET ‘stock’=0 WHERE ‘id’=1;
django实现这个sql语句:
Commodity.objects.select_for_update().get(id=1)
不推荐使用悲观锁,容易死锁。
2. 乐观锁
.乐观锁为逻辑锁,并不是真实存在的锁
UPDATE ‘commodity’ SET ‘stock’=0 WHERE ‘id’=1 AND ‘stock’=6;
django实现这个sql语句:
Commodity.objects.filter(id=1,stock=6).update(stock=0)
使用乐观锁,修改代码如下:
class OrderCartSerializer(ModelSerializer):
# 得到前端传入过来要结算的购物车商品id列表
cart = serializers.ListField(write_only=True)
class Meta:
model = Order
fields = '__all__'
# 定义只读字段,不允许前端传入,前端传入有直接通过页面检查修改的风险
read_only_fields = ['order_id','total_count','total_amount','status','user']
#在序列化器重写创建,完成自定义的创建功能
# validated_data保存前端传入的所有数据
def create(self,validated_data):
"""
自定义创建订单功能
"""
#生成订单编号:唯一性,标识性。 日期+时间+六位数的用户id+两位随机数 22位
#在视图集中self.request参数保存请求对象,在序列化器中是self.context['request']
user = self.context['request'].user
# '%06d%02d'格式化 d表示整数,6表示6位,不够补0
order_id = datetime.now().strftime('%Y%m%d%H%M%S') + '%06d%02d'%(user.id,randint(1,99))
# 获取前端传入的支付方式
pay_method = validated_data.get('pay_method')
# 如果没有传入id,只传入了地址,说明传入的是假地址
try:
address = Address.objects.get(id=validated_data.get('address'))
except Address.DoesNotExist:
raise serializers.ValidationError('address error')
# 格式化收货地址信息
adress_s = f'{address.province.name}{address.city.name}{address.district.name}{address.place}[{address.receive} 收]{address.mobile}'
with transaction.atomic():
#开启事务,在with缩进里面的代码执行完毕之后自动关闭事务,with上下文协议
# 里面的代码都是在一个原子,事务中
save_point = transaction.savepoint()#创建保存点
try:
#创建订单信息
order = Order.objects.create(
order_id = order_id,
total_count=0,
total_amount=Decimal('0.00'),
pay_method = pay_method,
# 支付宝支付,状态待支付。货到付款,状态为代发货 三目运算
status = 1 if pay_method == 1 else 2 ,
user =user,
address =adress_s,
)
#获取购物车中要结算的商品
carts = validated_data.get('cart') #要结算的商品id列表
for cart_id in carts:
while True:
try:
cart = ShoppingCart.objects.get(id=cart_id)
except:
transaction.savepoint_rollback(save_point) #回滚到指定保存点
raise serializers.ValidationError('购物信息不存在')
# 得到购物车中的商品信息,库存数量,销量
commodity = cart.commodity #关联查询
# 原本的库存与销量
origin_stock = commodity.stock
origin_sales = commodity.sales
#核算 购买的数量是否超过库存
if cart.number>origin_stock:
raise serializers.ValidationError('库存不足')
# 库存操作:减少库存,销量增加
new_stock = origin_stock-cart.number
new_sales = origin_sales+cart.number
# 更新商品信息:模型对象是修改属性调用save方法保存,查询集才是用update方法做修改
# 所以这里不用get,用filter
res = Commodity.objects.filter(id=commodity.id, stock=origin_stock).update(stock=new_stock,sales=new_sales)
# 增加乐观锁,在循环开始用while True:
if not res:
continue
# 创建订单对应的商品数据
OrderGoods.objects.create(
number=cart.number,
price = commodity.price,
order=order,
commodity=commodity
)
# 更新订单表
order.total_count += cart.number
order.total_amount +=(cart.number * commodity.price)
break
#保存订单信息
order.save()
#清除购物车
# id__in=carts 查询出id在carts中的数据
ShoppingCart.objects.filter(id__in=carts).delete()
except Exception as e:
transaction.savepoint_rollback(save_point) #回滚到指定保存点
raise serializers.ValidationError(e) #结束方法 响应给客户端错误信息
else:
transaction.savepoint_commit(save_point) #提交保存点的所有信息
return order