Unix 时间戳转北京时间:先看清是 10 位还是 13 位

· 4 分钟阅读 · 时间戳 时区 · English

日志里躺着一串 1735689600,要把它说成北京时间。先明确一件事:时间戳的定义是"从 1970-01-01T00:00:00 UTC 起走过的时间量",里面不含任何时区信息,同一个数字在上海和在纽约指的是同一个瞬间。所以"转北京时间"问的不是把它换算成另一个时间戳,而是这同一瞬间落到北京的墙上时钟是几点——答案是 2025-01-01 08:00(星期三),不是 00:00。

换算就这几行

JavaScript 里最省事的一行,toLocaleString 带上 timeZone:

const ts = 1735689600;                    // 10 位 = 秒
new Date(ts * 1000).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
// => 2025/1/1 08:00:00

Date 只认毫秒,所以秒要乘 1000。如果那串是 13 位,它已经是毫秒,再乘一次会飞到公元 5 万多年(1735689600000 当秒解释是 56971 年):

const ms = 1735689600000;                 // 13 位 = 毫秒
new Date(ms).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
// => 2025/1/1 08:00:00  同一个瞬间

Python 要显式给偏移,不写 tz 时会按操作系统时区解释,这正是跨机器结果不一致的来源:

from datetime import datetime, timezone, timedelta

datetime.fromtimestamp(1735689600, timezone(timedelta(hours=8)))
# datetime.datetime(2025, 1, 1, 8, 0, tzinfo=timezone(timedelta(seconds=28800)))

datetime.utcfromtimestamp() 在 3.12 起已弃用,改用带 tz 参数的 fromtimestamp。Java 同理,用 java.time:

Instant.ofEpochSecond(1735689600).atZone(ZoneId.of("Asia/Shanghai"));
// 2025-01-01T08:00+08:00[Asia/Shanghai]

命令行下 Linux 用 date:

date -d @1735689600                    # 按本机时区显示
TZ=Asia/Shanghai date -d @1735689600   # 指定北京时间
date -u -d @1735689600                 # 2025-01-01 00:00:00 UTC

位数几乎总能直接暴露单位

位数 通常含义 例子 备注
10 秒 1735689600 到 2286 年才会变 11 位
13 毫秒 1735689600000 JS / Java / Go 的默认口径
16 微秒 — Date 只到毫秒,当毫秒解释会跳到公元 56971 年

各家默认并不统一:Date.now()、System.currentTimeMillis()、time.Now().UnixMilli() 给毫秒;time()(PHP)、date +%s(Linux)、Go 的 Unix()、MySQL 的 TIMESTAMP 给秒;PostgreSQL 内部按微秒存。数位数只是启发式——它能覆盖绝大多数真实数据,但没法替你确认源头到底想给什么。所以像 8 位、11 位这种两头都不像的长度,Unix 时间戳换算页直接报错,而不是猜一个看着顺眼的值:单位猜错比一条错误信息难查得多。

差 8 小时,通常是被换了两次

同一串数字,有人看到 00:00 有人看到 08:00,几乎都不是算错,而是写入和读取各自换了一次时区。最典型的组合是:程序把本地的墙上时间当成本地时间存进了一个"其实是 UTC"的列,读出来又按 UTC 转成本地——偏移被叠了两遍。

能避免这类问题的约定只有一个:存 UTC、传 UTC,只在展示层换成本地时区。服务器和容器显式设 TZ=UTC,别依赖宿主机的默认时区;接口里传 ISO 8601 时一定带偏移,2025-01-01T08:00:00+08:00 和 2025-01-01T00:00:00Z 是同一个瞬间,写成裸的 2025-01-01T08:00:00 就把解释权丢给了对方。顺带一句 JavaScript 的怪癖:new Date("2026-01-02") 按 UTC 解析,而 new Date("2026-01-02T00:00:00") 按本地解析,在东八区两者相差 8 小时。

北京时间不总是 UTC+8

今天它是。但 1986 到 1991 年中国实行过夏令时,那几年夏天北京是 UTC+9——用这台机器的时区库查 1986-05-04 中午,Asia/Shanghai 给出的偏移就是 GMT+09:00。所以处理出生年月、老合同、档案日期时,把北京时间硬写成 +08:00 会差一小时。

结论是:偏移数字用来传递瞬间,时区名字用来解释历史。代码里优先写 IANA 名 Asia/Shanghai,而不是 GMT+8、UTC+8 或 PRC 这类别名——后者的可移植性明显差得多。

不想开编辑器的时候

Unix 时间戳换算是双向的:粘时间戳进去,出 UTC 与这台电脑的本地时间两种读法;反过来填日期和时间,出对应的时间戳。它不会把输入交给 new Date() 去猜时区,而是拆成字段再用本地 setter 组装,所以只有一种解释。所有换算都在这台设备的浏览器里完成,你填的内容不会离开这台设备。

配套的两个页面:算两段日期差多少,用日期间隔计算;从某个日期往后推一段时长,用日期加减推算。

相关问题

13 位时间戳怎么变回秒? 整除 1000,毫秒余数会丢掉。要保留精度就同时留两个字段,或者用字符串。

时间戳会到 2038 年溢出吗? 32 位有符号整数存秒的上限是 2038-01-19 03:14:07 UTC(北京 11:14:07),MySQL 的 TIMESTAMP 同样卡在这里。长期数据用 BIGINT 存秒或毫秒,JavaScript 的 Date 用 64 位毫秒,本来就不受这个限制。

闰秒呢? Unix 时间戳不数闰秒,跨闰秒的那一分钟里,时间戳的间隔和真实时间会差 1 秒。日常日志和排期不用管,做高频交易或计时同步才需要。

数据库里存时间戳还是 datetime? 优先时间戳或带时区语义的类型(PostgreSQL 的 timestamptz)。datetime 只记墙上时间不记时区,数据一跨时区就丢信息。

微信
微信
支付宝
支付宝