|
@@ -19,18 +19,22 @@ const argVal = (name) => {
|
|
|
return i >= 0 ? args[i + 1] : undefined;
|
|
return i >= 0 ? args[i + 1] : undefined;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-function ymd(d) {
|
|
|
|
|
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
|
|
|
|
|
+// 日期口径固定 Asia/Shanghai(UTC+8,无夏令时): 容器默认 UTC 时区,若用本地年月日
|
|
|
|
|
+// 「今天/本月1日」默认窗在每天 00:00~08:00(UTC)会错位一天(2026-09-06 Docker 适配)
|
|
|
|
|
+function ymdShanghai(d) {
|
|
|
|
|
+ // 任意绝对时刻 → 北京时间 YYYY-MM-DD(内部 +8h 后读 UTC getters;输入勿再预偏移)
|
|
|
|
|
+ const t = new Date(d.getTime() + 8 * 3600 * 1000);
|
|
|
|
|
+ const p = (n) => String(n).padStart(2, '0');
|
|
|
|
|
+ return `${t.getUTCFullYear()}-${p(t.getUTCMonth() + 1)}-${p(t.getUTCDate())}`;
|
|
|
}
|
|
}
|
|
|
function parseDate(s, label) {
|
|
function parseDate(s, label) {
|
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(s ?? '')) fail(`${label} 需为 YYYY-MM-DD,收到: ${JSON.stringify(s)}`, 4);
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(s ?? '')) fail(`${label} 需为 YYYY-MM-DD,收到: ${JSON.stringify(s)}`, 4);
|
|
|
- const d = new Date(`${s}T00:00:00`);
|
|
|
|
|
|
|
+ const d = new Date(`${s}T00:00:00+08:00`); // 显式 +08:00,避免容器本地时区漂移(UTC 容器差 8h)
|
|
|
if (Number.isNaN(d.getTime())) fail(`${label} 非合法日期: ${s}`, 4);
|
|
if (Number.isNaN(d.getTime())) fail(`${label} 非合法日期: ${s}`, 4);
|
|
|
return d;
|
|
return d;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const now = new Date();
|
|
|
|
|
-const todayYmd = ymd(now);
|
|
|
|
|
|
|
+const todayYmd = ymdShanghai(new Date());
|
|
|
const startArg = argVal('--start');
|
|
const startArg = argVal('--start');
|
|
|
const endArg = argVal('--end');
|
|
const endArg = argVal('--end');
|
|
|
const type = argVal('--type') ?? 'all';
|
|
const type = argVal('--type') ?? 'all';
|
|
@@ -58,12 +62,12 @@ const auth = { base, token };
|
|
|
const reports = await fetchAllReports({
|
|
const reports = await fetchAllReports({
|
|
|
base, token,
|
|
base, token,
|
|
|
type: type === 'all' ? undefined : type,
|
|
type: type === 'all' ? undefined : type,
|
|
|
- start: ymd(start), end: ymd(end), deviceNo,
|
|
|
|
|
|
|
+ start: ymdShanghai(start), end: ymdShanghai(end), deviceNo,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 2. 逐日 reconcile(并发,有损:失败跳过但披露)
|
|
// 2. 逐日 reconcile(并发,有损:失败跳过但披露)
|
|
|
const dayList = [];
|
|
const dayList = [];
|
|
|
-for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) dayList.push(ymd(d));
|
|
|
|
|
|
|
+for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) dayList.push(ymdShanghai(d));
|
|
|
const reconcile = new Map();
|
|
const reconcile = new Map();
|
|
|
if (!noReconcile) {
|
|
if (!noReconcile) {
|
|
|
for (let i = 0; i < dayList.length; i += concurrency) {
|
|
for (let i = 0; i < dayList.length; i += concurrency) {
|
|
@@ -110,7 +114,7 @@ const actions = noReportDays.map((d) => ({
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
|
const data = {
|
|
const data = {
|
|
|
- window: { start: ymd(start), end: ymd(end), days: dayList.length, generatedAt: new Date().toISOString() },
|
|
|
|
|
|
|
+ window: { start: ymdShanghai(start), end: ymdShanghai(end), days: dayList.length, generatedAt: new Date().toISOString() },
|
|
|
type,
|
|
type,
|
|
|
deviceNo: deviceNo ?? '全部',
|
|
deviceNo: deviceNo ?? '全部',
|
|
|
totalReports: reports.length,
|
|
totalReports: reports.length,
|
|
@@ -134,7 +138,7 @@ if (jsonOut) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const lines = [];
|
|
const lines = [];
|
|
|
-lines.push(`# 录音管道区间聚合(${ymd(start)} ~ ${ymd(end)})`);
|
|
|
|
|
|
|
+lines.push(`# 录音管道区间聚合(${ymdShanghai(start)} ~ ${ymdShanghai(end)})`);
|
|
|
lines.push(`> 生成: ${data.window.generatedAt} | 口径: type=${type}, 设备=${data.deviceNo}, 窗口 ${dayList.length} 天`);
|
|
lines.push(`> 生成: ${data.window.generatedAt} | 口径: type=${type}, 设备=${data.deviceNo}, 窗口 ${dayList.length} 天`);
|
|
|
lines.push('');
|
|
lines.push('');
|
|
|
lines.push('## 总体统计');
|
|
lines.push('## 总体统计');
|