时间戳为什么总差 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 展示差异 | 确认两端时区基准,展示层做转换 |
接口收到时间戳后被截断成 2147483647 | 13 位毫秒传给了 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 那一秒。
相关文章