Stripe Connect 与订阅计费
在一个 Stripe 账户上处理两种资金流。市场平台向买家收取一次费用,然后将款项分发给多个商户(Connect,分离收款与转账)。平台订阅非在线地向这些商户收取定期费用(Billing)。它们共享一个客户端、一对 Webhook 和一种货币类型。
塑造一切的洞见是:**买家的收款和商户的付款是独立失败的。**当结算运行时,资金已经转移,因此被拒绝的转账绝不能导致收款或 Webhook 失败,并且必须保持可再次驱动的能力,而不会向任何人支付两次。
何时使用
- 一笔支付分给多个卖家,平台抽取分成。
- 卖家入驻 Connect,并在延迟后获得付款(托管、储备、KYC)。
- 平台使用保存的卡片向自己的卖家收取定期费用。
- 关联账户看起来健康,但从未收到过任何资金。
何时不应使用
| 不应使用此方案 | 改用 |
|---|---|
| 单卖家结账,无需拆分 | 直接使用 Stripe Checkout / Payment Element |
| PayPal 市场入驻 + 付款 | 一个 PayPal 技能包;只有接缝处是共享的 |
| 卡片 UI、Payment Element 样式 | 你的设计系统;此技能包是服务端 |
架构
买家 ──收款(平台账户,transfer_group = orderId)──▶ 平台余额
│ 结算扇出(Webhook 或读取路径同步;租约声明)
┌────────────┴──┬────────────────────────┬────────────────┐
▼ ▼ ▼ ▼
转账:商户 转账:合作伙伴 托管持有 滚动储备
(source_transaction = 该收款) (release_at) (% 扣留)
│ │ 释放托管 cron
▼ ▼
关联账户余额 ◀── 转账重试 应付余额 ◀──▶ 付款(受 KYC/风险控制)
(未资助的分项)
平台 ──off_session PaymentIntent(客户 + 保存的 PM)──▶ 订阅发票
催缴:3 次尝试,每次间隔 3 天,然后暂停
关键事实
- **分离收款与转账,永远不要使用目的地收款。**一次收款只能有一个
destination;多供应商购物车需要在一个transfer_group下有 N 笔转账,因此application_fee_amount不可用。 - 没有
source_transaction的转账会从 可用 余额中提取,在收款仍在结算中时会被拒绝;在一个新平台账户上,每笔转账都是如此。这个标志正是让拆分在收款仍在结算时就能获得资金的关键。 - **Stripe 转账仅在美国/加拿大/英国/欧洲经济区/瑞士内跨境进行。**其他地区的平台只能向本国境内的关联账户付款。Stripe 在转账时而非入驻时强制执行此规则,因此模板在入驻时应用地区规则。
- **Connect 事件使用与平台事件不同的签名密钥。**两个端点,两个密钥,一个 URL——必须同时验证两者。
- 关联账户的国家是不可变的——错误的国家意味着重新入驻,而不是修补。
- 金钱永远不是浮点数——从始至终使用
numeric(19,4)字符串,bigint数学运算,仅在 SDK 边界使用最小单位。
硬性规则
**绝不能让失败的转账导致结算失败。**将分项记录为未资助并继续。收款已经成功,因此 Webhook 也必须成功;重试 cron 之后会为此分项提供资金。
**绝不要将重复的 Webhook 插入视为“已处理”。**插入是一种声明;在传递处理中途崩溃后的重试会重新运行处理程序,因此每个事件都恰好完整执行一次。
**在未向 Stripe 询问转账是否已存在之前,绝不要重新发送转账。**由于响应丢失而记录为未资助的分项,一旦 Stripe 的 24 小时幂等性窗口过去,将会支付两次。
**绝不要从末尾写入的状态标志中推导结算独占性。**两个调用者经常会同时到达结算;原子性的租约声明才能确保库存和转账只发生一次。
**绝不要撤销超过转账剩余的空间。**堆叠的撤销(网关费用,然后退款)如果超过原始金额将被拒绝。
快速开始
- 建模货币—— money.md, data-model.md, store.md。
- 客户端、双 Webhook 密钥、环境变量—— stripe-adapter.md。
- 入驻账户;首先检查地区规则—— connect-accounts.md。
- 幂等地接收事件—— webhooks.md。
- 结算:声明、扇出、托管、储备—— settlement.md。
- 收敛:重试分项、对账费用—— reconciliation.md。
- 向租户计费—— subscriptions.md;使用 operations.md 运行。
通过 adaptation.md 将其适配到你的应用;审计记录在 provenance.md 中。
适配契约
| 接缝 | 技能包提供 | 宿主应用提供 |
|---|---|---|
| 领域实体 | Tenant(卖家组织)、Order、VendorOrder、Partner | 其自身词汇 |
| 租户范围 | 一个服务器派生的 tenantId | 组织 / 工作空间 / 卖家 |
| 认证守卫 | 每个路由的适配器签名 | Clerk、NextAuth、Supabase、自定义 |
| 数据访问 | PaymentsStore 契约 + SQL | 其 ORM 或 SDK |
| 审计 + 通知 | 调用点、事件名称、载荷形状 | 其审计表 / 发件箱 |
| 后台工作 | 四个 cron 及其节奏 | 其调度器 |
| 验证 | 每个路由体的模式形状 | zod / valibot / yup |
| UI | 无——此技能包是服务端 | 全部 UI |
参考目录
| 场景 | 触发关键词 | 参考文档 |
|---|---|---|
| 为何采用此货币模型 | 分离收款与转账, 目的地收款, application_fee_amount, transfer_group, 账本 | architecture.md |
| 表、列、行级安全 | 模式, 迁移, payment_intents, transfers, escrow_holds, webhook_events | data-model.md |
| 数据访问契约 | store, 仓库, 原子声明, 条件更新, ORM | store.md |
| 金额与舍入 | numeric, bigint, 最小单位, 分配, 最大余数, 分 | money.md |
| 客户端与适配器代码 | stripeClient, apiVersion, constructEvent, whsec_, PaymentIntent, refund, payout | stripe-adapter.md |
| 入驻、账户状态 | Express, Accounts v2, account.updated, charges_enabled, acct_, 国家, 地区 | connect-accounts.md |
| 安全接收事件 | 幂等性, 重复, 重放, 原始体, 签名, invalid_signature | webhooks.md |
| 拆分已付款订单 | 结算, 扇出, source_transaction, 托管, 储备, 单次执行 | settlement.md |
| 修复未成功的部分 | 重试, 未资助, 退避, 采用, 网关费用, 收回, 撤销, 空间 | reconciliation.md |
| 定期平台费用 | SetupIntent, off_session, 保存的卡片, 催缴, 发票, past_due, 暂停 | subscriptions.md |
| 设置、环境变量、cron、调试 | STRIPE_SECRET_KEY, webhook 端点, stripe listen, cron, “商户从未收款” | operations.md |
| 适配至此应用 | 适配, 宿主探测, 重命名, ORM, 认证守卫, 移植 | adaptation.md |
| 审计记录 | 来源, 偏差, 保留, 添加, 未验证 | provenance.md |