Files
pda-overseas-uniapp/src/utils/app-update.js
T
2026-11-05 14:53:57 +08:00

247 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}