PAYATHON 2026

选择 offline payment 后如何将 Sylius 订单标记为 paid?

支付小周

结论

不要直接修改订单的 paymentState,也不要依赖 sylius_order_payment 的 complete 回调来完成线下付款。

应当对订单关联的 Payment 实体执行 sylius_payment 状态机的 complete transition。付款完成后,Sylius 会根据订单中各笔付款的状态重新计算订单的付款状态。已完成的付款覆盖全部应付金额后,订单会变为 paid。

为什么原配置没有触发

这里有两个不同层次的状态机:

  • sylius_payment:管理单笔 Payment,状态包括 new、processing、completed、failed。
  • sylius_order_payment:管理订单汇总后的付款状态,包括 awaiting_payment、partially_paid、paid。

sylius_order_payment 会参与处理,但它负责维护订单汇总后的付款状态。选择 offline payment method 只会创建付款或确定付款方式,不会自动把对应的 Payment 标记为 completed,所以没有事件推动订单进入 paid。

付款完成所用的 transition 名称是 complete,它属于 sylius_payment graph。订单的汇总付款状态通常由 pay 等内部 transition 更新,不能把单笔付款状态机和订单付款汇总状态机混为一谈。

正确完成线下付款

确认收到线下款项后,取得订单对应的 Payment,再执行付款状态机的 complete transition:

<?php

namespace AppBundle\Payment;

use SM\Factory\FactoryInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Payment\PaymentTransitions;

final class OfflinePaymentCompleter
{
    private $stateMachineFactory;

    public function __construct(FactoryInterface $stateMachineFactory)
    {
        $this->stateMachineFactory = $stateMachineFactory;
    }

    public function complete(OrderInterface $order): void
    {
        $payment = $order->getLastPayment();

        if (null === $payment) {
            throw new \RuntimeException('The order does not have a payment.');
        }

        $stateMachine = $this->stateMachineFactory->get(
            $payment,
            PaymentTransitions::GRAPH
        );

        if (!$stateMachine->can(PaymentTransitions::TRANSITION_COMPLETE)) {
            throw new \LogicException(sprintf(
                'Payment cannot be completed from state "%s".',
                $payment->getState()
            ));
        }

        $stateMachine->apply(PaymentTransitions::TRANSITION_COMPLETE);
    }
}

服务配置可以这样写:

services:
    app.payment.offline_payment_completer:
        class: AppBundle\Payment\OfflinePaymentCompleter
        arguments:
            - '@sm.factory'

调用后,还需要通过当前请求中的 Doctrine manager 执行 flush():

$this->offlinePaymentCompleter->complete($order);
$this->orderManager->flush();

具体注入 ObjectManager、订单 manager,还是其他持久化服务,可按项目现有结构选择。

如果确实需要 state-machine callback

如果需要在某笔付款完成后执行额外的业务逻辑,应把 callback 配置在 sylius_payment,而不是 sylius_order_payment:

winzou_state_machine:
    sylius_payment:
        callbacks:
            after:
                app_after_offline_payment_completed:
                    on: ['complete']
                    do:
                        - '@AppBundle\Payment\StateMachine\Callback\CallbackClass'
                        - 'afterPaymentCompleted'
                    args: ['object']

这里传入的 object 是 Payment,不是 Order:

<?php

namespace AppBundle\Payment\StateMachine\Callback;

use Sylius\Component\Payment\Model\PaymentInterface;

final class CallbackClass
{
    public function afterPaymentCompleted(PaymentInterface $payment): void
    {
        $order = $payment->getOrder();

        // 在这里执行额外业务逻辑。
        // 通常不需要手动调用 $order->setPaymentState('paid')。
    }
}

这个 callback 只能响应已经发生的 complete transition,不能让付款自动进入 completed。控制器、命令、后台操作或事件监听器仍需显式执行该 transition。

应该在什么时候执行

offline payment 通常用于银行转账、现金、支票等需要异步确认的付款方式。客户选择这种支付方式,并不表示款项已经到账。更稳妥的流程是:

  1. 客户完成结账,订单进入等待付款状态。
  2. 后台人员或外部对账流程确认款项到账。
  3. 对相应的 Payment 执行 PaymentTransitions::TRANSITION_COMPLETE。
  4. Sylius 把订单的付款汇总状态更新为 paid。

如果业务明确规定订单创建后立即视为已付款,可以在结账完成后的事件监听器中调用上述 completer。但不建议监听过早触发的通用 Order created 事件,因为此时付款对象、支付方式或订单金额可能还没有完整确定。可用的结账完成事件名称,应以项目实际安装的 Sylius 1.2.x 配置为准。

注意事项

  • 不要直接调用 $order->setPaymentState('paid'),否则会绕过付款状态机、状态校验及相关回调。
  • 不要把选择 offline payment method 当作付款成功,除非业务规则明确如此规定。
  • 一个订单可能有多笔付款。只有已完成的付款覆盖订单全部应付金额后,订单才应进入 paid。
  • 执行 transition 前应调用 can(),以免重复完成付款,或从不允许的状态强制转换。
  • 如果付款完成后订单没有更新为 paid,应检查付款金额、付款所属订单、其他付款记录,以及项目是否覆盖了 Sylius 默认的状态机 callback 配置。

备注:内容仅供参考。