商店 › Next.js + Stripe Starter
一个能直接跑的收款链路:Checkout、webhook 签名校验、事件幂等、订单状态机、邮件交付。 17 个文件,没有抽象层,改起来不用先读懂一个框架。
点击购买即表示你已阅读并同意 《购买条款》。 条款只适用于本商店的数字商品。
教程告诉你怎么写;这个是把已经写对的部分给你,你只需要改商品和交付。 代码在 Next.js 15.5 + Node 22 上验证过构建和启动,不是「理论上应该能跑」。
req.json() 拿 body
Stripe 的签名是对原始字节算的。一旦你重新序列化(哪怕只是键序变了),
constructEvent 一定抛 No signatures found matching the expected signature。
正确写法是 await req.text()。
Stripe 的投递是「至少一次」语义。接口超过 20 秒未响应、返回非 2xx、或网络抖动都会重投同一事件,
最长重投 3 天。不去重就会重复发货、重复发邮件。必须按 event.id 去重。
只做事件去重不够。两个不同的 event(如 checkout.session.completed 和
async_payment_succeeded)可能指向同一个 session。
交付前必须基于订单状态做原子占位:只有 paid 能转 delivered。
抛了错但返回 200,Stripe 认为投递成功,不会重投——订单永久卡在「已付款未发货」。
正确做法是返回 500 让 Stripe 重投,同时把订单标记为 failed 供人工补。
stripe-starter/ ├── app/ │ ├── page.tsx 商品页 │ ├── buy-button.tsx 购买按钮(客户端组件) │ ├── success/page.tsx 付款成功后轮询交付状态 │ ├── success/success-content.tsx │ ├── cancel/page.tsx 取消页 │ └── api/ │ ├── checkout/route.ts 创建 Checkout Session │ └── webhook/route.ts 接收并处理 Stripe 事件 ├── lib/ │ ├── stripe.ts Stripe 客户端 + 商品配置 │ ├── orders.ts 订单存储 + 幂等 + 状态机 │ └── email.ts 交付邮件 ├── .env.example └── package.json
只改环境变量,不用碰代码:
PRODUCT_NAME=我的模板 PRODUCT_PRICE_CENTS=4900 # 注意是「分」,4900 = $49.00 PRODUCT_CURRENCY=usd
/api/checkout 加 rate limitNEXT_PUBLIC_SITE_URL 是生产域名line_itemsmode: "payment" 的一次性收款)charge.refunded)stripe listen --forward-to localhost:3000/api/webhook,把打印的 whsec_... 填进 env 后重启。完整条款见 购买条款。