
这篇文章,希望能带你把散落在脑海里的几个关键词——Excel 日期、JavaScript
Date、Unix 时间戳——串成一条清晰又好懂的故事线。
如果你做过前端 / 全栈,尤其是遇到过「表格导入、Excel 对接、跨端时间乱跳」之类的需求,应该会有从「似曾相识的痛」到「啊,原来是这么回事」,最后收获一整套可落地的时间处理思路。
故事从一个「奇怪的日期」开始
最近我在写一个 Vue3 项目(技术栈:Vue3 + Element Plus + dayjs),需求本身一点也不花哨:
- 后台数据处理系统
- 支持从 Excel 上传一批数据
- 其中有一列字段是「日期」
一切都非常正常,直到我在控制台里随手写了这样一行调试代码:
console.log(row.date);
结果打印出来的是一个非常诡异、完全不知道从哪儿冒出来的数字:
25569
第一反应当然是:
「后端是不是又把什么奇怪的 ID 当成日期丢给我了?」🙂
但转念一想,我对 Excel 里「日期」的存储方式其实是有一点点印象的——它并不是直接存 2025-03-01 这种字符串,而是存了一个连续递增的数字。
所以我基本可以断定:
25569其实是 Excel 用来表示某一天的「日期序列号」。
一个原本只想「前端展示个日期」的小需求,突然变成了「多套时间系统互相掐架」的综合案例。
更要命的是,当你把它放到不同环境里尝试运行、格式化、转化,很可能会遇到:
NaNInvalid Date1970-01-01- 时间戳差一天
- 同一个时间戳,在浏览器里是一个日期,在 Node 里又是另一个日期
很多时候,我们会选择一种「工程师式逃避」:
「反正调到看起来对就行,别深究了。」
但这一次,我决定换个思路:
既然时间问题总是反复出现,不如趁这次把它系统梳理一遍,搞清楚 Excel、JS、Unix 各自的世界观,顺手把项目里的时间体系也搭一套更结实的地基。
希望你读完这篇文章后,再遇到时间相关的 Bug,不只是能「调对」,而是能从底层知道「为什么这样才是对的」。
在哪些场景,会有这类日期问题?
先一起回顾一下:只要你满足下面任意一个场景,大概率都被时间折腾过不止一次:
-
前端 + Excel 导入 / 导出
- 从 Excel 导入一列日期,拿到的是类似
25569、45123.5这样的数字,或者各种没有统一格式的日期字符串。 - 导出给别人之后,对方在 Excel 里一打开,发现全部变成了奇怪的「代码」或时间乱跳一天。
- 从 Excel 导入一列日期,拿到的是类似
-
前后端时区不一致
- 后端默认跑在 UTC 时区。
- 前端运行在本地时区(比如北京 / 新加坡 / 纽约)。
- 同一个时间戳,前后端显示的日期却不一样,或者接口文档写的是「当天 0 点」,结果线上变成「前一天 8 点」。
-
API 返回的时间戳单位不统一
- 有的接口返回「秒级」 Unix 时间戳(10 位)。
- 有的接口返回「毫秒级」 Unix 时间戳(13 位)。
- 前端混着用,时而乘 1000,时而不乘,最后表格里要么是 1970,要么是 51382 年。
-
排序 / 统计依赖日期字段
- 你以为
order by date是按时间顺序排,结果数据却成了这样的顺序:2025-3-1 2025-12-20 2025-5-2 - 这时候才发现,你其实是用字符串在做「字典序」排序。
- 你以为
-
多语言 / 多技术栈协同
- 前端是 JS / TS,后端可能是 Java / PHP / Go / Python。
- 每种语言都有自己的一套日期类型、时间戳单位和格式化函数。
- 稍不注意,就会出现「前端显示 2025-03-01,后端日志里写的是 2025-02-28」这种令人抓狂的情况。
如果你在当前或者过去的项目里,对以上任意一条有共鸣,那么可以接着往下看:
Excel 日期 vs JS Date vs Unix 时间戳
在真正解决问题之前,我们需要先搭好一个清晰的「世界观」:
- Excel 的世界:一切从 1900 年开始,以「天数」计数。
- JavaScript 的世界:
Date对象,以「毫秒」计数,还跟本地时区强关联。 - Unix 的世界:一切从 1970-01-01 00:00:00 UTC 开始,以「秒」计数。

接下来,我们一块把这三套世界观捋顺。
3.1 Excel 日期:看起来简单,背后充满历史包袱
在 Excel 里,日期并不是字符串,而是一个带小数的浮点数:
- 整数部分:距离某个「起始日」过去了多少天;
- 小数部分:这一天里过去了多少时间(占全天的比例)。
在 Windows 默认的「1900 日期系统」中:
1代表:1900-01-012代表:1900-01-0225569代表:1970-01-010.5代表:当天 12:00(正中午)25569.5代表:1970-01-01 12:00
看起来非常优雅对吧?
但 Excel 背后背负着一个著名的历史包袱:
Excel 错误地认为 1900 年是闰年,于是允许你选择一个历史上根本不存在的日期:1900-02-29。
为什么当年要这么设计?
原因非常工程化:为了兼容当时已经广泛使用的 Lotus 1-2-3 表格软件,Excel 直接复制了它的 Bug。
这也导致:
- Excel 的第 60 天是
1900-02-29(一个不存在的日期) - 真正的 1900-03-01 被当成第 61 天
这个历史包袱让 Excel 日期在「和其他系统对接」时变得非常微妙:
看起来只是一个普通数字,背后却隐藏着一个「从一开始就偏了一天」的时间线。
3.2 JavaScript Date:毫秒精度 + 时区魔法
JavaScript 的标准时间类型是 Date 对象,它存储的是:
自「Unix 纪元」开始(1970-01-01 00:00:00 UTC)以来经过的 毫秒数。
例子:
const d = new Date("1970-01-01T00:00:00.000Z");
console.log(d.getTime()); // 0
但 Date 真正让人头大的是:它在内部用 UTC 存储,在展示时默认使用本地时区。
比如你在北京(UTC+8)执行:
const d = new Date(0);
console.log(d.toISOString()); // 1970-01-01T00:00:00.000Z
console.log(d.toString()); // Thu Jan 01 1970 08:00:00 GMT+0800 ...
也就是说:
- 同一个时间戳
0,- 在「纯后端」眼里是:1970-01-01 00:00:00 UTC
- 在「用户眼里」是:1970-01-01 08:00:00(北京时间)
只要在转换和展示上稍不注意,就很容易出现:
- 接口传了「当日 0 点的 UTC 时间」,前端直接当本地时间展示;
- 或者前端用本地时间创建
Date,然后直接toISOString()往后端一丢; - 最后得到一堆「早八小时」或者「晚八小时」的诡异时间。
3.3 Unix 时间戳:最朴素、也最适合做「中间语言」
再来看 Unix 时间戳。
定义非常简单:
自 1970-01-01 00:00:00 UTC 起至某个时刻,经过的 整秒数。
例如:
0 => 1970-01-01 00:00:00 UTC
1 => 1970-01-01 00:00:01 UTC
86400 => 1970-01-02 00:00:00 UTC
1704067200 => 2024-01-01 00:00:00 UTC
它有几个特别适合「工程实践」的优点:
- 简单:就是一个整数,加减乘除都非常直观;
- 和时区无关:只要约定统一用 UTC,前后端、不同语言之间就非常好对齐;
- 跨语言通用:几乎所有语言 / 框架都能方便地用 Unix 时间戳互相通信。
因此,在一个有前端、后端、数据库、脚本工具的系统里:
最推荐的做法是——用 Unix 时间戳作为「内部通用语言」,
其他所有形式(Excel 日期、字符串日期、本地时间展示)都通过一层「转换规范」去做。
后面我们会在具体代码里演示这种做法。
实战:在 Vue / Node 中把 Excel 日期转成 Unix 时间
回到最开始的问题:
前端从 Excel 导入一堆数据,某一列是日期,进来之后都是像 25569 这样的数字。
我们想要做的是:
- 在前端统一把它转成 Unix 时间戳(秒);
- 存储和接口传输都使用 Unix 时间戳;
- 展示时再根据需要格式化成
YYYY-MM-DD或YYYY-MM-DD HH:mm:ss。
先看一段简化版的工具函数(假设我们使用 dayjs):
// src/utils/time.ts
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
// Excel 日期序列号 -> Unix 秒级时间戳
export function excelToUnix(excelNumber: number): number {
// Excel 1900 日期系统:1 = 1900-01-01
// 为了兼容 Excel 的 1900 闰年 Bug,多数库会做相应调整。
// 这里我们选择直接用「正确的」参考点,并在业务上统一处理。
const excelStart = new Date(Date.UTC(1899, 11, 30)); // 1899-12-30
const msPerDay = 86400000;
const date = new Date(excelStart.getTime() + excelNumber * msPerDay);
return Math.floor(date.getTime() / 1000);
}
// 「任意可疑输入」 -> Unix 秒级时间戳(或者 null)
export function anyToUnix(input: unknown): number | null {
if (input == null) return null;
if (typeof input === "number") {
// 这里可能是 Excel 序列号,也可能是 Unix 时间戳(秒/毫秒)
if (input > 1e5 && input < 6e9) {
// 约定:10 位左右,当成秒级 Unix 时间戳
return Math.floor(input);
}
if (input > 1e11 && input < 1e15) {
// 约定:13 位左右,当成毫秒级 Unix 时间戳
return Math.floor(input / 1000);
}
// 其他情况就按 Excel 日期序列号处理
return excelToUnix(input);
}
if (input instanceof Date) {
return Math.floor(input.getTime() / 1000);
}
if (typeof input === "string" && input.trim() !== "") {
const d = dayjs(input);
if (d.isValid()) return d.unix();
}
return null;
}
// Unix 秒级时间戳 -> 展示用字符串(本地时区)
export function unixToDisplay(
unix: number,
fmt = "YYYY-MM-DD HH:mm:ss"
): string {
return dayjs.unix(unix).format(fmt);
}
在 Vue 组件里,就可以这样用:
<template>
<el-table :data="rows">
<el-table-column prop="dateUnix" label="日期" sortable>
<template #default="{ row }">
{{ unixToDisplay(row.dateUnix, "YYYY-MM-DD") }}
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { unixToDisplay, anyToUnix } from "@/utils/time";
// 从后端 / Excel 读到的原始数据
const rawRows = /* ... */ [];
const rows = rawRows.map((r) => ({
...r,
dateUnix: anyToUnix(r.date), // 统一转换成 Unix 时间戳
}));
</script>
当你在团队里把这套约定讲清楚、工具函数封装好之后:
- 表格里的所有日期列,都可以正常排序 / 过滤;
- 接口联调时,只需要互相确认「传的是秒还是毫秒」;
- 后端也可以非常放心地用自己的语言处理时间,最终只对齐 Unix 时间戳这一层。
这就是「把时间问题从 Bug 变成规范」的第一步。
踩坑三连:每一个都常见
说几个真实场景,你很可能已经亲身经历过。
5.1 报表统计少一天 / 多一天
某天产品跑过来跟你说:
「这个周活跃统计怎么总是少一天?」
你一查 SQL:
where created_at >= '2025-03-01 00:00:00'
and created_at < '2025-03-08 00:00:00'
看起来没问题,但:
- 数据库存的是「本地时间」;
- 报表脚本用的是「UTC 时间」;
- 前端展示时又自己加了 8 小时。
最后结果就是:
某些边界上的数据被莫名其妙排除在统计之外。
如果一开始就约定:
- 数据库存的是「毫秒级 Unix 时间戳」(UTC);
- 统计脚本、报表服务只用 Unix 时间戳做比较;
- 最后展示给用户时再做时区换算;
那么这个问题大概率一开始就不会出现。
5.2 「凌晨 0 点」在不同系统里不是同一个点
运营要做一个「当天 0 点推送」的任务,后端写了一个定时任务:
每天 00:00 触发
结果:
- 有的用户在北京看到是凌晨 0 点推送;
- 有的用户在新加坡看到是 23:00;
- 甚至还有用户在美国看到是前一天晚上。
根因很简单:
- 定时任务是按服务器时区跑的;
- 用户所在时区完全不同。
更好的做法是:
- 所有定时任务都用 UTC 做参考;
- 用户的「本地 0 点」换算成对应的 UTC 时间戳;
- 最终按 Unix 时间戳统一调度。
5.3 Excel 导入后,所有日期都错一天
你从业务那拿到一份 Excel,里面日期一切正常。
一导入系统,发现所有日期都「早了一天」或「晚了一天」。
最常见两种原因:
- 忘了考虑 Excel 的 1900 闰年 Bug,导致基准日偏了一天;
- 没弄清楚 Excel 是按「1900 日期系统」还是「1904 日期系统」存的。
解决办法说难不难,说简单也不简单:
- 难的部分在于:你得先知道有这些坑存在;
- 简单的部分在于:一旦项目里有一套统一的时间工具函数,上面这些坑基本都可以用配置或封装解决。
从底层理解 Unix 时间
聊了这么多具体问题,我们不妨静下心来问一句:
「Unix 时间戳,这个数字到底在表示什么?」
一个比较好的理解方式是:
- 想象一条时间轴,从 1970-01-01 00:00:00 UTC 开始;
- 每过去一秒,这个数字就 +1;
- 不管你在哪个时区,这个数字都是一样的。
也就是说:
- Unix 时间戳是一个「与地理位置无关」的绝对时间标记;
- 时区只是「观察者的坐标系」不同;
- 当你把 Unix 时间戳映射到「某个时区」时,它才会呈现不同的本地时间。
这也是为什么:
- 在分布式系统里,想要把不同机器上的事件串成一条「时间线」,通常会优先使用 Unix 时间戳;
- 在跨语言、跨服务的接口通信里,统一使用 Unix 时间戳(配合明确约定秒 / 毫秒)是最省心的。
当你习惯从这个角度看时间问题时,很多看起来「玄学」的 Bug,都会突然变得非常朴素:
「哦,只是我在某一步把时间从 Unix 时间戳转成本地时间的时候,多加或少加了几个小时而已。」
工程实践
回到我们最熟悉的前端场景。
如果你正在维护一个 Vue / React / 小程序 / H5 项目,可以考虑这样设计时间相关的「工程规范」:
-
存储与传输层:统一用 Unix 时间戳
- 与后端约定:接口中时间字段优先使用 秒级 Unix 时间戳;
- 如果因历史原因已经是毫秒级,也要明确写在接口文档里,前端要统一处理;
- 数据表、缓存、日志里,尽量也保持这一一致性。
-
转换层:集中封装在工具模块
- 建一个
src/utils/time.ts(或类似名字)的工具模块; - 所有「Excel → Unix」「String → Unix」「Unix → 展示字符串」都集中在这里实现;
- 组件、接口调用层只调用工具函数,不自行拼装 / 解析日期。
- 建一个
-
展示层:明确本地时间 / UTC
- 用户看到的时间,一般用「本地时区」展示,比如
YYYY-MM-DD HH:mm; - 对开发 / 运维友好的日志页面,可以额外展示一份 UTC 时间或原始时间戳。
- 用户看到的时间,一般用「本地时区」展示,比如
-
跨端一致:浏览器 / Node / 定时任务用同一套规则
- Node 层的脚本、定时任务也尽量用 Unix 时间戳做统一输入输出;
- 需要按「自然日」划分的任务(例如 0 点跑批),要明确用哪个时区作为基准。
当你在团队里把这些约定讲清楚、写进文档、配合几个好用的工具函数之后:
- 时间相关的 Bug 数量会明显下降;
- 新同学接手时间相关需求时,有章可循;
- 即便改业务逻辑,也很少需要动到底层「时间体系」。
这就是工程实践里常说的:
「把复杂度集中在一个地方消化掉,让其他地方尽量简单。」
如果这篇文章对你有帮助,欢迎转给那个经常被时间戳折磨的同事,让他少脱几根头发。
1115

被折叠的 条评论
为什么被折叠?



