CalDAV 本身不在协议层“转换时区”,而是把“时区语义”交给 iCalendar 格式 + IANA 时区数据库来承载,同步过程只搬 TZID 引用 + 本地时间(或 UTC 绝对时间),各端按同一套 IANA 规则独立展开。核心原则是:存储原意,按引用解析,显示时转换,绝不在同步中静默平移墙钟时间。
一、iCalendar 里三种时间写法,决定同步语义
DTSTART/DTEND 的值形态直接决定跨时区行为(RFC 5545):
- UTC 绝对时间:末尾带
Z,如20260901T010000Z
含义是“格林威治那一瞬”,全球任何时区看到的是同一物理时刻,只是显示不同。无 TZID、不需要 VTIMEZONE。 - 带时区的本地时间:
TZID=America/New_York:20260831T210000
意思是“纽约墙钟 21:00”,由 TZID 指向的 VTIMEZONE/IANA 规则展开成 UTC。换到东京客户端,会算成东京时间显示,但物理时刻不变。 - 浮动时间(floating):
20260831T210000(无 Z 无 TZID)
不绑定任何时区,按“观察者所在时区”解释。跨时区会漂移,共享日程应避免使用。
CalDAV 同步时,服务器通常原样存客户端 PUT 进来的文本,不重写 DTSTART 形态(除非服务端做 TZID 规范化映射)。
二、TZID 怎么对齐:VTIMEZONE 内联 vs RFC 7809 按引用
早期做法(RFC 5545 强制):每个引用了 TZID 的 .ics 文件,必须把对应的 VTIMEZONE 组件内联进同一个 VCALENDAR。这样接收方就算自己 IANA 库旧也能展开。
问题:一个 VTIMEZONE(含历史夏令时规则)往往比事件本身还大,移动端同步浪费带宽。
RFC 7809(CalDAV Time Zones by Reference)的改进:
- 客户端/服务端都信任 IANA tz 数据库(如
Asia/Shanghai、America/New_York) - 同步时只传
TZID=America/New_York,不传 VTIMEZONE 本体 - 通过
CalDAV-Timezones: F请求头告诉服务端“别给我塞 VTIMEZONE”;服务端在DAV能力里声明calendar-no-timezone,并在timezone-service-set属性里告诉客户端去哪个 tzdist 服务拉定义 - 客户端本地 IANA 库过期导致展开不一致时,才按 RFC 7808 去 tz 分发服务更新
现实:iOS/macOS、Fastmail、Nextcloud(部分)、Radicale 均支持 7809 的“省略标准时区”模式;老客户端(某些 Android 第三方)仍要求内联 VTIMEZONE,此时服务端会按
CalDAV-Timezones: T补回。
三、服务端在同步里到底管什么时区事
服务端不负责把事件“转成某时区”,只在两种场景用时区:
- 时间窗过滤(
calendar-query的time-range)
客户端发REPORT查“2026-09 的事件”,服务端要把带 TZID 的本地时间、浮动时间都归到 UTC 再比窗口。
归法优先级(RFC 4791 §7.3):- 请求体里带
<C:timezone>(内联 VTIMEZONE)或<C:timezone-id>(RFC 7809)→ 用这个 - 否则用日历集合属性
CALDAV:calendar-timezone/calendar-timezone-id - 再没有 → 服务端自选(通常 UTC 或服务器本地)
所以浮动时间事件在哪个时区被过滤,取决于上述默认值——这也是为什么跨时区共享别用浮动时间。
- 请求体里带
- 忙闲查询(
free-busy-query)
同样依赖calendar-timezone把 DATE/DATE-TIME 浮动值展开成 UTC 段。
除此之外,PUT/GET/ETag 同步过程中服务端不改动 DTSTART 的时区表达,保证“原意不动”。
四、多客户端跨时区同步的正确链路
以“北京用户建会议、纽约同事接收”为例:
- 北京客户端(IANA
Asia/Shanghai)建事件
DTSTART;TZID=Asia/Shanghai:20260901T090000+ 内联/引用Asia/ShanghaiVTIMEZONE
PUT 到 CalDAV 服务器,存原文。 - 服务器存盘,ETag 变化,CTag/sync-token 推进。不转 UTC、不转纽约时间。
- 纽约客户端同步拉到同一份
.ics:- 若支持 7809 且服务端没发 VTIMEZONE → 用本地 IANA 库查
Asia/Shanghai的 2026-09-01 偏移(+08:00),算出 UTC20260901T010000Z,再按自己America/New_York(-04:00 DST)显示成 8 月 31 日 21:00。 - 若本地 IANA 旧(比如没装 2026 夏令时修正)→ 可能差一小时,这就是“时区库过期”bug 根源,需定期更新 tzdata。
- 若支持 7809 且服务端没发 VTIMEZONE → 用本地 IANA 库查
- 纽约客户端改了 SUMMARY 后 PUT 回去,仍带原
TZID=Asia/Shanghai,北京端再拉到,显示仍是自己时区的 09:00。物理时刻全程没变,只是墙钟随观察者变。
五、递归事件(RRULE)的时区坑
FREQ=WEEKLY;BYDAY=TU 配 TZID=America/New_York 的 09:00 会议:
- 展开每一周实例时,必须按该实例所属日期的纽约偏移算(冬令时 -05:00、夏令时 -04:00)。
- 所以 UTC 时间会随季节跳变(冬令 14:00Z、夏令 13:00Z),但纽约墙钟恒为 09:00。
- 服务端若做 RRULE 展开(少数服务端如 Radicale 不展开、客户端展开;Nextcloud 某些版本展开),必须用同一 IANA 版本,否则两端实例数/边界不一致。
六、常见时区同步故障与根因
- 会议差一小时:某端 IANA tzdata 旧(尤其 Android 厂商不更基带时区)、或用了
EST这种歧义缩写当 TZID(应改用America/New_York)。 - 跨时区后浮动事件漂移:原事件没 TZID,北京建 21:00,纽约看也是 21:00 但含义变了——共享日程禁用浮动时间。
- VTIMEZONE 膨胀:老 Mac 日历把历年所有 VTIMEZONE 都塞进一个 .ics,可借 7809 协商省略。
- Google CalDAV 接口:对 VTODO、TZID 支持残缺,部分客户端会改写成 UTC 绝对时间存,导致原时区上下文丢失(CalConnect 明确不推荐“全存 UTC 丢原时区”)。
- 服务端拒绝未知 TZID:RFC 7809 下服务端可回
CALDAV:valid-timezone预条件错误,客户端需改发内联 VTIMEZONE 或换标准 IANA ID。
七、工程落地建议(自托管/开发 CalDAV 应用时)
- 客户端:建事件优先用
TZID=IANA名称而非 UTC(保留“原时区意图”,方便跨国同事看墙钟);定期随系统更新 IANA tz 库;支持 7809 时发CalDAV-Timezones: F。 - 服务端:存原文不重写时区;
calendar-timezone-id默认设成服务器/用户偏好 IANA ID 供浮动时间兜底;对未知 TZID 要么保留原 VTIMEZONE 要么映射标准 IANA,别静默丢时区;RRULE 展开绑定服务端 tzdata 版本号。 - 调试:比对两端同一事件的
DTSTART原文 + ETag,若原文没变但显示不同 → 必有一端 IANA 库旧。