本项目基于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. android屏幕适配的全攻略--支持不同的屏幕尺寸适配平板和手机

    一. 核心概念与单位详解 1. 什么是屏幕尺寸.屏幕分辨率.屏幕像素密度? 屏幕分辨率越大,手机越清晰 dpi就是dot per inch dot意思是点,就是每英寸上面的像素点数 android原始 ...

  2. 02【熟悉】springboot和微服务的介绍

    1,springboot简介 Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程. 该框架使用了特定的方式来进行配置,从 ...

  3. git常用命令(部分)

    git常用命令 1.git init 初始化一个新本地仓库,它在工作目录下生成一个名为.git的隐藏文件夹. 安装好git的,新建一个文件夹,在空文件夹中鼠标右击点击Git Bash Here 2.g ...

  4. C#实现快速查找(递归,非递归)

    原文件: http://pan.baidu.com/share/link?shareid=2838344856&uk=3912660076 我英语很烂...哎,我正在努力... 效果图:

  5. css实现1px 像素线条_解决移动端1px线条的显示方式

    使用CSS 绘制出 1px 的边框,在移动端上渲染的效果会出现不同,部分手机发现1px 线条变胖了,这篇文章整理2种方式实现1px 像素线条. 1.利用box-shadow + transform & ...

  6. 数学计算 LibreOJ - 2573

    题目描述 小豆现在有一个数 x ,初始值为 1 . 小豆有 Q 次操作,操作有两种类型: 1 m: x=x×m ,输出 xmodM : 2 pos: x=x/ 第 pos 次操作所乘的数(保证第 po ...

  7. Electricity POJ - 2117 + SPF POJ - 1523 去除割点后求强连通分量个数问题

    Electricity POJ - 2117 题目描述 Blackouts and Dark Nights (also known as ACM++) is a company that provid ...

  8. day17 生成器, 面向过程, 三元表达式, 生成式

    1. 生成器 生成器:就是一种自定义的迭代器,是用来返回多次值自定义迭代器的好处:节省内存 return只能返回一次值,函数就立即结束了yield 1.可以挂起函数,保存函数的运行状态 2.可以用来返 ...

  9. 爬虫前篇 /https协议原理剖析

    爬虫前篇 /https协议原理剖析 目录 爬虫前篇 /https协议原理剖析 1. http协议是不安全的 2. 使用对称秘钥进行数据加密 3. 动态对称秘钥和非对称秘钥 4. CA证书的应用 5. ...

  10. AcWing 93. 递归实现组合型枚举

    AcWing 93. 递归实现组合型枚举 原题链接 从 1~n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案. 输入格式 两个整数 n,m ,在同一行用空格隔开. 输出格式 按照从小到大的 ...