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,方便确认偏移发生在哪一层。