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

Stripe异步支付成功与结账完成事件选型及代码疑问

Stripe Webhook事件:checkout.session.completed与checkout.session.async_payment_succeeded的困惑与问题

背景

我目前使用Stripe的checkout.session.completed Webhook事件处理支付,但对checkout.session.async_payment_succeeded存在困惑,想知道前者是否有未覆盖的场景,同时有以下核心问题:

核心问题

  • checkout.session.completed能否作为所有成功支付的通用处理事件?
  • 我当前的代码是否存在问题?
  • 是否需要额外处理checkout.session.async_payment_succeeded事件?

另外还有两个附加问题:

  • 处理checkout.session.async_payment_succeeded时,无法通过session.customer_details.id获取fulfillOrder所需的ID,该如何解决?
  • 如何测试Bacs Direct Debit、银行转账等指定异步支付方式?

附当前完整处理代码:

import { createId } from "@paralleldrive/cuid2"
import { error } from "next/dist/build/output/log"
import { NextRequest, NextResponse } from "next/server"
import type Stripe from "stripe"

import { createUser, updateUser } from "@/app/db/users"
import { STRIPE_WEBHOOK_SECRET } from "@/app/lib/env/server"
import { isAdmin } from "@/app/lib/isAdmin"
import { getLogger } from "@/app/lib/logger"
import { obfuscateEmail } from "@/app/lib/obfuscateEmail"
import { stripe } from "@/app/lib/stripe/server"

import { auth } from "@/app/auth/lucia"

const createOrder = async (session: any, logger: any) => {
    const email = String(session.customer_details?.email)
    const user_id = createId()
    try {
        const customer = await stripe.customers.create({
            email,
        })

        await auth.createUser({
            key: {
                providerId: "email",
                providerUserId: email.toLowerCase(),
                password: null,
            },
            attributes: {
                email: email.toLowerCase(),
                user_id,
            },
        })

        await createUser({
            id: user_id,
            stripePaymentStatus: "unpaid",
            stripeCustomerId: customer.id,
            isAdmin: isAdmin(email),
        })
    } catch (error) {
        logger.error(error, `[Stripe] Error in createOrder!`)
    }

    return user_id
}

const fulfillOrder = async (id: string, session: any, logger: any) => {
    await updateUser(id, {
        stripePaymentStatus: "paid",
    })
}

export const POST = async (req: NextRequest, res: NextResponse) => {
    const logger = getLogger()
    const body = await req.text()
    const sig = req.headers.get("stripe-signature")

    let event: Stripe.Event

    logger.info("[Stripe] Processing webhook")

    try {
        event = stripe.webhooks.constructEvent(body, sig as string, STRIPE_WEBHOOK_SECRET)

        logger.info({ type: event.type }, `[Stripe] Listening to Webhook Event!`)
    } catch (err) {
        error(err as string)
        return new Response(`Webhook Error: ${(err as Error).message}`, {
            status: 400,
        })
    }

    try {
        // Handle the event
        switch (event.type) {
            case "checkout.session.completed": {
                const session = event.data.object as Stripe.Checkout.Session

                const id = await createOrder(session, logger)

                if (session.payment_status === "paid") {
                    await fulfillOrder(id, session, logger)

                    const email = String(session.customer_details?.email)
                    logger.info({ email: obfuscateEmail(email) }, `[Stripe] 💰 Successfully charged`)
                }
            }
            case "checkout.session.async_payment_succeeded": {
                const session = event.data.object as Stripe.Checkout.Session

                await fulfillOrder(session.customer_details.id, session, logger)

                return NextResponse.json({ success: true })
            }
            case "checkout.session.async_payment_failed": {
                const session = event.data.object as Stripe.Checkout.Session

                logger.error(session, `[Stripe] Payment failed`)

                return NextResponse.json({ success: false })
            }
            default: {
                // Unexpected event type
                logger.warn(event.type, `🤷‍♀️ Unhandled event type`)
                return NextResponse.json({ success: false })
            }
        }
    } catch (err) {
        logger.error({ err }, `[Stripe] Webhook Error`)
        return NextResponse.json(
            { error: "Webhook handler failed. View logs." },
            {
                status: 400,
            }
        )
    }
}

解答

1. checkout.session.completed能否通用处理所有成功支付?

不能。checkout.session.completed仅代表结账流程完成,但支付状态可能是paid(同步支付成功)或unpaid(异步支付待确认)。对于Bacs Direct Debit、银行转账这类异步支付方式,用户完成结账后,支付需要银行侧确认,此时checkout.session.completed的payment_status是unpaid,只有当异步支付最终成功时,Stripe才会发送checkout.session.async_payment_succeeded事件。如果只依赖checkout.session.completed,会漏掉这类异步支付成功的场景,导致订单无法履约。

2. 当前代码存在的问题

  • switch case无终止逻辑:checkout.session.completed分支执行后,会继续进入后续case分支,导致逻辑混乱,必须在每个case末尾添加break或return。
  • 重复创建用户/客户:createOrder方法会无条件创建Stripe客户和系统用户,若用户已存在会触发报错,应先通过邮箱查询是否存在用户,避免重复操作。
  • 异步支付事件中ID获取错误:session.customer_details.id不存在,正确的Stripe客户ID是session.customer(字符串类型),需要通过这个ID查询系统内的用户ID,而非customer_details字段。
  • 异步支付与系统用户无关联:async_payment_succeeded分支中没有从数据库获取对应系统用户ID的逻辑,直接调用fulfillOrder会导致参数错误。

3. 是否需要处理checkout.session.async_payment_succeeded?

必须处理。异步支付方式的成功事件只会通过该Webhook发送,不处理的话,这类订单永远无法标记为已支付,无法完成履约流程。

附加问题解答

如何获取fulfillOrder所需的ID?

修改async_payment_succeeded分支逻辑,通过Stripe客户ID查询系统用户:

case "checkout.session.async_payment_succeeded": {
    const session = event.data.object as Stripe.Checkout.Session
    // 新增方法:通过Stripe客户ID查询系统用户
    const user = await getUserByStripeCustomerId(session.customer as string)
    if (user) {
        await fulfillOrder(user.id, session, logger)
        logger.info(`[Stripe] 异步支付成功,已更新用户${user.id}的支付状态`)
    } else {
        logger.error(`[Stripe] 未找到Stripe客户ID为${session.customer}的用户`)
    }
    return NextResponse.json({ success: true })
}

需新增getUserByStripeCustomerId方法,从数据库中通过stripeCustomerId字段查询用户。

如何测试异步支付方式?

  • Bacs Direct Debit:测试模式下使用卡号0000000000000000,输入任意姓名和地址提交,Stripe会模拟异步流程,1-2分钟后发送checkout.session.async_payment_succeeded事件。
  • 银行转账:测试模式下选择银行转账支付方式,Stripe生成虚拟转账信息后,点击「模拟付款成功」即可触发异步支付成功事件。
  • 也可在Stripe Dashboard的Webhooks -> Test Webhooks中手动触发事件,验证处理逻辑。

修正后的核心代码片段示例

// 修正switch case逻辑,添加终止与用户查询
switch (event.type) {
    case "checkout.session.completed": {
        const session = event.data.object as Stripe.Checkout.Session
        // 先查询用户是否已存在
        let existingUser = await getUserByEmail(session.customer_details?.email as string)
        let userId
        if (!existingUser) {
            userId = await createOrder(session, logger)
        } else {
            userId = existingUser.id
        }
        if (session.payment_status === "paid") {
            await fulfillOrder(userId, session, logger)
            const email = String(session.customer_details?.email)
            logger.info({ email: obfuscateEmail(email) }, `[Stripe] 💰 同步支付成功`)
        } else {
            logger.info(`[Stripe] 结账完成,等待异步支付确认`)
        }
        return NextResponse.json({ success: true })
    }
    case "checkout.session.async_payment_succeeded": {
        const session = event.data.object as Stripe.Checkout.Session
        const user = await getUserByStripeCustomerId(session.customer as string)
        if (user) {
            await fulfillOrder(user.id, session, logger)
            logger.info(`[Stripe] 💰 异步支付成功`)
        } else {
            logger.error(`[Stripe] 未找到关联用户`)
        }
        return NextResponse.json({ success: true })
    }
    case "checkout.session.async_payment_failed": {
        const session = event.data.object as Stripe.Checkout.Session
        logger.error(session, `[Stripe] 异步支付失败`)
        return NextResponse.json({ success: false })
    }
    default: {
        logger.warn(event.type, `🤷‍♀️ 未处理的事件类型`)
        return NextResponse.json({ success: false })
    }
}

内容的提问来源于stack exchange,提问作者deadcoder0904

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 09:13:15