支持 CC Storage、Authorize and Capture 的 PHP 支付服务商
明确结论
有较为完整的方案可以满足这些要求。对于大多数采用 PHP + MariaDB 的 marketplace 项目,建议优先评估 Stripe Payments + Stripe Connect。如果交易量较大、涉及多个国家,或需要定制收单方案,也可以同步评估 Adyen for Platforms。
这两类平台通常支持:
- 前端银行卡组件和 tokenization
- 保存支付凭证后由后端发起扣款
- Separate Authorization and Capture
- 全额或部分退款
- Marketplace 分账及平台佣金
- 收款人的 KYC 和 payouts
- PHP SDK、REST API、Webhooks 和移动端 SDK
不过,没有哪家全球 PSP 能在不考虑国家、行业和交易流程的情况下,保证支持所有功能。以下三个限制尤其需要提前确认:
- 个人 KYC 和 payout 是否开放,取决于收款人所在国家、平台主体所在地和业务类型。
- 银行卡授权不能无限期冻结。 Capture 必须在发卡行和卡组织规定的授权有效期内完成。
- 跨境分账并非适用于所有国家组合。 限制不只来自 API,还涉及支付牌照、资金流和当地监管。
因此,更稳妥的结论是:Stripe Connect 很可能是最接近这些需求的标准化方案,但仍需使用真实账户、真实国家组合和真实交易流程完成一次端到端验证,不能只依赖销售人员的口头确认。
推荐的整体架构
PHP、MariaDB 和移动应用本身不会限制支付服务商的选择。架构上的重点是让敏感卡数据只进入 PSP 提供的安全组件:
浏览器或移动端
│
│ Card data
▼
PSP Elements / Hosted Fields / Mobile SDK
│
│ PaymentMethod ID / token
▼
PHP Backend
│
├── MariaDB:保存 PSP customer ID、PaymentMethod ID、订单状态
├── PSP API:创建 authorization、capture、refund
└── Webhook:确认异步交易结果
数据库中不要保存 PAN、CVC、磁道数据或银行卡照片。通常只需保存:
- PSP customer ID
- PaymentMethod ID 或 provider-specific token
- Connected Account ID
- PaymentIntent 或 transaction ID
- 订单金额、币种及状态
- Authorization 到期时间
- Capture、refund 和 payout 的关联记录
PSP 返回的 token 通常也不是能在任意账户、商户或环境中使用的通用令牌。它一般只能在所属 customer、merchant account、platform account 以及对应的测试或生产环境中使用。
使用 Stripe Connect 实现
下面用 Stripe 的对象模型说明完整流程。代码仅用于展示架构,部署前需要按照当前 SDK 版本和账户能力核对相关字段。
1. 创建 Customer 和 SetupIntent
后端先为客户创建 Stripe Customer,再创建 SetupIntent,用于保存后续支付所需的银行卡凭证。
<?php
require 'vendor/autoload.php';
$stripe = new \Stripe\StripeClient($_ENV['STRIPE_SECRET_KEY']);
$customer = $stripe->customers->create([
'email' => $authenticatedUserEmail,
'metadata' => [
'internal_user_id' => (string) $authenticatedUserId,
],
]);
$setupIntent = $stripe->setupIntents->create([
'customer' => $customer->id,
'payment_method_types' => ['card'],
'usage' => 'off_session',
]);
echo json_encode([
'clientSecret' => $setupIntent->client_secret,
]);
浏览器端通过 Stripe.js 和 Elements 收集银行卡资料。银行卡号不能先提交到平台自己的 PHP 接口。
const stripe = Stripe(publishableKey);
const elements = stripe.elements();
const card = elements.create("card");
card.mount("#card-element");
const result = await stripe.confirmCardSetup(clientSecret, {
payment_method: {
card,
billing_details: {
name: customerName
}
}
});
if (result.error) {
console.error(result.error.message);
} else {
const paymentMethodId = result.setupIntent.payment_method;
// 将 PaymentMethod ID 提交给自己的后端保存
}
移动应用也应通过 PSP 官方移动 SDK 完成相同流程,不能让 App 把原始 PAN、expiry 和 CVC 发送到自己的 REST API。
使用 Elements 或 Hosted Fields 可以大幅缩小 PCI DSS 的适用范围,但不代表平台完全没有 PCI 义务。平台通常仍需完成相应的 PCI 自评,同时保证页面、JavaScript 依赖和服务器配置安全。
2. 创建只授权、不立即 Capture 的付款
客户点击支付按钮后,PHP 后端必须先验证登录身份、订单归属、价格和幂等键。最终金额不能直接采用客户端提交的值。
<?php
$stripe = new \Stripe\StripeClient($_ENV['STRIPE_SECRET_KEY']);
$paymentIntent = $stripe->paymentIntents->create(
[
'amount' => $orderAmountMinorUnit,
'currency' => $orderCurrency,
'customer' => $stripeCustomerId,
'payment_method' => $paymentMethodId,
'capture_method' => 'manual',
'confirm' => true,
'off_session' => true,
'metadata' => [
'order_id' => (string) $orderId,
'user_id' => (string) $authenticatedUserId,
],
],
[
'idempotency_key' => 'authorize-order-' . $orderId,
]
);
金额应使用最小货币单位。例如,1000 可能代表 10.00。对于零小数货币等特殊情况,需要单独处理,不能统一乘以 100。
授权成功后,PaymentIntent 通常会进入等待 Capture 的状态。业务系统不能只凭前端返回值判断交易成功,还应核对 API 响应以及通过签名验证的 Webhook。
如果保存的银行卡需要 SCA,或发卡行要求客户重新认证,off_session 支付可能返回需要认证的状态或错误。这时必须引导客户回到前端完成验证。因此,无法绝对保证保存银行卡后的每笔支付都能在后台无交互授权。
3. 服务交付后 Capture
服务完成后,根据数据库中保存的 PaymentIntent ID 执行 Capture:
<?php
$stripe = new \Stripe\StripeClient($_ENV['STRIPE_SECRET_KEY']);
$paymentIntent = $stripe->paymentIntents->capture(
$paymentIntentId,
[],
[
'idempotency_key' => 'capture-order-' . $orderId,
]
);
如果业务支持部分 Capture,可以传入实际结算金额:
$paymentIntent = $stripe->paymentIntents->capture(
$paymentIntentId,
[
'amount_to_capture' => $finalAmountMinorUnit,
],
[
'idempotency_key' => 'capture-order-' . $orderId,
]
);
授权有效期不能由平台任意设定。实际期限可能受到卡组织、卡种、国家、MCC、线上或线下交易方式,以及交易是否符合 extended authorization 条件等因素影响。
如果服务需要数周才能完成,就不能假设一次普通银行卡授权可以始终保留。常见的处理方式有:
- 缩短预约到 Capture 的时间
- 在授权过期前取消并重新授权
- 先保存支付方式,接近履约时间时再授权
- 符合条件时申请 extended authorization
- 改用订金加尾款模式
无论选择哪种方案,都需要在服务条款和客户授权中说明清楚。
4. 对已 Capture 的交易退款
可以按 PaymentIntent 发起全额或部分退款:
<?php
$stripe = new \Stripe\StripeClient($_ENV['STRIPE_SECRET_KEY']);
$refund = $stripe->refunds->create(
[
'payment_intent' => $paymentIntentId,
'amount' => $refundAmountMinorUnit,
'metadata' => [
'order_id' => (string) $orderId,
],
],
[
'idempotency_key' => 'refund-order-' . $orderId . '-' . $refundSequence,
]
);
退款成功后,款项不会立即回到客户银行卡。最终入账时间由 PSP、卡组织和发卡行共同决定。
Marketplace 退款还要处理平台佣金,以及已经转给服务提供者的资金。在不同的 Connect 收款模型下,退款是否自动冲回 transfer、是否退还 application fee、负余额由谁承担,规则并不相同。这些问题必须在设计阶段确定。
Split Payments 和平台佣金
Stripe Connect 常见的 marketplace 收款方式包括:
- Destination charges
- Separate charges and transfers
- Direct charges
例如,可以使用 destination charge 将净额转给 Connected Account,同时由平台保留佣金:
<?php
$paymentIntent = $stripe->paymentIntents->create(
[
'amount' => 10000,
'currency' => 'eur',
'customer' => $stripeCustomerId,
'payment_method' => $paymentMethodId,
'capture_method' => 'manual',
'confirm' => true,
'application_fee_amount' => 1500,
'transfer_data' => [
'destination' => $connectedAccountId,
],
'metadata' => [
'order_id' => (string) $orderId,
],
],
[
'idempotency_key' => 'authorize-order-' . $orderId,
]
);
这个例子的总额为 10000,平台佣金为 1500。实际余额还会受到支付手续费、退款、争议、汇率转换和 Connect 配置的影响,不能简单认定服务提供者最终一定会收到 8500。
选择资金流时,需要回答这些问题:
- 谁在法律上是 merchant of record
- 客户账单上显示谁的名称
- 谁承担 chargeback 和负余额
- 平台能否延迟向服务提供者 payout
- 是否需要把一笔付款拆给多个收款人
- 退款时是否自动撤销 transfer
- 目标国家是否允许该跨境资金流
个人 KYC 和 Payouts
Connect 可以表达个人经营者身份。例如,在支持的地区可以使用 business_type=individual。但这不代表所有国家都允许个人成为 Connected Account。
<?php
$account = $stripe->accounts->create([
'type' => 'express',
'country' => $providerCountry,
'email' => $providerEmail,
'business_type' => 'individual',
'capabilities' => [
'transfers' => ['requested' => true],
],
]);
随后创建 Account Link,将个人引导至 PSP 托管的 KYC 页面:
$accountLink = $stripe->accountLinks->create([
'account' => $account->id,
'refresh_url' => 'https://example.com/onboarding/refresh',
'return_url' => 'https://example.com/onboarding/complete',
'type' => 'account_onboarding',
]);
需要申请哪些 capabilities,应根据选用的 charge model、国家和业务类型决定。不能直接照搬示例,为所有账户申请相同的能力。
正式选型前,应向 PSP 提交一张国家矩阵:
| 项目 | 需要确认的内容 |
|---|---|
| 平台所在地 | 能否开通 marketplace 产品 |
| 客户所在地 | 能否完成收单及 SCA |
| 服务提供者所在地 | 是否允许个人 KYC 和 payouts |
| 结算币种 | 是否支持本地结算 |
| 跨境方向 | 是否允许 platform 向该国家转账 |
| 行业类型 | 是否属于 restricted business |
| 资金留存 | 是否允许延迟 payout |
| 税务 | 是否需要平台代报或代扣 |
如果某个国家只支持公司账户,即使 API 中存在 individual 字段,也不能改变当地的准入规则。
安全实现中的注意事项
理解“Secured public key authentication”时,需要区分前端和后端:
- Publishable key 可以放在浏览器或移动端,但只能执行受限操作。
- Secret API key 只能保存在后端密钥管理系统中。
- 后端请求一般使用 secret credential,不能只凭 public key 获得支付权限。
- Webhook 必须验证签名,并避免重复处理同一事件。
- 每次创建、Capture、refund 和 transfer 都应使用 idempotency key。
- 数据库更新应采用状态机,防止重复 Capture 或重复退款。
- 金额、币种、Connected Account ID 和订单归属必须由后端确定。
- 不应记录 client secret、API secret、CVC 或完整银行卡号。
- 测试和生产环境中的 customer、token、PaymentMethod 不能混用。
订单状态可以设计为:
created
→ payment_method_ready
→ authorization_pending
→ authorized
→ capture_pending
→ captured
→ partially_refunded / refunded
authorized
→ authorization_cancelled
→ authorization_expired
Webhook 处理器需要安全处理乱序事件。例如,退款事件可能早于业务接口响应写入数据库,系统不能假设事件总会按照调用顺序到达。
是否需要考虑 Adyen for Platforms
Adyen for Platforms 同样支持 tokenization、manual capture、refunds、分账、账户持有人验证和 payouts,适合涉及多个国家、交易量较高,或需要统一线上线下支付的企业。
它与 Stripe Connect 的主要差别通常不在 PHP 或 REST API,而在以下方面:
- 商务准入和最低交易规模
- 国家及支付方式覆盖
- 账户结构和资金流配置
- KYC/KYB 模型
- 合同、费率和实施周期
- 平台承担的风险责任
Adyen 是否支持某个国家的个人收款人,也需要根据平台所在地、收款人所在地和行业向官方确认。不能因为平台支持 KYC,就推断它同时支持个人 KYC。
选型时要求 PSP 完成的验收测试
不要只让销售人员回复一份功能清单。应要求候选 PSP 在测试环境或受控生产环境中完成以下完整流程:
- 个人服务提供者注册并通过 KYC。
- 客户通过 Hosted Fields、Elements 或移动 SDK 保存银行卡。
- 后端使用保存的凭证创建一笔 manual-capture authorization。
- 模拟 SCA、余额不足和银行卡拒付。
- 隔数日执行全额和部分 Capture。
- 执行全额及部分退款。
- 平台扣除佣金后向个人账户分账。
- 将余额 payout 到个人银行账户。
- 模拟退款发生在 payout 之后的资金回收。
- 模拟 chargeback、负余额和 Connected Account 被限制。
- 使用实际需要的国家、币种和跨境方向测试。
- 书面确认 authorization validity、费率和不支持的场景。
合同或技术确认中还应写明:
- 哪个实体负责 PCI DSS
- 谁承担 fraud 和 chargeback
- Currency conversion 使用什么汇率及附加费
- Refund 是否退回原支付手续费
- Platform commission 是否随退款退还
- Payout schedule 和 reserve 规则
- 账户关闭后如何处理退款及争议
- API 变更和 Webhook 版本策略
综合判断,Stripe Connect 是这类 PHP marketplace 项目较为现实的首选,Adyen for Platforms 则是值得同步验证的企业级备选。方案能否落地,主要取决于目标国家是否允许个人 KYC、授权有效期有多长、跨境资金流是否合规、分账责任如何划分,以及退款后如何追回资金,而不是是否存在 PHP SDK。
备注:内容仅供参考。
