PAYATHON 2026

Prestashop payment module 如何接收外部 POST 请求?

支付老李

结论

建议使用 PrestaShop 的 ModuleFrontController 创建独立的支付回调入口,例如:

https://shop.example.com/module/qrmobilepay/notification

不要把回调写在模块主类、主题文件或普通 PHP 文件中。Front Controller 会正确加载 PrestaShop 环境、模块实例和数据库,也能适配友好 URL、HTTPS 以及不同的安装目录。

这个接口应按服务器回调来处理。它不能依赖登录状态、Cookie 或浏览器跳转,只能接受经过签名验证,并向 Provider 核验过的支付结果。

创建回调 Controller

假设模块技术名称为 qrmobilepay,创建以下文件:

modules/qrmobilepay/controllers/front/notification.php

基础实现如下:

<?php

class QrmobilepayNotificationModuleFrontController extends ModuleFrontController
{
    public $ssl = true;

    public function postProcess()
    {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            header('Allow: POST');
            $this->respond(405, 'Method Not Allowed');
        }

        $requestString = isset($_POST['requestString'])
            ? trim((string) $_POST['requestString'])
            : '';

        if ($requestString === '') {
            $this->respond(400, 'Missing requestString');
        }

        try {
            $result = $this->module->handleProviderNotification(
                $requestString,
                $_SERVER
            );

            if (!$result) {
                $this->respond(400, 'Invalid notification');
            }

            $this->respond(200, 'OK');
        } catch (Exception $exception) {
            PrestaShopLogger::addLog(
                'qrmobilepay callback error: ' . $exception->getMessage(),
                3
            );

            $this->respond(500, 'Internal Server Error');
        }
    }

    private function respond($statusCode, $message)
    {
        http_response_code($statusCode);
        header('Content-Type: text/plain; charset=utf-8');
        exit($message);
    }
}

Controller 只处理几件事:接收请求、检查请求方法、调用模块业务逻辑,并返回 Provider 要求的响应。签名验证、订单查询和状态更新应放在模块类或独立服务中。

获取正确的回调地址

不要手工拼接 /module/... 地址,应由 PrestaShop 生成 URL:

$callbackUrl = $this->context->link->getModuleLink(
    $this->name,
    'notification',
    array(),
    true
);

生成结果通常类似:

https://shop.example.com/module/qrmobilepay/notification

实际 URL 会受到 PrestaShop 安装目录、友好 URL 和路由配置的影响,因此应把 getModuleLink() 返回的完整地址配置到 Provider 后台。

如果访问 URL 时返回 404,可以清理 PrestaShop 缓存,再到“流量与 SEO”中重新保存一次友好 URL 设置。

正确读取 Provider 的数据

示例请求如下:

/?requestString=QP%3A1.0*BID%3AMYBID AM%3A6.00

需要先确认 Provider 的协议。这个例子中有两个容易混淆的问题:

  • 参数放在 URL 查询字符串中时,它就是 GET 参数,即使 HTTP 请求方法是 POST。
  • 只有 Provider 使用 application/x-www-form-urlencoded 提交表单时,参数才会出现在 $_POST 中。

建议让 Provider 把数据作为表单字段发送:

POST /module/qrmobilepay/notification HTTP/1.1
Content-Type: application/x-www-form-urlencoded

requestString=QP%3A1.0%2ABID%3AMYBID%2AAM%3A6.00

此时可以这样读取:

$requestString = isset($_POST['requestString'])
    ? (string) $_POST['requestString']
    : '';

如果 Provider 明确要求把参数放在 URL 查询字符串中,则应读取:

$requestString = isset($_GET['requestString'])
    ? (string) $_GET['requestString']
    : '';

Tools::getValue('requestString') 可以同时读取 GET 和 POST。不过,支付回调最好明确数据来自哪里,以免 GET 参数覆盖或混淆 POST 数据。

如果 Provider 发送 JSON:

$rawBody = file_get_contents('php://input');
$payload = json_decode($rawBody, true);

if (!is_array($payload) || !isset($payload['requestString'])) {
    $this->respond(400, 'Invalid JSON payload');
}

$requestString = (string) $payload['requestString'];

如果签名是根据原始请求体计算的,必须保留 $rawBody。不能先解码,再重新编码后验签。

不要根据示例猜测字段格式

URL 解码后,示例大致如下:

QP:1.0*BID:MYBID AM:6.00

BID、AM 分别表示什么,以及字段究竟使用 * 还是空格分隔,都必须以 Provider 的协议文档为准。不要只看这个示例就自行调用 explode('*', ...)。如果字段值本身包含分隔符,这种处理方式可能会解析错误。

更稳妥的方式是使用 Provider 提供的 SDK 或官方解析规则。解析完成后,还应确认以下内容:

  • 支付状态明确为成功。
  • Provider 交易号存在。
  • 商户订单号可以映射到本地订单。
  • 币种一致。
  • 金额与本地订单金额一致。
  • 同一交易尚未处理。

验证回调真实性

收到 requestString 并不能证明付款成功。任何人都可以向公开的回调 URL 发送 POST 请求。

如果 Provider 支持签名,应严格按照其协议验签。下面只是一个 HMAC 验签结构示例,其中的 Header 名称、算法和签名编码都必须替换为 Provider 的真实规范:

$rawBody = file_get_contents('php://input');

$receivedSignature = isset($_SERVER['HTTP_X_SIGNATURE'])
    ? (string) $_SERVER['HTTP_X_SIGNATURE']
    : '';

$expectedSignature = hash_hmac(
    'sha256',
    $rawBody,
    Configuration::get('QRMOBILEPAY_WEBHOOK_SECRET')
);

if (
    $receivedSignature === '' ||
    !hash_equals($expectedSignature, $receivedSignature)
) {
    $this->respond(401, 'Invalid signature');
}

如果 Provider 没有可靠的签名机制,应使用通知中的交易号调用 Provider 的服务端查询接口,然后根据查询结果更新订单。IP 白名单可以作为辅助措施,但不能作为唯一的验证手段。

更新 PrestaShop 订单状态

发起支付时,需要保存本地订单或购物车与 Provider 交易之间的映射。收到回调后,应通过商户订单号或交易号查找记录,不要依赖:

$this->context->cart
$this->context->customer
$this->context->cookie

Provider 的服务器请求通常不包含顾客 Cookie,因此这些上下文可能为空,也可能属于错误的会话。

处理逻辑可以放在模块类中:

public function handleProviderNotification($requestString, array $server)
{
    $payment = $this->parseProviderRequest($requestString);

    if (!$this->verifyProviderPayment($payment, $server)) {
        return false;
    }

    $order = Order::getByReference($payment['merchant_reference'])
        ->getFirst();

    if (!Validate::isLoadedObject($order)) {
        throw new Exception('Order not found');
    }

    if ((string) $order->id_currency !== (string) $payment['currency_id']) {
        throw new Exception('Currency mismatch');
    }

    if (abs((float) $order->total_paid - (float) $payment['amount']) > 0.00001) {
        throw new Exception('Amount mismatch');
    }

    if ($this->isTransactionProcessed($payment['transaction_id'])) {
        return true;
    }

    $this->saveTransaction($order, $payment);

    $history = new OrderHistory();
    $history->id_order = (int) $order->id;
    $history->changeIdOrderState(
        (int) Configuration::get('PS_OS_PAYMENT'),
        (int) $order->id
    );
    $history->addWithemail();

    return true;
}

parseProviderRequest()、verifyProviderPayment()、isTransactionProcessed() 和 saveTransaction() 需要根据 Provider 协议及模块的数据表实现。示例中的 currency_id 也只是用来展示结构。如果 Provider 返回 ISO 币种代码,应按照实际协议进行转换和比较。

如果支付前还没有创建 PrestaShop 订单,可以在验证成功后调用支付模块的 validateOrder()。如果订单已经存在,就不要在每次回调时重复调用 validateOrder(),而应更新现有订单的状态。

必须保证幂等

如果出现超时、网络错误,或者 Provider 没有收到预期响应,它可能会重复发送同一条通知。因此,回调处理必须保证同一个 Provider 交易号只会入账一次。

建议在模块交易表中为 Provider 交易号建立唯一索引,并按以下顺序处理:

  1. 验证签名或查询 Provider。
  2. 查找对应的本地订单。
  3. 核对金额、币种和商户订单号。
  4. 检查交易号是否已经处理。
  5. 在数据库事务中保存交易并更新订单。
  6. 成功后返回 Provider 规定的确认内容。

对于已经处理过的合法通知,通常仍应返回 200 OK,否则 Provider 可能会持续重试。

还需要确认 Provider 对响应正文的具体要求。有些平台接受 OK,有些要求返回 JSON、固定字符串或特定状态码。成功响应必须严格遵循 Provider 的协议,不能自行假定。

备注:内容仅供参考。