开发指南支付系统
支付系统
🚀 Cheatsheet (快速执行)
目标: 15分钟内集成 Stripe 支付,让用户可以为你的产品付费
最快上手方式:
# 1. 注册 Stripe 账号
# 访问 https://stripe.com 注册
# 2. 获取 API 密钥
# Dashboard > Developers > API keys
# 3. 配置环境变量
echo 'STRIPE_SECRET_KEY=sk_test_你的密钥' >> .env.local
echo 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_你的公钥' >> .env.local
echo 'STRIPE_WEBHOOK_SECRET=whsec_你的webhook密钥' >> .env.local
# 4. 安装依赖 (已预装)
# bun add stripe @stripe/stripe-js
# 5. 测试支付
# 重启应用,访问 /pricing 页面测试支付
立即生效: 用户可以选择套餐并完成支付,订阅状态自动更新
❓ 为什么要集成支付系统
对 MVP 的关键价值:
- 商业化核心: 没有支付就没有收入,这是产品生存的基础
- 用户筛选: 愿意付费的用户是真正的价值用户
- 产品验证: 付费意愿是产品市场匹配的最强信号
- 业务规模化: 自动化收费,不用手动处理每个订单
真实场景举例: 你的 AI 工具有免费版和 Pro 版,用户试用后觉得好用,点击升级 → 支付页面 → 输入信用卡 → 立即获得 Pro 功能,整个过程2分钟完成。
🤔 为什么选择 Stripe
我们的实战经验:
- 全球接受度最高: 支持 135+ 国家,40+ 支付方式
- 开发者友好: API 设计优秀,文档清晰,测试工具完善
- 功能全面: 订阅、一次性支付、发票、退款、税务全覆盖
- 安全可靠: PCI DSS Level 1 认证,处理敏感信息
对比其他方案:
PayPal: 用户体验一般,跳转流程复杂
国内支付: 微信/支付宝仅限中国用户
Square: 主要面向线下,线上功能有限
Stripe: ✅ 全球化、开发友好、功能完整
🧠 简要原理 & 最简案例
支付流程架构: 用户选择套餐 → 创建支付会话 → 跳转 Stripe 页面 → 完成支付 → Webhook 通知 → 更新用户订阅
最简支付示例:
// 1. 创建支付按钮
function PaymentButton({ planId }: { planId: string }) {
const handlePayment = async () => {
// 创建支付会话
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planId })
})
const { url } = await response.json()
window.location.href = url // 跳转到 Stripe 支付页面
}
return <button onClick={handlePayment}>升级到 Pro</button>
}
// 2. API 路由创建支付会话
// app/api/create-checkout-session/route.ts
import { stripe } from '@/lib/payments/stripe'
export async function POST(request: Request) {
const { planId } = await request.json()
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{ price: planId, quantity: 1 }],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`
})
return Response.json({ url: session.url })
}
🛠️ 实际操作步骤
1. Stripe 账号设置 (5分钟)
注册和获取密钥:
1. 访问 https://stripe.com 注册账号
2. 进入 Dashboard > Developers > API keys
3. 复制以下密钥:
- Publishable key (pk_test_...) # 前端使用
- Secret key (sk_test_...) # 后端使用
4. 暂时使用测试模式,等产品稳定后切换到生产模式
2. 创建产品和价格 (5分钟)
在 Stripe Dashboard 创建:
1. Products > Create product
2. 基础版产品:
- Name: "Pro 订阅"
- Description: "解锁所有高级功能"
- Price: $29/month (或 ¥199/month)
3. 复制 Price ID (price_xxx),后续代码会用到
3. 配置支付页面 (5分钟)
创建价格展示组件:
// components/pricing/PricingCard.tsx
import { PricingPlan } from '@/types/pricing'
interface PricingCardProps {
plan: PricingPlan
onUpgrade: (priceId: string) => void
}
export function PricingCard({ plan, onUpgrade }: PricingCardProps) {
return (
<div className="border rounded-lg p-6">
<h3 className="text-xl font-bold">{plan.name}</h3>
<p className="text-3xl font-bold">${plan.price}/月</p>
<ul className="my-4 space-y-2">
{plan.features.map(feature => (
<li key={feature} className="flex items-center">
✅ {feature}
</li>
))}
</ul>
<button
onClick={() => onUpgrade(plan.priceId)}
className="w-full bg-blue-600 text-white px-4 py-2 rounded"
disabled={plan.loading}
>
{plan.loading ? '处理中...' : '立即升级'}
</button>
</div>
)
}
4. 实现支付逻辑 (10分钟)
支付处理 Hook:
// hooks/usePayment.ts
import { useState } from 'react'
export function usePayment() {
const [loading, setLoading] = useState(false)
const createCheckoutSession = async (priceId: string) => {
setLoading(true)
try {
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
priceId,
userId: user.id,
email: user.email
})
})
const { url } = await response.json()
if (url) {
window.location.href = url
}
} catch (error) {
console.error('支付创建失败:', error)
alert('支付失败,请重试')
} finally {
setLoading(false)
}
}
return { createCheckoutSession, loading }
}
API 路由完整实现:
// app/api/create-checkout-session/route.ts
import { NextRequest } from 'next/server'
import { stripe } from '@/lib/payments/stripe'
import { auth } from '@/lib/auth'
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user) {
return Response.json({ error: '未登录' }, { status: 401 })
}
const { priceId } = await request.json()
// 创建 Stripe 支付会话
const checkoutSession = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
customer_email: session.user.email,
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?canceled=true`,
// 重要:传递用户信息到 webhook
metadata: {
userId: session.user.id,
email: session.user.email
}
})
return Response.json({ url: checkoutSession.url })
} catch (error) {
console.error('Stripe 错误:', error)
return Response.json({ error: '支付创建失败' }, { status: 500 })
}
}
5. 配置 Webhook 处理 (10分钟)
Webhook 处理订阅状态:
// app/api/webhooks/stripe/route.ts
import { NextRequest } from 'next/server'
import { stripe } from '@/lib/payments/stripe'
import { db } from '@/lib/database'
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
let event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (error) {
console.error('Webhook 签名验证失败:', error)
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
// 处理不同的 Stripe 事件
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(event.data.object)
break
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event.data.object)
break
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object)
break
}
return Response.json({ received: true })
}
async function handleCheckoutCompleted(session: any) {
const userId = session.metadata.userId
const subscriptionId = session.subscription
// 更新用户订阅状态
await db.user.update({
where: { id: userId },
data: {
stripeSubscriptionId: subscriptionId,
subscriptionStatus: 'active',
planType: 'pro'
}
})
console.log(`用户 ${userId} 订阅激活成功`)
}
💡 支付套餐配置
典型的 dashboard 定价策略
// config/pricing.ts
export const pricingPlans = [
{
id: 'free',
name: '免费版',
price: 0,
priceId: null,
features: [
'每月 10 次 AI 对话',
'基础模板',
'社区支持'
],
limitations: ['功能限制', '无优先支持']
},
{
id: 'pro',
name: 'Pro 版',
price: 29,
priceId: 'price_1234567890', // 从 Stripe 获取
popular: true,
features: [
'无限 AI 对话',
'所有高级模板',
'优先技术支持',
'高级数据分析',
'团队协作功能'
]
},
{
id: 'enterprise',
name: '企业版',
price: 99,
priceId: 'price_0987654321',
features: [
'Pro 版所有功能',
'私有部署选项',
'定制化开发',
'SLA 保障',
'专属客户经理'
]
}
] as const
动态价格管理
// lib/pricing/dynamic.ts
export async function getPricingForUser(userId: string) {
const user = await db.user.findUnique({ where: { id: userId } })
// 新用户优惠
if (user?.createdAt && isWithinDays(user.createdAt, 7)) {
return {
...pricingPlans.find(p => p.id === 'pro'),
price: 19, // 新用户特价
priceId: 'price_new_user_special'
}
}
return pricingPlans
}
🎯 实用功能实现
订阅状态检查
// hooks/useSubscription.ts
export function useSubscription() {
const { data: user } = useUser()
const isSubscribed = user?.subscriptionStatus === 'active'
const isPro = user?.planType === 'pro'
const checkFeatureAccess = (feature: string) => {
if (!isSubscribed) return false
const plan = pricingPlans.find(p => p.id === user.planType)
return plan?.features.includes(feature) ?? false
}
return { isSubscribed, isPro, checkFeatureAccess }
}
用量限制控制
// lib/usage/limits.ts
export async function checkUsageLimit(userId: string, feature: string) {
const user = await getUserWithUsage(userId)
const limits = {
free: { ai_chats: 10, exports: 5 },
pro: { ai_chats: -1, exports: -1 }, // 无限制
}
const userLimit = limits[user.planType] || limits.free
const featureLimit = userLimit[feature]
if (featureLimit === -1) return { allowed: true }
const currentUsage = user.usage[feature] || 0
return {
allowed: currentUsage < featureLimit,
remaining: featureLimit - currentUsage
}
}
支付失败处理
// components/PaymentAlert.tsx
export function PaymentAlert() {
const { user } = useUser()
if (user?.subscriptionStatus === 'past_due') {
return (
<div className="bg-red-50 border border-red-200 rounded p-4 mb-4">
<p className="text-red-800">
⚠️ 支付失败,请更新支付方式以继续使用 Pro 功能
</p>
<a
href="/settings/billing"
className="text-red-600 underline"
>
立即更新支付方式
</a>
</div>
)
}
return null
}
🆘 常见问题解决
问题1: Webhook 未收到
# 检查 Webhook 配置
1. Stripe Dashboard > Webhooks
2. 确认 URL: https://yourdomain.com/api/webhooks/stripe
3. 选择事件: checkout.session.completed, customer.subscription.*
4. 测试 Webhook: Send test webhook
# 本地开发测试
bun add stripe-cli
stripe listen --forward-to localhost:3000/api/webhooks/stripe
问题2: 支付后状态未更新
// 调试 Webhook 处理
console.log('Webhook 收到事件:', event.type)
console.log('用户 ID:', event.data.object.metadata?.userId)
// 确保 metadata 传递正确
const session = await stripe.checkout.sessions.create({
// ... 其他配置
metadata: {
userId: user.id, // 必须传递
email: user.email
}
})
问题3: 测试卡号
# Stripe 测试卡号
4242 4242 4242 4242 # 成功支付
4000 0000 0000 0002 # 卡被拒绝
4000 0000 0000 9995 # 资金不足
# 任意未来日期作为到期日期
# 任意3位数作为 CVC
📊 收入数据分析
收入统计面板
// components/RevenueStats.tsx
export function RevenueStats() {
const { data: stats } = useRevenueStats()
return (
<div className="grid grid-cols-3 gap-4">
<div className="bg-white p-4 rounded shadow">
<h3 className="text-sm text-gray-600">月收入</h3>
<p className="text-2xl font-bold">${stats?.monthlyRevenue}</p>
</div>
<div className="bg-white p-4 rounded shadow">
<h3 className="text-sm text-gray-600">活跃订阅</h3>
<p className="text-2xl font-bold">{stats?.activeSubscriptions}</p>
</div>
<div className="bg-white p-4 rounded shadow">
<h3 className="text-sm text-gray-600">转化率</h3>
<p className="text-2xl font-bold">{stats?.conversionRate}%</p>
</div>
</div>
)
}
📎 延伸阅读
官方文档:
- Stripe 文档 - 完整的 API 参考和指南
- Stripe Next.js 集成 - 官方示例项目
最佳实践:
- dashboard 定价策略 - 定价心理学和策略
- 支付安全指南 - PCI 合规和安全最佳实践
工具推荐:
- Stripe CLI - 本地开发和测试工具
- Stripe Dashboard - 支付管理后台
下一步: 支付系统配置完成后,查看 [组织管理]./organizations) 了解如何实现团队订阅功能。