一:新建订单应用并迁移建表:
1:新建应用orders:
python3 ../manage.py startapp orders
2:注册子应用,配置路由:
1:dev.py
'apps.orders',
2:总路由
path("",include('apps.orders.urls')),
3:子路由:
urlpatterns = [
]
3:创建模型类:
from meiduo_mall.utils.models import BaseModel
from django.db import models
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('users.User', on_delete=models.PROTECT, verbose_name="下单用户")
address = models.ForeignKey('users.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('goods.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
4:迁移建表:
python3 manage.py makemigrations
python3 manage.py migrate
二:新建保存订单实现:
1:编辑orders/views.py
实现视图:
# 保存/新建订单
class OrderCommitView(LoginRequiredJSONMixin, View):
def post(self, request):
data = json.loads(request.body.decode())
address_id = data.get('address_id')
pay_method = data.get('pay_method')
if not all([address_id, pay_method]):
return JsonResponse({'code': 400, 'errmsg': '缺少参数'})
try:
Address.objects.get(pk=address_id)
except Address.DoesNotExist as e:
return JsonResponse({'code': 400, 'errmsg': '地址不存在'})
if pay_method not in [OrderInfo.PAY_METHODS_ENUM['CASH'], OrderInfo.PAY_METHODS_ENUM['ALIPAY']]:
return JsonResponse({'code': 400, 'errmsg': '付款方式不支持'})
# 0、把购物车中的sku商品数据读取出来
cart_dict = {} # {1: {"count": 5, "selected": True}}
conn = get_redis_connection('carts')
# {b"1": b"5"}
redis_skus = conn.hgetall('carts_%d'%request.user.id)
# [b'1']
redis_selected = conn.smembers('selected_%d'%request.user.id)
for sku_id,count in redis_skus.items():
if sku_id in redis_selected:
cart_dict[int(sku_id)] = {
'count': int(count),
'selected': sku_id in redis_selected
}
# 数据/业务处理
# 1、新建订单表数据OrderInfo(主)
# 20200912111256000001 ----> 约定订单的id的格式:时间戳 + 用户id(固定长度)
# 加上时间戳可以保证这个订单的id唯一性
order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + '%06d'%request.user.id
freight = Decimal('10.5')
order = OrderInfo.objects.create(
order_id=order_id,
user=request.user,
address_id=address_id,
total_count=0, # 初始化
total_amount=0, # 初始化
freight=freight,
pay_method=pay_method
)
# 2、插入订单商品表数据OrderGoods(从) --> 具体的sku商品就是购物车中选中的商品
sku_ids = cart_dict.keys()
for sku_id in sku_ids:
# 购物车中的sku商品
sku = SKU.objects.get(pk=sku_id)
# 中间表OrderGoods表中插入一条数据,表示新建的订单中的sku商品
OrderGoods.objects.create(
order=order,
sku=sku,
count=cart_dict[sku_id]['count'],
price=sku.price,
)
order.total_count += cart_dict[sku_id]['count']
order.total_amount += cart_dict[sku_id]['count'] * sku.price
order.total_amount += freight
order.save()
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'order_id': order_id
})
2:路由:
from django.urls import re_path
from . import views
urlpatterns = [
# 保存订单
re_path(r'^orders/commit/$', views.OrderCommitView.as_view()),
]
三:并发下单资源竞争问题:
四:乐观锁下单:
1:修改数据库的隔离级别:
- (1)、打开mysql配置文件:
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
- (2)、在配置文件尾部追加配置项:
transaction-isolation=READ-COMMITTED
- 注意:根据我们的分析,此处的隔离级别可以设置为
READ-COMMITTED
或READ-UNCOMMITTED
均可实现乐观锁机制,但遵循最大化隔离强度
原则,咱们设置为READ-COMMITTED
级别; - 隔离级别越高,越能够保证数据的安全性;隔离级别越低,越有可能产生错误的数据;
- 任何解决方案没有绝对优劣,只有相对合适;
- 注意:根据我们的分析,此处的隔离级别可以设置为
- (3)、重启Mysql数据库:
sudo /etc/init.d/mysql restart
2:编辑`apps/orders/views.py
# 保存/新建订单
class OrderCommitView(LoginRequiredJSONMixin, View):
def post(self, request):
data = json.loads(request.body.decode())
address_id = data.get('address_id')
pay_method = data.get('pay_method')
if not all([address_id, pay_method]):
return JsonResponse({'code': 400, 'errmsg': '缺少参数'})
try:
Address.objects.get(pk=address_id)
except Address.DoesNotExist as e:
return JsonResponse({'code': 400, 'errmsg': '地址不存在'})
if pay_method not in [OrderInfo.PAY_METHODS_ENUM['CASH'], OrderInfo.PAY_METHODS_ENUM['ALIPAY']]:
return JsonResponse({'code': 400, 'errmsg': '付款方式不支持'})
# 0、把购物车中的sku商品数据读取出来
cart_dict = {} # {1: {"count": 5, "selected": True}}
conn = get_redis_connection('carts')
# {b"1": b"5"}
redis_skus = conn.hgetall('carts_%d'%request.user.id)
# [b'1']
redis_selected = conn.smembers('selected_%d'%request.user.id)
for sku_id,count in redis_skus.items():
if sku_id in redis_selected:
cart_dict[int(sku_id)] = {
'count': int(count),
'selected': sku_id in redis_selected
}
sku_ids = cart_dict.keys()
# 数据/业务处理
# 1、新建订单表数据OrderInfo(主)
# 20200912111256000001 ----> 约定订单的id的格式:时间戳 + 用户id(固定长度)
# 加上时间戳可以保证这个订单的id唯一性
order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + '%06d'%request.user.id
freight = Decimal('10.5')
with transaction.atomic():
# 设置一个事务保存点
save_id = transaction.savepoint()
order = OrderInfo.objects.create(
order_id=order_id,
user=request.user,
address_id=address_id,
total_count=0, # 初始化
total_amount=0, # 初始化
freight=freight,
pay_method=pay_method
)
# 2、插入订单商品表数据OrderGoods(从) --> 具体的sku商品就是购物车中选中的商品
for sku_id in sku_ids:
# 乐观锁处理机制
while True:
# 购物车中的sku商品
sku = SKU.objects.get(pk=sku_id)
# (1)、读旧的库存和销量
old_stock = sku.stock
old_sales = sku.sales
# TODO: 判断库存和销量
# 用户下单量
count = cart_dict[sku_id]['count']
if count > old_stock:
# 库存不足, 回滚事务
transaction.savepoint_rollback(save_id)
return JsonResponse({'code': 400, 'errmsg': '库存不足'}, status=400)
# 修改销量和库存数据
# sku.stock -= count
# sku.sales += count
# sku.save()
# (2)、基于旧库存和销量计算新值
new_stock = old_stock - count
new_sales = old_sales + count
# (3)、在旧库存和销量基础上,查询后修改
ret = SKU.objects.filter(
pk=sku.id,
stock=old_stock,
sales=old_sales
).update(stock=new_stock, sales=new_sales)
# 如果update函数返回值为0,说明filter过滤结果为空,说明根据旧数据找不到原有的sku
# 说明有别的事务介入
if ret:
# 如果ret不为0,说明正确更新
break
# 中间表OrderGoods表中插入一条数据,表示新建的订单中的sku商品
OrderGoods.objects.create(
order=order,
sku=sku,
count=cart_dict[sku_id]['count'],
price=sku.price,
)
order.total_count += cart_dict[sku_id]['count']
order.total_amount += cart_dict[sku_id]['count'] * sku.price
order.total_amount += freight
order.save()
# 提交保存点(删除保存点)
transaction.savepoint_commit(save_id)
# 下单结束删除购物车
conn.hdel('carts_%d'%request.user.id, *sku_ids)
conn.srem('selected_%d'%request.user.id, *sku_ids)
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'order_id': order_id
})
五:结算页面:
1:编辑apps/orders/views.py
class OrderSettlementView(LoginRequiredJSONMixin, View):
def get(self, request):
user = request.user
# 返回用户可选地址
addresses = []
add_queryset = Address.objects.filter(
user=user,
is_deleted=False
)
for address in add_queryset:
addresses.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
})
# 返回选中的购物车数据
skus = []
conn = get_redis_connection('carts')
# redis_skus = {b'1': b'5'}
redis_skus = conn.hgetall('carts_%d'%user.id)
# redis_selected = [b'1']
redis_selected = conn.smembers('selected_%d'%user.id)
for sku_id,count in redis_skus.items():
# sku_id: b'1'; count: b'5'
if sku_id in redis_selected:
sku = SKU.objects.get(pk=int(sku_id))
skus.append({
'id': sku.id,
'name': sku.name,
'default_image_url': sku.default_image.url,
'count': int(count),
'price': sku.price
})
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'context': {
'addresses': addresses,
'skus': skus,
'freight': Decimal('10.5') # 计算的时候不会丢失精度
}
})
路由配置:
from django.urls import re_path
from . import views
urlpatterns = [
re_path(r'^orders/settlement/$', views.OrderSettlementView.as_view()),
]