文档
开发指南支付系统

支付系统

🚀 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>
  )
}

📎 延伸阅读

官方文档:

最佳实践:

工具推荐:


下一步: 支付系统配置完成后,查看 [组织管理]./organizations) 了解如何实现团队订阅功能。