Stripe
TanStack Start 模板的 Stripe Checkout、订阅、终身与计费门户。
Stripe 在 src/lib/payment/providers/stripe.ts 中实现共享的
PaymentProvider 接口。它使用 Stripe Checkout
处理周期性方案与一次性(终身)购买,通过 webhook 同步订阅,并提供 Billing Portal
供用户自助管理。
配置
| 变量 | 位置 | 用途 |
|---|---|---|
VITE_PAYMENT_PROVIDER | 构建时 | 设为 stripe 以选择该提供商。 |
STRIPE_API_KEY | Worker 密钥 | 用于 Stripe SDK 的密钥/受限密钥(sk_... 或 rk_...)。 |
STRIPE_WEBHOOK_SECRET | Worker 密钥 | 用于校验 webhook 签名的签名密钥(whsec_...)。 |
VITE_STRIPE_PRICE_PRO_MONTHLY / _PRO_YEARLY / _LIFETIME | 构建时 | 三个方案的公开 Price ID(price_...)。 |
wrangler secret put STRIPE_API_KEY
wrangler secret put STRIPE_WEBHOOK_SECRETStripe 的取值是 Price ID(price_...),不是 Product ID。客户端在
providers/stripe.ts 中固定 apiVersion: "2026-05-27.dahlia"。
见 环境变量。
结账
createCheckout 将方案的 productType 映射为一种 Checkout 模式:
productType | 模式 | 结果 |
|---|---|---|
month / year | subscription | 周期性订阅 |
one_time | payment | 一次性终身购买 |
它将 metadata(orderNo、productId、productType、userId)附加到会话上 ——
对于订阅,还附加到 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 行标记为 success;payment 模式下授予终身权益。 |
checkout.session.async_payment_failed | 将 payment 行标记为 failed。 |
checkout.session.expired | 将 payment 行标记为 closed。 |
customer.subscription.created / updated / deleted | upsert 该用户的 subscription 行(状态、周期、试用、取消时间戳)。 |
invoice.paid | 在匹配的订阅上写入 lastPaidAt。 |
charge.refunded / refund.created | 将匹配的 payment 行标记为 refunded。 |
订阅同步
syncStripeSubscription 将 Stripe 状态映射到本地枚举
(active / trialing / canceled / expired / unpaid),从价格周期推导
month 或 year,并写入周期窗口。
每个用户只有一行权益。一个守卫可防止旧的周期性订阅的迟到事件覆盖 one_time
(终身)购买。若你自定义同步逻辑,请保留该守卫。
终身购买
对于 payment 模式的结账,syncLifetimeAccess 写入一行 one_time 订阅,
status: active 且 periodEnd 设为极远的未来,使用户在没有 Stripe 订阅对象的情况下
仍拥有永久访问权。
计费门户
createPortal 打开一个 Stripe Billing Portal 会话。POST /api/payment/portal
读取用户的 subscription.customerId 并返回门户 URL;ManageBillingButton 组件
(src/components/payment/ManageBillingButton.tsx)链接到它。该门户仅对 Stripe 客户
可用(Creem 未实现 createPortal)。
本地测试
使用 Stripe CLI 将事件转发到你的开发服务器
(vite dev 运行在端口 3000):
stripe listen --forward-to localhost:3000/api/payment/notify/stripeCLI 会打印一个 whsec_... 密钥 —— 将其设为 STRIPE_WEBHOOK_SECRET。随后用
测试卡(如 4242 4242 4242 4242)完成一次结账,
确认 payment 行翻转为 success 且出现一行 subscription。
生产注意事项
- 使用正式密钥与正式 Price ID(
VITE_*ID 需重新构建)。 - 在
/api/payment/notify/stripe注册 webhook 端点并订阅上述事件;将其签名密钥 复制到STRIPE_WEBHOOK_SECRET。 - 增加幂等 —— Stripe 会重试投递;处理器必须可安全重复执行。
- 优先使用范围限定到 Checkout、Customers、Subscriptions 与 Billing Portal 的
受限密钥(
rk_...)。
验证
- 当
VITE_PAYMENT_PROVIDER=stripe时,定价 CTA 打开 Stripe Checkout。 - 订阅购买会创建一行
active的subscription,且周期正确。 - 终身(一次性)购买会创建一行
one_time,periodEnd为极远未来。 - “管理计费”可打开 Stripe Billing Portal。
- 无效的 webhook 签名返回
400。
