PAYATHON 2026

Prestashop 外部支付如何将 cart 转换为 order

支付老李

结论

你的理解基本正确:用户从支付网关返回时,Cart 不会自动变成 Order。支付模块必须调用 validateOrder() 创建订单,拿到有效的 id_order 后,才能跳转到 order-confirmation 页面。

外部支付流程还要注意两件事:

  • 不能只根据浏览器带回的参数判断支付成功,必须向支付网关验签或查询交易状态。
  • 通常更稳妥的方式是在跳转到网关前创建一个“等待支付”的订单,再通过服务器回调更新订单状态。这样即使用户付款后关闭页面,没有返回商店,订单也能正常入账。

Cart 与 Order 的关系

在 PrestaShop 1.7 中:

  • Cart 保存购物车及结账上下文。
  • 调用 PaymentModule::validateOrder() 后才会创建正式的 Order。
  • order-confirmation controller 只展示订单确认结果,不会把购物车转换成订单。
  • $this->module->currentOrder 通常要在 validateOrder() 成功执行后才会赋值。

所以,在调用 validateOrder() 之前,下面这个参数通常没有可用值:

$this->module->currentOrder

不能先跳转到 order-confirmation,再让这个页面创建订单。正确顺序是先验证支付结果,再创建订单、取得订单 ID,最后跳转。

两种支付流程

外部支付一般有两种实现方式。

方案一:跳转前创建待支付订单

这种方式通常更稳妥。

  1. 用户选择支付方式。
  2. 模块检查购物车、客户、币种和金额。
  3. 调用网关接口创建支付 token。
  4. 调用 validateOrder(),并使用模块自定义的“等待支付”状态。
  5. 保存网关 token、交易号与订单的对应关系。
  6. 将用户重定向到支付网关。
  7. 网关通过服务器通知返回支付结果。
  8. 模块验签并查询交易状态。
  9. 将订单更新为“付款已接受”或“付款失败”。
  10. 用户返回商店后进入订单确认页。

这样做的好处是,即使用户没有回到商店,订单也已经存在,网关的异步通知仍能找到并更新对应订单。

跳转前创建订单的核心代码可以写成:

<?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()。流程如下:

  1. 用户被重定向到支付网关。
  2. 用户完成支付。
  3. 网关将用户带回 confirmation controller。
  4. 模块验签,并通过网关 API 查询实际交易状态。
  5. 确认付款成功后调用 validateOrder()。
  6. 取得 currentOrder。
  7. 跳转到 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

这样,订单创建就不依赖用户能否成功返回商店,延迟支付、失败支付和重复通知也更容易处理。

备注:内容仅供参考。