支付流程详解
本文档详细说明用户购买定价计划的完整流程,包括前端交互、API 调用、支付处理和结果验证。
流程概览
用户选择计划
↓
点击购买按钮
↓
创建结账会话 (POST /api/payment/checkout-session)
↓
跳转到支付提供商页面
↓
用户完成支付
↓
重定向回应用 (GET /payment/success)
↓
验证支付状态 (GET /api/payment/verify-success)
↓
Webhook 处理 (POST /api/stripe/webhook 或 /api/creem/webhook)
↓
订单创建和积分授予第一步:用户选择计划
用户在定价页面看到所有可用的定价计划。每个计划卡片显示:
- 计划标题和描述
- 价格信息
- 功能列表
- 购买按钮
前端组件
PricingCardDisplay 组件负责显示计划卡片:
<PricingCardDisplay
plan={plan}
localizedPlan={localizedPlan}
/>PricingCTA 组件处理购买按钮的点击事件:
<PricingCTA plan={plan} localizedPlan={localizedPlan} />第二步:创建结账会话
当用户点击购买按钮时,前端调用 /api/payment/checkout-session API 创建结账会话。
API 请求
Endpoint: POST /api/payment/checkout-session
请求体 (Stripe):
{
"provider": "stripe",
"stripePriceId": "price_xxx",
"couponCode": "DISCOUNT10" // 可选
}请求体 (Creem):
{
"provider": "creem",
"creemProductId": "prod_xxx",
"couponCode": "DISCOUNT10" // 可选
}API 处理逻辑
Stripe 结账会话创建
app/api/payment/checkout-session/route.ts
// 1. 验证用户身份
const session = await getSession();
const user = session?.user;
if (!user) return apiResponse.unauthorized();
// 2. 获取计划信息
const plan = await db
.select()
.from(pricingPlansSchema)
.where(eq(pricingPlansSchema.stripePriceId, stripePriceId))
.limit(1);
// 3. 创建或获取 Stripe Customer
const customerId = await getOrCreateStripeCustomer(user.id);
// 4. 创建 Stripe Checkout Session
const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId,
line_items: [{ price: stripePriceId, quantity: 1 }],
mode: isSubscription ? 'subscription' : 'payment',
success_url: getURL('payment/success?session_id={CHECKOUT_SESSION_ID}&provider=stripe'),
cancel_url: getURL(process.env.NEXT_PUBLIC_PRICING_PATH!),
metadata: {
userId: user.id,
planId: plan.id,
planName: plan.cardTitle,
priceId: stripePriceId
}
});
// 5. 返回会话 URL
return apiResponse.success({
sessionId: checkoutSession.id,
url: checkoutSession.url
});Creem 结账会话创建
app/api/payment/checkout-session/route.ts
// 1. 验证用户身份
const session = await getSession();
const user = session?.user;
if (!user) return apiResponse.unauthorized();
// 2. 获取计划信息
const plan = await db
.select()
.from(pricingPlansSchema)
.where(eq(pricingPlansSchema.creemProductId, creemProductId))
.limit(1);
// 3. 创建 Creem Checkout Session
const checkoutSession = await createCreemCheckoutSession({
product_id: creemProductId,
units: 1,
discount_code: couponCode,
customer: {
email: user.email
},
success_url: getURL('payment/success?provider=creem'),
metadata: {
userId: user.id,
planId: plan.id,
planName: plan.cardTitle,
productId: creemProductId
}
});
// 4. 返回会话 URL
return apiResponse.success({
sessionId: checkoutSession.id,
url: checkoutSession.checkout_url
});前端处理
components/home/PricingCTA.tsx
const handleCheckout = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/payment/checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept-Language': locale
},
body: JSON.stringify({
provider: plan.provider,
stripePriceId: plan.stripePriceId, // Stripe
creemProductId: plan.creemProductId, // Creem
couponCode: plan.stripeCouponId || plan.creemDiscountCode // 最多存在一个
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Failed to create checkout session');
}
if (result.data.url) {
// 跳转到支付页面
router.push(result.data.url);
}
} catch (error) {
toast.error(error.message);
} finally {
setIsLoading(false);
}
};第三步:用户完成支付
用户被重定向到支付提供商的支付页面(Stripe Checkout 或 Creem Checkout),完成支付流程。
Stripe Checkout
- 用户输入支付信息
- Stripe 处理支付
- 支付成功后重定向到
success_url
Creem Checkout
- 用户输入支付信息
- Creem 处理支付
- 支付成功后重定向到
success_url
第四步:支付成功回调
支付成功后,用户被重定向回应用的 payment/success 页面。
页面路由
app/[locale]/payment/success/page.tsx
import { redirect } from 'next/navigation';
import { Suspense } from 'react';
export default function PaymentSuccessPage({
searchParams
}: {
searchParams: { session_id?: string; checkout_id?: string; provider?: string }
}) {
const provider = searchParams.provider;
if (!provider) {
redirect('/pricing');
}
return (
<div className="container mx-auto py-12">
<Suspense fallback={<div>Verifying payment...</div>}>
<PaymentVerification
provider={provider}
sessionId={searchParams.session_id}
checkoutId={searchParams.checkout_id}
/>
</Suspense>
</div>
);
}支付验证组件
async function PaymentVerification({
provider,
sessionId,
checkoutId
}: {
provider: string;
sessionId?: string;
checkoutId?: string;
}) {
const session = await getSession();
if (!session?.user) {
redirect('/login');
}
// 调用验证 API
const verifyUrl = new URL('/api/payment/verify-success', process.env.NEXT_PUBLIC_SITE_URL);
verifyUrl.searchParams.set('provider', provider);
if (provider === 'stripe' && sessionId) {
verifyUrl.searchParams.set('session_id', sessionId);
}
if (provider === 'creem' && checkoutId) {
verifyUrl.searchParams.set('checkout_id', checkoutId);
}
const response = await fetch(verifyUrl.toString(), {
headers: {
Cookie: cookies().toString()
}
});
const result = await response.json();
if (!result.success) {
return (
<div className="text-center">
<h1 className="text-2xl font-bold text-red-600">Payment Verification Failed</h1>
<p>{result.error}</p>
</div>
);
}
// 显示成功信息
return (
<div className="text-center">
<h1 className="text-2xl font-bold text-green-600">Payment Successful!</h1>
<p>Your order has been processed successfully.</p>
{result.data.order && (
<div className="mt-4">
<p>Order ID: {result.data.order.id}</p>
</div>
)}
</div>
);
}第五步:验证支付状态
/api/payment/verify-success API 负责验证支付状态并返回订单信息。
API 处理流程
Stripe 验证
app/api/payment/verify-success/stripe-handler.ts
export async function verifyStripePayment(req: NextRequest, userId: string) {
const sessionId = req.nextUrl.searchParams.get('session_id');
// 1. 获取 Checkout Session
const session = await stripe.checkout.sessions.retrieve(sessionId, {
expand: ['line_items', 'payment_intent', 'subscription']
});
// 2. 验证用户 ID
if (session.metadata?.userId !== userId) {
return apiResponse.unauthorized('User ID mismatch');
}
// 3. 验证会话状态
if (session.status !== 'complete') {
return apiResponse.badRequest(`Session status is not complete: ${session.status}`);
}
// 4. 处理订阅或一次性支付
if (session.mode === 'subscription') {
return handleStripeSubscription(session, userId, sessionId);
} else if (session.mode === 'payment') {
return handleStripePayment(session, userId);
}
}Creem 验证
app/api/payment/verify-success/creem-handler.ts
export async function verifyCreemPayment(req: NextRequest, userId: string) {
const checkoutId = req.nextUrl.searchParams.get('checkout_id');
// 1. 获取 Checkout Session
const session = await retrieveCreemCheckoutSession(checkoutId);
// 2. 验证用户 ID
if (session.metadata?.userId !== userId) {
return apiResponse.unauthorized('User ID mismatch');
}
// 3. 验证支付状态
if (session.status !== 'completed') {
return apiResponse.badRequest(`Checkout status is not completed: ${session.status}`);
}
// 4. 处理订阅或一次性支付
if (session.order.type === 'recurring') {
return handleCreemSubscription(session, userId, checkoutId);
} else {
return handleCreemPayment(session, userId);
}
}返回数据结构
一次性支付:
{
"success": true,
"data": {
"order": {
"id": "order-id",
"status": "succeeded",
"amountTotal": "29.00",
"currency": "USD"
}
}
}订阅支付:
{
"success": true,
"data": {
"subscription": {
"id": "subscription-id",
"status": "active",
"currentPeriodEnd": "2024-02-01T00:00:00Z"
}
}
}第六步:Webhook 处理
支付提供商会在支付完成后发送 Webhook 事件。Webhook 处理是支付流程的关键部分,负责:
- 创建订单记录
- 授予用户积分
- 同步订阅状态
Webhook 事件
Stripe Webhook 事件
checkout.session.completed- 结账完成(一次性支付)invoice.paid- 发票支付成功(订阅续费)customer.subscription.created- 订阅创建customer.subscription.updated- 订阅更新customer.subscription.deleted- 订阅取消charge.refunded- 退款
Creem Webhook 事件
checkout.completed- 结账完成(一次性支付)subscription.paid- 订阅支付成功subscription.active- 订阅激活subscription.update- 订阅更新subscription.canceled- 订阅取消subscription.expired- 订阅过期refund.created- 退款
详细的 Webhook 处理逻辑请参考 Webhook 处理机制。
支付流程时序图
用户浏览器 前端应用 后端 API 支付提供商
| | | |
|--点击购买------>| | |
| |--创建会话------>| |
| | |--创建会话-------->|
| | |<--返回URL---------|
| |<--返回URL-------| |
|<--重定向--------| | |
| | | |
|--跳转支付页面--->| | |
| | | |
|--完成支付------>| | |
| | | |
|<--重定向回应用--| | |
| | | |
|--验证支付------>|--验证支付------>| |
| | |--查询订单-------->|
| | |<--返回订单-------|
| |<--返回结果------| |
|<--显示结果------| | |
| | | |
| | |<--Webhook事件----|
| | |--处理事件-------->|
| | |--创建订单--------|
| | |--授予积分--------|安全考虑
1. 用户身份验证
所有支付相关的 API 都需要用户登录:
const session = await getSession();
if (!session?.user) {
return apiResponse.unauthorized();
}2. Webhook 签名验证
所有 Webhook 请求都必须验证签名:
Stripe:
const sig = headers.get('stripe-signature');
const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);Creem:
const signature = headers.get('creem-signature');
if (!verifySignature(body, signature, webhookSecret)) {
return apiResponse.badRequest('Signature verification failed');
}3. 幂等性处理
订单创建使用幂等性检查,避免重复创建:
const { order, existed } = await createOrderWithIdempotency(
provider,
orderData,
providerOrderId
);
if (existed) {
return; // 订单已存在,跳过处理
}4. 用户 ID 验证
验证支付会话中的用户 ID 与当前登录用户匹配:
if (session.metadata?.userId !== userId) {
return apiResponse.unauthorized('User ID mismatch');
}测试支付流程
使用 Stripe 测试模式
- 在 Stripe Dashboard 获取测试 API Key
- 创建测试产品和价格
- 使用测试卡号:
4242 4242 4242 4242 - 配置 Webhook URL 指向你的应用或使用 Stripe CLI 转发 Webhook:
stripe listen --forward-to localhost:3000/api/stripe/webhook
使用 Creem 测试模式
- 在 Creem Dashboard 获取测试 API Key
- 创建测试产品
- 使用测试卡号:
4242 4242 4242 4242 - 配置 Webhook URL 指向你的应用