Next.js 电商实战:购物车与 Stripe 支付完整实现指南
凌晨三点,我盯着屏幕上的错误日志,第27次检查 Stripe Webhook 的配置。用户投诉说钱已经扣了,但订单状态还是”待支付”。我整个人都是懵的——明明测试环境跑得好好的,为什么上线就翻车?
说实话,第一次做 Next.js 电商项目的时候,我以为最难的是搭界面、写样式。结果真正动手才发现,购物车状态管理、支付对接、订单流程,每一个环节都是坑。Redux 太重学不动,Context API 性能差,Stripe 文档全是英文看着头大,Webhook 是啥玩意儿更是一头雾水。
最让人崩溃的是,网上的教程要么只讲购物车,要么只讲支付,很少有人把完整流程串起来讲清楚。你可能会想:“到底怎么选状态管理库?""Webhook 到底干嘛用的?""订单状态怎么和支付状态对上?”
这篇文章,我想把这些坑一次性给你填平。我们会用 Zustand 管理购物车状态(轻量、好用),用 Stripe 对接支付(国际主流方案),用 Webhook 处理订单(唯一可靠的方式)。每一步都有完整代码,复制粘贴就能跑。
不知道你有没有经历过那种”终于跑通了”的成就感?跟着这篇文章走,你会有的。
为什么选 Zustand 管理购物车状态?
2025年状态管理选型:别再纠结了
老实讲,状态管理库的选择真的让人头疼。Redux 文档厚得像本字典,Context API 性能问题一搜一大把,Zustand 又感觉太新不敢用。我当时也在这三个之间反复横跳,直到看了一组数据才下定决心。
2021年开始,Zustand 成了 Star 增长最快的 React 状态管理库。到 2025 年,它的设计理念已经被证明:函数式、拥抱 hooks、API 简洁优雅。更关键的是,它的学习曲线真的很平缓,不像 Redux 那样要理解一堆概念(action、reducer、dispatch、middleware…)。
那到底怎么选?我给你画张图:
- 小项目(<10个页面):Context API 就够了,别折腾
- 中型项目(10-50个页面):Zustand,轻量且够用
- 大型项目(50+页面,多团队协作):Redux Toolkit,工具链完善
购物车这种场景,其实挺适合 Zustand 的。为啥?因为它需要跨组件共享(商品列表、购物车图标、结算页都要用),需要持久化(刷新页面不能丢数据),还得性能好(只更新相关组件,不要全局重渲染)。这几点 Zustand 都能搞定,而且代码量比 Redux 少一半。
Zustand 购物车实战代码
说了这么多理论,直接上代码吧。先装依赖:
npm install zustand
然后创建购物车 Store(/store/cartStore.js):
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useCartStore = create(
persist(
(set, get) => ({
// 状态
items: [], // [{ id, name, price, quantity, image }]
// 计算属性
get total() {
return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
get count() {
return get().items.reduce((sum, item) => sum + item.quantity, 0)
},
// 方法
addItem: (product) => set((state) => {
const existing = state.items.find(item => item.id === product.id)
if (existing) {
// 已存在,数量+1
return {
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
} else {
// 新商品,添加到购物车
return { items: [...state.items, { ...product, quantity: 1 }] }
}
}),
removeItem: (productId) => set((state) => ({
items: state.items.filter(item => item.id !== productId)
})),
updateQuantity: (productId, quantity) => set((state) => ({
items: state.items.map(item =>
item.id === productId ? { ...item, quantity } : item
)
})),
clearCart: () => set({ items: [] })
}),
{
name: 'shopping-cart', // localStorage 的 key
}
)
)
这段代码看着长,其实逻辑很简单。items 数组存商品,total 和 count 是计算出来的(总价和总数量),然后几个方法处理增删改。persist 中间件会自动把数据存到 localStorage,刷新页面也不会丢。
组件里用起来也超简单:
import { useCartStore } from '@/store/cartStore'
function ProductCard({ product }) {
const addItem = useCartStore(state => state.addItem)
return (
<button onClick={() => addItem(product)}>
加入购物车
</button>
)
}
function CartIcon() {
const count = useCartStore(state => state.count)
return <div>购物车 ({count})</div>
}
注意看,useCartStore(state => state.addItem) 这种写法是选择器。它只订阅 addItem 这个方法,不会因为购物车其他数据变化而重新渲染。这就是 Zustand 性能好的秘密——精准订阅。
对了,如果你之前用过 Redux 的 useSelector 和 useDispatch,会发现 Zustand 简洁太多了。不需要 action type,不需要 reducer 函数,直接在 Store 里写方法就行。
有人可能会问:“那我项目已经用了 Redux,还要换成 Zustand 吗?“不用。Redux Toolkit 其实也很好用,C-Shopping 这个开源电商项目就是用的 Redux Toolkit + RTK Query,一样能追踪数据流、保证稳定性。只是对于新项目,我个人更推荐 Zustand,学习成本低,出活快。
Stripe 支付集成完整流程
先搞清楚 Stripe 的支付流程
第一次看 Stripe 文档的时候,我脑子里全是问号:Checkout Session 是啥?Payment Intent 又是啥?为啥要跳转到 Stripe 的页面?能不能在自己网站上完成支付?
现在回过头看,其实流程挺清晰的:
- 前端:用户点”去支付”,调用你的 API 创建 Checkout Session
- 后端:创建 Session,返回一个 session.id
- 前端:拿到 session.id,用 Stripe.js 跳转到 Stripe 托管的支付页面
- 用户:在 Stripe 页面填信用卡信息,完成支付
- Stripe:支付成功后,发 Webhook 通知你的后端
- 后端:收到 Webhook,创建订单、扣库存、发邮件
- Stripe:把用户重定向回你的网站(success_url)
重点来了:永远不要在前端处理支付成功逻辑。为啥?因为用户可能支付完不点”完成”就关了浏览器,或者网络中断了,或者故意不跳转想白嫖。唯一可靠的方式是 Webhook,我们后面详细讲。
创建 Stripe Checkout Session
先装依赖:
npm install stripe @stripe/stripe-js
然后在环境变量里配置(.env.local):
STRIPE_SECRET_KEY=sk_test_xxxxx # 后端用,绝不能泄露到前端
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx # 前端用
STRIPE_WEBHOOK_SECRET=whsec_xxxxx # Webhook 签名验证用
创建 API 路由(/pages/api/create-checkout.js):
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { items } = req.body // 购物车数据
// 创建 line_items(Stripe 要求的格式)
const lineItems = items.map(item => ({
price_data: {
currency: 'usd',
product_data: {
name: item.name,
images: [item.image],
},
unit_amount: Math.round(item.price * 100), // Stripe 用分为单位
},
quantity: item.quantity,
}))
// 创建 Checkout Session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: lineItems,
mode: 'payment', // 一次性支付(订阅用 'subscription')
success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.origin}/cart`,
metadata: {
// 可以存一些自定义数据,后面 Webhook 能拿到
userId: req.user?.id || 'guest',
},
})
res.status(200).json({ sessionId: session.id })
} catch (err) {
console.error('创建 Checkout Session 失败:', err)
res.status(500).json({ error: err.message })
}
}
注意几个细节:
unit_amount要乘 100,因为 Stripe 用分作单位(99.99美元 = 9999分)success_url里的{CHECKOUT_SESSION_ID}是占位符,Stripe 会自动替换成真实的 session_idmetadata可以存你自己的数据(比如用户 ID、订单备注),Webhook 时能拿到
前端调用 Checkout
在结算页面(/pages/checkout.js):
import { loadStripe } from '@stripe/stripe-js'
import { useCartStore } from '@/store/cartStore'
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)
export default function CheckoutPage() {
const { items, total } = useCartStore()
const handleCheckout = async () => {
try {
// 调用后端 API 创建 Session
const response = await fetch('/api/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
})
const { sessionId } = await response.json()
// 跳转到 Stripe 支付页面
const stripe = await stripePromise
const { error } = await stripe.redirectToCheckout({ sessionId })
if (error) {
console.error('跳转支付页面失败:', error)
alert(error.message)
}
} catch (err) {
console.error('发起支付失败:', err)
alert('支付失败,请稍后重试')
}
}
return (
<div>
<h1>结算</h1>
{items.map(item => (
<div key={item.id}>
{item.name} x {item.quantity} = ${item.price * item.quantity}
</div>
))}
<div>总计: ${total}</div>
<button onClick={handleCheckout}>去支付</button>
</div>
)
}
点击”去支付”后,用户会跳转到 Stripe 托管的支付页面。这个页面 Stripe 帮你搞定了,不用自己写表单、处理信用卡验证、反欺诈检测,省了一大堆事。
有人可能会问:“我能自定义支付页面的样式吗?“能。Stripe 支持自定义颜色、logo、字体,但整体布局是固定的。如果你想完全自己控制 UI,可以用 Stripe Elements(前端嵌入支付表单),但那样复杂度会高很多,不太推荐新手用。
支付成功后跳转
用户支付完成后,Stripe 会重定向到你设置的 success_url。你可以在这个页面展示订单详情:
// /pages/success.js
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
export default function SuccessPage() {
const router = useRouter()
const { session_id } = router.query
const [order, setOrder] = useState(null)
useEffect(() => {
if (session_id) {
// 从后端获取订单信息
fetch(`/api/order?session_id=${session_id}`)
.then(res => res.json())
.then(data => setOrder(data))
}
}, [session_id])
if (!order) return <div>加载中...</div>
return (
<div>
<h1>支付成功!</h1>
<p>订单号: {order.id}</p>
<p>金额: ${order.total}</p>
</div>
)
}
但记住,这个页面只是展示用的,真正的订单创建必须在 Webhook 里完成。接下来我们聊聊 Webhook 怎么搞。
Webhook 订单处理与状态同步
为什么 Webhook 这么重要?
我第一次做支付功能的时候,天真地以为用户跳转回 success 页面就代表支付成功了,订单逻辑都写在那里。结果测试的时候发现,用户支付完直接关了浏览器,订单根本没创建,钱也不知道退没退。慌得一批。
后来看 Stripe 官方文档才明白:Webhook 是唯一可靠的订单处理方式。为啥?
- 用户跳转不可靠:关浏览器、网络断了、不点完成按钮,各种情况都可能发生
- 安全性要求:订单创建、扣库存、发货这些敏感操作必须在后端完成,不能让前端控制
- Stripe 官方推荐:所有关键业务逻辑都应该放在 Webhook 里
Webhook 是啥?简单说,就是 Stripe 服务器主动调用你的服务器,告诉你”嘿,有个支付成功了”或者”有个订阅被取消了”。你收到通知后,做相应的处理。
创建 Webhook 端点
在 Next.js 里创建 /pages/api/stripe-webhook.js:
import Stripe from 'stripe'
import { buffer } from 'micro'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
// 关键配置:禁用 Next.js 默认的 body 解析
export const config = {
api: {
bodyParser: false,
},
}
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).send('Method not allowed')
}
const buf = await buffer(req)
const sig = req.headers['stripe-signature']
let event
try {
// 验证 Webhook 签名(超级重要!)
event = stripe.webhooks.constructEvent(buf, sig, webhookSecret)
} catch (err) {
console.error('Webhook 签名验证失败:', err.message)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
// 处理不同的事件类型
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutSessionCompleted(event.data.object)
break
case 'payment_intent.succeeded':
await handlePaymentIntentSucceeded(event.data.object)
break
case 'invoice.payment_failed':
await handleInvoicePaymentFailed(event.data.object)
break
default:
console.log(`未处理的事件类型: ${event.type}`)
}
res.status(200).json({ received: true })
}
async function handleCheckoutSessionCompleted(session) {
console.log('支付成功!', session.id)
// 获取购物车数据(从 metadata 或通过 session.id 查询)
const userId = session.metadata.userId
const sessionId = session.id
const total = session.amount_total / 100 // 转换回美元
// 检查订单是否已存在(幂等性保证)
const existingOrder = await db.order.findUnique({
where: { stripeSessionId: sessionId }
})
if (existingOrder) {
console.log('订单已存在,跳过创建')
return
}
// 创建订单
const order = await db.order.create({
data: {
userId,
stripeSessionId: sessionId,
status: 'paid',
total,
// ... 其他字段
}
})
// 扣减库存
await updateInventory(order.items)
// 发送确认邮件
await sendOrderConfirmationEmail(userId, order)
console.log('订单创建成功:', order.id)
}
async function handlePaymentIntentSucceeded(paymentIntent) {
// 确认支付到账
console.log('支付确认:', paymentIntent.id)
}
async function handleInvoicePaymentFailed(invoice) {
// 处理订阅支付失败
console.log('支付失败:', invoice.id)
// 发送提醒邮件、暂停服务等
}
几个关键点:
- 必须禁用 bodyParser:Stripe 需要原始的请求体(raw body)来验证签名,如果 Next.js 提前解析了 body,签名验证会失败
- 必须验证签名:
stripe.webhooks.constructEvent()会验证请求确实来自 Stripe,防止恶意第三方伪造请求 - 幂等性处理:Stripe 可能重复发送 Webhook(网络问题、重试机制),你的代码必须能处理重复调用。用
stripeSessionId做唯一索引就能保证同一个支付不会创建多个订单
本地测试 Webhook
Stripe 不可能直接调你的 localhost,所以本地开发需要用 Stripe CLI。先装 CLI:
# Mac
brew install stripe/stripe-cli/stripe
# Windows (用 Scoop)
scoop install stripe
# 或者去官网下载
# https://stripe.com/docs/stripe-cli
登录并监听 Webhook:
stripe login
stripe listen --forward-to localhost:3000/api/stripe-webhook
CLI 会给你一个临时的 webhook secret(类似 whsec_xxxxx),把它复制到 .env.local:
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
然后在另一个终端触发测试事件:
stripe trigger checkout.session.completed
你会在 CLI 和 Next.js 控制台看到日志,说明 Webhook 收到了。这时候你就可以调试订单创建逻辑了。
我当时在这里卡了好久,一直报签名验证失败。后来发现是 Next.js 的 bodyParser 没关,导致 body 被提前解析了。记得一定要加那个 export const config!
订单状态管理
订单状态的流转大概是这样:
待支付 → 已支付 → 配货中 → 已发货 → 已完成
↓
已取消/已退款
在数据库里用枚举(enum)存状态:
// schema.prisma
model Order {
id String @id @default(cuid())
stripeSessionId String @unique // 幂等性保证
userId String
status OrderStatus @default(PENDING)
total Float
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum OrderStatus {
PENDING // 待支付
PAID // 已支付
PREPARING // 配货中
SHIPPED // 已发货
COMPLETED // 已完成
CANCELLED // 已取消
REFUNDED // 已退款
}
Webhook 收到 checkout.session.completed 时,把状态设为 PAID。后续的发货、完成等状态由你的后台管理系统手动或自动更新。
错误处理
Webhook 可能失败(数据库连接断了、第三方服务挂了等)。Stripe 有重试机制,但你最好记录失败日志:
async function handleCheckoutSessionCompleted(session) {
try {
// 业务逻辑
} catch (error) {
console.error('处理订单失败:', error)
// 记录到日志系统(Sentry、LogRocket 等)
await logError({
type: 'webhook_error',
event: 'checkout.session.completed',
sessionId: session.id,
error: error.message,
})
throw error // 抛出错误让 Stripe 知道失败了,会自动重试
}
}
如果 Webhook 失败了怎么办?Stripe 会自动重试 3 天。期间你可以去 Stripe Dashboard 查看失败的 Webhook,手动重发。
完整订单流程实战
到这里,所有的零件我们都准备好了。现在把它们串起来,看看一个完整的订单流程是怎么跑的。
用户下单的完整路径
- 商品页:用户点”加入购物车”,Zustand Store 更新,购物车图标数字+1
- 购物车页:用户查看购物车,调整数量,点”去结算”
- 结算页:展示订单摘要,点”去支付”按钮
- 前端:调用
/api/create-checkout,传购物车数据 - 后端:创建 Stripe Session,返回 sessionId
- 前端:跳转到 Stripe 托管支付页
- 用户:填信用卡信息,点”Pay”
- Stripe:处理支付,成功后发 Webhook 到
/api/stripe-webhook - 后端 Webhook:验证签名 → 创建订单 → 扣库存 → 发邮件
- Stripe:重定向用户回
/success?session_id=xxx - 前端:success 页面调
/api/order?session_id=xxx,展示订单详情
整个流程看起来复杂,但其实每一步都很清晰。关键是第9步必须在 Webhook 里完成,不能依赖第11步。
数据库设计要点
model Order {
id String @id @default(cuid())
stripeSessionId String @unique // 幂等性
userId String
status OrderStatus @default(PENDING)
total Float
items OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
}
model OrderItem {
id String @id @default(cuid())
orderId String
productId String
quantity Int
price Float // 下单时的价格,防止后期商品涨价影响订单
order Order @relation(fields: [orderId], references: [id])
product Product @relation(fields: [productId], references: [id])
}
注意 OrderItem 里的 price 字段,存的是下单时的价格,不是关联 Product 的当前价格。这样即使商品后来涨价了,历史订单还是按原价算。
边界情况处理
1. 库存不足怎么办?
在创建 Checkout Session 前检查:
// /pages/api/create-checkout.js
const { items } = req.body
// 检查库存
for (const item of items) {
const product = await db.product.findUnique({ where: { id: item.id } })
if (product.stock < item.quantity) {
return res.status(400).json({ error: `${product.name} 库存不足` })
}
}
// 库存充足,继续创建 Session...
2. 支付成功但 Webhook 失败了?
Stripe 会自动重试 3 天。你也可以手动在 Stripe Dashboard 重发 Webhook。或者写个定时任务,定期检查”支付成功但订单未创建”的 Session,补单。
3. 用户支付了但超时未发货?
发货前再次检查库存。如果没货了,联系用户退款或换货。
生产环境部署注意事项
测试环境跑通了,生产环境可别直接上。有几个坑一定要注意。
环境变量配置
生产环境的密钥和测试环境不一样,别搞混了:
# .env.production
STRIPE_SECRET_KEY=sk_live_xxxxx # 注意是 live 不是 test
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx # 生产环境的 Webhook Secret
在 Vercel 或其他平台部署时,记得配置环境变量。千万别把 Secret Key 提交到 Git。
Webhook 端点配置
测试环境用的是 Stripe CLI 转发,生产环境要去 Stripe Dashboard 手动配置:
- 登录 Stripe Dashboard
- 进入 “Developers” → “Webhooks”
- 点 “Add endpoint”
- 填入你的生产环境 URL:
https://yourdomain.com/api/stripe-webhook - 选择要监听的事件:
checkout.session.completed、payment_intent.succeeded等 - 保存后,复制 Signing secret(类似
whsec_xxxxx),配置到环境变量
我第一次部署的时候忘了配这个,上线后订单一直不创建,找了半天才发现 Webhook 根本没收到。
安全检查清单
部署前过一遍这个清单:
- ✅ 所有支付逻辑在后端完成(前端只负责跳转)
- ✅ Webhook 验证签名(
stripe.webhooks.constructEvent) - ✅ 检查支付金额与订单金额是否一致(防止前端篡改价格)
- ✅ 实现幂等性(
stripeSessionId唯一索引) - ✅ 记录所有支付相关日志(Sentry、Datadog 等)
- ✅ 设置异常告警(Webhook 失败率、支付成功率)
第三条特别重要。虽然创建 Session 时后端已经定价了,但还是要在 Webhook 里再次校验,防止有人绕过前端直接调 Stripe API(虽然概率很小,但安全无小事)。
监控与告警
生产环境最好接入监控系统:
// /pages/api/stripe-webhook.js
import * as Sentry from '@sentry/nextjs'
export default async function handler(req, res) {
try {
// ... Webhook 逻辑
} catch (error) {
Sentry.captureException(error, {
tags: {
type: 'stripe_webhook',
event: event.type,
},
})
throw error
}
}
监控指标建议关注:
- Webhook 失败率(>5% 就要告警)
- 支付成功率(突然下降可能是 Stripe 挂了或配置错误)
- 订单创建时长(>3秒就要查原因)
总结:从零到上线的完整路径
说了这么多,回顾一下关键步骤:
- 购物车状态管理:选 Zustand(轻量)或 Redux Toolkit(大项目),用 persist 中间件持久化
- 支付集成:创建 Stripe Checkout Session,跳转托管支付页,省去表单验证的麻烦
- 订单处理:在 Webhook 里创建订单、扣库存、发邮件,保证可靠性
- 生产部署:配置环境变量、设置 Webhook 端点、接入监控告警
核心原则记住三条:
- 支付逻辑必须在后端:前端不可信
- Webhook 是唯一可靠来源:用户跳转不可靠
- 安全永远是第一位:验签名、防重放、记日志
如果你是第一次做电商支付,建议先在 Stripe 测试环境跑通全流程。测试卡号可以用 4242 4242 4242 4242(有效期和 CVV 随便填)。等测试环境没问题了,再切换到生产环境。
最后,推荐几个进阶资料:
- Stripe 官方文档:https://stripe.com/docs(虽然是英文,但写得很详细)
- Next.js + Stripe 完整教程:Pedro Alonso 的 2025 年指南(搜”Stripe Next.js 15 complete guide”)
- 开源项目参考:C-Shopping 电商平台,用的 Redux Toolkit + Stripe
希望这篇文章能帮你少踩点坑。当你第一次看到订单自动创建成功、库存正确扣减、用户收到确认邮件的时候,那种成就感真的无与伦比。加油!
Next.js 电商购物车和 Stripe 支付完整实现流程
从零搭建电商购物车和支付系统的详细步骤,包含状态管理、支付集成、订单处理全流程
⏱️ 预计耗时: 2 小时
- 1
步骤1: 安装依赖并配置 Zustand 购物车
安装 Zustand 状态管理库:
• npm install zustand
创建购物车 Store(/store/cartStore.js):
• 定义 items 数组存储商品
• 添加 total 和 count 计算属性
• 实现 addItem、removeItem、updateQuantity、clearCart 方法
• 使用 persist 中间件保存到 localStorage
关键配置:
• persist 中间件自动持久化,刷新页面不丢数据
• 使用选择器订阅(useCartStore(state => state.addItem))避免不必要的重渲染
适用场景:中小型项目(10-50个页面),需要轻量级状态管理 - 2
步骤2: 创建 Stripe Checkout Session API
安装 Stripe 依赖:
• npm install stripe @stripe/stripe-js
配置环境变量(.env.local):
• STRIPE_SECRET_KEY=sk_test_xxxxx(后端用,不能泄露)
• NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx(前端用)
• STRIPE_WEBHOOK_SECRET=whsec_xxxxx(Webhook 验签用)
创建 API 路由(/pages/api/create-checkout.js):
• 接收购物车 items 数据
• 转换为 Stripe line_items 格式(注意 unit_amount 要乘 100)
• 创建 checkout.sessions(设置 success_url 和 cancel_url)
• 在 metadata 存储自定义数据(userId 等)
• 返回 sessionId 给前端
关键细节:
• Stripe 用分为单位,价格要乘 100
• success_url 使用占位符 {CHECKOUT_SESSION_ID}
• metadata 可存储业务数据,Webhook 能获取 - 3
步骤3: 前端调用 Stripe Checkout
结算页面实现(/pages/checkout.js):
• 使用 loadStripe 加载 Stripe.js
• 调用 /api/create-checkout 创建 Session
• 使用 stripe.redirectToCheckout() 跳转支付页
错误处理:
• catch 捕获网络错误
• 检查 stripe.redirectToCheckout 返回的 error
• 向用户展示友好的错误提示
支付页面说明:
• Stripe 托管支付页面,无需自己写表单
• 自动处理信用卡验证、反欺诈检测
• 可自定义颜色、logo、字体
注意事项:
• 永远不要在前端处理支付成功逻辑
• 用户可能关闭浏览器或不点完成按钮
• 真正的订单创建必须在 Webhook 里完成 - 4
步骤4: 配置 Webhook 端点处理订单
创建 Webhook API(/pages/api/stripe-webhook.js):
必须配置:
• export const config = { api: { bodyParser: false } }(禁用 body 解析)
• 使用 buffer(req) 获取原始请求体
• stripe.webhooks.constructEvent() 验证签名
处理事件类型:
• checkout.session.completed:支付成功,创建订单
• payment_intent.succeeded:确认支付到账
• invoice.payment_failed:订阅支付失败
幂等性保证:
• 检查 stripeSessionId 是否已存在
• 数据库添加 unique 索引
• 防止 Webhook 重复调用创建多个订单
业务逻辑:
• 创建订单记录(status: 'paid')
• 扣减库存(updateInventory)
• 发送确认邮件(sendOrderConfirmationEmail)
• 记录日志和错误
本地测试:
• stripe login(登录 CLI)
• stripe listen --forward-to localhost:3000/api/stripe-webhook
• stripe trigger checkout.session.completed(测试事件)
关键注意:
• 必须禁用 bodyParser,否则签名验证失败
• 必须验证签名,防止恶意伪造请求
• Webhook 失败 Stripe 会自动重试 3 天 - 5
步骤5: 生产环境部署与安全配置
环境变量配置:
• 使用生产环境密钥(sk_live_xxxxx 和 pk_live_xxxxx)
• 在平台(Vercel/Netlify)配置环境变量
• 千万别把 Secret Key 提交到 Git
Stripe Dashboard 配置:
• 进入 Developers → Webhooks
• 添加生产环境端点(https://yourdomain.com/api/stripe-webhook)
• 选择监听事件(checkout.session.completed 等)
• 复制 Signing secret 配置到环境变量
安全检查清单:
• ✅ 所有支付逻辑在后端完成
• ✅ Webhook 验证签名
• ✅ 校验支付金额与订单金额一致
• ✅ 实现幂等性(stripeSessionId 唯一索引)
• ✅ 记录所有支付日志
• ✅ 设置监控告警(Webhook 失败率 >5% 告警)
监控指标:
• Webhook 失败率
• 支付成功率
• 订单创建时长(>3秒要排查)
接入监控系统:
• 使用 Sentry/LogRocket 记录错误
• 配置告警规则
• 定期检查 Stripe Dashboard 的 Webhook 日志
测试流程:
• 使用测试卡号 4242 4242 4242 4242
• 验证完整流程(购物车 → 支付 → Webhook → 订单创建)
• 测试失败场景(库存不足、Webhook 失败等)
常见问题
Redux 和 Zustand 该怎么选?我的项目适合哪个?
• 小项目(<10 个页面):Context API 就够了,不需要额外的状态管理库
• 中型项目(10-50 个页面):Zustand 更合适,轻量级、学习成本低、代码量少
• 大型项目(50+ 页面,多团队协作):Redux Toolkit,工具链完善、调试能力强、社区成熟
具体场景:
• 新项目、快速迭代:选 Zustand,上手快、出活快
• 已有 Redux 项目:不用换,Redux Toolkit 同样好用
• 团队不熟悉状态管理:Zustand 学习曲线平缓,更容易上手
购物车场景推荐 Zustand:需要跨组件共享、持久化存储、性能优化,Zustand 都能搞定。
为什么不能在前端处理支付成功逻辑?
不可靠性:
• 用户支付完可能直接关闭浏览器
• 网络中断导致重定向失败
• 用户故意不点完成按钮想白嫖
安全风险:
• 前端代码可被篡改和绕过
• 订单创建、扣库存等敏感操作不能暴露在前端
• 无法防止恶意用户伪造支付成功状态
正确做法:
• 所有关键业务逻辑在 Webhook 里完成
• Stripe 服务器直接通知你的后端(不经过用户浏览器)
• Webhook 有签名验证,安全可靠
• Stripe 官方推荐:Webhook 是订单处理的唯一可靠来源
前端 success 页面只用于展示,不做任何业务逻辑处理。
Webhook 签名验证总是失败怎么办?
最常见原因(90%):
• Next.js 的 bodyParser 没关闭
• 解决:在 API 路由添加 export const config = { api: { bodyParser: false } }
其他原因:
• Webhook Secret 配置错误(检查 .env.local 中的 STRIPE_WEBHOOK_SECRET)
• 使用了错误的环境密钥(测试环境和生产环境的 secret 不一样)
• 请求体被中间件修改(检查是否有全局中间件处理 body)
调试步骤:
1. 确认 bodyParser: false 已配置
2. 打印 req.headers['stripe-signature'] 检查是否存在
3. 使用 Stripe CLI 测试:stripe listen --forward-to localhost:3000/api/stripe-webhook
4. 查看 CLI 输出的详细错误信息
5. 确认使用的是 CLI 提供的临时 webhook secret
本地测试注意:
• 本地开发必须用 Stripe CLI 转发
• CLI 会提供临时的 webhook secret(whsec_xxxxx)
• 每次重启 CLI 都会生成新的 secret,需要更新 .env.local
如何防止 Webhook 重复调用创建多个订单?
数据库层面:
• 给 stripeSessionId 字段添加 unique 索引
• Prisma 示例:stripeSessionId String @unique
• 数据库会自动拒绝重复插入
代码层面:
• 在创建订单前先查询是否已存在
• 使用 findUnique({ where: { stripeSessionId } })
• 如果已存在则直接返回,不创建新订单
示例代码:
```javascript
const existingOrder = await db.order.findUnique({
where: { stripeSessionId: sessionId }
})
if (existingOrder) {
console.log('订单已存在,跳过创建')
return
}
// 不存在才创建新订单
const order = await db.order.create({ ... })
```
为什么需要幂等性:
• Stripe 可能重复发送 Webhook(网络问题、重试机制)
• 你的代码必须能安全处理重复调用
• 避免同一个支付创建多个订单、重复扣库存
其他建议:
• 记录每次 Webhook 调用日志
• 监控重复调用频率
• 设置告警机制
生产环境部署后订单不创建怎么排查?
1. 检查 Webhook 是否收到:
• 登录 Stripe Dashboard → Developers → Webhooks
• 查看 Webhook 调用历史和状态(成功/失败)
• 如果没有调用记录,说明端点配置有问题
2. 检查端点配置:
• URL 是否正确(https://yourdomain.com/api/stripe-webhook)
• 事件类型是否选择了 checkout.session.completed
• 端点状态是否启用
3. 检查环境变量:
• STRIPE_WEBHOOK_SECRET 是否配置正确
• 是否使用了生产环境的 secret(不是测试环境的)
• 在部署平台(Vercel/Netlify)确认环境变量已设置
4. 检查 Webhook 端点代码:
• 是否禁用了 bodyParser
• 签名验证是否正确
• 是否有错误日志输出
5. 查看应用日志:
• 检查服务器日志(Vercel Logs/CloudWatch 等)
• 查看是否有错误堆栈
• 确认 Webhook 处理函数是否被执行
6. 手动测试:
• 在 Stripe Dashboard 找到失败的 Webhook
• 点击"Resend"手动重发
• 观察是否成功和错误信息
常见错误:
• 忘记在生产环境添加 Webhook 端点
• 使用了测试环境的 webhook secret
• 部署平台的防火墙拦截了 Stripe 请求
解决后验证:
• 使用测试卡号完整测试一次支付流程
• 确认订单创建、库存扣减、邮件发送都正常
14 分钟阅读 · 发布于: 2026年1月7日 · 修改于: 2026年1月15日
相关文章
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js E2E 测试:Playwright 自动化测试实战指南
Next.js E2E 测试:Playwright 自动化测试实战指南
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南

评论
使用 GitHub 账号登录后即可评论