PAYATHON 2026

如何在 prestashop 中添加自定义支付方式?

支付老李

结论

在 PrestaShop 中,如果支付方式不需要在下单时在线扣款,而是在订单创建后线下收款,应该将它开发成独立的支付模块。模块继承 PaymentModule,通过 paymentOptions 钩子在结账页显示支付选项,再由前台控制器调用 validateOrder() 创建订单。

如果业务流程和货到付款完全相同,安装并配置官方的 Cash on Delivery 模块即可。只有支付方式的名称、订单状态、手续费、适用地区或处理流程不同,才需要开发自定义模块。

以下示例采用 PrestaShop 1.7/8 中常见的模块结构。不同版本在目录、翻译系统和钩子方面可能存在差异,上线前应根据实际版本核对。

模块结构

在 modules 目录下创建 customoffline:

modules/
└── customoffline/
    ├── customoffline.php
    ├── controllers/
    │   └── front/
    │       └── validation.php
    └── views/
        └── templates/
            └── hook/
                ├── payment_info.tpl
                └── payment_return.tpl

模块目录名、主 PHP 文件名和模块类名必须相互对应,否则 PrestaShop 无法正确加载模块。

创建支付模块

modules/customoffline/customoffline.php:

<?php

if (!defined('_PS_VERSION_')) {
    exit;
}

use PrestaShop\PrestaShop\Core\Payment\PaymentOption;

class CustomOffline extends PaymentModule
{
    public function __construct()
    {
        $this->name = 'customoffline';
        $this->tab = 'payments_gateways';
        $this->version = '1.0.0';
        $this->author = 'Your Company';
        $this->bootstrap = true;
        $this->need_instance = 0;

        parent::__construct();

        $this->displayName = $this->l('Custom offline payment');
        $this->description = $this->l(
            'Allows customers to place an order and pay through an offline method.'
        );
    }

    public function install()
    {
        return parent::install()
            && $this->installOrderState()
            && $this->registerHook('paymentOptions')
            && $this->registerHook('paymentReturn');
    }

    private function installOrderState()
    {
        $configuredStateId = (int) Configuration::get('CUSTOMOFFLINE_ORDER_STATE');

        if ($configuredStateId > 0 && Validate::isLoadedObject(
            new OrderState($configuredStateId)
        )) {
            return true;
        }

        $orderState = new OrderState();
        $orderState->module_name = $this->name;
        $orderState->color = '#4169E1';
        $orderState->send_email = false;
        $orderState->invoice = false;
        $orderState->logable = false;
        $orderState->paid = false;
        $orderState->shipped = false;
        $orderState->delivery = false;
        $orderState->unremovable = false;
        $orderState->hidden = false;

        foreach (Language::getLanguages(false) as $language) {
            $orderState->name[(int) $language['id_lang']] =
                'Awaiting offline payment';
        }

        if (!$orderState->add()) {
            return false;
        }

        return Configuration::updateValue(
            'CUSTOMOFFLINE_ORDER_STATE',
            (int) $orderState->id
        );
    }

    public function hookPaymentOptions($params)
    {
        if (!$this->active || empty($params['cart'])) {
            return [];
        }

        /** @var Cart $cart */
        $cart = $params['cart'];

        if (!$this->checkCurrency($cart)) {
            return [];
        }

        $paymentOption = new PaymentOption();
        $paymentOption
            ->setModuleName($this->name)
            ->setCallToActionText($this->l('Pay with custom offline payment'))
            ->setAction(
                $this->context->link->getModuleLink(
                    $this->name,
                    'validation',
                    [],
                    true
                )
            )
            ->setAdditionalInformation(
                $this->fetch(
                    'module:' . $this->name .
                    '/views/templates/hook/payment_info.tpl'
                )
            );

        return [$paymentOption];
    }

    public function hookPaymentReturn($params)
    {
        if (!$this->active || empty($params['order'])) {
            return '';
        }

        /** @var Order $order */
        $order = $params['order'];

        $this->context->smarty->assign([
            'order_reference' => $order->reference,
            'total_to_pay' => Tools::displayPrice(
                $order->total_paid,
                new Currency((int) $order->id_currency)
            ),
        ]);

        return $this->fetch(
            'module:' . $this->name .
            '/views/templates/hook/payment_return.tpl'
        );
    }
}

这段代码会单独创建一个“等待线下付款”订单状态。不要直接把这类订单标记为已支付,否则库存、发票、统计数据或第三方 ERP 可能会误判订单已经收款。

创建订单验证控制器

modules/customoffline/controllers/front/validation.php:

<?php

class CustomOfflineValidationModuleFrontController extends ModuleFrontController
{
    public $ssl = true;

    public function postProcess()
    {
        if (!$this->module->active) {
            Tools::redirect('index.php?controller=order&step=1');
        }

        $cart = $this->context->cart;
        $customer = $this->context->customer;

        if (
            !Validate::isLoadedObject($cart)
            || (int) $cart->id_customer <= 0
            || !Validate::isLoadedObject($customer)
            || (int) $cart->id_customer !== (int) $customer->id
            || !$this->module->checkCurrency($cart)
        ) {
            Tools::redirect('index.php?controller=order&step=1');
        }

        // 防止刷新页面或重复提交时为同一个购物车重复创建订单。
        if ((int) Order::getIdByCartId((int) $cart->id) > 0) {
            Tools::redirect('index.php?controller=history');
        }

        $orderStateId = (int) Configuration::get(
            'CUSTOMOFFLINE_ORDER_STATE'
        );

        $orderState = new OrderState($orderStateId);

        if (!Validate::isLoadedObject($orderState)) {
            Tools::redirect('index.php?controller=order&step=1');
        }

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

        $this->module->validateOrder(
            (int) $cart->id,
            $orderStateId,
            $total,
            $this->module->displayName,
            null,
            [],
            (int) $cart->id_currency,
            false,
            $customer->secure_key
        );

        $orderId = (int) $this->module->currentOrder;

        Tools::redirect(
            $this->context->link->getPageLink(
                'order-confirmation',
                true,
                (int) $this->context->language->id,
                [
                    'id_cart' => (int) $cart->id,
                    'id_module' => (int) $this->module->id,
                    'id_order' => $orderId,
                    'key' => $customer->secure_key,
                ]
            )
        );
    }
}

validateOrder() 会根据购物车创建订单,并触发 PrestaShop 对库存、订单历史和邮件等相关流程的处理。不要绕过这个方法,直接向 orders 表写入记录。

添加支付说明模板

views/templates/hook/payment_info.tpl:

<div class="customoffline-payment-info">
    <p>
        {l s='Place your order now and pay using the agreed offline method.' mod='customoffline'}
    </p>
    <p>
        {l s='Payment instructions will be shown after the order is confirmed.' mod='customoffline'}
    </p>
</div>

views/templates/hook/payment_return.tpl:

<div class="customoffline-payment-return">
    <p>
        {l s='Your order has been created successfully.' mod='customoffline'}
    </p>

    <p>
        {l s='Order reference:' mod='customoffline'}
        <strong>{$order_reference|escape:'htmlall':'UTF-8'}</strong>
    </p>

    <p>
        {l s='Amount to pay:' mod='customoffline'}
        <strong>{$total_to_pay|escape:'htmlall':'UTF-8'}</strong>
    </p>

    <p>
        {l s='Please follow the offline payment instructions provided by the store.' mod='customoffline'}
    </p>
</div>

模块安装后,可以通过后台翻译功能,将模板中的英文文案翻译成中文或其他语言。

安装与测试

将模块目录放入 PrestaShop 的 modules 目录,然后在后台模块管理页面安装并启用。完成后,至少检查以下流程:

  1. 支付方式能否正常出现在结账页面。
  2. 禁用模块后,验证地址是否无法继续创建订单。
  3. 同一个购物车重复提交时,是否只生成一个订单。
  4. 订单金额、币种、商品数量和运费是否正确。
  5. 新订单是否处于“等待线下付款”状态,而不是“已付款”。
  6. 在后台手动确认收款后,订单状态能否正常变更。
  7. 游客结账、多店铺、多币种和不同配送方式是否符合业务规则。

开发期间新增或修改类、控制器或模板后,通常还需要清除 PrestaShop 缓存。

上线时需要注意

  • 不要修改 PrestaShop 核心文件,也不要直接改动现有的货到付款模块。核心或官方模块升级后,这些修改很容易丢失。
  • 如果支付方式只对特定国家、币种、客户组或承运商开放,应在后台配置支付限制,并在验证控制器中再次检查必要条件,不能只在前端隐藏支付选项。
  • 如果需要收取手续费,不能只改页面显示的金额。手续费必须进入购物车或订单的计算过程,并正确处理税费、退款和发票。
  • 如果需要用户填写交易编号或付款账户等信息,应提供表单并在服务端校验,同时做好 HTML 转义和长度限制。
  • 传给 validateOrder() 的金额必须由服务端根据购物车重新计算,不能使用浏览器提交的金额。
  • 卸载模块时,不建议直接删除已经被历史订单引用的订单状态。配置项可以删除,但应保证历史订单仍然可读。
  • 如果业务包含在线扣款、异步回调或签名验签,就不能采用简单的货到付款模式。此时还需要单独设计支付请求、回调幂等、金额校验和交易状态同步。

备注:内容仅供参考。