支持多个支付网关的 PHP Payment Library
结论
可以。比较稳妥的做法是采用“统一支付接口 + 网关适配器”:业务代码只依赖统一接口,再通过配置选择 PayPal、Stripe 或其他支付网关。
如果想使用现成的 PHP 库,可以考虑 Omnipay。它为多个支付网关提供了相对统一的调用方式,也支持跳转到网关页面完成付款。不过,各网关对支付状态、退款、异步通知和参数的要求并不相同,因此很难做到“更换网关时完全不用改代码”。更实际的目标是保留订单业务代码,只修改配置或更换适配器。
推荐的支付流程
如果网站不希望接触或保存银行卡信息,优先选择托管支付页面:
- 网站创建本地订单并生成唯一订单号。
- 后端调用当前网关的“创建支付”接口。
- 用户跳转到支付网关的托管页面。
- 支付完成后,网关将用户重定向到
return_url。 - 网关通过 Webhook 或服务器回调发送支付结果。
- 网站验证回调签名,并主动查询支付状态。
- 确认付款成功后,将订单标记为已支付。
银行卡信息由支付网关采集和处理。网站通常只保存订单号、网关交易号、金额、币种和支付状态。
使用统一接口隔离网关差异
先定义业务实际需要的能力:
<?php
interface PaymentGatewayInterface
{
public function createPayment(PaymentRequest $request): PaymentResponse;
public function queryPayment(string $transactionId): PaymentStatus;
public function verifyWebhook(
string $payload,
array $headers
): WebhookResult;
}
请求和响应使用自己的数据对象,避免业务层依赖某个网关 SDK 的类型:
<?php
final class PaymentRequest
{
public function __construct(
public readonly string $orderId,
public readonly int $amountMinor,
public readonly string $currency,
public readonly string $returnUrl,
public readonly string $cancelUrl
) {
}
}
final class PaymentResponse
{
public function __construct(
public readonly string $transactionId,
public readonly string $redirectUrl
) {
}
}
金额建议使用最小货币单位。例如,人民币 1000 表示 10.00 CNY,这样可以避免用浮点数计算金额。
通过配置切换支付网关
配置文件只指定当前启用的网关:
<?php
return [
'default' => getenv('PAYMENT_GATEWAY') ?: 'gateway_a',
'gateways' => [
'gateway_a' => [
'api_key' => getenv('GATEWAY_A_API_KEY'),
'webhook_secret' => getenv('GATEWAY_A_WEBHOOK_SECRET'),
],
'gateway_b' => [
'merchant_id' => getenv('GATEWAY_B_MERCHANT_ID'),
'secret' => getenv('GATEWAY_B_SECRET'),
],
],
];
再通过工厂创建相应的适配器:
<?php
final class PaymentGatewayFactory
{
public static function create(array $config): PaymentGatewayInterface
{
return match ($config['default']) {
'gateway_a' => new GatewayAAdapter(
$config['gateways']['gateway_a']
),
'gateway_b' => new GatewayBAdapter(
$config['gateways']['gateway_b']
),
default => throw new InvalidArgumentException(
'Unsupported payment gateway'
),
};
}
}
订单业务代码只需要调用统一接口:
<?php
$config = require __DIR__ . '/payment.php';
$gateway = PaymentGatewayFactory::create($config);
$request = new PaymentRequest(
orderId: 'ORDER-20260901-001',
amountMinor: 1000,
currency: 'CNY',
returnUrl: 'https://example.com/payment/return',
cancelUrl: 'https://example.com/payment/cancel'
);
$response = $gateway->createPayment($request);
header('Location: ' . $response->redirectUrl, true, 302);
exit;
以后更换网关时,通常只需新增适配器并修改 PAYMENT_GATEWAY。订单、库存和发货代码不需要随之调整。
使用 Omnipay 时的基本思路
Omnipay 可以减少适配器层的重复代码,但目标网关对应的驱动需要单独安装。驱动名称、支持的功能和维护状态可能发生变化,使用前应检查其代码仓库和目标支付平台的最新文档。
典型的调用结构如下:
<?php
use Omnipay\Omnipay;
$gateway = Omnipay::create($configuredDriver);
$gateway->initialize($gatewayOptions);
$response = $gateway->purchase([
'amount' => '10.00',
'currency' => 'CNY',
'transactionId' => 'ORDER-20260901-001',
'returnUrl' => 'https://example.com/payment/return',
'cancelUrl' => 'https://example.com/payment/cancel',
])->send();
if ($response->isRedirect()) {
$response->redirect();
}
if ($response->isSuccessful()) {
// 只有网关明确支持同步确认时,才能在这里处理成功状态。
}
throw new RuntimeException($response->getMessage());
这段代码只展示通用结构,不代表所有 Omnipay 驱动都接受完全相同的参数。有些网关还需要 completePurchase()、支付方式标识、客户信息或额外的签名参数,具体应以相应驱动和支付平台的文档为准。
回调处理不能只看浏览器跳转
用户进入支付成功页面,不代表付款已经成功。用户可能关闭页面,返回参数也可能被伪造。订单状态应以经过验证的服务器通知或主动查询结果为准:
<?php
$payload = file_get_contents('php://input');
$headers = function_exists('getallheaders') ? getallheaders() : [];
$result = $gateway->verifyWebhook($payload, $headers);
if (!$result->isValid()) {
http_response_code(400);
exit('Invalid signature');
}
$payment = $gateway->queryPayment($result->transactionId());
if ($payment->isPaid()) {
// 使用数据库事务和唯一约束,确保同一订单只处理一次。
markOrderAsPaid($payment);
}
http_response_code(200);
echo 'OK';
注意事项
- 不要在日志、数据库或异常信息中保存完整卡号、CVV 或敏感认证数据。
- API 密钥和 Webhook 密钥应存放在环境变量或密钥管理服务中,不要提交到代码仓库。
- 必须验证 Webhook 签名,同时检查金额、币种、订单号和商户身份。
- 回调处理必须具备幂等性。同一通知可能重复发送,不能因此重复扣减库存、发货或记账。
- 创建支付请求时应设置唯一的幂等键,防止网络重试产生多笔交易。
- 不要只凭
return_url中的查询参数修改订单状态。 - 保存网关的原始状态,并将其映射为内部状态,例如
pending、paid、failed、refunded。 - 退款、部分退款、预授权、撤销和订阅支付之间差异较大,不适合强行放进一个过度简化的接口。
- 托管支付页面可以显著缩小网站接触银行卡数据的范围,但不代表网站会自动免除所有 PCI DSS、安全或当地合规要求。具体责任需要根据支付集成方式和所在地区确认。
- 选择第三方库时,除了查看支持的网关数量,还要检查维护活跃度、Webhook 支持、错误处理方式,以及目标网关的 API 是否已经升级。
如果项目只需要一次性支付和页面跳转,统一接口加适配器通常就够用了。网关较多时,可以在适配器内部使用 Omnipay。这样既能利用现成驱动,也能避免订单业务直接依赖第三方库。
备注:内容仅供参考。
