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=stripeStripe 跳转地址会自动基于 DOWNCITY_CITY_BASE_URL 生成;未配置时会回退到当前请求 origin,并自动提供两张可直接访问的默认结果页:
GET /v1/payment/redirect/successGET /v1/payment/redirect/cancel
当前阶段真正同步什么
这层不是发放产品权益,也不是复制 Stripe 全量对象。
当前阶段真正做的是:
- 找到对应的 Stripe 支付记录
- 找到它关联的 topup
- 在支付成功时调用
balance.finishTopup() - 把支付记录标记成
paid/expired/failed
事件范围
一次性充值链路只需要很小的 Stripe 事件面:
checkout.session.completed:必须打通的成功路径,用它完成 topupcheckout.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 后会再次匹配并完成同步。
常见场景
场景一:支付成功了,但余额还没到账
最先该检查的是:
- webhook 到 City 了吗
- webhook 事件有没有被记录
- 支付记录有没有更新
- topup 有没有被 finish
场景二:需要人工对账或修复
如果 Stripe 和 City 当前状态不一致,webhook 记录层和支付记录层会是非常重要的依据。
常用 API / 入口
POST /v1/payment/checkout/createPOST /v1/payment/webhook?provider=stripeGET /v1/payment/payments