Stripe Payment 服务

充值单与 Checkout

为什么 Stripe 一次性充值应该建立在 balance topup 之上,而不是直接改余额。

@downcity/services 这一版不是 subscription 或 entitlement 系统,它的核心是:

让 Stripe 一次性支付去完成一笔已经存在的 topup。

为什么必须先有 topup

因为“用户说我要充钱”和“系统确认已经到账”不是一回事。

Downcity 里,更稳定的建模方式是:

  1. 先创建 pending topup
  2. 再去 Stripe 支付
  3. 支付成功后完成 topup

这样做的好处是:

  • 充值金额的可信源在 City,不在前端
  • Stripe 支付记录和内部充值单可以稳定一一对应
  • 到账动作统一收口到 balance.finishTopup()

最常见链路

const topup = await user.service("balance").action("topups/create").invoke({
  credits: 500_000_000,
  note: "recharge",
});

const checkout = await user.service("payment").action("checkout/create").invoke({
  method_id: "stripe",
  topup_id: topup.topup_id,
});

前端跳转到 checkout.checkout_url 后,用户就在 Stripe 完成支付。

并发创建行为

同一个 topup_id + method_id 同时收到多个创建请求时,PaymentService 只会向 Provider 创建一次 Checkout。首个请求持有短租约;租约期间的并发请求返回 409,前端稍后重试即可。Checkout 已创建后,重复请求会返回同一笔 payment 和 checkout_url

服务会把稳定的 payment_id 作为 Stripe 幂等键。即使创建请求在网络返回前中断,后续接管也不会生成第二笔 Stripe Checkout。

为什么不让 Stripe 服务直接收金额

如果让前端直接把金额传给 Stripe 服务,再由它独立发起支付,会有几个问题:

  • 前端金额更容易被篡改
  • 内部没有统一的充值单记录
  • 后续到账、失败、过期都很难对齐

所以这层的关键不是“怎么收钱”,而是“先让 City 拿到可信 topup,再让 Stripe 去完成它”。

常用入口

  • POST /v1/balance/topups/create
  • POST /v1/payment/checkout/create