首页 > 解决方案 > 我应该使用模型的 ID 处理订单号吗?

问题描述

我有一个个人电子商务网站。

我使用模型的 ID 作为订单号。只是因为它看起来很合乎逻辑,而且我期望 ID 每次都会增加 1。

但是,我注意到我的订单(我的订单模型)的 ID 已经跳跃了两次:

a) 从 54 到 86(相差 32)。
b) 从 99 到 132(相差 33)。

不知道为什么,不知道我是否应该使用自定义字段而不是模型 ID。

我正在使用 Django 3.0 并在 Heroku 上托管我的项目。

模型.py:

class Order(models.Model):
    ORDER_STATUS = (
        ('recibido_pagado', 'Recibido y pagado'),
        ('recibido_no_pagado', 'Recibido pero no pagado'),
        ('en_proceso', 'En proceso'),
        ('en_camino', 'En camino'),
        ('entregado', 'Entregado'),
        ('cancelado', 'Cancelado por no pagar' )
    )
    token = models.CharField(max_length=100, blank=True, null=True)
    first_name = models.CharField(max_length=50, blank=True, null=True)
    last_name = models.CharField(max_length=50, blank=True, null=True)
    phone_number = models.CharField(max_length=30, blank=True)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    stickers_price = models.DecimalField(max_digits=10, decimal_places=2)
    discount = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
    shipping_cost = models.DecimalField(max_digits=10, decimal_places=2)
    email = models.EmailField(max_length=250, blank = True, verbose_name= 'Correo electrónico')
    last_four = models.CharField(max_length=100, blank=True, null=True)
    created = models.DateTimeField(auto_now_add=True)
    shipping_address = models.CharField(max_length=100, blank=True, null=True)
    shipping_address1 = models.CharField(max_length=100, blank=True, null=True)
    reference = models.CharField(max_length=100, blank=True, null=True)
    shipping_department = models.CharField(max_length=100, blank=True, null=True)
    shipping_province = models.CharField(max_length=100, blank=True, null=True)
    shipping_district = models.CharField(max_length=100, blank=True, null=True)
    reason = models.CharField(max_length=400, blank=True, null=True, default='')
    status = models.CharField(max_length=20, choices=ORDER_STATUS, default='recibido_pagado')
    comments = models.CharField(max_length=400, blank=True, null=True, default='')
    cupon = models.ForeignKey('marketing.Cupons', blank=True, null=True, default=None, on_delete=models.SET_NULL)


    class Meta:
        db_table = 'Order'
        ordering = ['-created']

    def __str__(self):
        return str(self.id)

    def igv(self):
        igv = int(self.total) * 18/100
        return igv

    def shipping_date(self):
        shipping_date = self.created + datetime.timedelta(days=10)
        return shipping_date

    def deposit_payment_date(self):
        deposit_payment_date = self.created + datetime.timedelta(days=2)
        return 

创建订单的视图:

@csrf_exempt
def cart_charge_deposit_payment(request):
    amount = request.POST.get('amount')
    email = request.user.email
    shipping_address = request.POST.get('shipping_address')
    shipping_cost = request.POST.get('shipping_cost')
    discount = request.POST.get('discount')
    stickers_price = request.POST.get('stickers_price')
    comments = request.POST.get('comments')
    last_four = 1111  
    transaction_amount = amount  

    first_name = request.user.first_name

    last_name = request.user.last_name

    phone_number = request.user.profile.phone_number

    current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

    shipping_address1 = request.user.profile.shipping_address1

    reference = request.user.profile.reference

    shipping_department = request.user.profile.shipping_department

    shipping_province = request.user.profile.shipping_province

    shipping_district = request.user.profile.shipping_district



    order_details = Order.objects.create(
        token='Random',
        first_name=first_name,
        last_name=last_name,
        phone_number=phone_number,
        email=email,  # Using email entered in Culqi module, NOT user.email. Could be diff.
        total=transaction_amount,
        stickers_price = stickers_price,
        discount = discount,
        shipping_cost=shipping_cost,
        last_four=last_four,
        created=current_time,
        shipping_address=shipping_address,
        shipping_address1=shipping_address1,
        reference=reference,
        shipping_department=shipping_department,
        shipping_province=shipping_province,
        shipping_district=shipping_district,
        status='recibido_no_pagado',
        cupon=cupon,
        comments=comments
    )

    ...

标签: pythondjango

解决方案


如果您需要连续编号而没有孔,则不应使用 Django 的自动生成id字段作为您的订单号。

为了即使在并发下也能保证唯一性,Django 创建了一个数据库序列,它是数据库中的一个对象,每次查询时都会产生一个新值。请注意,即使未将其保存到任何地方的数据库中,该序列也会使用生成的值。

然后发生的情况是,每当您尝试创建实例并且此操作在数据库级别失败时,无论如何都会消耗序列中的一个数字。因此,假设您成功创建了第一个订单,它的 ID 号为 1。然后假设您尝试创建第二个订单,但INSERT数据库中的订单失败(例如某些完整性检查或其他)。之后您成功创建了第三个订单,您会期望该订单的 ID 号为 2,但实际上它的 ID 号为 3,因为即使没有保存,也已从序列中消费了 2 号。

id所以不,如果您需要确保订单号中没有漏洞,则不能使用。

现在为了有连续的编号,您可以简单地添加一列

    order_number = models.PositiveIntegerField(unique=True, null=True)

问题是如何正确设置其值。因此,在没有并发的理想世界中(两个进程对同一个数据库运行查询),您可以简单地获得迄今为止的最大订单号,加 1 然后将此值保存到order_number. 问题是如果你天真地这样做,你最终会出现重复(实际上是完整性错误,因为unique=True会防止重复)。

解决此问题的一种方法是在计算和更新订单号时锁定您的表(请参阅此 SO 问题) 。

正如我假设您并不关心订单号是否忠实地反映了创建订单的顺序,而只是它是顺序的并且没有漏洞,您可以做的是在事务中运行如下查询(假设您的Order模型存在在ordersdjango 应用程序中):

UPDATE orders_order SET order_number = (SELECT COALESCE(MAX(order_number), 0) FROM orders_order) + 1 WHERE id = [yourid] AND order_number IS NULL 

现在,即使使用此查询,您也可能遇到并发问题,因为 Django 默认使用 postgres 默认隔离级别。因此,为了使此查询安全,您需要更改隔离级别。请参阅此 SO 问题,以了解有关具有两个不同隔离级别的两个独立连接的方法。使此查询安全所需的是将隔离级别设置为SERIALIZABLE.

假设您能够解决隔离级别问题,那么关于如何运行此查询的问题

from django.db import connections, transaction
with transaction.atomic(using='your_isolated_db_alias'):
    with connections['your_isolated_db_alias'].cursor() as cursor:
        cursor.execute('UPDATE orders_order SET order_number = (SELECT COALESCE(MAX(order_number), 0) FROM orders_order) + 1 WHERE id = %s AND order_number IS NULL', order.id)

上面的代码片段假设您有想要在名为 的变量中设置订单号的订单order。如果您的隔离是正确的,那么您应该是安全的。

现在有第三种选择,它用作select_for_update()表锁定机制(尽管它不是为此而设计的,而是用于行级锁定)。所以这个想法很简单,就像在您首先创建订单然后更新它以设置订单号之前一样。因此,为了保证您不会得到重复的(又名 IntegrityError)订单号,您所做的是发出一个查询,选择数据库中的所有订单,然后select_for_update()按以下方式使用:

from django.db import transaction
with transaction.atomic():
    # This locks every row in orders_order until the end of the transaction
    Order.objects.all().select_for_update()  # pointless query just to lock the table
    max_on = Order.objects.aggregate(max_on=Max('order_number'))['max_on']
    Order.objects.filter(id=order.id).update(order_number=max_on + 1)

只要您确定在输入上面的代码块之前至少有 1 个订单并且您总是select_for_update()先完成全部订单,那么您也应该是安全的。

这些是我能想到的如何解决连续编号的方法。我很想看到一个开箱即用的解决方案,但不幸的是我不知道。


推荐阅读