如何使用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
相关产品推荐
相关产品推荐

