PAYATHON 2026

如何在 Django eCommerce Cart 中实现 State Machine?

支付老李

结论

不要创建模块级共享对象 state_machine = StateMachine(),也不建议把购物车、付款和下单的全部逻辑都放进 Cart。

更合适的职责划分如下:

  • Cart 管理商品、数量、金额等购物车数据。
  • empty、filled 通常可以根据购物车内容计算,无需单独保存。
  • Checkout 或 Order 管理结账、支付和订单状态。
  • 状态机用于校验当前状态是否允许执行某项操作。它可以是独立组件,也可以封装在 Checkout 或 Order 的方法中。
  • session 适合保存购物车和临时结账标识。支付结果与订单状态必须持久化到数据库。

状态属于某个购物车、结账流程或订单实例,不属于全局的 StateMachine 对象。

为什么不应使用全局 StateMachine 实例

下面的写法会让所有请求共享同一个对象:

state_machine = StateMachine()

如果 StateMachine 在内部保存当前状态,不同用户和不同 session 就可能读写同一份状态。在多线程、多进程或多服务器部署中,行为会更加不可预测。

即使暂时没有并发问题,模块级对象也无法可靠表示某位用户当前的结账状态。当前状态应该来自具体的业务对象,例如:

checkout.state

状态机本身最好是不保存用户状态的规则集合,或者是绑定到某个 Checkout 实例、仅短期存在的对象。

先区分购物车状态和结账状态

empty 和 filled 一般不属于需要持久化的工作流状态,它们是购物车内容的自然结果:

class Cart:
    @property
    def is_empty(self):
        return not bool(self.items)

删除最后一件商品后,购物车自然会变为空。如果同时维护 state = "empty",反而可能产生数据不一致:

{
    "state": "filled",
    "cart": {
        "items": []
    }
}

下面这些状态更接近真正的工作流状态:

checkout_started
payment_pending
payment_authorized
order_placed
payment_failed
cancelled

它们无法只根据购物车内容推导,而且涉及支付、订单和外部回调,更适合交给状态机管理。

prepayment 和 payment_auth 的具体含义还需要结合支付渠道定义。例如,在某些支付系统中,“已创建支付请求”、“已授权”和”已扣款”是三个不同阶段,不能合并为同一个状态。

推荐的数据组织方式

领域对象可以分为三层:

Cart
  └── 管理商品、数量、优惠和金额

Checkout
  └── 管理收货信息、库存确认和支付流程

Order
  └── 保存最终订单及其不可丢失的业务状态

session 只保存允许丢失或能够重建的数据,例如:

request.session["cart"] = {
    "items": [
        {"product_id": 42, "quantity": 2}
    ]
}

request.session["checkout_id"] = "..."

必须可靠追踪的数据则保存在数据库中:

Checkout.state
Order.status
Payment.status
Payment.provider_reference

不要只在 session 中记录”已付款”。用户可能清除 Cookie,session 也可能过期,支付平台还可能异步通知支付结果。因此,session 不能作为支付事实的唯一来源。

一个简单的状态机实现

项目规模不大时,未必需要引入状态机库。先用清晰的状态定义和转换表即可:

from enum import StrEnum


class CheckoutState(StrEnum):
    STARTED = "started"
    PAYMENT_PENDING = "payment_pending"
    PAYMENT_AUTHORIZED = "payment_authorized"
    ORDER_PLACED = "order_placed"
    PAYMENT_FAILED = "payment_failed"
    CANCELLED = "cancelled"


ALLOWED_TRANSITIONS = {
    CheckoutState.STARTED: {
        CheckoutState.PAYMENT_PENDING,
        CheckoutState.CANCELLED,
    },
    CheckoutState.PAYMENT_PENDING: {
        CheckoutState.PAYMENT_AUTHORIZED,
        CheckoutState.PAYMENT_FAILED,
        CheckoutState.CANCELLED,
    },
    CheckoutState.PAYMENT_FAILED: {
        CheckoutState.PAYMENT_PENDING,
        CheckoutState.CANCELLED,
    },
    CheckoutState.PAYMENT_AUTHORIZED: {
        CheckoutState.ORDER_PLACED,
    },
    CheckoutState.ORDER_PLACED: set(),
    CheckoutState.CANCELLED: set(),
}


class InvalidStateTransition(Exception):
    pass


class CheckoutStateMachine:
    @staticmethod
    def transition(current_state, target_state):
        current_state = CheckoutState(current_state)
        target_state = CheckoutState(target_state)

        allowed = ALLOWED_TRANSITIONS[current_state]
        if target_state not in allowed:
            raise InvalidStateTransition(
                f"Cannot transition from {current_state} to {target_state}"
            )

        return target_state

这里的 CheckoutStateMachine 不保存用户状态,只验证转换规则,所以可以安全复用。

在 Django Model 中保存状态

结账流程进入支付阶段后,建议创建数据库记录:

from django.db import models, transaction


class Checkout(models.Model):
    class State(models.TextChoices):
        STARTED = "started", "Started"
        PAYMENT_PENDING = "payment_pending", "Payment pending"
        PAYMENT_AUTHORIZED = "payment_authorized", "Payment authorized"
        ORDER_PLACED = "order_placed", "Order placed"
        PAYMENT_FAILED = "payment_failed", "Payment failed"
        CANCELLED = "cancelled", "Cancelled"

    state = models.CharField(
        max_length=32,
        choices=State.choices,
        default=State.STARTED,
    )

    def change_state(self, target_state):
        new_state = CheckoutStateMachine.transition(
            self.state,
            target_state,
        )
        self.state = new_state.value

调用方不应随意修改 state,而应该通过表达业务意图的方法触发状态转换:

class Checkout(models.Model):
    # 字段定义略

    def begin_payment(self):
        if not self.can_be_paid():
            raise ValueError("Checkout is not ready for payment")

        self.change_state(self.State.PAYMENT_PENDING)
        self.save(update_fields=["state"])

    def mark_payment_authorized(self):
        self.change_state(self.State.PAYMENT_AUTHORIZED)
        self.save(update_fields=["state"])

    def can_be_paid(self):
        # 示例:实际项目还应检查金额、库存、地址等条件
        return True

这比公开通用的 set_state() 更安全,因为状态转换通常还会涉及业务条件和副作用。

Cart 应该怎样触发流程

Cart.add_item() 无需显式触发 empty -> filled:

class Cart:
    def __init__(self, session):
        self.session = session
        self.data = session.get("cart", {"items": []})

    @property
    def is_empty(self):
        return len(self.data["items"]) == 0

    def add_item(self, product_id, quantity=1):
        self.data["items"].append({
            "product_id": product_id,
            "quantity": quantity,
        })
        self.session["cart"] = self.data
        self.session.modified = True

等用户点击结账时,再根据购物车创建 Checkout:

def start_checkout(request):
    cart = Cart(request.session)

    if cart.is_empty:
        raise ValueError("Cannot checkout an empty cart")

    checkout = Checkout.objects.create(
        state=Checkout.State.STARTED,
    )

    request.session["checkout_id"] = str(checkout.pk)
    return checkout

实际项目还需要把商品快照、价格、优惠和应付金额写入数据库,以免后续商品价格变化导致订单金额漂移。

并发和支付回调需要特别处理

支付流程不能只依赖一次 HTTP 请求。用户可能重复点击,支付平台也可能重复发送 webhook。状态转换需要具备幂等性,并在必要时使用数据库锁:

from django.db import transaction


@transaction.atomic
def handle_payment_authorized(checkout_id):
    checkout = (
        Checkout.objects
        .select_for_update()
        .get(pk=checkout_id)
    )

    if checkout.state == Checkout.State.ORDER_PLACED:
        return checkout

    if checkout.state == Checkout.State.PAYMENT_AUTHORIZED:
        return checkout

    checkout.mark_payment_authorized()
    return checkout

这段代码只说明并发控制方式。完整实现还要验证支付平台签名、商户订单号、支付金额、币种,以及支付渠道返回的唯一交易编号。

创建订单和更新状态通常也应放在同一个数据库事务中,避免状态已经变成 order_placed,订单记录却没有创建成功。

还需要注意的边界

  • 不要把 Django session 当作支付和订单的最终数据源。
  • 不要用模块级对象保存某个用户的当前状态。
  • 可以直接从数据推导的状态,无需重复存储。
  • 状态转换名称应表达业务事件,例如 begin_payment(),不要使用含义模糊的 next()。
  • 终止状态也要明确定义,例如 cancelled、expired 和 payment_failed。
  • 如果状态变化伴随发送邮件、扣减库存等操作,需要处理事务失败和重复执行。
  • 支付成功后,不应继续修改原购物车来表示订单,而应创建独立的订单快照。
  • session 中保存 Enum 或自定义对象可能受到 serializer 限制,保存字符串、数字和普通字典更稳妥。

状态机可以作为独立的规则组件,但当前状态应该保存在对应的 Checkout 或 Order 实例中。Cart 只管理购物车内容,空与非空由内容计算。一旦进入不可丢失的结账和支付阶段,就应把流程交给持久化到数据库的业务对象。

备注:内容仅供参考。