开源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
  1. 结算订单后端逻辑实现
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_idint用户地址编号
pay_methodint用户支付方式

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_numint当前页码

3.响应结果:HTML

user_center_order.html

4.后端接口定义和实现orders.views.py

class UserOrderInfoView(View):

    @method_decorator(login_required)
    def get(self, request, page):
        开源web框架django知识总结(二十)

开源web框架django知识总结(十五)

开源web框架django知识总结(十五)

开源web框架django知识总结(二十一)

开源web框架django知识总结(二十一)

开源web框架django知识总结(二十一)