Shopware 6 订单交易关联了错误的 payment method
结论
Shopware 6 的支付方式和支付状态都属于具体的 order_transaction,并不直接存储在 order 上。state_machine_history 记录的是状态转换历史,不能用来判断交易的当前状态。
通常可以将创建时间最新的 transaction 视为订单的最后一次支付尝试,但数据库中没有“当前交易”这一正式标记。后台修改支付状态时,修改的是请求中明确指定的 transaction ID,不一定是 created_at 最新的记录。
插件应按以下方式处理:
- 加载
transactions.paymentMethod和transactions.stateMachineState。 - 按
createdAt排序,并使用id作为稳定的第二排序条件。 - 明确选取最后一个 transaction。
- 直接读取该 transaction 的
paymentMethod和stateMachineState。 - 更新状态时,将同一个 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,而不是根据历史表反推当前交易。
备注:内容仅供参考。
