事故现场的经典台词
「服务器明明返回了 14:30,换个用户它显示的却是 06:30」「跨夏令时那周所有的到期时间都提前了一小时」——这两句吐槽足以说明:时间的问题从来不是『几点』,而是『绝对时刻』与『本地表示』的割裂。
先从最不可动摇的一条开始。
一切从 UTC 出发:绝对时刻
地球任何瞬间在宇宙中是唯一的。我们用 Unix 时间戳(自 1970-01-01 00:00:00 UTC 起的秒数/毫秒数)给它一个与位置无关的数字,或用带偏移的 ISO8601 给它一个自我说明的字符串:
时间戳 1785891600000 (毫秒)
ISO8601 2026-09-04T09:00:00Z (UTC)
带偏移 2026-09-04T17:00:00+08:00 (北京 17 点)
Z 表示 UTC,+08:00 表示比 UTC 快 8 小时。这三者指向同一个绝对时刻——只是『把表拨成几点』不同而已。
为什么 DST(夏令时)这么反人类
夏令时是很多国家每年两次的规则性改动。以北美为例:
春季拨快(次/d) 02:00 → 03:00 02:00-03:00 这一小时不存在
秋季拨回(次/d) 02:00 → 01:00 01:00-02:00 出现两次
于是:某一天只有 23 小时,另一天有 25 小时。直接用本地日历相减算时长,在这两天必然差一小时。
典型错误
// ❌ 用本地时间直接相减算时长,跨 DST 会差一小时
const hours = (new Date(b).getTime() - new Date(a).getTime()) / 3600e3
getTime() 返回绝对毫秒,本身没错;错的是你拿本地构造的时间对象——它内部已按机器时区解释,一旦包含 DST 边界就躲不开。
正确做法
// 时长一律基于绝对时刻
const start = Date.parse('2026-03-07T00:00:00Z') // 明确给绝对时刻
const end = Date.parse('2026-03-08T00:00:00Z')
const realHours = (end - start) / 3600e3 // 26h?取决于这两个 Z 时刻
IANA 时区库与「时区会变」的事实
时区是政治决定:某些国家加/退夏令时、永久取消、调整偏移、改切换日期,导致 IANA(tzdata)频繁更新。这意味着:
- 别硬编码偏移、别把 DST 规则写死在代码里;
- 用受维护的时区库(各语言的标准库/luxon/date-fns-tz 等),按更新节奏升级;
- API 里返回 IANA 时区名(
Asia/Shanghai、America/New_York)+ 当前偏移,让前端能正确展示。
三个真实事故复盘
事故一:存了本地时间丢失偏移。 用户在北京存的 09:00,同步到美国设备被当成 09:00 本地,实际差 N 小时。→ 修复:一律存 UTC/带偏移 ISO8601。
事故二:跨 DST 算续费时长。 用『本地日历 30 天后』而非『绝对 30 天』,恰好跨 DST 就短/长一小时。→ 时机用绝对时刻,加减 30 天用 24h×30。
事故三:前端按服务器时区显示。 服务器返回 09:00 但没带偏移,前端拿服务器所在地时区渲染,海外用户看到错误时间。→ 后端给绝对时刻,前端用浏览器本地时区呈现。
一张自查图
存储 → UTC / epoch / 带偏移 ISO8601 ✔
传输 → ISO8601 带偏移或 Z ✔
展示 → 前端本地时区 ✔
时长 → 一律从绝对时刻计算 ✔
时区名 → IANA 名 + 当前偏移 ✔
结尾
时间戳并不难,难的是把『物理时刻』和『墙上时钟的显示』分开。记住了「存带偏移、传带偏移、展示转本地、时长用绝对时刻、时区名用 IANA」,你就不会再成为那个『为什么总差一小时』的人。