Stripe

Next.js 模板的 Stripe Checkout、订阅、终身与计费门户。

Stripesrc/lib/payment/providers/stripe.ts 中实现共享的 PaymentProvider 接口。它使用 Stripe Checkout 处理 周期性方案与一次性(终身)购买,通过 webhook 同步订阅,并提供 Billing Portal 供用户自助管理。

配置

变量用途
NEXT_PUBLIC_PAYMENT_PROVIDER设为 stripe 以选择该提供商。
STRIPE_API_KEY用于 Stripe SDK 的密钥/受限密钥(sk_...rk_...)。
STRIPE_WEBHOOK_SECRET用于校验 webhook 签名的签名密钥(whsec_...)。
NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY / _PRO_YEARLY / _LIFETIME三个方案的公开 Price ID(price_...)。

Stripe 的取值是 Price IDprice_...),不是 Product ID。客户端在 providers/stripe.ts 中固定 apiVersion: "2026-05-27.dahlia"

环境变量

结账

createCheckout 将方案的 productType 映射为一种 Checkout 模式:

productType模式结果
month / yearsubscription周期性订阅
one_timepayment一次性终身购买

它将 metadataorderNoproductIdproductTypeuserId)附加到会话上 —— 对于订阅,还附加到 subscription_data,以便后续订阅事件能追溯到该用户。若用户已有 Stripe customerId,会话会复用它;否则 Stripe 会基于 customer_email 创建客户。

Webhook

  • 路径: POST /api/payment/notify/stripe(由共享的 /api/payment/notify/[provider] 路由提供)。
  • 校验: 针对 stripe-signature 请求头执行 stripe.webhooks.constructEvent(rawBody, signature, STRIPE_WEBHOOK_SECRET)。 缺少/无效签名时返回 400
  • 已处理事件:
事件效果
checkout.session.completed / checkout.session.async_payment_succeeded若已支付,将 payment 行标记为 successpayment 模式下授予终身权益。
checkout.session.async_payment_failedpayment 行标记为 failed
checkout.session.expiredpayment 行标记为 closed
customer.subscription.created / updated / deletedupsert 该用户的 subscription 行(状态、周期、试用、取消时间戳)。
invoice.paid在匹配的订阅上写入 lastPaidAt
charge.refunded / refund.created将匹配的 payment 行标记为 refunded

订阅同步

syncStripeSubscription 将 Stripe 状态映射到本地枚举 (active / trialing / canceled / expired / unpaid),从价格周期推导 monthyear,并写入周期窗口。

每个用户只有一行权益。一个守卫可防止旧的周期性订阅的迟到事件覆盖 one_time (终身)购买。若你自定义同步逻辑,请保留该守卫。

终身购买

对于 payment 模式的结账,syncLifetimeAccess 写入一行 one_time 订阅, status: activeperiodEnd 设为极远的未来,使用户在没有 Stripe 订阅对象的情况下 仍拥有永久访问权。

计费门户

createPortal 打开一个 Stripe Billing Portal 会话。POST /api/payment/portal 读取用户的 subscription.customerId 并返回门户 URL;ManageBillingButton 组件 (src/components/payment/ManageBillingButton.tsx)链接到它。该门户仅对 Stripe 客户 可用(Creem 未实现 createPortal)。

本地测试

使用 Stripe CLI 将事件转发到你的开发服务器:

stripe listen --forward-to localhost:3000/api/payment/notify/stripe

CLI 会打印一个 whsec_... 密钥 —— 将其设为 STRIPE_WEBHOOK_SECRET。随后用 测试卡(如 4242 4242 4242 4242)完成一次结账, 确认 payment 行翻转为 success 且出现一行 subscription

生产注意事项

  • 使用正式密钥与正式 Price ID(公开的 NEXT_PUBLIC_* ID 需重新构建)。
  • /api/payment/notify/stripe 注册 webhook 端点并订阅上述事件;将其签名密钥 复制到 STRIPE_WEBHOOK_SECRET
  • 增加幂等 —— Stripe 会重试投递;处理器必须可安全重复执行。
  • 优先使用范围限定到 Checkout、Customers、Subscriptions 与 Billing Portal 的 受限密钥rk_...)。

验证

  • NEXT_PUBLIC_PAYMENT_PROVIDER=stripe 时,定价 CTA 打开 Stripe Checkout。
  • 订阅购买会创建一行 activesubscription,且周期正确。
  • 终身(一次性)购买会创建一行 one_timeperiodEnd 为极远未来。
  • “管理计费”可打开 Stripe Billing Portal。
  • 无效的 webhook 签名返回 400

On this page