From 0c76f764b00b724c60f42fa647dd02decb9c0ca2 Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期四, 14 五月 2026 16:41:04 +0800
Subject: [PATCH] fix:版本更新修改

---
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java     |   60 +++++++++
 traditionalApp/pages/home/home.vue                                                                                   |   25 ----
 traditionalApp/utils/app-version.js                                                                                  |  190 +++++++++++++++++++++++++++++++
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java      |    5 
 tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java |   33 +++++
 traditionalApp/App.vue                                                                                               |    8 +
 traditionalApp/config/index.js                                                                                       |    3 
 7 files changed, 295 insertions(+), 29 deletions(-)

diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java
index 0d25e9d..015f1be 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java
@@ -31,7 +31,8 @@
      */
     @GetMapping("/check")
     public ApiResponse<AppVersionCheckVO> checkUpdate(@RequestParam String platform,
-                                                      @RequestParam Integer currentVersionCode) {
-        return ApiResponse.ok(appVersionSettingsAppService.checkUpdate(platform, currentVersionCode));
+                                                      @RequestParam Integer currentVersionCode,
+                                                      @RequestParam(required = false) String currentVersionNo) {
+        return ApiResponse.ok(appVersionSettingsAppService.checkUpdate(platform, currentVersionCode, currentVersionNo));
     }
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java
index 1102310..ce43d2c 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java
@@ -65,6 +65,10 @@
     }
 
     public AppVersionCheckVO checkUpdate(String platform, Integer currentVersionCode) {
+        return checkUpdate(platform, currentVersionCode, null);
+    }
+
+    public AppVersionCheckVO checkUpdate(String platform, Integer currentVersionCode, String currentVersionNo) {
         if (currentVersionCode == null) {
             throw new IllegalArgumentException("当前版本号不能为空");
         }
@@ -82,7 +86,7 @@
         String downloadUrl = AppVersionConfig.PLATFORM_ANDROID.equals(normalizedPlatform)
                 ? normalizeOptionalText(latest.getAndroidFileUrl())
                 : normalizeOptionalText(latest.getIosFileUrl());
-        boolean hasUpdate = downloadUrl != null && defaultNumber(latest.getVersionCode()) > currentVersionCode;
+        boolean hasUpdate = downloadUrl != null && isNewerVersion(latest, currentVersionCode, currentVersionNo);
         vo.setHasUpdate(hasUpdate);
         if (!hasUpdate) {
             return vo;
@@ -95,6 +99,60 @@
         return vo;
     }
 
+    /**
+     * 判断后台最新版本是否高于当前 APP,兼容原生 versionCode 与展示 versionNo 不一致的历史包。
+     */
+    private boolean isNewerVersion(AppVersionConfig latest, Integer currentVersionCode, String currentVersionNo) {
+        if (defaultNumber(latest.getVersionCode()) > currentVersionCode) {
+            return true;
+        }
+        return compareVersionNo(latest.getVersionNo(), currentVersionNo) > 0;
+    }
+
+    /**
+     * 比较数字点号版本号,无法解析的版本号不参与兜底判断。
+     */
+    private int compareVersionNo(String latestVersionNo, String currentVersionNo) {
+        String latest = normalizeOptionalText(latestVersionNo);
+        String current = normalizeOptionalText(currentVersionNo);
+        if (latest == null || current == null) {
+            return 0;
+        }
+        String[] latestParts = latest.split("\\.");
+        String[] currentParts = current.split("\\.");
+        int maxLength = Math.max(latestParts.length, currentParts.length);
+        for (int i = 0; i < maxLength; i++) {
+            Integer latestNumber = parseVersionNumber(latestParts, i);
+            Integer currentNumber = parseVersionNumber(currentParts, i);
+            if (latestNumber == null || currentNumber == null) {
+                return 0;
+            }
+            int compareResult = latestNumber.compareTo(currentNumber);
+            if (compareResult != 0) {
+                return compareResult;
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * 解析指定版本号片段,缺失片段按 0 处理。
+     */
+    private Integer parseVersionNumber(String[] parts, int index) {
+        if (index >= parts.length) {
+            return 0;
+        }
+        String value = parts[index] == null ? "" : parts[index].trim();
+        if (!value.matches("\\d+")) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(value);
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
     private AppVersionConfig latest() {
         return appVersionConfigMapper.selectOne(new LambdaQueryWrapper<AppVersionConfig>()
                 .eq(AppVersionConfig::getDeleted, 0)
diff --git a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java
index c7b7879..696076d 100644
--- a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java
+++ b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java
@@ -88,6 +88,39 @@
     }
 
     @Test
+    void checkUpdateShouldUseVersionNoWhenVersionCodeIsNotAligned() {
+        AppVersionConfig latest = new AppVersionConfig();
+        latest.setVersionNo("1.0.12");
+        latest.setVersionCode(12);
+        latest.setAndroidFileUrl("https://cdn.test/app-android.apk");
+        latest.setForceUpdate(0);
+        latest.setUpdateNotice("新增版本内容");
+        when(appVersionConfigMapper.selectOne(any())).thenReturn(latest);
+
+        AppVersionCheckVO result = appVersionSettingsAppService.checkUpdate("ANDROID", 111, "1.0.11");
+
+        assertTrue(result.getHasUpdate());
+        assertEquals("1.0.12", result.getLatestVersionNo());
+        assertEquals("https://cdn.test/app-android.apk", result.getDownloadUrl());
+    }
+
+    @Test
+    void checkUpdateShouldReturnFalseWhenPlatformPackageIsMissing() {
+        AppVersionConfig latest = new AppVersionConfig();
+        latest.setVersionNo("1.0.12");
+        latest.setVersionCode(12);
+        latest.setAndroidFileUrl("https://cdn.test/app-android.apk");
+        latest.setForceUpdate(0);
+        latest.setUpdateNotice("新增版本内容");
+        when(appVersionConfigMapper.selectOne(any())).thenReturn(latest);
+
+        AppVersionCheckVO result = appVersionSettingsAppService.checkUpdate("IOS", 1, "1.0.1");
+
+        assertFalse(result.getHasUpdate());
+        assertNull(result.getDownloadUrl());
+    }
+
+    @Test
     void checkUpdateShouldReturnFalseWhenCurrentVersionIsLatest() {
         AppVersionConfig latest = new AppVersionConfig();
         latest.setVersionNo("1.0.5");
diff --git a/traditionalApp/App.vue b/traditionalApp/App.vue
index e4e6a8a..612afdd 100644
--- a/traditionalApp/App.vue
+++ b/traditionalApp/App.vue
@@ -11,6 +11,10 @@
 	import { cacheInviteCode } from '@/utils/invite-cache.js'
 	import { initJPushLifecycle } from '@/utils/push.js'
 	import {
+		checkAppVersionUpdate,
+		resumeForceUpdatePrompt
+	} from '@/utils/app-version.js'
+	import {
 		getOpeninstallInstallData,
 		initOpeninstall,
 		registerOpeninstallWakeUp
@@ -27,6 +31,7 @@
 		initOpeninstall()
 		registerOpeninstallWakeUp(handleOpeninstallPayload)
 		getOpeninstallInstallData(handleOpeninstallPayload)
+		checkAppVersionUpdate()
 		// #endif
 	})
 
@@ -35,6 +40,9 @@
 	 */
 	onShow(() => {
 		handleOpenByScheme()
+		// #ifdef APP-PLUS
+		resumeForceUpdatePrompt()
+		// #endif
 	})
 
 	/**
diff --git a/traditionalApp/config/index.js b/traditionalApp/config/index.js
index e889a40..9934f0b 100644
--- a/traditionalApp/config/index.js
+++ b/traditionalApp/config/index.js
@@ -1,6 +1,7 @@
 export default {
 	// BASE_URL: 'http://192.168.110.188:48080',
 	BASE_URL: 'https://haizhentong.com',
+	APP_STORE_URL: '',
 	uploadUrl: "https://haizhentong.com/api/open/file/upload",
 	imageUrl: 'http://221.182.45.100:11224/images/'
-};
\ No newline at end of file
+};
diff --git a/traditionalApp/pages/home/home.vue b/traditionalApp/pages/home/home.vue
index 31c4a00..ac75847 100644
--- a/traditionalApp/pages/home/home.vue
+++ b/traditionalApp/pages/home/home.vue
@@ -107,13 +107,10 @@
 				</view>
 			</view>
 		</view>
-
-		<UpdatePopup :show="showUpdatePopup" @close="closeUpdatePopup" @submit="handleUpdateConfirm" />
 	</view>
 </template>
 
 <script setup lang="ts">
-	import UpdatePopup from "@/components/updatePopup.vue";
 	import { ref, onMounted, computed } from "vue";
 	import { onPageScroll, onPullDownRefresh, onShow } from "@dcloudio/uni-app";
 	import { getBannerList, getHomeFeaturedContent, getHomeLiveCourseList } from "./service";
@@ -163,7 +160,6 @@
 
 	const statusBarHeight = ref(0);
 	const scrollY = ref(0);
-	const showUpdatePopup = ref(false);
 	const bannerList = ref<HomeBannerItem[]>([]);
 	const bannerActiveIndex = ref(0);
 	const liveCourseList = ref<HomeLiveCourseItem[]>([]);
@@ -463,27 +459,6 @@
 		window.open(targetUrl, '_blank');
 		return;
 		// #endif
-	};
-
-	/**
-	 * 打开更新弹窗。
-	 */
-	const handleOpenUpdate = () => {
-		showUpdatePopup.value = true;
-	};
-
-	/**
-	 * 关闭更新弹窗。
-	 */
-	const closeUpdatePopup = () => {
-		showUpdatePopup.value = false;
-	};
-
-	/**
-	 * 确认更新后关闭弹窗。
-	 */
-	const handleUpdateConfirm = () => {
-		closeUpdatePopup();
 	};
 
 	/**
diff --git a/traditionalApp/utils/app-version.js b/traditionalApp/utils/app-version.js
new file mode 100644
index 0000000..c1ff6e7
--- /dev/null
+++ b/traditionalApp/utils/app-version.js
@@ -0,0 +1,190 @@
+import config from '@/config/index.js'
+
+const CHECK_VERSION_URL = '/api/app/version/check'
+let checkingVersion = false
+let updatePopupVisible = false
+let latestUpdateInfo = null
+
+/**
+ * 检查 APP 是否存在管理后台配置的新版本。
+ */
+export function checkAppVersionUpdate() {
+	// #ifndef APP-PLUS
+	return
+	// #endif
+	if (checkingVersion) {
+		return
+	}
+	checkingVersion = true
+	resolveRuntimeVersionInfo()
+		.then((runtimeInfo) => requestVersionCheck(runtimeInfo))
+		.then((payload) => {
+			const updateInfo = payload && payload.code === 0 ? payload.data : null
+			if (!updateInfo || updateInfo.hasUpdate !== true) {
+				return
+			}
+			const downloadUrl = resolveUpdateUrl(updateInfo)
+			if (!downloadUrl) {
+				return
+			}
+			showUpdateModal(Object.assign({}, updateInfo, {
+				downloadUrl
+			}))
+		})
+		.catch(() => {
+		})
+		.finally(() => {
+			checkingVersion = false
+		})
+}
+
+/**
+ * 读取 APP 原生版本信息,优先使用 manifest 中的 versionCode。
+ */
+function resolveRuntimeVersionInfo() {
+	return new Promise((resolve) => {
+		const fallbackInfo = {
+			platform: resolvePlatform(),
+			versionCode: 0,
+			versionName: ''
+		}
+		if (typeof plus === 'undefined' || !plus.runtime || typeof plus.runtime.getProperty !== 'function') {
+			resolve(fallbackInfo)
+			return
+		}
+		plus.runtime.getProperty(plus.runtime.appid, (info) => {
+			resolve({
+				platform: resolvePlatform(),
+				versionCode: normalizeVersionCode(info && info.versionCode),
+				versionName: normalizeText(info && info.version)
+			})
+		})
+	})
+}
+
+/**
+ * 将当前运行平台转换为后端版本检查平台值。
+ */
+function resolvePlatform() {
+	const platform = normalizeText(uni.getSystemInfoSync().platform).toLowerCase()
+	return platform === 'ios' ? 'IOS' : 'ANDROID'
+}
+
+/**
+ * 请求后端版本检查接口。
+ */
+function requestVersionCheck(runtimeInfo) {
+	return new Promise((resolve, reject) => {
+		uni.request({
+			url: `${config.BASE_URL}${CHECK_VERSION_URL}`,
+			method: 'GET',
+			data: {
+				platform: runtimeInfo.platform,
+				currentVersionCode: runtimeInfo.versionCode,
+				currentVersionNo: runtimeInfo.versionName
+			},
+			header: {
+				'content-type': 'application/json'
+			},
+			success: (res) => {
+				resolve(res.data || {})
+			},
+			fail: reject
+		})
+	})
+}
+
+/**
+ * 解析当前平台可用的更新地址,iOS 只允许跳转 App Store。
+ */
+function resolveUpdateUrl(updateInfo) {
+	if (isIosPlatform()) {
+		return normalizeText(config.APP_STORE_URL)
+	}
+	return normalizeText(updateInfo.downloadUrl)
+}
+
+/**
+ * 展示版本更新弹窗,强制更新时禁止跳过。
+ */
+function showUpdateModal(updateInfo) {
+	latestUpdateInfo = updateInfo
+	if (updatePopupVisible) {
+		return
+	}
+	updatePopupVisible = true
+	const forceUpdate = updateInfo.forceUpdate === true
+	let confirmed = false
+	uni.showModal({
+		title: forceUpdate ? '发现新版本,请立即更新' : '发现新版本',
+		content: buildUpdateContent(updateInfo),
+		showCancel: !forceUpdate,
+		cancelText: '暂不更新',
+		confirmText: '立即更新',
+		success: (modalResult) => {
+			if (modalResult.confirm) {
+				confirmed = true
+				openDownloadUrl(updateInfo.downloadUrl)
+				return
+			}
+		},
+		complete: () => {
+			updatePopupVisible = false
+			if (forceUpdate && !confirmed) {
+				setTimeout(() => {
+					showUpdateModal(updateInfo)
+				}, 100)
+			}
+		}
+	})
+}
+
+/**
+ * 拼接更新提示内容,确保后台更新说明能直接展示给用户。
+ */
+function buildUpdateContent(updateInfo) {
+	const versionNo = normalizeText(updateInfo.latestVersionNo)
+	const updateNotice = normalizeText(updateInfo.updateNotice) || '请更新到最新版本后继续使用。'
+	return versionNo ? `最新版本:${versionNo}\n${updateNotice}` : updateNotice
+}
+
+/**
+ * 打开后台配置的安装包或下载地址。
+ */
+function openDownloadUrl(downloadUrl) {
+	if (!downloadUrl) {
+		return
+	}
+	plus.runtime.openURL(downloadUrl)
+}
+
+/**
+ * APP 回到前台时,强制更新场景需要继续拦截用户使用旧版本。
+ */
+export function resumeForceUpdatePrompt() {
+	if (latestUpdateInfo && latestUpdateInfo.forceUpdate === true && !updatePopupVisible) {
+		showUpdateModal(latestUpdateInfo)
+	}
+}
+
+/**
+ * 判断当前是否为 iOS APP。
+ */
+function isIosPlatform() {
+	return resolvePlatform() === 'IOS'
+}
+
+/**
+ * 规范化可选字符串。
+ */
+function normalizeText(value) {
+	return String(value || '').trim()
+}
+
+/**
+ * 规范化原生 versionCode,避免异常值传到后端。
+ */
+function normalizeVersionCode(value) {
+	const versionCode = Number(value || 0)
+	return Number.isFinite(versionCode) && versionCode >= 0 ? versionCode : 0
+}

--
Gitblit v1.7.1