支付
通过拆分包设计集成 Stripe、Creem、Polar、Dodo 或 Lemon Squeezy 支付。处理一次性购买、订阅、Webhook 和账单,使用统一 API。
LaunchSaaS 通过拆分包设计提供支付支持。核心包 @launchsaas/payment 只定义接口;每个支付提供商都是独立包,按需安装。
更新时间:2026-03-26
架构
@launchsaas/payment ← 接口 + webhook 路由助手(始终需要)
@launchsaas/payment-stripe ← Stripe 提供商
@launchsaas/payment-creem ← Creem 提供商
@launchsaas/payment-polar ← Polar 提供商
@launchsaas/payment-dodo ← Dodo Payments 提供商
@launchsaas/payment-lemonsqueezy ← Lemon Squeezy 提供商你只需安装实际使用的提供商包。
LaunchSaaS 默认内置 @launchsaas/payment-stripe。在你自己的应用中,
可以安装任意你想用的提供商,或者在 src/capabilities.ts 中将 payment: null
以彻底禁用支付功能。
所有支付提供商都遵循同一套职责拆分:
checkout.completed只为一次性支付创建订单和权益- 订阅支付成功事件负责创建订阅订单并激活权益
- 其他
subscription.*事件只同步权益状态和周期日期
支付系统有三个核心概念:
- Order(订单) — 已确认货币交易的不可变记录
- Entitlement(权益) — 用户对产品访问权限的可变记录
- PaymentEventHandler — 用于持久化和后置操作的应用层接口
设置 Stripe
1. 安装提供商包
在你的应用目录中:
pnpm add @launchsaas/payment @launchsaas/payment-stripe2. 配置环境变量
将提供商的 keys 导出添加到 src/env.ts(或对应文件):
// src/env.ts
import { keys as paymentKeys } from "@launchsaas/payment-stripe/src/keys";
export const env = createEnv({
extends: [
// ... 其他 keys
paymentKeys,
],
// ...
});3. 在 src/capabilities.ts 中注册提供商
打开你的应用的 src/capabilities.ts,将 AppPaymentEventHandler 传入 create():
// src/capabilities.ts (apps/launchsaas/src/capabilities.ts)
import { StripePaymentProvider } from "@launchsaas/payment-stripe";
import { AppPaymentEventHandler } from "@/lib/features/payment/event-handler";
export const capabilities = {
// 设为 null 即可完全禁用支付功能
payment: StripePaymentProvider.create(new AppPaymentEventHandler()),
// ...其他 capabilities
};将 payment 设为 null 即可完全禁用支付功能。
4. 配置 Webhook 路由
创建接收支付 webhook 的 API 路由文件:
// src/app/api/payment/[provider]/webhook/route.ts
import { toHandler } from "@launchsaas/payment";
import { capabilities } from "@/capabilities";
export const { POST } = toHandler(capabilities.payment);这个文件适用于所有支付提供商。[provider] 段在运行时会与 capabilities.payment.name 进行校验。
5. 创建 Stripe 账户并获取 API 密钥
- 在 stripe.com 注册并完成账户验证
- 前往 Stripe Dashboard →
Developers→API keys - 复制你的密钥(测试模式下以
sk_test_开头) - 添加到你的
.env文件:
STRIPE_SECRET_KEY="sk_test_..."开发环境使用测试密钥(sk_test_...),生产环境使用正式密钥
(sk_live_...)。
6. 创建产品和价格
- 前往 Stripe Dashboard →
Product Catalog - 点击
Add product - 填写产品详情:
- 名称:例如 “终身访问”
- 描述:你的产品描述
- 价格:设置价格(一次性或定期)
- 点击 "Save product"
- 复制 Price ID(以
price_开头)
7. 配置产品
在 apps/launchsaas/src/configuration/product/products.ts 中更新产品目录:
// apps/launchsaas/src/configuration/product/products.ts
import type { Product } from "./types";
export const products: Product[] = [
{
id: "price_1SWtddRwnUQyjRPerb8ge9xD",
name: "终身访问",
provider: "stripe",
type: "onetime",
sandbox: false,
// 可选:在结账时向买家收集额外字段
customFields: [
{ key: "githubUsername", label: "GitHub Username", type: "text" },
],
},
{
id: "price_1SOwEE2Kn68A5jDtD9vcpirC",
name: "专业版月付",
provider: "stripe",
type: "subscription",
sandbox: false,
},
];customFields 用于定义 Stripe Checkout 页面向买家展示的额外字段,例如 GitHub 用户名。收集到的值会通过 data.metadata.customFields 传给你的 AppPaymentEventHandler。
通过 sandbox: true 表示测试环境产品,sandbox: false 表示生产环境产品。定价页实际使用哪个产品,则由 apps/launchsaas/src/configuration/product/index.ts 单独选择。
8. 设置 Webhooks
Webhooks 实时通知你的应用程序支付事件。
开发环境(本地测试)
使用 Stripe CLI 将 webhooks 转发到本地服务器:
- 安装 Stripe CLI:
# macOS
brew install stripe/stripe-cli/stripe
# 其他平台:https://stripe.com/docs/stripe-cli- 登录 Stripe:
stripe login- 转发 webhooks:
stripe listen --forward-to localhost:3000/api/payment/stripe/webhook- 复制 webhook secret(以
whsec_开头)并添加到你的.env文件:
STRIPE_WEBHOOK_SECRET="whsec_..."stripe listen 命令运行。生产环境
- 前往 Stripe Dashboard →
Developers→Webhooks - 点击
Add endpoint - 设置端点 URL:
https://yourdomain.com/api/payment/stripe/webhook - 选择要监听的事件:
checkout.session.completedinvoice.paidcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deleted
- 点击
Add endpoint - 复制 Signing secret 并添加到生产环境
必须监听的事件
在 Stripe Webhook 端点中配置以下事件:
checkout.session.completedinvoice.paidcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deleted
Stripe Webhook 流程
LaunchSaaS 使用分离事件模型,三条职责线完全独立,不受事件投递顺序影响。
一次性支付流程
订阅流程
事件职责
| 事件 | 操作 |
|---|---|
checkout.session.completed (mode=payment) | 创建订单 + 激活权益(一次性) |
checkout.session.completed (mode=subscription) | 忽略——订阅由 invoice.paid 处理 |
invoice.paid | 创建订单 + 激活权益(重复权益插入会被忽略) |
customer.subscription.created(试用期) | 创建权益,状态为 trialing |
customer.subscription.created/updated | 更新权益:周期、状态、取消信息(若不存在则为空操作) |
customer.subscription.deleted | 更新权益状态为 canceled |
为什么这样设计
checkout.session.completed(mode=payment) 是一次性购买的明确信号,mode字段无歧义。invoice.paid是订阅付款成功的权威信号。若active权益尚未存在,它会负责创建;重复创建事件会被安全忽略。customer.subscription.*分两种情况:对于trialing订阅,它负责创建权益(因为试用期间不会触发invoice.paid);对于其他状态,它只更新已有记录,绝不会将订阅升级为active,因此即使它先于invoice.paid到达,也不会错误授予访问权限。
这确保即使 Stripe 乱序投递事件,权益更新也始终正确。
幂等性
| 记录 | 键 |
|---|---|
| 订单(一次性) | stripe_${payment_intent.id} |
| 订单(订阅) | stripe_${invoice.id} |
| 权益(一次性) | stripe_pi_${payment_intent.id} |
| 权益(订阅) | stripe_${subscription.id} |
重复投递的事件是安全的——重复插入会被静默忽略。
测试卡
用于测试 Stripe 集成的测试信用卡:
| 卡号 | 描述 |
|---|---|
4242 4242 4242 4242 | 成功支付 |
4000 0000 0000 3220 | 需要 3D 验证 |
4000 0000 0000 9995 | 余额不足 |
使用任何未来的过期日期和任何 3 位 CVC。
设置 Creem
1. 安装提供商包
在你的应用目录中:
pnpm add @launchsaas/payment @launchsaas/payment-creem2. 在 src/capabilities.ts 中注册提供商
import { CreemPaymentProvider } from "@launchsaas/payment-creem";
import { AppPaymentEventHandler } from "@/lib/features/payment/event-handler";
export const capabilities = {
// 设为 null 即可禁用支付功能
payment: CreemPaymentProvider.create(new AppPaymentEventHandler()),
// ...其他 capabilities
};3. 创建 Creem 账户并获取 API 密钥
- 在 creem.io 注册并完成账户验证
- 前往 Creem Dashboard → Settings → API Keys
- 复制你的 API key 和 webhook secret
- 添加到你的
.env文件:
CREEM_API_KEY="your-api-key"
CREEM_WEBHOOK_SECRET="your-webhook-secret"4. 配置产品
在 apps/launchsaas/src/configuration/product/products.ts 中更新产品目录:
import type { Product } from "./types";
export const products: Product[] = [
{
id: "prod_3h218lKmpAIM9xTv97Mdy7",
name: "终身访问",
provider: "creem",
type: "onetime",
sandbox: false,
customFields: [
{ key: "githubUsername", label: "GitHub Username", type: "text" },
],
},
];Creem 的 checkout 自定义字段只支持 type: "text" 和 type: "checkbox"。其他字段类型会在创建结账会话时被拒绝。
5. 设置 Webhooks
在 Creem Dashboard 中配置 webhook 端点:
- URL:
https://yourdomain.com/api/payment/creem/webhook - Events: 所有支付相关事件
如果你正在设置环境,现在可以返回环境设置指南继续。
设置 Polar
1. 安装提供商包
pnpm add @launchsaas/payment @launchsaas/payment-polar2. 在 src/capabilities.ts 中注册提供商
import { PolarPaymentProvider } from "@launchsaas/payment-polar";
import { AppPaymentEventHandler } from "@/lib/features/payment/event-handler";
export const capabilities = {
payment: PolarPaymentProvider.create(new AppPaymentEventHandler()),
// ...其他 capabilities
};3. 创建 Polar 账户并获取 API 密钥
- 在 polar.sh 注册并创建一个组织
- 前往 Settings → Developers → Personal Access Tokens → New token
- 复制访问令牌并添加到你的
.env文件:
POLAR_ACCESS_TOKEN="your-access-token"
POLAR_WEBHOOK_SECRET="your-webhook-secret"
# "sandbox"(默认)或 "production"
POLAR_MODE="sandbox"4. 配置产品
Polar 使用产品价格 ID 作为产品标识符,从你的 Polar 产品页面复制价格 ID:
export const products: Product[] = [
{
id: "price_abc123", // Polar 产品价格 ID
name: "终身访问",
provider: "polar",
type: "onetime",
sandbox: false,
},
];Polar 不支持 LaunchSaaS 动态自定义 checkout 表单项。
LaunchSaaS 仍然可以从 Polar webhook 载荷中的 custom_field_data 读取提交值,
并将其暴露为支付事件处理器里的 data.metadata.customFields。
5. 设置 Webhooks
在你的 Polar 组织设置中配置 webhook 端点:
- URL:
https://yourdomain.com/api/payment/polar/webhook - Events:
order.paid,subscription.created,subscription.updated,subscription.canceled,subscription.revoked
设置 Dodo Payments
1. 安装提供商包
pnpm add @launchsaas/payment @launchsaas/payment-dodo2. 在 src/capabilities.ts 中注册提供商
import { DodoPaymentProvider } from "@launchsaas/payment-dodo";
import { AppPaymentEventHandler } from "@/lib/features/payment/event-handler";
export const capabilities = {
payment: DodoPaymentProvider.create(new AppPaymentEventHandler()),
// ...其他 capabilities
};3. 创建 Dodo Payments 账户并获取 API 密钥
- 在 dodopayments.com 注册并完成验证
- 前往 Developers → API Keys
- 复制你的 API key 和 webhook key 并添加到你的
.env文件:
DODO_PAYMENTS_API_KEY="your-api-key"
DODO_PAYMENTS_WEBHOOK_KEY="your-webhook-key"
# "test_mode"(默认)或 "live_mode"
DODO_PAYMENTS_ENVIRONMENT="test_mode"4. 配置产品
Dodo 使用你的 Dodo 产品目录中的产品 ID:
export const products: Product[] = [
{
id: "prod_abc123", // Dodo 产品 ID
name: "终身访问",
provider: "dodo",
type: "onetime",
sandbox: false,
},
];Dodo 会把 LaunchSaaS customFields 透传给 Dodo API。你可以继续使用内置的
options 便捷写法来配置下拉类字段。
5. 设置 Webhooks
在你的 Dodo Dashboard 中配置 webhook 端点:
- URL:
https://yourdomain.com/api/payment/dodo/webhook - Events:
payment.succeeded,subscription.active,subscription.renewed,subscription.updated,subscription.on_hold,subscription.cancelled,subscription.expired
Dodo 内置客户门户。调用 createBillingPortal() 时,账单门户 URL 会通过 Dodo
Payments API 自动获取。
设置 Lemon Squeezy
1. 安装提供商包
pnpm add @launchsaas/payment @launchsaas/payment-lemonsqueezy2. 在 src/capabilities.ts 中注册提供商
import { LemonSqueezyPaymentProvider } from "@launchsaas/payment-lemonsqueezy";
import { AppPaymentEventHandler } from "@/lib/features/payment/event-handler";
export const capabilities = {
payment: LemonSqueezyPaymentProvider.create(new AppPaymentEventHandler()),
// ...其他 capabilities
};3. 创建 Lemon Squeezy 账户并获取 API 密钥
- 在 lemonsqueezy.com 注册并创建商店
- 前往 Settings → API → New API Key
- 复制 API key 和商店设置中的 Store ID:
LEMONSQUEEZY_API_KEY="your-api-key"
LEMONSQUEEZY_WEBHOOK_SECRET="your-webhook-secret"
LEMONSQUEEZY_STORE_ID="your-store-id"4. 配置产品
Lemon Squeezy 使用变体 ID 作为产品标识符:
export const products: Product[] = [
{
id: "123456", // Lemon Squeezy 变体 ID(数字字符串)
name: "终身访问",
provider: "lemonsqueezy",
type: "onetime",
sandbox: false,
},
];Lemon Squeezy 不支持 LaunchSaaS 动态自定义 checkout 表单项。
5. 设置 Webhooks
在你的 Lemon Squeezy 商店设置中配置 webhook 端点:
- URL:
https://yourdomain.com/api/payment/lemonsqueezy/webhook - Events:
order_created,subscription_created,subscription_updated,subscription_cancelled,subscription_resumed,subscription_expired,subscription_payment_success
PaymentEventHandler 接口是支付后逻辑的核心扩展点。Webhook 处理期间,提供商会调用该接口的三个方法:
export interface PaymentEventHandler {
onOrderCreated(data: OrderData): Promise<void>;
onEntitlementCreated(data: EntitlementData): Promise<void>;
onEntitlementUpdated(data: EntitlementUpdateData): Promise<void>;
}LaunchSaaS 内置了 AppPaymentEventHandler,位于 src/lib/features/payment/event-handler.ts。它负责:
onOrderCreated— 将订单保存到数据库并发送支付确认邮件onEntitlementCreated— 若权益不存在则插入,然后在metadata中存在customFields.githubUsername时将买家添加为 GitHub 协作者onEntitlementUpdated— 将订阅状态/周期变更同步到数据库
自定义后置操作
直接编辑 AppPaymentEventHandler 来添加、删除或修改后置操作:
// src/lib/features/payment/event-handler.ts
export class AppPaymentEventHandler implements PaymentEventHandler {
async onOrderCreated(data: OrderData): Promise<void> {
await createOrder({
...data,
provider: data.provider as Order["provider"],
});
// 在此添加或替换后置操作
try {
await sendPaymentConfirmationEmail(data.referenceId);
} catch (error) {
console.error(
"[PaymentEventHandler] Failed to send payment email:",
error,
);
}
}
async onEntitlementCreated(data: EntitlementData): Promise<void> {
await createEntitlement({
...data,
provider: data.provider as Entitlement["provider"],
status: data.status as Entitlement["status"],
});
// 结账时收集的自定义字段存储在 data.metadata.customFields 中
const customFields = data.metadata?.customFields as
| Record<string, string>
| undefined;
try {
await addGitHubCollaborator(customFields?.githubUsername);
} catch (error) {
console.error(
"[PaymentEventHandler] Failed to add GitHub collaborator:",
error,
);
}
}
async onEntitlementUpdated(data: EntitlementUpdateData): Promise<void> {
const { id, ...rest } = data;
await updateEntitlement(id, {
...rest,
status: data.status as Entitlement["status"] | undefined,
});
}
}内置后置操作
GitHub 集成
支付后自动将客户添加为 GitHub 协作者。当买家在结账时填写 githubUsername 时触发(需要产品配置了 customFields,参见产品自定义字段)。
配置环境变量:
GITHUB_TOKEN="your-personal-access-token"
GITHUB_REPO="owner/repo"在 GitHub Settings 生成具有 admin:org 和 repo 权限的 GitHub Personal Access Token。
支付确认邮件
成功支付后发送确认邮件。需要在 capabilities.ts 中配置邮件提供商。
产品自定义字段
产品可以定义 customFields,在结账时向买家收集额外信息。LaunchSaaS 不会在共享层集中限制 type 字符串;只要 provider 支持,收集到的值就会通过 data.metadata.customFields 传递给 AppPaymentEventHandler。
{
id: "price_xxx",
name: "终身访问",
customFields: [
{ key: "githubUsername", label: "GitHub Username", type: "text" },
{ key: "plan", label: "Plan", type: "dropdown", options: ["starter", "pro"] },
{ key: "agreeToTerms", label: "Terms", type: "checkbox" },
],
}字段定义:
| 属性 | 类型 | 说明 |
|---|---|---|
key | string | metadata.customFields 中使用的键 |
label | string | 结账时展示给买家的标签 |
type | string | 由 provider 自定义的字段类型 |
optional | boolean | 是否可选(默认:true) |
options | string[] | 针对下拉类 provider 的便捷选项字段 |
提供商说明:
| 提供商 | 说明 |
|---|---|
| Stripe | Stripe Checkout 自定义字段只支持 text、numeric 和 dropdown。dropdown 必须提供 options。 |
| Creem | Creem checkout 自定义字段只支持 text 和 checkbox。 |
| Dodo Payments | Dodo 自定义字段只支持 text、number、email、url、date、dropdown 和 boolean。dropdown 必须提供 options。 |
| Polar | 不支持 LaunchSaaS 动态自定义 checkout 表单项。LaunchSaaS 只会读取 webhook 返回的 custom_field_data。 |
| Lemon Squeezy | 不支持 LaunchSaaS 动态自定义 checkout 表单项。 |
数据库架构
Order 表
代表已确认的货币交易:
- 支付确认时创建
- 一次性产品在结账完成时创建订单
- 订阅产品在每次成功支付时创建订单
Entitlement 表
代表用户对产品的访问权限:
- 订单支付时授予/延长
- 一次性产品创建状态为 "active" 的权益
- 订阅产品在试用期创建状态为
trialing的权益,首次支付后创建或变为active - 包含订阅的周期信息
自定义支付提供商
要添加自定义支付提供商:
- 创建新包(如
packages/capabilities/payment-custom/) - 实现
PaymentProvider接口 - 在你的应用的
src/capabilities.ts中注册
// packages/capabilities/payment-custom/src/provider.ts
import type {
BillingPortalResult,
CheckoutOptions,
CheckoutResult,
PaymentEventHandler,
PaymentProvider,
UpgradeSubscriptionOptions,
} from "@launchsaas/payment";
export class CustomPaymentProvider implements PaymentProvider {
readonly name = "custom";
constructor(private readonly handler: PaymentEventHandler) {}
static create(handler: PaymentEventHandler): CustomPaymentProvider {
return new CustomPaymentProvider(handler);
}
getSignatureHeader(): string {
return "custom-signature";
}
async verifySignature(
rawBody: string | Buffer,
signature: string,
): Promise<boolean> {
// 验证 webhook 签名
return true;
}
async handleEvent(
rawBody: string | Buffer,
signature: string,
): Promise<void> {
// 验证、解析并分发到:
// this.handler.onOrderCreated(...)
// this.handler.onEntitlementCreated(...)
// this.handler.onEntitlementUpdated(...)
}
async createCheckout(options: CheckoutOptions): Promise<CheckoutResult> {
return { url: "", sessionId: "" };
}
async createBillingPortal(
customerId: string,
returnUrl: string,
): Promise<BillingPortalResult> {
return { url: "" };
}
async upgradeSubscription(
options: UpgradeSubscriptionOptions,
): Promise<void> {}
async cancelSubscription(subscriptionId: string): Promise<void> {}
}在你的应用的 src/capabilities.ts 中注册:
import { CustomPaymentProvider } from "@launchsaas/payment-custom";
import { AppPaymentEventHandler } from "@/lib/features/payment/event-handler";
export const capabilities = {
payment: CustomPaymentProvider.create(new AppPaymentEventHandler()),
// ...其他 capabilities
};