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

Google Calendar员工可用时间查询代码修复需求

代码修复建议:Google Calendar 可用时段查询功能

原代码核心问题

  1. 仅跳过当前全天事件,未处理全天事件占据的完整日期,导致错误计算空闲窗口
  2. 未校验空闲时段是否完全落在8:00-17:00的工作时间范围内
  3. 遗漏了「当前时间到第一个事件开始」的空闲窗口检查
  4. 未处理空闲窗口跨天的情况,以及工作时间边界的截断

修复后的完整代码

function getAvailability(email, minutes) {
  if (!email) {
    throw new Error('Die E-Mail-Adresse ist erforderlich.');
  }
  if (!minutes || minutes <= 0) {
    throw new Error('Die Dauer in Minuten muss eine positive Zahl sein.');
  }

  var calendar;
  try {
    calendar = CalendarApp.getCalendarById(email);
    if (!calendar) {
      throw new Error('Kalender nicht gefunden oder Zugriff verweigert für: ' + email);
    }
  } catch (e) {
    console.error('Fehler beim Abrufen des Kalenders: ' + e.message);
    throw e;
  }

  const WORK_START_HOUR = 8;
  const WORK_END_HOUR = 17;
  const now = new Date();
  const twoWeeksFromNow = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
  const events = calendar.getEvents(now, twoWeeksFromNow);

  // 辅助函数:将日期调整为当天的工作开始/结束时间
  function getWorkStart(date) {
    const workStart = new Date(date);
    workStart.setHours(WORK_START_HOUR, 0, 0, 0);
    return workStart;
  }

  function getWorkEnd(date) {
    const workEnd = new Date(date);
    workEnd.setHours(WORK_END_HOUR, 0, 0, 0);
    return workEnd;
  }

  // 辅助函数:检查时段是否完全在工作时间内,且时长满足要求
  function isValidWindow(windowStart, windowEnd, requiredMinutes) {
    const requiredMs = requiredMinutes * 60 * 1000;
    // 窗口实际可用的起始(取windowStart和当天工作开始的较大值)
    const actualStart = new Date(Math.max(windowStart.getTime(), getWorkStart(windowStart).getTime()));
    // 窗口实际可用的结束(取windowEnd和当天工作结束的较小值)
    const actualEnd = new Date(Math.min(windowEnd.getTime(), getWorkEnd(windowStart).getTime()));
    
    // 如果实际可用时长足够,且起始不晚于结束
    return actualEnd - actualStart >= requiredMs && actualStart <= actualEnd;
  }

  // 辅助函数:获取全天事件覆盖的日期范围
  function getAllDayEventCoverage(event) {
    const start = event.getStartTime();
    const end = event.getEndTime();
    // 全天事件的结束日期通常是次日0点,所以覆盖的日期是start到end前一天
    const coverageStart = new Date(start.getFullYear(), start.getMonth(), start.getDate());
    const coverageEnd = new Date(end.getFullYear(), end.getMonth(), end.getDate() - 1);
    return { start: coverageStart, end: coverageEnd };
  }

  // 预处理事件:过滤并标记全天事件,排序(API返回已排序,但保险起见)
  const processedEvents = events.map(event => ({
    event: event,
    isAllDay: event.isAllDayEvent(),
    coverage: event.isAllDayEvent() ? getAllDayEventCoverage(event) : null,
    start: event.getStartTime(),
    end: event.getEndTime()
  })).sort((a, b) => a.start.getTime() - b.start.getTime());

  let currentCheckStart = now;

  for (let i = 0; i < processedEvents.length; i++) {
    const currentEvent = processedEvents[i];
    
    // 如果当前检查时段在全天事件覆盖范围内,直接跳到全天事件结束后的下一个工作日开始
    if (currentEvent.isAllDay) {
      const nextWorkStart = getWorkStart(new Date(currentEvent.coverage.end.getTime() + 24 * 60 * 60 * 1000));
      currentCheckStart = nextWorkStart;
      continue;
    }

    // 检查当前检查起始到当前事件开始之间的空闲窗口
    if (currentCheckStart < currentEvent.start) {
      if (isValidWindow(currentCheckStart, currentEvent.start, minutes)) {
        const requiredMs = minutes * 60 * 1000;
        const actualStart = new Date(Math.max(currentCheckStart.getTime(), getWorkStart(currentCheckStart).getTime()));
        return {
          start: actualStart,
          end: new Date(actualStart.getTime() + requiredMs)
        };
      } else {
        // 如果当前窗口不够,跳到当前事件结束后,继续检查
        currentCheckStart = currentEvent.end;
      }
    } else {
      // 当前检查起始晚于事件开始,直接跳到事件结束
      currentCheckStart = currentEvent.end;
    }
  }

  // 检查最后一个事件结束到两周后之间的空闲窗口
  if (currentCheckStart < twoWeeksFromNow) {
    if (isValidWindow(currentCheckStart, twoWeeksFromNow, minutes)) {
      const requiredMs = minutes * 60 * 1000;
      const actualStart = new Date(Math.max(currentCheckStart.getTime(), getWorkStart(currentCheckStart).getTime()));
      return {
        start: actualStart,
        end: new Date(actualStart.getTime() + requiredMs)
      };
    }
  }

  // 遍历所有工作日,检查是否有完整的空闲工作日(避免事件之间无窗口,但全天有空的情况)
  const tempDate = new Date(now);
  while (tempDate <= twoWeeksFromNow) {
    const workStart = getWorkStart(tempDate);
    const workEnd = getWorkEnd(tempDate);
    
    // 如果当天工作时间在查询范围内,且没有事件覆盖
    if (workStart >= now && workEnd <= twoWeeksFromNow) {
      const hasEventsOnDay = processedEvents.some(event => {
        if (event.isAllDay) {
          return event.coverage.start <= tempDate && event.coverage.end >= tempDate;
        } else {
          return (event.start >= workStart && event.start < workEnd) || (event.end > workStart && event.end <= workEnd);
        }
      });
      
      if (!hasEventsOnDay && (workEnd - workStart) >= minutes * 60 * 1000) {
        return {
          start: workStart,
          end: new Date(workStart.getTime() + minutes * 60 * 1000)
        };
      }
    }
    
    // 跳到下一天
    tempDate.setDate(tempDate.getDate() + 1);
  }

  return null;
}

function doGet(e) {
  console.log("Received parameters:", e.parameters);

  try {
    var email = e.parameter['Bearbeiter-Mail'];
    var minutes = parseInt(e.parameter['Dauer-Minuten'], 10);

    if (!email) {
      throw new Error('Die E-Mail-Adresse ist erforderlich.');
    }
    if (isNaN(minutes) || minutes <= 0) {
      throw new Error('Die Dauer in Minuten muss eine positive Zahl sein.');
    }

    var availability = getAvailability(email, minutes);

    if (availability) {
      console.log("Verfügbarkeit gefunden:", availability);
      return ContentService.createTextOutput(JSON.stringify(availability))
        .setMimeType(ContentService.MimeType.JSON);
    } else {
      console.log("Keine Verfügbarkeit gefunden.");
      return ContentService.createTextOutput('Keine Verfügbarkeit gefunden')
        .setMimeType(ContentService.MimeType.TEXT);
    }
  } catch (error) {
    console.error("Error:", error);
    return ContentService.createTextOutput(JSON.stringify({ 'error': error.message }))
      .setMimeType(ContentService.MimeType.JSON);
  }
}

关键修复说明

  • 全天事件处理:通过辅助函数标记全天事件覆盖的完整日期,直接跳过这些日期的所有时段,避免错误计算空闲窗口
  • 工作时间校验:新增isValidWindow函数,确保返回的空闲时段完全落在8:00-17:00范围内,自动截断超出工作时间的部分
  • 全场景空闲检查:补充了「当前时间到第一个事件」「最后一个事件到两周后」「无事件的完整工作日」三个场景的检查,避免遗漏可用时段
  • 参数合法性增强:新增会议时长必须为正数的校验,避免无效请求
  • 日期边界处理:通过辅助函数getWorkStart和getWorkEnd统一处理工作日的时间边界,避免手动计算出错

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 20:45:00