Stripe 支付集成实战:从 Checkout 到 Subscription 的全栈 TypeScript 开发指南

深入解析 Stripe 支付集成全流程,涵盖 Checkout Session、Payment Intents、订阅计费、Webhook 事件处理与安全最佳实践,附完整 TypeScript/Node.js 代码示例,帮你用最短路径构建全球收款能力。

API 设计 2026-06-04 18 分钟

根据 Stripe 2025 年度报告,全球已有超过 340 万家企业在使用 Stripe 处理在线支付,年处理交易额突破 1.1 万亿美元。对于中国开发者来说,Stripe 是产品出海的首选支付方案——支持 135+ 种货币、20+ 种支付方式,且提供极其开发者友好的 API 和文档。但「集成 Stripe」和「正确集成 Stripe」是两回事:Webhook 丢事件导致订单状态不一致、订阅计费逻辑出错引发客诉、安全漏洞导致 PCI 合规问题——这些都是真实生产事故。

本文不讲概念,直接上手。从一次性支付到订阅计费,从前后端代码到 Webhook 处理,用完整的 TypeScript 代码带你走通 Stripe 集成的每一步。

📌 记住: Stripe 的 API 设计哲学是「渐进式复杂度」——先用 Checkout Session 5 分钟跑通支付,再按需切换到 Payment Intents 自定义流程。不要一开始就挑战最复杂的方案。

💳 一、核心概念与快速接入

1.1 Stripe 支付架构全景

Stripe 的 API 体系围绕几个核心对象构建,理解它们的关系是正确集成的前提:

核心对象 作用 生命周期
Customer 代表一个付款方(用户) 长期存在,可关联多张卡
PaymentIntent 代表一次支付意图 从创建到支付成功/失败
Checkout Session Stripe 托管的支付页面 一次性,支付完成后过期
Subscription 代表一个订阅关系 长期存在,按周期自动扣款
Webhook Endpoint 接收 Stripe 事件通知 持续监听
Price 定义价格(含周期) 长期存在,可关联多个产品

💡 提示: PaymentIntentCheckout Session 是两种不同的支付入口。Checkout Session 是 Stripe 托管的完整支付页面(零前端代码),Payment Intents 是你自己构建支付 UI(完全自定义)。大多数项目应该从 Checkout Session 开始。

1.2 五分钟跑通第一个支付

先用最小代码验证 Stripe 集成是否工作:

# 安装 Stripe SDK
npm install stripe @stripe/stripe-js
// server/stripe.ts — 服务端 Stripe 配置
// ⚠️ 永远不要在客户端暴露 sk_test_ 密钥
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2025-04-30.basil', // 使用最新 API 版本
  typescript: true,
})

export default stripe
// server/create-checkout.ts — 创建 Checkout Session
import stripe from './stripe'

async function createCheckoutSession() {
  const session = await stripe.checkout.sessions.create({
    mode: 'payment', // 'payment' | 'subscription' | 'setup'
    payment_method_types: ['card', 'alipay', 'wechat_pay'], // 支持支付宝和微信支付
    line_items: [
      {
        price_data: {
          currency: 'usd',
          product_data: {
            name: 'Pro Plan - 月度订阅',
            description: '解锁全部高级功能',
            images: ['https://your-app.com/images/pro-plan.png'],
          },
          unit_amount: 2999, // $29.99,单位是分(cent)
        },
        quantity: 1,
      },
    ],
    success_url: 'https://your-app.com/payment/success?session_id={CHECKOUT_SESSION_ID}',
    cancel_url: 'https://your-app.com/payment/cancel',
    // 可选:自动收集客户邮箱
    customer_email: 'user@example.com',
    // 可选:允许优惠码
    allow_promotion_codes: true,
    // 可选:自动征收税费
    automatic_tax: { enabled: true },
  })

  return session.url // 重定向用户到此 URL
}
// client/PaymentButton.tsx — 前端 React 组件
import { loadStripe } from '@stripe/stripe-js'

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)

export function PaymentButton() {
  const handleCheckout = async () => {
    // 调用你的后端 API 创建 Checkout Session
    const res = await fetch('/api/create-checkout', { method: 'POST' })
    const { url } = await res.json()

    // 方式一:直接跳转(最简单)
    window.location.href = url

    // 方式二:使用 Stripe.js 重定向(推荐,可追踪转化)
    // const stripe = await stripePromise
    // await stripe?.redirectToCheckout({ sessionId })
  }

  return (
    <button onClick={handleCheckout} className="stripe-button">
      立即购买 $29.99/月
    </button>
  )
}

⚠️ 警告: 永远不要在前端代码中硬编码 sk_test_sk_live_ 密钥。Stripe 的 Publishable Key(pk_test_ / pk_live_)才是前端安全密钥。Secret Key 只存在于服务端环境变量中。

1.3 测试环境与支付模拟

Stripe 提供了完善的测试环境,无需真实扣款即可验证完整流程:

测试卡号 行为 用途
4242 4242 4242 4242 支付成功 基本流程测试
4000 0025 0000 3155 需要 3D Secure 验证 测试强认证流程
4000 0000 0000 9995 余额不足失败 测试支付失败处理
4000 0000 0000 0341 支付被拒绝 测试拒绝处理

💡 提示: 测试时使用 exp_date 随意填写未来日期(如 12/30),CVC 填任意 3 位数字即可。

🔄 二、自定义支付流程:Payment Intents

当 Checkout Session 的托管页面无法满足 UI 需求时,切换到 Payment Intents API 自行构建支付体验。

2.1 Payment Intents 工作流

Payment Intents 遵循「状态机」模型,每次支付从 created 开始,最终到达 succeededcanceled

// server/create-payment-intent.ts — 创建支付意图
import stripe from './stripe'

async function createPaymentIntent(amount: number, currency: string, customerId: string) {
  const paymentIntent = await stripe.paymentIntents.create({
    amount, // 单位:分
    currency,
    customer: customerId,
    // 自动确认支付(适用于前端收集卡片信息后直接支付)
    // 若需要服务端手动确认,设为 false
    automatic_payment_methods: {
      enabled: true,
    },
    // 可选:存储支付方式供后续使用
    setup_future_usage: 'off_session', // 'on_session' | 'off_session'
    // 可选:元数据(方便你自己追踪)
    metadata: {
      order_id: 'ORD-2026-001',
      product: 'pro-plan',
    },
  })

  return {
    clientSecret: paymentIntent.client_secret, // 返回给前端
    paymentIntentId: paymentIntent.id,
  }
}
// client/PaymentForm.tsx — 使用 Stripe Elements 构建自定义支付表单
import { useState } from 'react'
import {
  PaymentElement,
  useStripe,
  useElements,
} from '@stripe/react-stripe-js'

export function PaymentForm({ clientSecret }: { clientSecret: string }) {
  const stripe = useStripe()
  const elements = useElements()
  const [error, setError] = useState<string | null>(null)
  const [processing, setProcessing] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!stripe || !elements) return

    setProcessing(true)
    setError(null)

    const { error: stripeError } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/payment/success`,
      },
    })

    // 如果需要 3D Secure 等重定向验证,Stripe 会自动跳转
    // 以下代码只在不需要重定向时执行
    if (stripeError) {
      setError(stripeError.message ?? '支付失败,请重试')
      setProcessing(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      {error && <div className="error-message">{error}</div>}
      <button type="submit" disabled={!stripe || processing}>
        {processing ? '处理中...' : '确认支付'}
      </button>
    </form>
  )
}

2.2 Checkout Session vs Payment Intents 选型

维度 Checkout Session Payment Intents
开发成本 ⭐ 极低(零前端代码) ⭐⭐⭐ 高(需自建 UI)
UI 定制度 ❌ 仅可改 Logo 和颜色 ✅ 完全自定义
支付方式 ✅ 自动适配地区 ⚠️ 需手动配置
3D Secure ✅ 自动处理 ⚠️ 需手动集成
多商品购物车 ✅ 原生支持 ⚠️ 需自建逻辑
税费计算 ✅ 内置 Tax API ⚠️ 需额外集成
订阅计费 ✅ 支持 ⚠️ 需配合 Subscription API
推荐场景 MVP、SaaS 定价页 复杂电商、定制化体验

关键结论: 90% 的 SaaS 项目应该使用 Checkout Session。只有当你的业务需要「支付流程深度集成到应用 UI 中」时才切换到 Payment Intents。

🔁 三、订阅计费:SaaS 收入的基石

订阅是 SaaS 商业模式的核心,Stripe 的 Subscription API 处理了所有复杂性——试用期、按比例计费、升降级、取消和恢复。

3.1 创建订阅产品与价格

// server/setup-subscription.ts — 创建产品和价格
import stripe from './stripe'

async function createSubscriptionProduct() {
  // 1. 创建产品
  const product = await stripe.products.create({
    name: 'Pro Plan',
    description: '包含全部高级功能的订阅方案',
    metadata: {
      plan_id: 'pro',
    },
  })

  // 2. 创建月度价格
  const monthlyPrice = await stripe.prices.create({
    product: product.id,
    unit_amount: 2999, // $29.99/月
    currency: 'usd',
    recurring: {
      interval: 'month',
      // 可选:试用期(天)
      // trial_period_days: 14,
    },
    metadata: {
      billing_cycle: 'monthly',
    },
  })

  // 3. 创建年度价格(通常有折扣)
  const yearlyPrice = await stripe.prices.create({
    product: product.id,
    unit_amount: 29999, // $299.99/年(相当于 $25/月,约 17% 折扣)
    currency: 'usd',
    recurring: {
      interval: 'year',
    },
    metadata: {
      billing_cycle: 'yearly',
    },
  })

  return { product, monthlyPrice, yearlyPrice }
}

3.2 创建订阅与升降级

// server/create-subscription.ts
import stripe from './stripe'

async function createSubscription(customerId: string, priceId: string) {
  const subscription = await stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: priceId }],
    // 支付行为配置
    payment_behavior: 'default_incomplete',
    // 当首次支付失败时自动重试
    payment_settings: {
      save_default_payment_method: 'on_subscription',
      payment_method_options: {
        card: {
          request_three_d_secure: 'automatic',
        },
      },
    },
    // 需要客户端确认的支付方式
    expand: ['latest_invoice.payment_intent'],
  })

  // 返回 client_secret 供前端完成 3D Secure 等验证
  const invoice = subscription.latest_invoice as Stripe.Invoice
  const paymentIntent = invoice.payment_intent as Stripe.PaymentIntent

  return {
    subscriptionId: subscription.id,
    clientSecret: paymentIntent.client_secret,
  }
}

// 升降级订阅
async function upgradeSubscription(subscriptionId: string, newPriceId: string) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId)

  const updatedSubscription = await stripe.subscriptions.update(subscriptionId, {
    items: [
      {
        id: subscription.items.data[0].id,
        price: newPriceId,
      },
    ],
    // 按比例计费(proration)
    proration_behavior: 'always_invoice', // 立即生成差价发票
    // 其他选项:'create_prorations'(下次账单时结算)、'none'(不按比例)
  })

  return updatedSubscription
}

⚠️ 警告: proration_behavior 的选择直接影响用户体验和现金流。always_invoice 会立即向用户收取差价(升级时)或提供余额(降级时),而 create_prorations 会在下一个计费周期结算。推荐 SaaS 产品使用 always_invoice,让用户立刻看到变化。

3.3 订阅生命周期管理

事件 Stripe 处理方式 你的应用需要做的
首次订阅成功 创建 Subscription,状态 active 开通用户权限
续费成功 自动扣款,发送 invoice.paid 无需操作(保持权限)
续费失败 自动重试(Smart Retries),进入 past_due 通知用户更新支付方式
用户取消 当前周期结束后停止(cancel_at_period_end 发挽留邮件,到期关闭权限
用户恢复 重新激活订阅 恢复权限
试用到期 尝试首次扣款 引导用户添加支付方式
// server/handle-subscription-events.ts — 订阅相关事件处理
async function handleSubscriptionEvent(event: Stripe.Event) {
  switch (event.type) {
    case 'customer.subscription.created': {
      const subscription = event.data.object as Stripe.Subscription
      // 开通用户权限
      await grantUserAccess(subscription.customer as string, subscription.items.data[0].price.id)
      break
    }

    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription
      if (subscription.cancel_at_period_end) {
        // 用户请求取消 — 发送挽留邮件
        await sendRetentionEmail(subscription.customer as string)
      }
      // 更新订阅状态(可能包含升降级)
      await updateSubscriptionStatus(subscription)
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription
      // 关闭用户权限
      await revokeUserAccess(subscription.customer as string)
      break
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice
      // 通知用户支付失败,引导更新支付方式
      await notifyPaymentFailed(invoice.customer as string)
      break
    }
  }
}

🔔 四、Webhook 事件处理:支付系统的生命线

Webhook 是 Stripe 集成中最容易出错也最关键的环节。支付状态的最终确认不是靠前端回调,而是靠 Webhook 事件——因为用户可能在支付成功后关闭浏览器,前端回调永远不会到达。

4.1 Webhook 安全验证

⚠️ 警告: 永远不要跳过 Webhook 签名验证。攻击者可以伪造 Webhook 请求来免费激活服务。Stripe 每个 Webhook 请求都包含 Stripe-Signature 头,必须验证。

// server/webhook-handler.ts — 完整的 Webhook 处理器
import Stripe from 'stripe'
import express from 'express'
import stripe from './stripe'

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!

const app = express()

// ⚠️ 重要:Webhook 路由必须使用 raw body,不能使用 JSON 解析中间件
app.post(
  '/api/webhooks/stripe',
  express.raw({ type: 'application/json' }), // 必须是 raw body
  async (req, res) => {
    const sig = req.headers['stripe-signature']!

    let event: Stripe.Event

    try {
      // 验证签名 — 这一步不能跳过!
      event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret)
    } catch (err) {
      console.error('⚠️ Webhook 签名验证失败:', err)
      res.status(400).send(`Webhook Error: ${(err as Error).message}`)
      return
    }

    // 处理事件
    try {
      await handleEvent(event)
      res.status(200).json({ received: true })
    } catch (err) {
      console.error('事件处理失败:', err)
      // 返回 500 让 Stripe 重试
      res.status(500).send('Internal Server Error')
    }
  }
)

async function handleEvent(event: Stripe.Event) {
  switch (event.type) {
    // 支付成功 — 这是最终确认
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session
      await fulfillOrder(session)
      break
    }

    // 异步支付方式确认(如银行转账)
    case 'checkout.session.async_payment_succeeded': {
      const session = event.data.object as Stripe.Checkout.Session
      await fulfillOrder(session)
      break
    }

    // 异步支付失败
    case 'checkout.session.async_payment_failed': {
      const session = event.data.object as Stripe.Checkout.Session
      await markOrderFailed(session)
      break
    }

    // 发票已支付(订阅续费成功)
    case 'invoice.paid': {
      const invoice = event.data.object as Stripe.Invoice
      await extendSubscription(invoice.customer as string)
      break
    }

    default:
      console.log(`未处理的事件类型: ${event.type}`)
  }
}

📌 记住: Webhook 处理函数必须是幂等的(Idempotent)。Stripe 可能因为网络问题重试同一个事件,你的代码不能因为收到同一个事件两次就重复发货或重复扣款。用 event.id 作为幂等键。

4.2 Webhook 最佳实践清单

实践 原因
✅ 验证签名 防止伪造请求
✅ 返回 200 快速响应 超过 30 秒 Stripe 会认为失败并重试
✅ 异步处理复杂逻辑 先返回 200,再在后台处理业务逻辑
✅ 记录所有事件到数据库 方便排查问题和幂等检查
✅ 使用幂等键(event.id 防止重复处理
❌ 在 Webhook 中调用 Stripe API 会导致超时和循环触发
❌ 跳过未处理的事件类型 Stripe 可能新增事件类型,记录即可

🛡️ 五、安全与合规要点

5.1 PCI 合规

使用 Stripe Elements 或 Checkout Session 时,信用卡信息完全不经过你的服务器——Stripe 的前端 SDK 直接将卡片数据发送到 Stripe 服务器,返回一个安全的 Token。这意味着你的 PCI 合规等级自动降到最宽松的 SAQ A

// ❌ 错误写法:自己收集信用卡号 — 需要最严格的 PCI SAQ D 合规
const cardNumber = document.getElementById('card-number') // 危险!
await fetch('/api/charge', { body: JSON.stringify({ cardNumber }) })

// ✅ 正确写法:使用 Stripe Elements — 卡号不经过你的服务器
const { error, paymentMethod } = await stripe.createPaymentMethod({
  type: 'card',
  card: cardElement, // Stripe Elements 组件,数据直达 Stripe
})

5.2 防重复支付

用户可能在网络不稳定时多次点击「支付」按钮,导致重复创建 Payment Intent:

// 使用幂等键防止重复创建
const idempotencyKey = `payment_${userId}_${orderId}`

const paymentIntent = await stripe.paymentIntents.create(
  {
    amount: 2999,
    currency: 'usd',
    customer: userId,
  },
  {
    idempotencyKey, // 相同 key 的重复请求返回同一结果
  }
)

📊 六、支付方式与地区适配

不同地区用户的支付习惯差异巨大,盲目只支持信用卡会损失大量转化:

地区 主流支付方式 渗透率 Stripe 支持
🇺🇸 北美 信用卡、Apple Pay、Google Pay 信用卡 85% ✅ 全部
🇪🇺 欧洲 iDEAL(荷兰)、Bancontact(比利时)、SEPA 本地支付 40% ✅ 全部
🇨🇳 中国 支付宝、微信支付 移动支付 90%+ ✅ 支付宝、微信
🇧🇷 巴西 PIX PIX 70% ✅ PIX
🇯🇵 日本 便利店支付(Konbini) 信用卡 60% ✅ Konbini
🇮🇳 印度 UPI UPI 60%+ ✅ UPI

💡 提示: 在 Checkout Session 中设置 payment_method_types 时,不要手动列举所有支付方式。使用 automatic_payment_methods: { enabled: true } 让 Stripe 根据用户地理位置和设备自动推荐最优支付方式,转化率平均提升 12-18%。

⚠️ 七、常见坑点与避坑指南

7.1 金额单位陷阱

Stripe 所有金额都以最小货币单位表示,不同货币的最小单位不同:

// ❌ 错误:以为金额单位都是「分」
await stripe.paymentIntents.create({
  amount: 2999,  // 美元 OK($29.99),但日元就是 ¥2999 而不是 ¥29.99
  currency: 'jpy',
})

// ✅ 正确:根据货币类型处理金额
function toStripeAmount(amount: number, currency: string): number {
  // 零小数货币:日元、韩元等
  const zeroDecimal = ['jpy', 'krw', 'vnd', 'clp']
  if (zeroDecimal.includes(currency.toLowerCase())) {
    return Math.round(amount)
  }
  return Math.round(amount * 100)
}

7.2 Webhook 顺序问题

⚠️ 警告: Webhook 事件不保证按顺序到达payment_intent.created 可能比 checkout.session.completed 晚到。你的代码必须能处理乱序事件。

7.3 退款处理

// 创建退款
async function processRefund(paymentIntentId: string, amount?: number) {
  const refund = await stripe.refunds.create({
    payment_intent: paymentIntentId,
    amount, // 不传则全额退款
    reason: 'requested_by_customer', // 'duplicate' | 'fraudulent' | 'requested_by_customer'
  })

  // ⚠️ 退款也需要通过 Webhook 确认
  // 监听 charge.refunded 事件,而不是依赖 API 返回值
  return refund
}

✅ 总结与最佳实践清单

实践 推荐
使用 Checkout Session 而非自建支付 UI ✅ 除非有特殊 UI 需求
Webhook 签名验证 ✅ 永远不要跳过
幂等键(Idempotency Key) ✅ 防止重复操作
记录所有 Stripe 事件 ✅ 方便排查问题
使用 Stripe CLI 本地测试 Webhook stripe listen --forward-to localhost:3000/api/webhooks/stripe
前端使用 Stripe.js/Elements ✅ 降低 PCI 合规负担
自动支付方式推荐 ✅ 优于手动列举
零小数货币特殊处理 ✅ 避免金额错误

关键结论: Stripe 集成的核心不是「调 API」,而是「正确处理异步事件」。Webhook 是你支付系统的生命线——签名验证、幂等处理、事件记录缺一不可。先用 Checkout Session 5 分钟跑通支付,再按需升级到 Payment Intents 自定义方案。

🔧 相关工具推荐

📚 相关文章