UPTools UPTools
开发 主笔:工具匠

时间戳为什么总差 8 小时?Unix 时间戳转日期的三个坑

上周帮一个前端排查 bug,他信誓旦旦地说后端接口返回的时间戳错了:日志里写的是 1700000000,他 console 出来是 1970 年 1 月 20 日。我让他把那串数字 × 1000 再传给 new Date(),瞬间变成 2023 年 11 月 15 日。他愣了三秒。

时间戳转换看着简单,实际踩坑的人一茬接一茬。下面这三个是我联调这些年见过最高频的,每一个都能让一段看似正确的代码产生离谱的结果。

第一个坑:秒级和毫秒级搞混

Unix 时间戳是从 1970-01-01 00:00:00 UTC 起经过的秒数。注意是秒数。但 JavaScript 的 Date 对象设计时拍了一下脑袋,选了毫秒。于是同一个时间戳在两种语境下位数完全不同:

  • 秒级:10 位,比如 1700000000,对应 2023-11-15 06:13:20(UTC+8)。后端、数据库、Linux 命令大多用这种。
  • 毫秒级:13 位,比如 1700000000000。JS、Java 的部分接口用这种。

坑就在这:你在 JS 里写 new Date(1700000000),JS 以为这是 17 亿毫秒,换算下来是 1970 年 1 月 20 日。日志里凭空多出五十多年。

修复办法只有一种,记住方向:

// 秒级时间戳 → JS Date
new Date(seconds * 1000)

// JS 毫秒 → 传给只认秒的后端
Math.floor(ms / 1000)

判断时间戳是秒还是毫秒,看位数最快:10 位是秒,13 位是毫秒。极少数会遇到微秒(16 位)或纳秒(19 位),那是另一个故事。

第二个坑:UTC 和本地时间差 8 小时

这是标题里的那个坑。同一个时间戳 1700000000,UTC 显示是 2023-11-14 22:13:20,UTC+8(北京)显示是 2023-11-15 06:13:20。数值差了 8 小时,但它们指向同一绝对时刻。时间戳本身不带时区,它就是从 UTC 纪元起的秒数,所谓「差 8 小时」只是展示层面的差异。

实际项目里典型的踩坑场景:

  • 后端存 UTC,前端用本地时区展示,转换链路里有人忘了统一基准。
  • 服务器时区配置成了 UTC,但日志格式化函数假设本地时区,于是日志时间比真实时间晚 8 小时。
  • 数据库里 DATETIME 没带时区信息,写入时按本地、读出时按 UTC 解释。

排查这类问题的思路:先确认两端各自的时区基准,再谈转换。在浏览器里用 Date.prototype.toLocaleString('zh-CN', { timeZone: 'UTC' }) 强制按 UTC 显示,跟后端日志对齐,就能判断偏差是数据问题还是展示问题。在 Node 服务里,建议日志和存储统一用 UTC,只在面向用户的展示层做时区转换。

第三个坑:13 位毫秒时间戳传给只认秒的接口

这个坑比前两个隐蔽。前端用 Date.now() 拿到的永远是 13 位毫秒数,比如 1700000000000。如果后端的某个接口(比如某个老 cron 配置、某个 C 程序、某个只接受 int32 的字段)只认秒级时间戳,直接把这个 13 位数传过去,会出现两种结果:

  • 接口把数值截断成 32 位有符号整数,得到 2147483647(也就是 2^31 - 1),对应 2038-01-19 03:14:07 UTC。你以为约的是周三下午,系统帮你约到了 2038 年。
  • 接口直接报参数越界或类型错误,连错误原因都看不出来。

正确做法是传参前在 JS 侧先转一次:

const ts = Date.now()           // 13 位毫秒
const apiParam = Math.floor(ts / 1000)   // 10 位秒

Math.floor 不能省。如果用 Math.round,某些边界值会四舍五入多 1 秒。如果直接除不取整,传过去是个小数,更麻烦。

顺便提一句 2038 年问题。32 位有符号 time_t 最大值是 2147483647,对应 2038-01-19 03:14:07 UTC,过了这一秒就会溢出成负数,老的 32 位系统会以为时间回到了 1901 年。现代 64 位系统不受影响,但嵌入式设备、老旧服务端上跑的代码还有机会踩到。离 2038 还有十几年,足够慢慢迁移。

常见错误对照

现象原因修复
new Date(1700000000) → 1970 年秒级时间戳传给了认毫秒的 Date× 1000 后再传
时间显示比预期晚 8 小时UTC 与 UTC+8 展示差异确认两端时区基准,展示层做转换
接口收到时间戳后被截断成 214748364713 位毫秒传给了 32 位秒级字段Math.floor(ts / 1000) 先转秒
时间戳显示在 2038 年附近同上,溢出到 2^31 - 1同上

实操:把时间戳丢进工具看一眼

排查时与其在控制台里反复敲 new Date(),不如直接粘到 UPTools 时间戳转换工具 里。它同时给出秒级和毫秒级两种结果,以及本地时间和 UTC,对齐基准一眼就能看出问题出在哪一段。本地运算,不传任何数据。

三句话总结

秒级 10 位、毫秒级 13 位,JS 的 Date 用毫秒,传参前对不上就乘或除 1000。所谓差 8 小时几乎都是 UTC 和 UTC+8 的展示差异,指向的是同一时刻,先统一基准再排查。13 位毫秒戳别硬塞给只认秒的接口,否则要么被截断到 1970 附近,要么被卡在 2038 那一秒。

相关文章