时间戳与时区:六个容易出错的地方
时间问题有个特点:平时不出错,一出错就是一整类问题——统计报表差一天、定时任务提前或延后一小时、日志里的事件顺序对不上。
根源几乎都是同一个:把"某个时刻"和"某个地方的时间"混为一谈。
这篇梳理六个高频坑。
一、服务端返回的时间没有时区标记
{"commit_time": "2026-11-03 12:43:35", "amount": 60}
这个字符串是什么时区?没有说明。它就是一颗定时炸弹。
前端拿到后直接 new Date(str),浏览器会按本地时区解析——也就是说,同一个接口,北京用户看到的是 12:43,伦敦用户看到的还是 12:43,但两者相差八小时。所有基于这个字段的排序、比较、展示,在两个时区下结论不同。
正确做法只有一条:接口返回带时区的时间。
{"commit_time": "2026-11-03T12:43:35Z"} // UTC,Z 结尾
{"commit_time": "2026-11-03T12:43:35+08:00"} // 北京时间
如果拿到的接口就是不带时区的(现实里很多),那就必须问清楚它到底是 UTC 还是服务器本地时间,然后在解析时显式处理:
// 已知是 UTC,手动补上标记
const d = new Date(str.replace(' ', 'T') + 'Z');
// 已知是北京时间
const d = new Date(str.replace(' ', 'T') + '+08:00');
补上这六七个字符,能省掉后面几小时的排查。
二、new Date('2026-11-03 12:43:35') 的行为不统一
带空格的日期字符串不是标准格式。不同引擎的处理方式不一样:
- 现代浏览器(Chrome、Firefox、Safari):按本地时区解析
- 老版本 Safari:可能直接返回
Invalid Date - Node.js:通常按本地时区
而在标准里,2026-11-03(纯日期)被规定解析为 UTC,2026-11-03T00:00:00(带时间无时区)被规定为本地时间。同一个日期,加不加时间,时区规则就变了:
new Date('2026-11-03').getTime() // UTC 零点
new Date('2026-11-03T00:00:00').getTime() // 本地零点
在北京,这两个值差八小时。如果你用前者算"当天开始",跨时区用户会看到"昨天"的数据。
实践建议:不要依赖字符串解析的默认行为。要 UTC 就写 Z,要本地就显式构造。
三、toLocaleString() 的输出取决于运行环境
d.toLocaleString() // 服务器上跑出来是 "11/3/2026, 12:43:35 PM"
同一行代码,在开发者的 Mac 上是 2026/11/3 12:43:35,在 Linux 容器里可能是 11/3/2026, 12:43:35 PM。因为默认的 locale 和时区来自运行环境。
这会导致一个很隐蔽的问题:本地测试通过,线上日志格式不对,或者反过来——用日志里的时间做正则匹配的脚本突然失效。
要么明确指定:
d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false })
要么干脆自己拼:
const pad = (n) => String(n).padStart(2, '0');
const fmt = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
后者啰嗦,但结果完全可控,不随环境漂移。
四、存储:一律 UTC,展示时再转
这条几乎是铁律:数据库里存 UTC,接口里传带时区的时间,只在最后展示给用户的那一刻转成本地时间。
反过来做(存本地时间)会遇到这些问题:
- 用户出差换了时区,历史记录的时间全乱了
- 多地区部署时,不同机器写入的时间无法比较
- 夏令时切换那天,会出现重复的一小时
- 数据库迁移到别的时区,所有历史数据都错位
存储用 timestamptz(PostgreSQL 的带时区类型)或统一存 Unix 时间戳(整数毫秒/秒),都能规避这一类问题。
注意 MySQL 的 timestamp 和 datetime 行为不同:timestamp 会按连接时区转换,datetime 原样存。用哪个都行,但要在连接层统一设置时区,否则同一个值在不同连接里含义不同。
五、夏令时:那天有 23 小时或 25 小时
中国不用夏令时,所以国内开发者经常忽略这一点——但服务一旦面向欧美用户就会踩到。
夏令时开始那天,本地时间会跳过一小时(比如 02:00 直接跳到 03:00);结束那天,重复一小时(02:00 出现两次)。
后果:
// 假设某天 02:30 不存在
new Date('2026-03-08T02:30:00-05:00') // 可能被规范化为 03:30
更麻烦的是"当天"的计算。如果你用"加 24 小时"来表示"明天同一时刻",跨夏令时会偏一小时。正确做法是用日历加法:
const tomorrow = new Date(d);
tomorrow.setDate(tomorrow.getDate() + 1); // 日历加法,自动处理 DST
如果项目要处理多时区,老老实实用一个库(date-fns-tz、luxon、Temporal),不要自己算偏移量。自己算迟早会漏掉某个地区的规则变更。
六、秒和毫秒混用
Unix 时间戳有十位(秒)和十三位(毫秒)两种。两者相差一千倍,混用后的表现是"日期跑到 1970 年"或者"跑到几万年以后"。
Date.now() // 1793681841899 ← 13 位,毫秒
Math.floor(Date.now()/1000) // 1793681841 ← 10 位,秒
排查时有个快速判断法:
const isSeconds = (ts) => String(Math.abs(ts)).length <= 10;
很多语言的时间函数默认单位不同——JavaScript 用毫秒,Python 的 time.time() 用秒,Java 的 System.currentTimeMillis() 用毫秒,Go 的 time.Now().Unix() 用秒。跨语言传递时最容易在这里翻车。
七、小结
六条,归纳起来其实就两句话:
第一,任何时间值都必须带上"它是哪个时区"这个信息。 没有时区的时间不是时间,是一串数字。
第二,存储和计算全部用 UTC,只在展示的最后一刻转换。 中间任何一步掺入本地时区,后面都会连锁出错。
排查这类问题时,最有效的办法是把时间戳和人类可读时间并排打印出来:
raw=1793681841899 iso=2026-11-03T05:37:21.899Z local=2026-11-03 13:37:21 (Asia/Shanghai)
一眼就能看出偏差是八小时(时区)、一千倍(单位)、还是一天(边界)。
文中示例基于 JavaScript 的 Date API 与常见数据库行为,不同运行时实现略有差异。
- 点赞
- 收藏
- 关注作者
评论(0)