DJANGO-天天生鲜项目从0到1-011-订单-订单提交和创建
本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习
https://www.bilibili.com/video/BV1vt41147K8?p=1
提交订单页面展示
购物车页面点击‘去结算’按钮后,跳转至/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 两种锁比较
悲观锁,顾名思义,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,其他人则需要等待,加锁和释放锁都需要消耗一定的资源。
乐观锁,顾明思义,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只是最后更新的时候,判断其中是否被其他人修改过,若不一致,则需要进行下一次循环查询,而循环也需要一定的资源
DJANGO-天天生鲜项目从0到1-011-订单-订单提交和创建的更多相关文章
- django天天生鲜项目
		
.后台admin管理天天生鲜商品信息 models里 from django.db import modelsfrom tinymce.models import HTMLField #需要pip安装 ...
 - DJANGO-天天生鲜项目从0到1-012-订单-用户订单页面
		
本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...
 - DJANGO-天天生鲜项目从0到1-007-首页静态化与缓存
		
本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...
 - python 天天生鲜项目
		
python 天天生鲜项目 django版:https://github.com/Ivy-1996/fresh flask版:https://github.com/Ivy-1996/flask-fre ...
 - Django之天天生鲜项目
		
准备工作 1.配置settings.py内置文件 注意: AUTH_USER_MODEL配置参数要在第一次迁移数据库之前配置,否则可能django的认证系统工作不正常 2.创建应用 3.配置主路由 一 ...
 - DJANGO-天天生鲜项目从0到1-010-购物车-购物车操作页面(勾选+删改)
		
本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...
 - DJANGO-天天生鲜项目从0到1-009-购物车-Ajax实现添加至购物车功能
		
本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...
 - DJANGO-天天生鲜项目从0到1-009-搜索功能实现(django-haystack+whoosh+jieba)
		
本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...
 - DJANGO-天天生鲜项目从0到1-006-首页-内容展示
		
本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...
 
随机推荐
- android屏幕适配的全攻略--支持不同的屏幕尺寸适配平板和手机
			
一. 核心概念与单位详解 1. 什么是屏幕尺寸.屏幕分辨率.屏幕像素密度? 屏幕分辨率越大,手机越清晰 dpi就是dot per inch dot意思是点,就是每英寸上面的像素点数 android原始 ...
 - 02【熟悉】springboot和微服务的介绍
			
1,springboot简介 Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程. 该框架使用了特定的方式来进行配置,从 ...
 - git常用命令(部分)
			
git常用命令 1.git init 初始化一个新本地仓库,它在工作目录下生成一个名为.git的隐藏文件夹. 安装好git的,新建一个文件夹,在空文件夹中鼠标右击点击Git Bash Here 2.g ...
 - C#实现快速查找(递归,非递归)
			
原文件: http://pan.baidu.com/share/link?shareid=2838344856&uk=3912660076 我英语很烂...哎,我正在努力... 效果图:
 - css实现1px 像素线条_解决移动端1px线条的显示方式
			
使用CSS 绘制出 1px 的边框,在移动端上渲染的效果会出现不同,部分手机发现1px 线条变胖了,这篇文章整理2种方式实现1px 像素线条. 1.利用box-shadow + transform & ...
 - 数学计算 LibreOJ - 2573
			
题目描述 小豆现在有一个数 x ,初始值为 1 . 小豆有 Q 次操作,操作有两种类型: 1 m: x=x×m ,输出 xmodM : 2 pos: x=x/ 第 pos 次操作所乘的数(保证第 po ...
 - Electricity  POJ - 2117 + SPF POJ - 1523 去除割点后求强连通分量个数问题
			
Electricity POJ - 2117 题目描述 Blackouts and Dark Nights (also known as ACM++) is a company that provid ...
 - day17  生成器, 面向过程, 三元表达式, 生成式
			
1. 生成器 生成器:就是一种自定义的迭代器,是用来返回多次值自定义迭代器的好处:节省内存 return只能返回一次值,函数就立即结束了yield 1.可以挂起函数,保存函数的运行状态 2.可以用来返 ...
 - 爬虫前篇 /https协议原理剖析
			
爬虫前篇 /https协议原理剖析 目录 爬虫前篇 /https协议原理剖析 1. http协议是不安全的 2. 使用对称秘钥进行数据加密 3. 动态对称秘钥和非对称秘钥 4. CA证书的应用 5. ...
 - AcWing   93. 递归实现组合型枚举
			
AcWing 93. 递归实现组合型枚举 原题链接 从 1~n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案. 输入格式 两个整数 n,m ,在同一行用空格隔开. 输出格式 按照从小到大的 ...