Unix 时间戳的定义是「从 1970-01-01T00:00:00Z 起经过的秒数」,它描述的是一个绝对时刻,不含任何时区信息。所以当两处显示相差 8 小时,问题一定出在格式化环节,而不是存储环节。

三个函数,三种语义

  • Date.now():绝对时刻,13 位毫秒。任何时候调用都一致。
  • toISOString():永远输出 UTC,带 Z 后缀。适合日志和接口传输。
  • toLocaleString():依赖运行环境的时区设置。同一段代码,在你本机是 +08:00,在容器里可能是 UTC。

最常见的症状:本地开发日志时间正常,部署到服务器后整体偏移 8 小时。因为容器默认 TZ 是 UTC,而你的代码在某处用了 toLocaleString() 或者手写了 getHours()。

秒和毫秒混用

10 位是秒,13 位是毫秒。传到前端只认毫秒的 API 里,10 位时间戳会被解释成 1970 年 1 月 20 日左右——不会报错,只是年份完全不对。所以接口约定必须写清楚单位,或者干脆统一用毫秒。

// 一个稳妥的归一化函数
const toMs = (v) => {
  const n = Number(v);
  return n < 1e11 ? n * 1000 : n;  // 小于 1e11 视为秒
};

数据库字段怎么选

MySQL 的 TIMESTAMP 会随时区设置转换,DATETIME 不会。如果团队成员分布在不同的数据库时区配置下,用 TIMESTAMP 容易出现「同一行数据在不同连接里显示不同」的诡异现象。要么全部统一 UTC 并用 DATETIME,要么全链路都用 BIGINT 存毫秒。

夏令时与历史时区

不是所有地区都固定偏移。欧美有夏令时,某些历史日期的偏移还与今天不同。所以不要自己写「加 8 小时」这种换算,交给 Intl 处理:

new Intl.DateTimeFormat('zh-CN', {
  timeZone: 'Asia/Shanghai',
  dateStyle: 'medium',
  timeStyle: 'medium'
}).format(new Date());
推荐做法:存储层一律 UTC 时间戳或带时区的 ISO 字符串,传输层用 ISO 8601,展示层按用户时区格式化。中间任何一层都不要「顺手加 8 小时」。

对着时间戳转换核对一下,它会同时给出本地时间和 UTC,方便确认偏移发生在哪一层。