DJANGO-天天生鲜项目从0到1-011-订单-订单提交和创建

提交订单页面展示

购物车页面点击‘去结算’按钮后,跳转至/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 两种锁比较

悲观锁,顾名思义,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,其他人则需要等待,加锁和释放锁都需要消耗一定的资源。

乐观锁,顾明思义,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只是最后更新的时候,判断其中是否被其他人修改过,若不一致,则需要进行下一次循环查询,而循环也需要一定的资源

乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
 
posted @ 2020-05-15 15:11  Alex-GCX  阅读(705)  评论(0编辑  收藏  举报