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

如何使用Firebase Cloud Functions处理Stripe重复触发的webhook事件

问题根因

Stripe的webhook服务未及时收到你的云函数返回的200响应时,就会自动重试触发同一事件,你的现有代码没有做幂等处理,每次收到checkout.session.completed事件都会直接写入新订单,所以会生成重复记录。

具体实现方案

第一步:新增已处理事件存储

在Firestore中新建名为processed_stripe_events的集合,专门存储已经处理过的Stripe事件ID,以事件ID作为文档ID,利用Firestore的文档唯一性和事务能力做原子校验,避免并发问题。

第二步:修改stripeWebhook云函数逻辑

核心修改是在处理事件前先校验事件是否已被处理,修改后的完整stripeWebhook代码如下:

exports.stripeWebhook = functions
    .region("europe-west3")
    .https.onRequest(async (req, res) => {
      const getItems = (checkoutSessionID) => {
        return new Promise((resolve, reject) => {
          stripe.checkout.sessions.listLineItems(
              checkoutSessionID,
              (err, lineItems) => {
                if (err) {
                  return reject(err);
                }
                resolve(lineItems);
              },
          );
        });
      };
      let event;
      let stripe;
      try {
        stripe = require("stripe")(functions.config().stripe.secret_key);
        const whSec = functions.config().stripe.payments_webhook_secret;

        event = stripe.webhooks.constructEvent(
            req.rawBody,
            req.headers["stripe-signature"],
            whSec,
        );
      } catch (err) {
        console.error("Webhook signature verification failed.");
        return res.sendStatus(400);
      }

      // 新增幂等校验逻辑
      const eventId = event.id;
      const eventRef = admin.firestore().collection('processed_stripe_events').doc(eventId);
      const eventDoc = await eventRef.get();
      if (eventDoc.exists) {
        console.log(`重复事件${eventId},跳过处理`);
        return res.sendStatus(200);
      }
      // 先标记事件为已处理,避免并发请求同时处理
      await eventRef.set({
        processedAt: admin.firestore.FieldValue.serverTimestamp(),
        eventType: event.type,
        checkoutSessionId: event.data.object.id
      });

      if (event.type === "checkout.session.completed") {
        const dataObject = event.data.object;
        const items = await getItems(dataObject.id);
        let product;
        const products = [];
        for (let i=0; i<items.data.length; i++) {
          product = await stripe.products.retrieve(items.data[i].price.product);
          const obj = {
            name: product.name,
            description: product.description,
            images: product.images,
          };
          products.push(obj);
        }
        await admin.firestore().collection("orders").doc().set({
          checkoutSessionId: dataObject.id,
          paymentStatus: dataObject.payment_status,
          shippingInfo: dataObject.shipping,
          currency: dataObject.currency,
          amountTotal: dataObject.amount_total,
          email: dataObject.customer_details.email,
          phone: dataObject.customer_details.phone,
          items: items,
          desc_img: products,
        }).then(()=>{
          return res.sendStatus(200);
        })
            .catch(async (error)=>{
              console.error(error);
              // 订单写入失败时删除已标记的事件记录,方便后续重试
              await eventRef.delete();
              return res.sendStatus(400);
            });
      } else {
        // 无需处理的事件也删除标记记录
        await eventRef.delete();
        return res.sendStatus(400);
      }
    });

额外优化建议

Stripe要求webhook接口必须在3秒内返回响应,你当前代码中需要循环调用Stripe接口拉取商品信息,耗时过长很容易超时触发重试。可以在创建checkout session时,把需要的商品信息提前存入session的metadata中,webhook收到事件时直接读取metadata里的数据,无需额外调用Stripe接口,大幅缩短响应时间。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 12:36:02