如何在 WordPress WooCommerce 中集成 giropay 和 sofort-banking?
结论
不建议再为 WooCommerce 开发新的 giropay 集成。giropay 已于 2024 年 6 月 30 日停止服务。即使现有插件仍保留这一付款选项,通常也无法再处理新交易。
Sofort(Sofortüberweisung)应通过仍支持该付款方式的支付服务商接入,例如 Klarna 或其他在目标地区提供服务的 PSP。确定服务商后,再安装其官方 WooCommerce 插件。结账时是否仍显示为 “Sofort”,以及支持哪些国家和币种,应以服务商当前的商户后台和文档为准。
推荐的集成方案
实际项目不需要分别向 giropay 和 Sofort 申请底层银行接口。可以选择一家支付服务商,由其负责以下工作:
- 商户身份验证与合规要求
- 银行跳转和用户授权
- 支付状态查询
- Webhook 签名验证
- 退款与对账
- WooCommerce 订单状态同步
具体实施步骤:
- 确认服务商当前是否向德国商户开放 Sofort 或 Klarna 在线银行转账。
- 注册商户账户并完成 KYC 审核。
- 在 WordPress 后台安装服务商提供的 WooCommerce 插件。
- 在
WooCommerce → 设置 → 付款中启用对应的付款方式。 - 填写 API key、商户编号和 Webhook secret 等凭据。
- 先在测试环境验证付款、失败、取消、退款和异步回调。
- 切换为生产凭据后,再用小额真实交易进行验证。
选择插件时,需要确认它支持当前使用的 WooCommerce 和 WordPress 版本,并检查是否明确兼容:
- WooCommerce Checkout Blocks
- High-Performance Order Storage(HPOS)
- 异步 Webhook
- 自动退款
- 多币种或订阅功能(如果项目需要)
不要因为插件名称含有 “giropay” 或 “Sofort” 就直接安装。长期没有更新的支付插件可能仍在调用已经停用的接口,也可能没有签名校验或缺少新版 WooCommerce 的兼容声明。
为什么不应直接调用支付 API
Sofort 这类在线银行转账通常无法通过一次同步 HTTP 请求完成。常见流程如下:
- WooCommerce 创建待支付订单。
- 网站向支付服务商创建 Payment 或 Checkout Session。
- 用户跳转到服务商或银行页面完成授权。
- 用户返回网站,但支付结果此时不一定已经最终确认。
- 服务商通过 Webhook 异步通知支付成功或失败。
- 网站验证 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 或其他替代方案。
备注:内容仅供参考。
