PAYATHON 2026

Django Billing System 数据库设计:Product、Article 与 Order

支付小周

结论

Product 保存商品的公共信息。订单中的购买数量不应直接放在 Product 中,而应与商品、成交单价一起保存在独立的订单明细模型中,例如 OrderItem。

模型关系如下:

Order 1 ─── N OrderItem N ─── 1 Product

OrderItem 分别通过 ForeignKey 关联 Order 和 Product。不建议让 Order 直接使用 ManyToManyField(Product),因为一条订单明细通常只属于一个订单,不会由多个订单共享。

为什么数量不能直接放在 Product 中

Product 表示商品,例如“机械键盘”。OrderItem 表示一次具体交易中的明细,例如“订单 A 购买了 2 个机械键盘”。

同一商品可以出现在多个订单中,每个订单购买的数量也可能不同:

OrderProductQuantity
A001机械键盘2
A002机械键盘5

如果把 qty 放在 Product 中,只能保存一个数量,无法判断这个数量属于哪个订单。

订单明细表中重复出现 product_id 是正常的,因为每一行记录的是一笔不同的交易。商品名称、描述等公共信息仍然只保存在 Product 中。

如果 qty 表示库存数量,应改用含义明确的字段名,例如 stock_quantity,也可以单独建立库存表。库存数量和订单购买数量不应混用。

推荐的 Django 模型

from django.db import models
from django.db.models import Q


class Product(models.Model):
    name = models.CharField(max_length=200)
    current_price = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self):
        return self.name


class Order(models.Model):
    number = models.CharField(max_length=20, unique=True)
    client = models.ForeignKey(
        "Client",
        on_delete=models.PROTECT,
        related_name="orders",
    )
    invent = models.ForeignKey(
        "Invent",
        on_delete=models.PROTECT,
        related_name="orders",
    )
    created_at = models.DateTimeField(auto_now_add=True)

    products = models.ManyToManyField(
        Product,
        through="OrderItem",
        related_name="orders",
    )

    def __str__(self):
        return self.number


class OrderItem(models.Model):
    order = models.ForeignKey(
        Order,
        on_delete=models.CASCADE,
        related_name="items",
    )
    product = models.ForeignKey(
        Product,
        on_delete=models.PROTECT,
        related_name="order_items",
    )
    quantity = models.PositiveIntegerField()
    unit_price = models.DecimalField(max_digits=10, decimal_places=2)

    class Meta:
        constraints = [
            models.CheckConstraint(
                condition=Q(quantity__gt=0),
                name="order_item_quantity_gt_zero",
            ),
            models.UniqueConstraint(
                fields=["order", "product"],
                name="unique_product_per_order",
            ),
        ]

    @property
    def subtotal(self):
        return self.unit_price * self.quantity

这里的 ManyToManyField 并非必需。它通过 through="OrderItem" 提供了便捷的商品查询接口,真正保存订单关系和数量的仍是 OrderItem:

order.items.all()
order.products.all()
product.order_items.all()

如果不需要 order.products.all() 这样的快捷查询,可以删除 Order.products 字段,只保留两个 ForeignKey。

创建订单及明细

from decimal import Decimal
from django.db import transaction


@transaction.atomic
def create_order(client, invent, product, quantity):
    order = Order.objects.create(
        number="A001",
        client=client,
        invent=invent,
    )

    OrderItem.objects.create(
        order=order,
        product=product,
        quantity=quantity,
        unit_price=Decimal(product.current_price),
    )

    return order

如果同一商品在一个订单中只能出现一次,可以保留 UniqueConstraint。再次增加该商品时,更新已有明细即可:

item, created = OrderItem.objects.get_or_create(
    order=order,
    product=product,
    defaults={
        "quantity": 2,
        "unit_price": product.current_price,
    },
)

if not created:
    item.quantity += 2
    item.save(update_fields=["quantity"])

如果同一商品可能因折扣、批次或税率不同,在一个订单中出现多行,就不要添加 unique_product_per_order 约束。另一种做法是将批次、价格类型等字段纳入唯一约束。

为什么订单明细需要保存成交单价

如果只在 Product 中保存当前价格,历史账单可能失真。假设商品下单时的价格是 100.00,后来调整为 120.00,历史订单仍应显示原成交价 100.00。

各字段的职责如下:

  • Product.current_price 表示当前销售价格。
  • OrderItem.unit_price 表示下单时确定的成交单价。
  • 折扣、税率等影响账单的值,也应根据业务需要保存在订单或订单明细中。

成交价格快照是必要的交易记录,不属于应当消除的数据重复。

删除行为与字段类型

OrderItem.order 适合使用 CASCADE,删除订单时一并删除其明细。

OrderItem.product 通常使用 PROTECT,防止物理删除已经出现在历史订单中的商品。商品下架可以通过状态字段处理:

is_active = models.BooleanField(default=True)

金额通常应使用 DecimalField,不宜使用 IntegerField 或 FloatField,以免出现浮点数精度问题。如果系统明确以“分”作为最小货币单位,也可以使用整数,但必须在整个系统中保持一致。

通常不需要手动声明 AutoField(primary_key=True),Django 会自动创建主键。订单编号也未必需要作为数据库主键。使用默认主键,并为 number 添加 unique=True,以后修改编号格式或建立外键会更方便。

各模型最终负责的数据如下:

  • Product:商品目录和当前价格。
  • Order:客户、订单号、时间等订单头信息。
  • OrderItem:所属订单、商品、购买数量和成交单价。
  • 库存模型:实际库存数量及其变动记录。

备注:内容仅供参考。