Google Calendar员工可用时间查询代码修复需求
代码修复建议:Google Calendar 可用时段查询功能
原代码核心问题
- 仅跳过当前全天事件,未处理全天事件占据的完整日期,导致错误计算空闲窗口
- 未校验空闲时段是否完全落在8:00-17:00的工作时间范围内
- 遗漏了「当前时间到第一个事件开始」的空闲窗口检查
- 未处理空闲窗口跨天的情况,以及工作时间边界的截断
修复后的完整代码
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
相关产品推荐
相关产品推荐

