“mitao”
2026-05-14 0c76f764b00b724c60f42fa647dd02decb9c0ca2
fix:版本更新修改
1个文件已添加
6个文件已修改
324 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/App.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/config/index.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/home.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/app-version.js 190 ●●●●● 补丁 | 查看 | 原始文档 | 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
}