← 返回博客首页

时区与夏令时陷阱:为什么你的时间戳总是差一小时

事故现场的经典台词

「服务器明明返回了 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/ShanghaiAmerica/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」,你就不会再成为那个『为什么总差一小时』的人。

常见问题

存时间到底该存 UTC 还是本地时间?

**存储与传输一律用 UTC(或带完整时区偏移的 ISO8601),展示时才转本地**。原因是绝对时刻是客观物理量,而『几点钟』取决于观察者时区,且会随用户旅行、夏令时变化而变化。若存本地时间但丢失偏移,同一行数据在不同时区会被误读成不同时刻;若存 UTC,无论用户在哪、无论 DST 与否都能正确换算。习惯:数据库 `TIMESTAMP WITH TIME ZONE` / 存 epoch,API 返回带 `+08:00` 或 `Z` 的 ISO8601。

为什么有的时长计算在夏令时前后多了/少了一小时?

**DST 切换让『一天包含 23 或 25 小时』**。例如北美春季拨快一小时那天,本地 02:00-03:00 不存在;秋季拨回时 01:00-02:00 会重复出现两次。若算『两个本地时间点相差多久』直接用本地日历相减,会在这两天差出 1 小时。正确做法:先把两端都转成绝对值(时刻/UTC)再求差,或明确用『物理时长』:天按 24h、周按 168h,只有跨 DST 的『日』才可能不是 24h。

同一 moment 的展示,为什么要带时区而不是只写数字?

因为同一个 14:30 在不同时区指向不同的绝对时刻,只写数字会丢失语义。ISO8601 标准做法是后缀显示偏移:`2026-09-04T14:30:00+08:00`(或 UTC 时 `Z`)。这样任何解析器都能还原成绝对时刻,再按观众本地转呈。**最佳实践:后端给数据只给绝对时刻(带偏移),前端用浏览器本地时区、以「用户所在地」展示**——服务器永远不知道用户在哪,也不该替用户决定显示成几点。

为什么有的国家/地区时区会变?

时区不是物理定律,是**政治与立法决定的**。国家会调整偏移、加入/退出/永久取消夏令时、改变切换日期(土耳其、埃及、墨西哥等多地近年变动),这就导致时区数据库 IANA(`tzdata`)频繁发版。影响:固定写死偏移(如写死 `+08:00`)或硬编码 DST 规则的代码会过时;一定要用**上架的时区库**并按需更新,且 API 返回时应带最新的 IANA 名(如 `Asia/Shanghai`、`America/New_York`)并携带当前偏移供显示。

← 返回博客首页