Files
pda-uniapp/utils/app-update.js
2026-09-20 14:22:14 +08:00

203 lines
5.9 KiB
JavaScript
Raw Permalink 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.
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
}
plus.runtime.getProperty(plus.runtime.appid, (properties) => {
resolve({
appid: plus.runtime.appid,
appVersion: String(plus.runtime.version || ''),
appVersionCode: Number(plus.runtime.versionCode || 0),
wgtVersion: String(properties?.version || plus.runtime.version || '')
})
})
})
}
function normalizeUpdate(payload, runtime) {
const value = payload && typeof payload === 'object' ? payload : {}
if (value.update !== true) 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 installUpdate(filePath, update) {
return new Promise((resolve, reject) => {
plus.runtime.install(
filePath,
{ force: false },
resolve,
() => reject(new Error('更新包安装失败,请重新下载'))
)
})
}
function showFailure(error, mandatory) {
const message = error?.message || '检查更新失败,请稍后重试'
if (!mandatory) {
uni.showToast({ title: message, icon: 'none', duration: 3000 })
return Promise.resolve(false)
}
return new Promise((resolve) => {
uni.showModal({
title: '更新失败',
content: message,
confirmText: '重新尝试',
showCancel: false,
complete: () => resolve(true)
})
})
}
async function runUpdate(update) {
const accepted = await confirmUpdate(update)
if (!accepted) return
try {
const filePath = await downloadUpdate(update)
await installUpdate(filePath, update)
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?.data ?? 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
}
}