Cal.diy 日历 API v2 深度指南:连接日历、查询忙碌时间与可用性调度
2026/9/9 19:50:33 网站建设 项目流程

Cal.diy 日历 API v2 深度指南:连接日历、查询忙碌时间与可用性调度

【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy

本文以 Calendars API Reference 为骨架,结合 Cal.diy(开源版 Cal.com 调度平台)仓库内apps/api/v2的实际控制器、服务与端到端测试源码进行校验与扩充,帮助你掌握「列出已连接日历、查询忙碌时间(busy times)、管理连接/断开/ICS Feed、设置目标日历与选中日历」这组调度基础设施中最核心的日历集成能力。读完你将能:看懂 v2 日历相关端点的真实路由与参数形态,正确发起 OAuth 连接流程,理解 busy times 如何参与可用性计算,并能用 Webhook 与连接池化等方式写出更稳的生产级集成代码。

一、端点总览:文档描述与仓库实现的差异

参考文档给出的端点为总览如下:

MethodEndpointDescription
GET/v2/calendarsList connected calendars
GET/v2/calendars/busy-timesGet busy times
GET/v2/calendars/{calendar}/checkCheck calendar connection
POST/v2/calendars/{calendar}/connectConnect a calendar
DELETE/v2/calendars/{calendar}/disconnectDisconnect a calendar
GET/v2/calendars/{calendar}/credentialsGet calendar credentials
GET/v2/destination-calendarsList destination calendars
GET/v2/selected-calendarsList selected calendars

对照仓库源码(calendars.controller.ts、destination-calendars.controller.ts、selected-calendars.controller.ts),当前实现中端点存在若干差异,下文均以仓库代码为准

  • connect实际是GET /v2/calendars/:calendar/connect,返回授权 URL 而非直接 POST;OAuth 回调走GET /v2/calendars/:calendar/save
  • disconnect实际是POST /v2/calendars/:calendar/disconnect,请求体携带credentialId
  • credentials路由为POST /v2/calendars/:calendar/credentials,当前仅对 Apple 日历生效(提交username/password)。
  • destination-calendars控制器当前仅暴露PUT /v2/destination-calendars(更新),列表信息通过GET /v2/calendars响应体中的destinationCalendar一并返回。
  • selected-calendars控制器当前暴露POST /v2/selected-calendars(新增)与DELETE /v2/selected-calendars(删除,Query 传credentialId/integration/externalId)。
  • busy-times 的真实查询参数为timeZone(或已废弃的loggedInUsersTz)+dateFrom/dateTo+calendarsToLoad数组,而不是参考文档示例中的startTime/endTime/credentialId单值形态(细节见第三节)。

以 e2e 测试为证(calendars.controller.e2e-spec.ts)覆盖了/v2/calendars/office365/connect/v2/calendars/google/check/v2/calendars/busy-times/v2/calendars等路由;selected-calendars.controller.e2e-spec.ts 与 destination-calendars.controller.e2e-spec.ts 则验证了POST /v2/selected-calendarsDELETE /v2/selected-calendarsPUT /v2/destination-calendars的读写行为。

路径参数{calendar}的取值不是人读的 slug(google-calendar),而是平台常量中的googleoffice365apple(见 platform/constants/apps.ts),ICS Feed 则是固定路由ics-feed。这些值下文统称「日历类型标识」。

二、列出已连接日历:GET /v2/calendars

GET /v2/calendars

请求头需要携带 API Key 或 Access Token(控制器上统一使用API_KEY_OR_ACCESS_TOKEN_HEADERApiAuthGuard,参考 calendars.controller.ts)。

响应结构

{ "status": "success", "data": { "connectedCalendars": [ { "integration": { "type": "google_calendar", "title": "Google Calendar", "slug": "google-calendar" }, "credentialId": 123, "primary": { "externalId": "primary", "name": "john@gmail.com", "email": "john@gmail.com", "isSelected": true, "readOnly": false }, "calendars": [ { "externalId": "primary", "name": "john@gmail.com", "email": "john@gmail.com", "isSelected": true, "readOnly": false }, { "externalId": "calendar-id-2", "name": "Work Calendar", "email": "john@gmail.com", "isSelected": true, "readOnly": false } ] } ], "destinationCalendar": { "id": 1, "integration": "google_calendar", "externalId": "primary", "name": "john@gmail.com" } } }

字段含义与实现依据:

  • data.connectedCalendars:按凭据分组的已连接日历列表;每组含integration(App 元数据,type形如google_calendarslug形如google-calendar)、credentialIdprimary(该凭据下的主日历)与calendars(该凭据可用的全部日历)。
  • data.destinationCalendar:当前用户/事件类型级别的目标日历,即新建预约要写入的日历。见 connected-calendars.output.ts:响应数据被建模为connectedCalendars+destinationCalendar两部分,其中destinationCalendar还包含userIdeventTypeIdcredentialIddelegationCredentialIdprimaryEmail等可空字段。
  • 服务端由 calendars.service.ts 的getCalendars(userId)实现,内部先查缓存再回源数据库,并通过CalendarsCacheService写回缓存;也就是说该接口结果会被缓存,写入侧(如连接、断开、保存 ICS)都会主动清除缓存,保证一致性。

小贴士:响应 DTO 中的类型细节

ConnectedCalendarsOutputexternalId/name/email采用@IsEmail()校验(见 connected-calendars.output.ts),因此当第三方日历 externalId 不是邮箱形态时,接口输出的归一化规则应以实际返回为准——例如 Google 的externalIdprimary或日历资源 ID 时即为字符串原样。

三、忙碌时间查询:GET /v2/calendars/busy-times

忙碌时间接口负责把「已连接日历上用户不可用的时间段」拉取出来,是上层 slot 计算的核心输入。

真实 Query 参数(以仓库 DTO 为准)

定义见 busy-times.input.ts:

ParameterTypeRequiredDescription
timeZonestring是(与 loggedInUsersTz 二选一)查询忙碌时间所用的时区,例如America/New_York;会被normalizeTimezone规整
loggedInUsersTzstring否(已废弃)旧版时区参数,Deprecated: Use timeZone instead
dateFromstring查询起始日期,ISO 8601 date string,如2024-12-18
dateTostring查询结束日期,ISO 8601 date string,如2024-12-18
calendarsToLoadarray需要检查的日历集合,每项为{ credentialId: number, externalId: string }

类级别校验器@ValidateTimezoneRequired()保证timeZoneloggedInUsersTz必须提供其一,否则返回400,错误信息为"Either timeZone or loggedInUsersTz must be provided"loggedInUsersTz仅为向后兼容保留。

请求示例

GET /v2/calendars/busy-times?timeZone=America/New_York&dateFrom=2024-12-18&dateTo=2024-12-18&calendarsToLoad[0][credentialId]=135&calendarsToLoad[0][externalId]=john@gmail.com

控制器在读取时优先使用timeZone,取不到再回退loggedInUsersTz,随后把参数交给calendarsService.getBusyTimes()(见 calendars.controller.ts)。e2e 测试同时覆盖了「缺时区返回 400」「传 timeZone 成功」「传 loggedInUsersTz 向后兼容成功」三种场景(calendars.controller.e2e-spec.ts)。

响应结构

{ "status": "success", "data": [ { "start": "2024-01-15T10:00:00.000Z", "end": "2024-01-15T11:00:00.000Z", "source": "google_calendar" }, { "start": "2024-01-16T14:00:00.000Z", "end": "2024-01-16T15:00:00.000Z", "source": "google_calendar" } ] }

注意:响应的数据项定义在 busy-times.output.ts,官方 DTO 明确包含start(Date)、end(Date)与可空的source(string)。source为来源日历类型(如google_calendar);title等字段取决于底层 provider 返回值,不应作为稳定契约依赖。

服务端调用链

CalendarsService.getBusyTimes()(calendars.service.ts)的处理顺序:

  1. getUniqCalendarCredentials():去重calendarsToLoad中的credentialId,按用户拉取凭据,若数量对不上抛出UnauthorizedException("These credentials do not belong to you")防止越权查询他人的日历凭据
  2. getCalendarsWithCredentials():把integration = credential.type拼入待查日历,再交给getBusyCalendarTimes()(platform-libraries 提供)执行各 provider 的 busy/free 查询。
  3. 拿到EventBusyDate数组后,用 LuxonDateTimestart/end换算到请求时区再序列化返回。

四、连接生命周期:check / connect / save / disconnect / credentials

4.1 检查连接状态:GET /v2/calendars/{calendar}/check

GET /v2/calendars/google/check

路径参数{calendar}支持googleoffice365apple。该路由额外受PermissionsGuard保护并要求APPS_READ权限(见 calendars.controller.ts)。内部由各 provider service 分派:

  • Google →GoogleCalendarService.check(userId)(gcal.service.ts)
  • Office 365 →OutlookService.check(userId)
  • Apple →AppleCalendarService.check(userId)

响应形如{ "status": "success" };若未连接或凭据失效则抛出 4xx。文档最佳实践第 1 条「操作前先 check」正对应此端点。

4.2 发起 OAuth 连接:GET /v2/calendars/{calendar}/connect

注意:参考文档写作POST .../connect,但仓库实现是GET,且只支持googleoffice365

GET /v2/calendars/google/connect?redir=https://myapp.com/callback&isDryRun=false

可选 Query:

ParameterTypeDescription
redirstring授权成功后的回跳地址(需与 OAuth 应用白名单一致)
isDryRunboolean干跑模式,不真正换取凭据,直接返回授权链接

响应:

{ "status": "success", "data": { "authUrl": "https://accounts.google.com/o/oauth2/auth?..." } }

实现上GoogleCalendarService.connect()会用当前租户配置的api.url拼出 OAuth 回调地址{api.url}/gcal/oauth/save,再generateAuthUrl()生成带 scope、state(内含accessToken/origin/redir/isDryRun)的授权 URL(gcal.service.ts)。典型流程:前端拿到 authUrl 后跳转 → 用户在 Google/Microsoft 完成授权 → 平台回调/save→ 后端用code换取并持久化凭据 → 301 重定向回redir || origin

4.3 OAuth 回调落库:GET /v2/calendars/{calendar}/save

GET /v2/calendars/google/save?state=...&code=4/0AfJohXmB...

控制器会先尝试把state按 JSON 解析,失败则退化为 URLSearchParams 解析(字段accessTokenoriginredirisDryRun),校验后再分派到OutlookService.save()GoogleCalendarService.save(),最终以 301 跳回业务地址(见 calendars.controller.ts)。

4.4 断开连接:POST /v2/calendars/{calendar}/disconnect

当前实现是POST(参考文档写作 DELETE),请求体为要删除的凭据 ID:

POST /v2/calendars/google/disconnect Content-Type: application/json { "credentialId": 123 }

处理逻辑(calendars.controller.ts):先checkCalendarCredentials(credentialId, user.id)校验凭据归属,再调用CalendarsRepository.deleteCredentials()删除,并主动清除该用户的 connected/destination 缓存,最后返回被删除凭据的快照(idtypeuserIdteamIdappIdinvalid)。

4.5 Apple 日历凭据:POST /v2/calendars/{calendar}/credentials

Apple 不走 OAuth,而是提交应用专用密码:

POST /v2/calendars/apple/credentials Content-Type: application/json { "username": "john@icloud.com", "password": "xxxx-xxxx-xxxx-xxxx" }

仅支持apple,由AppleCalendarService.save()处理(calendars.controller.ts)。

五、受支持的日历类型与标识映射

参考文档的表格,并结合 platform/constants/apps.ts 修正后的映射关系如下:

日历路由{calendar}标识App 元数据 slug连接方式
Google Calendargooglegoogle-calendarOAuth(GET connect / save)
Microsoft 365 / Outlookoffice365office365-calendarOAuth(GET connect / save)
Apple Calendar(CalDAV/iCloud)appleapple-calendarPOST credentials(用户名+应用密码)
通用 CalDAV / ICS Feedics-feedics-feedPOST ics-feed/save

其中google_calendar/office365_calendar/apple_calendar是凭据的type(数据库侧),google-calendar等是 App 展示用 slug,三层标识在 apps.ts 中被集中维护,集成时务必区分使用场景,不要混用。

六、目标日历(Destination Calendar)与选中日历(Selected Calendars)

6.1 两者在可用性模型中的角色

用一句话概括(参考文档核心语义):

  1. Selected Calendars(选中日历):这些日历里的事件会阻塞可用时间(参与冲突检测)。
  2. Destination Calendar(目标日历):新预约被创建时写入的日历。
  3. Busy Times:API 聚合所有选中日历的忙碌时间,供可用 slot 计算排除。

6.2 更新目标日历:PUT /v2/destination-calendars

参考文档描述了「列表目标日历」端点,但当前仓库中该控制器只实现了更新GET /v2/calendars的响应已内含目标日历,列表场景通常无需单独查询):

PUT /v2/destination-calendars Content-Type: application/json { "integration": "google_calendar", "externalId": "primary" }

服务端(destination-calendars.service.ts)会从用户的已连接日历中反查该externalId对应的credentialId;同时支持可选的delegationCredentialId走委托凭据写入场景。若既无delegationCredentialId也反查不到credentialId,则直接报错。e2e 测试验证了更新成功(200)与非法日历类型(400)两种路径。

6.3 新增/删除选中日历

POST /v2/selected-calendars Content-Type: application/json { "integration": "google_calendar", "externalId": "work-calendar-id", "credentialId": 123 }
DELETE /v2/selected-calendars?credentialId=123&integration=google_calendar&externalId=work-calendar-id

两者的服务层(selected-calendars.service.ts)都会先checkCalendarCredentials(credentialId, user.id)做归属校验,再写/删SelectedCalendar记录。只有处于 selected 状态的日历才会进入 busy-times 冲突检测,这一点与第三节的calendarsToLoad语义配合:busy-times 接口允许显式指定任意已连接日历,而可用性引擎默认只关心 selected 集合。

七、ICS Feed 订阅日历

ICS Feed 是「只读订阅任意 .ics 地址」的轻量接入方式,不涉及 OAuth。

检查 ICS Feed

GET /v2/calendars/ics-feed/check

服务端(ics-feed.service.ts)依次检查:是否存在该用户的ics_feed类型凭据 → 凭据是否invalid→ 该凭据是否真的连出了ics_feed日历,任一环节失败都会抛出对应的 400/401,全部通过返回{ "status": "success" }

保存 ICS Feed

POST /v2/calendars/ics-feed/save Content-Type: application/json { "urls": ["https://calendar.example.com/feed.ics"], "readOnly": true }

请求体 DTO 见 create-ics.input.ts:

FieldTypeDefaultDescription
urlsstring[]必填ICS 地址数组;自定义校验器IsICSUrlConstraint要求协议为 http/https 且路径以.ics结尾
readOnlybooleantrue是否允许写回该日历(只读订阅建议保持默认 true)

保存逻辑(ics-feed.service.ts):用CALENDSO_ENCRYPTION_KEY对称加密 urls 后 upsert 凭据,并通过BuildIcsFeedCalendarService.listCalendars()预校验「订阅数量 == 实际可列出的日历数量」,不匹配即 400(提示改用私有 ICS Feed);成功后同样清除相关缓存。

八、日历事件读写(Unified Calendar Events)

/v2/calendars前缀下,仓库还有一个独立的Cal Unified Calendars控制器(cal-unified-calendars.controller.ts),提供事件级 CRUD 与 free/busy。参考文档中的事件端点在这里有对应实现:

8.1 获取单个事件(会议明细)

GET /v2/calendars/google/events/{eventUid}

返回会议明细,含idtitledescriptionstart/end(各带timetimeZone)、attendees(含responseStatus)与statussource

{ "status": "success", "data": { "id": "event-id-123", "title": "Meeting", "description": "Discussion", "start": { "time": "2024-01-15T10:00:00.000Z", "timeZone": "America/New_York" }, "end": { "time": "2024-01-15T11:00:00.000Z", "timeZone": "America/New_York" }, "attendees": [ { "email": "attendee@example.com", "name": "Attendee Name", "responseStatus": "accepted" } ], "status": "accepted", "source": "google" } }

eventUid即 Google Calendar 事件 ID,可通过GET /v2/bookings/{bookingUid}/references从预约的 bookingReference 中取得。历史单数路径/:calendar/event/:eventUid已被标记废弃,请使用复数/events/;更多事件级端点包括:

  • GET /v2/calendars/{calendar}/events?from=...&to=...&timeZone=...&calendarId=...— 列表事件(仅 Google 日历当前支持)
  • POST /v2/calendars/{calendar}/events— 创建事件
  • PATCH /v2/calendars/{calendar}/events/{eventUid}— 更新事件
  • DELETE /v2/calendars/{calendar}/events/{eventUid}— 删除/取消事件(返回 204)
  • GET /v2/calendars/{calendar}/freebusy?from=...&to=...— free/busy 聚合

该控制器同时提供 connection-scoped 变体:GET /v2/calendars/connections(列出连接,返回connectionId/type/email)以及connections/:connectionId/eventsfreebusy等(cal-unified-calendars.controller.ts)。注意控制器说明:事件 CRUD 目前仅对 Google Calendar 连接生效,其它类型会返回 400

九、日历集成原理:可用性与同步流

9.1 日历如何影响可用性

参考文档总结的三条因果,与仓库实现完全对得上:

  1. 选中日历上的既有事件通过 busy-times 阻塞候选 slot;
  2. 目标日历决定新预约落到哪里;
  3. busy-times 结果由调度器汇总后从可用时间段中剔除(底层 slot 计算位于 apps/api/v2/src/lib/services/available-slots.service.ts 与getBusyCalendarTimes的调用链中)。

9.2 完整同步流程

1. 用户连接日历(OAuth / 凭据) GET /v2/calendars/google/connect → 获得 authUrl GET /v2/calendars/google/save ← OAuth 回调,落库凭据 2. 用户勾选参与冲突检测的日历(selected calendars) POST /v2/selected-calendars DELETE /v2/selected-calendars 3. 用户设定新预约写入的目标日历 PUT /v2/destination-calendars 4. 查询可预约 slot 时: - API 从所有 selected calendars 拉取 busy times - busy times 从可用 slot 中排除 5. 预约创建时: - 事件写入目标日历(destination calendar) - 系统向与会者发送确认邮件

9.3 Cal.diy 事件识别

Cal.diy 写入外部日历的事件可以通过其iCalUID后缀识别(形如2GBXSdEixretciJfKVmYN8@Cal.diy)。在你的事件同步/去重逻辑中,可用这一标识区分「本平台创建的事件」与「用户手动创建的事件」,避免把平台预约当作普通事件重复处理或误删。

十、团队场景与进阶

参考文档在团队日历集成下列出了会议(conferencing)类端点,可用于团队级管理入口的编排:

GET /v2/organizations/{orgId}/teams/{teamId}/conferencing POST /v2/organizations/{orgId}/teams/{teamId}/conferencing/{app}/connect

注意:conferencing 管理的是视频会议应用而非日历写入本身;团队级日历凭据管理与凭据所有权校验的实现细节可进一步查阅CalendarsRepository/CredentialsRepository以及团队 API 路由树,确认其响应结构与个人日历一致后再接入。

十一、生产级集成最佳实践

在参考文档 5 条最佳实践基础上,结合仓库代码补充工程化要点:

  1. 先检查连接再操作:连接/读写事件前调用GET /v2/calendars/{calendar}/check;对应实现会区分「无凭据」「凭据 invalid」「未连接」等错误态,便于给出精确提示。
  2. 处理 OAuth 过期与刷新:日历 token 会过期,需捕获 401 类错误并触发重新授权(重新走 connect/save 或让用户在 Cal.diy 前端重连)。Google/OAuth 客户端构造见 gcal.service.ts。
  3. 尊重 provider 限流:日历供应商(Google/Outlook)各自有速率配额,聚合查询会放大请求量;busy-times 聚合请批量、低频调用。
  4. 合理缓存 busy timesGET /v2/calendars本身已被 CalendarsCacheService 缓存,业务侧对 busy times 也应按需缓存——忙碌信息变化频率远低于查询频率;但记住删除凭据/保存 ICS 等写操作会触发缓存失效,缓存策略要与失效时机对齐。
  5. 优先 Webhook 而非轮询:订阅预约/事件变更 Webhook,替代对日历反复轮询;这能同时降低 provider 限流风险与自身 API 负载。
  6. 参数以契约为准:busy-times 的calendarsToLoad是数组且必须携带timeZone/dateFrom/dateTo,参考文档中的startTime/endTime写法不会生效,接入前对照 busy-times.input.ts 与 e2e 用例修正。
  7. 凭据归属校验是安全底线:新增/删除 selected calendar、busy-times 查询、disconnect 都内含checkCalendarCredentials/凭据数量比对,任何绕过校验的「替他查忙、删凭据」设计都不被 API 允许。

十二、小结与速查

能力端点备注
已连接日历 + 目标日历GET /v2/calendars结果含connectedCalendarsdestinationCalendar,服务端缓存
忙碌时间GET /v2/calendars/busy-times参数timeZone/dateFrom/dateTo/calendarsToLoad
连接检查GET /v2/calendars/{google\|office365\|apple}/checkAPPS_READ权限
OAuth 连接GET /v2/calendars/{google\|office365}/connect/saveGET 发起,返回authUrl
断开POST /v2/calendars/{calendar}/disconnectbody:credentialId
Apple 凭据POST /v2/calendars/apple/credentialsbody:username/password
ICS FeedPOST /v2/calendars/ics-feed/saveGET /v2/calendars/ics-feed/checkurls 必须以.ics结尾
选中日历POST/DELETE /v2/selected-calendars参与冲突检测
目标日历PUT /v2/destination-calendars写入新预约
事件读写/v2/calendars/{calendar}/events.../v2/calendars/connections/{connectionId}/events...目前仅 Google 日历支持 CRUD

以参考文档为主线、以 apps/api/v2 源码为佐证的这组日历 API,是 Cal.diy 调度引擎与用户真实日程之间的桥梁。理解「selected 阻塞、destination 落库、busy-times 聚合、凭据归属校验」这一闭环,你就掌握了在任何日历驱动的调度产品中安全、正确地做可用性集成的核心方法论。

【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询