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

NetSuite Map Reduce脚本Reduce阶段使用量超限问题求助

NetSuite MapReduce脚本Reduce阶段治理超限问题解决方案

问题根源

你的脚本Reduce阶段无实际聚合逻辑,仅传递Map输出的单条数据,导致大量独立Reduce任务被创建,每个任务都消耗治理单元,数据量大时直接触发超限。同时,getInputData全量加载数据集到内存、Map阶段频繁单条lookup操作也加剧了治理消耗。

优化方案

1. 移除冗余的Reduce阶段

当前Reduce阶段仅做数据传递,无任何聚合/合并操作,完全可以跳过该阶段,让Map输出直接进入Summarize阶段处理,减少治理单元浪费。

2. 优化getInputData,避免内存过载

原代码将所有数据集结果存入数组后返回,数据量大时会占用大量内存且增加框架处理负担。改为直接返回数据集对象,让NetSuite自动分页处理输入数据。

3. 缓存Lookup结果,减少API调用

原Map阶段每条数据都执行多次单条lookup,每个lookup都会消耗治理单元。新增缓存机制,同一个记录ID只查询一次,大幅减少API调用次数。

4. 批量写入CSV,提升效率

原代码逐行appendLine写入文件,数据量大时IO操作频繁。改为先将所有行存入数组,最后一次性写入文件内容,提升文件生成效率。

修改后的完整代码

/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* Script Description:
* 该Map/Reduce脚本提取数据集结果,生成CSV文件并发送给指定收件人,优化后支持处理大数据量。
*/
define(['N/email', 'N/file', 'N/runtime', 'N/dataset', 'N/log', 'N/search', 'N/compress'],
function (email, file, runtime, dataset, log, search, compress) {

    // 预加载需要的名称映射,减少重复lookup次数
    let nameMappings = {
        customer: new Map(),
        vertical: new Map(),
        subdepartment: new Map(),
        subsidiary: new Map(),
        currency: new Map()
    };

    function getInputData() {
        try {
            let today = new Date();
            let firstWorkingDay = getFirstWorkingDay(today.getFullYear(), today.getMonth() + 1);

            // 非当月第一个工作日则退出
            if (today.toDateString() !== firstWorkingDay.toDateString()) {
                log.audit('非第一个工作日', `今日: ${today.toDateString()}, 当月第一个工作日: ${firstWorkingDay.toDateString()}`);
                return [];
            }

            // 直接返回数据集对象,让框架自动分页处理
            let datasetObj = dataset.load({ id: 'custdataset70' });
            log.audit('数据集加载完成', `列数: ${datasetObj.columns.length}`);
            return datasetObj;
        } catch (error) {
            log.error('getInputData错误', error);
            return [];
        }
    }

    function map(context) {
        try {
            let result = context.value;
            let rowValues = result.values;
            let datasetColumns = context.columns.map(col => ({ id: col.id, alias: col.alias }));

            let poNumberIndex = -1, poStatusIndex = -1, billStatusIndex = -1, currencyIndex = -1;
            let customerIndex = -1, verticalIndex = -1, subproductIndex = -1, subsidiaryIndex = -1;

            // 获取列索引
            datasetColumns.forEach((col, index) => {
                if (col.alias === 'tranid') poNumberIndex = index;
                if (col.alias === 'status') poStatusIndex = index;
                if (col.alias === 'status_1') billStatusIndex = index;
                if (col.alias === 'currency') currencyIndex = index;
                if (col.alias === 'entity') customerIndex = index;
                if (col.alias === 'cseg_vertical') verticalIndex = index;
                if (col.alias === 'custbody_subdepartment') subproductIndex = index;
                if (col.alias === 'subsidiary') subsidiaryIndex = index;
            });

            if (poNumberIndex === -1 || poStatusIndex === -1 || billStatusIndex === -1 || currencyIndex === -1 || customerIndex === -1 || verticalIndex === -1 || subproductIndex === -1 || subsidiaryIndex === -1) {
                log.error('错误', '数据集中未找到必要列');
                return;
            }

            // 替换账单状态
            if (billStatusIndex !== -1) {
                switch(rowValues[billStatusIndex]) {
                    case 'A': rowValues[billStatusIndex] = 'Bill: Open'; break;
                    case 'B': rowValues[billStatusIndex] = 'Bill: Paid In Full'; break;
                    case 'C': rowValues[billStatusIndex] = 'Item Fulfillment : Shipped'; break;
                    case 'Y': rowValues[billStatusIndex] = 'Item Receipt : Undefined'; break;
                }
            }

            // 替换采购订单状态
            if (poStatusIndex !== -1) {
                switch(rowValues[poStatusIndex]) {
                    case 'A': rowValues[poStatusIndex] = 'Purchase Order: Pending Supervisor Approval'; break;
                    case 'B': rowValues[poStatusIndex] = 'Purchase Order: Pending Receipt'; break;
                    case 'C': rowValues[poStatusIndex] = 'Purchase Order : Rejected by Supervisor'; break;
                    case 'D': rowValues[poStatusIndex] = 'Purchase Order : Partially Received'; break;
                    case 'E': rowValues[poStatusIndex] = 'Purchase Order : Pending Billing/Partially Received'; break;
                    case 'F': rowValues[poStatusIndex] = 'Purchase Order : Pending Bill'; break;
                    case 'G': rowValues[poStatusIndex] = 'Purchase Order : Fully Billed'; break;
                    case 'H': rowValues[poStatusIndex] = 'Purchase Order : Closed'; break;
                }
            }

            // 处理客户信息,使用缓存避免重复查询
            if(rowValues[customerIndex]) {
                let customerId = rowValues[customerIndex];
                if (!nameMappings.customer.has(customerId)) {
                    let customerLookup = search.lookupFields({
                        type: search.Type.CUSTOMER,
                        id: customerId,
                        columns: ['entityid', 'altname']
                    });
                    let concatnatecustomer = `${customerLookup.entityid} ${customerLookup.altname}`;
                    nameMappings.customer.set(customerId, concatnatecustomer);
                }
                rowValues[customerIndex] = nameMappings.customer.get(customerId);
            }

            // 处理其他lookup字段,使用缓存
            rowValues[verticalIndex] = getCachedName('vertical', 'customrecord_cseg_vertical', rowValues[verticalIndex]);
            rowValues[subproductIndex] = getCachedName('subdepartment', 'customrecord_subdepartment', rowValues[subproductIndex]);
            rowValues[subsidiaryIndex] = getCachedName('subsidiary', 'subsidiary', rowValues[subsidiaryIndex]);
            rowValues[currencyIndex] = getCachedName('currency', 'currency', rowValues[currencyIndex]);

            // 格式化CSV行
            let csvRow = rowValues.map(value => {
                if (typeof value === 'string' && value.includes(',')) {
                    return `"${value.replace(/"/g, '""')}"`;
                }
                return value ?? '';
            }).join(',');

            context.write({ key: 'csv_row', value: csvRow });
        } catch (error) {
            log.error('Map阶段错误', JSON.stringify(error));
        }
    }

    function summarize(summary) {
        try {
            let scriptObj = runtime.getCurrentScript();
            let recipientEmailsString = scriptObj.getParameter({ name: 'custscript_recepient_email_address' });

            if (!recipientEmailsString) {
                log.error('邮件错误', '未在脚本参数中提供收件人邮箱');
                return;
            }

            let recipientEmailsArray = recipientEmailsString.split(',').map(email => email.trim());
            log.audit('收件人列表', recipientEmailsArray);

            // 获取CSV表头
            let datasetObj = dataset.load({ id: 'custdataset70' });
            let headers = datasetObj.columns.map(col => col.label || col.id);
            let csvRows = [headers.join(',')];

            // 收集所有CSV行
            summary.output.iterator().each((key, value) => {
                csvRows.push(value);
                return true;
            });

            // 生成CSV文件
            let today = new Date(new Date().toLocaleString("en-US", { timeZone: "Australia/Sydney" }));
            let isoString = today.toISOString().replace(/:/g, '_');
            let fileName = `Vendor Spend Report_${isoString}.csv`; 
            let fileObj = file.create({
                name: fileName,
                fileType: file.Type.CSV,
                contents: csvRows.join('\n'),
                isOnline: true,
                folder: 5081389
            });

            // 压缩文件
            let archiver = compress.createArchiver();
            archiver.add({ file: fileObj });
            let zipFileName = `Vendor Spend Report File_${new Date(today).toJSON()}.zip`;
            let zipFile = archiver.archive({ name: zipFileName });
            zipFile.folder = 5081389; 
            let fileId = zipFile.save();

            if (fileId) {
                sendEmail(fileId, recipientEmailsArray);
            }

            log.audit('成功', '已发送包含数据集结果的邮件');
            log.audit('剩余治理单元', runtime.getCurrentScript().getRemainingUsage());
        } catch (error) {
            log.error('Summarize阶段错误', error);
        }
    }

    function sendEmail(fileId, recipientEmailsArray) {
        try {
            if (!fileId) {
                log.audit('邮件跳过', '未生成文件');
                return;
            }

            let lastDayOfPreviousMonth = getLastDayOfPreviousMonth("Australia/Sydney");
            if (!lastDayOfPreviousMonth) return;

            let startDate = '1 January 2022';
            let endDate = lastDayOfPreviousMonth.toLocaleDateString('en-AU', { day: 'numeric', month: 'long', year: 'numeric' });

            let emailBody = `<p>*** 这是自动发送的邮件,请不要回复。 ****</p>` +
                `<p>亲爱的用户:</p>` +
                `<p>请查收附件中2022年1月1日至${endDate}的供应商支出报告。</p>` +
                `<p>谢谢<br>` + 
                `NetSuite 支持团队</p>` +
                `<p>请勿直接回复此邮件,我们无法处理此类回复。若您并非该邮件的合适收件人,请通过NetSuite支持联系我们:<br>` +
                `netsuitesupport@team.telstra.com</p>`;

            email.send({
                author: 115293, 
                recipients: recipientEmailsArray, 
                subject: '月度供应商支出报告',
                body: emailBody,
                attachments: [file.load({ id: fileId })]
            });
        } catch (error) {
            log.error('发送邮件错误', JSON.stringify(error));
        }
    }

    // 缓存lookup结果,减少重复API调用
    function getCachedName(mapKey, recordType, internalId) {
        if (!internalId) return '';
        let map = nameMappings[mapKey];
        if (map.has(internalId)) {
            return map.get(internalId);
        }
        try {
            let lookup = search.lookupFields({
                type: recordType,
                id: internalId,
                columns: ['name']
            });
            let name = lookup.name ?? 'Null';
            map.set(internalId, name);
            return name;
        } catch (error) {
            log.error(`查询${recordType} ID ${internalId}错误`, JSON.stringify(error));
            return 'Null';
        }
    }

    function getFirstWorkingDay(year, month) {
        let date = new Date(year, month - 1, 1);
        while (date.getDay() === 0 || date.getDay() === 6) { // 0=周日,6=周六
            date.setDate(date.getDate() + 1);
        }
        return date;
    }

    function getLastDayOfPreviousMonth(timezone) {
        try {
            let todayDate = new Date(new Date().toLocaleString("en-US", { timeZone: timezone }));
            return new Date(todayDate.getFullYear(), todayDate.getMonth(), 0);
        } catch (error) {
            log.error('获取上月最后一天错误', JSON.stringify(error));
            return null;
        }
    }

    return {
        getInputData: getInputData,
        map: map,
        summarize: summarize // 移除reduce阶段
    };
});

关键优化点说明

  • 移除Reduce阶段:减少不必要的任务调度和治理消耗,Map输出直接进入Summarize。
  • 缓存Lookup结果:同一个记录ID只查询一次,大幅减少API调用次数。
  • 批量写入CSV:先收集所有行再一次性写入,提升文件生成效率。
  • 直接返回数据集:让NetSuite框架自动分页处理输入,避免内存过载。

内容的提问来源于stack exchange,提问作者Maira S

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 22:14:52