一:新建订单应用并迁移建表:

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

商城项目---day09---订单模块_时间戳

二:新建保存订单实现:

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()),
]

三:并发下单资源竞争问题:

商城项目---day09---订单模块_数据_02
商城项目---day09---订单模块_时间戳_03
商城项目---day09---订单模块_时间戳_04

商城项目---day09---订单模块_数据_05

商城项目---day09---订单模块_redis_06

四:乐观锁下单:

1:修改数据库的隔离级别:

  • (1)、打开mysql配置文件: sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
  • (2)、在配置文件尾部追加配置项: transaction-isolation=READ-COMMITTED
    • 注意:根据我们的分析,此处的隔离级别可以设置为READ-COMMITTEDREAD-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()),
]