提交订单页面展示
购物车页面点击‘去结算’按钮后,跳转至/order/place/页面,显示提交订单的信息。这里就需要将勾选框和提交按钮一起放在一个<form></form>中,提交时,html只会将checked(已勾选)的input的value值提交,因为这里有标记每行的input还有是否全选的input,而我们只需要提交每行商品的input,因此将两种input通过name进行区分,并且由于存在很多行name都为‘goods_ids’的input,此时传给后台的即为一个名为goods_ids的list数组,最终后台通过goods_ids = request.POST.getlist('goods_ids')获取该数组
<form method="post" action="{% url 'order:place' %}"> {% csrf_token %} {% for goods in goods_list %} <ul class="cart_list_td clearfix"> <li class="col01"><input type="checkbox" name="goods_ids" value="{{ goods.id }}" checked></li> <li class="col02"><a href="{% url 'goods:detail' goods.id %}"><img src="{{ goods.image.url }}"></a></li> <li class="col03"><a href="{% url 'goods:detail' goods.id %}">{{ goods.name }}<br><em>{{ goods.price }}元/{{ goods.uom }}</em></a></li> <li class="col04">{{ goods.uom }}</li> <li class="col05">{{ goods.price }}元</li> <li class="col06"> <div class="num_add"> {% csrf_token %} <a href="javascript:;" class="add fl">+</a> <input type="text" goods_id="{{ goods.id }}" class="num_show fl" value="{{ goods.count }}"> <a href="javascript:;" class="minus fl">-</a> </div> </li> <li class="col07">{{ goods.amount }}元</li> <li class="col08"><a href="javascript:;" class="delete">删除</a></li> </ul> {% endfor %} <ul class="settlements"> <li class="col01"><input type="checkbox" name="select_all" checked></li> <li class="col02">全选</li> <li class="col03">合计(不含运费):<span>¥</span><em>{{ total_amount }}</em><br>共计<b>{{ total_count }}</b>件商品</li> <li class="col04"><input type="submit" value='去结算'/></li> </ul> </form>
新增提交订单url和view
urlpatterns = [ ... path('place/', OrderPlaceView.as_view(), name='place'), ... ]
class OrderPlaceView(LoginRequiredMixin, View): '''订单视图类''' template_name = 'order/order.html' def post(self, request): '''显示订单信息''' user = request.user # 获取post数据 goods_ids = request.POST.getlist('goods_ids') # 校验数据 if not goods_ids: return redirect(reverse('cart:cart')) goods_list = [] total_count = 0 total_amount = 0 # redis连接 connect = get_redis_connection('default') cart_key = 'cart_%d'%(user.id) # 获取用户要购买的商品信息 for goods_id in goods_ids: try: goods = Goods.objects.get(id=goods_id) # 获取redis中的数量 try: count = int(connect.hget(cart_key, goods_id)) except Exception as e: return redirect(reverse('cart:cart')) # 计算小计 amount = goods.price * count # 给goods添加属性 goods.count = count goods.amount = amount # 添加至goods列表 goods_list.append(goods) # 汇总数量和小计 total_amount += amount total_count += count except Goods.DoesNotExist: return redirect(reverse('cart:cart')) # 获取地址 address_list = Address.objects.filter(user=user) # 运费 transit_amount = 10 # 实付 total_pay = transit_amount + total_amount # 商品id字符串,以逗号隔开 goods_str = ','.join(goods_ids) # 上下文 context = { 'goods_list': goods_list, 'address_list': address_list, 'total_count': total_count, 'total_amount': total_amount, 'transit_amount': transit_amount, 'total_pay': total_pay, 'goods_str': goods_str, } return render(request, self.template_name, context)
新建提交订单显示的模板文件
{% extends 'base_no_cart.html'%} {% load static %} {% block title %}天天生鲜-提交订单{% endblock title %} {% block infoname %}提交订单{% endblock infoname %} {% block body %} <h3 class="common_title">确认收货地址</h3> <div class="common_list_con clearfix"> <dl> <dt>寄送到:</dt> {% for address in address_list %} <dd><input type="radio" name="address" value="{{ address.id }}" {% if address.is_default %}checked{% endif %}>{{ address.address }} ({{ address.receiver }} 收) {{ address.phone }}</dd> {% endfor %} </dl> <a href="{% url 'user:address' %}" class="edit_site">编辑收货地址</a> </div> <h3 class="common_title">支付方式</h3> <div class="common_list_con clearfix"> <div class="pay_style_con clearfix"> <input type="radio" name="pay_style" value="1" checked> <label class="cash">货到付款</label> <input type="radio" name="pay_style" value="2"> <label class="weixin">微信支付</label> <input type="radio" name="pay_style" value="3"> <label class="zhifubao"></label> <input type="radio" name="pay_style" value="4"> <label class="bank">银行卡支付</label> </div> </div> <h3 class="common_title">商品列表</h3> <div class="common_list_con clearfix"> <ul class="goods_list_th clearfix"> <li class="col01">商品名称</li> <li class="col02">商品单位</li> <li class="col03">商品价格</li> <li class="col04">数量</li> <li class="col05">小计</li> </ul> {% for goods in goods_list %} <ul class="goods_list_td clearfix"> <li class="col01">{{ forloop.counter }}</li> <li class="col02"><img src="{{ goods.image.url }}"></li> <li class="col03">{{ goods.name }}</li> <li class="col04">{{ goods.uom }}</li> <li class="col05">{{ goods.price }}元</li> <li class="col06">{{ goods.count }}</li> <li class="col07">{{ goods.amount }}元</li> </ul> {% endfor %} </div> <h3 class="common_title">总金额结算</h3> <div class="common_list_con clearfix"> <div class="settle_con"> <div class="total_goods_count">共<em>{{ total_count }}</em>件商品,总金额<b>{{ total_amount }}元</b></div> <div class="transit">运费:<b>{{ transit_amount }}元</b></div> <div class="total_pay">实付款:<b>{{ total_pay }}元</b></div> </div> </div> {% csrf_token %} <div class="order_submit clearfix"> <a href="javascript:;" id="order_btn" goods_str={{ goods_str }}>提交订单</a> </div> {% endblock body %} {% block endfiles %} <div class="popup_con"> <div class="popup"> <p>订单提交成功!</p> </div> <div class="mask"></div> </div> <script type="text/javascript" src="{% static 'js/jquery-1.12.4.min.js'%}"></script> <script type="text/javascript"> $('#order_btn').click(function() { //获取传给后台的数据 address_id = $('input[name="address"]:checked').val() pay_method = $('input[name="pay_style"]:checked').val() goods_str = $(this).attr('goods_str') csrf = $('input[name="csrfmiddlewaretoken"]').val() parameter = { 'address_id': address_id, 'pay_method': pay_method, 'goods_str': goods_str, 'csrfmiddlewaretoken': csrf } $.post('/order/create/', parameter, function(data){ //回调函数 if (data.status =='S'){ localStorage.setItem('order_finish',2); $('.popup_con').fadeIn('fast', function() { setTimeout(function(){ $('.popup_con').fadeOut('fast',function(){ window.location.href = '/user/user_center_order/1/'; }); },1000) }); }else{ alert(data.errmsg) } }) }); </script> {% endblock endfiles %}
创建订单
在提交订单页面,点击‘提交订单’按钮,向后台发送ajax请求,调用OrderCreateView
from sequences import get_next_value
class OrderCreateView(View): '''创建订单视图''' @transaction.atomic def post(self, request): context = { 'status': 'E', 'errmsg': '' } user = request.user if not user.is_authenticated: context['errmsg'] = '用户未登录!' return JsonResponse(context) # 接受数据 address_id = request.POST.get('address_id') pay_method = request.POST.get('pay_method') goods_str = request.POST.get('goods_str') # 校验数据 if not all([address_id, pay_method, goods_str]): context['errmsg'] = '数据不完整' return JsonResponse(context) # 地址ID是否正确 try: address_id = int(address_id) address = Address.objects.get(id=address_id) except Exception as e: context['errmsg'] = '地址不存在!' return JsonResponse(context) # 支付方式是否正确 if pay_method not in OrderInfo.PAY_METHOD_DIC: context['errmsg'] = '支付方式不存在!' return JsonResponse(context) pay_method = int(pay_method) # 创建订单 # 订单头信息 # 使用日期+序列创建订单号 order_sequence = get_next_value('order') order_num = datetime.now().strftime('%Y%m%d%H%M%S')+str(order_sequence) total_count = 0 total_amount = 0 transit_amount = 10 # 设置保存点 save_id = transaction.savepoint() try: # 创建订单头记录 order = OrderInfo.objects.create(order_num=order_num, user=user, address=address, pay_method=pay_method, total_count=total_count, total_amount=total_amount, transit_amount=transit_amount) # 连接redis connect = get_redis_connection('default') cart_key = 'cart_%d'%(user.id) # 创建订单行记录 goods_ids = goods_str.split(',') for goods_id in goods_ids: for i in range(1, 4): try: goods = Goods.objects.get(id=goods_id) # 悲观锁 # goods = Goods.objects.select_for_update().get(id=goods_id) except Goods.DoesNotExist: transaction.savepoint_rollback(save_id) context['errmsg'] = '商品不存在!' return JsonResponse(context) # print('username:%s onhand:%d'%(user.username, goods.onhand)) # import time # time.sleep(5) # 获取数量 try: count = int(connect.hget(cart_key, goods_id)) except Exception as e: transaction.savepoint_rollback(save_id) context['errmsg'] = '购物车中不存在提交的商品!' return JsonResponse(context) # 校验是否超库存 old_onhand = goods.onhand if count > old_onhand: transaction.savepoint_rollback(save_id) context['errmsg'] = '库存不足!' return JsonResponse(context) # 计算新库存和新销量 new_onhand = old_onhand - count new_sales = goods.sales + count # 乐观锁,更新goods start affected_rows = Goods.objects.filter(id=goods_id, onhand=old_onhand).update(onhand=new_onhand, sales=new_sales) # 若受影响条数为0,即没有更新goods,则继续尝试 if affected_rows == 0: if i == 3: #第三次尝试还是没更新到数据,则认为失败 transaction.savepoint_rollback(save_id) context['errmsg'] = '下单失败' continue # 乐观锁 end # 创建订单行信息 OrderGoods.objects.create(goods=goods, order=order, count=count, price=goods.price) # 获取小计 amount = goods.price * count # 汇总数量和价格 total_count += count total_amount += amount # 更新商品表的销量和库存 # goods.onhand = new_onhand # goods.sales = new_sales # goods.save() break # 更新订单头总数量和总价格 order.total_amount = total_amount order.total_count = total_count order.save() # 删除购物车 connect.hdel(cart_key, *goods_ids) except Exception as e: transaction.savepoint_rollback(save_id) context['errmsg'] = '创建订单失败!' # 返回应答 transaction.savepoint_commit(save_id) context['status'] = 'S' return JsonResponse(context)
1. 接收数据并校验
2. 对于订单编号,这里使用简单的格式为日期+序列号的方式,不过这个序列号是每次创建就自增1,这样其实会暴露网站的营业数据,所以在实际项目中这种将营业信息暴露的订单编号并不可取。
为了使用序列,这里安装了django-sequences模块(pip install django-sequences),使用 get_next_value('sequence_name')创建并获取下一个序列值
3. 日期对象(datetime)格式化字符串:datetime.strftime(format[, t]),
- %y 两位数的年份表示(00-99)
- %Y 四位数的年份表示(000-9999)
- %m 月份(01-12)
- %d 月内中的一天(0-31)
- %H 24小时制小时数(0-23)
- %I 12小时制小时数(01-12)
- %M 分钟数(00=59)
- %S 秒(00-59)
from datetime import datetime
now = datetime.now()
now.strftime('%Y%m%d%H%M%S')
now.strftime('%Y-%m-%d %H:%M:%S')
#结果
'20200515102529'
'2020-05-15 10:25:29'
4. mysql事务
在创建订单的过程中,需要插入订单头信息,订单行商品信息,还需要修改商品表库存等信息,这些操作,要么全部都成功,如果其中某一步失败了,那么其他操作也需要回滚至原始数据,这就是事务的一致性,创建的事务最后遇到commit或者rollback语句才会结束事务,在django中创建事务的方法为:
- 导入transaction模块:from django.db import transaction
- 给外层方法添加装饰器:@transaction.atomic
class OrderCreateView(View): '''创建订单视图''' @transaction.atomic def post(self, request): ....
创建了事务后,在第一步增删改语句前,创建一个保存点:save_id = transaction.savepoint(),在后续增删改操作的异常处理中,加入 transaction.savepoint_rollback(save_id) ,将数据回滚到保存点。
5. 添加锁解决订单并发问题
在这个创建订单的事务中,会先从商品表df_goods中查询出剩余库存,然后验证购买的数量是否超过了库存数量,若未超过则,创建订单,并更新商品表的库存(减一)。当存在多个用户同时购买一件商品时,这时会产生多个进程或者多个线程,由于最终CPU去处理多进程或者多线程时,其实采用的是时间片轮转方式,轮流处理多个进程或线程,所以实际上CPU在具体时间点上其实还是只能处理一个进程或线程。这时就可能发现这种情况:A、B两个用户同时购买同一件商品,购买数量都为1,购买前商品的库存为1,功能上设计只能一个用户能够购买成功。但是两个用户同时点击购买,这时开了A、B两个进程,CPU先处理A进程,处理到验证购买数量是否超出库存量时,发现验证通过,此时停止A进程的执行,然后去处理B进程,同样验证到数量校验成功后,又转去执行A进程的后续代码,成功购买运行完毕后库存变为0,然后去执行B进程,也能运行完毕后库存变为-1。这样就产生了并发问题。解决这个问题的方式就是通过锁。
5.1 悲观锁
在通过商品ID查询商品表时,使用select * from df_goods where id=p_goods_id for update;这样就实现了。如果是A用户先运行这句话,拿到了锁,则若CPU再调度B进程,当B进程运行到这句话时,就拿不到这个锁,导致B进程一直处于等待状态,等A进程释放掉锁后,其他进程运行这句话时才能拿到锁。这样就保证了同一时间只能一个进程能创建订单。django自带的ORM实现select ... for update的方式是:
# 悲观锁 goods = Goods.objects.select_for_update().get(id=goods_id)
5.2 乐观锁
不对语句加for update锁,而是在查询商品表时,将这次查到的库存保存下来。然后在后面进行update更新商品表的库存信息时,限制条件除了id=goods_id外,再加上库存限制条件onhand=old_onhand。
# 乐观锁,更新goods start affected_rows = Goods.objects.filter(id=goods_id, onhand=old_onhand).update(onhand=new_onhand, sales=new_sales)
通过这样来判断最后更新时库存是否和之前查询到的库存一致,如果不一致,就说明在查询和更新这段时间内,有其他用户更新过了这条信息,那么update语句的影响数据条数就为0,此时就需要重新回到获取商品信息的那一步代码,重新获取新的库存,并重新更新,一般循环尝试3次,若三次尝试都失败了,则回滚transaction.savepoint_rollback(save_id),认为这次购买失败。
for i in range(1, 4): try: goods = Goods.objects.get(id=goods_id) # 悲观锁 # goods = Goods.objects.select_for_update().get(id=goods_id) except Goods.DoesNotExist: transaction.savepoint_rollback(save_id) context['errmsg'] = '商品不存在!' return JsonResponse(context) # 获取数量 try: count = int(connect.hget(cart_key, goods_id)) except Exception as e: transaction.savepoint_rollback(save_id) context['errmsg'] = '购物车中不存在提交的商品!' return JsonResponse(context) # 校验是否超库存 old_onhand = goods.onhand if count > old_onhand: transaction.savepoint_rollback(save_id) context['errmsg'] = '库存不足!' return JsonResponse(context) # 计算新库存和新销量 new_onhand = old_onhand - count new_sales = goods.sales + count # 乐观锁,更新goods start affected_rows = Goods.objects.filter(id=goods_id, onhand=old_onhand).update(onhand=new_onhand, sales=new_sales) # 若受影响条数为0,即没有更新goods,则继续尝试 if affected_rows == 0: if i == 3: #第三次尝试还是没更新到数据,则认为失败 transaction.savepoint_rollback(save_id) context['errmsg'] = '下单失败' continue # 乐观锁 end # 创建订单行信息 OrderGoods.objects.create(goods=goods, order=order, count=count, price=goods.price) # 获取小计 amount = goods.price * count # 汇总数量和价格 total_count += count total_amount += amount break
5.3 两种锁比较
悲观锁,顾名思义,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,其他人则需要等待,加锁和释放锁都需要消耗一定的资源。
乐观锁,顾明思义,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只是最后更新的时候,判断其中是否被其他人修改过,若不一致,则需要进行下一次循环查询,而循环也需要一定的资源