本项目基于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. 并发05--JAVA并发容器、框架、原子操作类

    一.ConcurrentHashMap的实现原理与使用 1.为什么要使用ConsurrentHashMap 两个原因,hashMap线程不安全(多线程并发put时,可能造成Entry链表变成环形数据结 ...

  2. jquery入门(1)

    1.jQuery简介 jQuery是一个快速.简洁的JavaScript框架,倡导写更少的代码,做更多的事情 jquery官方网站 jquery中文文档 1.1.简单函数封装 根据id.类名称来获取元 ...

  3. pythonic context manager知多少

    Context Managers 是我最喜欢的 python feature 之一,在恰当的时机使用 context manager 使代码更加简洁.清晰,更加安全,复用性更好,更加 pythonic ...

  4. 关于数据文件的文件头2-P2

    文章目录 1 疑问点 2 实验验证 2.1 实验环境 2.2 创建统一区大小管理表空间 2.2.1 统一区大小40k 2.2.2 统一区大小56k 2.2.3 统一区大小64k 2.2.4 统一区大小 ...

  5. Python3笔记002 - 1.2 搭建python开发环境

    第1章 认识python 1.2 搭建python开发环境 1.2.1 python开发环境概述 python开发环境常见的操作系统: Windows Mac OS Linux 1.2.2 安装pyt ...

  6. 读CSAPP第一章的收获

    这个系列只写了CSAPP第三版对于我的收获. 里面的内容很多,我只写我以前不知道的,然后现在又觉得挺有用的内容. 没有很好的排版,将就看. Amadhl定律:主要观点,想要显著加速整个系统,必须提升全 ...

  7. 从0开始,手把手教你使用React开发答题App

    项目演示地址 项目演示地址 项目源码 项目源码 其他版本教程 Vue版本 小程序版本 项目代码结构 前言 React 框架的优雅不言而喻,组件化的编程思想使得React框架开发的项目代码简洁,易懂,但 ...

  8. 爬虫03 /代理、cookie处理、模拟登录、提升爬虫效率

    爬虫03 /代理.cookie处理.模拟登录.提升爬虫效率 目录 爬虫03 /代理.cookie处理.模拟登录.提升爬虫效率 1. 代理 2. cookie处理 3. 模拟登录 4. 如何提升爬取数据 ...

  9. HotSpot VM运行时

    HotSpot VM运行时系统为HotSpot JIT编译器和垃圾收集器提供服务和通用API,同时还为VM提供启动.线程管理.JNI(Java本地接口)等基本功能.HotSpot VM运行时环境担当许 ...

  10. LDAP脚本批量导出用户

    背景:工作原因,搭建了LDAP服务,然后用户数过多,因为懒所以就通过python代码生成ldap脚本进行批量导入用户 1.整理用户名单,格式如下: 注:上述格式影响代码中的excel读取代码 2.py ...