本项目基于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 两种锁比较

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

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

乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

DJANGO-天天生鲜项目从0到1-011-订单-订单提交和创建的更多相关文章

  1. django天天生鲜项目

    .后台admin管理天天生鲜商品信息 models里 from django.db import modelsfrom tinymce.models import HTMLField #需要pip安装 ...

  2. DJANGO-天天生鲜项目从0到1-012-订单-用户订单页面

    本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...

  3. DJANGO-天天生鲜项目从0到1-007-首页静态化与缓存

    本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...

  4. python 天天生鲜项目

    python 天天生鲜项目 django版:https://github.com/Ivy-1996/fresh flask版:https://github.com/Ivy-1996/flask-fre ...

  5. Django之天天生鲜项目

    准备工作 1.配置settings.py内置文件 注意: AUTH_USER_MODEL配置参数要在第一次迁移数据库之前配置,否则可能django的认证系统工作不正常 2.创建应用 3.配置主路由 一 ...

  6. DJANGO-天天生鲜项目从0到1-010-购物车-购物车操作页面(勾选+删改)

    本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...

  7. DJANGO-天天生鲜项目从0到1-009-购物车-Ajax实现添加至购物车功能

    本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...

  8. DJANGO-天天生鲜项目从0到1-009-搜索功能实现(django-haystack+whoosh+jieba)

    本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...

  9. DJANGO-天天生鲜项目从0到1-006-首页-内容展示

    本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...

随机推荐

  1. springboot @Cacheable 基本使用

    加入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>sp ...

  2. 在 Spring Boot 中使用 HikariCP 连接池

    上次帮小王解决了如何在 Spring Boot 中使用 JDBC 连接 MySQL 后,我就一直在等,等他问我第三个问题,比如说如何在 Spring Boot 中使用 HikariCP 连接池.但我等 ...

  3. 语言模型 N-gram 与其平滑方法推导

    N-gram N-gram 作为一个名词表示的是一个给定文本/音频样本中有n项(音素,音节,字母,单词)的一个连续序列. 数学表达 N-gram 模型表示的是当前这个 word \(w_i\) 依赖于 ...

  4. 队列的顺序存储与链式存储c语言实现

    一. 队列 1.队列定义:只允许在表的一端进行插入,表的另一端进行删除操作的线性表. 2.循环队列:把存储队列的顺序队列在逻辑上视为一个环. 循环队列状态: 初始时:Q.front=Q.rear=0 ...

  5. css实现左边定宽右边自适应的5种方法总汇

    在网页布局中,通常需要实现左边定宽右边自适应布局,默认html的结构如下: <div class="box"> <div class="left&quo ...

  6. NuGet 应用指南

     一.前言 在产品开发过程中,一点有很多类库:这么多类库大家是如何管理的呢,TFS.SVN.Github……?在开发人员使用对应类库是否存在类库引用路径不一致.版本不一致问题.依赖类库版本不对应等一些 ...

  7. 弹性碰撞问题:Ants+Linear world

    题目一:Ants 传送门 题目描述 输入 输出 样例 样例输入 样例输出 分析 一句话题意:有n只蚂蚁在木棍上爬行,每只蚂蚁的速度都是每秒1单位长度,现在给你所有蚂蚁初始的位置(蚂蚁运动方向未定),蚂 ...

  8. 02 . SaltStack高级用法(Python API)

    Python API简单使用 第一条命令 /usr/bin/salt默认使用的接口是LocalClient,该接口只能在salt master上使用 >>> import salt. ...

  9. 深克隆(deepclone)

    1.简单版: <script type="text/javascript"> const newObj = JSON.parse(JSON.stringify(oldO ...

  10. python--动态网页渲染pyqt5

    原文:https://blog.csdn.net/tymatlab/article/details/78647543 PyQt5 渲染动态网页 示例代码: # -*- coding: UTF-8 -* ...