Commerce 2 中 onReturn 后如何保持订单当前状态?
结论
不能通过 onReturn() 的返回值阻止 Commerce 2 继续结账。onReturn() 是 void 方法。只要它正常结束,Commerce 就会认为返回处理成功,并进入下一个 checkout step。如果当前已经是最后一步,订单可能随即进入 completed 状态。
对于浏览器返回不等于支付成功、只能信任服务端通知的网关,通常有两种处理方式:
- 尚未收到有效通知时,让
onReturn()抛出PaymentGatewayException,中断结账流程。 - 对于异步支付,使用自定义 return route 通常更合适。客户返回后先进入“支付结果确认中”页面,payment 仍由
onNotify()创建或更新。
不要让一个没有实际处理逻辑的 onReturn() 正常结束,否则 Commerce 会继续结账。
为什么订单会自动进入下一状态
Commerce 的标准站外支付返回地址不只会调用支付网关插件的 onReturn(),还会恢复并继续 checkout flow。
因此,下面的实现虽然没有修改订单,Commerce 仍会认为返回处理成功:
public function onReturn(OrderInterface $order, Request $request) {
// Only write a log entry.
}
onReturn() 没有类似下面这样的控制接口:
return FALSE;
它实际上只有两种执行结果:
- 正常结束:继续 checkout flow。
- 抛出异常:返回处理失败,不进入下一个 checkout step。
问题并不是 onReturn() 显式修改了订单状态,而是它正常结束后,外层 checkout controller 会继续执行结账流程。
方案一:未确认时抛出异常
如果仍要使用 Commerce 提供的标准 return URL,可以在 onReturn() 中查询该订单是否已经收到可信的服务端通知。尚未确认时,抛出 PaymentGatewayException:
<?php
namespace Drupal\my_module\Plugin\Commerce\PaymentGateway;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Symfony\Component\HttpFoundation\Request;
public function onReturn(OrderInterface $order, Request $request) {
// 浏览器携带的参数只能用于关联请求,不能作为支付成功的依据。
$transaction_id = $request->query->get('transaction_id');
// 此处应查询由 onNotify() 写入的 payment 或本地交易记录。
$payment = $this->loadPaymentForTransaction($order, $transaction_id);
if (!$payment || !$this->isPaymentConfirmed($payment)) {
throw new PaymentGatewayException(
'The payment result has not yet been confirmed.'
);
}
// 已经由服务端通知确认,可以正常返回,让 Commerce 继续 checkout。
}
这里的 loadPaymentForTransaction() 和 isPaymentConfirmed() 只是示意方法,需要根据网关交易号、payment remote ID 和实际支付状态自行实现。
这种方式可以让结账流程停留在当前 checkout step,但 Commerce 通常会把异常作为支付错误展示给客户。异步通知出现正常延迟时,类似“支付失败”的提示可能不够友好。因此,这种方式更适合通知很快就能到达,或者允许客户稍后重试的场景。
方案二:使用自定义等待页面
如果支付平台允许单独配置浏览器返回地址,可以采用下面的流程:
浏览器返回
↓
自定义 return route
↓
显示“正在确认支付结果”
↓
轮询本地支付状态或提供刷新按钮
支付平台服务器
↓
Commerce notify URL
↓
onNotify()
↓
验证签名并创建或更新 payment
自定义 return controller 不应调用 Commerce 的标准 payment return controller,也不应执行订单状态转换。它只需要:
- 验证用于关联订单的签名或一次性 token。
- 记录客户已经返回。
- 查询 payment 是否已经由
onNotify()确认。 - 显示“处理中”“成功”或“失败”页面。
- 确认成功后,将客户安全地引导回合适的 checkout 页面。
示意路由:
my_module.payment_return:
path: '/payment/my-gateway/return/{commerce_order}'
defaults:
_controller: '\Drupal\my_module\Controller\PaymentReturnController::returnPage'
_title: 'Payment status'
requirements:
_custom_access: '\Drupal\my_module\Controller\PaymentReturnController::access'
示意控制器:
<?php
namespace Drupal\my_module\Controller;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
final class PaymentReturnController extends ControllerBase {
public function returnPage(
OrderInterface $commerce_order,
Request $request,
): array {
// 必须验证签名或一次性 token,不能仅凭订单 ID 授权访问。
$this->validateReturnRequest($commerce_order, $request);
$confirmed = $this->hasConfirmedPayment($commerce_order);
if (!$confirmed) {
return [
'#markup' => $this->t(
'Your payment is being confirmed. Please refresh this page shortly.'
),
'#cache' => ['max-age' => 0],
];
}
return [
'#markup' => $this->t('Your payment has been confirmed.'),
'#cache' => ['max-age' => 0],
];
}
}
如何把自定义 URL 传给支付平台,要根据网关的请求参数确定。有些平台分别使用 return_url、cancel_url 和 notify_url,也有平台使用其他名称。
onNotify() 应负责最终支付状态
支付平台的服务端通知才是可信来源,因此应由 onNotify() 负责创建或确认 payment:
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Symfony\Component\HttpFoundation\Request;
public function onNotify(Request $request) {
$payload = $request->getContent();
if (!$this->verifyNotification($payload, $request->headers->all())) {
throw new PaymentGatewayException('Invalid payment notification.');
}
$notification = $this->decodeNotification($payload);
$order = $this->loadOrderFromNotification($notification);
if (!$order) {
throw new PaymentGatewayException('The referenced order was not found.');
}
// 按网关交易号查找,避免重复通知创建多个 payment。
$payment = $this->loadPaymentByRemoteId(
$notification['transaction_id']
);
if (!$payment) {
$payment = $this->createPaymentForNotification(
$order,
$notification
);
}
$this->applyRemoteStatus($payment, $notification['status']);
}
实际代码需要根据所用 Commerce 版本提供的 payment storage、payment state 和 transition API 调整。示例中的辅助方法并不是 Commerce 核心 API,不能直接照搬。
需要特别注意的事项
通知可能早于浏览器返回
onNotify() 和浏览器 return 的先后顺序无法保证。实现时要兼容以下情况:
- notify 先到,return 后到。
- return 先到,notify 后到。
- 客户关闭浏览器,只收到 notify。
- notify 被重复发送。
- 客户多次刷新返回页面。
因此,onNotify() 必须是幂等的,通常可以把网关的 transaction ID 或 remote payment ID 作为唯一标识。
不要信任浏览器返回的支付状态
查询字符串、表单字段和订单 ID 都可能被修改。浏览器返回的数据最多只能用于定位交易,不能单独触发以下操作:
- 创建成功 payment。
- 将 payment 标记为 completed。
- 将订单推进到 completed。
- 扣减余额或发货。
不建议直接修改 checkout controller
覆盖核心 controller、强行拦截订单 transition 或修改核心 route,都会增加后续升级成本,也可能影响其他支付网关。应在网关协议层明确区分三类操作:
- 客户返回:展示状态。
- 服务端通知:确认支付。
- checkout 推进:只有确认结果满足业务条件后才能发生。
标准 onReturn() 的语义是返回处理成功后继续结账,它不是单纯的通知入口。如果客户返回时支付状态仍未确定,可以抛出异常来中断流程,也可以改用自定义等待页面。最终的 payment 状态应由 onNotify() 处理。
备注:内容仅供参考。
