import request from '@/utils/request.js' let hasCheckedVersionThisLaunch = false export const IOS_APP_STORE_URL = 'https://apps.apple.com/cn/app/%E6%B5%B7%E8%AF%8A%E5%BA%B7%E9%81%93/id6762694057' /** * 版本检查接口要求的平台字段:iOS 传 ios,Android 传 Android。 */ export function resolveVersionCheckPlatform() { const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase() if (platform === 'android') { return 'Android' } return 'ios' } /** * 去掉展示文案里的 V 前缀,统一版本号格式。 */ export function normalizeVersionText(value) { if (typeof value !== 'string') { return '' } const trimmedValue = value.trim() if (!trimmedValue) { return '' } return trimmedValue.replace(/^[vV]/, '') } /** * 检查 APP 最新版本信息。 */ export function checkAppVersion(showLoading = true) { return request.get('/api/app/version/check', { platform: resolveVersionCheckPlatform() }, false, showLoading) } /** * 按真实接口结构归一化版本检查数据。 */ export function normalizeVersionCheckData(data) { const source = data && typeof data === 'object' ? data : {} const rawDownloadUrl = typeof source.downloadUrl === 'string' ? source.downloadUrl.trim() : '' return { hasUpdate: Boolean(source.hasUpdate), latestVersionNo: normalizeVersionText(source.latestVersionNo), latestVersionCode: Number.isFinite(Number(source.latestVersionCode)) ? Number(source.latestVersionCode) : null, forceUpdate: Boolean(source.forceUpdate), updateNotice: typeof source.updateNotice === 'string' ? source.updateNotice.trim() : '', downloadUrl: rawDownloadUrl.replace(/^`|`$/g, ''), } } /** * 比较当前版本与最新版本是否真的存在升级。 * 即使后端误返回 hasUpdate=true,只要版本号相同,前端也不再弹更新提示。 */ export function shouldShowVersionUpdate(currentVersion, latestVersion) { const safeCurrentVersion = normalizeVersionText(currentVersion) const safeLatestVersion = normalizeVersionText(latestVersion) if (!safeCurrentVersion || !safeLatestVersion) { return false } if (safeCurrentVersion === safeLatestVersion) { return false } return compareVersionText(safeCurrentVersion, safeLatestVersion) < 0 } /** * 优先读取运行时版本号,取不到时回退 manifest 配置版本。 */ export function resolveRuntimeVersion(fallbackVersion = '') { // #ifdef APP-PLUS try { const runtimeVersion = typeof plus !== 'undefined' && plus.runtime ? normalizeVersionText(plus.runtime.version) : '' if (runtimeVersion) { return runtimeVersion } } catch (e) { console.warn('[app-version] 读取运行时版本失败:', e) } // #endif return normalizeVersionText(fallbackVersion) } /** * 当前 App 运行周期内是否还需要自动检查版本。 * 返回 true 时会顺便占用这次检查机会,确保只触发一次。 */ export function shouldAutoCheckVersionOnLaunch() { if (hasCheckedVersionThisLaunch) { return false } hasCheckedVersionThisLaunch = true return true } /** * 统一处理版本更新动作。 * - Android:下载 APK 后调用系统安装 * - iOS:优先跳转 App Store */ export function startVersionUpdate(downloadUrl, onProgress) { const safeDownloadUrl = typeof downloadUrl === 'string' ? downloadUrl.trim() : '' if (!safeDownloadUrl) { uni.showToast({ title: '下载地址缺失', icon: 'none', }) return Promise.reject(new Error('downloadUrl is empty')) } // #ifdef APP-PLUS const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase() if (platform !== 'android') { notifyVersionUpdateProgress(onProgress, { status: 'redirecting', percent: 0, text: '前往 App Store', }) plus.runtime.openURL(resolveIosUpdateUrl(safeDownloadUrl)) return Promise.resolve() } return downloadAndInstallAndroidApk(safeDownloadUrl, onProgress) // #endif // #ifndef APP-PLUS uni.showToast({ title: '请在APP内执行更新', icon: 'none', }) return Promise.resolve() // #endif } /** * iOS 更新地址统一转成 App Store 可识别的链接。 * 优先使用 itms-apps:// 协议直接拉起 App Store。 */ function resolveIosUpdateUrl(downloadUrl) { const candidateUrl = IOS_APP_STORE_URL || downloadUrl if (typeof candidateUrl !== 'string') { return '' } const safeDownloadUrl = candidateUrl.trim() if (!safeDownloadUrl) { return '' } if (safeDownloadUrl.startsWith('itms-apps://')) { return safeDownloadUrl } if (safeDownloadUrl.includes('apps.apple.com') || safeDownloadUrl.includes('itunes.apple.com')) { return safeDownloadUrl.replace(/^https?:\/\//i, 'itms-apps://') } return safeDownloadUrl } /** * Android 端下载 APK 后拉起系统安装。 */ function downloadAndInstallAndroidApk(downloadUrl, onProgress) { return new Promise((resolve, reject) => { try { const downloadTarget = `_doc/update/app-release-${Date.now()}.apk` notifyVersionUpdateProgress(onProgress, { status: 'downloading', percent: 0, text: '正在下载 0%', }) const downloadTask = plus.downloader.createDownload( downloadUrl, { filename: downloadTarget, }, (download, status) => { if (status !== 200 || !download || !download.filename) { uni.showToast({ title: '下载更新失败', icon: 'none', }) notifyVersionUpdateProgress(onProgress, { status: 'failed', percent: 0, text: '立即更新', }) reject(new Error(`download failed: ${status}`)) return } notifyVersionUpdateProgress(onProgress, { status: 'downloaded', percent: 100, text: '安装中...', }) installDownloadedApk(download.filename, resolve, reject, onProgress) } ) // 监听下载进度,把百分比回传给页面,直接展示到弹窗按钮文案。 downloadTask.addEventListener('statechanged', (task) => { const totalSize = Number(task && task.totalSize) || 0 const downloadedSize = Number(task && task.downloadedSize) || 0 if (totalSize <= 0 || downloadedSize < 0) { return } const percent = Math.min(99, Math.max(0, Math.floor((downloadedSize / totalSize) * 100))) notifyVersionUpdateProgress(onProgress, { status: 'downloading', percent, text: `正在下载 ${percent}%`, }) }) downloadTask.start() } catch (e) { uni.showToast({ title: '启动下载失败', icon: 'none', }) notifyVersionUpdateProgress(onProgress, { status: 'failed', percent: 0, text: '立即更新', }) reject(e) } }) } /** * APK 下载完成后,交给系统安装器处理。 */ function installDownloadedApk(filePath, resolve, reject, onProgress) { try { notifyVersionUpdateProgress(onProgress, { status: 'installing', percent: 100, text: '安装中...', }) // 这里直接交给系统安装器处理,用户确认后即可安装新版本。 plus.runtime.install( filePath, {}, () => { notifyVersionUpdateProgress(onProgress, { status: 'completed', percent: 100, text: '立即更新', }) resolve(filePath) }, (error) => { console.error('[app-version] 安装 APK 失败:', error) uni.showToast({ title: '拉起安装失败', icon: 'none', }) notifyVersionUpdateProgress(onProgress, { status: 'failed', percent: 100, text: '立即更新', }) reject(error) } ) } catch (e) { uni.showToast({ title: '拉起安装失败', icon: 'none', }) notifyVersionUpdateProgress(onProgress, { status: 'failed', percent: 100, text: '立即更新', }) reject(e) } } /** * 通知页面当前更新进度,页面可把文案直接展示到弹窗按钮。 */ function notifyVersionUpdateProgress(onProgress, progressInfo) { if (typeof onProgress !== 'function') { return } try { onProgress(progressInfo) } catch (e) { console.warn('[app-version] 更新进度回调失败:', e) } } /** * 逐段比较版本号,返回值: * -1:current < latest * 0:current = latest * 1:current > latest */ function compareVersionText(currentVersion, latestVersion) { const currentParts = splitVersionText(currentVersion) const latestParts = splitVersionText(latestVersion) const maxLength = Math.max(currentParts.length, latestParts.length) for (let index = 0; index < maxLength; index += 1) { const currentPart = currentParts[index] || 0 const latestPart = latestParts[index] || 0 if (currentPart < latestPart) { return -1 } if (currentPart > latestPart) { return 1 } } return 0 } /** * 把版本号拆成数字数组,便于逐段比较。 */ function splitVersionText(versionText) { return String(versionText || '') .split('.') .map((item) => Number.parseInt(item, 10)) .filter((item) => Number.isFinite(item)) }