// utils/timezone.js - 完整版, 支持偏移量与 IANA 时区名称 const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc'); const timezone = require('dayjs/plugin/timezone'); dayjs.extend(utc); dayjs.extend(timezone); class TimezoneUtils { constructor(timezoneNameOrOffset) { // 默认使用上海时区 (UTC+8) if (timezoneNameOrOffset === null || timezoneNameOrOffset === undefined) { timezoneNameOrOffset = 'Asia/Shanghai'; } if (typeof timezoneNameOrOffset === 'number') { // 数字模式 => 偏移量(小时) this.offsetHours = timezoneNameOrOffset; this.offsetMs = timezoneNameOrOffset * 60 * 60 * 1000; this.useOffset = true; this.timezoneName = null; } else { // 字符串模式 => IANA 时区 this.timezoneName = timezoneNameOrOffset; this.useOffset = false; this.offsetMs = 0; this.offsetHours = 0; } } /** ✅ UTC -> 本地时区时间 */ utcToLocal(utcDate) { if (!(utcDate instanceof Date)) { utcDate = new Date(utcDate); } if (this.useOffset) { return new Date(utcDate.getTime() + this.offsetMs); } else { return dayjs.utc(utcDate).tz(this.timezoneName).toDate(); } } /** ✅ 本地时区时间 -> UTC */ localToUtc(localDate) { if (!(localDate instanceof Date)) { localDate = new Date(localDate); } if (this.useOffset) { return new Date(localDate.getTime() - this.offsetMs); } else { return dayjs.tz(localDate, this.timezoneName).utc().toDate(); } } /** * ✅ 获取某个 UTC 时间对应的“本地日期零点的 UTC 时间” * 例如北京时间 2025-11-02T10:00Z -> 返回该天本地零点对应的 UTC 时间 2025-11-01T16:00Z */ getLocalDayStart(timestamp) { const utcDate = new Date(timestamp); const localTime = this.utcToLocal(utcDate); const year = localTime.getFullYear(); const month = localTime.getMonth(); const day = localTime.getDate(); // 构造该时区日期的本地零点 const localMidnight = new Date(Date.UTC(year, month, day, 0, 0, 0, 0)); // 转换回 UTC return this.localToUtc(localMidnight); } /** * ✅ 获取目标时区当前日期的“零点 UTC 时间” */ getTodayStartUtc() { const now = new Date(); return this.getLocalDayStart(now); } /** * ✅ 获取目标时区的当前本地日期对象(不带时间) */ getTodayInLocal() { const now = new Date(); const localNow = this.utcToLocal(now); return new Date(Date.UTC( localNow.getUTCFullYear(), localNow.getUTCMonth(), localNow.getUTCDate() )); } /** * ✅ 提取日期部分(丢弃时间) */ getLocalDateOnly(timestamp) { const localTime = this.utcToLocal(new Date(timestamp)); return new Date(Date.UTC( localTime.getUTCFullYear(), localTime.getUTCMonth(), localTime.getUTCDate() )); } /** * ✅ 解析输入日期(字符串 / Date / 时间戳) */ parseDate(dateInput) { if (dateInput === null || dateInput === undefined) { throw new Error('Date input cannot be null or undefined'); } let date; if (dateInput instanceof Date) { date = dateInput; } else if (typeof dateInput === 'string') { if (/^\d{4}-\d{2}-\d{2}$/.test(dateInput)) { const [year, month, day] = dateInput.split('-').map(Number); date = new Date(Date.UTC(year, month - 1, day)); } else { date = new Date(dateInput); if (isNaN(date.getTime())) { throw new Error(`Invalid date string: ${dateInput}`); } } } else if (typeof dateInput === 'number') { date = new Date(dateInput); } else { throw new Error(`Invalid date input type: ${typeof dateInput}`); } return new Date(Date.UTC( date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() )); } } module.exports = TimezoneUtils;