ProductReadyProductReady

计费与订阅

多服务商计费支持 - Creem、Stripe、LemonSqueezy 和 Airwallex,订阅、一次性支付和 Webhook

计费与订阅

ProductReady 使用 @billing - 一个多服务商计费抽象层,支持 Creem(默认)、StripeLemonSqueezyAirwallex,提供统一的 API。

默认服务商:Creem - ProductReady 默认使用 Creem 作为计费服务商。Creem 是商户记录服务商(MoR),自动处理税务、合规和全球支付。


快速概览

支持的服务商:

  • Creem - 商户记录,全球支付(默认)
  • Stripe - 行业标准(推荐用于美国/欧盟)
  • LemonSqueezy - 商户记录(处理税务/合规)
  • Airwallex - 适合亚太地区

主要特性:

  • 🔒 类型安全 - 完整的 TypeScript 支持和 Zod 验证
  • 🎯 服务商抽象 - 所有服务商的一致接口
  • 🚀 延迟创建 - 按需创建产品/价格
  • 📦 可树摇 - 只导入需要的内容
  • 🔄 轻松迁移 - 切换服务商无需重写代码

你可以做什么:

  • ✅ 一次性支付
  • ✅ 定期订阅(按月/按年)
  • ✅ 基于使用量的计费
  • ✅ 客户管理
  • ✅ Webhook 处理
  • ✅ 订阅生命周期(取消、更新、重新激活)

安装

pnpm add billing

无需额外依赖 - 服务商 SDK 已捆绑。


快速开始

步骤 1:选择服务商

最适合:全球业务,自动处理税务/合规

环境变量

CREEM_API_KEY=creem_test_...  # 或 creem_live_... 用于生产环境
CREEM_WEBHOOK_SECRET=your-webhook-secret

获取密钥

  1. creem.io 注册
  2. Dashboard → API Keys
  3. Webhooks:Settings → Webhooks → Add endpoint

自动检测:API URL 根据密钥前缀自动检测:

密钥前缀API URL
creem_test_*https://test-api.creem.io
creem_live_*https://api.creem.io

最适合:美国/欧盟企业,最大灵活性

环境变量

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

获取密钥

  1. stripe.com 注册
  2. Dashboard → Developers → API keys
  3. Webhooks:Developers → Webhooks → Add endpoint

最适合:个人创始人(为您处理增值税/税务)

环境变量

LEMONSQUEEZY_API_KEY=your-api-key
LEMONSQUEEZY_STORE_ID=your-store-id
LEMONSQUEEZY_WEBHOOK_SECRET=your-webhook-secret

最适合:亚太地区企业

环境变量

AIRWALLEX_API_KEY=your-api-key
AIRWALLEX_CLIENT_ID=your-client-id
AIRWALLEX_ACCOUNT_ID=your-account-id

步骤 2:创建服务商实例

ProductReady 默认使用 Creem。服务商配置在 src/lib/billing/provider.ts

// src/lib/billing/provider.ts(默认配置)
import { createBillingProvider } from 'billing';

export function getBillingProvider() {
  const apiKey = process.env.CREEM_API_KEY;
  if (!apiKey) {
    throw new Error("CREEM_API_KEY environment variable is required");
  }

  return createBillingProvider({
    provider: 'creem',
    apiKey,
    webhookSecret: process.env.CREEM_WEBHOOK_SECRET,
    options: {
      baseUrl: apiKey.startsWith('creem_test_') 
        ? 'https://test-api.creem.io' 
        : 'https://api.creem.io',
    },
  });
}

切换服务商,修改配置:

// 示例:切换到 Stripe
export const billingProvider = createBillingProvider({
  provider: 'stripe',
  apiKey: process.env.STRIPE_SECRET_KEY!,
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
});

步骤 3:创建结账

import { billingProvider } from '~/lib/billing';

// 创建结账会话
const checkout = await billingProvider.createCheckout({
  userId: user.id,
  productId: 'prod_pro_plan',
  priceId: 'price_monthly_29',
  successUrl: 'https://productready.com/success',
  cancelUrl: 'https://productready.com/pricing',
  customerEmail: user.email,
});

// 将用户重定向到结账 URL
return { checkoutUrl: checkout.url };

核心概念

产品和价格

产品代表您销售的内容(例如"专业版套餐") 价格定义费用(例如"每月 $29")

// 获取或创建产品(如果不存在则创建)
const product = await billingProvider.getOrCreateProduct({
  name: '专业版套餐',
  description: '团队的专业功能',
});

// 获取或创建价格(如果不存在则创建)
const price = await billingProvider.getOrCreatePrice({
  productId: product.providerProductId!,
  amountCents: 2900, // $29.00
  currency: 'usd',
  interval: 'monthly', // 或 'yearly'
});

创建订阅

tRPC 集成

// src/server/routers/billing.ts
import { createTRPCRouter, protectedProcedure } from '../trpc';
import { billingProvider } from '~/lib/billing';
import { z } from 'zod';

export const billingRouter = createTRPCRouter({
  createCheckout: protectedProcedure
    .input(z.object({
      planId: z.enum(['starter', 'pro', 'enterprise']),
      interval: z.enum(['monthly', 'yearly']),
    }))
    .mutation(async ({ ctx, input }) => {
      // 将套餐映射到价格
      const pricing = {
        starter: { monthly: 900, yearly: 9000 },
        pro: { monthly: 2900, yearly: 29000 },
        enterprise: { monthly: 9900, yearly: 99000 },
      };

      const amountCents = pricing[input.planId][input.interval];

      // 获取或创建产品
      const product = await billingProvider.getOrCreateProduct({
        name: `${input.planId} 套餐`,
        description: `ProductReady ${input.planId} 订阅`,
      });

      // 获取或创建价格
      const price = await billingProvider.getOrCreatePrice({
        productId: product.providerProductId!,
        amountCents,
        currency: 'usd',
        interval: input.interval === 'monthly' ? 'monthly' : 'yearly',
      });

      // 创建结账会话
      const checkout = await billingProvider.createCheckout({
        userId: ctx.user.id,
        productId: product.id,
        priceId: price.id,
        successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=1`,
        cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
        customerEmail: ctx.user.email,
      });

      return { checkoutUrl: checkout.url };
    }),
});

Webhook 处理

Webhooks 通知您的应用支付事件(订阅创建、支付失败等)

设置 Webhook 端点

// src/app/api/webhooks/billing/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { billingProvider } from '~/lib/billing';
import { db } from '~/db';
import { users } from '~/db/schema';
import { eq } from 'drizzle-orm';

export async function POST(request: NextRequest) {
  try {
    const body = await request.text();
    const signature = request.headers.get('stripe-signature') || 
                     request.headers.get('x-signature') || '';

    // 验证 webhook 签名
    const event = await billingProvider.verifyWebhook(body, signature);

    // 处理不同的事件类型
    switch (event.type) {
      case 'subscription.created':
      case 'subscription.updated': {
        // 更新用户订阅状态
        await db
          .update(users)
          .set({
            subscriptionId: event.data.id,
            subscriptionStatus: event.data.status,
            planId: event.data.planId,
          })
          .where(eq(users.id, event.data.userId));
        break;
      }

      case 'subscription.cancelled': {
        // 标记订阅为已取消
        await db
          .update(users)
          .set({
            subscriptionStatus: 'cancelled',
            subscriptionEndsAt: event.data.endsAt,
          })
          .where(eq(users.id, event.data.userId));
        break;
      }
    }

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook 错误:', error);
    return NextResponse.json({ error: 'Webhook 处理失败' }, { status: 400 });
  }
}

订阅管理

获取用户订阅

export const billingRouter = createTRPCRouter({
  getSubscription: protectedProcedure
    .query(async ({ ctx }) => {
      const user = await db.query.users.findFirst({
        where: eq(users.id, ctx.user.id),
      });

      if (!user?.subscriptionId) {
        return null;
      }

      const subscription = await billingProvider.getSubscription(
        user.subscriptionId
      );

      return subscription;
    }),
});

取消订阅

export const billingRouter = createTRPCRouter({
  cancelSubscription: protectedProcedure
    .mutation(async ({ ctx }) => {
      const user = await db.query.users.findFirst({
        where: eq(users.id, ctx.user.id),
      });

      if (!user?.subscriptionId) {
        throw new Error('没有活跃订阅');
      }

      // 取消订阅(保留到计费周期结束)
      const subscription = await billingProvider.cancelSubscription({
        subscriptionId: user.subscriptionId,
        immediately: false, // false = 在周期结束时取消
      });

      await db
        .update(users)
        .set({
          subscriptionStatus: 'cancelled',
          subscriptionEndsAt: subscription.currentPeriodEnd,
        })
        .where(eq(users.id, ctx.user.id));

      return { success: true };
    }),
});

测试

测试模式

所有服务商都支持测试模式:

# Stripe 测试密钥以 sk_test_ 开头
STRIPE_SECRET_KEY=sk_test_...

测试卡号

Stripe

  • 成功:4242 4242 4242 4242
  • 拒绝:4000 0000 0000 0002

服务商间迁移

无需重写代码即可切换服务商:

// src/lib/billing.ts

// 之前:Stripe
const billingProvider = createBillingProvider({
  provider: 'stripe',
  apiKey: process.env.STRIPE_SECRET_KEY!,
});

// 之后:LemonSqueezy
const billingProvider = createBillingProvider({
  provider: 'lemonsqueezy',
  apiKey: process.env.LEMONSQUEEZY_API_KEY!,
  storeId: process.env.LEMONSQUEEZY_STORE_ID!,
});

所有 tRPC 端点和 webhook 处理程序保持不变!


下一步

  • 使用 Creem(默认) - 已配置好,只需添加 API 密钥
  • 设置 webhooks - 配置 webhook 端点 /api/billing/webhook
  • 创建产品 - 产品在首次结账时延迟创建
  • 构建 UI - 创建定价页面和结账流程
  • 充分测试 - 测试所有支付场景

ProductReady 默认使用 Creem - 自动处理全球税务和合规。如需更多控制,切换到 Stripe 用于美国/欧盟市场。LemonSqueezy 是另一个 MoR 替代方案。

On this page