PAYATHON 2026

如何为 Learnpress 添加 IDEAL payment method?

支付老李

明确结论

不要修改 payment-method.php。这段代码只是结账页的展示模板,用于遍历并显示已经注册的支付网关,并不负责创建支付方式。即使直接在模板中加入 iDEAL 单选框,让它出现在页面上,也无法完成付款请求、回调验证和订单状态更新。LearnPress 升级时,这类修改还会被覆盖。

可以采用以下两种方式:

  1. 推荐通过 WooCommerce 接入 iDEAL。安装 LearnPress 的 WooCommerce 支付集成组件,再安装支持 iDEAL 的 WooCommerce 支付网关,例如 Mollie、Stripe 或其他荷兰支付服务商提供的插件。

  2. 如果业务有特殊要求,可以开发独立的 LearnPress 支付网关插件。该插件需要注册支付方式、创建支付请求、跳转到支付服务商、处理 webhook,并根据服务端查询结果更新 LearnPress 订单。

为什么修改模板没有用

问题中的代码大致执行以下逻辑:

foreach ( $payment_gateways as $gateway ) {
    // 输出支付方式单选框
    // 输出网关标题、描述和付款表单
}

其中 $gateway->id、$gateway->get_title() 和 $gateway->get_payment_form() 都来自已经注册的网关对象。

也就是说,payment-method.php 只负责显示支付方式,无法完成以下工作:

  • 向 iDEAL 服务商创建交易;
  • 生成支付跳转地址;
  • 验证 webhook;
  • 防止重复处理通知;
  • 将 LearnPress 订单从 pending 更新为 completed;
  • 处理取消、失败和退款等状态。

iDEAL 也不是一个简单的“银行转账”选项。网站必须通过支付服务商(PSP)接入,例如 Mollie、Stripe、Adyen 或 Buckaroo。具体能够使用哪家服务商,取决于商户所在地区、账户资质以及服务商当前的产品支持情况。

推荐:通过 WooCommerce 接入

如果网站没有绕过 WooCommerce 的硬性要求,通过 WooCommerce 接入通常更容易维护。

基本流程如下:

  1. 安装并启用 WooCommerce。
  2. 安装 LearnPress 官方提供或与当前版本兼容的 WooCommerce 支付集成组件。
  3. 安装支付服务商提供的 WooCommerce 网关插件。
  4. 在 WooCommerce 支付设置中启用 iDEAL。
  5. 配置支付服务商提供的 API Key、Webhook 或其他商户凭据。
  6. 确认 LearnPress 课程结账已经交由 WooCommerce 处理。
  7. 分别测试付款成功、付款取消、付款失败以及 webhook 延迟到达的情况。

插件名称、配置入口和兼容范围可能会随 LearnPress、WooCommerce 及支付插件的版本变化,应以当前安装版本的说明为准。后台出现 iDEAL 选项并不代表接入已经完成。上线前还要确认付款完成后,LearnPress 订单状态和课程访问权限都能正确更新。

定制独立网关的实现结构

如果需要直接连接 LearnPress,建议创建一个普通的 WordPress 插件,例如:

learnpress-ideal-gateway/
├── learnpress-ideal-gateway.php
├── includes/
│   └── class-lp-gateway-ideal.php
└── templates/
    └── payment-form.php

不要把代码放进 LearnPress 插件目录,也不要写入主题的结账模板。

LearnPress 不同版本使用的网关注册钩子、抽象类方法和订单 API 可能不同。下面的代码只用于说明实现结构。在没有核对当前版本源码之前,不应直接用于生产环境。

注册网关

<?php
/**
 * Plugin Name: LearnPress iDEAL Gateway
 * Description: Adds an iDEAL payment gateway to LearnPress.
 * Version: 1.0.0
 */

defined( 'ABSPATH' ) || exit;

add_action( 'plugins_loaded', function () {
    if ( ! class_exists( 'LP_Gateway_Abstract' ) ) {
        return;
    }

    require_once __DIR__ . '/includes/class-lp-gateway-ideal.php';
} );

/*
 * 该过滤器名称和数组格式必须根据当前 LearnPress 版本确认。
 */
add_filter( 'learn-press/payment-gateways', function ( $gateways ) {
    $gateways['ideal'] = 'LP_Gateway_Ideal';

    return $gateways;
} );

部分 LearnPress 版本可能采用不同的注册方式。可以查看同一版本中的现有网关实现,例如 PayPal 网关,并确认以下内容:

  • 注册网关所使用的 filter;
  • 注册值是类名、对象还是配置数组;
  • process_payment() 的参数和返回值;
  • LearnPress 订单状态允许使用哪些值;
  • 如何声明和读取设置字段。

网关类示例

<?php

defined( 'ABSPATH' ) || exit;

class LP_Gateway_Ideal extends LP_Gateway_Abstract {

    public function __construct() {
        $this->id                 = 'ideal';
        $this->method_title       = 'iDEAL';
        $this->method_description = 'Pay securely with iDEAL.';
        $this->title              = 'iDEAL';
        $this->description        = 'You will be redirected to your bank to complete payment.';

        parent::__construct();
    }

    public function get_payment_form() {
        return '<p>' . esc_html(
            'After placing the order, you will be redirected to the payment provider.'
        ) . '</p>';
    }

    public function process_payment( $order_id ) {
        $order = learn_press_get_order( $order_id );

        if ( ! $order ) {
            return array(
                'result'  => 'fail',
                'message' => 'Invalid order.',
            );
        }

        /*
         * 1. 从 LearnPress 订单读取金额、货币和订单号。
         * 2. 使用支付服务商官方 SDK 或 REST API 创建付款。
         * 3. metadata 中保存 LearnPress order ID。
         * 4. 设置 return URL 和 webhook URL。
         * 5. 保存服务商返回的 payment ID。
         * 6. 返回或执行到 checkout URL 的跳转。
         */

        $payment = $this->create_provider_payment( $order );

        if ( empty( $payment['id'] ) || empty( $payment['checkout_url'] ) ) {
            return array(
                'result'  => 'fail',
                'message' => 'Unable to create payment.',
            );
        }

        update_post_meta(
            $order_id,
            '_ideal_payment_id',
            sanitize_text_field( $payment['id'] )
        );

        return array(
            'result'   => 'success',
            'redirect' => esc_url_raw( $payment['checkout_url'] ),
        );
    }

    private function create_provider_payment( $order ) {
        /*
         * 这里调用所选支付服务商的 API。
         * 不要在浏览器端暴露 Secret API Key。
         */
        return array();
    }
}

这里的返回数组采用了常见的支付网关形式,但当前 LearnPress 版本可能要求其他字段,也可能要求网关直接执行重定向。实际开发时,必须按照已安装版本的 LP_Gateway_Abstract 和内置网关实现进行调整。

创建支付请求

创建付款时,至少需要向支付服务商发送以下信息:

$payload = array(
    'amount' => array(
        'currency' => $currency,
        'value'    => number_format( $amount, 2, '.', '' ),
    ),
    'description' => sprintf( 'LearnPress order #%d', $order_id ),
    'redirectUrl' => $return_url,
    'webhookUrl'  => $webhook_url,
    'metadata'    => array(
        'learnpress_order_id' => $order_id,
    ),
);

这段代码只是通用示例。字段名称、金额格式、签名方式和接口地址,都必须遵循所选支付服务商的官方 API 定义。

支付创建成功后,应保存服务商返回的 payment ID,不能只依赖浏览器返回的查询参数:

update_post_meta(
    $order_id,
    '_ideal_payment_id',
    sanitize_text_field( $provider_payment_id )
);

处理 webhook

可以通过 WordPress REST API 提供 webhook 地址:

add_action( 'rest_api_init', function () {
    register_rest_route(
        'learnpress-ideal/v1',
        '/webhook',
        array(
            'methods'             => 'POST',
            'callback'            => 'lp_ideal_handle_webhook',
            'permission_callback' => '__return_true',
        )
    );
} );

function lp_ideal_handle_webhook( WP_REST_Request $request ) {
    $payment_id = sanitize_text_field(
        (string) $request->get_param( 'id' )
    );

    if ( $payment_id === '' ) {
        return new WP_REST_Response( array( 'ok' => false ), 400 );
    }

    /*
     * 必须使用服务器端 API Key 向支付服务商查询交易。
     * 不要仅相信 webhook 请求中的 status、amount 或 order_id。
     */
    $payment = lp_ideal_fetch_payment_from_provider( $payment_id );

    if ( ! $payment ) {
        return new WP_REST_Response( array( 'ok' => false ), 400 );
    }

    $order_id = absint( $payment['metadata']['learnpress_order_id'] ?? 0 );
    $order    = learn_press_get_order( $order_id );

    if ( ! $order ) {
        return new WP_REST_Response( array( 'ok' => false ), 404 );
    }

    /*
     * 还应核对:
     * - 保存的 payment ID 是否一致;
     * - 实付金额是否等于订单金额;
     * - 币种是否一致;
     * - 商户账户是否正确;
     * - 该通知是否已经处理过。
     */

    if ( $payment['status'] === 'paid' ) {
        /*
         * 使用当前 LearnPress 版本提供的正式订单 API,
         * 将订单标记为已付款/已完成,并授予课程权限。
         */
    } elseif ( in_array(
        $payment['status'],
        array( 'failed', 'canceled', 'expired' ),
        true
    ) ) {
        /*
         * 根据业务规则把订单标记为失败或取消。
         */
    }

    return new WP_REST_Response( array( 'ok' => true ), 200 );
}

将 permission_callback 设为允许访问,是为了让外部支付服务能够调用 webhook,但这不等于可以省略验证。真正的授权依据应当是签名校验,或者使用收到的 payment ID 向支付服务商查询交易,再核对金额、币种和订单关联信息。

返回页面不能作为付款凭证

用户付款后通常会被重定向回网站,但浏览器返回页面只能用于展示结果,不能用来确认付款。

完整流程应当是:

创建 LearnPress 待付款订单
→ 服务端向支付商创建 iDEAL 交易
→ 用户跳转到银行付款
→ 支付商发送 webhook
→ 网站从支付商服务端查询交易
→ 核对金额、币种和订单号
→ 更新 LearnPress 订单并开通课程

用户付款后可能直接关闭页面,导致 return URL 根本没有被访问。另一方面,return URL 中的参数也可能被人为伪造。因此,是否开通课程必须以经过验证的服务端支付状态为准。

上线前的注意事项

  • 不要修改 payment-method.php 或 LearnPress 核心文件。
  • Secret API Key 只能保存在服务器端,不能输出到 HTML 或 JavaScript。
  • 金额必须使用支付服务商规定的格式,避免浮点数精度问题。
  • webhook 必须能够安全处理重复调用,已经完成的订单不能被重复记账或重复开通课程。
  • 必须核对 payment ID、订单号、金额和币种。
  • 新订单应先保持待付款状态,不能在用户跳转到银行时就标记为完成。
  • 使用 HTTPS,日志中不要记录 API Key、完整请求头或敏感支付信息。
  • 测试付款成功、取消、失败、过期、重复通知和延迟通知等情况。
  • 退款通常需要单独实现。修改 LearnPress 订单状态并不一定会自动向用户退款。
  • 更新 LearnPress 前,应先在测试环境检查网关注册、结账跳转、webhook 和课程授权流程。

如果只是要让 LearnPress 支持 iDEAL,较合适的做法是通过 WooCommerce 支付集成连接成熟的 iDEAL 网关。只有结账流程、服务商接口或订单逻辑存在特殊要求时,维护独立的 LearnPress 网关插件才有必要。

备注:内容仅供参考。