You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Stripe charge.succeeded Webhook重复执行问题排查求助

问题分析与解决方案

核心原因

你遇到的重复执行问题,本质是Stripe Webhook的重试机制被触发,添加凭证查询逻辑后,以下三种情况会导致Stripe判定端点处理失败,进而重复发送事件:

  1. 处理超时:新增的Prisma查询+更新操作,加上邮件发送,整体耗时可能超过Stripe要求的10秒响应阈值,Stripe会自动重试事件。
  2. 未捕获异常导致5xx响应:当找不到可用凭证时,代码直接throw new Error,会让Next.js返回500错误,Stripe会判定处理失败并重试。
  3. 缺乏幂等性校验:未判断当前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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 19:07:47