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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.10 00:45:04