refactor: OTA 热更新抽离为公共模块 utils/ota.js
原先 checkUpdate / parseVersionCode / downloadAndInstall / downloadWgt /
installWgt / otaFailed 整个长在 App.vue 的 methods 里,个人中心「设置 →
检查更新」想手动触发一次根本做不到。抽到 utils/ota.js 后两处共用同一份
实现,改版本比对规则或下载策略只需改一处。
- 节流状态从 this._lastUpdateCheck 改为模块级单例
- 新增 checkAppUpdate({ manual }),manual 模式跳过节流、显示「检查中」、
并在「已是最新 / 检查失败」时给出明确反馈 —— 用户主动点的按钮必须有回音,
自动检查才静默收场
- App.vue 的 onLaunch/onShow 改为调用同一函数,自动模式行为不变
同时 App.vue 的全局样式新增 .success-badge-wrapper / .success-badge
(上传成功绿勾角标,供各页面图片列表复用):
- 直角三角形用 border-top(实色) + border-left(透明),直角落在右上;
写成 border-top + border-right 会得到左上角的三角,方向是反的
- 白勾用两条边框旋转 -45° 画成,不用 "✓" 字符 —— 该字形在安卓与 iOS 上
的字形和基线差异很大,对不齐
- 容器 overflow:hidden 只包图片本身,不能包住单元格:各页面的删除按钮
.img-del 定位在 -12rpx 处(单元格之外),包进去会被一并裁掉
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { getNotifications } from "./api/notification";
|
||||
import { checkAppUpdate } from "./utils/ota";
|
||||
|
||||
export default {
|
||||
onLaunch() {
|
||||
@ -14,9 +15,9 @@ export default {
|
||||
}
|
||||
// 已登录 → 原地渲染 scan 页,不需任何跳转
|
||||
|
||||
// 🚀 OTA 热更新检测(仅 App 端生效)
|
||||
// 🚀 OTA 热更新检测(仅 App 端生效;实现见 utils/ota.js,设置页手动检查共用同一份)
|
||||
// #ifdef APP-PLUS
|
||||
this.checkUpdate();
|
||||
checkAppUpdate();
|
||||
// #endif
|
||||
},
|
||||
onShow() {
|
||||
@ -24,121 +25,13 @@ export default {
|
||||
this.updateTabBarBadge();
|
||||
// 🚀 每次回到前台也检查更新,用户无需杀后台就能感知新版本
|
||||
// #ifdef APP-PLUS
|
||||
this.checkUpdate();
|
||||
checkAppUpdate();
|
||||
// #endif
|
||||
},
|
||||
onHide() {
|
||||
console.log("App 隐藏");
|
||||
},
|
||||
methods: {
|
||||
// ==========================================================
|
||||
// OTA 热更新雷达 — 版本检测 + WGT 下载 + 静默安装
|
||||
// ==========================================================
|
||||
checkUpdate() {
|
||||
// 🚀 节流:5分钟内不重复检查,避免 onShow 频繁触发弹窗骚扰
|
||||
const now = Date.now();
|
||||
if (this._lastUpdateCheck && now - this._lastUpdateCheck < 5 * 60 * 1000) {
|
||||
console.log("[OTA] 距上次检查不足5分钟,跳过");
|
||||
return;
|
||||
}
|
||||
this._lastUpdateCheck = now;
|
||||
|
||||
// 🔧 appWgtVersion 跟随 WGT 更新,解析算法: major*100 + lastNum
|
||||
// "T1.0.1"→101, "T1.0.10"→110, "T1.0.99"→199
|
||||
const sysInfo = uni.getSystemInfoSync();
|
||||
const wgtVer = sysInfo.appWgtVersion || sysInfo.appVersion || "0";
|
||||
const currentVersionCode = this.parseVersionCode(wgtVer);
|
||||
const baseUrl = uni.getStorageSync("env_base_url") || "http://track_back.iris-rs.cn/api/v1";
|
||||
|
||||
console.log("[OTA] 本地版本:", wgtVer, "→ 数字:", currentVersionCode);
|
||||
|
||||
uni.request({
|
||||
url: `${baseUrl}/app/check-update`,
|
||||
method: "GET",
|
||||
timeout: 8000,
|
||||
success: (res) => {
|
||||
if (res.statusCode !== 200) return;
|
||||
const data = res.data;
|
||||
if (!data || !data.wgt_url) {
|
||||
console.log("[OTA] 服务端无可用更新包");
|
||||
return;
|
||||
}
|
||||
|
||||
const serverVersionCode = data.version_code || 0;
|
||||
console.log("[OTA] 服务端版本:", data.version, "| 数字版本:", serverVersionCode);
|
||||
|
||||
// 客户端自行对比:服务端 > 本地 = 需要更新
|
||||
if (serverVersionCode <= currentVersionCode) {
|
||||
console.log("[OTA] 已是最新版本,无需更新");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[OTA] 发现新版本:", data.version);
|
||||
this.downloadAndInstall(data.wgt_url, data.version, data.description);
|
||||
},
|
||||
fail: () => {
|
||||
console.log("[OTA] 版本检测网络失败,跳过");
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** 版本号→数字: "T1.0.10" → 1*100+10 = 110 */
|
||||
parseVersionCode(v) {
|
||||
if (!v) return 0;
|
||||
const nums = v.match(/\d+/g);
|
||||
if (!nums || nums.length < 2) return 0;
|
||||
return parseInt(nums[0], 10) * 100 + parseInt(nums[nums.length - 1], 10);
|
||||
},
|
||||
|
||||
downloadAndInstall(wgtUrl, newVersion, description) {
|
||||
if (!wgtUrl) {
|
||||
console.log("[OTA] 无 WGT 下载地址");
|
||||
return;
|
||||
}
|
||||
|
||||
const content = description
|
||||
? `发现新版本 ${newVersion}\n\n${description}\n\n是否立即更新?`
|
||||
: `发现新版本 ${newVersion},是否立即更新?`;
|
||||
|
||||
uni.showModal({
|
||||
title: "版本更新",
|
||||
content,
|
||||
confirmText: "立即更新",
|
||||
cancelText: "稍后再说",
|
||||
success: (modalRes) => {
|
||||
if (!modalRes.confirm) return;
|
||||
|
||||
// 🚀 静默更新:无进度条、无 toast、不监听 onProgressUpdate
|
||||
uni.downloadFile({
|
||||
url: wgtUrl,
|
||||
success: (downloadRes) => {
|
||||
if (downloadRes.statusCode !== 200) {
|
||||
console.error("[OTA] 下载失败, statusCode:", downloadRes.statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
plus.runtime.install(
|
||||
downloadRes.tempFilePath,
|
||||
{ force: true },
|
||||
() => {
|
||||
console.log("[OTA] 安装成功,3秒后自动重启");
|
||||
setTimeout(() => {
|
||||
plus.runtime.restart();
|
||||
}, 3000);
|
||||
},
|
||||
(err) => {
|
||||
console.error("[OTA] 安装失败:", err.message);
|
||||
}
|
||||
);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error("[OTA] 下载失败:", err.errMsg);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// ==========================================================
|
||||
// TabBar 消息红点
|
||||
// ==========================================================
|
||||
@ -177,4 +70,56 @@ page {
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
上传成功角标 —— 图片缩略图右上角「绿底白勾」
|
||||
============================================================
|
||||
用法:给图片的【直接父容器】加 .success-badge-wrapper,
|
||||
容器内放 <image> 和一个空的 <view class="success-badge" />,
|
||||
后者仅在确认该图已上传成功时才渲染。
|
||||
|
||||
⚠️ 容器只能包住图片本身,不要连删除按钮一起包 —— overflow:hidden 会把
|
||||
定位在单元格外的 ✕ 按钮一并裁掉(各页面的 .img-del 都在 -12rpx 处)。
|
||||
============================================================ */
|
||||
.success-badge-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 12rpx; /* 与缩略图圆角一致,负责裁剪贴边的角标 */
|
||||
}
|
||||
|
||||
/* 直角三角形本体:border-top 实色 + border-left 透明,直角落在右上角。
|
||||
注意别写成 border-top + border-right —— 那样得到的直角在左上,方向是反的。 */
|
||||
.success-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
z-index: 2;
|
||||
pointer-events: none; /* 不遮挡图片本身的点击(预览) */
|
||||
}
|
||||
.success-badge::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 40rpx solid #67c23a;
|
||||
border-left: 40rpx solid transparent;
|
||||
}
|
||||
|
||||
/* 白色对勾:用两条边框旋转 -45° 画出来,不依赖字体。
|
||||
直接用 "✓" 字符的话,安卓与 iOS 的字形和基线差异很大,对不齐。 */
|
||||
.success-badge::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 9rpx;
|
||||
right: 7rpx;
|
||||
width: 12rpx;
|
||||
height: 7rpx;
|
||||
border-left: 3rpx solid #ffffff;
|
||||
border-bottom: 3rpx solid #ffffff;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
400
track-uniapp/src/utils/ota.js
Normal file
400
track-uniapp/src/utils/ota.js
Normal file
@ -0,0 +1,400 @@
|
||||
/**
|
||||
* OTA 热更新 — 版本检测 + WGT 下载 + 安装。App 端专属能力的唯一来源。
|
||||
*
|
||||
* 原先这套逻辑整个长在 App.vue 的 methods 里(checkUpdate / parseVersionCode /
|
||||
* downloadAndInstall / downloadWgt / installWgt / otaFailed),个人中心的
|
||||
*「设置 → 检查更新」想手动触发一次就得把 App.vue 的方法捞出来,做不到。
|
||||
* 抽到这里后两处共用同一份实现,改版本比对规则或下载策略只需改这里。
|
||||
*
|
||||
* 调用方:
|
||||
* - App.vue onLaunch / onShow 静默自动检查(受 5 分钟节流)
|
||||
* - settings.vue 「检查更新」手动检查(manual = true,跳过节流并给出明确反馈)
|
||||
*/
|
||||
|
||||
import { getBaseUrl } from "./request";
|
||||
|
||||
/** 自动检查的节流窗口:5 分钟内不重复检查,避免 onShow 频繁触发弹窗骚扰 */
|
||||
const THROTTLE_MS = 5 * 60 * 1000;
|
||||
|
||||
/** 上次检查时间戳(模块级单例,等价于原 App.vue 的 this._lastUpdateCheck) */
|
||||
let lastCheckAt = 0;
|
||||
|
||||
/** 版本号→数字: "T1.0.10" → 1*100+10 = 110 */
|
||||
export function parseVersionCode(v) {
|
||||
if (!v) return 0;
|
||||
const nums = v.match(/\d+/g);
|
||||
if (!nums || nums.length < 2) return 0;
|
||||
return parseInt(nums[0], 10) * 100 + parseInt(nums[nums.length - 1], 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有新版本。
|
||||
*
|
||||
* @param {{manual?: boolean}} [opts]
|
||||
* manual = true 时(设置页手动触发):跳过节流、显示「检查中」、
|
||||
* 并在「已是最新 / 检查失败」时给出明确提示 —— 用户主动点的按钮
|
||||
* 必须有个回音,不能像自动检查那样静默收场。
|
||||
* @returns {Promise<"latest"|"available"|"skipped"|"unsupported"|"error">}
|
||||
*/
|
||||
export async function checkAppUpdate({ manual = false } = {}) {
|
||||
// #ifndef APP-PLUS
|
||||
// 非 App 环境(H5 / 小程序)没有 plus.runtime,热更新无从谈起
|
||||
if (manual) {
|
||||
uni.showToast({ title: "当前环境不支持热更新", icon: "none", duration: 2500 });
|
||||
}
|
||||
return "unsupported";
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
const now = Date.now();
|
||||
if (!manual && lastCheckAt && now - lastCheckAt < THROTTLE_MS) {
|
||||
console.log("[OTA] 距上次检查不足5分钟,跳过");
|
||||
return "skipped";
|
||||
}
|
||||
lastCheckAt = now;
|
||||
|
||||
// 🔧 appWgtVersion 跟随 WGT 更新,解析算法: major*100 + lastNum
|
||||
// "T1.0.1"→101, "T1.0.10"→110, "T1.0.99"→199
|
||||
//
|
||||
// ⚠️ 这两个字段必须分开看,别只看合并后的值:
|
||||
// appWgtVersion = 已安装的 wgt 资源版本(热更新成功后会变)
|
||||
// appVersion = APK 基座版本(重装多少次热更新都不会变)
|
||||
// 若 appWgtVersion 为空,说明热更新从未装成功过,此时取到的是 APK 的
|
||||
// 静态版本号,表现就是"更新提示反复弹、版本号永远不动"。
|
||||
const sysInfo = uni.getSystemInfoSync();
|
||||
const wgtVersion = sysInfo.appWgtVersion || "";
|
||||
const apkVersion = sysInfo.appVersion || "";
|
||||
const wgtVer = wgtVersion || apkVersion || "0";
|
||||
const currentVersionCode = parseVersionCode(wgtVer);
|
||||
|
||||
// 🔧 复用 getBaseUrl():与业务请求永远指向同一个后端(含 App 内动态切换的环境)
|
||||
const baseUrl = getBaseUrl();
|
||||
|
||||
console.log(
|
||||
"[OTA] wgt版本:", wgtVersion || "(空,说明热更新从未装成功)",
|
||||
"| APK版本:", apkVersion,
|
||||
"→ 取用:", wgtVer, "=", currentVersionCode
|
||||
);
|
||||
|
||||
if (manual) uni.showLoading({ title: "检查更新中...", mask: true });
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.request({
|
||||
url: `${baseUrl}/app/check-update`,
|
||||
method: "GET",
|
||||
timeout: 8000,
|
||||
success: (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
if (manual) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: `检查更新失败 (${res.statusCode})`, icon: "none" });
|
||||
}
|
||||
resolve("error");
|
||||
return;
|
||||
}
|
||||
const data = res.data;
|
||||
if (!data || !data.wgt_url) {
|
||||
console.log("[OTA] 服务端无可用更新包");
|
||||
if (manual) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "当前已是最新版本", icon: "none" });
|
||||
}
|
||||
resolve("latest");
|
||||
return;
|
||||
}
|
||||
|
||||
const serverVersionCode = data.version_code || 0;
|
||||
console.log("[OTA] 服务端版本:", data.version, "| 数字版本:", serverVersionCode);
|
||||
|
||||
// 客户端自行对比:服务端 > 本地 = 需要更新
|
||||
if (serverVersionCode <= currentVersionCode) {
|
||||
console.log("[OTA] 已是最新版本,无需更新");
|
||||
if (manual) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "当前已是最新版本", icon: "none" });
|
||||
}
|
||||
resolve("latest");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[OTA] 发现新版本:", data.version);
|
||||
if (manual) uni.hideLoading();
|
||||
downloadAndInstall(data.wgt_url, data.version, data.description);
|
||||
resolve("available");
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log("[OTA] 版本检测网络失败,跳过");
|
||||
if (manual) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: err && err.errMsg && /timeout/i.test(err.errMsg) ? "检查更新超时" : "网络连接失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
resolve("error");
|
||||
},
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
function downloadAndInstall(wgtUrl, newVersion, description) {
|
||||
if (!wgtUrl) {
|
||||
console.log("[OTA] 无 WGT 下载地址");
|
||||
return;
|
||||
}
|
||||
if (typeof plus === "undefined") {
|
||||
console.log("[OTA] 非 App 环境,跳过安装");
|
||||
return;
|
||||
}
|
||||
|
||||
const content = description
|
||||
? `发现新版本 ${newVersion}\n\n${description}\n\n是否立即更新?`
|
||||
: `发现新版本 ${newVersion},是否立即更新?`;
|
||||
|
||||
uni.showModal({
|
||||
title: "版本更新",
|
||||
content,
|
||||
confirmText: "立即更新",
|
||||
cancelText: "稍后再说",
|
||||
success: (modalRes) => {
|
||||
if (!modalRes.confirm) return;
|
||||
downloadWgt(wgtUrl, newVersion);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载 WGT 更新包。
|
||||
*
|
||||
* 为什么不用 uni.downloadFile(旧实现的坑):
|
||||
* plus.runtime.install 是**靠文件扩展名**区分 wgt / apk 的,而
|
||||
* uni.downloadFile 返回的 tempFilePath 不保证带 `.wgt` 后缀,路径不对时
|
||||
* 安装会被直接拒绝 —— 且旧代码失败只打 console.error,工人只看到
|
||||
* 「点了立即更新没反应」。这里改用 plus.downloader 并显式指定带后缀的
|
||||
* 落盘路径;写到 _doc/(应用私有目录)也顺带避开 Android 10+ 分区存储限制。
|
||||
* (注意:本函数走的是 plus.downloader,不是 uni.downloadFile —— 后者没有
|
||||
* 可用的落盘路径控制,正是当初被换掉的原因。)
|
||||
*
|
||||
* ⚠️ 两道内容校验的由来("invalid LOC header" 事故):
|
||||
* plus.runtime.install 拿到文件就按 ZIP 解压,文件不是合法 wgt 时抛的是
|
||||
* "invalid LOC header (bad signature)" 这种底层报错,工人只看到一句看不懂的
|
||||
* 英文。而拿到非 wgt 内容的情形并不罕见:静态服务器把 404 配成返回首页、
|
||||
* 反向代理吐 JSON 错误体、SSO 把请求 302 到登录页 —— 这些**都可能带着
|
||||
* HTTP 200 回来**,只判断 status === 200 根本拦不住。
|
||||
* 故:下载前探测响应头,下载后校验 ZIP 魔数,两道都过了才交给 install。
|
||||
*/
|
||||
async function downloadWgt(wgtUrl, newVersion) {
|
||||
// ── 第一道:下载前探测(状态码 / Content-Type / 体积)──
|
||||
// 目的是在浪费流量之前就拦下明显的错误页,并给出人话错误
|
||||
const probeError = await probeWgtUrl(wgtUrl);
|
||||
if (probeError) {
|
||||
otaFailed(probeError);
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件名必须带 .wgt 后缀 —— 这是 install 能识别为 wgt 资源包的前提。
|
||||
// 从下载地址取文件名(去掉 query),拿不到合法后缀时退回默认名。
|
||||
const rawName = String(wgtUrl).split("?")[0].split("/").pop() || "";
|
||||
const fileName = /\.wgt$/i.test(rawName) ? rawName : "__UNI__B572616.wgt";
|
||||
const savePath = "_doc/" + fileName;
|
||||
console.log("[OTA] 开始下载:", wgtUrl, "→", savePath);
|
||||
|
||||
let lastPct = -1;
|
||||
let task = null;
|
||||
|
||||
const onFinished = async (d, status) => {
|
||||
uni.hideLoading();
|
||||
console.log("[OTA] 下载结束, status =", status, "| 文件:", d && d.filename);
|
||||
|
||||
// 非 2xx 一律当失败:404=包没传上去,401/403=下载地址需要鉴权
|
||||
if (status !== 200) {
|
||||
const hint =
|
||||
status === 404 ? "更新包不存在,请联系管理员确认是否已上传"
|
||||
: status === 401 || status === 403 ? "更新包下载被拒绝(无权限),请联系管理员"
|
||||
: "请稍后重试";
|
||||
otaFailed(`更新包下载失败(HTTP ${status}):${hint}`);
|
||||
return;
|
||||
}
|
||||
// 没拿到落盘路径就没法安装 —— 明确报出来,别让它以"没反应"收场
|
||||
if (!d || !d.filename) {
|
||||
otaFailed("下载已完成但未生成本地文件,请重试");
|
||||
return;
|
||||
}
|
||||
// 截断检测:服务端中途断开时 status 也可能是 200,但文件是不完整的
|
||||
if (d.totalSize > 0 && d.downloadedSize > 0 && d.downloadedSize < d.totalSize) {
|
||||
otaFailed(`更新包下载不完整(${d.downloadedSize}/${d.totalSize} 字节),请重试`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 第二道:ZIP 魔数校验(拦下所有「HTTP 200 但内容不是 wgt」的情况)──
|
||||
const contentError = await verifyWgtFile(d.filename);
|
||||
if (contentError) {
|
||||
otaFailed(contentError);
|
||||
return;
|
||||
}
|
||||
|
||||
installWgt(d.filename, newVersion);
|
||||
};
|
||||
|
||||
try {
|
||||
task = plus.downloader.createDownload(wgtUrl, { filename: savePath }, onFinished);
|
||||
// 进度反馈:每跨 10% 刷新一次提示(过于频繁会卡顿)
|
||||
task.addEventListener("statechanged", (d) => {
|
||||
if (!d || d.state !== 3) return; // 3 = 下载中
|
||||
if (!d.totalSize || d.totalSize <= 0) return;
|
||||
const pct = Math.floor((d.downloadedSize / d.totalSize) * 100);
|
||||
if (pct >= lastPct + 10) {
|
||||
lastPct = pct - (pct % 10);
|
||||
uni.showLoading({ title: `下载中 ${pct}%`, mask: true });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[OTA] 创建下载任务异常:", e);
|
||||
otaFailed("无法创建下载任务:" + ((e && e.message) || "未知原因"));
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showLoading({ title: "下载中 0%", mask: true });
|
||||
task.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载前探测更新包地址 —— 在消耗流量之前拦下「错误页」。
|
||||
*
|
||||
* 用 HEAD 只取响应头,不下载正文。核对三件事:
|
||||
* 1. statusCode 必须是 200(404 / 401 / 403 直接判失败)
|
||||
* 2. Content-Type 不能是 JSON / HTML —— 那说明拿回来的是错误页而不是安装包
|
||||
* 3. Content-Length 不能小得离谱(合法 wgt 不可能只有几百字节)
|
||||
*
|
||||
* @returns {Promise<string>} 空串 = 通过;非空 = 给用户看的失败原因
|
||||
*/
|
||||
function probeWgtUrl(wgtUrl) {
|
||||
return new Promise((resolve) => {
|
||||
uni.request({
|
||||
url: wgtUrl,
|
||||
method: "HEAD",
|
||||
timeout: 8000,
|
||||
success: (res) => {
|
||||
const header = res.header || {};
|
||||
const contentType = String(header["Content-Type"] || header["content-type"] || "");
|
||||
const contentLength = Number(header["Content-Length"] || header["content-length"] || 0);
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
resolve(
|
||||
res.statusCode === 404 ? "更新包不存在,请联系管理员确认是否已上传"
|
||||
: res.statusCode === 401 || res.statusCode === 403 ? "更新包下载被拒绝(无权限),请联系管理员"
|
||||
: `更新包地址不可访问(HTTP ${res.statusCode})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (/json|html|text\/plain/i.test(contentType)) {
|
||||
resolve(`更新包地址返回的是 ${contentType},不是安装包 —— 请检查服务端配置`);
|
||||
return;
|
||||
}
|
||||
if (contentLength > 0 && contentLength < 1024) {
|
||||
resolve(`更新包体积异常(仅 ${contentLength} 字节),疑似错误页面`);
|
||||
return;
|
||||
}
|
||||
// Content-Type / Content-Length 都可能被服务端省略,此处只做「明显不对」的拦截
|
||||
console.log("[OTA] 预检通过 | Content-Type:", contentType || "(未返回)", "| 长度:", contentLength || "(未返回)");
|
||||
resolve("");
|
||||
},
|
||||
fail: (err) => {
|
||||
// HEAD 不被支持(部分静态服务器返回 405)或探测请求本身超时:
|
||||
// 不阻断下载 —— 还有第二道魔数校验兜底,没必要因此卡死更新
|
||||
console.warn("[OTA] 预检请求失败,跳过预检,改由下载后校验兜底:", err && err.errMsg);
|
||||
resolve("");
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验已下载的文件确实是 WGT —— 读取文件头 4 字节比对 ZIP 魔数。
|
||||
*
|
||||
* WGT 就是 ZIP 包,合法文件必定以 "50 4B 03 04" 开头(ASCII 即 "PK\x03\x04")。
|
||||
* 这正是 install 报 "invalid LOC header (bad signature)" 时会去校验的东西,
|
||||
* 我们提前自己验一遍,就能把底层报错换成一句人话。
|
||||
*
|
||||
* 实现上用 readAsDataURL 而非读二进制:base64 后 4 字节是 "UEsD",比对前缀
|
||||
* 即可,避开了 plus.io.FileReader 对二进制读取支持不一致的问题。
|
||||
*
|
||||
* @returns {Promise<string>} 空串 = 通过;非空 = 给用户看的失败原因
|
||||
*/
|
||||
function verifyWgtFile(filePath) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
plus.io.resolveLocalFileSystemURL(
|
||||
filePath,
|
||||
(entry) => {
|
||||
entry.file(
|
||||
(file) => {
|
||||
// slice 不可用的老机型上放弃校验(不阻断更新),交给 install 自己报错
|
||||
if (typeof file.slice !== "function") {
|
||||
console.warn("[OTA] 当前环境不支持文件切片,跳过魔数校验");
|
||||
resolve("");
|
||||
return;
|
||||
}
|
||||
const reader = new plus.io.FileReader();
|
||||
reader.onloadend = (e) => {
|
||||
const result = String((e.target && e.target.result) || "");
|
||||
if (/;base64,UEsD/.test(result)) {
|
||||
console.log("[OTA] 更新包文件头校验通过 (ZIP signature OK)");
|
||||
resolve("");
|
||||
} else {
|
||||
resolve("下载到的文件不是有效的更新包(文件头异常),请联系管理员重新打包上传");
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.warn("[OTA] 读取更新包文件头失败,跳过校验");
|
||||
resolve("");
|
||||
};
|
||||
reader.readAsDataURL(file.slice(0, 4));
|
||||
},
|
||||
() => resolve("")
|
||||
);
|
||||
},
|
||||
() => resolve("")
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("[OTA] 魔数校验异常,跳过:", e);
|
||||
resolve("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 安装已下载的 WGT —— 成败都要在界面上说清楚 */
|
||||
function installWgt(filePath, newVersion) {
|
||||
console.log("[OTA] 开始安装:", filePath);
|
||||
uni.showLoading({ title: "安装中...", mask: true });
|
||||
plus.runtime.install(
|
||||
filePath,
|
||||
{ force: true },
|
||||
() => {
|
||||
uni.hideLoading();
|
||||
console.log("[OTA] 安装成功:", newVersion, "→ 即将重启");
|
||||
uni.showToast({ title: "更新完成,即将重启", icon: "none", duration: 2500 });
|
||||
setTimeout(() => {
|
||||
plus.runtime.restart();
|
||||
}, 2500);
|
||||
},
|
||||
(err) => {
|
||||
uni.hideLoading();
|
||||
console.error("[OTA] 安装失败:", JSON.stringify(err));
|
||||
otaFailed(
|
||||
`更新包安装失败:${(err && (err.message || err.code)) || "未知原因"}`
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 更新失败必须在界面上说出来 —— 只打日志等于让工人对着「点了没反应」干瞪眼 */
|
||||
function otaFailed(msg) {
|
||||
uni.showModal({
|
||||
title: "更新失败",
|
||||
content: `${msg}\n\n请确认网络正常后重试;若反复失败请联系管理员。`,
|
||||
showCancel: false,
|
||||
confirmText: "知道了",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user