From e7de52a6d1b6d9bebeb94f2566dcffbf7de29a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E9=93=B6=E5=86=9B?= Date: Sat, 19 Sep 2026 23:31:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(scan):=20=E4=BB=8A=E6=97=A5=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=BB=9F=E8=AE=A1=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=8C=89=E5=8C=97=E4=BA=AC=E6=97=B6=E9=97=B4=E6=8D=A2=E7=AE=97?= =?UTF-8?q?=E5=B9=B6=E8=AE=A1=E5=85=A5=20.tgz=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=97=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lastUpdated 原取 package.json time 字段的 UTC 日期截断,跨 8 小时会显示为前一天(如 UTC 09-18 20:00 = 北京 09-19),导致「今日更新包数」漏计 - 版本发布日改用北京时间(Intl.DateTimeFormat Asia/Shanghai)换算 - 新增 .tgz 文件 mtime 作为补充:今天拉取缓存的包即使发布时间是昨天,也计入「今天更新」 --- backend/scanner.js | 52 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/backend/scanner.js b/backend/scanner.js index a58969e..8ed17f5 100644 --- a/backend/scanner.js +++ b/backend/scanner.js @@ -88,6 +88,27 @@ function parseVersion(v) { return (String(v).replace(/^v/, '') || '').trim() } +// —— 日期口径(项目统一北京时间,避免 UTC slice 跨日偏差) —— +const SH_DATE = new Intl.DateTimeFormat('en-CA', { + timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', +}) +/** ISO 字符串 → 北京时间日期(YYYY-MM-DD);time 字段为 UTC,直接 slice 会与本地跨日 */ +function shDate(iso) { + if (!iso) return null + const d = new Date(iso) + return Number.isNaN(d.getTime()) ? null : SH_DATE.format(d) +} +/** 本地时区日期(YYYY-MM-DD):.tgz 文件 mtime 为本地时间,与前端 todayStr 同口径 */ +function localDate(d) { + const p = n => String(n).padStart(2, '0') + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` +} +/** 最近更新 = max(元数据最新版本发布日, 最新 .tgz 缓存日) —— 今天拉取缓存也算「今天更新」 */ +function latestDate(metaUpdated, tgzMtime) { + const tgzUpdated = tgzMtime ? localDate(new Date(tgzMtime)) : null + return [metaUpdated, tgzUpdated].filter(Boolean).sort().pop() || null +} + /** * 扫描 storage 目录 * @returns {Array} 包列表(versionsDetail: [{version, size(MB), date}]) @@ -112,12 +133,15 @@ export function scanStorage(storageDir) { // 先统计再读元数据:即使元数据损坏也能算出真实占用 const tgzSizes = new Map() let tgzTotal = 0 + let tgzLatest = 0 // 最新 .tgz 文件 mtime(毫秒):今天缓存了新版本也算「今天更新」 try { for (const f of fs.readdirSync(pkgDir)) { if (!f.endsWith('.tgz')) continue - const size = fs.statSync(path.join(pkgDir, f)).size + const st = fs.statSync(path.join(pkgDir, f)) + const size = st.size tgzTotal += size tgzSizes.set(f.slice(0, -4), size) + if (st.mtimeMs > tgzLatest) tgzLatest = st.mtimeMs } } catch {} @@ -129,7 +153,7 @@ export function scanStorage(storageDir) { status: 'broken', versions: 0, size: fmtMb(tgzTotal), - lastUpdated: null, + lastUpdated: latestDate(null, tgzLatest), versionsDetail: [], }) continue @@ -168,16 +192,17 @@ export function scanStorage(storageDir) { versionsDetail.push({ version: parseVersion(v), size: fmtMb(size), - date: timeObj[v] ? timeObj[v].slice(0, 10) : null, + date: timeObj[v] ? shDate(timeObj[v]) : null, }) } // 按 semver 数值排序(旧 → 新) versionsDetail.sort((a, b) => a.version.localeCompare(b.version, undefined, { numeric: true })) - // 最近更新时间:取 time 字段最大 - const lastUpdated = Object.keys(timeObj).length - ? Object.entries(timeObj).sort((a, b) => b[1].localeCompare(a[1]))[0][1].slice(0, 10) + // 最近更新:取「元数据最新版本发布日」与「最新 .tgz 缓存日」中较新者(今天拉缓存也算今天更新) + const metaUpdated = Object.keys(timeObj).length + ? Object.entries(timeObj).map(([, iso]) => shDate(iso)).filter(Boolean).sort().pop() : null + const lastUpdated = latestDate(metaUpdated, tgzLatest) packages.push({ name: pkgNameFromDir(pkgDir, storageRoot), @@ -224,13 +249,16 @@ export async function scanStorageAsync(storageDir, onProgress) { const tgzSizes = new Map() let tgzTotal = 0 + let tgzLatest = 0 // 最新 .tgz 文件 mtime(毫秒):今天缓存了新版本也算「今天更新」 try { for (const f of fs.readdirSync(pkgDir)) { - const size = fs.statSync(path.join(pkgDir, f)).size + const st = fs.statSync(path.join(pkgDir, f)) + const size = st.size scannedBytes += size if (f.endsWith('.tgz')) { tgzTotal += size tgzSizes.set(f.slice(0, -4), size) + if (st.mtimeMs > tgzLatest) tgzLatest = st.mtimeMs } } } catch {} @@ -243,7 +271,7 @@ export async function scanStorageAsync(storageDir, onProgress) { status: 'broken', versions: 0, size: fmtMb(tgzTotal), - lastUpdated: null, + lastUpdated: latestDate(null, tgzLatest), versionsDetail: [], }) } else { @@ -277,13 +305,15 @@ export async function scanStorageAsync(storageDir, onProgress) { versionsDetail.push({ version: parseVersion(v), size: fmtMb(size), - date: timeObj[v] ? timeObj[v].slice(0, 10) : null, + date: timeObj[v] ? shDate(timeObj[v]) : null, }) } versionsDetail.sort((a, b) => a.version.localeCompare(b.version, undefined, { numeric: true })) - const lastUpdated = Object.keys(timeObj).length - ? Object.entries(timeObj).sort((a, b) => b[1].localeCompare(a[1]))[0][1].slice(0, 10) + // 最近更新:取「元数据最新版本发布日」与「最新 .tgz 缓存日」中较新者(今天拉缓存也算今天更新) + const metaUpdated = Object.keys(timeObj).length + ? Object.entries(timeObj).map(([, iso]) => shDate(iso)).filter(Boolean).sort().pop() : null + const lastUpdated = latestDate(metaUpdated, tgzLatest) packages.push({ name: pkgNameFromDir(pkgDir, storageRoot), versions: versionsDetail.length,