PAYATHON 2026

支持 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 能在不考虑国家、行业和交易流程的情况下,保证支持所有功能。以下三个限制尤其需要提前确认:

  1. 个人 KYC 和 payout 是否开放,取决于收款人所在国家、平台主体所在地和业务类型。
  2. 银行卡授权不能无限期冻结。 Capture 必须在发卡行和卡组织规定的授权有效期内完成。
  3. 跨境分账并非适用于所有国家组合。 限制不只来自 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 在测试环境或受控生产环境中完成以下完整流程:

  1. 个人服务提供者注册并通过 KYC。
  2. 客户通过 Hosted Fields、Elements 或移动 SDK 保存银行卡。
  3. 后端使用保存的凭证创建一笔 manual-capture authorization。
  4. 模拟 SCA、余额不足和银行卡拒付。
  5. 隔数日执行全额和部分 Capture。
  6. 执行全额及部分退款。
  7. 平台扣除佣金后向个人账户分账。
  8. 将余额 payout 到个人银行账户。
  9. 模拟退款发生在 payout 之后的资金回收。
  10. 模拟 chargeback、负余额和 Connected Account 被限制。
  11. 使用实际需要的国家、币种和跨境方向测试。
  12. 书面确认 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。

备注:内容仅供参考。