Next.js生产环境Google Calendar API时区偏移致预约冲突错误
解决Google Calendar API时间偏移4小时的实操方案
问题根源
- Vercel生产服务器默认使用UTC时区,而本地开发环境是UTC+4时区,时间处理时未统一时区标准,要么把API返回的UTC时间当成了本地时区,要么本地时间转UTC时未对齐,直接差出4小时。
- 使用date-fns指定时区但未覆盖全流程:数据库取时间、API请求传参、返回结果解析,只要某一步没按统一时区处理,冲突校验就会失效。
分步解决
1. 给Google Calendar API明确指定时区
调用freebusy.query接口时,必须同时完成两件事:
- 将请求的
timeMin/timeMax转成UTC后传参 - 给API指定
timeZone参数(和业务使用的时区一致,比如"Asia/Shanghai"),让API返回对应时区的时间,或返回带时区标识的UTC字符串
修正后的服务器端代码示例:
// 服务器端处理函数 import { format, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'; // 固定业务时区,建议存入环境变量而非硬编码 const BUSINESS_TIMEZONE = 'Asia/Shanghai'; export async function filterConflictingSlots(availableSlots) { // 把当前时间和7天后的时间转成UTC,作为API请求的时间范围 const timeMin = zonedTimeToUtc(new Date(), BUSINESS_TIMEZONE); const timeMax = zonedTimeToUtc(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), BUSINESS_TIMEZONE); const calendarResponse = await google.calendar('v3').freebusy.query({ auth: yourCalendarAuth, requestBody: { timeMin: timeMin.toISOString(), timeMax: timeMax.toISOString(), timeZone: BUSINESS_TIMEZONE, // 关键:告诉API返回对应时区的时间 items: [{ id: 'primary' }] } }); // 把API返回的忙碌时段转成业务时区的Date对象 const busySlots = calendarResponse.data.calendars.primary.busy.map(busy => ({ start: utcToZonedTime(new Date(busy.start), BUSINESS_TIMEZONE), end: utcToZonedTime(new Date(busy.end), BUSINESS_TIMEZONE) })); // 过滤冲突:只要时段有重叠就排除 return availableSlots.filter(slot => { const slotStart = utcToZonedTime(new Date(slot.start), BUSINESS_TIMEZONE); const slotEnd = utcToZonedTime(new Date(slot.end), BUSINESS_TIMEZONE); return !busySlots.some(busy => slotStart < busy.end && slotEnd > busy.start); }); }
2. 数据库时间存读要统一
- 数据库里的可用时段建议存储UTC时间(比如PostgreSQL用
TIMESTAMPTZ类型),不要存本地时区的时间,避免环境差异导致的偏差。 - 读取数据库时间时,统一转成业务时区再处理,不要直接用
new Date()自动转换。
3. 客户端时间处理对齐服务器
客户端展示或校验时间时,必须和服务器使用同一个时区转换逻辑,不要依赖浏览器本地时区:
// 客户端组件示例 import { format, utcToZonedTime } from 'date-fns-tz'; const BUSINESS_TIMEZONE = 'Asia/Shanghai'; export default function AvailableSlots({ slots }) { return ( <div className="slots-container"> {slots.map(slot => ( <div key={slot.id} className="slot-item"> {/* 把UTC时间转成业务时区后再格式化展示 */} {format(utcToZonedTime(new Date(slot.start), BUSINESS_TIMEZONE), 'yyyy-MM-dd HH:mm', { timeZone: BUSINESS_TIMEZONE })} - {format(utcToZonedTime(new Date(slot.end), BUSINESS_TIMEZONE), 'yyyy-MM-dd HH:mm', { timeZone: BUSINESS_TIMEZONE })} </div> ))} </div> ); }
4. 给Vercel服务器指定时区
在Vercel项目的环境变量中添加TZ,值设为你的业务时区(比如Asia/Shanghai),确保服务器运行时使用指定时区,避免默认UTC导致的时间计算偏差。
验证方式
- 在生产环境日志里同时打印UTC时间和业务时区时间,对比确认是否对齐:
console.log('UTC时间:', new Date().toISOString()); console.log('业务时区时间:', format(utcToZonedTime(new Date(), BUSINESS_TIMEZONE), 'yyyy-MM-dd HH:mm', { timeZone: BUSINESS_TIMEZONE }));
- 检查Google Calendar API返回的
busy时段,看start/end是否带时区标识(比如2024-05-20T10:00:00+08:00),如果是纯UTC字符串(2024-05-20T02:00:00Z),必须手动转成业务时区再处理。
内容的提问来源于stack exchange,提问作者Cip
相关产品推荐
相关产品推荐

