Stripe支付后触发405错误,Prisma数据库未更新求助
Stripe Webhook返回405 Method Not Allowed错误(Vercel部署)
我的AI SaaS应用已集成Replicate数据交互,接入Stripe支付系统实现付费功能,创建了/pages/api/stripe-webhook端点,但Stripe回调请求被Vercel服务器拒绝,返回405 'Method Not Allowed'错误,导致支付成功后无法更新Prisma数据库中的用户积分及购买记录。
Webhook端点代码 (/pages/api/stripe-webhook.ts)
import { NextApiRequest, NextApiResponse } from "next"; import prisma from "../../lib/prismadb"; import Stripe from "stripe"; import { buffer } from "micro"; import Cors from "micro-cors"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2022-11-15", }); export const config = { api: { bodyParser: false, }, }; const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET || ""; const cors = Cors({ allowMethods: ["POST", "HEAD"], }); const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === "POST") { const buf = await buffer(req); const sig = req.headers["stripe-signature"]!; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( buf.toString(), sig, webhookSecret ); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; if (err! instanceof Error) console.log(err); console.log(`❌ Error message: ${errorMessage}`); res.status(400).send(`Webhook Error: ${errorMessage}`); return; } console.log("✅ Success:", event.id); if ( event.type === "payment_intent.succeeded" || event.type === "checkout.session.completed" ) { const paymentIntent = event.data.object as Stripe.PaymentIntent; console.log(`💰 PaymentIntent: ${JSON.stringify(paymentIntent)}`); // @ts-ignore const userEmail = paymentIntent.customer_details.email; let creditAmount = 0; // @ts-ignore switch (paymentIntent.amount_subtotal) { case 500: case 1000: creditAmount = 20; break; case 1900: case 3000: creditAmount = 100; break; case 3500: case 5000: creditAmount = 250; break; case 7000: case 10000: creditAmount = 750; break; } await prisma.user.update({ where: { email: userEmail, }, data: { credits: { increment: creditAmount, }, }, }); await prisma.purchase.create({ data: { creditAmount: creditAmount, user: { connect: { email: userEmail, }, }, }, }); } else if (event.type === "payment_intent.payment_failed") { const paymentIntent = event.data.object as Stripe.PaymentIntent; console.log( `❌ Payment failed: ${paymentIntent.last_payment_error?.message}` ); } else if (event.type === "charge.succeeded") { const charge = event.data.object as Stripe.Charge; console.log(`💵 Charge id: ${charge.id}`); } else { console.warn(`🤷♀️ Unhandled event type: ${event.type}`); } res.json({ received: true }); } else { res.setHeader("Allow", "POST"); res.status(405).end("Method Not Allowed"); } }; export default cors(webhookHandler as any);
Prisma Schema
datasource db { provider = "postgresql" url = env("DATABASE_URL") directUrl = env("DIRECT_URL") shadowDatabaseUrl = env("SHADOW_DATABASE_URL") } generator client { provider = "prisma-client-js" } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? credits Int @default(1) location String? accounts Account[] sessions Session[] rooms Room[] purchases Purchase[] } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) } model Room { id String @id @default(cuid()) replicateId String userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) inputImage String outputImage String prompt String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([replicateId, userId]) } model Purchase { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) creditAmount Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
解决方法
1. 移除不必要的CORS配置
Stripe webhook请求由服务器直接发起,不存在浏览器跨域问题,当前的micro-cors配置可能与Vercel的路由规则冲突,导致请求被拦截:
- 删除CORS相关导入与包装代码
- 将
export default cors(webhookHandler as any);改为export default webhookHandler;
2. 修正事件类型转换错误
checkout.session.completed事件的对象是Checkout.Session,而非PaymentIntent,类型强制转换会导致逻辑异常,需分开处理:
if (event.type === "payment_intent.succeeded") { const paymentIntent = event.data.object as Stripe.PaymentIntent; const userEmail = paymentIntent.customer_details?.email; // 原有积分计算与数据库操作逻辑 } else if (event.type === "checkout.session.completed") { const session = event.data.object as Stripe.Checkout.Session; const userEmail = session.customer_details?.email; const amountSubtotal = session.amount_subtotal; // 复用积分计算逻辑 let creditAmount = 0; switch (amountSubtotal) { case 500: case 1000: creditAmount = 20; break; case 1900: case 3000: creditAmount = 100; break; case 3500: case 5000: creditAmount = 250; break; case 7000: case 10000: creditAmount = 750; break; } // 数据库操作逻辑 }
3. 验证环境变量与Webhook配置
- 确认Vercel环境变量中
STRIPE_WEBHOOK_SECRET为Stripe Dashboard生成的签名密钥(而非测试密钥) - 检查Stripe Dashboard中配置的Webhook URL与Vercel部署后的URL完全一致(必须为HTTPS协议)
4. 增加数据库操作容错
添加错误捕获,避免数据库操作失败导致Webhook返回错误(Stripe会自动重试失败的Webhook请求):
try { await prisma.user.update({ where: { email: userEmail }, data: { credits: { increment: creditAmount } }, }); await prisma.purchase.create({ data: { creditAmount, user: { connect: { email: userEmail } } }, }); } catch (err) { console.error("数据库更新失败:", err); }
内容的提问来源于stack exchange,提问作者Tom
相关产品推荐
相关产品推荐

