根据 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 | 定义价格(含周期) | 长期存在,可关联多个产品 |
💡 提示:
PaymentIntent和Checkout 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 开始,最终到达 succeeded 或 canceled:
// 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 自定义方案。
🔧 相关工具推荐
- 🔧 Stripe CLI — 本地测试 Webhook 和 API
- 🔧 Stripe Dashboard — 交易监控和报表
- 🔧 Stripe Test Mode — 完整测试环境
- 🔧 Stripe Radar — 内置欺诈检测
- 🔧 jsjson.com 在线工具 — JSON 格式化、编码转换等开发工具