Stripe Payment 服务

Webhook 与同步

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

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

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

为什么 webhook 要单独记录

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

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

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

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

启用示例

base.use(new PaymentService({
  readTopup: async (topup_id) => await balance.readTopup(topup_id),
  finishTopup: async (topup_id, extra) => await balance.finishTopup(topup_id, extra),
  providers: [
    stripePaymentProvider({
      secret_key: process.env.STRIPE_SECRET_KEY,
      webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
    }),
  ],
}));

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

POST /v1/payment/webhook?provider=stripe

Stripe 跳转地址会自动基于 DOWNCITY_CITY_BASE_URL 生成;未配置时会回退到当前请求 origin,并自动提供两张可直接访问的默认结果页:

  • GET /v1/payment/redirect/success
  • GET /v1/payment/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() 成为唯一的钱包到账入口。

事件处理采用带租约的 processing 状态。同一个 event_id 的并发请求只有一个会执行同步;处理失败会记录为 failed,Stripe 后续重发同一事件时会重新处理。进程在处理中退出时,租约过期后也可以由下一次重发接管,不需要手工删除事件记录。

如果 webhook 比本地 payment 回填更早到达,事件会保留为 pending,而不是写成永久 ignored。Stripe 重发同一 event_id 后会再次匹配并完成同步。

常见场景

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

最先该检查的是:

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

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

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

常用 API / 入口

  • POST /v1/payment/checkout/create
  • POST /v1/payment/webhook?provider=stripe
  • GET /v1/payment/payments