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
相关产品推荐
相关产品推荐

