时间戳在线转换:10 位秒级 / 13 位毫秒级与时区的坑

时间戳在线转换看着简单,但第一步就别猜时区:日志里的 created_at: 1700000000 就是 2023-11-14 22:13:20 UTC,全球同一时刻。

真正让人踩坑的是另外三件事:10 位和 13 位混着用、把「显示成北京时间」误当成「时间戳变过」、以及用 86400 秒做日期加减。这篇一次讲清,并给一张各语言取当前时间戳的速查表。

一、先纠正一个流传很广的错误认知

Unix 时间戳是「从 1970-01-01 00:00:00 UTC 起经过的秒数」,它本身不带时区属性。

同一时刻,北京(UTC+8)和纽约(UTC-5)墙上的时钟不同,但 Date.now()time.time()time.Now().Unix() 拿到的数是同一个。时区只在你把时间戳格式化成字符串的那一刻才参与进来。

所以「时间戳要不要转时区」是个伪命题:要转的是显示,不是值。

👉 时间戳在线转换(免登录,秒 / 毫秒一键切换) —— 同时给出本地时间、UTC 时间和 ISO 8601,不用自己心算时差

二、时间戳在线转换前,先分清 10 位和 13 位

位数 单位 示例值 对应 UTC 时间 常见来源
10 位 1700000000 2023-11-14 22:13:20 后端接口、MySQL UNIX_TIMESTAMP()、Go time.Now().Unix()
13 位 毫秒 1700000000000 2023-11-14 22:13:20 JS Date.now()、Java System.currentTimeMillis()、前端埋点

判断不要用数位数(秒级会在 2286 年变成 11 位),用数量级判断:

function toMs(ts) {
  const n = Number(ts)
  // 1e11 毫秒 ≈ 1973 年,1e11 秒 ≈ 5138 年
  return n < 1e11 ? n * 1000 : n
}

业务里最容易出事的边界是 JWT:exp / iat / nbf 按 RFC 7519 规定是秒级(NumericDate),而前端手里的是 Date.now() 毫秒。判断 token 是否过期必须写成 payload.exp * 1000 < Date.now();直接 payload.exp < Date.now() 的结果是「永远不过期」。同理,Redis 的 EXPIRE / EXPIREAT、Nginx 缓存时间、Cache-Control: max-age 全是秒,前端算出来的毫秒记得除回去。

顺带说一个常被误传的点:Number.MAX_SAFE_INTEGER 是 9007199254740991(约 9e15),毫秒级时间戳现在约 1.7e12,离它还差一千倍,时间戳本身没有 JS 整数精度问题。真正会出事的是秒 / 毫秒精度混用:后端给秒,前端直接 new Date(ts),时间回到 1970 年;反过来把毫秒写进「秒级」字段,时间会变成公元 5 万年,还不一定报错。

三、new Date(ts * 1000) 看着「差 8 小时」,怪谁

const ts = 1700000000

new Date(ts * 1000).toString()
// 'Wed Nov 15 2023 06:13:20 GMT+0800 (中国标准时间)'

new Date(ts * 1000).toISOString()
// '2023-11-14T22:13:20.000Z'   ← 末尾的 Z 表示 UTC

两个字符串差 8 小时,但不是算错了:toString() 用运行环境的本地时区(中国是 UTC+8),toISOString() 固定输出 UTC。判断标准只有一个——看有没有 Z 后缀

这个坑在 SSR 场景会被放大:Node 容器的 TZ 通常是 UTC,浏览器是 UTC+8,同一份代码两边渲染结果差 8 小时。解决办法是传输层统一用时间戳或带 Z 的 ISO 字符串,只在最外层展示时才格式化。

MySQL 是同样的道理,而且更隐蔽:

SELECT @@session.time_zone;                -- 先看这个
SELECT FROM_UNIXTIME(1700000000);          -- 按会话时区返回
SET time_zone = '+08:00';
SELECT FROM_UNIXTIME(1700000000);          -- 2023-11-15 06:13:20
SELECT UNIX_TIMESTAMP('2023-11-15 06:13:20'); -- 1700000000,按 +08:00 解释入参
SELECT FROM_UNIXTIME(1700000000000);       -- NULL,超出 TIMESTAMP 范围

FROM_UNIXTIME() 按会话时区返回,UNIX_TIMESTAMP(字符串) 按会话时区解释入参。所以「库里查出来的和接口返回的不一样」,先查会话时区,别急着改代码。

还有一个高频坑是时间字符串的解析:

new Date('2023-11-15 06:13:20')    // Safari / 老浏览器:Invalid Date
new Date('2023-11-15T06:13:20')    // 不带时区 → 按本地时区解释
new Date('2023-11-15T06:13:20Z')   // 带 Z → 按 UTC 解释
new Date('2023-11-15')             // 纯日期 → 按 UTC 解释(不是本地!)

ES 规范的规定是:带时间的字符串不带时区时按本地时区解释,纯日期字符串按 UTC 解释。中间是空格而不是 T 的写法不属于 ISO 8601,各浏览器行为不一致,Safari 上尤其容易直接 Invalid Date。接口返回时间字符串时,一律要求带 TZ,或者干脆返回时间戳。

四、各语言取当前时间戳速查

语言 / 环境 当前(秒,10 位) 当前(毫秒,13 位) 时间戳 → 时间对象
JavaScript Math.floor(Date.now() / 1000) Date.now() new Date(ms),入参必须是毫秒
Python int(time.time()) int(time.time() * 1000) datetime.fromtimestamp(ts, tz=timezone.utc)
Go time.Now().Unix() time.Now().UnixMilli() time.Unix(sec, 0)(第一参数是秒)
Java Instant.now().getEpochSecond() System.currentTimeMillis() Instant.ofEpochSecond(sec)
PHP time() round(microtime(true) * 1000) (new DateTime())->setTimestamp(sec)
MySQL UNIX_TIMESTAMP() UNIX_TIMESTAMP() * 1000 FROM_UNIXTIME(sec)
Shell date +%s date +%s%3N(GNU date) date -d @1700000000

Python 和 Go 这两个细节最容易写错:

import time
from datetime import datetime, timezone

int(time.time())                       # 秒,10 位
int(time.time() * 1000)                # 毫秒,13 位

# 坑:naive datetime(不带 tzinfo)的 timestamp() 会按本地时区解释
datetime(2023, 11, 15, 6, 13, 20).timestamp()
# 想要确定的结果,必须显式带时区
datetime(2023, 11, 15, 6, 13, 20, tzinfo=timezone.utc).timestamp()
now := time.Now()
now.Unix()         // 秒,int64
now.UnixMilli()    // 毫秒,Go 1.17+;低版本用 now.UnixNano() / 1e6

time.Unix(1700000000, 0)   // 正确
time.Unix(1700000000000, 0) // 错:把毫秒当秒传,得到公元 5 万年

五、2038 年问题:还早,但已经该处理了

32 位有符号整数上限是 2147483647,对应 2038-01-19 03:14:07 UTC,再加一秒溢出成负数,时间跳回 1901 年。

会中招的地方:

  • MySQL 的 TIMESTAMP 类型范围就是 1970–2038,超范围会存成 0000-00-00 或直接报错;新表用 DATETIME(1000–9999)或 BIGINT
  • 数据库字段用 INT 存时间戳——INT 上限恰好是 2147483647
  • C/C++ 老系统、嵌入式设备、部分老旧 ORM 里 32 位的 time_t
  • 9999999999(2286 年)当「永不过期」哨兵值,写进 TIMESTAMP 字段直接溢出

对策很简单:全链路用 64 位(BIGINT / int64 / bigint)。Java、Go、Python 3 的默认实现已经没问题,主要查数据库字段类型和老服务。

六、日期加减别手写 86400

// 错:有时区存在夏令时的地区,某一天只有 23 小时或 25 小时
const tomorrow = new Date(Date.now() + 86400 * 1000)

// 对:交给 Date 的日期运算,让运行时按本地日历处理
const d = new Date()
d.setDate(d.getDate() + 1)

关于闰秒要澄清一句:Unix 时间戳不计算闰秒,每天恒定 86400 秒,那一秒是被「抹平」的,所以 +86400 不会因为闰秒出错。真正会错的是夏令时——本地时间的某天是 23 或 25 小时,加 86400 秒会落在同一天的 23:00 或次日 01:00。跨月跨年同理,用日期 API 或 dayjs / date-fns 的 add(1, 'month'),别自己算天数。

算两个日期相隔多久也一样:别拿毫秒差除以 86400000,跨夏令时会得到 30.96 天这种小数。用 日期计算器 直接出天数、周数和工作日数,还能按天 / 周 / 月 / 年 / 工作日往前推,比手算可靠。

七、上线前跑一遍这份清单

  1. 接口文档写死单位,字段名带后缀:created_at_s / created_at_ms,别让调用方猜
  2. 全链路统一用秒或统一用毫秒,跨语言边界(Go 秒 / JS 毫秒)处显式转换
  3. 前端 new Date() 入参永远是毫秒,拿到秒记得 ×1000
  4. 数据库和日志存 UTC 时间戳或带 Z 的 ISO 字符串,别存本地时间字符串
  5. 校验外部传进来的时间戳:先判数量级,不信调用方
  6. 空值和「永不过期」用 NULL,不用 0 或 9999999999
  7. MySQL 新表不用 TIMESTAMP + INT,改 DATETIMEBIGINT
  8. 日期加减用日期 API,不手写 86400
  9. 排查「差 8 小时」先打一行 toISOString() 对比,确认是不是显示层时区问题
  10. 容器镜像里显式设 TZ,别让 Node 服务和浏览器各按各的时区渲染
  11. 涉及 JWT / Redis / HTTP 缓存时间时,先确认单位是秒,比较前 ×1000

相关工具:时间戳转换日期相差天数计算在线二维码生成

全部纯前端实现,输入的内容不上传服务器。

还有 53 个免费在线工具

纯前端实现,不用注册,数据不上传服务器。

浏览全部工具