PAYATHON 2026

Shopware 6 订单交易关联了错误的 payment method

支付老李

结论

Shopware 6 的支付方式和支付状态都属于具体的 order_transaction,并不直接存储在 order 上。state_machine_history 记录的是状态转换历史,不能用来判断交易的当前状态。

通常可以将创建时间最新的 transaction 视为订单的最后一次支付尝试,但数据库中没有“当前交易”这一正式标记。后台修改支付状态时,修改的是请求中明确指定的 transaction ID,不一定是 created_at 最新的记录。

插件应按以下方式处理:

  1. 加载 transactions.paymentMethod 和 transactions.stateMachineState。
  2. 按 createdAt 排序,并使用 id 作为稳定的第二排序条件。
  3. 明确选取最后一个 transaction。
  4. 直接读取该 transaction 的 paymentMethod 和 stateMachineState。
  5. 更新状态时,将同一个 transaction ID 传给 Shopware 的状态机接口。

数据之间的关系

订单本身只有订单状态:

order.state_id

支付尝试及其状态位于:

order_transaction.payment_method_id
order_transaction.state_id

这里需要区分三种状态:

  • order.stateMachineState:订单状态。
  • order.deliveries[*].stateMachineState:配送状态。
  • order.transactions[*].stateMachineState:支付状态。

问题中的查询加载了:

->addAssociation('stateMachineState')

这里加载的是订单状态,而不是 transaction 的支付状态。支付状态应通过以下代码读取:

$order->getTransactions()
    ->last()
    ->getStateMachineState();

为什么状态历史出现在较早的 transaction 上

Shopware 每次转换支付状态时,操作的都是一个确定的 order_transaction。后台请求通常会包含 transaction ID,逻辑类似:

POST /api/_action/order_transaction/{transactionId}/state/{transition}

状态机不会先查询订单,再自动选择 created_at 最新的 transaction。请求传入哪个 ID,它就更新哪个 transaction,并为该 ID 写入 state_machine_history。

因此,较早的 transaction 上有 7 条历史记录,只能说明后台或其他调用方一直在操作它。这并不能证明 Shopware 将最新 transaction 的状态错误地写到了旧 transaction 上。

手动修改状态时,可以在浏览器开发者工具的 Network 面板中检查请求 URL,确认后台实际提交的是:

83042D493DC04A4290BE8D7F523DE32F

还是:

911DF5D15BC542E598C2F11AECDAFD61

如果后台提交的是较早的 ID,原因可能与所用的 Shopware 版本、Administration 选择 transaction 的逻辑、第三方扩展或订单数据的生成方式有关。没有具体版本和请求内容时,不能只根据 state_machine_history 判断这是 Shopware 核心缺陷。

正确加载最后一个 transaction

可以一次加载 transaction 关联、支付方式和状态:

use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;

$criteria = new Criteria();

$criteria->addFilter(
    new EqualsFilter('orderNumber', $orderNumber)
);

$transactions = $criteria->getAssociation('transactions');

$transactions->addAssociation('paymentMethod');
$transactions->addAssociation('stateMachineState');

$transactions->addSorting(
    new FieldSorting('createdAt', FieldSorting::ASCENDING)
);

$transactions->addSorting(
    new FieldSorting('id', FieldSorting::ASCENDING)
);

$order = $this->orderRepository
    ->search($criteria, $context)
    ->first();

if ($order === null) {
    throw new \RuntimeException(
        sprintf('Order "%s" was not found.', $orderNumber)
    );
}

$transaction = $order->getTransactions()?->last();

if ($transaction === null) {
    throw new \RuntimeException(
        sprintf('Order "%s" has no transaction.', $orderNumber)
    );
}

$paymentMethod = $transaction->getPaymentMethod();
$paymentState  = $transaction->getStateMachineState();

$transactionId    = $transaction->getId();
$paymentMethodName = $paymentMethod?->getTranslation('name');
$paymentStateName  = $paymentState?->getTechnicalName();

这段代码使用升序排序,然后调用 last()。也可以改为降序排序并调用 first(),但排序方向必须与集合的读取方式一致。

使用 id 作为第二排序字段,可以在多个 transaction 的 createdAt 相同时得到稳定的结果。不过,transaction ID 通常是随机 UUID,只能用于稳定排序,不能表示业务上的先后顺序。

更新同一个 transaction 的支付状态

不要根据订单 ID 猜测应该更新哪条 transaction。先按照同一套规则确定 transaction,再把它的 ID 交给状态机:

use Shopware\Core\System\StateMachine\StateMachineRegistry;
use Shopware\Core\System\StateMachine\Transition;

$transition = new Transition(
    StateMachineRegistry::STATE_MACHINE_ORDER_TRANSACTION,
    $transaction->getId(),
    'paid',
    'stateId'
);

$this->stateMachineRegistry->transition(
    $transition,
    $context
);

可用的 transition 取决于 transaction 的当前状态和 Shopware 的状态机配置。如果当前状态不允许直接执行 paid,状态机会拒绝这次转换。不要通过 repository 直接修改 stateId 来绕过状态机。

ERP 同步时,建议保存明确的 order_transaction.id。后续的支付确认、退款或失败通知都应针对这个 ID 处理,不要每次都重新根据时间推断。

不要把 state_machine_history 当作当前状态

当前支付状态应通过以下代码读取:

$transaction->getStateMachineState();

在数据库中,对应的字段是:

order_transaction.state_id

state_machine_history 只记录已经执行的状态转换。没有历史记录可能有以下几种原因:

  • transaction 从未发生状态转换。
  • transaction 仍处于创建时的初始状态。
  • 状态曾绕过状态机直接写入。
  • 实际操作的是另一条 transaction。

因此,“查不到历史”不等于“没有支付状态”。

“最后创建”不一定等于“最终有效”

按 createdAt 选择最后一条 transaction,是 Shopware 项目中判断“最新支付尝试”的常见方式。不过,这只是一条业务规则,order_transaction 表中并没有正式的 current 或 active 标记。

最新的 transaction 可能来自一次失败、取消或尚未完成的重试,而较早的 transaction 可能已经支付成功。如果插件需要查找“最终成功支付的 transaction”,就应根据业务目标筛选状态,不能只看创建时间:

$paidTransaction = $order->getTransactions()?->filter(
    static function ($transaction): bool {
        return $transaction
            ->getStateMachineState()
            ?->getTechnicalName() === 'paid';
    }
)->last();

集成时可以按以下规则处理:

  • “客户最后选择的支付方式”:读取最新 transaction 的 paymentMethod。
  • “最新支付尝试的状态”:读取同一 transaction 的 stateMachineState。
  • “实际完成付款的交易”:优先查找处于 paid 等成功状态的 transaction。
  • ERP 发起状态更新:使用创建支付流程时保存的 transaction ID。
  • 多次支付重试:分别保存和处理每个 transaction,不要混淆订单级状态与支付级状态。

对于给出的两条记录,如果业务规则明确规定“最后创建的 transaction 就是当前支付尝试”,就应该读取 911DF5D15BC542E598C2F11AECDAFD61 的 payment_method_id 和 state_id。如果后台修改的却是 83042D493DC04A4290BE8D7F523DE32F,应先检查后台状态转换请求中的 transaction ID,而不是根据历史表反推当前交易。

备注:内容仅供参考。