如何在 PHP 中实现信用卡 payment gateway?
明确结论
不能只用 PHP 独立完成信用卡扣款和资金入账。PHP 可以处理订单、发起支付请求和接收回调,但真正的扣款仍需通过收单银行、支付处理商或持牌支付服务商接入 Visa、Mastercard 等银行卡网络。
实际项目更适合接入成熟支付服务商的 API,并采用托管支付页、Hosted Fields 或前端令牌化组件。不要让信用卡号、有效期和 CVV 经过自己的 PHP 服务器,否则会扩大 PCI DSS 合规范围,也会增加数据泄露风险。
为什么无法绕过中间机构
一笔信用卡支付通常要经过以下环节:
- 用户输入信用卡信息。
- 支付服务商对卡片信息进行令牌化。
- 收单机构通过银行卡网络发送授权请求。
- 发卡银行批准或拒绝交易。
- 支付成功后进入清算和结算流程。
- 服务商按照结算周期将款项转入商户银行账户。
即使选择直接与银行合作,也需要签订收单协议、申请商户号、建立风控体系、满足 PCI DSS 要求,并接入银行提供的网关接口。这类方案通常适合交易规模较大,而且拥有专门支付和安全团队的企业。
推荐的实现方式
比较稳妥的架构如下:
浏览器
├─ 将卡片信息直接提交给支付服务商
└─ 获得 token 或确认 Payment Intent
↓
PHP 后端
├─ 创建支付请求
├─ 绑定内部订单号
├─ 验证金额和币种
└─ 接收并验证 webhook
↓
数据库
└─ 根据 webhook 更新订单状态
后端不能因为浏览器返回了”支付成功”就直接发货。订单的最终支付状态应以通过签名验证的服务端 webhook 为准。
PHP 示例
下面使用 Stripe 的 Payment Intent 模型说明基本流程。其他支付服务商通常也有类似的创建支付、前端确认和异步通知机制。具体字段要以实际使用的服务商及 SDK 版本文档为准。
先安装官方 PHP SDK:
composer require stripe/stripe-php
创建支付请求:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Stripe\StripeClient;
$stripe = new StripeClient($_ENV['STRIPE_SECRET_KEY']);
$orderId = 'ORDER-20260901-001';
$amount = 19900; // 最小货币单位,例如 USD 199.00 = 19900 cents
try {
$paymentIntent = $stripe->paymentIntents->create(
[
'amount' => $amount,
'currency' => 'usd',
'automatic_payment_methods' => [
'enabled' => true,
],
'metadata' => [
'order_id' => $orderId,
],
],
[
'idempotency_key' => 'payment:' . $orderId,
]
);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(
[
'clientSecret' => $paymentIntent->client_secret,
],
JSON_THROW_ON_ERROR
);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(
['error' => 'Unable to create payment'],
JSON_THROW_ON_ERROR
);
}
金额必须由后端根据商品、税费和优惠重新计算,不能直接采用前端提交的最终金额。
浏览器拿到 clientSecret 后,应通过服务商提供的前端组件采集卡片信息并确认支付。卡号和 CVV 不应发送到上面的 PHP 接口。
处理支付回调
受 3-D Secure、银行风控或网络延迟影响,支付可能暂时处于异步状态。因此,订单状态需要通过 webhook 更新,不能只依赖用户付款后跳转到的页面。
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Stripe\Exception\SignatureVerificationException;
use Stripe\Webhook;
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$webhookSecret = $_ENV['STRIPE_WEBHOOK_SECRET'];
try {
$event = Webhook::constructEvent(
$payload,
$signature,
$webhookSecret
);
} catch (UnexpectedValueException | SignatureVerificationException $e) {
http_response_code(400);
exit;
}
switch ($event->type) {
case 'payment_intent.succeeded':
$paymentIntent = $event->data->object;
$orderId = $paymentIntent->metadata->order_id ?? null;
if ($orderId !== null) {
/*
* 在事务中执行:
* 1. 查询本地订单;
* 2. 核对金额、币种和支付对象;
* 3. 如果尚未处理,则标记为已支付;
* 4. 保存 event ID,防止重复处理。
*/
}
break;
case 'payment_intent.payment_failed':
$paymentIntent = $event->data->object;
// 记录失败状态,但不要保存完整支付敏感信息。
break;
}
http_response_code(200);
生产环境还要保存 webhook 的事件 ID,并为该字段建立唯一索引。支付服务商可能重复发送同一个事件,因此处理逻辑必须具备幂等性。
数据库应保存什么
通常只需保存以下信息:
- 内部订单号
- 支付服务商的交易或 Payment Intent ID
- 金额及币种
- 支付状态
- webhook 事件 ID
- 创建和更新时间
- 必要的失败原因代码
以下信息不应保存:
- 完整信用卡号
- CVV/CVC
- 磁道数据
- PIN
- 未加密的支付密钥
即使服务商返回了卡片摘要,通常也只应保存品牌、后四位和有效期等用于展示的非敏感信息。
必须注意的事项
PCI DSS
只要系统接触、传输或存储银行卡数据,就可能需要满足更严格的 PCI DSS 要求。使用服务商的托管页面或令牌化组件可以缩小合规范围,但不代表商户自动免除所有合规责任。
3-D Secure 和强客户认证
部分国家或地区要求 Strong Customer Authentication。支付流程需要支持跳转或额外的用户验证,不能假设一次同步 API 请求必然立即成功。
幂等性
创建支付和处理 webhook 都要支持幂等。网络超时后重试请求时,不能对同一订单重复扣款。
金额校验
金额应以整数形式的最小货币单位表示,并由后端计算。不同货币的小数位规则并不完全相同,不能默认所有货币都有两位小数。
密钥管理
API Secret 和 webhook secret 应存放在环境变量或专用密钥管理系统中,不能写入源码、提交到 Git,也不能发送给浏览器。
HTTPS 和日志脱敏
所有支付接口都必须使用 HTTPS。应用日志不能记录卡号、CVV、完整请求体、密钥或其他支付敏感信息。
如果确实要直接连接收单银行
首先需要向银行申请互联网收单商户资质,并获取银行提供的 API、证书、商户号和测试环境。PHP 端通常需要负责:
- 使用客户端证书建立 TLS 连接
- 按照银行协议生成签名
- 提交授权、撤销、退款和查询请求
- 验证响应签名
- 处理对账文件
- 管理重复请求和异常状态
- 满足 PCI DSS、审计及风控要求
这些工作不是编写一个 PHP 表单就能完成的。除非已经具备收单协议和完整的安全能力,否则接入成熟支付服务商通常成本更低,也更容易正确处理退款、拒付、3-D Secure、风控和资金结算。
备注:内容仅供参考。
