Next-js-Boilerplate支付系统集成:Stripe与PayPal API对接实践
引言:解决现代Web应用的支付集成痛点
你是否正在为Next.js应用寻找一套完整的支付解决方案?是否在Stripe与PayPal的API对接中遇到过类型定义混乱、支付流程断裂、国际化适配困难等问题?本文将基于Next-js-Boilerplate,通过12个实战步骤,构建一套同时支持Stripe与PayPal的企业级支付系统,解决从支付意向创建到异步通知处理的全流程技术挑战。
读完本文你将获得:
- 两套支付系统的无缝集成方案(Stripe Elements + PayPal Smart Buttons)
- 符合PCI DSS的安全支付架构设计
- 多语言环境下的支付流程国际化实现
- 完整的支付状态管理与异常处理机制
- 可复用的支付组件与API封装
技术栈与环境准备
核心依赖清单
| 依赖名称 | 版本要求 | 用途 |
|---|---|---|
| stripe | ^16.0.0 | Stripe支付API客户端 |
| @paypal/checkout-server-sdk | ^1.0.0 | PayPal服务端SDK |
| @stripe/stripe-js | ^2.1.0 | Stripe前端组件库 |
| @stripe/react-stripe-js | ^2.4.0 | React支付表单组件 |
| 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 =
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



