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; - 展示和对接:明确知道对方需要的是哪个时区的时间。
只要坚持这三点,基本就不会再被时区问题困扰。