一、整体流程图
-
结构图
image
二、购物车需求
1.1、购物车中的物品可以修改数量
1.2、购物车中的物品可以选择支付【并不对全部物品支付】
1.3、需支付的物品总价,需要显示,并在用户修改商品时,实时修改!
1、购物车添加
- 说明
需要提供商品主键,以及选择的价格,并且要在后端验证该价格策略是否合法, - 步骤
- 判断该用户原来是否存在购物车,创建一条新的记录
- 示例代码
@login_required def add(request): try: # 查询主表 就直接能获取子表的数据 # 查询子表 也可以直接子表.外键 uid = request.user.userprofile.uid num = int(request.GET.get('num')) shop_id = int(request.GET.get('shop_id')) # 两个操作 # 创建操作 如果商品不存在购物车 # 更新的操作 商品已经存在 数量+ car = ShopCar.objects.filter(user=request.user.userprofile, shop_id=shop_id) if car: # 更新 数字 + 数字 运算 car = car.first() car.number = F('number') + num car.save() else: car = ShopCar(user_id=uid, shop_id=shop_id, number=num) car.save() request.session['count'] += 1 return HttpResponse('success') except Exception as e: return HttpResponse('error')
2、修改商品数量
- 说明
修改商品与添加商品类似,需要提供商品ID与价格策略 - 步骤
- 该用户的购物车是否存在
- 购物车中是否存在选中商品
- 以及传递过来的价格策略是否合法
3、删除商品
通过商品的id删除购物车记录
4、购物车接口
- 购物车list列表
- 购物车添加商品
- 更新购物车某一个产品数量
- 移除购物车某一个产品
- 购物车的选中与取消选中
- 查询购物车的数量
- 购物车的全选与反选
二、订单
1、实现步骤
- 生成订单编号
- 保存订单基本信息数据 Order
- 从redis中获取购物车结算商品数据(如果使用了redis)
- 遍历结算商品:判断商品库存是否充足
- 减少商品库存,增加商品销量
- 保存订单商品数据
- 在redis购物车中删除已计算商品数据(如果使用了redis)
2、数据库事务
1、说明
在保存订单数据中,涉及到多张表(OrderInfo、OrderGoods、SKU)的数据修改,对这些数据的修改应该是一个整体事务,即要么一起成功,要么一起失败。
Django中对于数据库的事务,默认每执行一句数据库操作,便会自动提交。我们需要在保存订单中自己控制数据库事务的执行流程。
在Django中可以通过
django.db.transaction
模块提供的atomic
来定义一个事务,atomic
提供两种用法
2、装饰器用法:可以装饰在视图函数上
@transaction.atomic
def viewfunc(request):
# 这些代码会在一个事务中执行
# 注意不要处理异常
3、with语句用法
from django.db import transaction
def viewfunc(request):
# 这部分代码不在事务中,会被Django自动提交
with transaction.atomic():
# 这部分代码会在事务中执行
3、并发处理
1、说明
在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。
2、示例图
image
3、解决办法
3.1、 悲观锁
- 说明
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作 - 示例代码
select stock from shop where id=1 for update; Shop.objects.select_for_update().get(id=1)
- 备注
悲观锁类似于我们在多线程资源竞争时添加的互斥锁,容易出现死锁现象,采用不多
3.2、乐观锁
- 说明
乐观锁并不是真实存在的锁,而是在更新的时候判断此时的库存是否是之前查询出的库存,如果相同,表示没人修改,可以更新库存,否则表示别人抢过资源,不再执行库存更新。类似如下操作 - 示例代码
update shop set stock=2 where id=1 and stock=7; Shop.objects.filter(id=1, stock=7).update(stock=2)
3.3、任务队列
- 说明
例如秒杀功能,将下单的逻辑放到任务队列中(如celery),将并行转为串行,所有人排队下单。比如开启只有一个进程的Celery,一个订单一个订单的处理。(不推荐)
4、使用乐观锁改写下单逻辑
- 示例代码
def create(self, validated_data): """创建订单记录:保存OrderInfo和OrderGoods信息""" # 获取当前保存订单时需要的信息 # 获取当前的登录用户 user = self.context[‘request‘].user # 生成订单编号 order_id = timezone.now().strftime(‘%Y%m%d%H%M%S‘) + (‘%09d‘ % user.id) # 获取地址和支付方式 address = validated_data.get(‘address‘) pay_method = validated_data.get(‘pay_method‘) # 开启事务 with transaction.atomic(): # 在安全的地方,创建保存点,将来操作数据库失败回滚到此 save_id = transaction.savepoint() try: # 保存订单基本信息 OrderInfo order = OrderInfo.objects.create( order_id=order_id, user = user, address = address, total_count = 0, total_amount = 0, freight = Decimal(‘10.00‘), pay_method = pay_method, # 如果用户传入的是"支付宝支付",那么下了订单后,订单的状态要是"待支付" # 如果用户传入的是"货到付款",那么下了订单后,订单的状态要是"待发货" status = OrderInfo.ORDER_STATUS_ENUM[‘UNPAID‘] if pay_method==OrderInfo.PAY_METHODS_ENUM[‘ALIPAY‘] else OrderInfo.ORDER_STATUS_ENUM[‘UNSEND‘] ) # 从redis读取购物车中被勾选的商品信息 redis_conn = get_redis_connection(‘carts‘) # 读取出所有的购物车数据 # redis_cart = {b‘1‘:b‘10‘, b‘2‘:b‘20‘, b‘3‘:b‘30‘} redis_cart = redis_conn.hgetall(‘cart_%s‘ % user.id) # cart_selected = [b‘1‘, b‘2‘] cart_selected = redis_conn.smembers(‘selected_%s‘ % user.id) # 定义将来要支付的商品信息的字典 # carts = {1:10, 2:20} carts = {} for sku_id in cart_selected: carts[int(sku_id)] = int(redis_cart[sku_id]) # 读取出所有要支付的商品的sku_id # sku_ids = [1,2] sku_ids = carts.keys() # 遍历购物车中被勾选的商品信息 for sku_id in sku_ids: # 死循环的下单:当库存满足,你在下单时,库存没有同时的被别人的更改,下单成功 # 如果下单库存被更改,但是你的sku_count依然在被更改后的库存范围内,继续下单 # 直到库存真的不满足条件时才下单失败 while True: # 获取sku对象 sku = SKU.objects.get(id=sku_id) # 获取原始的库存和销量 origin_stock = sku.stock origin_sales = sku.sales sku_count = carts[sku_id] # 判断库存? if sku_count > origin_stock: # 回滚 transaction.savepoint_rollback(save_id) raise serializers.ValidationError(‘库存不足‘) # 读取要更新的库存和销量 new_stock = origin_stock - sku_count new_sales = origin_sales + sku_count # 使用乐观锁更新库存:在调用update()去更新库存之前,使用filter()拿着原始的库存去查询记录是否存在 # 如果记录不存在的,在调用update()时返回0 result = SKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales) if 0 == result: # 死循环的下单:当库存满足,你在下单时,库存没有同时的被别人的更改,下单成功 # 如果下单库存被更改看,但是你的sku_count依然在被更改后的库存范围内,继续下单 # 直到库存真的不满足条件时才下单失败 continue # 修改SPU销量 sku.goods.sales += sku_count sku.goods.save() # 保存订单商品信息 OrderGoods OrderGoods.objects.create( order=order, sku = sku, count = sku_count, price = sku.price, ) # 累加计算总数量和总价 order.total_count += sku_count order.total_amount += (sku_count * sku.price) # 下单成功要跳出死循环 break # 最后加入邮费和保存订单信息 order.total_amount += order.freight order.save() except Exception: transaction.savepoint_rollback(save_id) raise # 自动的将捕获的异常抛出,不需要给异常起别名 # 没有问题,需要明显的提交 transaction.savepoint_commit(save_id) # 清除购物车中已结算的商品 pl = redis_conn.pipeline() pl.hdel(‘cart_%s‘ % user.id, *sku_ids) pl.srem(‘selected_%s‘ % user.id, *sku_ids) pl.execute() # 响应结果 return order