import appManifest from '@/manifest.json' const updateEndpoint = String(import.meta.env.VITE_PDA_UPDATE_URL || '').trim() let checking = false function request(options) { return new Promise((resolve, reject) => { uni.request({ ...options, success: resolve, fail: reject }) }) } function getRuntimeInfo() { return new Promise((resolve, reject) => { if (typeof plus === 'undefined' || !plus.runtime) { reject(new Error('当前环境不支持 App 在线更新')) return } // 标准真机基座的版本属于 HBuilderX;调试更新按当前工程版本模拟。 const debugBase = import.meta.env.DEV || uni.getAppBaseInfo().packageName === 'io.dcloud.HBuilder' const runtime = { appid: String((debugBase ? appManifest.appid : plus.runtime.appid) || ''), appVersion: String((debugBase ? appManifest.versionName : plus.runtime.version) || ''), appVersionCode: Number(debugBase ? appManifest.versionCode : plus.runtime.versionCode) } if (!runtime.appid || !runtime.appVersion || !Number.isInteger(runtime.appVersionCode) || runtime.appVersionCode < 1) { reject(new Error('无法读取当前应用版本,请重新运行 App')) return } if (debugBase) { resolve({ ...runtime, wgtVersion: runtime.appVersion }) return } // 正式 APK 始终保留原生基座版本码,WGT 升级只改变资源版本。 plus.runtime.getProperty(plus.runtime.appid, (properties) => { resolve({ ...runtime, wgtVersion: String(properties?.version || runtime.appVersion) }) }) }) } function normalizeUpdate(payload, runtime) { const value = payload && typeof payload === 'object' ? payload : {} if ('code' in value && String(value.code) !== '200') { const error = new Error(String(value.message || '检查更新失败,请稍后重试')) error.code = value.code error.requestId = value.requestId error.traceId = value.traceId throw error } if (typeof value.update !== 'boolean') throw new Error('更新服务返回了无效的版本信息') if (!value.update) return null if (value.appid !== runtime.appid) throw new Error('更新包与当前应用不匹配') const packageType = String(value.packageType || '').toLowerCase() const downloadUrl = String(value.downloadUrl || '').trim() const versionName = String(value.versionName || '').trim() if (!['apk', 'wgt'].includes(packageType) || !downloadUrl || !/^\d+(\.\d+)+$/.test(versionName)) { throw new Error('更新服务返回了无效的版本信息') } if (!/^https:\/\//i.test(downloadUrl)) throw new Error('更新包必须使用 HTTPS 地址') const minAppVersionCode = Number(value.minAppVersionCode) if (packageType === 'wgt' && (!Number.isInteger(minAppVersionCode) || minAppVersionCode < 1 || minAppVersionCode > runtime.appVersionCode)) { throw new Error('当前安装包不支持此热更新,请安装新版 APK') } return { packageType, downloadUrl, versionName, versionCode: Number(value.versionCode || 0), minAppVersionCode, mandatory: value.mandatory === true, releaseNotes: String(value.releaseNotes || '').trim() } } function compareVersions(left, right) { const leftParts = String(left).split('.').map(Number) const rightParts = String(right).split('.').map(Number) const length = Math.max(leftParts.length, rightParts.length) for (let index = 0; index < length; index += 1) { const difference = (leftParts[index] || 0) - (rightParts[index] || 0) if (difference !== 0) return difference } return 0 } function isNewerUpdate(update, runtime) { if (update.packageType === 'apk') { return Number.isInteger(update.versionCode) && update.versionCode > runtime.appVersionCode } return compareVersions(update.versionName, runtime.wgtVersion) > 0 } function confirmUpdate(update) { const notes = update.releaseNotes || '修复已知问题并提升作业稳定性。' return new Promise((resolve) => { uni.showModal({ title: `${update.mandatory ? '必须' : '发现'}更新 ${update.versionName}`, content: notes, confirmText: '立即更新', cancelText: '稍后再说', showCancel: !update.mandatory, success: ({ confirm }) => resolve(Boolean(confirm)), fail: () => resolve(false) }) }) } function downloadUpdate(update) { return new Promise((resolve, reject) => { uni.showLoading({ title: '正在下载 0%', mask: true }) const task = uni.downloadFile({ url: update.downloadUrl, success: (result) => { if (result.statusCode === 200 && result.tempFilePath) { resolve(result.tempFilePath) return } reject(new Error(`更新包下载失败(${result.statusCode || '未知状态'})`)) }, fail: () => reject(new Error('更新包下载失败,请检查网络后重试')), complete: () => uni.hideLoading() }) task.onProgressUpdate?.(({ progress }) => { uni.showLoading({ title: `正在下载 ${progress}%`, mask: true }) }) }) } function prepareUpdateFile(filePath, update) { return new Promise((resolve, reject) => { const fail = (cause) => { const error = new Error('更新包文件准备失败,请重新下载') error.stage = 'prepare' error.code = cause?.code reject(error) } // 下载接口以 /package 结尾,临时文件可能没有安装器要求的扩展名。 const filename = `overseas-pda-${update.versionName}-${Date.now()}.${update.packageType}` plus.io.resolveLocalFileSystemURL(filePath, (entry) => { entry.getParent((parent) => { entry.moveTo(parent, filename, (file) => resolve(file.toLocalURL()), fail) }, fail) }, fail) }) } function installUpdate(filePath) { return new Promise((resolve, reject) => { plus.runtime.install( filePath, { force: false }, resolve, (cause) => { const code = cause?.code == null ? '' : `(${cause.code})` const reason = String(cause?.message || '请重新下载后重试') const error = new Error(`更新包安装失败${code}:${reason}`) error.stage = 'install' error.code = cause?.code reject(error) } ) }) } function showFailure(error, mandatory) { const message = error?.message || '检查更新失败,请稍后重试' if (!mandatory && error?.stage !== 'install' && error?.stage !== 'prepare') { uni.showToast({ title: message, icon: 'none', duration: 3000 }) return Promise.resolve(false) } return new Promise((resolve) => { uni.showModal({ title: '更新失败', content: message, confirmText: mandatory ? '重新尝试' : '知道了', showCancel: false, complete: () => resolve(mandatory) }) }) } async function runUpdate(update) { const accepted = await confirmUpdate(update) if (!accepted) return try { const filePath = await downloadUpdate(update) const installPath = await prepareUpdateFile(filePath, update) await installUpdate(installPath) if (update.packageType === 'wgt') { uni.showModal({ title: '更新完成', content: '应用将立即重启以启用新版本。', showCancel: false, complete: () => plus.runtime.restart() }) } } catch (error) { const retry = await showFailure(error, update.mandatory) if (retry) await runUpdate(update) } } export async function checkForAppUpdate({ manual = false } = {}) { if (checking) return if (!updateEndpoint) { if (manual) uni.showToast({ title: '未配置更新服务', icon: 'none' }) return } checking = true try { if (!/^https:\/\//i.test(updateEndpoint)) throw new Error('更新服务必须使用 HTTPS 地址') const runtime = await getRuntimeInfo() const response = await request({ url: updateEndpoint, method: 'GET', data: { appid: runtime.appid, platform: 'android', appVersion: runtime.appVersion, appVersionCode: runtime.appVersionCode, wgtVersion: runtime.wgtVersion } }) if (response.statusCode !== 200) throw new Error('更新服务暂时不可用') const update = normalizeUpdate(response.data, runtime) if (update && isNewerUpdate(update, runtime)) { await runUpdate(update) } else if (manual) { uni.showToast({ title: '当前已是最新版本', icon: 'none' }) } } catch (error) { if (manual) await showFailure(error, false) } finally { checking = false } }