支付

Next.js 模板的提供商无关结账与 Webhook —— Creem 或 Stripe。

支付是一套可在其上构建的端到端计费流程。模板在同一套接口背后内置了两个可互换的 提供商 —— CreemStripe,通过单个环境变量切换。结账、订单与计费页面、 订阅状态,以及提供商 webhook(含订阅生命周期与退款)都基于真实数据运行。

选择一个提供商

架构

一个小型注册表解析当前提供商;路由与定价 UI 从不直接导入某个提供商。

文件作用
src/config/payment-config.ts提供商选择(getPaymentProvider)与 env 驱动的产品 ID(getPaymentProducts
src/lib/payment/index.ts提供商注册表 + getProvider()(实例记忆化)
src/lib/payment/types.tsPaymentProvider 接口与共享的参数/结果类型
src/lib/payment/providers/creem.tsCreem 实现
src/lib/payment/providers/stripe.tsStripe 实现
src/lib/payment/order-no.ts订单号生成(buildOrderNo
src/app/api/payment/create/route.ts已认证的结账创建
src/app/api/payment/notify/[provider]/route.ts所有提供商共用的单一 webhook 接收端
src/app/api/payment/portal/route.ts自助计费门户(取决于提供商)
src/app/api/payment/query/route.tsorderId 查询订单状态
src/lib/db/schema/payment.tspaymentsubscription

每个提供商都实现同一个接口:

interface PaymentProvider {
  readonly type: PaymentProviderType;
  createCheckout(params: CreateCheckoutParams): Promise<CreateCheckoutResult>;
  handleWebhook(request: WebhookRequest): Promise<void>;
  createPortal?(customerId: string, returnUrl: string): Promise<string>;
}

切换提供商

设置提供商,并提供该提供商的产品/价格 ID:

# creem(默认)或 stripe
NEXT_PUBLIC_PAYMENT_PROVIDER=stripe

当提供商为 stripe 时,getPaymentProducts() 读取 NEXT_PUBLIC_STRIPE_PRICE_* ID,否则读取 NEXT_PUBLIC_CREEM_PRODUCT_* ID。展示价格位于 src/config/price-config.ts,与提供商实际收费相互独立 —— 需你自行保持一致。见 环境变量

共享流程

定价 CTA → 创建结账

POST /api/payment/create 要求已认证会话,用 Zod 校验 { productId },解析产品, 生成订单号,插入一条 payment 行(status: payingprovider: <当前>),随后调用 当前提供商的 createCheckout。返回 { orderNo, checkoutUrl }

跳转到托管结账

浏览器被导向 checkoutUrl。成功 URL 为 {origin}/pay-success?orderId={orderNo}; 取消则返回 {origin}/#pricing

成功页 → 查询订单

/pay-success 可调用 GET /api/payment/query?orderId=...,从 payment 表读取 订单状态。

Webhook → 更新数据表

提供商调用 POST /api/payment/notify/{provider}。路由按路径段查找提供商并调用其 handleWebhook,后者校验签名(失败返回 400 而非 500),并更新 payment / subscription 表。

数据模型

两个提供商写入相同的数据表。subscription 行是用户唯一的权益来源;provider 记录其 归属,以便 /api/payment/portal 将计费门户路由到正确的 SDK。

说明
payment.provider / subscription.providercreem | stripe(枚举还允许 zpaypolar
payment.thirdOrderNo提供商结账/会话 ID
subscription.customerId提供商客户 ID —— 计费门户必需
subscription.subscriptionId提供商订阅 ID(一次性/终身为 null)
subscription.productTypemonth | year | one_time

新增一个提供商

  1. payment-config.tspaymentProviders 中添加其值。
  2. src/lib/payment/providers/ 中实现 PaymentProvider 接口。
  3. src/lib/payment/index.ts 的注册表中注册该类。

无需改动任何路由或调用方 —— [provider] webhook 路由与 getProvider() 会自动识别它。

已接入的部分

  • 订单页(account/order)列出用户真实的 payment 历史。
  • 订阅页(account/subscription)展示来自 subscription 表的实时套餐、状态、 周期与取消情况。
  • 计费页(setting/billing)渲染套餐概要与计费历史,并将 Stripe 客户链接到 Billing Portal 以管理支付方式、发票与取消。
  • GET /api/payment/query/api/payment/orders 均已认证并限定到当前登录用户。

相关页面

On this page