| tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/App.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/config/index.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/pages/home/home.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traditionalApp/utils/app-version.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
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)); } } 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) 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"); 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 }) /** 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/' }; 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(); }; /** traditionalApp/utils/app-version.js
New file @@ -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 }