• 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,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
     
  • 相关阅读:
    函数式编程(三元运算、文件操作、函数、装饰器)
    开发基础(练习题)
    开发基础(字符串操作、元祖、元组、Hash、字典、集合、字符编码转换)
    开发基础(字符编码、列表操作)
    开发基础 (变量、数据类型、格式化输出、运算符、流程控制、while循环)
    [LeetCode] 127. 单词接龙
    [LeetCode] 126. 单词接龙 II
    [LeetCode] 122. 买卖股票的最佳时机 II
    [LeetCode] 124. 二叉树中的最大路径和
    [LeetCode] 125. 验证回文串
  • 原文地址:https://www.cnblogs.com/gcxblogs/p/12890221.html
Copyright © 2020-2023  润新知