Next-js-Boilerplate支付系统集成:Stripe与PayPal API对接实践

Next-js-Boilerplate支付系统集成:Stripe与PayPal API对接实践

【免费下载链接】Next-js-Boilerplate 🚀🎉📚 Boilerplate and Starter for Next.js 14+ with App Router and Page Router support, Tailwind CSS 3.3 and TypeScript ⚡️ Made with developer experience first: Next.js + TypeScript + ESLint + Prettier + Husky + Lint-Staged + Jest + Testing Library + Cypress + Storybook + Commitlint + VSCode + Netlify + PostCSS + Tailwind CSS 【免费下载链接】Next-js-Boilerplate 项目地址: https://gitcode.com/GitHub_Trending/ne/Next-js-Boilerplate

引言:解决现代Web应用的支付集成痛点

你是否正在为Next.js应用寻找一套完整的支付解决方案?是否在Stripe与PayPal的API对接中遇到过类型定义混乱、支付流程断裂、国际化适配困难等问题?本文将基于Next-js-Boilerplate,通过12个实战步骤,构建一套同时支持Stripe与PayPal的企业级支付系统,解决从支付意向创建到异步通知处理的全流程技术挑战。

读完本文你将获得:

  • 两套支付系统的无缝集成方案(Stripe Elements + PayPal Smart Buttons)
  • 符合PCI DSS的安全支付架构设计
  • 多语言环境下的支付流程国际化实现
  • 完整的支付状态管理与异常处理机制
  • 可复用的支付组件与API封装

技术栈与环境准备

核心依赖清单

依赖名称版本要求用途
stripe^16.0.0Stripe支付API客户端
@paypal/checkout-server-sdk^1.0.0PayPal服务端SDK
@stripe/stripe-js^2.1.0Stripe前端组件库
@stripe/react-stripe-js^2.4.0React支付表单组件
zod^4.0.17支付参数验证(项目已集成)
drizzle-orm^0.44.4支付记录数据库操作(项目已集成)

环境变量配置

在项目根目录创建.env.local文件,添加以下支付相关配置:

# Stripe配置
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_你的公钥
STRIPE_SECRET_KEY=sk_test_你的密钥
STRIPE_WEBHOOK_SECRET=whsec_你的Webhook密钥

# PayPal配置
PAYPAL_CLIENT_ID=AZDx...你的客户端ID
PAYPAL_CLIENT_SECRET=EHsb...你的客户端密钥
PAYPAL_WEBHOOK_ID=5J92...你的Webhook ID

# 支付系统通用配置
NEXT_PUBLIC_PAYMENT_CURRENCY=USD
PAYMENT_SUCCESS_REDIRECT_URL=/dashboard/payments/success
PAYMENT_CANCEL_REDIRECT_URL=/dashboard/payments/cancel

数据库模型设计:支付记录与订单管理

支付相关表结构设计

使用Drizzle ORM扩展现有数据库模型,创建src/models/PaymentSchema.ts

import { integer, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core';
import { users } from './UserSchema'; // 假设已存在用户表

export const paymentMethods = pgTable('payment_methods', {
  id: serial('id').primaryKey(),
  userId: varchar('user_id', { length: 255 }).notNull(), // 关联Clerk用户ID
  type: varchar('type', { length: 20 }).notNull(), // 'stripe'或'paypal'
  providerCustomerId: varchar('provider_customer_id', { length: 255 }), // 支付平台客户ID
  providerPaymentMethodId: varchar('provider_payment_method_id', { length: 255 }), // 支付方式ID
  isDefault: integer('is_default').default(0), // 1表示默认支付方式
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().$onUpdate(() => new Date()),
});

export const orders = pgTable('orders', {
  id: serial('id').primaryKey(),
  orderNumber: varchar('order_number', { length: 50 }).unique().notNull(), // 业务订单号
  userId: varchar('user_id', { length: 255 }).notNull(),
  amount: integer('amount').notNull(), // 金额(分)
  currency: varchar('currency', { length: 3 }).default('USD').notNull(),
  status: varchar('status', { length: 20 }).notNull(), // 'pending'|'paid'|'failed'|'refunded'
  items: text('items').notNull(), // 订单项JSON字符串
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().$onUpdate(() => new Date()),
});

export const payments = pgTable('payments', {
  id: serial('id').primaryKey(),
  orderId: integer('order_id').references(() => orders.id).notNull(),
  paymentMethodId: integer('payment_method_id').references(() => paymentMethods.id),
  provider: varchar('provider', { length: 20 }).notNull(), // 'stripe'或'paypal'
  providerPaymentId: varchar('provider_payment_id', { length: 255 }).notNull(), // 支付平台交易ID
  amount: integer('amount').notNull(),
  currency: varchar('currency', { length: 3 }).default('USD').notNull(),
  status: varchar('status', { length: 20 }).notNull(),
  webhookEvent: text('webhook_event'), // 存储原始Webhook事件
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().$onUpdate(() => new Date()),
});

生成数据库迁移

执行以下命令生成并应用迁移:

npm run db:generate # 生成迁移文件
npm run db:migrate  # 应用迁移到数据库

支付服务封装:统一接口设计

创建支付服务抽象层

创建src/libs/payments/PaymentService.ts定义统一接口:

import { z } from 'zod';

export const CreatePaymentIntentSchema = z.object({
  orderId: z.number(),
  amount: z.number().int().positive(),
  currency: z.string().length(3),
  metadata: z.record(z.string()).optional(),
});

export type CreatePaymentIntentParams = z.infer<typeof CreatePaymentIntentSchema>;

export interface PaymentIntentResult {
  clientSecret?: string; // Stripe使用
  approvalUrl?: string;  // PayPal使用
  paymentId?: string;    // PayPal使用
}

export abstract class PaymentService {
  abstract createPaymentIntent(params: CreatePaymentIntentParams): Promise<PaymentIntentResult>;
  abstract handleWebhook(event: Buffer, signature: string): Promise<void>;
  abstract verifyPayment(paymentId: string): Promise<boolean>;
}

Stripe支付服务实现

创建src/libs/payments/StripeService.ts

import Stripe from 'stripe';
import { Env } from '@/libs/Env';
import { PaymentService, CreatePaymentIntentParams, PaymentIntentResult } from './PaymentService';
import { db } from '@/libs/DB';
import { payments } from '@/models/PaymentSchema';

export class StripeService implements PaymentService {
  private stripe: Stripe;

  constructor() {
    this.stripe = new Stripe(Env.STRIPE_SECRET_KEY, {
      apiVersion: '2024-06-20',
      appInfo: {
        name: 'Next-js-Boilerplate',
        version: '0.1.0'
      }
    });
  }

  async createPaymentIntent(params: CreatePaymentIntentParams): Promise<PaymentIntentResult> {
    const intent = await this.stripe.paymentIntents.create({
      amount: params.amount,
      currency: params.currency.toLowerCase(),
      metadata: {
        orderId: params.orderId.toString(),
        ...params.metadata
      }
    });

    // 记录支付意向到数据库
    await db.insert(payments).values({
      orderId: params.orderId,
      provider: 'stripe',
      providerPaymentId: intent.id,
      amount: params.amount,
      currency: params.currency,
      status: 'pending'
    });

    return { clientSecret: intent.client_secret };
  }

  async handleWebhook(eventBuffer: Buffer, signature: string): Promise<void> {
    let event: Stripe.Event;

    try {
      event = this.stripe.webhooks.constructEvent(
        eventBuffer,
        signature,
        Env.STRIPE_WEBHOOK_SECRET
      );
    } catch (err) {
      throw new Error(`Webhook signature verification failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
    }

    // 处理支付成功事件
    if (event.type === 'payment_intent.succeeded') {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      await this.updatePaymentStatus(
        paymentIntent.id,
        'paid',
        JSON.stringify(event)
      );
    }

    // 处理支付失败事件
    if (event.type === 'payment_intent.payment_failed') {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      await this.updatePaymentStatus(
        paymentIntent.id,
        'failed',
        JSON.stringify(event)
      );
    }
  }

  private async updatePaymentStatus(paymentId: string, status: string, webhookEvent: string) {
    await db.update(payments)
      .set({ status, webhookEvent })
      .where(eq(payments.providerPaymentId, paymentId));
  }

  async verifyPayment(paymentId: string): Promise<boolean> {
    const payment = await this.stripe.paymentIntents.retrieve(paymentId);
    return payment.status === 'succeeded';
  }
}

PayPal支付服务实现

创建src/libs/payments/PayPalService.ts

import { PayPalHttpClient, environment } from '@paypal/checkout-server-sdk/lib/core';
import { CreateOrderRequest, CaptureOrderRequest } from '@paypal/checkout-server-sdk/lib/orders';
import { Env } from '@/libs/Env';
import { PaymentService, CreatePaymentIntentParams, PaymentIntentResult } from './PaymentService';
import { db } from '@/libs/DB';
import { payments } from '@/models/PaymentSchema';
import { eq } from 'drizzle-orm';

class PayPalEnvironment extends environment.CoreEnvironment {
  constructor(clientId: string, clientSecret: string) {
    super(clientId, clientSecret);
  }

  getBaseUrl(): string {
    return Env.NODE_ENV === 'production' 
      ? 'https://api-m.paypal.com' 
      : 'https://api-m.sandbox.paypal.com';
  }
}

export class PayPalService implements PaymentService {
  private client: PayPalHttpClient;

  constructor() {
    const env = new PayPalEnvironment(Env.PAYPAL_CLIENT_ID, Env.PAYPAL_CLIENT_SECRET);
    this.client = new PayPalHttpClient(env);
  }

  async createPaymentIntent(params: CreatePaymentIntentParams): Promise<PaymentIntentResult> {
    const request = new CreateOrderRequest();
    request.prefer("return=representation");
    request.requestBody({
      intent: 'CAPTURE',
      purchase_units: [{
        reference_id: `order_${params.orderId}`,
        amount: {
          currency_code: params.currency,
          value: (params.amount / 100).toFixed(2) // PayPal使用美元为单位,需转换为小数
        },
        custom_id: params.orderId.toString()
      }]
    });

    const response = await this.client.execute(request);
    const order = response.result;
    
    // 记录支付意向到数据库
    await db.insert(payments).values({
      orderId: params.orderId,
      provider: 'paypal',
      providerPaymentId: order.id,
      amount: params.amount,
      currency: params.currency,
      status: 'pending'
    });

    // 提取批准URL
    const approvalUrl = order.links?.find(link => link.rel === 'approve')?.href;
    
    return {
      approvalUrl,
      paymentId: order.id
    };
  }

  async handleWebhook(eventBuffer: Buffer): Promise<void> {
    const event = JSON.parse(eventBuffer.toString());
    
    // 处理支付完成事件
    if (event.event_type === 'PAYMENT.CAPTURE.COMPLETED') {
      const paymentId = event.resource.id;
      await this.updatePaymentStatus(
        paymentId,
        'paid',
        JSON.stringify(event)
      );
    }

    // 处理支付失败事件
    if (event.event_type === 'PAYMENT.CAPTURE.DENIED') {
      const paymentId = event.resource.id;
      await this.updatePaymentStatus(
        paymentId,
        'failed',
        JSON.stringify(event)
      );
    }
  }

  private async updatePaymentStatus(paymentId: string, status: string, webhookEvent: string) {
    await db.update(payments)
      .set({ status, webhookEvent })
      .where(eq(payments.providerPaymentId, paymentId));
  }

  async verifyPayment(paymentId: string): Promise<boolean> {
    const request = new CaptureOrderRequest(paymentId);
    try {
      const response = await this.client.execute(request);
      return response.result.status === 'COMPLETED';
    } catch (error) {
      return false;
    }
  }
}

API路由实现:支付意向与Webhook处理

创建支付服务工厂

创建src/libs/payments/index.ts统一导出支付服务:

import { PaymentService } from './PaymentService';
import { StripeService } from './StripeService';
import { PayPalService } from './PayPalService';

export type PaymentProvider = 'stripe' | 'paypal';

export const createPaymentService = (provider: PaymentProvider): PaymentService => {
  switch (provider) {
    case 'stripe':
      return new StripeService();
    case 'paypal':
      return new PayPalService();
    default:
      throw new Error(`Unsupported payment provider: ${provider}`);
  }
};

支付意向创建API

创建src/app/[locale]/api/payments/create-intent/route.ts

import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { getAuth } from '@clerk/nextjs/server';
import { createPaymentService } from '@/libs/payments';
import { CreatePaymentIntentSchema } from '@/libs/payments/PaymentService';

// 扩展验证 schema,增加支付方式字段
const PaymentIntentRequestSchema = CreatePaymentIntentSchema.extend({
  provider: z.enum(['stripe', 'paypal']),
});

export async function POST(request: NextRequest) {
  try {
    // 1. 验证用户认证
    const { userId } = getAuth(request);
    if (!userId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    // 2. 验证请求数据
    const body = await request.json();
    const validatedData = PaymentIntentRequestSchema.safeParse(body);
    
    if (!validatedData.success) {
      return NextResponse.json(
        { error: 'Invalid request data', details: validatedData.error.format() },
        { status: 400 }
      );
    }

    // 3. 创建支付意向
    const service = createPaymentService(validatedData.data.provider);
    const result = await service.createPaymentIntent(validatedData.data);

    return NextResponse.json(result);
  } catch (error) {
    console.error('Payment intent creation failed:', error);
    return NextResponse.json(
      { error: 'Failed to create payment intent' },
      { status: 500 }
    );
  }
}

Stripe Webhook处理路由

创建src/app/[locale]/api/payments/stripe/webhook/route.ts

import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { createPaymentService } from '@/libs/payments';

export async function POST(request: NextRequest) {
  try {
    const signature = headers().get('Stripe-Signature') || '';
    const eventBuffer = await request.arrayBuffer();
    
    const stripeService = createPaymentService('stripe');
    await stripeService.handleWebhook(Buffer.from(eventBuffer), signature);

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Stripe webhook error:', error);
    return NextResponse.json(
      { error: error instanceof Error ? error.message : 'Unknown error' },
      { status: 400 }
    );
  }
}

// 配置Next.js不解析请求体,以便我们能直接访问原始Body
export const config = {
  api: {
    bodyParser: false,
  },
};

PayPal Webhook处理路由

创建src/app/[locale]/api/payments/paypal/webhook/route.ts

import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { createPaymentService } from '@/libs/payments';

export async function POST(request: NextRequest) {
  try {
    // 验证PayPal签名(简化版,生产环境需实现完整验证)
    const paypalSignature = headers().get('PayPal-Signature') || '';
    if (!paypalSignature) {
      return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
    }

    // 处理Webhook事件
    const eventBuffer = await request.arrayBuffer();
    const paypalService = createPaymentService('paypal');
    await paypalService.handleWebhook(Buffer.from(eventBuffer));

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('PayPal webhook error:', error);
    return NextResponse.json(
      { error: error instanceof Error ? error.message : 'Unknown error' },
      { status: 400 }
    );
  }
}

export const config = {
  api: {
    bodyParser: false,
  },
};

前端组件实现:支付表单与流程控制

Stripe支付表单组件

创建src/components/Payments/StripePaymentForm.tsx

'use client';

import { useState } from 'react';
import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js';
import { useRouter } from 'next/navigation';
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { Env } from '@/libs/Env';

const PaymentFormSchema = z.object({
  amount: z.coerce.number().int().positive().min(100), // 最小1美元(以分为单位)
  description: z.string().min(3).max(100),
});

type PaymentFormValues =

【免费下载链接】Next-js-Boilerplate 🚀🎉📚 Boilerplate and Starter for Next.js 14+ with App Router and Page Router support, Tailwind CSS 3.3 and TypeScript ⚡️ Made with developer experience first: Next.js + TypeScript + ESLint + Prettier + Husky + Lint-Staged + Jest + Testing Library + Cypress + Storybook + Commitlint + VSCode + Netlify + PostCSS + Tailwind CSS 【免费下载链接】Next-js-Boilerplate 项目地址: https://gitcode.com/GitHub_Trending/ne/Next-js-Boilerplate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值