PAYATHON 2026

如何在 WordPress WooCommerce 中集成 giropay 和 sofort-banking?

支付阿杰

结论

不建议再为 WooCommerce 开发新的 giropay 集成。giropay 已于 2024 年 6 月 30 日停止服务。即使现有插件仍保留这一付款选项,通常也无法再处理新交易。

Sofort(Sofortüberweisung)应通过仍支持该付款方式的支付服务商接入,例如 Klarna 或其他在目标地区提供服务的 PSP。确定服务商后,再安装其官方 WooCommerce 插件。结账时是否仍显示为 “Sofort”,以及支持哪些国家和币种,应以服务商当前的商户后台和文档为准。

推荐的集成方案

实际项目不需要分别向 giropay 和 Sofort 申请底层银行接口。可以选择一家支付服务商,由其负责以下工作:

  • 商户身份验证与合规要求
  • 银行跳转和用户授权
  • 支付状态查询
  • Webhook 签名验证
  • 退款与对账
  • WooCommerce 订单状态同步

具体实施步骤:

  1. 确认服务商当前是否向德国商户开放 Sofort 或 Klarna 在线银行转账。
  2. 注册商户账户并完成 KYC 审核。
  3. 在 WordPress 后台安装服务商提供的 WooCommerce 插件。
  4. 在 WooCommerce → 设置 → 付款 中启用对应的付款方式。
  5. 填写 API key、商户编号和 Webhook secret 等凭据。
  6. 先在测试环境验证付款、失败、取消、退款和异步回调。
  7. 切换为生产凭据后,再用小额真实交易进行验证。

选择插件时,需要确认它支持当前使用的 WooCommerce 和 WordPress 版本,并检查是否明确兼容:

  • WooCommerce Checkout Blocks
  • High-Performance Order Storage(HPOS)
  • 异步 Webhook
  • 自动退款
  • 多币种或订阅功能(如果项目需要)

不要因为插件名称含有 “giropay” 或 “Sofort” 就直接安装。长期没有更新的支付插件可能仍在调用已经停用的接口,也可能没有签名校验或缺少新版 WooCommerce 的兼容声明。

为什么不应直接调用支付 API

Sofort 这类在线银行转账通常无法通过一次同步 HTTP 请求完成。常见流程如下:

  1. WooCommerce 创建待支付订单。
  2. 网站向支付服务商创建 Payment 或 Checkout Session。
  3. 用户跳转到服务商或银行页面完成授权。
  4. 用户返回网站,但支付结果此时不一定已经最终确认。
  5. 服务商通过 Webhook 异步通知支付成功或失败。
  6. 网站验证 Webhook 签名,然后更新订单状态。

因此,用户返回成功页面并不等于网站已经收到款项。浏览器返回地址可能被伪造,也可能早于 Webhook 到达。

自定义 WooCommerce 网关示例

如果选定的服务商没有可用插件,可以开发自定义支付网关。下面仅展示 WooCommerce 网关的基本结构。代码中的 /payments、返回字段和认证方式必须按照实际服务商的文档替换,不能直接用于生产环境。

<?php

add_filter('woocommerce_payment_gateways', function ($gateways) {
    $gateways[] = 'WC_Gateway_Bank_Redirect';
    return $gateways;
});

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

    class WC_Gateway_Bank_Redirect extends WC_Payment_Gateway
    {
        public function __construct()
        {
            $this->id = 'bank_redirect';
            $this->method_title = 'Online Bank Transfer';
            $this->method_description = 'Redirect payment through a payment service provider.';
            $this->has_fields = false;
            $this->supports = ['products', 'refunds'];

            $this->init_form_fields();
            $this->init_settings();

            $this->title = $this->get_option('title');
            $this->enabled = $this->get_option('enabled');
            $this->api_key = $this->get_option('api_key');

            add_action(
                'woocommerce_update_options_payment_gateways_' . $this->id,
                [$this, 'process_admin_options']
            );

            add_action(
                'woocommerce_api_wc_gateway_bank_redirect',
                [$this, 'handle_webhook']
            );
        }

        public function init_form_fields()
        {
            $this->form_fields = [
                'enabled' => [
                    'title'   => 'Enable',
                    'type'    => 'checkbox',
                    'label'   => 'Enable online bank transfer',
                    'default' => 'no',
                ],
                'title' => [
                    'title'   => 'Checkout title',
                    'type'    => 'text',
                    'default' => 'Online bank transfer',
                ],
                'api_key' => [
                    'title' => 'API key',
                    'type'  => 'password',
                ],
            ];
        }

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

            $response = wp_remote_post('https://api.example.com/payments', [
                'timeout' => 30,
                'headers' => [
                    'Authorization' => 'Bearer ' . $this->api_key,
                    'Content-Type'  => 'application/json',
                ],
                'body' => wp_json_encode([
                    'amount' => [
                        'value'    => (string) $order->get_total(),
                        'currency' => $order->get_currency(),
                    ],
                    'reference'    => (string) $order->get_id(),
                    'return_url'   => $this->get_return_url($order),
                    'webhook_url'  => WC()->api_request_url(
                        'wc_gateway_bank_redirect'
                    ),
                ]),
            ]);

            if (is_wp_error($response)) {
                wc_add_notice(
                    'Payment service is temporarily unavailable.',
                    'error'
                );
                return ['result' => 'failure'];
            }

            $status = wp_remote_retrieve_response_code($response);
            $data = json_decode(
                wp_remote_retrieve_body($response),
                true
            );

            if (
                $status < 200 ||
                $status >= 300 ||
                empty($data['payment_id']) ||
                empty($data['redirect_url'])
            ) {
                wc_add_notice('Unable to initialize payment.', 'error');
                return ['result' => 'failure'];
            }

            $order->update_meta_data(
                '_provider_payment_id',
                sanitize_text_field($data['payment_id'])
            );
            $order->update_status(
                'pending',
                'Waiting for payment provider confirmation.'
            );
            $order->save();

            return [
                'result'   => 'success',
                'redirect' => esc_url_raw($data['redirect_url']),
            ];
        }

        public function handle_webhook()
        {
            $payload = file_get_contents('php://input');
            $signature = isset($_SERVER['HTTP_X_PROVIDER_SIGNATURE'])
                ? sanitize_text_field(
                    wp_unslash($_SERVER['HTTP_X_PROVIDER_SIGNATURE'])
                )
                : '';

            // 必须按照服务商文档验证签名。
            if (!$this->is_valid_signature($payload, $signature)) {
                status_header(401);
                exit;
            }

            $event = json_decode($payload, true);
            $order_id = isset($event['reference'])
                ? absint($event['reference'])
                : 0;

            $order = wc_get_order($order_id);

            if (!$order) {
                status_header(404);
                exit;
            }

            $payment_id = $order->get_meta('_provider_payment_id');

            if (
                empty($event['payment_id']) ||
                !hash_equals(
                    (string) $payment_id,
                    (string) $event['payment_id']
                )
            ) {
                status_header(400);
                exit;
            }

            if (($event['status'] ?? '') === 'paid' && !$order->is_paid()) {
                $order->payment_complete($event['payment_id']);
                $order->add_order_note(
                    'Payment confirmed by provider webhook.'
                );
            }

            status_header(200);
            exit;
        }

        private function is_valid_signature($payload, $signature)
        {
            // 示例故意不实现签名算法。
            // 必须使用服务商规定的 Webhook secret、算法和时间戳校验。
            return false;
        }
    }
});

生产版本还需要实现退款接口,并处理日志脱敏、请求幂等、Webhook 重放防护和各种明确的失败状态。如果使用 Checkout Blocks,还需要单独注册前端 payment method。仅继承 WC_Payment_Gateway 通常只能覆盖经典结账流程。

订单状态处理

订单状态应由通过验证的 Webhook 更新:

  • 创建支付后,订单保持 pending。
  • 服务商确认付款后,调用 $order->payment_complete()。
  • 用户取消后,可以保持 pending,也可以根据业务规则改为 cancelled。
  • 支付明确失败后,改为 failed。
  • 如果支付仍在处理中,不要提前标记为 processing 或 completed。

服务商可能重复发送 Webhook,因此处理逻辑必须具备幂等性。更新订单前,需要确认订单是否已经付款,并核对支付编号、订单编号、金额和币种。

安全与上线注意事项

API secret 和 Webhook secret 只能存放在服务器端,不能写入 JavaScript、HTML 或公开仓库。上线时还要做到:

  • 全程使用 HTTPS。
  • 使用服务商 SDK 或 WordPress HTTP API,不要关闭 TLS 证书验证。
  • 严格验证 Webhook 签名和时间戳。
  • 不记录完整凭据、银行信息或敏感个人数据。
  • 金额必须读取服务端订单,不能信任浏览器提交的金额。
  • 使用唯一的幂等键,避免重复创建支付。
  • 隔离生产环境和测试环境的凭据。
  • 不在网站内采集用户的网银账号、密码或 TAN。
  • 上线前测试取消付款、重复通知、超时、退款,以及付款成功但用户没有返回网站等情况。

对于新的德国 WooCommerce 项目,应放弃 giropay,先确认目前仍可用的德国本地支付方式,再通过持续维护的支付服务商插件接入 Sofort、Klarna 或其他替代方案。

备注:内容仅供参考。