Firebase Cloud Function创建zip文件仅含CSV表头无有效数据问题
问题根因
- 核心问题是异步逻辑未等待完成就触发打包:你调用的子集合查询
utilisationCollection.get()是异步IO操作,你没有等待所有子集合的查询、数据拼接逻辑执行完毕,就直接执行了archive.append(utilText)和archive.finalize(),此时utilText仅完成了表头初始化,业务数据的拼接逻辑还在异步队列中未执行,所以打包出来的文件只有表头。 - Firebase云函数的执行环境会在主线程同步逻辑执行完后直接回收进程,未等待的异步回调会直接被终止,不会执行。
修复方案
推荐将回调写法改为async/await写法,明确等待所有异步操作完成后再执行打包逻辑,修改后的代码如下:
const archiver = require('archiver'); const path = require('path'); const os = require('os'); const fs = require('fs'); newApp.get('/createUtilCSV', async (req, res) => { try { console.log("CreateAuditCSV started"); const tempFilePath = path.join(os.tmpdir(), "Util_" + req.query.start + "---" + req.query.end + ".csv"); console.log("Temp File Path: " + tempFilePath); const docRef = firestore.collection('utilisation'); const fromD = admin.firestore.Timestamp.fromDate(new Date(req.query.start)); const toD = admin.firestore.Timestamp.fromDate(new Date(req.query.end)); // 等待主集合查询完成 const snapshot = await docRef.where("timestamp", ">=", fromD).where("timestamp", "<=", toD).get(); console.log("主集合查询结果数量:" + snapshot.size); let utilText = "App Version,Author,Building,City,Country,Date,Floor ID,Floor Name, Floor Region,Bookable,Business Unit,Capacity,Fixed,Furniture Height,Occupancy,Position X,Position Y,Restricted,Status Date,Status Type,Unique Ref,Workspace Type,Report ID\n"; // 等待所有子集合查询、数据拼接全部完成 await Promise.all(snapshot.docs.map(async doc => { const line = doc.data(); let utilLine = ""; utilLine += line.appVersion + ","; utilLine += line.authorEmail + ","; utilLine += line.building + ","; utilLine += line.city + ","; utilLine += line.country + ","; utilLine += line.timestamp.toDate() + ","; utilLine += line.floorId + ","; utilLine += line.floorName + ","; utilLine += line.region + ","; // 等待当前文档的子集合查询完成 const utilisationCollection = firestore.collection('utilisation').doc(doc.id).collection('workspaces'); const snap = await utilisationCollection.get(); snap.docs.forEach(space => { const lineSpace = space.data(); let spaceAdd = utilLine; spaceAdd += lineSpace.bookable + ","; spaceAdd += lineSpace.businessUnit + ","; spaceAdd += lineSpace.capacity + ","; spaceAdd += lineSpace.fixed + ","; spaceAdd += lineSpace.height + ","; spaceAdd += lineSpace.occupancy + ","; spaceAdd += lineSpace.positionX + ","; spaceAdd += lineSpace.positionY + ","; spaceAdd += lineSpace.restricted + ","; spaceAdd += lineSpace.statusDate.toDate() + ","; spaceAdd += lineSpace.statusType + ","; spaceAdd += lineSpace.uniqueRef + ","; spaceAdd += lineSpace.workspaceType + ","; spaceAdd += doc.id + "\n"; utilText += spaceAdd; }); })); // 所有数据拼接完成后再执行压缩逻辑 const output = fs.createWriteStream(tempFilePath); const archive = archiver('zip', { gzip: true, zlib: { level: 9 } }); const metadata = { contentType: "application/zip" }; output.on('close', async function() { console.log(archive.pointer() + ' total bytes'); console.log('archiver已完成,输出文件描述符已关闭'); console.log("完整CSV内容长度: ", utilText.length); // 等待文件上传完成 await bucket.upload(tempFilePath, { make_public: true, gzip: true, destination: tempFilePath, metadata: metadata, }); // 生成签名URL const file = admin.storage().bucket().file(tempFilePath); const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + 1); const urls = await file.getSignedUrl({ action: 'read', expires: expiryDate }); const signedUrl = urls[0]; console.log('返回签名URL: ' + signedUrl); res.redirect(signedUrl); res.end(); }); archive.on('warning', function(err) { if(err.code !== 'ENOENT') throw err; }); archive.on('error', function(err) { throw err; }); archive.pipe(output); archive.append(utilText, { name: path.basename(tempFilePath) }); archive.finalize(); console.log("压缩逻辑已触发"); } catch(error) { console.log("导出错误:" + error); res.status(500).send("导出失败: " + error.message); } });
额外注意事项
- 如果数据量较大,不要将所有CSV内容存在内存变量
utilText中,可以直接将行内容写入文件流,避免云函数内存溢出。 - 提前调整Firebase云函数的超时时间,默认1秒的超时限制无法支撑大量数据的查询、压缩、上传操作,建议根据数据量级调整到30~300秒。
- 所有异步IO操作(数据库查询、文件上传、签名URL生成)都需要加
await等待执行完成,避免逻辑被云函数环境提前终止。
内容的提问来源于stack exchange,提问作者Richard Hope
相关产品推荐
相关产品推荐

