Prestashop 外部支付如何将 cart 转换为 order
结论
你的理解基本正确:用户从支付网关返回时,Cart 不会自动变成 Order。支付模块必须调用 validateOrder() 创建订单,拿到有效的 id_order 后,才能跳转到 order-confirmation 页面。
外部支付流程还要注意两件事:
- 不能只根据浏览器带回的参数判断支付成功,必须向支付网关验签或查询交易状态。
- 通常更稳妥的方式是在跳转到网关前创建一个“等待支付”的订单,再通过服务器回调更新订单状态。这样即使用户付款后关闭页面,没有返回商店,订单也能正常入账。
Cart 与 Order 的关系
在 PrestaShop 1.7 中:
Cart保存购物车及结账上下文。- 调用
PaymentModule::validateOrder()后才会创建正式的Order。 order-confirmationcontroller 只展示订单确认结果,不会把购物车转换成订单。$this->module->currentOrder通常要在validateOrder()成功执行后才会赋值。
所以,在调用 validateOrder() 之前,下面这个参数通常没有可用值:
$this->module->currentOrder
不能先跳转到 order-confirmation,再让这个页面创建订单。正确顺序是先验证支付结果,再创建订单、取得订单 ID,最后跳转。
两种支付流程
外部支付一般有两种实现方式。
方案一:跳转前创建待支付订单
这种方式通常更稳妥。
- 用户选择支付方式。
- 模块检查购物车、客户、币种和金额。
- 调用网关接口创建支付 token。
- 调用
validateOrder(),并使用模块自定义的“等待支付”状态。 - 保存网关 token、交易号与订单的对应关系。
- 将用户重定向到支付网关。
- 网关通过服务器通知返回支付结果。
- 模块验签并查询交易状态。
- 将订单更新为“付款已接受”或“付款失败”。
- 用户返回商店后进入订单确认页。
这样做的好处是,即使用户没有回到商店,订单也已经存在,网关的异步通知仍能找到并更新对应订单。
跳转前创建订单的核心代码可以写成:
<?php
$cart = $this->context->cart;
$customer = new Customer((int) $cart->id_customer);
$currency = new Currency((int) $cart->id_currency);
if (!Validate::isLoadedObject($cart)
|| !Validate::isLoadedObject($customer)
|| !$cart->id_customer
) {
throw new PrestaShopException('Invalid cart or customer.');
}
$total = (float) $cart->getOrderTotal(true, Cart::BOTH);
if (!$this->module->active) {
throw new PrestaShopException('Payment module is disabled.');
}
$this->module->validateOrder(
(int) $cart->id,
(int) Configuration::get('MYMODULE_OS_PENDING'),
$total,
$this->module->displayName,
null,
array(),
(int) $currency->id,
false,
$customer->secure_key
);
$idOrder = (int) $this->module->currentOrder;
if (!$idOrder) {
throw new PrestaShopException('Unable to create order.');
}
MYMODULE_OS_PENDING 应该是模块安装时创建的自定义“等待网关支付”状态。除非支付方式确实是银行转账,否则不要把 PS_OS_BANKWIRE 当作通用的等待状态。
方案二:支付成功后创建订单
也可以等网关返回成功结果后,再在 confirmation controller 中调用 validateOrder()。流程如下:
- 用户被重定向到支付网关。
- 用户完成支付。
- 网关将用户带回
confirmationcontroller。 - 模块验签,并通过网关 API 查询实际交易状态。
- 确认付款成功后调用
validateOrder()。 - 取得
currentOrder。 - 跳转到
order-confirmation。
示例:
<?php
$cart = new Cart((int) Tools::getValue('id_cart'));
if (!Validate::isLoadedObject($cart)) {
throw new PrestaShopException('Cart not found.');
}
$customer = new Customer((int) $cart->id_customer);
$currency = new Currency((int) $cart->id_currency);
if (!Validate::isLoadedObject($customer)) {
throw new PrestaShopException('Customer not found.');
}
/*
* 不能只相信 Tools::getValue('status')。
* 此处应执行网关验签,并根据 token 或 transaction ID
* 向支付网关查询交易状态和实际支付金额。
*/
$payment = $this->verifyPaymentWithGateway();
if (!$payment->isPaid()) {
Tools::redirect(
$this->context->link->getPageLink(
'order',
true,
null,
array('step' => 1)
)
);
}
$expectedTotal = (float) $cart->getOrderTotal(true, Cart::BOTH);
$paidTotal = (float) $payment->getAmount();
if (abs($expectedTotal - $paidTotal) > 0.01) {
throw new PrestaShopException('Paid amount does not match cart total.');
}
/*
* 防止用户刷新返回页,或者网关重复发送通知时重复创建订单。
*/
$idOrder = (int) Order::getIdByCartId((int) $cart->id);
if (!$idOrder) {
$this->module->validateOrder(
(int) $cart->id,
(int) Configuration::get('PS_OS_PAYMENT'),
$expectedTotal,
$this->module->displayName,
null,
array(),
(int) $currency->id,
false,
$customer->secure_key
);
$idOrder = (int) $this->module->currentOrder;
}
if (!$idOrder) {
throw new PrestaShopException('Order could not be created.');
}
Tools::redirect(
$this->context->link->getPageLink(
'order-confirmation',
true,
null,
array(
'id_cart' => (int) $cart->id,
'id_module' => (int) $this->module->id,
'id_order' => $idOrder,
'key' => $customer->secure_key,
)
)
);
这里使用 PS_OS_PAYMENT 的前提是,已经通过可信渠道确认交易成功。如果支付还在处理中,就应使用模块自己的等待状态。
不要把 returnUrl 当作支付通知
returnUrl 只是用户浏览器的返回地址,不能作为可靠的支付结果来源。实际流程中可能出现以下情况:
- 用户付款后直接关闭页面。
- 浏览器重定向被网络或插件拦截。
- 用户刷新返回页面。
- URL 参数被人为修改。
- 某些支付方式无法立即得到最终状态。
如果网关支持 webhook、notify URL 或 server callback,应以服务器通知为主要依据:
支付网关服务器
→ 模块 callback controller
→ 验签并查询交易
→ 创建订单或更新订单状态
浏览器的 returnUrl 主要用于展示结果:
用户浏览器
→ return controller
→ 查找当前订单
→ 跳转到 order-confirmation
服务器回调 controller 通常还要启用 SSL。是否关闭 CSRF 检查或改用专门的签名认证,应根据网关协议决定。不能为了省事而跳过网关验签。
处理重复请求
网关通知、浏览器刷新和网络重试,都可能让同一个请求执行多次。调用 validateOrder() 前,至少要检查该购物车是否已经生成订单:
$idOrder = (int) Order::getIdByCartId((int) $cart->id);
if (!$idOrder) {
$this->module->validateOrder(
(int) $cart->id,
(int) Configuration::get('PS_OS_PAYMENT'),
$total,
$this->module->displayName,
null,
array(),
(int) $cart->id_currency,
false,
$customer->secure_key
);
$idOrder = (int) $this->module->currentOrder;
}
还需要根据网关交易号检查唯一性。只检查 id_cart 无法防住所有并发请求,因为 webhook 和浏览器返回可能几乎同时到达。最好在模块交易表中为 transaction ID 建立唯一约束。
核对金额和订单状态
创建或确认订单前,至少要检查:
- 网关交易签名是否有效。
- 交易状态是否确实为成功。
- 网关商户号是否属于当前商店。
- transaction ID 是否已经处理。
- 支付币种是否与购物车一致。
- 支付金额是否与购物车应付金额一致。
id_cart、token 和网关交易之间是否存在可信关联。- 客户
secure_key是否来自数据库,而不是直接取自请求参数。
如果订单已经在跳转前创建,回调中就不应再次调用 validateOrder()。此时应该加载现有订单并更新状态,例如:
<?php
$order = new Order($idOrder);
if (!Validate::isLoadedObject($order)) {
throw new PrestaShopException('Order not found.');
}
$currentState = (int) $order->getCurrentState();
$paidState = (int) Configuration::get('PS_OS_PAYMENT');
if ($currentState !== $paidState) {
$order->setCurrentState($paidState);
}
实际模块中还要保存网关 transaction ID,并根据需要写入 OrderPayment 或模块自己的交易记录表。
最终建议
你的顺序判断是对的。如果选择“支付完成后创建订单”,就要在 confirmation controller 中先可靠地验证支付,再调用 validateOrder()。拿到 currentOrder 后,才能跳转到 order-confirmation。
对于支持异步通知的外部支付,更适合采用下面的结构:
创建网关 token
→ 创建待支付订单
→ 跳转到网关
→ webhook 验签并更新订单状态
→ returnUrl 查找订单
→ 跳转到 order-confirmation
这样,订单创建就不依赖用户能否成功返回商店,延迟支付、失败支付和重复通知也更容易处理。
备注:内容仅供参考。
