切换语言
切换主题

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 数组存商品,totalcount 是计算出来的(总价和总数量),然后几个方法处理增删改。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 的 useSelectoruseDispatch,会发现 Zustand 简洁太多了。不需要 action type,不需要 reducer 函数,直接在 Store 里写方法就行。

有人可能会问:“那我项目已经用了 Redux,还要换成 Zustand 吗?“不用。Redux Toolkit 其实也很好用,C-Shopping 这个开源电商项目就是用的 Redux Toolkit + RTK Query,一样能追踪数据流、保证稳定性。只是对于新项目,我个人更推荐 Zustand,学习成本低,出活快。

Stripe 支付集成完整流程

先搞清楚 Stripe 的支付流程

第一次看 Stripe 文档的时候,我脑子里全是问号:Checkout Session 是啥?Payment Intent 又是啥?为啥要跳转到 Stripe 的页面?能不能在自己网站上完成支付?

现在回过头看,其实流程挺清晰的:

  1. 前端:用户点”去支付”,调用你的 API 创建 Checkout Session
  2. 后端:创建 Session,返回一个 session.id
  3. 前端:拿到 session.id,用 Stripe.js 跳转到 Stripe 托管的支付页面
  4. 用户:在 Stripe 页面填信用卡信息,完成支付
  5. Stripe:支付成功后,发 Webhook 通知你的后端
  6. 后端:收到 Webhook,创建订单、扣库存、发邮件
  7. 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_id
  • metadata 可以存你自己的数据(比如用户 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)
  // 发送提醒邮件、暂停服务等
}

几个关键点:

  1. 必须禁用 bodyParser:Stripe 需要原始的请求体(raw body)来验证签名,如果 Next.js 提前解析了 body,签名验证会失败
  2. 必须验证签名stripe.webhooks.constructEvent() 会验证请求确实来自 Stripe,防止恶意第三方伪造请求
  3. 幂等性处理: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,手动重发。

完整订单流程实战

到这里,所有的零件我们都准备好了。现在把它们串起来,看看一个完整的订单流程是怎么跑的。

用户下单的完整路径

  1. 商品页:用户点”加入购物车”,Zustand Store 更新,购物车图标数字+1
  2. 购物车页:用户查看购物车,调整数量,点”去结算”
  3. 结算页:展示订单摘要,点”去支付”按钮
  4. 前端:调用 /api/create-checkout,传购物车数据
  5. 后端:创建 Stripe Session,返回 sessionId
  6. 前端:跳转到 Stripe 托管支付页
  7. 用户:填信用卡信息,点”Pay”
  8. Stripe:处理支付,成功后发 Webhook 到 /api/stripe-webhook
  9. 后端 Webhook:验证签名 → 创建订单 → 扣库存 → 发邮件
  10. Stripe:重定向用户回 /success?session_id=xxx
  11. 前端: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 手动配置:

  1. 登录 Stripe Dashboard
  2. 进入 “Developers” → “Webhooks”
  3. 点 “Add endpoint”
  4. 填入你的生产环境 URL:https://yourdomain.com/api/stripe-webhook
  5. 选择要监听的事件:checkout.session.completedpayment_intent.succeeded
  6. 保存后,复制 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秒就要查原因)

总结:从零到上线的完整路径

说了这么多,回顾一下关键步骤:

  1. 购物车状态管理:选 Zustand(轻量)或 Redux Toolkit(大项目),用 persist 中间件持久化
  2. 支付集成:创建 Stripe Checkout Session,跳转托管支付页,省去表单验证的麻烦
  3. 订单处理:在 Webhook 里创建订单、扣库存、发邮件,保证可靠性
  4. 生产部署:配置环境变量、设置 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

    步骤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

    步骤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

    步骤3: 前端调用 Stripe Checkout

    结算页面实现(/pages/checkout.js):
    • 使用 loadStripe 加载 Stripe.js
    • 调用 /api/create-checkout 创建 Session
    • 使用 stripe.redirectToCheckout() 跳转支付页

    错误处理:
    • catch 捕获网络错误
    • 检查 stripe.redirectToCheckout 返回的 error
    • 向用户展示友好的错误提示

    支付页面说明:
    • Stripe 托管支付页面,无需自己写表单
    • 自动处理信用卡验证、反欺诈检测
    • 可自定义颜色、logo、字体

    注意事项:
    • 永远不要在前端处理支付成功逻辑
    • 用户可能关闭浏览器或不点完成按钮
    • 真正的订单创建必须在 Webhook 里完成
  4. 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

    步骤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 签名验证总是失败怎么办?
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日

评论

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

相关文章