订单建模,包含'订单基本信息'和'订单商品'两张表
from django.db import models
from utils.models import BaseModel
from users.models import UserInfo,Address
from goods.models import SKU
class OrderInfo(BaseModel):
'''
- 订单基本信息
- 用到 User 和 Address两个外键
'''
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, "已取消"),
)
# 不使用默认自动生成的ID字段,而是自定义
order_id = models.CharField(max_length=64, primary_key=True, verbose_name="订单号")
user = models.ForeignKey(UserInfo, on_delete=models.PROTECT, verbose_name="下单用户")
address = models.ForeignKey(Address, on_delete=models.PROTECT, verbose_name="收货地址")
# 正整数
total_count = models.PositiveIntegerField(default=1, verbose_name="商品总数")
# 设置了精度的十进制数字(推荐使用)
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):
"""
- 订单商品
-两个外键: order & sku
"""
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.PositiveIntegerField(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
提交订单展示页
-
用户收货地址
-
支付方式('货到付款'/'支付宝')
-
商品信息展示页(不再提供修改功能)
-
前端要获取两块信息,当网页开始加载的时候,立即向后端发起请求
-
获取用户的收货地址(接口之前已经写好)
-
获取购物车结算的商品信息(后端要做的事情)
-
......
mounted: function(){
// 获取地址信息
axios.get(this.host + '/user/addresses/', {
headers: {
'Authorization': 'JWT ' + this.token
},
responseType: 'json'
})
.then(response => {
this.addresses = response.data.addresses;
this.nowsite = response.data.default_address_id;
})
......
// 获取结算商品信息
axios.get(this.host+'/orders/settlement/', {
headers: {
'Authorization': 'JWT ' + this.token
},
responseType: 'json'
})
.then(response => {
this.skus = response.data.skus;
this.freight = response.data.freight;
this.total_count = 0;
this.total_amount = 0;
for(var i=0; i<this.skus.length; i++){
var amount = parseFloat(this.skus[i].price)*this.skus[i].count;
this.skus[i].amount = amount.toFixed(2);
this.total_count += this.skus[i].count;
this.total_amount += amount;
}
this.payment_amount = parseFloat(this.freight) + this.total_amount;
this.payment_amount = this.payment_amount.toFixed(2);
this.total_amount = this.total_amount.toFixed(2);
})
......
- 获取redis中,该用户购物车所有商品集,再加运费字段,构造成dict,返回给前端
构造类似下面的数据格式,用两个序列化器来搞定
{
"freight":"100.00",
"skus":[
{
"id":10,
"name":'华为xxxx手机',
"default_image_url":"https://www.dfsdfsf.xsdfsdss",
"price":"3788.00",
"count":1
},
{
"id":11,
"name":'苹果xxxx收集',
"default_image_url":"https://www.xxxx.yyyyyy",
"price":"6700.00",
"count":2
},
]
}
......
### orders.serializers
class CartSKUSerializer(serializers.ModelSerializer):
'''订单中,商品数据的序列化(提供给下面的序列化类使用)'''
count = serializers.PositiveIntegerField(label='购买数量')
class Meta:
model = SKU
fields = ['id','name','price','default_image_url','count']
class OrderSettlementSerializer(serializers.Serializer):
'''订单序列化器'''
# 多条数据,要加上many,skus对应一个查询集
skus = CartSKUSerializer(many=True)
# 简单赋值
freight = serializers.DecimalField(max_digits=10,decimal_places=2,label='运费')
- 在上述字段中,sku_id 和 count 这两个字段,我们需要从redis获取
在views中,我们实现这个需求
from decimal import Decimal
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django_redis import get_redis_connection
from goods.models import SKU
from .serializers import OrderSettlementSerializer
class OrderSettlementView(APIView):
# 检查权限
permission_classes = [IsAuthenticated,]
def get(self,request):
redis_conn = get_redis_connection('cart')
# 这里 user无需再校验,因为权限类已经检查好了
user = request.user
# 获取 hash 和 set 中,该用户的所有商品数据,使用空dict来收集
redis_cart_dict = redis_conn.hgetall('cart_{}'.format(user.id))
selected_ids = redis_conn.smembers('selected_{}'.format(user.id))
cart_dict = {}
for sku_id_bytes in selected_ids:
cart_dict[int(sku_id_bytes)] = int(redis_cart_dict[sku_id_bytes])
skus = SKU.objects.filter(id__in=cart_dict.keys())
# 添加count字段
for sku in skus:
sku.count = cart_dict[sku.id]
# 构造数据,并序列化返回
freight = Decimal('10.00')
data_dict = {
'freight':freight,
'skus':skus
}
# 这里序列化的对象如果是列表,即数据格式外层包一层list,就要加many=True
serializer = OrderSettlementSerializer(data_dict) # 序列化器可以对 模型对象/查询集/列表/字典 进行序列化
return Response(serializer.data)
提交订单按钮
-
当用户点击'提交订单'按钮时,前端只传两个字段
- 用户收货地址
- 支付方式(货到付款/支付宝)
- 商品的数据,不要从这里取(虽然用户该页面无法修改,但可以再打开购物车页面,进行编辑)
......
on_order_submit: function(){
if (this.order_submitting == false){
this.order_submitting = true;
axios.post(this.host+'/orders/', {
// 提交这两个字段
address: this.nowsite,
pay_method: this.pay_method
}, {
headers: {
'Authorization': 'JWT ' + this.token
},
responseType: 'json'
})
.then(response => {
location.href = '/order_success.html?order_id='+response.data.order_id
+'&amount='+this.payment_amount
+'&pay='+this.pay_method;
})
.catch(error => {
this.order_submitting = false;
alert(error.response.data[0]);
})
}
}
- 保存订单功能实现,当用户提交订单以后,我们把订单信息保存下来
以便后续用户根据订单信息支付商品费用
### views
# 标准的新增行为,使用 CreateAPIView
class CommitOrderView(CreateAPIView):
serializer_class = CommitOrderSerializer
permission_classes = [IsAuthenticated,]
### serializers
class CommitOrderSerializer(serializers.ModelSerializer):
class Meta:
model = OrderInfo
fields = ['address','pay_method','order_id']
# 提交订单以后,只展示一个'订单号'
read_only_fields = ['order_id']
extra_kwargs = {
'address':{'write_only':True},
'pay_method':{'write_only',True},
}
def create(self,validated_data):
pass
首先保存订单信息(即生成一条OrderInfo记录)
- 根据 OrderInfo模型,确定需要传值的字段
### models
class OrderInfo(BaseModel):
......
order_id # 手动生成
user # request 取
address # 前端传
total_count = 0 # 先默认0(商品的总数/总金额等订单生成成功以后,再从redis取(确保数据不会被用户改))
# 设置了精度的十进制数字
total_amount = 0
freight = 固定值
pay_method = 前端传
status = 根据 pay_method 确定,如果是支付宝,就显示未支付;其他显示 未发货
......
- 保存 OrderInfo 记录
class CommitOrderSerializer(serializers.ModelSerializer):
class Meta:
model = OrderInfo
......
def create(self,validated_data):
user = self.context['request'].user
# '20230202143404'
# order_id = timezone.localtime().strftime('%Y%m%d%H%M%S')+ ('%09d' % user.id)
order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + '0'*9 + str(user.id)
address = self.validated_data.get('address')
pay_method = self.validated_data.get('pay_method')
# 如果是支付宝,就显示未支付;其他显示 未发货
status = (OrderInfo.ORDER_STATUS_ENUM['UNPAID']
if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY']
else OrderInfo.ORDER_STATUS_ENUM['UNSEND'])
total_count = 0
total_amount = 0
freight = 10
OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=total_count,
total_amount=total_amount,
freight=10,
pay_method=pay_method,
status=status
)
- 从redis获取被勾选的商品信息,更新库存和销量
......
def create(self,validated_data):
......
order_info_obj = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=total_count,
total_amount=total_amount,
freight=10,
pay_method=pay_method,
status=status
)
### 获取redis商品数据
redis_conn = get_redis_connection('cart')
cart_dict = redis_conn.hgetall('cart_{}'.format(user.id))
selected_ids = redis_conn.smembers('selected_{}'.format(user.id))
# queryset两个特性:惰性和缓存
# 以下写法,生成queryset对象,会产生'缓存'问题(抢购/秒杀场景会导致库存不准确,引发严重问题!)
# SKU.objects.filter(id__in=selected_ids)
### 修改商品库存,销量
for sku_id_bytes in selected_ids:
sku_obj = SKU.objects.get(id=sku_id_bytes)
count = int(cart_dict['sku_id_bytes'])
if count > sku_obj.stock:
raise serializers.ValidationError('库存不足')
new_stock = sku_obj.stock - count
new_sales = sku_obj.sales + count
sku_obj.stock = new_stock
sku_obj.sales = new_sales
sku_obj.save()
- 保存订单商品信息
- 累加商品总数量和总价(之前默认值均为0)
- 加入邮费并保存订单信息,最后返回订单信息模型
from datetime import datetime
from django.utils import timezone
from rest_framework import serializers
from django_redis import get_redis_connection
from goods.models import SKU
from .models import OrderInfo,OrderGoods
class CommitOrderSerializer(serializers.ModelSerializer):
class Meta:
......
def create(self,validated_data):
### 保存订单基本信息
user = self.context['request'].user
# '20230202143404'
# order_id = timezone.localtime().strftime('%Y%m%d%H%M%S')+ ('%09d' % user.id)
order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + '0'*9 + str(user.id)
address = self.validated_data.get('address')
pay_method = self.validated_data.get('pay_method')
# 如果是支付宝,就显示未支付;其他显示 未发货
status = (OrderInfo.ORDER_STATUS_ENUM['UNPAID']
if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY']
else OrderInfo.ORDER_STATUS_ENUM['UNSEND'])
total_count = 0
total_amount = 0
freight = 10
order_info_obj = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=total_count,
total_amount=total_amount,
freight=10,
pay_method=pay_method,
status=status
)
### 获取redis商品数据
redis_conn = get_redis_connection('cart')
cart_dict = redis_conn.hgetall('cart_{}'.format(user.id))
selected_ids = redis_conn.smembers('selected_{}'.format(user.id))
# queryset两个特性:惰性和缓存
# 以下写法,生成queryset对象,会产生'缓存'问题(抢购/秒杀场景会导致库存不准确,引发严重问题!)
# SKU.objects.filter(id__in=selected_ids)
### 修改商品库存,销量
for sku_id_bytes in selected_ids:
sku_obj = SKU.objects.get(id=sku_id_bytes)
count = int(cart_dict['sku_id_bytes'])
if count > sku_obj.stock:
raise serializers.ValidationError('库存不足')
new_stock = sku_obj.stock - count
new_sales = sku_obj.sales + count
sku_obj.stock = new_stock
sku_obj.sales = new_sales
sku_obj.save()
### 保存 订单商品记录
OrderGoods.objects.create(
order=order_info_obj,
sku=sku_obj,
count=count,
price=sku_obj.price
)
### 累加商品总数量和总价(之前默认值均为0)
order_info_obj.total_count += count
order_info_obj.total_amount += sku_obj.price * count
### 加入邮费并保存订单信息
order_info_obj.total_amount += order_info_obj.freight
order_info_obj.save()
return order_info_obj
- 上述代码存在的问题
- 触发异常时: raise serializers.ValidationError('库存不足')
- 此时 order_info_obj 记录已经生成,而这笔订单实际是失败的,不能生成订单让用户支付
- 在上述view中,我们同时操作了4张表,应该共同进退,要么一起成功,要么一起失败
- MySQL支持事务,刚好满足需求
- MySQL 默认并支持开启事务,每次操作一次表会默认拷贝一个副本,在副本上操作数据
若副本上成功操作数据,则让副本的数据覆盖源数据;若失败,就退回源数据
- 如果把上述同时操作的4张表看成一次事务,就可以满足需求,即4张表共同进退~
django事务API
- 参考网址
http://47.101.37.192/%E8%AF%BE%E4%BB%B6/%E7%BE%8E%E5%A4%9A%E5%95%86%E5%9F%8E%E8%AF%BE%E4%BB%B6/orders/commit/transaction.html
- 在 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():
# 这部分代码会在事务中执行
......
- 两种方法取舍
- 装饰器用法:整个视图中所有 MySQL 数据库的操作都看做一个事务,范围太大,不够灵活
而且无法直接作用于类视图
- with 语句用法(推荐):可以灵活的有选择性的把某些 MySQL 数据库的操作看做一个事务
而且不用关心视图的类型
- 事务中的保存点
在 Django 中,还提供了保存点的支持,可以在事务中创建保存点来记录数据的特定状态
数据库出现错误时,可以回滚到数据保存点的状态
from django.db import transaction
# 创建保存点
save_id = transaction.savepoint()
# 回滚到保存点
transaction.savepoint_rollback(save_id)
# 提交从保存点到当前状态的所有数据库事务操作
transaction.savepoint_commit(save_id)
事务运用到项目,实现'共同进退'
class CommitOrderSerializer(serializers.ModelSerializer):
class Meta:
......
def create(self,validated_data):
user = self.context['request'].user
order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + '0'*9 + str(user.id)
address = self.validated_data.get('address')
pay_method = self.validated_data.get('pay_method')
status = (OrderInfo.ORDER_STATUS_ENUM['UNPAID']
if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY']
else OrderInfo.ORDER_STATUS_ENUM['UNSEND'])
total_count = 0
total_amount = 0
freight = 10
# 开始事务
with transaction.atomic():
save_point = transaction.savepoint() # 创建事务保存点
try: # 若出现任何异常,则捕获,并回滚
order_info_obj = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=total_count,
total_amount=total_amount,
freight=10,
pay_method=pay_method,
status=status
)
redis_conn = get_redis_connection('cart')
cart_dict = redis_conn.hgetall('cart_{}'.format(user.id))
selected_ids = redis_conn.smembers('selected_{}'.format(user.id))
for sku_id_bytes in selected_ids:
sku_obj = SKU.objects.get(id=sku_id_bytes)
count = int(cart_dict['sku_id_bytes'])
# 模拟多个用户同时提交
# import time
# time.sleep(10)
if count > sku_obj.stock:
raise serializers.ValidationError('库存不足')
new_stock = sku_obj.stock - count
new_sales = sku_obj.sales + count
sku_obj.stock = new_stock
sku_obj.sales = new_sales
sku_obj.save()
OrderGoods.objects.create(
order=order_info_obj,
sku=sku_obj,
count=count,
price=sku_obj.price
)
order_info_obj.total_count += count
order_info_obj.total_amount += sku_obj.price * count
order_info_obj.total_amount += order_info_obj.freight
order_info_obj.save()
except Exception:
transaction.savepoint_rollback(save_point) # 回滚并触发异常
raise serializers.ValidationError('库存不足')
else:
transaction.savepoint_commit(save_point) # 操作成功则提交事务
return order_info_obj
用户同时下单引发的库存问题
-
若A用户和B用户同时下单,可能造成库存不足而多个用户却下单成功的场景
- 比如同时查询了库存,此时库存充足,而A用户网速好,优先修改了库存量;
由于B用户是同时查询,所以也会查询成功,然后和A用户一样的操作
最终结果:库存15个,却卖出18个...商家自己填坑!
- 比如同时查询了库存,此时库存充足,而A用户网速好,优先修改了库存量;
-
解决办法:有三种方式,悲观锁,乐观锁,任务队列
- 悲观锁: 当查询某条记录时,即让数据库为该记录加锁,锁住记录后其他人无法操作,使用类似如下语法
select stock from tb_sku where id=1 for update;
SKU.objects.select_for_update().get(id=1)
- 缺点: 类似于我们在多线程资源竞争时添加的互斥锁,容易出现死锁现象(即忘记释放锁)
而且用户只能等,用户体验并不好
- 乐观锁(推荐): 乐观锁并不是真实存在的锁,而是在更新的时候判断此时的库存是否是之前查询出的库存
如果相同,表示没人修改,可以更新库存,否则表示别人抢过资源,不再执行库存更新
update tb_sku set stock=2 where id=1 and stock=7
SKU.objects.filter(id=1, stock=7).update(stock=2)
- 任务队列: 将下单的逻辑放到任务队列中(如celery),将并行转为串行,所有人排队下单
比如开启只有一个进程的Celery,一个订单一个订单的处理
- 项目应用
......
def create(self,validated_data):
user = self.context['request'].user
order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + '0'*9 + str(user.id)
address = self.validated_data.get('address')
pay_method = self.validated_data.get('pay_method')
status = (OrderInfo.ORDER_STATUS_ENUM['UNPAID']
if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY']
else OrderInfo.ORDER_STATUS_ENUM['UNSEND'])
total_count = 0
total_amount = 0
freight = 10
with transaction.atomic():
save_point = transaction.savepoint()
try:
order_info_obj = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=total_count,
total_amount=total_amount,
freight=10,
pay_method=pay_method,
status=status
)
redis_conn = get_redis_connection('cart')
cart_dict = redis_conn.hgetall('cart_{}'.format(user.id))
selected_ids = redis_conn.smembers('selected_{}'.format(user.id))
for sku_id_bytes in selected_ids:
while True: # 让用户拥有无限次下单的机会
sku_obj = SKU.objects.get(id=sku_id_bytes)
count = int(cart_dict['sku_id_bytes'])
origin_stock = sku_obj.stock
# if count > sku_obj.stock:
if count > sku_obj.origin_stock:
raise serializers.ValidationError('库存不足')
new_stock = origin_stock - count
new_sales = sku_obj.sales + count
# 先查一遍是否符合原始数据,再修改
update_nums = SKU.objects.filter(stock=origin_stock,id=sku_id_bytes).update(stock=new_stock,sales=new_sales)
if update_nums == 0:
# raise serializers.ValidationError('资源抢夺')
continue # 虽然库存被修改过,若库存量仍满足B用户的需求,就继续上述操作
OrderGoods.objects.create(
order=order_info_obj,
sku=sku_obj,
count=count,
price=sku_obj.price
)
order_info_obj.total_count += count
order_info_obj.total_amount += sku_obj.price * count
break # 订单完成,就退出循环
order_info_obj.total_amount += order_info_obj.freight
order_info_obj.save()
except Exception:
transaction.savepoint_rollback(save_point) # 回滚并触发异常
raise serializers.ValidationError('库存不足')
else:
transaction.savepoint_commit(save_point) # 操作成功则提交事务
return order_info_obj
MySQL事务隔离级别
-
定义: 指的是在处理同一个数据的多个事务中,一个事务修改数据后
其他事务何时能看到修改后的结果 -
MySQL数据库事务隔离级别主要有四种
- Serializable:串行化,一个事务一个事务的执行
- Repeatable read(默认设置):可重复读,无论其他事务是否修改并提交了数据,
在这个事务中看到的数据值始终不受其他事务影响
- Read committed:读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值
- Read uncommitted:读取未提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值
- 使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值
所以不能使用可重复读的隔离级别,应该修改为读取已提交(Read committed)
- windows系统下,修改"my.ini",文件末尾增加这句
transaction-isolation=READ-COMMITTED
清除购物车中,已结算的商品
......
def create(self,validated_data):
......
# 清除购物车已结算商品
pl = redis_conn.pipeline()
pl.hdel('cart_{}'.format(user.id),*selected_ids)
pl.srem('selected_{}'.format(user.id),*selected_ids)
pl.execute()
return order_info_obj