返回首页
🚀 独立开发 / OPC
Stripe Billing 集成:从 0 到订阅 SaaS 的完整流程
本文用 Next.js + Stripe Billing 演示从产品定价、订阅创建、webhook 处理到客户自服务门户的完整 SaaS 集成流程。
Stripe · Billing · 订阅 · SaaS · 独立开发
📰
今日技术简讯
📰 技术简讯 · 2026-06-11
今日聚合 6 条热门技术内容。
🤖 AI / LLM
1. Anthropic 推出 Skills Marketplace
- 链接:https://www.anthropic.com/skills-marketplace
- 来源:Anthropic 官方
- 摘要:社区开发者可以发布自定义 Skills,企业按使用付费,类似"AI 插件商店"。
2. Google 发布 Gemma 4 开源模型
- 链接:https://ai.google.dev/gemma
- 来源:Google AI
- 摘要:Gemma 4 27B 模型,开源 Apache 2.0,性能接近 Claude Sonnet 3.5。
⚙️ 后端 / 架构
3. ClickHouse 推出 ClickPipes
- 链接:https://clickhouse.com/clickpipes
- 来源:ClickHouse
- 摘要:托管数据摄入服务,自动从 Kafka / S3 / Postgres 同步数据。
🚀 独立开发 / OPC
4. Stripe Billing 集成教程
- 链接:https://stripe.com/docs/billing
- 来源:Stripe 官方
- 摘要:从订阅创建到 webhook 处理的完整文档,开发者收藏率最高的教程。
5. ConvertKit 推出 Newsletter AI 助手
- 链接:https://convertkit.com/ai
- 来源:ConvertKit
- 摘要:AI 自动生成 newsletter 草稿,分析读者喜好,独立创作者的福音。
6. 国内独立开发者社区"独立铺"成立
- 链接:https://www.indiepu.com
- 来源:社区
- 摘要:100+ 独立开发者入驻,专注产品互推和经验分享。
数据来源:HN / Reddit / 各厂博客 采集时间:2026-06-11 09:00 (UTC+8)
📝
今日深度文
Stripe Billing 集成:从 0 到订阅 SaaS 的完整流程
一句话结论:Stripe 是事实标准的支付方案。集成需要 2-3 天,但踩坑点都在 webhook。
背景
Stripe 是全球最流行的支付平台之一,在独立开发者中尤其受欢迎:
- 全球覆盖:47 个国家,135 种货币
- 集成简单:API 设计优雅
- 文档完善:开发者体验最佳
- 合规无忧:PCI DSS Level 1
但 Stripe Billing(订阅)的集成比一次性支付复杂得多。本文给出完整流程。
核心概念
Customer 用户
Subscription 用户订阅某个 Plan
Plan 价格方案(月付 / 年付 / 按量)
Invoice 自动生成的账单
Webhook Stripe 主动通知你的服务(订阅创建、续费、失败等)
完整集成步骤
Step 1:安装依赖
npm install stripe @stripe/stripe-js
npm install -D @types/stripe
Step 2:配置环境
# .env.local
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Step 3:服务端 Stripe 客户端
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-08-27.basil',
typescript: true,
});
// 定价配置(推荐放在配置中心而非 Stripe Dashboard)
export const PRICING = {
starter: {
name: 'Starter',
monthly: { amount: 900, currency: 'usd' }, // $9.00/月
yearly: { amount: 8640, currency: 'usd' }, // $86.40/年(省 20%)
features: ['Up to 1,000 API calls/day', 'Community support'],
stripePriceId: {
monthly: 'price_starter_monthly_xxx',
yearly: 'price_starter_yearly_xxx',
},
},
pro: {
name: 'Pro',
monthly: { amount: 2900, currency: 'usd' }, // $29.00/月
yearly: { amount: 27840, currency: 'usd' }, // $278.40/年
features: ['Unlimited API calls', 'Priority support', 'Custom domains'],
stripePriceId: {
monthly: 'price_pro_monthly_xxx',
yearly: 'price_pro_yearly_xxx',
},
},
} as const;
Step 4:定价页面
// app/pricing/page.tsx (Server Component)
import { PRICING } from '@/lib/stripe';
import { PricingCard } from '@/components/PricingCard';
export default function PricingPage() {
return (
<div className="container py-12">
<h1 className="text-4xl font-bold text-center mb-12">定价</h1>
<div className="grid md:grid-cols-3 gap-8">
{Object.entries(PRICING).map(([key, plan]) => (
<PricingCard key={key} planKey={key} plan={plan} />
))}
</div>
</div>
);
}
// components/PricingCard.tsx (Client Component)
'use client';
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { PRICING } from '@/lib/stripe';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
export function PricingCard({ planKey, plan }: any) {
const [interval, setInterval] = useState<'monthly' | 'yearly'>('monthly');
const [loading, setLoading] = useState(false);
const handleSubscribe = async () => {
setLoading(true);
try {
// 1. 创建 checkout session
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ planKey, interval }),
});
const { sessionId } = await res.json();
// 2. 跳转到 Stripe Checkout
const stripe = await stripePromise;
await stripe!.redirectToCheckout({ sessionId });
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="border rounded-lg p-6">
<h3 className="text-2xl font-bold">{plan.name}</h3>
<p className="text-4xl font-bold my-4">
${(plan[interval].amount / 100).toFixed(0)}
<span className="text-sm text-gray-500">/{interval === 'monthly' ? '月' : '年'}</span>
</p>
<ul className="my-6 space-y-2">
{plan.features.map((f: string) => (
<li key={f}>✓ {f}</li>
))}
</ul>
<button
onClick={handleSubscribe}
disabled={loading}
className="w-full bg-blue-600 text-white py-2 rounded"
>
{loading ? '加载中...' : '订阅'}
</button>
</div>
);
}
Step 5:创建 Checkout Session
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe, PRICING } from '@/lib/stripe';
import { auth } from '@/lib/auth'; // 你的认证方案
export async function POST(req: NextRequest) {
// 1. 验证用户登录
const user = await auth(req);
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { planKey, interval } = await req.json();
const plan = PRICING[planKey];
if (!plan) {
return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
}
// 2. 创建或获取 Customer
let customer;
if (user.stripeCustomerId) {
customer = await stripe.customers.retrieve(user.stripeCustomerId);
} else {
customer = await stripe.customers.create({
email: user.email,
name: user.name,
metadata: { userId: user.id },
});
// 保存到数据库
await db.users.update(user.id, { stripeCustomerId: customer.id });
}
// 3. 创建 Checkout Session
const session = await stripe.checkout.sessions.create({
customer: customer.id,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{
price: plan.stripePriceId[interval],
quantity: 1,
}],
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
metadata: {
userId: user.id,
planKey,
interval,
},
});
return NextResponse.json({ sessionId: session.id });
}
Step 6:Webhook 处理(最关键)
// app/api/webhook/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';
import { db } from '@/lib/db';
export const config = {
api: { bodyParser: false }, // Stripe 需要原始 body
};
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = headers().get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// 处理各种事件
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
const userId = session.metadata!.userId;
// 用户完成首次订阅
await db.subscriptions.create({
userId,
stripeSubscriptionId: session.subscription as string,
status: 'active',
});
await db.users.update(userId, { plan: session.metadata!.planKey });
break;
}
case 'invoice.paid': {
const invoice = event.data.object;
// 订阅续费成功
await db.subscriptions.updateByStripeId(invoice.subscription as string, {
status: 'active',
currentPeriodEnd: new Date(invoice.lines.data[0].period.end * 1000),
});
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object;
// 续费失败,通知用户
const subscription = await db.subscriptions.findByStripeId(
invoice.subscription as string,
);
await sendEmail(subscription.userId, {
subject: '支付失败,请更新支付方式',
template: 'payment-failed',
});
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
// 订阅取消
await db.subscriptions.updateByStripeId(subscription.id, {
status: 'canceled',
});
const sub = await db.subscriptions.findByStripeId(subscription.id);
await db.users.update(sub.userId, { plan: 'free' });
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object;
// 升降级
await db.subscriptions.updateByStripeId(subscription.id, {
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
});
break;
}
}
return NextResponse.json({ received: true });
}
Step 7:本地测试 webhook
# 安装 Stripe CLI
brew install stripe/stripe-cli/stripe
# 登录
stripe login
# 转发 webhook 到本地
stripe listen --forward-to localhost:3000/api/webhook/stripe
# 输出类似:
# > Ready! Your webhook signing secret is whsec_xxx (use this in .env)
# 触发测试事件
stripe trigger checkout.session.completed
Step 8:客户自服务门户
// app/api/portal/route.ts
export async function POST(req: NextRequest) {
const user = await auth(req);
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/account`,
});
return NextResponse.json({ url: session.url });
}
// components/ManageSubscription.tsx
'use client';
export function ManageSubscription() {
return (
<button
onClick={async () => {
const res = await fetch('/api/portal', { method: 'POST' });
const { url } = await res.json();
window.location.href = url;
}}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
管理订阅
</button>
);
}
5 个常见坑
坑 1:Webhook 签名验证失败
// ❌ 错误:用默认的 JSON parser
const body = await req.json();
event = stripe.webhooks.constructEvent(body, sig, secret); // 失败
// ✅ 正确:用原始 text
const body = await req.text();
event = stripe.webhooks.constructEvent(body, sig, secret);
坑 2:金额单位搞错
// ❌ 错误:金额写为 $9
amount: 9
// ✅ 正确:Stripe 用 cents
amount: 900 // $9.00
坑 3:未处理重复 webhook
Stripe 会最多重试 3 天,如果你的 webhook 返回 5xx,事件会被重发。
// ✅ 用 event.id 去重
const existingEvent = await db.webhookEvents.findById(event.id);
if (existingEvent) {
return NextResponse.json({ received: true }); // 已处理,跳过
}
await db.webhookEvents.create({ id: event.id, type: event.type });
// ... 处理事件
坑 4:测试模式 vs 生产模式
# 测试模式的卡号
4242 4242 4242 4242 // 成功
4000 0000 0000 9995 // 余额不足
4000 0025 0000 3155 // 需要 3DS 验证
# 一定要在测试模式充分测试
坑 5:忘记处理 tax
2026 年起,很多国家要求 SaaS 收税:
// Stripe Tax(自动计算)
automatic_tax: { enabled: true },
// 或者使用 TaxJar / Avalara 集成
订阅指标计算
// lib/metrics.ts
// MRR (Monthly Recurring Revenue)
async function calculateMRR() {
const subs = await db.subscriptions.find({ status: 'active' });
return subs.reduce((total, sub) => {
if (sub.interval === 'monthly') {
return total + sub.amount;
} else { // yearly
return total + sub.amount / 12;
}
}, 0);
}
// Churn Rate(月度流失率)
async function calculateChurn(month: string) {
const startCount = await db.subscriptions.count({
status: 'active',
createdAt: { $lt: new Date(month + '-01') },
});
const churned = await db.subscriptions.count({
status: 'canceled',
canceledAt: {
$gte: new Date(month + '-01'),
$lt: new Date(month + '-31'),
},
});
return churned / startCount;
}
// LTV (Lifetime Value)
async function calculateLTV() {
const mrr = await calculateMRR();
const churn = await calculateChurn(currentMonth());
return mrr / (churn || 0.05); // 假设保底流失 5%
}
我的看法
Stripe Billing 是事实标准:
- 集成简单:相比 Paddle / Lemon Squeezy,文档最完整
- 覆盖广:全球 47 国,无需自建支付
- 合规无忧:PCI DSS Level 1,税务自动化
- 成本可控:2.9% + $0.30/笔,没有月费
但不要假设 Stripe 解决一切:
- 国内支付:需要对接微信 / 支付宝(Ping++ / 虎皮椒支付)
- B2B 大客户:可能需要 PO / 发票定制
- 超高 ARPU:可以考虑直接对接银行
未来值得关注:
- Stripe Crypto:加密货币支付
- Stripe Issuing:发自己的信用卡
- Stripe Treasury:银行账户服务
参考
本文代码基于 Next.js 16 + Stripe 2026-08 API 版本,集成示例可直接复制使用。
📚 同主题文章
🚀独立开发 / OPC·
独立开发者方法论:如何用 AI 工具链 10x 生产力 2026
2026 年独立开发者 10x 生产力的完整方法论:AI 工具链 + 工作流 + 心法 + 习惯。本文 6 大场景实战 + 30 天落地 + 5 个核心心法。
独立开发AI 工具方法论
🚀独立开发 / OPC·
AI SaaS 实战:从 0 到月入 $10K 的产品方法论 2026
AI SaaS 是 2026 年独立开发者的最佳赛道。本文拆解 4 个真实案例(AI 客服 / 内容运营 / 代码助手 / 数据洞察),覆盖选品 → MVP → 定价 → 增长 → 变现全流程,月入 $10K 完整路径。
AI SaaS独立开发OPC
🚀独立开发 / OPC·
Cursor 1.5 + Claude Code 实战:2026 独立开发者编程工作流
2026 年独立开发者 AI 编程工作流:Cursor 1.5(编辑器内)+ Claude Code(深度任务)+ Skills(工作流模板)。本文从需求到部署完整流程,含 5 个真实项目 + Skills 实战。
CursorClaude CodeAI 编程