PAYATHON 2026

PrestaShop 1.6 外部支付后如何确认订单

支付阿杰

结论

外部支付完成后,不能再依赖原有的 PrestaShop session 或当前购物车确认订单。建议按以下流程处理:

  1. 支付平台通过服务端回调发送支付结果。
  2. 模块验证回调签名、金额和交易状态。
  3. 根据预先保存的 id_cart 重新加载购物车。
  4. 调用 validateOrder() 创建订单,同时确保重复回调不会重复下单。
  5. 用户返回商城后,通过包含 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

收到支付平台回调后:

  1. 验证签名、时间戳和交易状态。
  2. 根据商户交易号查找 id_cart。
  3. 使用 new Cart($idCart) 重新加载购物车。
  4. 检查该购物车是否已经生成订单。
  5. 如果尚未生成订单,调用 validateOrder()。
  6. 如果订单已经存在,直接读取现有订单,避免重复创建。
  7. 按照支付平台的协议返回成功响应。

用户浏览器返回时:

  • 如果订单已经创建,跳转到订单确认页。
  • 如果服务端通知还未到达,可以暂时显示“支付结果确认中”,并继续查询后台结果。
  • 不要因为 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 的订单访问校验,不能替代支付平台的签名验证。
  • 如果支付平台先让用户返回,随后才发送服务端通知,返回页应允许短暂显示“确认中”,不要把尚未收到通知直接视为支付失败。

备注:内容仅供参考。