247 lines
8.0 KiB
JavaScript
247 lines
8.0 KiB
JavaScript
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
|
||
}
|
||
}
|