PrestaShop 1.6 外部支付后如何确认订单
结论
外部支付完成后,不能再依赖原有的 PrestaShop session 或当前购物车确认订单。建议按以下流程处理:
- 支付平台通过服务端回调发送支付结果。
- 模块验证回调签名、金额和交易状态。
- 根据预先保存的
id_cart重新加载购物车。 - 调用
validateOrder()创建订单,同时确保重复回调不会重复下单。 - 用户返回商城后,通过包含
id_cart、id_module、id_order和客户secure_key的完整绝对 URL 跳转到order-confirmation页面。
调用 validateOrder() 后,购物车不再是当前可结算购物车,这是正常的。订单确认页通过订单、购物车 ID 和安全密钥识别订单,不要求原来的购物车 session 继续有效。
当前实现的问题
缺少 key 参数
PrestaShop 1.6 的订单确认页通常需要客户的 secure_key:
key={customer_secure_key}
如果只传递 id_cart、id_module 和 id_order,一旦 session 失效或无法恢复用户身份,确认页就不能验证订单是否属于当前客户,可能直接显示“页面不可用”。
返回地址不是可靠的绝对 URL
下面的写法通常只能得到相对于商城根目录的路径:
PS_BASE_URI . 'order-confirmation.php?...'
外部支付网关通常需要完整地址,例如:
https://shop.example.com/index.php?controller=order-confirmation&...
应使用 PrestaShop 的 Link 对象生成 URL,这样才能兼容 HTTPS、子目录安装、URL 重写和多店铺配置。
查询参数疑似拼写错误
示例中出现了:
&id;_module=
&id;_order=
正确的参数名应为:
&id_module=
&id_order=
如果实际生成的 URL 确实包含分号,控制器将无法读取 id_module 和 id_order。如果分号只是 HTML 转义或复制过程造成的显示问题,则需要检查最终发送给支付平台的原始 URL。
把浏览器返回当成支付确认不安全
用户从支付页面跳回商城,只能证明浏览器访问了返回地址,不能证明付款已经成功。攻击者也可以直接构造这个 URL。
订单状态应以支付平台的服务端通知为准。如果网关没有独立的通知接口,至少要在返回控制器中调用网关的订单查询 API,核实结果后再决定是否创建或更新订单。
推荐处理流程
发起支付前,应保存以下关联信息:
- PrestaShop 的
id_cart - 支付平台交易号
- 订单金额和币种
- 模块内部生成的随机标识
- 必要时保存客户 ID 或
secure_key
收到支付平台回调后:
- 验证签名、时间戳和交易状态。
- 根据商户交易号查找
id_cart。 - 使用
new Cart($idCart)重新加载购物车。 - 检查该购物车是否已经生成订单。
- 如果尚未生成订单,调用
validateOrder()。 - 如果订单已经存在,直接读取现有订单,避免重复创建。
- 按照支付平台的协议返回成功响应。
用户浏览器返回时:
- 如果订单已经创建,跳转到订单确认页。
- 如果服务端通知还未到达,可以暂时显示“支付结果确认中”,并继续查询后台结果。
- 不要因为 URL 中带有“支付成功”参数就调用
validateOrder()。
生成确认页 URL
validateOrder() 执行成功后,可以这样生成确认地址:
<?php
$idCart = (int) $cart->id;
$idOrder = (int) $this->currentOrder;
$customer = new Customer((int) $cart->id_customer);
$confirmationUrl = $this->context->link->getPageLink(
'order-confirmation',
true,
null,
array(
'id_cart' => $idCart,
'id_module' => (int) $this->id,
'id_order' => $idOrder,
'key' => $customer->secure_key,
)
);
Tools::redirect($confirmationUrl);
PrestaShop 1.6 不同小版本的 getPageLink() 对数组参数的处理可能不同。如果当前版本不能正确处理数组,可以显式生成查询字符串:
<?php
$params = http_build_query(array(
'id_cart' => (int) $cart->id,
'id_module' => (int) $this->id,
'id_order' => (int) $this->currentOrder,
'key' => $customer->secure_key,
));
$confirmationUrl = $this->context->link->getPageLink(
'order-confirmation',
true,
null,
$params
);
Tools::redirect($confirmationUrl);
生成的地址应类似:
https://shop.example.com/index.php?controller=order-confirmation&id_cart=123&id_module=45&id_order=678&key=customer_secure_key
不要手动固定为 order-confirmation.php。使用 getPageLink() 生成地址,可以减少 URL 重写、商城子目录和 SSL 配置引起的问题。
回调控制器示例
模块可以提供一个前台控制器,用来处理支付平台发送的服务端回调:
<?php
class MyPaymentCallbackModuleFrontController extends ModuleFrontController
{
public $ssl = true;
public function postProcess()
{
$transactionId = (string) Tools::getValue('transaction_id');
$signature = (string) Tools::getValue('signature');
if (!$this->module->verifyGatewayCallback($_POST, $signature)) {
header('HTTP/1.1 400 Bad Request');
exit('invalid signature');
}
$payment = $this->module->findPaymentByTransactionId($transactionId);
if (!$payment) {
header('HTTP/1.1 404 Not Found');
exit('payment not found');
}
$cart = new Cart((int) $payment['id_cart']);
if (!Validate::isLoadedObject($cart)) {
header('HTTP/1.1 404 Not Found');
exit('cart not found');
}
if (!$this->module->validateGatewayAmount($payment, $cart)) {
header('HTTP/1.1 400 Bad Request');
exit('invalid amount');
}
if (!$this->module->checkOrderExists((int) $cart->id)) {
$customer = new Customer((int) $cart->id_customer);
$this->module->validateOrder(
(int) $cart->id,
(int) Configuration::get('PS_OS_PAYMENT'),
(float) $payment['amount'],
$this->module->displayName,
null,
array(
'transaction_id' => $transactionId,
),
(int) $cart->id_currency,
false,
$customer->secure_key
);
}
exit('OK');
}
}
verifyGatewayCallback()、findPaymentByTransactionId() 和 validateGatewayAmount() 都属于模块自身的业务逻辑,不能直接保留为空实现。它们至少需要验证支付平台签名、交易状态、金额、币种和商户订单号。
防止重复订单
支付平台可能重复发送同一条通知,用户也可能多次刷新返回页面,因此回调必须具备幂等性。
调用 validateOrder() 前,应先检查购物车是否已经生成订单:
<?php
if (!$this->module->checkOrderExists((int) $cart->id)) {
$this->module->validateOrder(/* ... */);
}
如果订单已经存在,可以根据购物车 ID 读取订单:
<?php
$idOrder = (int) Order::getOrderByCartId((int) $cart->id);
if (!$idOrder) {
// 尚未创建订单
}
可用的具体方法可能因 PrestaShop 1.6 小版本而异,应以当前安装版本中的 Order 和 PaymentModule 类为准。无论采用哪种方法,都要保证同一个 id_cart 只能成功创建一次订单。
用户返回控制器
用户浏览器的返回地址和服务端通知地址最好分开。返回控制器只负责查找已有订单并完成跳转:
<?php
class MyPaymentReturnModuleFrontController extends ModuleFrontController
{
public $ssl = true;
public function initContent()
{
parent::initContent();
$idCart = (int) Tools::getValue('id_cart');
$cart = new Cart($idCart);
if (!Validate::isLoadedObject($cart)) {
Tools::redirect('index.php?controller=order-history');
}
$idOrder = (int) Order::getOrderByCartId($idCart);
if (!$idOrder) {
// 服务端通知可能尚未到达。
// 可显示“支付结果确认中”页面,或通过网关查询接口核实状态。
$this->setTemplate('payment-pending.tpl');
return;
}
$customer = new Customer((int) $cart->id_customer);
$url = $this->context->link->getPageLink(
'order-confirmation',
true,
null,
http_build_query(array(
'id_cart' => $idCart,
'id_module' => (int) $this->module->id,
'id_order' => $idOrder,
'key' => $customer->secure_key,
))
);
Tools::redirect($url);
}
}
返回地址还应包含模块生成的随机 token,或者通过模块数据库中的交易记录建立关联,避免只依赖可以枚举的 id_cart。
注意事项
- 只有在可信的支付结果通过验证后,才能调用
validateOrder()。 - 不要依赖
Context::getContext()->cart。完成外部跳转后,应使用已保存的id_cart重新创建Cart。 - 确认页 URL 必须包含正确的
key,其值应为客户的secure_key。 - 回调 URL 和返回 URL 都应使用完整的 HTTPS 地址。
- 金额需要与原购物车金额核对,同时考虑币种、税费、折扣和支付平台使用的最小货币单位。
- 回调处理必须能接受重复通知,不能重复创建订单或重复变更状态。
- 不要把
secure_key当作支付签名。它用于 PrestaShop 的订单访问校验,不能替代支付平台的签名验证。 - 如果支付平台先让用户返回,随后才发送服务端通知,返回页应允许短暂显示“确认中”,不要把尚未收到通知直接视为支付失败。
备注:内容仅供参考。
