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

Google Apps Script超最大执行时间 分批续跑逻辑失效求助

现有代码超时、逻辑不生效的核心原因
  • 分片续跑逻辑完全没跑通:函数启动第一行就把存储进度的D1单元格重置为0,紧接着直接调用全量导出函数,这个全量导出会遍历所有日历、逐行写表,本身就会直接触发执行时长限制,后面的循环、触发器逻辑根本没有执行机会。
  • 存在未定义变量:循环中使用的xngData从未被赋值,就算全量导出没超时,执行到这一步也会直接抛出引用错误。
  • 写入性能极差:导出逻辑每处理1条事件就单独调用1次值写入、7次公式设置,Apps Script的表格读写接口存在固定网络开销,逐行操作的耗时是批量写入的几十上百倍,是超时的核心诱因。
  • 分片逻辑冲突:导出函数每次启动第一行就清空整张导出表,就算实现了分片续跑,下一次执行会直接清空上一次分片写入的内容,永远无法得到完整结果。
  • 时间阈值设置不合理:注释标注的时间阈值和实际代码设置值不符,且没有给状态写入、触发器创建预留冗余时间,很容易卡着时间点触发超时。
优化实现方案

不用拆分8份独立脚本,直接用文档属性存储执行进度,把逐行读写替换为批量操作,按日历粒度分片,每次执行尽可能多处理任务,临近执行时限自动存储进度、创建续跑触发器,全程无需人工干预。
优化后批量写入的性能比原实现高10~100倍,大部分数据量场景下甚至不需要分片就能一次执行完成。

// 统一配置常量,无需在代码中零散查找修改
const CONFIG = {
  CAL_SHEET_NAME: "Id Calendriers - Dates Debut et Fin",
  EXPORT_SHEET_NAME: "Extraction 1 - Calendrier",
  TIME_LIMIT_MS: 1000 * 60 * 4.5, // 预留30秒执行状态存储、触发器创建,避免卡线超时
  TRIGGER_DELAY_MS: 1000 * 30, // 下次执行延迟30秒,避免触发接口频控
  HEADER: [["Titre", "Description", "Location", "Début", "Fin", "Heures effectives","Extraction 2","Extraction 3","Heures Planifiées", "Vacances", "Maladie","Congé légal", "Absence"]]
}

function update_main_master() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const calSheet = ss.getSheetByName(CONFIG.CAL_SHEET_NAME);
  const exportSheet = ss.getSheetByName(CONFIG.EXPORT_SHEET_NAME);
  const docProps = PropertiesService.getDocumentProperties();
  
  // 读取上次执行中断的位置,首次执行默认从0开始
  let currentCalendarIndex = Number(docProps.getProperty('last_processed_cal_index')) || 0;
  const startTime = new Date();

  // 首次执行时清空导出表、写入表头
  if (currentCalendarIndex === 0) {
    exportSheet.clear();
    exportSheet.getRange(7,1,1,13).setValues(CONFIG.HEADER);
  }

  // 一次性读取所有配置、日历ID,避免循环内重复读表
  const startDate = calSheet.getRange('k1').getValue();
  const endDate = calSheet.getRange('k2').getValue();
  const allCalendars = calSheet.getRange('b3:B').getValues().flat().filter(id => id !== "");

  const allRowData = [];
  const allFormulas = [];

  // 从上次中断的日历位置开始处理
  while (currentCalendarIndex < allCalendars.length) {
    // 处理每个日历前先判断剩余执行时间,不足则提前中断
    if (new Date() - startTime > CONFIG.TIME_LIMIT_MS) break;

    const calId = allCalendars[currentCalendarIndex];
    const cal = CalendarApp.getCalendarById(calId);
    if (!cal) {
      currentCalendarIndex++;
      continue;
    }
    const events = cal.getEvents(startDate, endDate);

    // 把当前日历所有事件的静态值、公式全部攒到内存,不逐行写表
    events.forEach(event => {
      const rowIndex = exportSheet.getLastRow() + allRowData.length + 1;
      // 静态字段值
      allRowData.push([
        event.getTitle(),
        event.getDescription(),
        event.getLocation(),
        event.getStartTime(),
        event.getEndTime(),
        "", "", "", "", "", "", "", ""
      ]);
      // 对应行的公式,按列顺序拼接
      allFormulas.push([
        `=(HOUR(RIGHT(B${rowIndex};5))+(MINUTE(RIGHT(B${rowIndex};5))/60))-(HOUR(LEFT(B${rowIndex};5))+(MINUTE(LEFT(B${rowIndex};5))/60))`,
        `=IFERROR(TEXT(INDEX(SPLIT(A${rowIndex};" ");2);"hh:mm");"")`,
        `=IFERROR(TEXT(INDEX(SPLIT(A${rowIndex};" ");3);"hh:mm");"")`,
        `=IF(OR(G${rowIndex}="Maladie";G${rowIndex}="Congé";G${rowIndex}="Absence";G${rowIndex}="00:00";G${rowIndex}="Vacances");0;(HOUR(H${rowIndex})+(MINUTE(H${rowIndex})/60))-(HOUR(G${rowIndex})+(MINUTE(G${rowIndex})/60)))`,
        `=IF(IFNA(VLOOKUP(D${rowIndex}; feries;1;FALSE);1)<>1;0;IF(AND(G${rowIndex}="00:00";H${rowIndex}="Vacances");0,5;IF(G${rowIndex}="Vacances";1;0)))`,
        `=IF(G${rowIndex}="Maladie";1;0)`,
        `=IF(G${rowIndex}="Congé";1;0)`,
        `=IF(G${rowIndex}="Absence";1;0)`
      ]);
    });

    currentCalendarIndex++;
  }

  // 攒完一批数据一次性写入表格,性能比逐行写高几十倍
  if (allRowData.length > 0) {
    const startWriteRow = exportSheet.getLastRow() + 1;
    // 批量写入静态值
    exportSheet.getRange(startWriteRow, 1, allRowData.length, 13).setValues(allRowData);
    // 批量写入公式
    exportSheet.getRange(startWriteRow, 6, allFormulas.length, 8).setFormulas(allFormulas);
    // 统一设置工时列数字格式
    exportSheet.getRange(startWriteRow, 6, allFormulas.length, 1).setNumberFormat('.00');
  }

  // 判断是否处理完全部日历
  if (currentCalendarIndex < allCalendars.length) {
    // 存储当前进度
    docProps.setProperty('last_processed_cal_index', currentCalendarIndex.toString());
    // 清理旧的续跑触发器,避免重复触发
    ScriptApp.getProjectTriggers().forEach(t => {
      if (t.getHandlerFunction() === 'update_main_master') ScriptApp.deleteTrigger(t);
    });
    // 创建新触发器下次续跑
    ScriptApp.newTrigger("update_main_master")
      .timeBased()
      .after(CONFIG.TRIGGER_DELAY_MS)
      .create();
  } else {
    // 全部处理完成,清理进度标记,写入更新时间
    docProps.deleteProperty('last_processed_cal_index');
    calSheet.getRange('e1').setValue('Updated');
    calSheet.getRange('f1').setValue(new Date());
    // 清理所有续跑触发器
    ScriptApp.getProjectTriggers().forEach(t => {
      if (t.getHandlerFunction() === 'update_main_master') ScriptApp.deleteTrigger(t);
    });
  }
}
使用注意事项
  • 首次运行前,删除项目中所有之前手动创建的定时触发器,避免重复执行导出。
  • 如果存在单个日历事件量过万的极端场景,可以把分片粒度从「单个日历」进一步拆分为「单日历的事件分页」,只需要把存储的进度值从「日历索引」改为「日历索引+事件偏移量」即可,绝大多数业务场景不需要做这层调整。
  • 所有和表格、日历的交互尽量批量操作,不要在循环里重复发起读写请求,这是规避Apps Script执行超时最有效的手段。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 23:24:24