diff --git a/frontend-vue/src/shared/composables/useVersionUpdate.ts b/frontend-vue/src/shared/composables/useVersionUpdate.ts index 9c058add..9de1f752 100644 --- a/frontend-vue/src/shared/composables/useVersionUpdate.ts +++ b/frontend-vue/src/shared/composables/useVersionUpdate.ts @@ -8,6 +8,7 @@ import { computed, ref } from 'vue' import { getPywebviewApi } from '@/shared/bridges/pywebview' +import { compareVersions, normVersion } from '@/shared/utils/version-compare' import { normalizeUpdateProgress, type UpdateProgress, @@ -32,10 +33,6 @@ function bindProgressListener() { }) } -function normVersion(value: unknown): string { - return String(value ?? '').trim().replace(/^[vV]/, '') -} - async function fetchJson(url: string): Promise> { const resp = await window.fetch(url, { credentials: 'same-origin' }) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) @@ -116,11 +113,16 @@ export function useVersionUpdate() { : `线上最新版本 v${latest.version},暂无下载地址` return } - hasUpdate.value = local !== latest.version + // 只有线上版本严格高于本机版本才算"有更新":本机领先(灰度/回滚/漏发版) + // 时不能反过来提示更新,否则会把用户往低版本上引导。 + const diff = compareVersions(latest.version, local) + hasUpdate.value = diff > 0 canDownload.value = hasUpdate.value && Boolean(latest.fileUrl) hint.value = hasUpdate.value ? `发现新版本 v${latest.version}${latest.fileUrl ? '' : ',暂无下载地址'}` - : `已是最新版本 v${local}` + : diff < 0 + ? `当前版本 v${local} 高于线上发布版本 v${latest.version},无需更新` + : `已是最新版本 v${local}` } catch { hasUpdate.value = false canDownload.value = false diff --git a/frontend-vue/src/shared/utils/version-compare.ts b/frontend-vue/src/shared/utils/version-compare.ts new file mode 100644 index 00000000..95bec0c1 --- /dev/null +++ b/frontend-vue/src/shared/utils/version-compare.ts @@ -0,0 +1,26 @@ +/** + * 客户端版本号归一化与比较(桌面端更新检测用)。 + * + * 只比数字段、缺失段按 0 补齐(3.0.7 == 3.0.7.0);非纯数字段(如 + * 3.0.71-beta 的 beta)按 0 处理,避免 NaN 让比较结果乱序。 + */ + +/** 归一化版本号:去空白、去前缀 v/V,便于 'v3.0.71' 与 '3.0.71' 相等比较。 */ +export function normVersion(value: unknown): string { + return String(value ?? '').trim().replace(/^[vV]/, '') +} + +/** 返回 >0 表示 a 比 b 新,<0 表示 a 比 b 旧,0 表示相同。 */ +export function compareVersions(a: string, b: string): number { + const pa = normVersion(a).split('.') + const pb = normVersion(b).split('.') + const len = Math.max(pa.length, pb.length) + for (let i = 0; i < len; i++) { + const na = parseInt(pa[i] ?? '', 10) + const nb = parseInt(pb[i] ?? '', 10) + const va = Number.isFinite(na) ? na : 0 + const vb = Number.isFinite(nb) ? nb : 0 + if (va !== vb) return va - vb + } + return 0 +} diff --git a/frontend-vue/tests/version-compare.test.ts b/frontend-vue/tests/version-compare.test.ts new file mode 100644 index 00000000..7a6e138d --- /dev/null +++ b/frontend-vue/tests/version-compare.test.ts @@ -0,0 +1,39 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' + +import { compareVersions, normVersion } from '../src/shared/utils/version-compare.ts' + +test('版本比较:线上高于本地时返回正数(应提示更新)', () => { + assert.ok(compareVersions('3.0.71', '3.0.70') > 0) + assert.ok(compareVersions('3.1.0', '3.0.71') > 0) +}) + +test('版本比较:线上低于本地时返回负数(不得提示更新)', () => { + // 现网真实故障场景:本地已升到 3.0.71,线上发布记录仍是 3.0.70, + // 旧逻辑 local !== latest 会误报"发现新版本 v3.0.70"并诱导降级。 + assert.ok(compareVersions('3.0.70', '3.0.71') < 0) + assert.ok(compareVersions('2.9.9', '3.0.0') < 0) +}) + +test('版本比较:完全相同(含 v 前缀、段数补齐)返回 0', () => { + assert.equal(compareVersions('3.0.71', '3.0.71'), 0) + assert.equal(compareVersions('v3.0.71', '3.0.71'), 0) + assert.equal(compareVersions('3.0.7', '3.0.7.0'), 0) +}) + +test('版本比较:按数字段而非字符串比较(3.0.9 < 3.0.10)', () => { + assert.ok(compareVersions('3.0.9', '3.0.10') < 0) + assert.ok(compareVersions('3.0.10', '3.0.9') > 0) +}) + +test('版本比较:非数字段按 0 处理,不得产生 NaN 乱序', () => { + assert.equal(compareVersions('3.0.71-beta', '3.0.71'), 0) + assert.ok(compareVersions('3.0.72', '3.0.71-beta') > 0) +}) + +test('版本归一化:去空白与 v/V 前缀', () => { + assert.equal(normVersion(' v3.0.71 '), '3.0.71') + assert.equal(normVersion('V3.0.71'), '3.0.71') + assert.equal(normVersion(null), '') + assert.equal(normVersion(undefined), '') +})