LogoLaunchSaaS

支付

通过拆分包设计集成 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-stripe

2. 配置环境变量

将提供商的 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 密钥

  1. stripe.com 注册并完成账户验证
  2. 前往 Stripe DashboardDevelopersAPI keys
  3. 复制你的密钥(测试模式下以 sk_test_ 开头)
  4. 添加到你的 .env 文件:
STRIPE_SECRET_KEY="sk_test_..."

开发环境使用测试密钥(sk_test_...),生产环境使用正式密钥 (sk_live_...)。

6. 创建产品和价格

  1. 前往 Stripe DashboardProduct Catalog
  2. 点击 Add product
  3. 填写产品详情:
    • 名称:例如 “终身访问”
    • 描述:你的产品描述
    • 价格:设置价格(一次性或定期)
  4. 点击 "Save product"
  5. 复制 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 转发到本地服务器:

  1. 安装 Stripe CLI:
# macOS
brew install stripe/stripe-cli/stripe

# 其他平台:https://stripe.com/docs/stripe-cli
  1. 登录 Stripe:
stripe login
  1. 转发 webhooks:
stripe listen --forward-to localhost:3000/api/payment/stripe/webhook
  1. 复制 webhook secret(以 whsec_ 开头)并添加到你的 .env 文件:
STRIPE_WEBHOOK_SECRET="whsec_..."
在开发时保持 stripe listen 命令运行。

生产环境

  1. 前往 Stripe DashboardDevelopersWebhooks
  2. 点击 Add endpoint
  3. 设置端点 URL:https://yourdomain.com/api/payment/stripe/webhook
  4. 选择要监听的事件:
    • checkout.session.completed
    • invoice.paid
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
  5. 点击 Add endpoint
  6. 复制 Signing secret 并添加到生产环境

必须监听的事件

在 Stripe Webhook 端点中配置以下事件:

  • checkout.session.completed
  • invoice.paid
  • customer.subscription.created
  • customer.subscription.updated
  • customer.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-creem

2. 在 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 密钥

  1. creem.io 注册并完成账户验证
  2. 前往 Creem Dashboard → Settings → API Keys
  3. 复制你的 API key 和 webhook secret
  4. 添加到你的 .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-polar

2. 在 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 密钥

  1. polar.sh 注册并创建一个组织
  2. 前往 SettingsDevelopersPersonal Access TokensNew token
  3. 复制访问令牌并添加到你的 .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-dodo

2. 在 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 密钥

  1. dodopayments.com 注册并完成验证
  2. 前往 DevelopersAPI Keys
  3. 复制你的 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-lemonsqueezy

2. 在 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 密钥

  1. lemonsqueezy.com 注册并创建商店
  2. 前往 SettingsAPINew API Key
  3. 复制 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:orgrepo 权限的 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" },
  ],
}

字段定义:

属性类型说明
keystringmetadata.customFields 中使用的键
labelstring结账时展示给买家的标签
typestring由 provider 自定义的字段类型
optionalboolean是否可选(默认:true)
optionsstring[]针对下拉类 provider 的便捷选项字段

提供商说明:

提供商说明
StripeStripe Checkout 自定义字段只支持 textnumericdropdowndropdown 必须提供 options
CreemCreem checkout 自定义字段只支持 textcheckbox
Dodo PaymentsDodo 自定义字段只支持 textnumberemailurldatedropdownbooleandropdown 必须提供 options
Polar不支持 LaunchSaaS 动态自定义 checkout 表单项。LaunchSaaS 只会读取 webhook 返回的 custom_field_data
Lemon Squeezy不支持 LaunchSaaS 动态自定义 checkout 表单项。

数据库架构

Order 表

代表已确认的货币交易:

  • 支付确认时创建
  • 一次性产品在结账完成时创建订单
  • 订阅产品在每次成功支付时创建订单

Entitlement 表

代表用户对产品的访问权限:

  • 订单支付时授予/延长
  • 一次性产品创建状态为 "active" 的权益
  • 订阅产品在试用期创建状态为 trialing 的权益,首次支付后创建或变为 active
  • 包含订阅的周期信息

自定义支付提供商

要添加自定义支付提供商:

  1. 创建新包(如 packages/capabilities/payment-custom/
  2. 实现 PaymentProvider 接口
  3. 在你的应用的 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
};

参考资料

下一步