0
点赞
收藏
分享

微信扫一扫

美多商城项目:结算订单与提交订单


一、结算订单

美多商城项目:结算订单与提交订单_数据库

 

1. 结算订单逻辑分析

结算订单是从Redis购物车中查询出被勾选的商品信息进行结算并展示。

2. 结算订单接口设计和定义

1.请求方式

选项

方案

请求方法

GET

请求地址

/orders/settlement/

3. 结算订单后端逻辑实现

class OrderSettlementView(LoginRequiredMixin, View):
"""结算订单"""

def get(self, request):
"""提供订单结算页面"""
# 获取登录用户
user = request.user

# 查询当前用户的所有地址信息
addresses = Address.objects.filter(user=request.user,
is_deleted=False)

# 从Redis购物车中查询出被勾选的商品信息
redis_conn = get_redis_connection('carts')
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
cart_selected = redis_conn.smembers('selected_%s' % user.id)
cart = {}
for sku_id in cart_selected:
cart[int(sku_id)] = int(redis_cart[sku_id])

# 查询商品信息
sku_list = []
# 查询商品信息
skus = SKU.objects.filter(id__in=cart.keys())
for sku in skus:
sku_list.append({
'id':sku.id,
'name':sku.name,
'default_image_url':sku.default_image.url,
'count': cart[sku.id],
'price':sku.price
})

# 补充运费
freight = Decimal('10.00')

addresses_list = []
for address in addresses:
addresses_list.append({
'id':address.id,
'province':address.province.name,
'city':address.city.name,
'district':address.district.name,
'place':address.place,
'receiver':address.receiver,
'mobile':address.mobile
})

# 渲染界面
context = {
'addresses': addresses_list,
'skus': sku_list,
'freight': freight,
}

return http.JsonResponse({'code':0,
'errmsg':'ok',
'context':context})

二、提交订单 

2.1 创建订单数据库表

生成的订单数据要做持久化处理,而且需要在《我的订单》页面展示出来。

1. 订单数据库表分析

注意:

  • 订单号不再采用数据库自增主键,而是由后端生成。
  • 一个订单中可以有多个商品信息,订单基本信息和订单商品信息是一对多的关系。

美多商城项目:结算订单与提交订单_数据库_02

 

 2. 订单模型类迁移建表

from django.db import models
from apps.goods.models import SKU
from apps.users.models import User, Address
from utils.models import BaseModel


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, "已取消"),
)
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="商品总数")
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

2.1 保存订单基本信息和订单商品信息

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. 保存订单基本信息

class OrderCommitView(LoginRequiredJSONMixin, View):
"""提交订单"""

def post(self, request):
"""保存订单信息和订单商品信息"""
# 获取当前要保存的订单数据
json_dict = json.loads(request.body.decode())
address_id = json_dict.get('address_id')
pay_method = json_dict.get('pay_method')
# 校验参数
if not all([address_id, pay_method]):
return http.HttpResponseBadRequest('缺少必传参数')
# 判断address_id是否合法
try:
address = Address.objects.get(id=address_id)
except Exception:
return http.HttpResponseBadRequest('参数address_id错误')
# 判断pay_method是否合法
if pay_method not in [OrderInfo.PAY_METHODS_ENUM['CASH'], OrderInfo.PAY_METHODS_ENUM['ALIPAY']]:
return http.HttpResponseBadRequest('参数pay_method错误')

# 获取登录用户
user = request.user
# 生成订单编号:年月日时分秒+用户编号
order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + ('%09d' % user.id)
# 保存订单基本信息 OrderInfo(一)
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0,
total_amount=Decimal('0'),
freight=Decimal('10.00'),
pay_method=pay_method,
status=OrderInfo.ORDER_STATUS_ENUM['UNPAID'] if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY'] else
OrderInfo.ORDER_STATUS_ENUM['UNSEND']
)
pass

3. 保存订单商品信息

class OrderCommitView(LoginRequiredJSONMixin, View):
"""提交订单"""

def post(self, request):
"""保存订单信息和订单商品信息"""
# 获取当前保存订单时需要的信息
......

# 保存订单基本信息 OrderInfo(一)
......

# 从redis读取购物车中被勾选的商品信息
redis_conn = get_redis_connection('carts')
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
selected = redis_conn.smembers('selected_%s' % user.id)
carts = {}
for sku_id in selected:
carts[int(sku_id)] = int(redis_cart[sku_id])
sku_ids = carts.keys()

# 遍历购物车中被勾选的商品信息
for sku_id in sku_ids:
# 查询SKU信息
sku = SKU.objects.get(id=sku_id)
# 判断SKU库存
sku_count = carts[sku.id]
if sku_count > sku.stock:
return http.JsonResponse({'code': 400, 'errmsg': '库存不足'})

# SKU减少库存,增加销量
sku.stock -= sku_count
sku.sales += sku_count
sku.save()

# 保存订单商品信息 OrderGoods(多)
OrderGoods.objects.create(
order=order,
sku=sku,
count=sku_count,
price=sku.price,
)

# 保存商品订单中总价和总数量
order.total_count += sku_count
order.total_amount += (sku_count * sku.price)

# 添加邮费和保存订单信息
order.total_amount += order.freight
order.save()

# 清除购物车中已结算的商品
pl = redis_conn.pipeline()
pl.hdel('carts_%s' % user.id, *selected)
pl.srem('selected_%s' % user.id, *selected)
pl.execute()

# 响应提交订单结果
return http.JsonResponse({'code': 0, 'errmsg': '下单成功', 'order_id': order.order_id})

 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. 使用事务保存订单数据

class OrderCommitView(LoginRequiredJSONMixin, View):
"""订单提交"""

def post(self, request):
"""保存订单信息和订单商品信息"""
# 获取当前保存订单时需要的信息
......

# 显式的开启一个事务
with transaction.atomic():
# 创建事务保存点
save_id = transaction.savepoint()

# 暴力回滚
try:
# 保存订单基本信息 OrderInfo(一)
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0,
total_amount=Decimal('0'),
freight=Decimal('10.00'),
pay_method=pay_method,
status=OrderInfo.ORDER_STATUS_ENUM['UNPAID'] if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY'] else
OrderInfo.ORDER_STATUS_ENUM['UNSEND']
)

# 从redis读取购物车中被勾选的商品信息
redis_conn = get_redis_connection('carts')
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
selected = redis_conn.smembers('selected_%s' % user.id)
carts = {}
for sku_id in selected:
carts[int(sku_id)] = int(redis_cart[sku_id])
sku_ids = carts.keys()

# 遍历购物车中被勾选的商品信息
for sku_id in sku_ids:
# 查询SKU信息
sku = SKU.objects.get(id=sku_id)
# 判断SKU库存
sku_count = carts[sku.id]
if sku_count > sku.stock:
# 出错就回滚
transaction.savepoint_rollback(save_id)
return http.JsonResponse({'code': 400, 'errmsg': '库存不足'})

# SKU减少库存,增加销量
sku.stock -= sku_count
sku.sales += sku_count
sku.save()

# 保存订单商品信息 OrderGoods(多)
OrderGoods.objects.create(
order=order,
sku=sku,
count=sku_count,
price=sku.price,
)

# 保存商品订单中总价和总数量
order.total_count += sku_count
order.total_amount += (sku_count * sku.price)

# 添加邮费和保存订单信息
order.total_amount += order.freight
order.save()
except Exception as e:
logger.error(e)
transaction.savepoint_rollback(save_id)
return http.JsonResponse({'code': 400, 'errmsg': '下单失败'})

# 提交订单成功,显式的提交一次事务
transaction.savepoint_commit(save_id)

# 清除购物车中已结算的商品
pl = redis_conn.pipeline()
pl.hdel('carts_%s' % user.id, *selected)
pl.srem('selected_%s' % user.id, *selected)
pl.execute()

# 响应提交订单结果
return http.JsonResponse({'code': 0, 'errmsg': '下单成功', 'order_id': order.order_id})

 2.4 使用乐观锁并发下单

重要提示:

  • 在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。

1. 并发下单问题演示和解决方案

美多商城项目:结算订单与提交订单_django_03

 

解决办法:

  • 悲观锁
  • 当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作,使用类似如下语法

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,一个订单一个订单的处理。

2. 使用乐观锁并发下单

思考:

  • 下单成功的条件是什么?
  • 首先库存大于购买量,然后更新库存和销量时原始库存没变。

结论:

  • 所以在用户库存满足的情况下,如果更新库存和销量时原始库存有变,那么继续给用户下单的机会。

class OrderCommitView(LoginRequiredJSONMixin, View):
"""订单提交"""

def post(self, request):
"""保存订单信息和订单商品信息"""
# 获取当前保存订单时需要的信息
......

# 显式的开启一个事务
with transaction.atomic():
# 创建事务保存点
save_id = transaction.savepoint()

# 暴力回滚
try:
# 保存订单基本信息 OrderInfo(一)
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0,
total_amount=Decimal('0'),
freight=Decimal('10.00'),
pay_method=pay_method,
status=OrderInfo.ORDER_STATUS_ENUM['UNPAID'] if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY'] else
OrderInfo.ORDER_STATUS_ENUM['UNSEND']
)

# 从redis读取购物车中被勾选的商品信息
redis_conn = get_redis_connection('carts')
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
selected = redis_conn.smembers('selected_%s' % user.id)
carts = {}
for sku_id in selected:
carts[int(sku_id)] = int(redis_cart[sku_id])
sku_ids = carts.keys()

# 遍历购物车中被勾选的商品信息
for sku_id in sku_ids:
while True:
# 查询SKU信息
sku = SKU.objects.get(id=sku_id)

# 读取原始库存
origin_stock = sku.stock
origin_sales = sku.sales

# 判断SKU库存
sku_count = carts[sku.id]
if sku_count > origin_stock:
# 事务回滚
transaction.savepoint_rollback(save_id)
return http.JsonResponse({'code': 400, 'errmsg': '库存不足'})

# 模拟延迟
# import time
# time.sleep(5)

# SKU减少库存,增加销量
# sku.stock -= sku_count
# sku.sales += sku_count
# sku.save()

# 乐观锁更新库存和销量
new_stock = origin_stock - sku_count
new_sales = origin_sales + sku_count
result = SKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
# 如果下单失败,但是库存足够时,继续下单,直到下单成功或者库存不足为止
if result == 0:
continue

# 保存订单商品信息 OrderGoods(多)
OrderGoods.objects.create(
order=order,
sku=sku,
count=sku_count,
price=sku.price,
)

# 保存商品订单中总价和总数量
order.total_count += sku_count
order.total_amount += (sku_count * sku.price)

# 下单成功或者失败就跳出循环
break

# 添加邮费和保存订单信息
order.total_amount += order.freight
order.save()
except Exception as e:
logger.error(e)
# 事务回滚
transaction.savepoint_rollback(save_id)
return http.JsonResponse({'code': 400, 'errmsg': '下单失败'})

# 保存订单数据成功,显式的提交一次事务
transaction.savepoint_commit(save_id)

# 清除购物车中已结算的商品
pl = redis_conn.pipeline()
pl.hdel('carts_%s' % user.id, *selected)
pl.srem('selected_%s' % user.id, *selected)
pl.execute()

# 响应提交订单结果
return http.JsonResponse({'code': 0, 'errmsg': '下单成功', 'order_id': order.order_id})

3. MySQL事务隔离级别

  • 事务隔离级别指的是在处理同一个数据的多个事务中,一个事务修改数据后,其他事务何时能看到修改后的结果。
  • MySQL数据库事务隔离级别主要有四种:
  • ​Serializable​​:串行化,一个事务一个事务的执行。
  • ​Repeatable read​​:可重复读,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响。
  • ​Read committed​​:读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值。
  • ​Read uncommitted​​:读取未提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值。
  • MySQL数据库默认使用可重复读( Repeatable read)。
  • 使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值,所以不能使用可重复读的隔离级别,应该修改为读取已提交(Read committed)。
  • 修改方式:

sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

修改完成之后重启
sudo service mysql restart

 

美多商城项目:结算订单与提交订单_redis_04

 ​​transaction-isolation=READ-COMMITTED​

美多商城项目:结算订单与提交订单_redis_05

 

举报

相关推荐

0 条评论