Node 20 中用 date-fns 做时区转换,我漏掉夏令时那一小时
This post is available in 简体中文 only — the original follows.
上个月做定时任务,需要把用户本地时间转换成 UTC 存库。线上用的是 Node 20.11,容器时区统一设成 UTC,数据量不大,一天几十万条记录。上线第二天发现,凌晨那批任务的执行时间比预期晚了一个小时,用户投诉说提醒迟到了。
问题出在时区转换这一层。我原来的写法是这样的:
const { zonedTimeToUtc } = require('date-fns-tz');
const utc = zonedTimeToUtc(localTimeStr, 'Asia/Shanghai');
本地测试一直没问题,因为测试数据都避开了夏令时。但服务面向的用户有欧洲和北美时区,America/New_York 在 3 月切夏令时那天,凌晨 2 点到 3 点这个区间根本不存在。date-fns-tz 遇到这种“不存在的本地时间”不会报错,它会按某个默认规则往前或往后推,结果和业务预期对不上。
我的做法是:先判断这个本地时间在目标时区是否存在,再决定怎么转。用 Intl.DateTimeFormat 反向验证:
const { formatInTimeZone } = require('date-fns-tz');
function isLocalTimeValid(localStr, timeZone) {
const utc = zonedTimeToUtc(localStr, timeZone);
const back = formatInTimeZone(utc, timeZone, "yyyy-MM-dd'T'HH:mm:ss");
return back === localStr;
}
如果 back 和原始字符串不一致,说明这个本地时间在目标时区里不存在(夏令时跳变)或者有歧义(回拨那一小时出现两次)。对不存在的时段,我直接让任务跳过并记录一条日志,而不是猜一个偏移量;对出现两次的时段,按业务约定取第一次出现的时间。
为什么这样做:时区转换本质上是把“墙上时间”映射到绝对时间,而墙上时间在某些时刻不是单射。date-fns-tz 的 zonedTimeToUtc 不负责校验这个映射是否合法,它只做计算。用 formatInTimeZone 转回去比一次,是最省事的一致性检查,不需要引入额外依赖,代价是多一次格式化开销。一天几十万条、每条多几微秒,可以接受。
哪里会失效:这个校验只对 IANA 时区名有效,如果传的是固定偏移量 +08:00 这种,夏令时信息本身就丢了,校验没有意义。另外 Intl 的时区数据库依赖 Node 的 ICU 版本,Node 20 默认 full-icu,但如果你的镜像被裁过,可能拿到旧数据。我踩过的坑是本地 Docker 镜像用了 node:20-slim,某次升级前 ICU 数据落后,导致校验结果和线上不一致,排查了半天。
还有一个边界:跨年切换时如果本地时间和 UTC 落在不同年份,字符串比较依然成立,因为比的是格式化后的完整时间串,不受影响。真正麻烦的是用户输入本身带歧义又没带偏移量,这种只能靠业务层约定。
你们在处理用户本地时间时,是让前端直接传 UTC 偏移量,还是后端拿时区名自己算?如果传时区名,遇到夏令时跳变那一个小时,你们是跳过还是补一条?
0 comments
No comments yet — be the first.