iconvibetake docs
Frameworks

支付集成

使用 Stripe 实现支付功能

Stripe 支付集成指南

vibetake 集成了 Stripe 支付系统,为你的应用提供完整的支付解决方案。本文档将详细介绍如何配置和使用 Stripe 支付功能。

概述

Stripe 是全球领先的在线支付处理平台,提供安全、可靠的支付基础设施。vibetake 的 Stripe 集成支持多种支付场景和业务模式。

核心特性

  • 💳 多种支付方式 - 信用卡、借记卡、数字钱包等
  • 🔄 订阅管理 - 灵活的订阅计费和管理
  • 📊 支付分析 - 详细的交易数据和报告
  • 🔒 安全合规 - PCI DSS 合规,数据加密保护
  • 🌍 全球支持 - 支持135+种货币和45+个国家
  • 🎯 智能路由 - 优化支付成功率和成本
  • 📱 移动优化 - 响应式支付界面
  • 🔧 强大 API - 灵活的集成和自定义

系统架构

graph TB
    A[客户端] --> B[支付组件]
    B --> C[Checkout Session]
    C --> D[Stripe Dashboard]
    D --> E[Webhook]
    E --> F[后端处理]
    F --> G[数据库更新]
    
    subgraph "支付流程"
        H[用户选择商品] --> I[创建支付会话]
        I --> J[跳转 Stripe]
        J --> K[完成支付]
        K --> L[Webhook 通知]
        L --> M[订单确认]
    end
    
    subgraph "订阅流程"
        N[选择订阅计划] --> O[创建客户]
        O --> P[创建订阅]
        P --> Q[定期扣费]
        Q --> R[状态同步]
    end

安装和配置

1. 安装依赖

# 安装 Stripe 相关包
npm install stripe @stripe/stripe-js @stripe/react-stripe-js

# 安装类型定义(如果使用 TypeScript)
npm install --save-dev @types/stripe

2. 环境变量配置

.env.local 文件中配置 Stripe 密钥:

# Stripe 配置
STRIPE_SECRET_KEY=sk_test_51...  # 服务端密钥(测试环境)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51...  # 客户端密钥(测试环境)
STRIPE_WEBHOOK_SECRET=whsec_...  # Webhook 签名密钥

# 生产环境配置
# STRIPE_SECRET_KEY=sk_live_51...
# NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_51...

# 应用配置
NEXT_PUBLIC_APP_URL=http://localhost:3000  # 应用基础 URL

3. 创建 Stripe 服务

// src/services/payment/stripe-config.ts
import Stripe from 'stripe';

if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error('STRIPE_SECRET_KEY is not set in environment variables');
}

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2024-06-20',
  typescript: true,
});

// Stripe 配置常量
export const STRIPE_CONFIG = {
  currency: 'usd', // 默认货币
  successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/payment/success`,
  cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/payment/cancel`,
  webhookEndpoint: `${process.env.NEXT_PUBLIC_APP_URL}/api/payment/webhooks/stripe`,
} as const;

// 支持的支付方式
export const PAYMENT_METHODS = [
  'card',
  'alipay',
  'wechat_pay',
  'ideal',
  'sepa_debit',
] as const;

4. 客户端配置

// src/services/payment/stripe-client.ts
import { loadStripe, Stripe } from '@stripe/stripe-js';

let stripePromise: Promise<Stripe | null>;

export const getStripe = () => {
  if (!stripePromise) {
    if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
      throw new Error('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set');
    }
    
    stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
  }
  
  return stripePromise;
};

// Stripe 元素配置
export const stripeElementsOptions = {
  appearance: {
    theme: 'stripe' as const,
    variables: {
      colorPrimary: '#0570de',
      colorBackground: '#ffffff',
      colorText: '#30313d',
      colorDanger: '#df1b41',
      fontFamily: 'Inter, system-ui, sans-serif',
      spacingUnit: '4px',
      borderRadius: '8px',
    },
  },
  locale: 'zh' as const,
};

快速开始

1. 创建产品和价格

首先在 Stripe Dashboard 中创建产品和价格,或通过 API 创建:

// src/services/payment/products.ts
import { stripe } from './stripe-config';

// 创建产品
export async function createProduct(productData: {
  name: string;
  description?: string;
  images?: string[];
  metadata?: Record<string, string>;
}) {
  const product = await stripe.products.create({
    name: productData.name,
    description: productData.description,
    images: productData.images,
    metadata: productData.metadata,
  });

  return product;
}

// 创建价格
export async function createPrice(priceData: {
  productId: string;
  unitAmount: number; // 以分为单位
  currency?: string;
  recurring?: {
    interval: 'day' | 'week' | 'month' | 'year';
    intervalCount?: number;
  };
}) {
  const price = await stripe.prices.create({
    product: priceData.productId,
    unit_amount: priceData.unitAmount,
    currency: priceData.currency || 'usd',
    recurring: priceData.recurring,
  });

  return price;
}

// 获取所有产品和价格
export async function getProductsWithPrices() {
  const products = await stripe.products.list({
    active: true,
    expand: ['data.default_price'],
  });

  const prices = await stripe.prices.list({
    active: true,
    expand: ['data.product'],
  });

  return {
    products: products.data,
    prices: prices.data,
  };
}

2. 一次性支付

创建支付会话 API

// src/app/api/payment/create-checkout-session/route.ts
import { stripe, STRIPE_CONFIG } from '@/services/payment/stripe-config';
import { auth } from '@/services/userauth/auth';
import { headers } from 'next/headers';
import { NextRequest } from 'next/server';

export async function POST(req: NextRequest) {
  try {
    // 验证用户身份
    const session = await auth.api.getSession({
      headers: headers(),
    });

    if (!session) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const { 
      priceId, 
      quantity = 1, 
      metadata = {},
      successUrl,
      cancelUrl 
    } = await req.json();

    // 验证价格 ID
    if (!priceId) {
      return Response.json({ error: 'Price ID is required' }, { status: 400 });
    }

    // 创建或获取 Stripe 客户
    let customer;
    try {
      const customers = await stripe.customers.list({
        email: session.user.email,
        limit: 1,
      });

      if (customers.data.length > 0) {
        customer = customers.data[0];
      } else {
        customer = await stripe.customers.create({
          email: session.user.email,
          name: session.user.name,
          metadata: {
            userId: session.user.id,
          },
        });
      }
    } catch (error) {
      console.error('Error creating/finding customer:', error);
      return Response.json({ error: 'Customer creation failed' }, { status: 500 });
    }

    // 创建支付会话
    const checkoutSession = await stripe.checkout.sessions.create({
      customer: customer.id,
      mode: 'payment',
      payment_method_types: ['card', 'alipay', 'wechat_pay'],
      line_items: [
        {
          price: priceId,
          quantity,
        },
      ],
      success_url: successUrl || `${STRIPE_CONFIG.successUrl}?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: cancelUrl || STRIPE_CONFIG.cancelUrl,
      metadata: {
        userId: session.user.id,
        ...metadata,
      },
      // 自动税费计算
      automatic_tax: { enabled: true },
      // 允许促销码
      allow_promotion_codes: true,
      // 账单地址收集
      billing_address_collection: 'required',
      // 运输地址收集(如果需要)
      shipping_address_collection: {
        allowed_countries: ['US', 'CA', 'GB', 'AU', 'CN'],
      },
    });

    return Response.json({ 
      sessionId: checkoutSession.id,
      url: checkoutSession.url,
    });
  } catch (error) {
    console.error('Checkout session creation error:', error);
    return Response.json(
      { error: 'Failed to create checkout session' },
      { status: 500 }
    );
  }
}

支付按钮组件

// src/components/payment/checkout-button.tsx
'use client';

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2, CreditCard } from 'lucide-react';
import { toast } from 'sonner';
import { getStripe } from '@/services/payment/stripe-client';

interface CheckoutButtonProps {
  priceId: string;
  quantity?: number;
  metadata?: Record<string, string>;
  children?: React.ReactNode;
  className?: string;
  disabled?: boolean;
}

export function CheckoutButton({
  priceId,
  quantity = 1,
  metadata = {},
  children,
  className,
  disabled = false,
}: CheckoutButtonProps) {
  const [loading, setLoading] = useState(false);

  const handleCheckout = async () => {
    if (disabled || loading) return;

    setLoading(true);
    
    try {
      // 创建支付会话
      const response = await fetch('/api/payment/create-checkout-session', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          priceId,
          quantity,
          metadata,
        }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to create checkout session');
      }

      // 跳转到 Stripe Checkout
      const stripe = await getStripe();
      if (!stripe) {
        throw new Error('Stripe failed to load');
      }

      const { error } = await stripe.redirectToCheckout({
        sessionId: data.sessionId,
      });

      if (error) {
        throw new Error(error.message);
      }
    } catch (error) {
      console.error('Checkout error:', error);
      toast.error(
        error instanceof Error ? error.message : '支付失败,请重试'
      );
    } finally {
      setLoading(false);
    }
  };

  return (
    <Button
      onClick={handleCheckout}
      disabled={disabled || loading}
      className={className}
    >
      {loading ? (
        <>
          <Loader2 className="w-4 h-4 mr-2 animate-spin" />
          处理中...
        </>
      ) : (
        <>
          <CreditCard className="w-4 h-4 mr-2" />
          {children || '立即购买'}
        </>
      )}
    </Button>
  );
}

3. 嵌入式支付表单

对于更好的用户体验,可以使用嵌入式支付表单:

// src/components/payment/embedded-checkout.tsx
'use client';

import { useState, useEffect } from 'react';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Loader2 } from 'lucide-react';
import { getStripe, stripeElementsOptions } from '@/services/payment/stripe-client';

interface PaymentFormProps {
  clientSecret: string;
  onSuccess?: (paymentIntent: any) => void;
  onError?: (error: string) => void;
}

function PaymentForm({ clientSecret, onSuccess, onError }: PaymentFormProps) {
  const stripe = useStripe();
  const elements = useElements();
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState<string | null>(null);

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();

    if (!stripe || !elements) {
      return;
    }

    setLoading(true);
    setMessage(null);

    const { error, paymentIntent } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/payment/success`,
      },
      redirect: 'if_required',
    });

    if (error) {
      setMessage(error.message || '支付失败');
      onError?.(error.message || '支付失败');
    } else if (paymentIntent && paymentIntent.status === 'succeeded') {
      setMessage('支付成功!');
      onSuccess?.(paymentIntent);
    }

    setLoading(false);
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <PaymentElement />
      
      {message && (
        <div className={`p-3 rounded-lg text-sm ${
          message.includes('成功') 
            ? 'bg-green-50 text-green-700 border border-green-200'
            : 'bg-red-50 text-red-700 border border-red-200'
        }`}>
          {message}
        </div>
      )}

      <Button
        type="submit"
        disabled={!stripe || loading}
        className="w-full"
      >
        {loading ? (
          <>
            <Loader2 className="w-4 h-4 mr-2 animate-spin" />
            处理中...
          </>
        ) : (
          '确认支付'
        )}
      </Button>
    </form>
  );
}

export function EmbeddedCheckout({ 
  amount, 
  currency = 'usd',
  metadata = {},
  onSuccess,
  onError 
}: {
  amount: number;
  currency?: string;
  metadata?: Record<string, string>;
  onSuccess?: (paymentIntent: any) => void;
  onError?: (error: string) => void;
}) {
  const [clientSecret, setClientSecret] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 创建 PaymentIntent
    fetch('/api/payment/create-payment-intent', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        amount,
        currency,
        metadata,
      }),
    })
      .then((res) => res.json())
      .then((data) => {
        setClientSecret(data.clientSecret);
        setLoading(false);
      })
      .catch((error) => {
        console.error('Error creating payment intent:', error);
        onError?.('创建支付失败');
        setLoading(false);
      });
  }, [amount, currency, metadata, onError]);

  if (loading) {
    return (
      <Card className="w-full max-w-md mx-auto">
        <CardContent className="flex items-center justify-center p-6">
          <Loader2 className="w-6 h-6 animate-spin" />
          <span className="ml-2">加载支付表单...</span>
        </CardContent>
      </Card>
    );
  }

  if (!clientSecret) {
    return (
      <Card className="w-full max-w-md mx-auto">
        <CardContent className="p-6">
          <p className="text-red-600">支付表单加载失败</p>
        </CardContent>
      </Card>
    );
  }

  return (
    <Card className="w-full max-w-md mx-auto">
      <CardHeader>
        <CardTitle>支付信息</CardTitle>
      </CardHeader>
      <CardContent>
        <Elements 
          stripe={getStripe()} 
          options={{
            clientSecret,
            ...stripeElementsOptions,
          }}
        >
          <PaymentForm
            clientSecret={clientSecret}
            onSuccess={onSuccess}
            onError={onError}
          />
        </Elements>
      </CardContent>
    </Card>
  );
}

订阅管理

1. 创建订阅服务

// src/services/payment/subscription.ts
import { stripe } from './stripe-config';
import { db } from '@/services/database/client';
import { user } from '@/services/database/schema';
import { eq } from 'drizzle-orm';

export interface SubscriptionPlan {
  id: string;
  name: string;
  description: string;
  priceId: string;
  price: number;
  currency: string;
  interval: 'month' | 'year';
  features: string[];
}

// 预定义的订阅计划
export const SUBSCRIPTION_PLANS: SubscriptionPlan[] = [
  {
    id: 'basic',
    name: '基础版',
    description: '适合个人用户',
    priceId: 'price_basic_monthly',
    price: 999, // $9.99
    currency: 'usd',
    interval: 'month',
    features: ['基础功能', '5GB 存储', '邮件支持'],
  },
  {
    id: 'pro',
    name: '专业版',
    description: '适合小团队',
    priceId: 'price_pro_monthly',
    price: 1999, // $19.99
    currency: 'usd',
    interval: 'month',
    features: ['所有基础功能', '50GB 存储', '优先支持', 'API 访问'],
  },
  {
    id: 'enterprise',
    name: '企业版',
    description: '适合大型组织',
    priceId: 'price_enterprise_monthly',
    price: 4999, // $49.99
    currency: 'usd',
    interval: 'month',
    features: ['所有专业功能', '无限存储', '24/7 支持', '自定义集成'],
  },
];

// 创建订阅
export async function createSubscription(
  customerId: string,
  priceId: string,
  options: {
    trialPeriodDays?: number;
    metadata?: Record<string, string>;
    coupon?: string;
  } = {}
) {
  try {
    const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{ price: priceId }],
      payment_behavior: 'default_incomplete',
      payment_settings: { 
        save_default_payment_method: 'on_subscription',
        payment_method_types: ['card'],
      },
      expand: ['latest_invoice.payment_intent'],
      trial_period_days: options.trialPeriodDays,
      metadata: options.metadata,
      coupon: options.coupon,
      // 自动税费
      automatic_tax: { enabled: true },
    });

    return {
      success: true,
      subscription,
      clientSecret: subscription.latest_invoice?.payment_intent?.client_secret,
    };
  } catch (error) {
    console.error('Create subscription error:', error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

// 更新订阅
export async function updateSubscription(
  subscriptionId: string,
  updates: {
    priceId?: string;
    quantity?: number;
    metadata?: Record<string, string>;
  }
) {
  try {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    
    const updateData: any = {
      metadata: updates.metadata,
    };

    if (updates.priceId) {
      updateData.items = [
        {
          id: subscription.items.data[0].id,
          price: updates.priceId,
          quantity: updates.quantity || 1,
        },
      ];
      updateData.proration_behavior = 'create_prorations';
    }

    const updatedSubscription = await stripe.subscriptions.update(
      subscriptionId,
      updateData
    );

    return {
      success: true,
      subscription: updatedSubscription,
    };
  } catch (error) {
    console.error('Update subscription error:', error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

// 取消订阅
export async function cancelSubscription(
  subscriptionId: string,
  immediately: boolean = false
) {
  try {
    let subscription;
    
    if (immediately) {
      subscription = await stripe.subscriptions.cancel(subscriptionId);
    } else {
      subscription = await stripe.subscriptions.update(subscriptionId, {
        cancel_at_period_end: true,
      });
    }

    return {
      success: true,
      subscription,
    };
  } catch (error) {
    console.error('Cancel subscription error:', error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

// 获取用户订阅
export async function getUserSubscription(userId: string) {
  try {
    // 从数据库获取用户信息
    const userData = await db
      .select()
      .from(user)
      .where(eq(user.id, userId))
      .limit(1);

    if (!userData.length) {
      return { success: false, error: 'User not found' };
    }

    // 查找 Stripe 客户
    const customers = await stripe.customers.list({
      email: userData[0].email,
      limit: 1,
    });

    if (!customers.data.length) {
      return { success: true, subscription: null };
    }

    // 获取活跃订阅
    const subscriptions = await stripe.subscriptions.list({
      customer: customers.data[0].id,
      status: 'active',
      limit: 1,
    });

    return {
      success: true,
      subscription: subscriptions.data[0] || null,
      customer: customers.data[0],
    };
  } catch (error) {
    console.error('Get user subscription error:', error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

2. 订阅管理 API

// src/app/api/payment/subscription/create/route.ts
import { createSubscription } from '@/services/payment/subscription';
import { auth } from '@/services/userauth/auth';
import { headers } from 'next/headers';

export async function POST(req: Request) {
  try {
    const session = await auth.api.getSession({
      headers: headers(),
    });

    if (!session) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }

    const { priceId, trialPeriodDays, coupon } = await req.json();

    if (!priceId) {
      return Response.json({ error: 'Price ID is required' }, { status: 400 });
    }

    // 创建或获取客户
    const customers = await stripe.customers.list({
      email: session.user.email,
      limit: 1,
    });

    let customer;
    if (customers.data.length > 0) {
      customer = customers.data[0];
    } else {
      customer = await stripe.customers.create({
        email: session.user.email,
        name: session.user.name,
        metadata: { userId: session.user.id },
      });
    }

    // 创建订阅
    const result = await createSubscription(customer.id, priceId, {
      trialPeriodDays,
      coupon,
      metadata: { userId: session.user.id },
    });

    if (!result.success) {
      return Response.json({ error: result.error }, { status: 500 });
    }

    return Response.json({
      subscriptionId: result.subscription.id,
      clientSecret: result.clientSecret,
    });
  } catch (error) {
    console.error('Create subscription API error:', error);
    return Response.json(
      { error: 'Failed to create subscription' },
      { status: 500 }
    );
  }
}

3. 订阅管理组件

// src/components/payment/subscription-manager.tsx
'use client';

import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { CheckCircle, XCircle, Clock, CreditCard } from 'lucide-react';
import { SUBSCRIPTION_PLANS } from '@/services/payment/subscription';
import { toast } from 'sonner';

interface SubscriptionStatus {
  subscription: any;
  customer: any;
}

export function SubscriptionManager() {
  const [status, setStatus] = useState<SubscriptionStatus | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchSubscriptionStatus();
  }, []);

  const fetchSubscriptionStatus = async () => {
    try {
      const response = await fetch('/api/payment/subscription/status');
      const data = await response.json();
      
      if (response.ok) {
        setStatus(data);
      }
    } catch (error) {
      console.error('Failed to fetch subscription status:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleCancelSubscription = async (immediately: boolean = false) => {
    if (!status?.subscription) return;

    try {
      const response = await fetch('/api/payment/subscription/cancel', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          subscriptionId: status.subscription.id,
          immediately,
        }),
      });

      if (response.ok) {
        toast.success(immediately ? '订阅已立即取消' : '订阅将在期末取消');
        fetchSubscriptionStatus();
      } else {
        throw new Error('Failed to cancel subscription');
      }
    } catch (error) {
      toast.error('取消订阅失败');
    }
  };

  const getStatusBadge = (subscription: any) => {
    const status = subscription.status;
    const cancelAtPeriodEnd = subscription.cancel_at_period_end;

    if (status === 'active' && !cancelAtPeriodEnd) {
      return <Badge className="bg-green-100 text-green-800">活跃</Badge>;
    } else if (status === 'active' && cancelAtPeriodEnd) {
      return <Badge className="bg-yellow-100 text-yellow-800">即将取消</Badge>;
    } else if (status === 'trialing') {
      return <Badge className="bg-blue-100 text-blue-800">试用中</Badge>;
    } else if (status === 'past_due') {
      return <Badge className="bg-red-100 text-red-800">逾期</Badge>;
    } else {
      return <Badge variant="secondary">{status}</Badge>;
    }
  };

  if (loading) {
    return (
      <Card>
        <CardContent className="flex items-center justify-center p-6">
          <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
          <span className="ml-2">加载订阅信息...</span>
        </CardContent>
      </Card>
    );
  }

  if (!status?.subscription) {
    return (
      <Card>
        <CardHeader>
          <CardTitle>订阅管理</CardTitle>
        </CardHeader>
        <CardContent>
          <p className="text-muted-foreground mb-4">您当前没有活跃的订阅</p>
          <Button>
            <CreditCard className="w-4 h-4 mr-2" />
            选择订阅计划
          </Button>
        </CardContent>
      </Card>
    );
  }

  const subscription = status.subscription;
  const currentPlan = SUBSCRIPTION_PLANS.find(
    plan => plan.priceId === subscription.items.data[0].price.id
  );

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center justify-between">
          订阅管理
          {getStatusBadge(subscription)}
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-6">
        {/* 当前计划信息 */}
        <div>
          <h3 className="font-semibold mb-2">当前计划</h3>
          <div className="flex items-center justify-between">
            <div>
              <p className="font-medium">{currentPlan?.name || '未知计划'}</p>
              <p className="text-sm text-muted-foreground">
                ${(subscription.items.data[0].price.unit_amount / 100).toFixed(2)} / 
                {subscription.items.data[0].price.recurring?.interval}
              </p>
            </div>
          </div>
        </div>

        <Separator />

        {/* 计费信息 */}
        <div>
          <h3 className="font-semibold mb-2">计费信息</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between">
              <span>下次计费日期:</span>
              <span>
                {new Date(subscription.current_period_end * 1000).toLocaleDateString()}
              </span>
            </div>
            <div className="flex justify-between">
              <span>计费周期:</span>
              <span>
                {new Date(subscription.current_period_start * 1000).toLocaleDateString()} - 
                {new Date(subscription.current_period_end * 1000).toLocaleDateString()}
              </span>
            </div>
          </div>
        </div>

        <Separator />

        {/* 操作按钮 */}
        <div className="space-y-2">
          <Button variant="outline" className="w-full">
            更改计划
          </Button>
          
          {subscription.status === 'active' && !subscription.cancel_at_period_end && (
            <Button
              variant="destructive"
              className="w-full"
              onClick={() => handleCancelSubscription(false)}
            >
              取消订阅
            </Button>
          )}
          
          {subscription.cancel_at_period_end && (
            <Button
              variant="outline"
              className="w-full"
              onClick={() => {
                // 重新激活订阅的逻辑
              }}
            >
              重新激活订阅
            </Button>
          )}
        </div>
      </CardContent>
    </Card>
  );
}

Webhook 处理

1. Webhook 端点

// src/app/api/payment/webhooks/stripe/route.ts
import { stripe } from '@/services/payment/stripe-config';
import { db } from '@/services/database/client';
import { user } from '@/services/database/schema';
import { eq } from 'drizzle-orm';
import { headers } from 'next/headers';

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get('stripe-signature');

  if (!signature) {
    return Response.json(
      { error: 'Missing stripe-signature header' },
      { status: 400 }
    );
  }

  let event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return Response.json(
      { error: 'Webhook signature verification failed' },
      { status: 400 }
    );
  }

  try {
    switch (event.type) {
      case 'payment_intent.succeeded':
        await handlePaymentIntentSucceeded(event.data.object);
        break;

      case 'payment_intent.payment_failed':
        await handlePaymentIntentFailed(event.data.object);
        break;

      case 'customer.subscription.created':
        await handleSubscriptionCreated(event.data.object);
        break;

      case 'customer.subscription.updated':
        await handleSubscriptionUpdated(event.data.object);
        break;

      case 'customer.subscription.deleted':
        await handleSubscriptionDeleted(event.data.object);
        break;

      case 'invoice.payment_succeeded':
        await handleInvoicePaymentSucceeded(event.data.object);
        break;

      case 'invoice.payment_failed':
        await handleInvoicePaymentFailed(event.data.object);
        break;

      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    return Response.json({ received: true });
  } catch (error) {
    console.error('Webhook handler error:', error);
    return Response.json(
      { error: 'Webhook handler failed' },
      { status: 500 }
    );
  }
}

// 处理支付成功
async function handlePaymentIntentSucceeded(paymentIntent: any) {
  console.log('Payment succeeded:', paymentIntent.id);
  
  // 更新订单状态
  // 发送确认邮件
  // 记录支付日志
}

// 处理支付失败
async function handlePaymentIntentFailed(paymentIntent: any) {
  console.log('Payment failed:', paymentIntent.id);
  
  // 通知用户支付失败
  // 记录失败原因
}

// 处理订阅创建
async function handleSubscriptionCreated(subscription: any) {
  console.log('Subscription created:', subscription.id);
  
  const customerId = subscription.customer;
  const customer = await stripe.customers.retrieve(customerId);
  
  if (customer && !customer.deleted && customer.metadata?.userId) {
    // 更新用户订阅状态
    await updateUserSubscriptionStatus(
      customer.metadata.userId,
      subscription.status,
      subscription.id
    );
  }
}

// 处理订阅更新
async function handleSubscriptionUpdated(subscription: any) {
  console.log('Subscription updated:', subscription.id);
  
  const customerId = subscription.customer;
  const customer = await stripe.customers.retrieve(customerId);
  
  if (customer && !customer.deleted && customer.metadata?.userId) {
    await updateUserSubscriptionStatus(
      customer.metadata.userId,
      subscription.status,
      subscription.id
    );
  }
}

// 处理订阅删除
async function handleSubscriptionDeleted(subscription: any) {
  console.log('Subscription deleted:', subscription.id);
  
  const customerId = subscription.customer;
  const customer = await stripe.customers.retrieve(customerId);
  
  if (customer && !customer.deleted && customer.metadata?.userId) {
    await updateUserSubscriptionStatus(
      customer.metadata.userId,
      'canceled',
      null
    );
  }
}

// 处理发票支付成功
async function handleInvoicePaymentSucceeded(invoice: any) {
  console.log('Invoice payment succeeded:', invoice.id);
  
  // 记录支付历史
  // 更新账户余额
}

// 处理发票支付失败
async function handleInvoicePaymentFailed(invoice: any) {
  console.log('Invoice payment failed:', invoice.id);
  
  // 通知用户支付失败
  // 可能需要暂停服务
}

// 更新用户订阅状态
async function updateUserSubscriptionStatus(
  userId: string,
  status: string,
  subscriptionId: string | null
) {
  try {
    // 这里应该根据你的数据库结构来更新用户订阅状态
    // 示例:假设用户表有订阅相关字段
    /*
    await db
      .update(user)
      .set({
        subscriptionStatus: status,
        subscriptionId: subscriptionId,
        updatedAt: new Date(),
      })
      .where(eq(user.id, userId));
    */
    
    console.log(`Updated user ${userId} subscription status to ${status}`);
  } catch (error) {
    console.error('Failed to update user subscription status:', error);
  }
}

2. Webhook 安全验证

// src/services/payment/webhook-security.ts
import crypto from 'crypto';

export function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const elements = signature.split(',');
  const signatureElements: Record<string, string> = {};

  for (const element of elements) {
    const [key, value] = element.split('=');
    signatureElements[key] = value;
  }

  const timestamp = signatureElements.t;
  const signatures = [
    signatureElements.v1,
    signatureElements.v0,
  ].filter(Boolean);

  if (!timestamp || signatures.length === 0) {
    return false;
  }

  // 检查时间戳(防止重放攻击)
  const timestampNumber = parseInt(timestamp, 10);
  const currentTime = Math.floor(Date.now() / 1000);
  const tolerance = 300; // 5分钟容差

  if (Math.abs(currentTime - timestampNumber) > tolerance) {
    return false;
  }

  // 验证签名
  const payloadForSignature = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payloadForSignature)
    .digest('hex');

  return signatures.some(signature => 
    crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expectedSignature, 'hex')
    )
  );
}

安全和最佳实践

1. 环境变量安全

// src/services/payment/security.ts

// 验证必需的环境变量
export function validateStripeConfig() {
  const requiredVars = [
    'STRIPE_SECRET_KEY',
    'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
    'STRIPE_WEBHOOK_SECRET',
  ];

  const missing = requiredVars.filter(
    varName => !process.env[varName]
  );

  if (missing.length > 0) {
    throw new Error(
      `Missing required Stripe environment variables: ${missing.join(', ')}`
    );
  }

  // 验证密钥格式
  const secretKey = process.env.STRIPE_SECRET_KEY!;
  const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!;

  if (!secretKey.startsWith('sk_')) {
    throw new Error('Invalid Stripe secret key format');
  }

  if (!publishableKey.startsWith('pk_')) {
    throw new Error('Invalid Stripe publishable key format');
  }

  // 检查是否在生产环境使用测试密钥
  if (process.env.NODE_ENV === 'production') {
    if (secretKey.includes('test') || publishableKey.includes('test')) {
      console.warn('Warning: Using test keys in production environment');
    }
  }
}

// 初始化时验证配置
validateStripeConfig();

2. 错误处理和日志

// src/services/payment/error-handling.ts
export class PaymentError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 400
  ) {
    super(message);
    this.name = 'PaymentError';
  }
}

export function handleStripeError(error: any): PaymentError {
  if (error.type) {
    switch (error.type) {
      case 'StripeCardError':
        return new PaymentError(
          '您的银行卡被拒绝,请尝试其他支付方式',
          'card_declined',
          400
        );
      case 'StripeRateLimitError':
        return new PaymentError(
          '请求过于频繁,请稍后重试',
          'rate_limit',
          429
        );
      case 'StripeInvalidRequestError':
        return new PaymentError(
          '支付请求无效',
          'invalid_request',
          400
        );
      case 'StripeAPIError':
        return new PaymentError(
          '支付服务暂时不可用,请稍后重试',
          'api_error',
          500
        );
      case 'StripeConnectionError':
        return new PaymentError(
          '网络连接错误,请检查网络后重试',
          'connection_error',
          500
        );
      case 'StripeAuthenticationError':
        return new PaymentError(
          '支付认证失败',
          'authentication_error',
          401
        );
      default:
        return new PaymentError(
          '支付处理失败,请重试',
          'unknown_error',
          500
        );
    }
  }

  return new PaymentError(
    error.message || '未知错误',
    'unknown_error',
    500
  );
}

// 支付日志记录
export function logPaymentEvent(
  event: string,
  data: any,
  userId?: string
) {
  const logData = {
    timestamp: new Date().toISOString(),
    event,
    userId,
    data: JSON.stringify(data),
  };

  // 在生产环境中,应该使用专业的日志服务
  console.log('Payment Event:', logData);
  
  // 可以集成到日志服务,如 Winston, Pino 等
}

3. 测试和调试

// src/services/payment/testing.ts

// 测试用的信用卡号码
export const TEST_CARDS = {
  visa: '4242424242424242',
  visaDebit: '4000056655665556',
  mastercard: '5555555555554444',
  amex: '378282246310005',
  declined: '4000000000000002',
  insufficientFunds: '4000000000009995',
  lostCard: '4000000000009987',
  stolenCard: '4000000000009979',
};

// 测试模式检查
export function isTestMode(): boolean {
  return process.env.STRIPE_SECRET_KEY?.includes('test') || false;
}

// 创建测试产品和价格
export async function createTestProducts() {
  if (!isTestMode()) {
    throw new Error('Test products can only be created in test mode');
  }

  const products = await Promise.all([
    stripe.products.create({
      name: '基础版订阅',
      description: '测试用基础版订阅',
    }),
    stripe.products.create({
      name: '专业版订阅',
      description: '测试用专业版订阅',
    }),
  ]);

  const prices = await Promise.all([
    stripe.prices.create({
      product: products[0].id,
      unit_amount: 999,
      currency: 'usd',
      recurring: { interval: 'month' },
    }),
    stripe.prices.create({
      product: products[1].id,
      unit_amount: 1999,
      currency: 'usd',
      recurring: { interval: 'month' },
    }),
  ]);

  return { products, prices };
}

故障排除

常见问题

  1. Webhook 验证失败

    • 检查 STRIPE_WEBHOOK_SECRET 配置
    • 确认 Webhook 端点 URL 正确
    • 验证请求体未被修改
  2. 支付失败

    • 检查测试卡号是否正确
    • 确认金额和货币设置
    • 查看 Stripe Dashboard 错误日志
  3. 订阅问题

    • 验证价格 ID 是否存在
    • 检查客户是否已有活跃订阅
    • 确认支付方式已保存

调试技巧

// 启用详细日志
if (process.env.NODE_ENV === 'development') {
  stripe.setAppInfo({
    name: 'vibetake',
    version: '1.0.0',
    url: 'https://your-app.com',
  });
}

// 监控 API 调用
const originalRequest = stripe._request;
stripe._request = function(method, path, data, auth, options, callback) {
  console.log(`Stripe API: ${method} ${path}`, data);
  return originalRequest.call(this, method, path, data, auth, options, callback);
};

通过本文档,你应该能够在 vibetake 中完整地集成和使用 Stripe 支付系统。记住始终在测试环境中验证所有功能,并确保遵循 Stripe 的最佳实践和安全指南。如需更多信息,请参考 Stripe 官方文档