再见 1970!一篇说透 Unix 时间戳、Vue 里的日期和 Excel 日期

在这里插入图片描述

这篇文章,希望能带你把散落在脑海里的几个关键词——Excel 日期JavaScript DateUnix 时间戳——串成一条清晰又好懂的故事线。
如果你做过前端 / 全栈,尤其是遇到过「表格导入、Excel 对接、跨端时间乱跳」之类的需求,应该会有从「似曾相识的痛」到「啊,原来是这么回事」,最后收获一整套可落地的时间处理思路。


故事从一个「奇怪的日期」开始

最近我在写一个 Vue3 项目(技术栈:Vue3 + Element Plus + dayjs),需求本身一点也不花哨:

  • 后台数据处理系统
  • 支持从 Excel 上传一批数据
  • 其中有一列字段是「日期」

一切都非常正常,直到我在控制台里随手写了这样一行调试代码:

console.log(row.date);

结果打印出来的是一个非常诡异、完全不知道从哪儿冒出来的数字:

25569

第一反应当然是:

「后端是不是又把什么奇怪的 ID 当成日期丢给我了?」🙂

但转念一想,我对 Excel 里「日期」的存储方式其实是有一点点印象的——它并不是直接存 2025-03-01 这种字符串,而是存了一个连续递增的数字

所以我基本可以断定:

25569 其实是 Excel 用来表示某一天的「日期序列号」

一个原本只想「前端展示个日期」的小需求,突然变成了「多套时间系统互相掐架」的综合案例。
更要命的是,当你把它放到不同环境里尝试运行、格式化、转化,很可能会遇到:

  • NaN
  • Invalid Date
  • 1970-01-01
  • 时间戳差一天
  • 同一个时间戳,在浏览器里是一个日期,在 Node 里又是另一个日期

很多时候,我们会选择一种「工程师式逃避」:

「反正调到看起来对就行,别深究了。」

但这一次,我决定换个思路:

既然时间问题总是反复出现,不如趁这次把它系统梳理一遍,搞清楚 Excel、JS、Unix 各自的世界观,顺手把项目里的时间体系也搭一套更结实的地基。

希望你读完这篇文章后,再遇到时间相关的 Bug,不只是能「调对」,而是能从底层知道「为什么这样才是对的」。


在哪些场景,会有这类日期问题?

先一起回顾一下:只要你满足下面任意一个场景,大概率都被时间折腾过不止一次:

  1. 前端 + Excel 导入 / 导出

    • 从 Excel 导入一列日期,拿到的是类似 2556945123.5 这样的数字,或者各种没有统一格式的日期字符串。
    • 导出给别人之后,对方在 Excel 里一打开,发现全部变成了奇怪的「代码」或时间乱跳一天。
  2. 前后端时区不一致

    • 后端默认跑在 UTC 时区。
    • 前端运行在本地时区(比如北京 / 新加坡 / 纽约)。
    • 同一个时间戳,前后端显示的日期却不一样,或者接口文档写的是「当天 0 点」,结果线上变成「前一天 8 点」。
  3. API 返回的时间戳单位不统一

    • 有的接口返回「秒级」 Unix 时间戳(10 位)。
    • 有的接口返回「毫秒级」 Unix 时间戳(13 位)。
    • 前端混着用,时而乘 1000,时而不乘,最后表格里要么是 1970,要么是 51382 年。
  4. 排序 / 统计依赖日期字段

    • 你以为 order by date 是按时间顺序排,结果数据却成了这样的顺序:
      2025-3-1
      2025-12-20
      2025-5-2
      
    • 这时候才发现,你其实是用字符串在做「字典序」排序。
  5. 多语言 / 多技术栈协同

    • 前端是 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-01
  • 2 代表:1900-01-02
  • 25569 代表:1970-01-01
  • 0.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 这样的数字。
我们想要做的是:

  1. 在前端统一把它转成 Unix 时间戳(秒)
  2. 存储和接口传输都使用 Unix 时间戳;
  3. 展示时再根据需要格式化成 YYYY-MM-DDYYYY-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,里面日期一切正常。
一导入系统,发现所有日期都「早了一天」或「晚了一天」。

最常见两种原因:

  1. 忘了考虑 Excel 的 1900 闰年 Bug,导致基准日偏了一天;
  2. 没弄清楚 Excel 是按「1900 日期系统」还是「1904 日期系统」存的。

解决办法说难不难,说简单也不简单:

  • 难的部分在于:你得先知道有这些坑存在;
  • 简单的部分在于:一旦项目里有一套统一的时间工具函数,上面这些坑基本都可以用配置或封装解决。

从底层理解 Unix 时间

聊了这么多具体问题,我们不妨静下心来问一句:

「Unix 时间戳,这个数字到底在表示什么?」

一个比较好的理解方式是:

  • 想象一条时间轴,从 1970-01-01 00:00:00 UTC 开始;
  • 每过去一秒,这个数字就 +1;
  • 不管你在哪个时区,这个数字都是一样的。

也就是说:

  • Unix 时间戳是一个「与地理位置无关」的绝对时间标记
  • 时区只是「观察者的坐标系」不同;
  • 当你把 Unix 时间戳映射到「某个时区」时,它才会呈现不同的本地时间。

这也是为什么:

  • 在分布式系统里,想要把不同机器上的事件串成一条「时间线」,通常会优先使用 Unix 时间戳;
  • 在跨语言、跨服务的接口通信里,统一使用 Unix 时间戳(配合明确约定秒 / 毫秒)是最省心的。

当你习惯从这个角度看时间问题时,很多看起来「玄学」的 Bug,都会突然变得非常朴素:

「哦,只是我在某一步把时间从 Unix 时间戳转成本地时间的时候,多加或少加了几个小时而已。」


工程实践

回到我们最熟悉的前端场景。
如果你正在维护一个 Vue / React / 小程序 / H5 项目,可以考虑这样设计时间相关的「工程规范」:

  1. 存储与传输层:统一用 Unix 时间戳

    • 与后端约定:接口中时间字段优先使用 秒级 Unix 时间戳
    • 如果因历史原因已经是毫秒级,也要明确写在接口文档里,前端要统一处理;
    • 数据表、缓存、日志里,尽量也保持这一一致性。
  2. 转换层:集中封装在工具模块

    • 建一个 src/utils/time.ts(或类似名字)的工具模块;
    • 所有「Excel → Unix」「String → Unix」「Unix → 展示字符串」都集中在这里实现;
    • 组件、接口调用层只调用工具函数,不自行拼装 / 解析日期。
  3. 展示层:明确本地时间 / UTC

    • 用户看到的时间,一般用「本地时区」展示,比如 YYYY-MM-DD HH:mm
    • 对开发 / 运维友好的日志页面,可以额外展示一份 UTC 时间或原始时间戳。
  4. 跨端一致:浏览器 / Node / 定时任务用同一套规则

    • Node 层的脚本、定时任务也尽量用 Unix 时间戳做统一输入输出;
    • 需要按「自然日」划分的任务(例如 0 点跑批),要明确用哪个时区作为基准。

当你在团队里把这些约定讲清楚、写进文档、配合几个好用的工具函数之后:

  • 时间相关的 Bug 数量会明显下降;
  • 新同学接手时间相关需求时,有章可循;
  • 即便改业务逻辑,也很少需要动到底层「时间体系」。

这就是工程实践里常说的:

「把复杂度集中在一个地方消化掉,让其他地方尽量简单。」


如果这篇文章对你有帮助,欢迎转给那个经常被时间戳折磨的同事,让他少脱几根头发。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员义拉冠

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值