开源web框架django知识总结(二十)
Posted 主打Python
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开源web框架django知识总结(二十)相关的知识,希望对你有一定的参考价值。
开源web框架django知识总结(二十)
阿尔法商城(订单)
订单
提示:
- 订单入口 在《购物车》页面的《去结算》。
- 《去结算》后进入到《结算订单》页面,展示出要结算的商品信息。
结算订单
新建apps->orders,同步数据,注册app,新建子路由urls.py,添加项目主路由
1. 结算订单逻辑分析
结算订单是从Redis购物车中查询出被勾选的商品信息进行结算并展示。
2. 结算订单接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /orders/settlement/ |
**2.请求参数:**无
3.响应结果:HTML place_order.html
4.后端接口定义
class OrderSettlementView(LoginRequiredMixin, View):
"""结算订单"""
def get(self, request):
"""提供订单结算页面"""
pass
- 结算订单后端逻辑实现
from django.views import View
from django_redis import get_redis_connection
from goods.models import SKU
from users.models import Address
from django.http import JsonResponse
# Paginator:分页器
from decimal import Decimal # 数据精确处理
from aerf_mall.utils.views import login_required
from django.utils.decorators import method_decorator
class OrderSettlementView(View):
@method_decorator(login_required)
def get(self, request):
user = request.user
# 1、读取用户购物商品(必须是选中的!)
conn = get_redis_connection('carts')
# b'1': b'4' hgetall,返回所有的字段和值。
cart_redis = conn.hgetall('carts_%s'%user.id)
# [b'1'] smembers返回所有集合
cart_selected = conn.smembers('selected_%s'%user.id)
# 构造商品返回数据
sku_list = []
for k,v in cart_redis.items():
# k:b'1'; v:b'4'
if k in cart_selected:
sku = SKU.objects.get(pk=k)
sku_list.append(
'id': sku.id,
'name': sku.name,
'default_image_url': sku.default_image_url.url,
'price': sku.price,
'count': int(v)
)
# 2、读取用户的收货地址数据
addresss_list = []
addresses = Address.objects.filter(user=user)
for address in addresses:
if not address.is_deleted:
addresss_list.append(
"id": address.id,
"province": address.province.name,
"city": address.city.name,
"district": address.district.name,
"place": address.place,
"mobile": address.mobile,
"receiver": address.receiver,
)
# 3、构建响应数据
# freight = 10.0
# 结论:使用Decimal类型来保存十进制数,目的是为了保证计算的精确度,Decimal(10,5)10位数,其中小数占5位
freight = Decimal('10.5')
return JsonResponse(
'code': 0,
'errmsg': 'ok',
'context':
'addresses': addresss_list,
'skus': sku_list,
'freight': freight
)
=======================================
提交订单
提示:
- 确认了要结算的商品信息后,就可以去提交订单了。
创建订单数据库表
生成的订单数据要做持久化处理,而且需要在《我的订单》页面展示出来。
1. 订单数据库表分析
注意:
- 订单号不再采用数据库自增主键,而是由后端生成。
- 一个订单中可以有多个商品信息,订单基本信息和订单商品信息是一对多的关系。
2. 订单模型类、迁移、建表
orders.models.py
from django.db import models
from aerf_mall.utils.BaseModel import BaseModel
from users.models import User,Address
from goods.models import SKU
# Create your models here.
#
class OrderInfo(BaseModel):
"""订单信息"""
PAY_METHODS_ENUM =
"CASH": 1,
"ALIPAY": 2
PAY_METHOD_CHOICES = (
(1, "货到付款"),
(2, "支付宝"),
)
ORDER_STATUS_ENUM =
"UNPAID": 1,
"UNSEND": 2,
"UNRECEIVED": 3,
"UNCOMMENT": 4,
"FINISHED": 5
ORDER_STATUS_CHOICES = (
(1, "待支付"),
(2, "待发货"),
(3, "待收货"),
(4, "待评价"),
(5, "已完成"),
(6, "已取消"),
)
# primary_key=True表明当前字段作为主键——自定义主键
order_id = models.CharField(max_length=64, primary_key=True, verbose_name="订单号")
user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name="下单用户")
address = models.ForeignKey(Address, on_delete=models.PROTECT, verbose_name="收货地址")
total_count = models.IntegerField(default=1, verbose_name="商品总数")
# DecimalField类型字段:十进制的数(高精度)
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="商品总金额")
freight = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="运费")
pay_method = models.SmallIntegerField(choices=PAY_METHOD_CHOICES, default=1, verbose_name="支付方式")
status = models.SmallIntegerField(choices=ORDER_STATUS_CHOICES, default=1, verbose_name="订单状态")
class Meta:
db_table = "tb_order_info"
verbose_name = '订单基本信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.order_id
#
class OrderGoods(BaseModel):
"""订单商品"""
SCORE_CHOICES = (
(0, '0分'),
(1, '20分'),
(2, '40分'),
(3, '60分'),
(4, '80分'),
(5, '100分'),
)
order = models.ForeignKey(OrderInfo, related_name='skus', on_delete=models.CASCADE, verbose_name="订单")
sku = models.ForeignKey(SKU, on_delete=models.PROTECT, verbose_name="订单商品")
count = models.IntegerField(default=1, verbose_name="数量")
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="单价")
comment = models.TextField(default="", verbose_name="评价信息")
score = models.SmallIntegerField(choices=SCORE_CHOICES, default=5, verbose_name='满意度评分')
is_anonymous = models.BooleanField(default=False, verbose_name='是否匿名评价')
is_commented = models.BooleanField(default=False, verbose_name='是否评价了')
class Meta:
db_table = "tb_order_goods"
verbose_name = '订单商品'
verbose_name_plural = verbose_name
def __str__(self):
return self.sku.name
注意:apps/carts/views.py中第八行修改
from aerf_mall.utils.views import login_required # 注意修改导包路径
===========================================
保存订单基本信息和订单商品信息
1. 提交订单接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | POST |
请求地址 | /orders/commit/ |
2.请求参数:JSON
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
address_id | int | 是 | 用户地址编号 |
pay_method | int | 是 | 用户支付方式 |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
order_id | 订单编号 |
4.后端接口定义
class OrderCommitView(LoginRequiredJSONMixin, View):
"""订单提交"""
def post(self, request):
"""保存订单信息和订单商品信息"""
pass
提示:
- 订单数据分为订单基本信息和订单商品信息,二者为一对多的关系。
- 保存到订单的数据是从Redis购物车中的已勾选的商品信息。
2. 保存订单基本信息
3. 保存订单商品信息
=========================================
使用事务保存订单数据
重要提示:
- 在保存订单数据时,涉及到多张表(OrderInfo、OrderGoods、SKU、SPU)的数据修改,对这些数据的修改应该是一个整体事务,即要么一起成功,要么一起失败。
- Django中对于数据库的事务,默认每执行一句数据库操作,便会自动提交。所以我们需要在保存订单中自己控制数据库事务的执行流程。
1. Django中事务的使用
1.Django中事务的使用方案
-
在Django中可以通过django.db.transaction模块提供的atomic来定义一个事务。
-
atomic提供两种方案实现事务:
装饰器用法:
from django.db import transaction
@transaction.atomic
def viewfunc(request):
# 这些代码会在一个事务中执行
......
with语句用法:
from django.db import transaction
def viewfunc(request):
# 这部分代码不在事务中,会被Django自动提交
......
with transaction.atomic():
# 这部分代码会在事务中执行
.....
2.事务方案的选择:
- **装饰器用法:**整个视图中所有mysql数据库的操作都看做一个事务,范围太大,不够灵活。而且无法直接作用于类视图。
- **with语句用法:**可以灵活的有选择性的把某些MySQL数据库的操作看做一个事务。而且不用关心视图的类型。
- 综合考虑后我们选择 with语句实现事务
3.事务中的保存点:
- 在Django中,还提供了保存点的支持,可以在事务中创建保存点来记录数据的特定状态,数据库出现错误时,可以回滚到数据保存点的状态。
from django.db import transaction
# 创建保存点
save_id = transaction.savepoint()
# 回滚到保存点
transaction.savepoint_rollback(save_id)
# 提交从保存点到当前状态的所有数据库事务操作
transaction.savepoint_commit(save_id)
2. 使用事务保存订单数据
==========================================
使用乐观锁并发下单
重要提示:
- 在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。
1. 并发下单问题演示和解决方案
2. 使用乐观锁并发下单
思考:
- 下单成功的条件是什么?
- 首先库存大于购买量,然后更新库存和销量时原始库存没变。
结论:
- 所以在用户库存满足的情况下,如果更新库存和销量时原始库存有变,那么继续给用户下单的机会。
3. MySQL事务隔离级别
-
事务隔离级别指的是在处理同一个数据的多个事务中,一个事务修改数据后,其他事务何时能看到修改后的结果。
-
MySQL数据库事务隔离级别主要有四种:
Serializable
:串行化,一个事务一个事务的执行。Repeatable read
:可重复读,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响。Read committed
:读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值。Read uncommitted
:读取未提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值。- MySQL数据库默认使用可重复读( Repeatable read)。
-
使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值,所以不能使用可重复读的隔离级别,应该修改为读取已提交(Read committed)。
-
修改方式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rnnbZoF1-1642429547962)(C:/Users/DELL/Desktop/day21/images/13.4.png)]
重启mysql服务:sudo service mysql restart
下单后端接口实现:orders.views.py
import json
from django.db import transaction # Django中事务的使用
# timezone:是django提供的一个专门用于处理时间的模块
from django.utils import timezone
from orders.models import OrderInfo, OrderGoods
class OrderCommitView(View):
@method_decorator(login_required)
def post(self, request):
# 1、提取参数
data = json.loads(request.body.decode())
address_id = data.get('address_id')
pay_method = data.get('pay_method')
# 2、校验参数
if not all([address_id, pay_method]):
return JsonResponse('code': 400, 'errmsg': '缺少参数', status=400)
try:
address = Address.objects.get(pk=address_id)
except Address.DoesNotExist as e:
print(e)
return JsonResponse('code': 400, 'errmsg': '地址有误!', status=404)
if pay_method not in [1,2]:
return JsonResponse('code': 400, 'errmsg': '不支持的付款方式', status=400)
# 3、新建/保存数据(新建订单和订单商品数据)
# 3.1 新建订单表数据(主)
# 保证订单id的唯一
# order_id = "202008060901560001"
#
# timezone.localtime() --> 本地时间点对象(取决于TIME_ZONE配置参数)
user = request.user
order_id = timezone.localtime().strftime("%Y%m%d%H%M%S") + "%09d"%user.id
# 获取用户的购物车sku商品
conn = get_redis_connection('carts')
# b'1': b'4'
cart_redis = conn.hgetall('carts_%s'%user.id)
# [b'1']
selected_redis = conn.smembers('selected_%s'%user.id)
carts = # 保存当前已选中的sku商品信息
for k,v in cart_redis.items():
# k: b'1'; v:b'4'
if k in selected_redis:
carts[int(k)] = int(v) # carts[1]=4
# 3.2 新建订单商品数据
sku_ids = carts.keys() # 选中的购物车中的所有sku的主键
with transaction.atomic():
# 关键的时间阶段——order订单新建节点
save_id = transaction.savepoint() # 事务执行保存点
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0, # 后续在统计订单商品的时候再修改
total_amount=0,
freight=Decimal('10.00'),
pay_method=pay_method,
# status= 2 if pay_method==1 else 1
status=OrderInfo.ORDER_STATUS_ENUM['UNSEND'] if pay_method == OrderInfo.PAY_METHODS_ENUM['CASH'] else
OrderInfo.ORDER_STATUS_ENUM['UNPAID']
)
for sku_id in sku_ids:
while True:
sku = SKU.objects.get(pk=sku_id)
# 旧数据
old_stock = sku.stock
old_sales = sku.sales
if carts[sku_id] > old_stock:
# 由于库存不足,我事务需要会滚到,订单新建之前的节点
transaction.savepoint_rollback(save_id)
return JsonResponse('code': 400, 'errmsg': '库存不足!', status=400)
# 商品销量和库存修改
# sku.stock -= carts[sku_id]
# sku.sales += carts[sku_id]
# sku.save()
# 计算新值——此处的计算,可能很漫长,期间很有可能别的事务介入
new_stock = old_stock - carts[sku_id]
new_sales = old_sales + carts[sku_id]
# 更新——在原有的旧数据基础上过滤更新
result = SKU.objects.filter(
pk=sku.id,
stock=old_stock,
sales=old_sales
).update(stock=new_stock, sales=new_sales)
if result == 0:
continue
break
# 插入一个订单商品数据
OrderGoods.objects.create(
order=order,
sku=sku,
count=carts[sku_id],
price=sku.price
)
# 订单表数据修改
order.total_count += carts[sku_id]
order.total_amount += (carts[sku_id] * sku.price)
# 每一次插入订单商品的时候修改了当前订单的属性
order.total_amount += order.freight
order.save()
transaction.savepoint_commit(save_id) # 删除保存点
# 把用户的购车车商品删除
conn.hdel('carts_%s'%user.id, *sku_ids)
conn.srem('selected_%s'%user.id, *sku_ids)
# 4、构建响应
return JsonResponse('code': 0, 'errmsg': 'ok', 'order_id': order_id)
=============================================
展示提交订单成功页面
支付方式:货到付款
=======================================
我的订单
1.请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /orders/info/(?P<page_num>\\d+)/ |
2.请求参数:路径参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
page_num | int | 是 | 当前页码 |
3.响应结果:HTML
user_center_order.html
4.后端接口定义和实现orders.views.py
class UserOrderInfoView(View):
@method_decorator(login_required)
def get(self, request, page):
开源web框架django知识总结(二十)