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

如何将UserId从Payment API端点传递至Webhook并解决undefined问题

解决Stripe Webhook中userUid偶尔为undefined的问题

问题分析

核心问题是首次支付后Webhook能正常获取userUid,但后续请求中userUid变为undefined,原因包括:

  • Stripe触发的事件类型不同(如首次是checkout.session.completed,后续可能是payment_intent.succeeded),不同事件的metadata存储位置不一致
  • 当前代码仅从event.data.object.metadata提取userUid,未适配多事件类型的场景
  • 未对userUid做强制校验,导致空值流入Firebase操作

解决方案

1. 全局传递userUid到Stripe关联对象

创建Checkout Session时,将userUid同步到Customer、Session和Payment Intent的metadata中,确保多事件类型下都能获取到。

2. Webhook多维度提取userUid

根据事件类型从对应位置提取userUid,当前事件对象取不到时,从关联的Customer拉取兜底。

3. 强制校验userUid非空

执行Firebase操作前必须确认userUid存在,否则直接返回错误。

4. 优化Firebase初始化

避免在API路由中重复初始化Firebase,统一封装初始化逻辑。


修改后的代码

api/stripe/stripe-payment.js

import Stripe from "stripe";
import { initializeApp } from "firebase/app";
import { NextResponse } from "next/server";
import { getDatabase, ref, set } from "firebase/database";

const firebaseConfig = {
  apiKey: "some",
  authDomain: "some",
  projectId: "some",
  storageBucket: "some",
  messagingSenderId: "some",
  appId: "some",
  measurementId: "some",
  databaseURL: "some",
};

// 全局初始化Firebase与Stripe
const app = initializeApp(firebaseConfig);
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function POST(req) {
  const reqBody = await req.json();
  const { amount, metaData } = reqBody;

  // 提前校验userUid必填
  if (!metaData?.userUid) {
    return NextResponse.json(
      { error: "userUid is required" },
      { status: 400 }
    );
  }

  try {
    const customer = await stripe.customers.create({
      metadata: {
        userUid: metaData.userUid,
      },
    });

    const session = await stripe.checkout.sessions.create({
      payment_method_types: ["card"],
      line_items: [
        {
          price_data: {
            currency: "usd",
            product_data: {
              name: "INV-" + new Date().toISOString(),
            },
            unit_amount: amount * 100 || 100,
          },
          quantity: 1,
        },
      ],
      metadata: {
        userUid: metaData.userUid,
        customer: customer.id,
      },
      mode: "payment",
      success_url: `http://localhost:3000/success`,
      cancel_url: `${req.headers.get("host")}`,
      customer: customer.id,
      // 让Payment Intent继承userUid
      payment_intent_data: {
        metadata: {
          userUid: metaData.userUid,
        },
      },
    });

    const db = getDatabase();
    const userRef = ref(db, `userUids/${Date.now()}`);
    await set(userRef, {
      userUid: metaData.userUid,
      customerID: customer.id,
      sessionID: session.id,
      timestamp: new Date().toISOString(),
    });

    return NextResponse.json({ session });
  } catch (err) {
    console.error("Checkout session error:", err);
    return NextResponse.json(
      { error: "Error creating checkout session" },
      { status: 500 }
    );
  }
}

api/stripe/stripe-webhook.js

import Stripe from "stripe";
import { initializeApp } from "firebase/app";
import { NextResponse } from "next/server";
import { getDatabase, ref, set } from "firebase/database";

const firebaseConfig = {
  apiKey: "some",
  authDomain: "some",
  projectId: "some",
  storageBucket: "some",
  messagingSenderId: "some",
  appId: "some",
  measurementId: "some",
  databaseURL: "some",
};

// 全局初始化Firebase与Stripe
const app = initializeApp(firebaseConfig);
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET_KEY;

export async function POST(req) {
  const sig = req.headers.get("stripe-signature");
  if (!sig) {
    return NextResponse.json(
      { error: "No stripe-signature header value was provided." },
      { status: 400 }
    );
  }

  let event;
  let userUid;

  try {
    const rawBody = await req.text();
    event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret);

    // 根据事件类型提取userUid
    switch (event.type) {
      case "checkout.session.completed":
        userUid = event.data.object.metadata.userUid;
        break;
      case "payment_intent.succeeded":
        // 优先从Payment Intent取,取不到则从关联Customer拉取
        userUid = event.data.object.metadata.userUid;
        if (!userUid && event.data.object.customer) {
          const customer = await stripe.customers.retrieve(event.data.object.customer);
          userUid = customer.metadata.userUid;
        }
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    // 强制校验userUid非空
    if (!userUid) {
      console.error("userUid not found in event:", event.type);
      return NextResponse.json(
        { error: "userUid not found in event data" },
        { status: 400 }
      );
    }
  } catch (err) {
    console.error(`Webhook error: ${err}`);
    return NextResponse.json({ error: "Webhook error" }, { status: 400 });
  }

  // 保存事件到Firebase(避免覆盖已有事件)
  try {
    const db = getDatabase();
    const userEventsRef = ref(db, `users/${userUid}/events/${event.id}`);
    await set(userEventsRef, {
      type: event.type,
      timestamp: new Date().toISOString(),
      eventData: event.data.object,
    });
    console.log(`Event saved for user ${userUid}: ${event.type}`);
  } catch (error) {
    console.error("Firebase save error:", error);
    return NextResponse.json({ error: "Firebase error" }, { status: 500 });
  }

  return NextResponse.json({ received: true });
}

额外注意事项

  • 确保Stripe CLI转发所有需要的事件类型(如checkout.session.completed、payment_intent.succeeded)
  • 前端调用stripe-payment接口时,必须传递当前登录用户的有效userUid
  • 建议为Firebase的users/{userUid}/events节点设置安全规则,限制仅对应用户读写

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 07:29:52