Skip to content
返回变现与收入

Stripe 集成

如何为 SaaS 端到端集成 Stripe Checkout + webhooks + 客户门户。

为什么

Stripe 是 SaaS 的默认支付处理器,因为它开箱即用处理 PCI 合规、135+ 种货币和成熟的 webhook 系统。自己建支付意味着存储卡号,这会让你进入 PCI-DSS Level 1 审计范围 — 每年 5 万美元以上的审计成本。Stripe Elements + Checkout 把卡输入转移到 Stripe 托管的 iframe,你的服务器永远看不到 PAN,保持 PCI-SAQ-A 级别。webhook 架构是 Stripe 健壮的根源:幂等事件、指数退避自动重试、签名载荷证明事件来自 Stripe。第一次就集成正确,能避免一类后来修复昂贵的 bug — 重复扣款、漏发权益、孤儿订阅。

何时使用

当你上线付费 SaaS 或给免费产品加付费计划时。写任何结账代码前,先确认 Stripe 在你的国家可用且支持你的业务类型(部分限制数字服务)。

如何做

1. 创建 Stripe 账号,验证业务信息,仅在测试模式完整 E2E 测试后才切到 live 模式
2. 安装官方 SDK:npm install stripe,用环境变量中的密钥初始化(绝不提交)
3. 通过 Dashboard 或 API 为每个计划创建 Product + Price(stripe.products.create / stripe.prices.create)
4. 服务端创建结账会话:stripe.checkout.sessions.create,mode='subscription',含 line_items、success_url、cancel_url
5. 在 /api/stripe-webhook 实现 webhook 处理器:用原始 body 和 webhook secret 通过 stripe.webhooks.constructEvent 验证签名
6. 至少处理这些事件:checkout.session.completed、customer.subscription.updated、customer.subscription.deleted、invoice.payment_failed
7. 让每个 webhook 处理器幂等:先检查是否已处理过该 event id 再行动(把 event id 存表)
8. 嵌入客户门户(stripe.billingPortal.sessions.create)让用户自助改卡 / 取消,无需你的客服
9. 上线前用 stripe.webhooks.generateTestHeaderString 和 fixture 事件写 E2E 测试

示例

// 服务端:创建结账会话
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

const session = await stripe.checkout.sessions.create({
  mode: "subscription",
  line_items: [{ price: "price_abc123", quantity: 1 }],
  success_url: "https://app.example.com/billing?session_id={CHECKOUT_SESSION_ID}",
  cancel_url: "https://app.example.com/pricing",
  customer_email: user.email,
  metadata: { userId: user.id },
});

// webhook 处理器(需要原始 body)
app.post("/api/stripe-webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const sig = req.headers["stripe-signature"]!;
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err) {
    return res.status(400).send("签名无效");
  }
  if (await processedAlready(event.id)) return res.json({ received: true });
  switch (event.type) {
    case "checkout.session.completed":
      await grantEntitlement(event.data.object.metadata.userId);
      break;
    case "invoice.payment_failed":
      await dunningWorkflow(event.data.object.customer);
      break;
  }
  await markProcessed(event.id);
  res.json({ received: true });
});

常见问题

  • ⚠用解析后的 JSON body 而非原始 body 验证 webhook 签名 — 签名是对精确字节计算的,body-parser 会破坏它。仅在 webhook 路由用 express.raw。
  • ⚠webhook 处理器不做幂等 — Stripe 在任何非 2xx 响应时重试,每次调用都授权的处理器在重试时重复授权。存 event id,已处理则跳过。
  • ⚠生产用测试密钥或反之 — 密钥以 sk_test_ 和 sk_live_ 开头,但配置 bug 可能发错。加启动断言,密钥前缀不匹配 NODE_ENV 时快速失败。
  • ⚠漏处理 customer.subscription.deleted — 用户取消但你的 app 继续授权,因为只监听 checkout.session.completed。权益应从当前订阅状态派生,而非结账事件。

最佳实践

  • ✓首次结账时把 stripe_customer_id 存到用户行上 — 客户门户、退款、催收都需要它,每次扣款创建重复客户会破坏订阅管理。
  • ✓所有扣款和退款请求用 Stripe 幂等键(Idempotency-Key 头),网络重试不会重复扣款。
  • ✓在 Dashboard 订阅 webhook 事件时只选你处理的事件 — 接收你忽略的事件浪费请求,且意外开始处理时增加 bug 风险。
  • ✓开发时用 Stripe CLI 本地监听(stripe listen --forward-to localhost:3000/api/stripe-webhook),无需部署即可测试 webhook。