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,