我的博客

Node.js 里处理时区的几个坑

本地开发一切正常,部署到服务器以后,所有时间都差了 8 个小时——这个问题我至少遇到过五次。整理一下和时区相关的几个坑。

1. 服务器默认是 UTC

大部分云服务器镜像、Docker 镜像默认的时区都是 UTC。Node 中 new Date().getHours() 等方法返回的是进程所在时区的时间,所以同样的代码在本地和服务器上结果不同。

解决方法有两种:

# 方法一:修改系统时区
sudo timedatectl set-timezone Asia/Shanghai

# 方法二:只针对 Node 进程
TZ=Asia/Shanghai node app.js

我更推荐在代码中显式设置,不依赖服务器的配置:

// 放在入口文件的最开始
process.env.TZ ||= 'Asia/Shanghai';

Docker 中可以在 Dockerfile 里设置 ENV TZ=Asia/Shanghai。

2. toISOString 永远是 UTC

new Date().toISOString(); // "2026-10-01T02:30:00.000Z"

结尾的 Z 表示 UTC。北京时间上午 10:30,这里显示的是 02:30。

如果想要北京时间的"年-月-日",不要用 toISOString().slice(0, 10),在凌晨 0 点到 8 点之间会得到前一天的日期。应该用本地时间的方法拼接:

const d = new Date();
const p = (n) => String(n).padStart(2, '0');
const today = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;

3. JSON.stringify 中的日期

JSON.stringify({ t: new Date() }); // {"t":"2026-10-01T02:30:00.000Z"}

Date 对象序列化时会调用 toISOString,所以接口返回的时间是 UTC 格式。前端用 new Date(str) 解析时会正确转换成本地时间,这本身没有问题;但如果前端直接截取字符串来显示,就会差 8 小时。

4. SQLite 的 datetime('now') 是 UTC

SELECT datetime('now');                -- UTC 时间
SELECT datetime('now', 'localtime');   -- 本地时间

我的习惯是数据库中统一存 Unix 时间戳(整数),只在展示时格式化。时间戳不涉及时区,比较、排序、计算时间差都很方便:

SELECT datetime(created_at, 'unixepoch', 'localtime') FROM orders;

5. 第三方接口要求的时间格式

很多国内的接口(比如支付宝的 timestamp、time_expire 参数)要求的是北京时间的 yyyy-MM-dd HH:mm:ss 格式。如果服务器是 UTC 而代码中用本地时间方法拼接,传过去的时间就会早 8 小时——订单可能一创建就已经过期。

6. "今天"的边界

统计"今日订单"时,"今天"从什么时候开始?

const start = new Date();
start.setHours(0, 0, 0, 0);
const startTs = Math.floor(start.getTime() / 1000);

这段代码在 UTC 时区的服务器上,得到的是 UTC 的零点,也就是北京时间上午 8 点。结果就是每天早上 8 点之前的订单都被算到了前一天。

总结

  • 存储:统一用时间戳或带时区的 ISO 字符串;
  • 进程:在入口处显式设置 TZ;
  • 展示和对接:明确知道对方需要的是哪个时区的时间。

只要坚持这三点,基本就不会再被时区问题困扰。