EasyStarter logoEasyStarter
支付

Creem 支付

配置 Creem Checkout、账单管理门户和 Webhook

Creem 支付集成

EasyStarter 的 Web 端内置了完整的 Creem 支付支持,仅适用于 Web 端,开箱即用,包含:

  • 订阅制(月付 / 年付)结账
  • 一次性买断(Lifetime)结账
  • 免费试用期(Trial)
  • 客户账单管理门户(用户自助管理订阅)
  • 计划升级(支持按比例折算)
  • Webhook 事件处理(订阅状态同步、退款、争议等)

支付配置分为两部分:

  1. 环境变量:API 密钥与 Webhook 密钥,填入服务端 .dev.vars / .env.production
  2. 定价计划:在 packages/app-config/src/app-config.ts 中配置 Creem Product ID 与价格信息

所需环境变量

CREEM_API_KEY=
CREEM_WEBHOOK_SECRET=

注册 Creem 并获取 API 密钥

  1. 前往 creem.io 注册账号
  2. 登录后进入控制台的 API Keys 页面
  3. 复制 API 密钥(以 creem_test_ 开头为测试密钥,creem_live_ 为生产密钥)

初始阶段使用测试密钥即可。上线前切换为生产密钥。

将复制的密钥填入:

apps/server/.dev.vars
CREEM_API_KEY=creem_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
apps/server/.env.production
CREEM_API_KEY=creem_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

EasyStarter 会根据密钥前缀自动路由 API 请求:

  • creem_test_https://test-api.creem.io/v1
  • creem_live_https://api.creem.io/v1

在 Creem 创建产品

EasyStarter 默认包含三个计划:FreePro(月付 + 年付)、Lifetime(一次性买断)。

Free 计划无需在 Creem 创建,仅在本地配置中用作占位。以下为 Pro 和 Lifetime 的创建步骤:

  1. 在 Creem 控制台进入 Products 页面,点击 Create product
  2. 填写产品名称,例如 Pro Monthly
  3. 订阅计划(月付 / 年付):计费类型选 Recurring,分别设置月付周期和年付周期,每个周期各建一个产品
  4. 买断计划(Lifetime):计费类型选 One time,建一个产品
  5. 每个产品保存后,复制其 Product ID(格式为 prod_xxxxxxx),下一步将用到

配置定价计划

将上一步获取的 Product ID 填入 packages/app-config/src/app-config.tsweb.payments.plans 字段:

packages/app-config/src/app-config.ts
web: {
  payments: {
    provider: "creem",
    // test/prod 分别配置不同环境的 Creem Product ID。
    // creem_test_ 使用测试产品 ID,creem_live_ 使用生产产品 ID。
    plans: [
      {
        id: "free",   // 免费计划,无需 Product ID
      },
      {
        id: "pro",
        prices: [
          {
            id: "monthly",
            provider: "creem",
            test: {
              providerPriceId: "prod_xxxxxxxxxxxxxxxx",  // 测试环境 Creem 月付 Product ID
            },
            prod: {
              providerPriceId: "prod_xxxxxxxxxxxxxxxx",  // 生产环境 Creem 月付 Product ID
            },
            currency: "usd",
            amountCents: 1000,    // $10.00
            priceType: "subscription",
            interval: "month",
            trialDays: 7,         // 必须与 Creem 产品中设置的试用天数一致
            status: "active",
          },
          {
            id: "yearly",
            provider: "creem",
            test: {
              providerPriceId: "prod_xxxxxxxxxxxxxxxx",  // 测试环境 Creem 年付 Product ID
            },
            prod: {
              providerPriceId: "prod_xxxxxxxxxxxxxxxx",  // 生产环境 Creem 年付 Product ID
            },
            currency: "usd",
            amountCents: 10000,   // $100.00
            priceType: "subscription",
            interval: "year",
            trialDays: 7,         // 必须与 Creem 产品中设置的试用天数一致
            status: "active",
          },
        ],
      },
      {
        id: "lifetime",
        prices: [
          {
            id: "lifetime",
            provider: "creem",
            test: {
              providerPriceId: "prod_xxxxxxxxxxxxxxxx",  // 测试环境 Creem 一次性 Product ID
            },
            prod: {
              providerPriceId: "prod_xxxxxxxxxxxxxxxx",  // 生产环境 Creem 一次性 Product ID
            },
            currency: "usd",
            amountCents: 20000,   // $200.00
            priceType: "lifetime",
            status: "active",
          },
        ],
      },
    ],
  },
},

字段说明:

字段说明
test.providerPriceIdCreem 测试环境的 Product ID,格式 prod_xxx
prod.providerPriceIdCreem 生产环境的 Product ID,格式 prod_xxx
amountCents价格(分),1000 = $10.00
priceType"subscription" 订阅 / "lifetime" 一次性
interval订阅周期:"month" / "year"(lifetime 不填)
trialDays必须与 Creem 产品中设置的试用天数保持一致 —— Creem 不支持通过 API 设置试用期,此字段仅用于前端展示,实际试用天数以 Creem 控制台产品配置为准
status"active" 启用 / "archived" 归档(不显示在定价页)

配置 Creem Webhook

Webhook 用于接收 Creem 推送的事件(如支付成功、订阅变更、退款等),是支付状态同步的核心机制。

  1. 在 Creem 控制台进入 Webhooks 页面

  2. 点击 Add endpoint

  3. 填写 Endpoint URL

    • 本地开发:https://your-ngrok-url/api/webhooks/creem(需使用 ngrok 等隧道工具)
    • 生产环境:https://your-server.workers.dev/api/webhooks/creem
  4. Events to send 中选择以下事件(EasyStarter 已处理):

    事件说明
    checkout.completed结账完成(订阅或一次性)
    subscription.active订阅激活
    subscription.trialing试用开始
    subscription.paid续费成功
    subscription.scheduled_cancel到期取消已安排
    subscription.past_due付款逾期
    subscription.update订阅变更
    subscription.expired订阅到期
    subscription.canceled订阅取消
    subscription.paused订阅暂停
    refund.created退款处理
    dispute.created争议 / 拒付创建
  5. 保存后,复制 Webhook Secret 填入:

apps/server/.dev.vars
CREEM_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
apps/server/.env.production
CREEM_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Creem 使用 HMAC-SHA256 签名 Webhook 负载,EasyStarter 会自动从 creem-signature 请求头验证签名。

填入环境变量并启动

确认两个变量已在开发环境中填写完整:

apps/server/.dev.vars
CREEM_API_KEY=creem_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CREEM_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

然后启动服务:

pnpm dev:server

由于 Creem 没有像 Stripe CLI 那样的本地转发工具,本地测试 Webhook 需要使用隧道服务(如 ngrok):

ngrok http 3001

然后将 ngrok URL 设置为 Creem 控制台中的 Webhook 端点。

定价路由配置

支付成功 / 取消后的跳转页面在 packages/app-config/src/app-config.tsweb.routes 中配置:

packages/app-config/src/app-config.ts
web: {
  routes: {
    billingSuccess: "/billing/success",  // 支付成功后跳转
    billingCancel: "/billing/cancel",    // 取消支付后跳转
    billingReturn: "/settings/billing",  // 账单管理门户返回后跳转
  },
},

客户账单管理门户

EasyStarter 已内置 Creem 客户账单管理门户集成。用户可在 /settings/billing 页面点击管理订阅,系统会自动通过 Creem API 创建管理会话并跳转至 Creem 提供的管理界面。

订阅状态映射

Creem 的订阅状态比 Stripe 更细粒度。以下是 Creem 状态到 EasyStarter 内部状态的映射关系:

Creem 状态内部状态有权限说明
activeactive正常活跃订阅
trialingtrialing免费试用期
scheduled_cancelactive (cancelAtPeriodEnd)到期前仍可使用
pausedpaused订阅已暂停
past_dueunpaid付款逾期
unpaidunpaid未付款
incompleteunpaid设置未完成
failedunpaid支付失败
canceledcanceled订阅已取消
expiredcanceled订阅已过期

扩展其他支付服务商

EasyStarter 的支付层基于 PaymentProvider 接口设计,内置 Stripe 和 Creem 实现。如需替换为其他服务商(如 PaddleLemonSqueezy 等),只需按以下六步操作,无需改动业务逻辑。

第一步:注册服务商 Key

packages/app-config/src/types.ts 中,将新服务商 key 追加到 SUPPORTED_WEB_PAYMENT_PROVIDERS

packages/app-config/src/types.ts
export const SUPPORTED_WEB_PAYMENT_PROVIDERS = ["stripe", "creem", "paddle"] as const;

这会自动同步更新 WebPaymentProviderKeyServerPaymentProviderKey 类型。

第二步:实现 PaymentProvider 接口

apps/server/src/payments/providers/ 下新建目录,实现 PaymentProvider 接口:

apps/server/src/payments/providers/paddle/provider.ts
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 事件映射到数据库操作:

apps/server/src/payments/providers/paddle/webhook/handle-event.ts
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 注册进工厂:

apps/server/src/payments/providers/index.ts
import { createPaddlePaymentProvider } from "./paddle/provider";

const providers: Record<ServerPaymentProviderKey, () => PaymentProvider> = {
  stripe: createStripePaymentProvider,
  creem: createCreemPaymentProvider,
  paddle: createPaddlePaymentProvider,  // 新增
};

第五步:添加 Webhook 路由

apps/server/src/index.ts 中,为新服务商添加一个专属 Webhook 路由:

apps/server/src/index.ts
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:

packages/app-config/src/app-config.ts
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",
          },
        ],
      },
    ],
  },
},

完成后,所有结账、升级、门户入口都会自动走新服务商,无需改动任何业务代码。

生产上线前检查

项目检查点
API 密钥切换为生产密钥 creem_live_
Webhook Secret生产 Webhook 端点的签名密钥
Product ID使用生产模式下创建的 Product ID
Webhook 端点生产 URL 已在 Creem 控制台配置
环境变量推送通过 pnpm run secrets:bulk:production 推送到 Cloudflare Workers