Django设计的订单相关的表如下所示:
由于每一个订单中的商品种类与数量都不定,因此单独将订单商品提出为一个表,为一对多的关系。
订单的提交
从购物车页面提交是通过form形式提交的,在checkbox元素中定义参数value并设为对应的商品id,则传递到后端的为一个由选中商品id组成的列表,在后端中的业务流程为:
①获取参数并校验,表单中的checkbox只有被选中时其value才会被提交,若不选中则不提交,若有多个name相同,则使用POST.getlist获取到一个由多个value(checkbox的value)组成的列表;
②从redis中获取该用户购物车的信息,从mysql中获取商品id对应的信息,计算数量与总价格,并将其动态添加到查询到的sku模型类实例中;
③处理运费与实付款;
④组织参数,渲染订单创建的页面。
订单的提交并不复杂,其中重点在于如何将选中的商品id传入后端,以及传入订单创建页面的参数选择。
订单的创建
订单创建的前端页面
共分为三部分:
①收货地址的选择,此处将默认收货地址默认选中,可以在该用户已有的收货地址中选择,也可以跳转到收货地址的编辑页面;
②支付方式的选择,因为此处只做了支付宝的接口因此其他支付方式暂时无法支付,但是可以提交;
③将要提交的商品按条目显示(在订单提交后端中传入),并显示其数量、单价、小计、单位等信息,显示总价、运费、总数量信息;
注:①在订单创建页面不使用form进行post提交,使用ajax post提交,订单创建成功时跳转到订单页面,创建失败时不跳转,显示后端传递的具体信息;
②由于订单中的信息是从购物车中查询到的,这个信息可能与真实情况不符(如商品下架,库存卖完等),因此在订单创建时必须要进行多次校验之后才可以确定订单是否可以创建成功。
订单创建的参数传递
从购物车提交至订单创建页面再到订单创建后端,其传递选中的商品都是通过传递sku_id然后通过查询redis中的用户购物车信息来进行的(并不直接传递具体的商品信息),但由于订单创建功能不与购物车页面交互,因此将被选中商品的sku_ids重构为一个字符串,直接从购物车传递到订单创建页面,并在订单创建页面中动态添加一个属性为sku_ids(此方法常用于在页面中获取变量)并使用ajax post请求发回后端,在订单创建流程中使用。
订单创建的注意事项
(1)关于订单提交失败:由于可能因为各种原因导致的订单无法完成,但订单记录会在数据库中生成,因此使用mysql事务来对订单提交(即把在两个表中的创建打包为一组事务,不允许出现空订单的情况);
(2)关于并发订单的处理:
①悲观锁:在某一条ORM查询语句上加锁(objects.select_for_update()),则多个用户同时进行订单提交时,哪个线程先执行到该语句则获取锁,待事务结束后才会释放,其他线程在执行至此时阻塞等待获取锁,保证了同一时刻只有一个事务在运行;
②乐观锁:查询时不加锁,在变更时对比原数据与当前重新查询的数据,若不一致则失败(即乐观的认为当前没有其他线程在同时进行此过程),一般采用3次循环重复此过程,但由于数据库事务的隔离性,查询到的原数据可能不会更新,因此需要修正数据库事务隔离的级别为read committed(在Django2.0版本中已经自动将所有数据库的事务隔离级别修改为read-committed,因此无需专门修改mysql数据库隔离级别),此时乐观锁可执行;
③使用方式:在冲突较少时使用乐观锁(省去加锁、释放锁的开销,提高性能),在冲突较多时使用悲观锁(省去大量无用的循环),且若乐观锁重复操作的代价比较大也选用悲观锁。
注:一般将整个事务的过程都放置于try中。
订单创建的后端流程
①校验参数;
②添加事务,在事务中进行乐观/悲观锁设置,创建订单模型类实例,通过传入的sku_ids在redis购物车中进行查询,查询到sku商品实例后操作库存值并添加订单商品实例;
③重复②中对sku_ids的操作,对所有选中的商品都进行此操作,其中在操作失效、校验失败时需要进行回滚,操作成功后更新订单模型类实例的内容并提交;
④清空用户购物车中已提交的记录,提交事务,并返回应答,当创建成功时跳转到个人订单页面,创建失败时提示错误信息。
class OrderCommitView(View): '''订单提交创建''' @transaction.atomic def post(self, request): # 判断用户是否登录 user = request.user if not user.is_authenticated: return JsonResponse({'res':0, 'errmsg':'请先登录'}) # 获取参数 addr_id = request.POST.get('addr_id') pay_method = int(request.POST.get('pay_method')) sku_ids = request.POST.get('sku_ids') # 校验参数 if not all([addr_id, pay_method, sku_ids]): return JsonResponse({'res':1, 'errmsg':'数据不完整'}) # 校验支付方式 if pay_method not in OrderInfo.PAY_METHOD.keys(): print(pay_method, type(pay_method)) return JsonResponse({'res':2, 'errmsg':'非法的支付方式'}) # 校验地址 try: addr = Address.objects.get(id=addr_id) except Address.DoesNotExist: return JsonResponse({'res':3, 'errmsg':'地址不存在'}) # 创建订单核心业务 # 创建订单信息缺少的内容 # 订单ID,使用年月日时分秒+用户ID创建订单编号 order_id = datetime.now().strftime('%Y%m%d%H%M%S')+str(user.id) # 运费 transit_price = 10 # 总数目和总金额,添加记录,先使用默认值,后续修改 total_count = 0 total_price = 0 # 添加数据库事务的保存点 save_id = transaction.savepoint() try: # 向订单信息表中添加记录 order = OrderInfo.objects.create(order_id=order_id, user=user, addr=addr, pay_method=pay_method, transit_price=transit_price, total_price=total_price, total_count=total_count) if order.pay_method == 1: order.order_status = 2 # 获取订单商品表的参数 conn = get_redis_connection('default') cart_key = 'cart_{}'.format(user.id) sku_ids = sku_ids.split(',') for sku_id in sku_ids: for i in range(3): try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 回滚到保存点,此处的回滚是为了撤销已创建的表 transaction.savepoint_rollback(save_id) return JsonResponse({'res':4, 'errmsg':'商品不存在'}) # 获取商品的数量 count = conn.hget(cart_key, sku_id) # 校验库存值 if int(count)>sku.stock: # 回滚到保存点 transaction.savepoint_rollback(save_id) return JsonResponse({'res':5, 'errmsg':'商品库存不足'}) # 保存原库存与新库存 origin_stock = sku.stock new_stock = origin_stock - int(count) new_sales = sku.sales + int(count) print('user:{} times:{} stock:{}'.format(user.id, i, sku.stock)) # 返回受影响的行数,0/1 res = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales) if res == 0: if i == 2: transaction.savepoint_rollback(save_id) return JsonResponse({'res':7, 'errmsg':'订单创建失败'}) continue # 向订单商品表中添加记录,由于此处并没有设置保存点,因此将判断放在添加记录的前面,防止重复添加 OrderGoods.objects.create(order=order, sku=sku, count=count, price=sku.price) # 更新相关商品的销量和库存 sku.stock -= int(count) sku.sales += int(count) sku.save() # 计算订单商品的总数量和总价格 amount = sku.price*int(count) total_count += int(count) total_price += amount break # 更新订单详情表中的总数量和总价格 order.total_count = total_count order.total_price = total_price order.save() # 清除用户购物车中的记录 conn.hdel(cart_key, *sku_ids) except Exception: transaction.savepoint_rollback(save_id) return JsonResponse({'res':7, 'errmsg':'订单创建失败'}) # 提交事务,返回应答 transaction.savepoint_commit(save_id) return JsonResponse({'res':6, 'message':'订单创建成功'})
订单的显示
订单显示在个人中心中,根据用户从订单模型类中取出所有的订单实例,再根据每个订单实例从订单商品类中取出对应的商品(此处不从sku商品表中取,因为订单提交时的价格与当前价格可能不同),计算小计并动态添加属性,然后进行分页,对分页对象进行处理。
注:①操作与商品列表分页类似,对于订单的排序此处略过,默认按照创建时间进行排序;
②在前端中,根据用户订单的支付状态和支付方式确定提供给用户的按钮文字,并根据支付状态来判断点击时进行的逻辑(去支付/去评论)。
订单的支付
此处调用支付宝的测试接口进行支付,关于支付宝接口的调用详情可参支付宝沙箱环境开发文档.
订单支付结果的查询
如上图所示,支付宝在支付结果产生后会向网站返回支付结果,但可能由于网络原因并不准确,因此主动去调用支付宝接口查询支付状态,在引导用户去支付页面之后就发起ajax post请求用于获取支付结果,循环调用支付宝接口进行查询(此接口的返回值中包括等待用户付款),若支付成功则更改订单支付状态并返回,若支付失败则返回错误信息。
注:订单的支付是由用户与支付宝交互完成的,网站服务器不参与,网站服务器只负责提供支付链接与查询支付结果,通过支付宝的返回状态来对当前用户订单状态进行更改。
商品评论
①评论页面显示:当订单支付成功,直接跳转到商品评论页面,商品评论页面中单个订单的多个商品都有自己的评论框,使用form表单的形式进行提交。
注:由于有多个商品,而显示的时候是循环显示,因此使用forloop.counter给商品和评论框起名,这样可以对它们进行绑定,避免混淆。
②评论内容提交:如上所述,在后端中获取提交的评论内容并将其添加到订单商品类中,更改订单状态,订单完成,重定向到订单页面。
注:由于一个订单中的商品可能有的评价有的不评价,会造成订单状态的不确定,因此可以设置若不评价则给出默认值或评论框不允许为空,即只要点击进入评论页面,无论如何处理只要逻辑完成则更改订单状态为已完成。
神龙|纯净稳定代理IP免费测试>>>>>>>>天启|企业级代理IP免费测试>>>>>>>>IPIPGO|全球住宅代理IP免费测试