PAYATHON 2026

Online Payment 如何实现 account-to-account transfer?

支付阿杰

明确结论

可以实现,但这类业务通常不属于普通的 account-to-account transfer,而是“平台型支付”或“Marketplace Payments”。

常见做法是让每个 Seller 成为支付服务商的子商户或 Connected Account,由支付服务商负责身份验证、收款和结算。Customer 使用信用卡或借记卡付款时,交易直接创建在对应 Seller 的支付账户下,再由支付服务商把款项结算给 Seller,不需要先进入平台的银行账户。

可选方案包括:

  • Stripe Connect 的 Direct Charges
  • PayPal Commerce Platform
  • Adyen for Platforms
  • 部分地区支付机构提供的 Sub-merchant、Split Payment 或 Marketplace 产品

具体能否使用,要看平台和 Seller 所在国家、经营类型、币种,以及支付服务商当时的开户政策。

为什么普通 Payment Gateway 不够

传统 Payment Gateway 通常只为一个商户服务。如果平台用自己的商户号收款,再把钱打给 Seller,就会形成二次资金流转:

Customer → Platform → Seller

这种资金路径可能涉及代收代付、资金托管、牌照、税务、退款和其他合规责任。

Marketplace 支付的资金路径则是:

Customer → Payment Provider → Seller
                         └── Platform fee(可选)

信用卡付款严格来说并不是银行账户之间的转账。Customer 实际是在向 Seller 发起银行卡支付,由支付服务商完成收单,并将结算款汇入 Seller 的银行账户。

即使平台不经手资金,通常仍要负责以下工作:

  • 引导 Seller 完成 KYC/KYB 身份认证
  • 保存 Seller 与支付账户的关联关系
  • 为订单创建支付
  • 处理支付状态和 Webhook
  • 明确退款、拒付和负余额的责任归属
  • 遵守平台所在地的税务及消费者保护要求

推荐实现方式

以 Stripe Connect 为例,如果希望交易和资金都归属于 Seller,可以优先研究 Direct Charges,而不是先把款项收进平台余额,再转给 Seller。

基本流程如下:

  1. Seller 注册平台账户。
  2. 平台为 Seller 创建 Connected Account。
  3. Seller 在支付服务商托管的页面中提交身份和银行账户资料。
  4. 平台保存 Seller 对应的 stripe_account_id。
  5. Customer 下单时,在该 Seller 的 Connected Account 上创建 PaymentIntent。
  6. 支付成功后,Stripe 根据该 Connected Account 的结算设置向 Seller 打款。
  7. 平台通过 Webhook 更新订单状态。

如果一个购物车中包含多个 Seller,通常要拆成多个订单和多笔支付。一般不能通过一次普通的银行卡扣款,把资金随意拆分给多个独立商户。

Rails 示例

以下代码只展示核心流程。API 版本、可用的账户类型和地区限制,应以接入时的官方文档为准。

先为 Seller 创建 Connected Account:

account = Stripe::Account.create(
  {
    type: "express",
    country: seller.country_code,
    email: seller.email,
    capabilities: {
      card_payments: { requested: true },
      transfers: { requested: true }
    }
  }
)

seller.update!(stripe_account_id: account.id)

生成 Seller 的开户链接:

account_link = Stripe::AccountLink.create(
  {
    account: seller.stripe_account_id,
    refresh_url: onboarding_refresh_url,
    return_url: onboarding_complete_url,
    type: "account_onboarding"
  }
)

redirect_to account_link.url, allow_other_host: true

Customer 下单后,在 Seller 的 Connected Account 上创建 PaymentIntent:

payment_intent = Stripe::PaymentIntent.create(
  {
    amount: order.total_cents,
    currency: order.currency,
    automatic_payment_methods: {
      enabled: true
    },
    metadata: {
      order_id: order.id
    }
  },
  {
    stripe_account: order.seller.stripe_account_id,
    idempotency_key: "order-#{order.id}-payment"
  }
)

order.update!(
  payment_intent_id: payment_intent.id,
  payment_account_id: order.seller.stripe_account_id
)

amount 通常采用最小货币单位。例如,对于使用两位小数的币种,1000 表示 10.00。并非所有币种都使用两位小数,因此不能对所有币种一律乘以 100。

前端确认支付时,也要在同一个 Connected Account 的上下文中初始化 Stripe,否则可能无法找到对应的 PaymentIntent:

const stripe = Stripe(publicKey, {
  stripeAccount: sellerStripeAccountId
});

如果平台需要收取服务费,可以在支付服务商允许的模式下设置 application_fee_amount。设置后,平台会获得相应费用,因此需要在服务协议、退款规则和账务处理中写清楚:

payment_intent = Stripe::PaymentIntent.create(
  {
    amount: order.total_cents,
    currency: order.currency,
    application_fee_amount: order.platform_fee_cents,
    automatic_payment_methods: {
      enabled: true
    }
  },
  {
    stripe_account: order.seller.stripe_account_id,
    idempotency_key: "order-#{order.id}-payment"
  }
)

Webhook 处理

不要仅凭浏览器的跳转结果把订单标记为已支付。付款可能需要额外验证,也可能异步失败。订单状态应以通过签名验证的 Webhook 为准。

class StripeWebhooksController < ApplicationController
  skip_forgery_protection

  def create
    payload = request.raw_post
    signature = request.headers["Stripe-Signature"]

    event = Stripe::Webhook.construct_event(
      payload,
      signature,
      ENV.fetch("STRIPE_WEBHOOK_SECRET")
    )

    case event.type
    when "payment_intent.succeeded"
      intent = event.data.object
      order = Order.find_by(id: intent.metadata.order_id)

      order&.update!(payment_status: "paid")
    when "payment_intent.payment_failed"
      intent = event.data.object
      order = Order.find_by(id: intent.metadata.order_id)

      order&.update!(payment_status: "failed")
    end

    head :ok
  rescue JSON::ParserError, Stripe::SignatureVerificationError
    head :bad_request
  end
end

实际实现还要保证 Webhook 幂等,避免同一个事件重复送达后造成重复发货或重复修改库存。处理 Connected Account 事件时,也要记录事件所属的账户,防止不同 Seller 的订单发生串联。

需要特别确认的事项

Seller 必须完成支付平台开户

平台不能只收集 Seller 的银行卡号,再把信用卡款项直接转过去。Seller 通常要接受支付服务商的协议,并完成个人或企业身份验证。

“平台不经手资金”不等于平台没有责任

以下责任需要提前确认由谁承担:

  • 信用卡手续费
  • Customer 退款
  • Chargeback 和争议处理
  • Seller 账户出现负余额
  • 欺诈交易损失
  • Seller 无法完成 KYC 时的订单处理
  • 税务申报及交易凭证

支付模式不同,平台与 Seller 之间的责任划分也可能完全不同。

不要自行保存银行卡信息

建议使用 Payment Element、Checkout 或支付服务商提供的托管组件,让银行卡数据直接提交给支付服务商,从而缩小 PCI DSS 合规范围。

一个订单最好只对应一个 Seller

如果 Customer 同时购买多个 Seller 的商品,可以按 Seller 拆分订单:

Cart
├── Seller A order → Seller A PaymentIntent
└── Seller B order → Seller B PaymentIntent

这种方式会产生多笔支付,但退款、对账、拒付和发货之间的对应关系更清楚。如果业务要求只扣款一次再拆分,应选择明确支持 split settlement 的平台型支付产品,并确认这条资金路径在目标地区是否合法可用。

方案选择建议

如果项目使用 Ruby on Rails,而且服务地区支持 Stripe Connect,可以先评估 Direct Charges。它更接近“Customer 向 Seller 付款,平台负责撮合和技术接入”的业务需求。

正式开发前,应向候选支付服务商确认:

  1. 是否支持平台或 Marketplace 模式;
  2. Seller 是否可以作为独立子商户入驻;
  3. 交易是否记在 Seller 名下;
  4. 款项是否直接结算给 Seller;
  5. 平台能否收取服务费;
  6. 退款、拒付和负余额由谁承担;
  7. 所在国家、币种和商品类别是否受支持。

不要用普通商户账户模拟 Marketplace 的资金分发。即使技术上可以先收款再转账,这样做也可能违反支付服务商的条款,并带来额外的资金合规风险。

备注:内容仅供参考。