PHP & MySQL 网站如何接入瑞典本地银行支付?
结论
PHP 和 MySQL 没有内置的“瑞典本地银行支付”模块。接入这类支付,需要选择支持瑞典市场的支付服务商,再使用其 API、官方 SDK 或托管收银台完成付款流程。
常见方案有:
- Swish:瑞典常用的即时移动支付方式,适合面向个人消费者的网站。
- 开放银行转账服务:支付服务商连接用户的瑞典银行账户,用户授权后直接付款。
- Bankgiro 或普通银行转账:适合订单量较少,并且可以接受人工确认或延迟到账的场景。
- 综合支付平台:有些平台同时支持 Swish、银行转账和银行卡支付,具体功能取决于商户所在地区、行业和合同。
费用没有统一标准,通常包括开户费、月费、单笔交易费、退款费,以及可能存在的最低消费。准确价格需要向服务商申请商户报价。不能只看插件价格,银行协议、证书管理和交易手续费往往才是主要成本。
应该选择哪种方案
如果网站主要服务瑞典个人消费者,可以优先考虑 Swish,或者包含 Swish 的托管支付页面。这类方案的付款路径较短,网站也不必自行处理用户的银行账户凭据。
如果要求用户“从银行账户直接付款”,可以选择提供开放银行支付功能的服务商。此类服务通常会:
- 展示可用银行列表;
- 引导用户前往银行完成身份验证和授权;
- 创建银行付款;
- 通过回调或 webhook 通知网站付款状态;
- 提供退款、对账和交易查询功能。
订单量很小的网站也可以先使用 Bankgiro 或普通银行转账。网站创建订单后显示付款参考号,收到银行流水后再把订单标记为已付款。这种方式开发成本较低,但到账确认、退款和对账的自动化程度也比较低。
需要注意,BankID 是身份验证和电子签名工具,不是支付方式。部分支付流程会使用 BankID 完成授权,但仅接入 BankID 无法完成收款。
推荐的接入结构
简单的 PHP 网站可以使用“托管收银台 + webhook”结构:
浏览器
→ PHP 创建本地订单
→ PHP 调用支付服务商 API
→ 浏览器跳转到托管支付页面
→ 用户在银行或 Swish 中确认
→ 支付服务商向 webhook 发送结果
→ PHP 验证通知并更新 MySQL 订单
相比自行制作付款表单,托管支付页面通常更稳妥。银行授权、身份验证和支付界面交给服务商处理,网站只需保存订单号、金额、币种和支付状态。
数据库设计
订单金额建议使用最小货币单位的整数保存。例如,199.00 SEK 应保存为 19900 öre,以免产生浮点数计算误差。
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(64) NOT NULL UNIQUE,
amount_minor INT UNSIGNED NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'SEK',
payment_provider VARCHAR(32) DEFAULT NULL,
provider_payment_id VARCHAR(128) DEFAULT NULL,
payment_status ENUM(
'pending',
'authorized',
'paid',
'failed',
'cancelled',
'refunded'
) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
paid_at DATETIME DEFAULT NULL,
UNIQUE KEY uk_provider_payment_id (provider_payment_id)
);
payment_status 应按照所选服务商的状态模型调整。有些银行支付在用户完成授权后仍处于处理中,因此不能把“用户返回网站”直接视为付款成功。
创建支付的 PHP 示例
下面是与具体服务商无关的示意代码。其中的 API 地址、字段名称、认证方式和返回结构都是占位内容,正式接入时必须替换为服务商官方文档规定的值。
<?php
declare(strict_types=1);
session_start();
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4',
'shop_user',
'replace-with-password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
$orderNo = bin2hex(random_bytes(16));
$amountMinor = 19900;
$currency = 'SEK';
$stmt = $pdo->prepare(
'INSERT INTO orders (order_no, amount_minor, currency)
VALUES (:order_no, :amount_minor, :currency)'
);
$stmt->execute([
'order_no' => $orderNo,
'amount_minor' => $amountMinor,
'currency' => $currency,
]);
$requestBody = [
'merchant_reference' => $orderNo,
'amount' => $amountMinor,
'currency' => $currency,
'payment_method' => 'swedish_bank_payment',
'return_url' => 'https://example.com/payment/return.php',
'cancel_url' => 'https://example.com/payment/cancel.php',
'webhook_url' => 'https://example.com/webhooks/payment.php',
];
$ch = curl_init('https://api.payment-provider.example/v1/payments');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PAYMENT_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . $orderNo,
],
CURLOPT_POSTFIELDS => json_encode(
$requestBody,
JSON_THROW_ON_ERROR
),
CURLOPT_TIMEOUT => 30,
]);
$responseBody = curl_exec($ch);
if ($responseBody === false) {
throw new RuntimeException(curl_error($ch));
}
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
if ($statusCode < 200 || $statusCode >= 300) {
throw new RuntimeException('Payment creation failed');
}
$paymentId = $response['payment_id'] ?? null;
$checkoutUrl = $response['checkout_url'] ?? null;
if (!is_string($paymentId) || !is_string($checkoutUrl)) {
throw new RuntimeException('Invalid payment provider response');
}
$stmt = $pdo->prepare(
'UPDATE orders
SET payment_provider = :provider,
provider_payment_id = :payment_id
WHERE order_no = :order_no'
);
$stmt->execute([
'provider' => 'selected_provider',
'payment_id' => $paymentId,
'order_no' => $orderNo,
]);
header('Location: ' . $checkoutUrl, true, 303);
exit;
在生产环境中,API 密钥不能直接写入代码或提交到 Git 仓库,应通过环境变量或专用密钥管理系统提供。如果服务商要求使用客户端证书,还需要妥善保存私钥,并建立证书续期流程。
处理支付通知
付款结果应以服务商签名后的 webhook 或主动查询 API 的结果为准。不能只相信浏览器传来的 success=1、订单号或支付状态。
下面仍是通用示例。不同服务商可能使用 HMAC、非对称签名、客户端证书或自定义签名头,接入时必须按照其官方规则进行验证:
<?php
declare(strict_types=1);
$rawBody = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_PAYMENT_SIGNATURE'] ?? '';
$webhookSecret = getenv('PAYMENT_WEBHOOK_SECRET');
if ($rawBody === false || $webhookSecret === false) {
http_response_code(400);
exit;
}
$expectedSignature = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
$eventId = $event['event_id'] ?? null;
$paymentId = $event['payment_id'] ?? null;
$status = $event['status'] ?? null;
$amountMinor = $event['amount'] ?? null;
$currency = $event['currency'] ?? null;
if (
!is_string($eventId) ||
!is_string($paymentId) ||
!is_string($status)
) {
http_response_code(400);
exit;
}
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4',
'shop_user',
'replace-with-password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$pdo->beginTransaction();
$stmt = $pdo->prepare(
'SELECT id, amount_minor, currency, payment_status
FROM orders
WHERE provider_payment_id = :payment_id
FOR UPDATE'
);
$stmt->execute(['payment_id' => $paymentId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if ($order === false) {
$pdo->rollBack();
http_response_code(404);
exit;
}
if (
(int) $order['amount_minor'] !== (int) $amountMinor ||
$order['currency'] !== $currency
) {
$pdo->rollBack();
http_response_code(409);
exit;
}
if ($status === 'paid' && $order['payment_status'] !== 'paid') {
$stmt = $pdo->prepare(
"UPDATE orders
SET payment_status = 'paid',
paid_at = CURRENT_TIMESTAMP
WHERE id = :id"
);
$stmt->execute(['id' => $order['id']]);
}
$pdo->commit();
http_response_code(204);
系统还应单独保存 event_id 并进行去重,以免重复处理服务商多次发送的同一通知。示例省略了 webhook 事件表、日志和完整的状态映射,不能直接作为某个服务商的正式实现使用。
费用通常由哪些部分组成
具体价格取决于交易量、行业风险、结算周期和合同。常见收费项目有:
- 商户开户费或接入费;
- 每月服务费;
- 每笔交易的固定费用;
- 按交易金额计算的比例费用;
- 退款、拒付或失败交易费用;
- 证书、技术支持或高级对账服务费用;
- 最低月消费或最低交易量要求;
- 货币兑换费用。如果只收取并结算 SEK,通常可以减少这项成本。
询价时,最好要求服务商按照预计订单量给出完整测算。例如,月成本可以粗略表示为:
月成本 = 月费
+ 成功交易数 × 单笔固定费用
+ 月交易金额 × 比例费率
+ 退款、对账及其他附加费用
只有拿到合同报价,才能估算出可靠的金额。即使服务商公开了价格,实际费用也可能因国家、企业类型和产品组合而不同。
上线前需要确认的事项
选择服务商前,需要确认:
- 是否支持瑞典注册的企业和 SEK 结算;
- 是否确实支持 Swish 或瑞典银行账户付款;
- 是否提供 PHP SDK;如果没有,是否提供稳定的 REST API;
- 是否有测试环境、测试账号和 webhook 调试工具;
- 用户授权后,付款会立即成功,还是可能继续处于处理中;
- 是否可以通过 API 完成退款;
- 结算周期、最低交易量和完整收费结构;
- 是否要求商户协议、企业银行账户或客户端证书;
- 遇到重复通知、超时或状态不一致时,如何查询最终结果;
- GDPR、日志保存和个人数据处理责任如何划分。
对于简单网站,开发和维护成本较低的做法通常不是寻找所谓的“PHP 内置模块”,而是使用支持瑞典本地支付的托管收银台。网站管理订单和业务状态,服务商处理银行连接、用户授权和支付流程,这样更容易控制整体风险。
备注:内容仅供参考。
