PAYATHON 2026

Prestashop payment module 返回 500 server status

支付小周

结论

支付平台的服务器回调不应使用 Tools::redirect()。订单处理成功后,应直接输出简短响应并立即结束当前请求:

http_response_code(200);
header('Content-Type: application/json; charset=utf-8');

echo json_encode(['status' => 'ok']);
exit;

exit/die 可以用于生产环境。订单处理完成后,需要终止 PrestaShop 后续的控制器渲染流程。否则,后面发生的 PHP 异常、模板错误或未初始化变量,仍可能让最终响应变成 500。

为什么会出现 302 或 500

Tools::redirect() 会向客户端发送重定向响应,因此返回 302 属于正常行为。它适合让顾客的浏览器跳转,不适合处理支付网关发来的服务器到服务器通知。

仅调用:

http_response_code(200);

或者:

header('HTTP/1.1 200 OK');

只能设置当前状态码,不能结束程序。后续代码依然会执行。如果 PrestaShop 随后尝试渲染不存在的模板、触发异常或访问未定义变量,最终响应仍可能变成 500。

当前代码还有几个可能引发异常的问题:

  • $paymentId 只在 confirmed 和 received 两种状态下赋值。
  • getRow() 可能返回 false,此时直接访问 $row['current_state'] 并不安全。
  • Tools::redirect(__FILE__, 'payment_confirmation.tpl') 不能用于渲染模板。
  • 重定向只在数据库更新成功后执行。如果更新失败,程序会继续进入控制器的后续流程。
  • 直接修改 orders.current_state 会绕过 PrestaShop 的订单状态处理机制。

推荐的回调处理方式

回调接口应区分成功和失败,并在每个响应出口立即终止请求:

<?php

class YourModuleValidationModuleFrontController extends ModuleFrontController
{
    public function postProcess()
    {
        header('Content-Type: application/json; charset=utf-8');

        try {
            $hash = (string) Tools::getValue('hash');
            $status = (string) Tools::getValue('status');
            $optDataRaw = (string) Tools::getValue('optData');

            $expectedHash = hash('sha256', $this->module->secret_key);

            if (!hash_equals($expectedHash, $hash)) {
                $this->sendJsonResponse(401, [
                    'status' => 'error',
                    'message' => 'Invalid signature',
                ]);
            }

            parse_str($optDataRaw, $optData);
            $idCart = isset($optData['cartId']) ? (int) $optData['cartId'] : 0;

            if ($idCart <= 0) {
                $this->sendJsonResponse(400, [
                    'status' => 'error',
                    'message' => 'Invalid cart ID',
                ]);
            }

            if (!in_array($status, ['confirmed', 'received'], true)) {
                $this->sendJsonResponse(400, [
                    'status' => 'error',
                    'message' => 'Unsupported payment status',
                ]);
            }

            $orderId = (int) Order::getOrderByCartId($idCart);

            if ($orderId > 0) {
                // 回调可能重复发送。订单已经存在时按幂等规则处理。
                $this->sendJsonResponse(200, [
                    'status' => 'ok',
                    'message' => 'Order already processed',
                ]);
            }

            $cart = new Cart($idCart);

            if (
                !Validate::isLoadedObject($cart)
                || !$cart->id_customer
                || !$cart->id_address_delivery
                || !$cart->id_address_invoice
                || !$this->module->active
            ) {
                $this->sendJsonResponse(400, [
                    'status' => 'error',
                    'message' => 'Invalid cart',
                ]);
            }

            $customer = new Customer((int) $cart->id_customer);

            if (!Validate::isLoadedObject($customer)) {
                $this->sendJsonResponse(400, [
                    'status' => 'error',
                    'message' => 'Invalid customer',
                ]);
            }

            $currency = new Currency((int) $cart->id_currency);

            if (!Validate::isLoadedObject($currency)) {
                $this->sendJsonResponse(400, [
                    'status' => 'error',
                    'message' => 'Invalid currency',
                ]);
            }

            $paymentState = $status === 'confirmed'
                ? (int) Configuration::get('PS_OS_PAYMENT')
                : 11;

            $orderAmount = (float) $cart->getOrderTotal(true, Cart::BOTH);

            $this->module->validateOrder(
                (int) $cart->id,
                $paymentState,
                $orderAmount,
                $this->module->displayName,
                'Invoice Code: ' . (string) Tools::getValue('invoiceCode'),
                [],
                (int) $currency->id,
                false,
                $customer->secure_key
            );

            $this->sendJsonResponse(200, [
                'status' => 'ok',
            ]);
        } catch (Throwable $e) {
            PrestaShopLogger::addLog(
                'Payment callback failed: ' . $e->getMessage(),
                3
            );

            $this->sendJsonResponse(500, [
                'status' => 'error',
                'message' => 'Internal server error',
            ]);
        }
    }

    private function sendJsonResponse($statusCode, array $data)
    {
        http_response_code((int) $statusCode);
        header('Content-Type: application/json; charset=utf-8');

        echo json_encode($data);
        exit;
    }
}

如果旧版 PHP 不支持 Throwable,可以改为捕获 Exception。具体写法取决于当前 PrestaShop 使用的 PHP 版本。

已存在订单时不要直接更新数据库

不建议执行:

UPDATE ps_orders SET current_state = 2 WHERE id_cart = ...

这种写法会绕过订单状态历史、邮件通知、库存处理和相关钩子。需要修改已有订单的状态时,应使用 PrestaShop 的订单历史机制:

$order = new Order($orderId);

if (!Validate::isLoadedObject($order)) {
    $this->sendJsonResponse(404, [
        'status' => 'error',
        'message' => 'Order not found',
    ]);
}

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

状态 ID 不应直接写死为 2 或 11。如果这些 ID 对应模块创建的自定义状态,应从模块配置中读取。如果代表标准的“已付款”状态,应优先使用:

Configuration::get('PS_OS_PAYMENT')

如果支付平台只接受纯文本

有些支付平台要求返回固定字符串,不接受 JSON。此时可以简化为:

http_response_code(200);
header('Content-Type: text/plain; charset=utf-8');

echo 'OK';
exit;

在控制请求流程方面,这与 die('OK') 没有本质区别。使用 exit 能更直接地表示响应已经完成,PrestaShop 控制器不再继续运行。

还需要检查 500 的具体原因

设置状态码为 200 后仍然返回 500,说明后续代码发生了错误,并非状态码设置失败。应检查 Web 服务器、PHP-FPM 和 PrestaShop 日志,重点排查:

  • $paymentId 未定义
  • $row 为 false
  • validateOrder() 抛出异常
  • 重复回调导致同一购物车重复创建订单
  • 模板路径或控制器渲染错误
  • 输出响应前已经发送 HTTP headers
  • 支付金额、币种或签名字段不合法

支付回调只负责验证请求、以幂等方式处理订单、输出协议要求的响应,并立即 exit。顾客支付完成后的页面跳转,应通过单独的浏览器返回地址处理。

备注:内容仅供参考。