Unix 时间戳排错:秒、毫秒、UTC、时区与夏令时

时间戳 bug 很常见,因为 Unix 时间戳本身简单,但围绕它的系统并不简单。同一个数字可能代表秒,也可能代表毫秒。数据库可能存 UTC,前端却按本地时区显示。日志可能用 ISO 8601,API 却传 epoch time。到了夏令时附近,“本地午夜”也可能变得不稳定。

这篇指南给出一套稳定的排错流程。

第一步永远是确认单位

先问:这是秒还是毫秒?

传统 Unix 时间是从 1970-01-01 00:00:00 UTC 开始经过的秒数。JavaScript Date、Java System.currentTimeMillis() 和很多前端 API 使用毫秒。

常见长度:

  • 10 位:秒,例如 1721347200
  • 13 位:毫秒,例如 1721347200000

如果日期跑到 1970 年附近,通常是把秒传给了需要毫秒的函数。如果日期跑到很远的未来,通常是把毫秒又乘了一次 1000。

// 秒级时间戳转 JavaScript Date
new Date(1721347200 * 1000)

// 毫秒级时间戳转 JavaScript Date
new Date(1721347200000)

UTC 存储,本地展示

Unix 时间戳表示一个绝对时刻,本身不包含时区。时区只在展示为日历日期时出现。

同一个时间戳,在纽约、伦敦、上海、东京会显示成不同本地时间。这是正常现象。变的是展示规则,不是时间戳。

排查时同时比较两种输出:

const d = new Date(1721347200 * 1000)
console.log(d.toISOString()) // UTC
console.log(d.toString())    // 本地时间

如果 UTC 正确但 UI 看起来差一天,问题多半在本地时区展示,而不是时间戳本身。

日期型值不要随便转时间戳

2026-07-19 这种只有日期、没有时间的字符串很容易被误用。有些系统把它当作 UTC 零点,有些系统当作本地零点。转成时间戳后,不同时区用户可能看到前一天或后一天。

生日、账单日期、活动日期这类“日历概念”,最好存原始日期。如果你需要的是具体瞬间,再转成时间戳。

夏令时问题

夏令时会让本地时间不连续。某个本地小时可能不存在,也可能出现两次。如果任务调度设在本地 02:30,有些日期可能根本没有这个时间点。

可靠系统建议:

  • 用 UTC 时间戳存储
  • 需要时另存用户时区
  • 只在展示层转换为本地时间
  • 用户期待“本地日历的一天后”时,不要简单加 86400

数据库和 API 排查清单

当时间戳跨越数据库和 API 时,写下:

  1. 原始值
  2. 单位:秒还是毫秒
  3. UTC ISO 字符串
  4. 本地展示字符串
  5. 数据库字段类型
  6. API 合约

大多数问题会在这里暴露。BIGINT 可能存毫秒,timestamp with time zone 可能已经存了规范化后的时刻。API 文档可能写秒,前端代码却发送毫秒。

日志和监控

日志里经常混用格式:

2026-07-19T10:30:00Z
1721385000
1721385000000
Jul 19 18:30:00

比较事件顺序前,先把格式统一。建议以 UTC ISO 字符串作为排查基准,因为可读且不容易误解。

当两个系统的事件顺序对不上,检查:

  • 两台机器时间是否同步
  • 两边时间戳单位是否一致
  • 一个字段是接收时间,另一个是否是事件发生时间
  • 一个日志查看器是否显示本地时间,另一个是否显示 UTC

JavaScript 解析提醒

JavaScript 的日期解析比较宽松,但并不总是跨环境一致。优先使用明确的时间戳、带时区的 ISO 字符串,或由表单字段主动转换。

推荐:

new Date('2026-07-19T10:30:00Z')
new Date(1721385000000)

风险较高:

new Date('2026/07/19 10:30:00')
new Date('07/19/2026')

模糊格式在不同浏览器或运行环境中可能得到不同结果。

实用排错流程

Unix 时间戳转换工具 把原始值双向转换。如果工具显示的 UTC 时刻正确,检查你的 UI 展示逻辑。如果工具显示的时刻就不对,检查单位和来源系统。

如果时间戳在 JSON payload 里,先用 JSON 格式化工具 验证结构。如果时间戳在 URL 参数中,例如 ?expires=1721385000,可以用 URL 参数解析工具 单独提取再转换。