Stripe charge.succeeded Webhook重复执行问题排查求助
问题分析与解决方案
核心原因
你遇到的重复执行问题,本质是Stripe Webhook的重试机制被触发,添加凭证查询逻辑后,以下三种情况会导致Stripe判定端点处理失败,进而重复发送事件:
- 处理超时:新增的Prisma查询+更新操作,加上邮件发送,整体耗时可能超过Stripe要求的10秒响应阈值,Stripe会自动重试事件。
- 未捕获异常导致5xx响应:当找不到可用凭证时,代码直接
throw new Error,会让Next.js返回500错误,Stripe会判定处理失败并重试。 - 缺乏幂等性校验:未判断当前
paymentId(即Stripe事件IDdata.id)是否已处理,导致重复执行创建用户逻辑,触发数据库唯一约束报错。
分步解决方案
1. 优先返回响应,异步处理非关键操作
把邮件发送这类耗时操作放到后台异步执行,不阻塞响应返回,确保Stripe在10秒内收到2xx响应。
2. 添加幂等性检查
处理charge.succeeded事件时,先查询数据库是否已存在该paymentId的记录,存在则直接返回成功,跳过后续逻辑。
3. 正确处理异常,避免返回5xx错误
当找不到可用凭证时,不要直接抛出异常,而是返回明确的400响应,告知Stripe该事件无法处理,无需重试。
修改后的完整代码
import { NextRequest, NextResponse } from "next/server"; import { PrismaClient } from "@prisma/client"; import nodemailer from 'nodemailer'; // 全局初始化Prisma实例,避免重复创建连接 const prisma = new PrismaClient(); const getCorrectVoucher = (data: number): string => { switch (data) { case 12000: return '101'; case 23000: return '102'; case 20000: return '201'; case 40000: return '202'; default: return 'price not found'; } } // 封装异步邮件发送函数,不阻塞主流程 const sendVoucherEmail = async (userEmail: string, voucherCode: string, emailConfig: any) => { const transporter = nodemailer.createTransport(emailConfig); const subject = 'Gutschein'; const message = `Vielen Dank für deine Bestellung, anbei der Gutscheincode ${voucherCode}`; try { await transporter.sendMail({ from: emailConfig.auth.user, to: userEmail, subject, text: `Gutscheinbestellung\nEmail: ${userEmail}\nNachricht: ${message}` }); console.log(`邮件已发送至 ${userEmail}`); } catch (error) { console.error(`邮件发送失败: ${error}`); } } export async function POST(request: NextRequest) { try { const data = await request.json(); console.log(`收到Webhook事件: ${data.type}`); const emailConfig = { host: process.env.EMAIL_SERVER, port: 465, secure: true, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORT } }; switch (data.type) { case 'payment_intent.created': case 'checkout.session.completed': case 'payment_intent.succeeded': return NextResponse.json({ message: `${data.type} 已处理` }, { status: 200 }); case 'charge.succeeded': // 幂等性检查:判断当前支付事件是否已处理 const existingUser = await prisma.user.findFirst({ where: { paymentId: data.id } }); if (existingUser) { console.log(`事件 ${data.id} 已处理过,跳过`); return NextResponse.json({ message: '事件已处理' }, { status: 200 }); } const customerMail = data.data.object.billing_details.email; const voucherType = getCorrectVoucher(data.data.object.amount); const activeVoucher = await prisma.voucher.findFirst({ where: { active: true, voucherType } }); if (!activeVoucher?.id) { console.error(`找不到可用凭证,类型: ${voucherType}`); // 返回400告知Stripe无需重试 return NextResponse.json({ message: '无可用凭证' }, { status: 400 }); } // 事务确保凭证更新和用户创建的原子性 await prisma.$transaction([ prisma.voucher.update({ where: { id: activeVoucher.id }, data: { active: false } }), prisma.user.create({ data: { email: customerMail, voucher: activeVoucher.voucherCode, voucherType, paymentId: data.id, paymentStatus: 'payed' } }) ]); console.log(`用户创建成功,凭证ID: ${activeVoucher.id}`); // 异步发送邮件,不阻塞响应返回 sendVoucherEmail(customerMail, activeVoucher.voucherCode, emailConfig); return NextResponse.json({ message: 'success' }, { status: 200 }); default: return NextResponse.json({ message: 'webhook success' }, { status: 200 }); } } catch (error) { console.error(`Webhook处理失败: ${error}`); return NextResponse.json({ message: `处理失败: ${error}` }, { status: 500 }); } finally { await prisma.$disconnect(); } }
额外优化点
- 全局Prisma实例:避免在每个请求中创建新实例,防止数据库连接池耗尽。
- 数据库事务:将凭证更新和用户创建放在同一个事务中,确保操作原子性,避免部分执行的异常情况。
- 日志完善:添加详细日志,便于排查重复执行的具体触发场景。
内容的提问来源于stack exchange,提问作者pmo42
相关产品推荐
相关产品推荐

