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。
基本流程如下:
- Seller 注册平台账户。
- 平台为 Seller 创建 Connected Account。
- Seller 在支付服务商托管的页面中提交身份和银行账户资料。
- 平台保存 Seller 对应的
stripe_account_id。 - Customer 下单时,在该 Seller 的 Connected Account 上创建
PaymentIntent。 - 支付成功后,Stripe 根据该 Connected Account 的结算设置向 Seller 打款。
- 平台通过 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 付款,平台负责撮合和技术接入”的业务需求。
正式开发前,应向候选支付服务商确认:
- 是否支持平台或 Marketplace 模式;
- Seller 是否可以作为独立子商户入驻;
- 交易是否记在 Seller 名下;
- 款项是否直接结算给 Seller;
- 平台能否收取服务费;
- 退款、拒付和负余额由谁承担;
- 所在国家、币种和商品类别是否受支持。
不要用普通商户账户模拟 Marketplace 的资金分发。即使技术上可以先收款再转账,这样做也可能违反支付服务商的条款,并带来额外的资金合规风险。
备注:内容仅供参考。
