打开网易新闻 查看精彩图片

2026年3月8日,美国切换到夏令时。3月29日,欧洲才跟进。中间这21天,纽约和伦敦的时差从5小时变成4小时——如果你的应用硬编码了时区偏移量,它现在正在给用户提供错误时间。

这不是理论问题。开发者 Patrick McKenzie 在3月24日写下这篇指南时,特意标注了「实时相关性」:我们就活在这个 DST 缺口窗口里。

时区地狱的常住人口,是每个 ship 过日程功能的开发者。

第一步:你的服务器不讲人话

第一步:你的服务器不讲人话

你住在布拉格,熟悉中欧时间。这对你后端毫无意义。

服务器不关心布拉格。服务器只说 UTC(协调世界时)。GMT 和 UTC 共享 +00:00 偏移,但 GMT 在某些边缘情况会观测夏令时,且基于天文观测。UTC 是原子钟精度、无夏令时。永远存 UTC,别争论这个。

Kotlin 代码里,`LocalDateTime.now()` 是个陷阱——谁的 now?你的 now?服务器的 now?东京的 now?

正确写法是 `Instant.now()`。通用、无歧义、无聊得恰到好处。

第二步:事件发生在哪,时间就属于哪

第二步:事件发生在哪,时间就属于哪

你的应用关心东京证券交易所9:00开盘。那是东京的钟,不是你的。

先获取事件所在时区的标准时间,再转成 UTC 作为锚点。比如东京时间2026年3月24日9:00,对应 UTC 是3月24日0:00。这个 Z 结尾的时间戳,才是你能真正计算的 ground truth。

计算倒计时很简单:用 `Duration.between(Instant.now(), tokyoOpenUtc)`。日本不观测夏令时,幸运。但你的用户可能住在观测夏令时的地方。

陷阱来了:倒计时本身是对的(比较的是 Instant 值,底层是 UTC),但客户端显示的本地时间可能因季节偏移 ±1 小时。

Prague 冬季显示 CET(UTC+1),夏季显示 CEST(UTC+2)。用户看到的时间变了,但 UTC 锚点没变——这是对的,只是容易让人困惑。

第三步:客户端不是翻译器,是解释器

第三步:客户端不是翻译器,是解释器

客户端拿到 UTC 时间戳,要转成用户本地时间。TypeScript 里 `toLocaleString` 配合 `timeZone` 选项,能强制显示特定时区的时间。

关键区分:用户「当前所在时区」和「事件所在时区」。航班应用尤其危险——起飞用出发地时间,降落用目的地时间,中间飞行时长用 UTC 计算。

如果全用本地时间算,跨夏令时边界的航班会「少飞一小时」或「多飞一小时」。实际飞行时间没变,你的显示错了。

第四步:测试用真实数据,别用假时间

第四步:测试用真实数据,别用假时间

不要用 `Date.now()` 做测试基准。时区规则会变(美国2007年把夏令时提前到3月),政府会临时取消(俄罗斯2011年永久夏令时,2014年又改回来)。

用 IANA 时区数据库(`ZoneId` 背后那个),别自己维护偏移量表。数据库更新时,你的应用自动跟进。

单元测试要覆盖 DST 切换窗口:切换前24小时、切换瞬间、切换后24小时。边界条件才是 bug 温床。

McKenzie 的指南没提具体框架,但强调了产品经理该懂的事:时间显示是信任问题。用户看到「会议在2小时后」,结果因为夏令时实际只剩1小时,他们不会骂操作系统,会骂你的应用。

2026年的这个 DST 缺口窗口还剩5天。你的代码经得起29号欧洲切换时的二次验证吗?