Stripe Payment 服务

Webhook 与同步

Stripe webhook、支付记录和 topup 到账之间的同步关系。

支付服务最关键的同步链路是:

Stripe 事件
  -> webhook 记录
  -> 更新支付记录
  -> 完成 topup
  -> 用户余额到账

为什么 webhook 要单独记录

因为 Stripe 是支付事实的上游来源。

把 webhook 单独记录下来有几个直接好处:

  • 后续可以排查
  • 支付记录状态可以重建
  • 必要时可以重放或补偿同步

也就是说,webhook 记录层不是多余步骤,而是支付同步的事实底座。

启用示例

base.use(stripePaymentService({
  balance: {
    readTopup: async (topup_id) => await balance.readTopup(topup_id),
    finishTopup: async (topup_id, extra) => await balance.finishTopup(topup_id, extra),
  },
  secret_key: process.env.STRIPE_SECRET_KEY,
  webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
}));

配置好以后,Stripe 应该把事件发到:

POST /v1/payment.stripe/webhook

如果你没有显式配置跳转地址,服务会优先基于 DOWNCITY_CITY_BASE_URL,再回退到当前请求 origin,并自动提供两张可直接访问的默认结果页:

  • GET /v1/payment.stripe/redirect/success
  • GET /v1/payment.stripe/redirect/cancel

当前阶段真正同步什么

这层不是发放产品权益,也不是复制 Stripe 全量对象。

当前阶段真正做的是:

  • 找到对应的 Stripe 支付记录
  • 找到它关联的 topup
  • 在支付成功时调用 balance.finishTopup()
  • 把支付记录标记成 paid / expired / failed

事件范围

一次性充值链路只需要很小的 Stripe 事件面:

  • checkout.session.completed:必须打通的成功路径,用它完成 topup
  • checkout.session.expired:把支付记录标记为过期
  • payment_intent.payment_failed:把支付记录标记为失败

其他 Stripe 事件可以先忽略,等产品明确需要新的支付流再接。

幂等规则

webhook 处理必须在两层保证幂等:

  • 事件级幂等:重复的 Stripe event_id 不能重复应用
  • 到账级幂等:重复成功事件不能让同一笔 topup 重复到账

实践里,先检查已保存的 payment 状态,再让 balance.finishTopup() 成为唯一的钱包到账入口。

常见场景

场景一:支付成功了,但余额还没到账

最先该检查的是:

  1. webhook 到 City 了吗
  2. webhook 事件有没有被记录
  3. 支付记录有没有更新
  4. topup 有没有被 finish

场景二:需要人工对账或修复

如果 Stripe 和 City 当前状态不一致,webhook 记录层和支付记录层会是非常重要的依据。

常用 API / 入口

  • POST /v1/payment.stripe/checkout/create
  • POST /v1/payment.stripe/webhook
  • GET /v1/payment.stripe/payments