Stripe 支付
配置 Stripe Checkout、Billing Portal 和 Webhook
Stripe 支付集成
EasyStarter 的 Web 端内置了完整的 Stripe 支付支持,开箱即用,包含:
- 订阅制(月付 / 年付)结账
- 一次性买断(Lifetime)结账
- 免费试用期(Trial)
- Stripe Billing Portal(用户自助管理订阅、取消、更换支付方式、查看发票)
- 计划升级(月付 → 年付,直接在后台完成,无需跳转 Checkout)
- Webhook 事件处理(订阅状态同步、发票、退款、争议等)
支付配置分为两部分:
- 环境变量:API 密钥与 Webhook 签名密钥,填入服务端
.dev.vars/.env.production - 定价计划:在
packages/app-config/src/app-config.ts中配置 Stripe Price ID 与价格信息
所需环境变量
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=注册 Stripe 并获取 API 密钥
官方文档:Stripe API Keys
- 前往 stripe.com 注册账号并完成邮箱验证
- 登录后进入 Developers → API keys
- 复制 Secret key(以
sk_test_开头为测试密钥,sk_live_为生产密钥)
初始阶段使用测试密钥即可。上线前在同一页面切换到 Live mode 获取正式密钥。
将复制的密钥填入:
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSTRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx在 Stripe 创建产品与价格
EasyStarter 默认包含三个计划:Free、Pro(月付 + 年付)、Lifetime(一次性买断)。
Free 计划无需在 Stripe 创建,仅在本地配置中用作占位。以下为 Pro 和 Lifetime 的创建步骤:
- 进入 Products 页面,点击 Add product
- 填写产品名称,例如
Pro - 在 Pricing 区域:
- 选择 Recurring(订阅),填写价格,选择计费周期(Monthly 或 Yearly)
- 点击 More price options (更多价格选项) 可在同一产品下添加多个价格
- 保存后,点击每个价格的详情,复制 Price ID(格式为
price_xxxxxxx) - 重复以上步骤为 Lifetime 创建产品,定价类型选择 One time(一次性收费)
每个价格对应一个唯一的 Price ID,下一步将用到。
配置定价计划
将上一步获取的 Price ID 填入 packages/app-config/src/app-config.ts 的 web.payments.plans 字段:
web: {
payments: {
provider: "stripe",
plans: [
{
id: "free", // 免费计划,无需 Price ID
},
{
id: "pro",
prices: [
{
id: "monthly",
provider: "stripe",
providerPriceId: "price_xxxxxxxxxxxxxxxx", // Stripe 月付 Price ID
currency: "usd",
amountCents: 1000, // $10.00
priceType: "subscription",
interval: "month",
trialDays: 7, // 免费试用天数,不需要则删除此字段
status: "active",
},
{
id: "yearly",
provider: "stripe",
providerPriceId: "price_xxxxxxxxxxxxxxxx", // Stripe 年付 Price ID
currency: "usd",
amountCents: 10000, // $100.00
priceType: "subscription",
interval: "year",
trialDays: 7,
status: "active",
},
],
},
{
id: "lifetime",
prices: [
{
id: "lifetime",
provider: "stripe",
providerPriceId: "price_xxxxxxxxxxxxxxxx", // Stripe 一次性 Price ID
currency: "usd",
amountCents: 20000, // $200.00
priceType: "lifetime",
status: "active",
},
],
},
],
},
},字段说明:
| 字段 | 说明 |
|---|---|
providerPriceId | Stripe 后台的 Price ID,格式 price_xxx |
amountCents | 价格(分),1000 = $10.00 |
priceType | "subscription" 订阅 / "lifetime" 一次性 |
interval | 订阅周期:"month" / "year"(lifetime 不填) |
trialDays | 免费试用天数,不需要则删除此字段 |
status | "active" 启用 / "archived" 归档(不显示在定价页) |
配置 Stripe Webhook
Webhook 用于接收 Stripe 推送的事件(如支付成功、订阅变更、发票等),是支付状态同步的核心机制。
本地开发(使用 Stripe CLI):
-
安装 Stripe CLI:
# macOS brew install stripe/stripe-cli/stripe -
登录 Stripe CLI:
stripe login -
启动本地 Webhook 转发(服务端默认监听
http://localhost:3001):stripe listen --forward-to http://localhost:3001/api/webhooks/stripe -
启动后终端会输出一个 Webhook signing secret,格式以
whsec_开头,将其填入:apps/server/.dev.vars STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
生产环境(Stripe Dashboard):
-
点击 Add endpoint
-
Endpoint URL 填写你的生产地址:
https://your-server.workers.dev/api/webhooks/stripe -
在 Events to send 选择以下事件(EasyStarter 已处理):
事件 说明 checkout.session.completed结账完成 checkout.session.async_payment_succeeded异步支付成功 checkout.session.async_payment_failed异步支付失败 checkout.session.expired结账会话过期 customer.subscription.created订阅创建 customer.subscription.updated订阅变更 customer.subscription.deleted订阅取消 payment_intent.succeeded支付意图成功 payment_intent.payment_failed支付意图失败 payment_intent.canceled支付意图取消 invoice.paid发票支付成功 invoice.payment_failed发票支付失败 invoice.marked_uncollectible发票标记为无法收取 invoice.voided发票作废 charge.dispute.created争议创建 charge.dispute.updated争议更新 charge.dispute.closed争议关闭 charge.refunded退款 -
保存后,点击 Webhook 端点详情,复制 Signing secret(格式
whsec_xxx)填入:apps/server/.env.production STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
填入环境变量并启动
确认两个变量已在开发环境中填写完整:
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx然后启动服务:
pnpm dev:server同时在另一个终端保持 Stripe CLI 转发运行,即可在本地完整测试支付流程。
测试卡号:Stripe 测试模式下可使用
4242 4242 4242 4242,有效期任意未来日期,CVV 任意三位数字。 更多测试卡见 Stripe Testing Docs。
定价路由配置
支付成功 / 取消后的跳转页面在 packages/app-config/src/app-config.ts 的 web.routes 中配置:
web: {
routes: {
billingSuccess: "/billing/success", // 支付成功后跳转
billingCancel: "/billing/cancel", // 取消支付后跳转
billingReturn: "/settings/billing", // Billing Portal 返回后跳转
},
},Billing Portal(用户自助管理)
EasyStarter 已内置 Stripe Billing Portal 集成。用户可在 /settings/billing 页面点击管理订阅,系统会自动创建 Portal 会话并跳转至 Stripe 提供的管理界面。
使用前需要在 Stripe 后台启用并配置 Billing Portal:
- 进入 Customer Portal Settings
- 按需开启取消订阅、更换支付方式、查看发票等功能
- 保存配置后即可生效
生产上线前检查
| 项目 | 检查点 |
|---|---|
| API 密钥 | 切换为 Live 模式的 sk_live_ 密钥 |
| Webhook Secret | 生产 Webhook 端点的 whsec_ 签名密钥 |
| Price ID | 使用 Live 模式下创建的 Price ID |
| Billing Portal | 已在 Stripe Dashboard 配置并启用 |
| 环境变量推送 | 通过 pnpm run secrets:bulk:production 推送到 Cloudflare Workers |
扩展其他支付服务商
EasyStarter 的支付层基于 PaymentProvider 接口设计,内置 Stripe 实现。如需替换为其他服务商(如 Paddle、LemonSqueezy 等),只需按以下六步操作,无需改动业务逻辑。
第一步:注册服务商 Key
在 packages/app-config/src/types.ts 中,将新服务商 key 追加到 SUPPORTED_WEB_PAYMENT_PROVIDERS:
export const SUPPORTED_WEB_PAYMENT_PROVIDERS = ["stripe", "paddle"] as const;这会自动同步更新 WebPaymentProviderKey 和 ServerPaymentProviderKey 类型。
第二步:实现 PaymentProvider 接口
在 apps/server/src/payments/providers/ 下新建目录,实现 PaymentProvider 接口:
import type {
CreateCheckoutInput,
CreatePortalInput,
ParsedWebhookEvent,
PaymentProvider,
WebhookInput,
} from "../../public/types";
export function createPaddlePaymentProvider(): PaymentProvider {
return {
key: "paddle",
async createCheckoutSession(input: CreateCheckoutInput) {
// 调用 Paddle SDK 创建结账会话
// 返回 { providerSessionId, url, expiresAt }
},
async createPortalSession(input: CreatePortalInput) {
// 返回 Paddle 订阅管理页面的 URL
// 返回 { providerSessionId, url }
},
async parseWebhookEvent(input: WebhookInput): Promise<ParsedWebhookEvent> {
// 验证签名,解析 payload
// 返回 { providerEventId, type, createdAt, payload }
},
async setSubscriptionCancelAtPeriodEnd(input) {
// 调用 Paddle API 设置到期取消
},
async updateSubscriptionPrice(input) {
// 调用 Paddle API 升级订阅价格
},
};
}接口方法说明:
| 方法 | 说明 |
|---|---|
createCheckoutSession | 创建结账会话,返回跳转 URL |
createPortalSession | 创建订阅管理门户会话,返回跳转 URL |
parseWebhookEvent | 验证 Webhook 签名并解析事件 |
setSubscriptionCancelAtPeriodEnd | 设置订阅到期时取消 |
updateSubscriptionPrice | 变更订阅价格(用于计划升级) |
第三步:实现 Webhook 事件处理器
在 apps/server/src/payments/providers/paddle/webhook/ 下创建事件处理逻辑,将 Paddle 事件映射到数据库操作:
import type { Database } from "@/db";
export async function handlePaddleEvent(db: Database, payload: unknown) {
const event = payload as { event_type: string; data: unknown };
switch (event.event_type) {
case "subscription.created":
case "subscription.updated":
case "subscription.canceled": {
// 同步订阅状态到 billing_subscription 表
break;
}
case "transaction.completed": {
// 处理一次性购买,写入 billing_purchase 表
break;
}
// 按需处理其他事件...
default:
break;
}
}参考
apps/server/src/payments/providers/stripe/webhook/目录的结构,将复杂事件拆分到独立文件中。
第四步:注册到 Provider 工厂
在 apps/server/src/payments/providers/index.ts 中,将新 Provider 注册进工厂:
import { createPaddlePaymentProvider } from "./paddle/provider";
const providers: Record<ServerPaymentProviderKey, () => PaymentProvider> = {
stripe: createStripePaymentProvider,
paddle: createPaddlePaymentProvider, // 新增
};第五步:添加 Webhook 路由
在 apps/server/src/index.ts 中,为新服务商添加一个专属 Webhook 路由:
app.post("/api/webhooks/paddle", async (c) => {
const context = await createContext({ context: c });
const rawBody = await c.req.text();
const signature = c.req.header("paddle-signature");
await context.payments.handleWebhookEvent({
provider: "paddle",
rawBody,
signature,
});
return c.json({ received: true });
});服务端的 handleWebhookEvent 会自动将事件路由到对应的 handlePaddleEvent 处理器。
第六步:配置定价计划并切换 Provider
在 packages/app-config/src/app-config.ts 中,将 web.payments.provider 改为新服务商,并填入对应的 Price ID:
web: {
payments: {
provider: "paddle", // 切换到新服务商
plans: [
{
id: "pro",
prices: [
{
id: "monthly",
provider: "paddle",
providerPriceId: "pri_xxxxxxxxxxxxxxxx", // Paddle Price ID
currency: "usd",
amountCents: 1000,
priceType: "subscription",
interval: "month",
status: "active",
},
],
},
],
},
},完成后,所有结账、升级、门户入口都会自动走新服务商,无需改动任何业务代码。