如何通过Apps Script Calendar API直接获取Out of Office事件并统计休假天数(含单日/多日事件处理)
解决Google Calendar API捕获单日Out of Office事件及多日事件统计问题
我来帮你搞定这个问题,你的代码主要卡在两个关键点:单日全天的Out of Office事件没被API正确识别,以及多日事件的跨月天数统计不准确。下面是具体的解决方案和修正后的完整代码:
核心问题分析
- 单日事件丢失的原因:Google Calendar API对全天事件和定时事件的时间字段存储不同——全天的Out of Office事件用
start.date和end.date(格式为YYYY-MM-DD),而你的原代码只处理了dateTime字段,直接漏掉了这类单日休假。 - 多日事件统计误差:如果休假事件跨月,直接用整个事件的总天数会把非当月的部分也算进去,必须计算事件与当月时间范围的交集天数,再筛选其中的工作日。
修正后的完整代码
const calculateMonthlyLeaveDays = () => { const WEEK_DAYS = [1, 2, 3, 4, 5]; // 周一到周五对应getDay()的返回值 // 计算两个日期之间的工作日数量(包含起止日) const countWeekDaysInRange = (startDate, endDate) => { let count = 0; const current = new Date(startDate); while (current <= endDate) { if (WEEK_DAYS.includes(current.getDay())) { count++; } current.setDate(current.getDate() + 1); } return count; }; // 解析Calendar API返回的事件时间(兼容全天/定时事件) const parseEventDate = (dateObj) => { if (dateObj.date) { // 全天事件:date字段是YYYY-MM-DD,转成当月第一天0点 return new Date(dateObj.date); } else { // 定时事件:直接用dateTime转成Date对象 return new Date(dateObj.dateTime); } }; // 获取当月起止时间 const now = new Date(); const currentYear = now.getFullYear(); const currentMonth = now.getMonth(); const monthStart = new Date(currentYear, currentMonth, 1); const monthEnd = new Date(currentYear, currentMonth + 1, 0, 23, 59, 59); // 当月最后一天的23:59:59 // 1. 获取当月所有Out of Office事件(包含全天和定时) const oooEvents = Calendar.Events.list('primary', { timeMin: monthStart.toISOString(), timeMax: monthEnd.toISOString(), singleEvents: true, // 展开重复事件 eventTypes: 'outOfOffice' // 直接筛选Out of Office类型 }).items.map(event => { return { start: parseEventDate(event.start), end: parseEventDate(event.end) }; }); // 2. 计算每个Out of Office事件在当月内的工作日天数 const totalOoODays = oooEvents.reduce((total, event) => { // 计算事件与当月的时间交集 const eventStart = event.start; const eventEnd = event.end; // 交集的开始是事件开始和当月开始的较大值 const overlapStart = new Date(Math.max(eventStart.getTime(), monthStart.getTime())); // 交集的结束是事件结束和当月结束的较小值(注意全天事件的end是次日,所以要减一天) const overlapEnd = new Date(Math.min(eventEnd.getTime() - 86400000, monthEnd.getTime())); if (overlapStart > overlapEnd) return total; // 无交集,跳过 return total + countWeekDaysInRange(overlapStart, overlapEnd); }, 0); // 3. 获取当月法定节假日(法国)的工作日数量 const holidayCalendar = CalendarApp.getCalendarsByName('Holidays in France')[0]; const totalHolidays = holidayCalendar.getEvents(monthStart, monthEnd) .filter(event => WEEK_DAYS.includes(event.getStartTime().getDay())) .length; // 4. 计算当月工作日总数 const totalWorkDays = countWeekDaysInRange(monthStart, monthEnd); // 生成邮件内容 const message = ` ${totalWorkDays} working days from ${monthStart.toLocaleDateString()} to ${monthEnd.toLocaleDateString()} - ${totalOoODays} out of office days - ${totalHolidays} bank holidays Total leave days: ${totalOoODays + totalHolidays} `; // 发送邮件(替换成你的邮箱) MailApp.sendEmail('your.email@provider.com', 'Monthly Leave Summary', message); Logger.log(message); };
关键优化点说明
- 兼容全天/定时事件:新增
parseEventDate函数,自动识别事件的时间字段类型,不会漏掉单日全天的Out of Office。 - 精确计算当月有效天数:通过计算事件与当月的时间交集,避免跨月事件的天数统计错误,同时只统计工作日。
- 移除#dayoff依赖:直接通过API的
eventTypes: 'outOfOffice'参数筛选休假事件,无需手动加标签。 - 展开重复事件:调用API时添加
singleEvents: true,确保重复的Out of Office事件也能被正确统计。
分享给同事的步骤
- 让同事打开Google Apps Script编辑器,新建项目。
- 将上述代码粘贴进去,替换邮件地址为他们自己的邮箱。
- 启用高级Calendar API:点击编辑器菜单「资源」→「高级Google服务」,找到「Calendar API」并开启。
- 设置定时触发器:点击编辑器菜单「编辑」→「当前项目的触发器」,添加一个每月触发的时间驱动触发器,选择每月最后一天执行。
内容的提问来源于stack exchange,提问作者Michel Hua
相关产品推荐
相关产品推荐

