fix(scan): 今日更新统计修复——按北京时间换算并计入 .tgz 缓存日

- lastUpdated 原取 package.json time 字段的 UTC 日期截断,跨 8 小时会显示为前一天(如 UTC 09-18 20:00 = 北京 09-19),导致「今日更新包数」漏计
- 版本发布日改用北京时间(Intl.DateTimeFormat Asia/Shanghai)换算
- 新增 .tgz 文件 mtime 作为补充:今天拉取缓存的包即使发布时间是昨天,也计入「今天更新」
This commit is contained in:
陈银军
2026-09-19 23:31:19 +08:00
parent 02ac744e13
commit e7de52a6d1
+41 -11
View File
@@ -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,