diff --git a/.env.example b/.env.example index df31f09..a227b2d 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,8 @@ VITE_H5_OVERSEAS_API_BASE_URL=/overseas-api # H5 开发代理上游;正式 App 构建请在本机环境文件中改为可访问的绝对 API 根地址。 API_PROXY_TARGET=http://127.0.0.1:9000 OVERSEAS_API_PROXY_TARGET=http://127.0.0.1:9300 +# 上游本身以 /overseas-api 对外代理时设为 true;直连 gateway-overseas 时保持 false。 +OVERSEAS_API_PROXY_PRESERVE_PREFIX=false # 海外 PDA 独立更新清单地址;不得与国内 PDA 发布记录混用。 VITE_PDA_UPDATE_URL=https://updates.example.com/pda/overseas/latest # 仅开发服务器自动预填;请在不提交的 .env.local 中填写真实开发账号。 diff --git a/README.md b/README.md index b10f9b2..676776d 100644 --- a/README.md +++ b/README.md @@ -57,16 +57,18 @@ Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查海外 PDA 的独 ```json { "update": true, + "appid": "<海外PDA AppID>", "packageType": "wgt", "versionName": "0.0.2", "versionCode": 2, + "minAppVersionCode": 1, "downloadUrl": "https://updates.example.com/pda/overseas/0.0.2.wgt", "mandatory": true, "releaseNotes": "本次更新内容" } ``` -`packageType` 只允许 `apk` 或 `wgt`。`mandatory=true` 时员工不能跳过,并在下载或安装失败后提供重试;wgt 安装完成会立即重启,APK 交由 Android 安装器完成覆盖安装。正式发布要求: +`packageType` 只允许 `apk` 或 `wgt`,`appid` 必须与当前安装包一致。WGT 清单还须提供 `minAppVersionCode`(最低兼容 APK 整数版本),不满足时客户端拒绝安装并提示升级 APK。`mandatory=true` 时员工不能跳过,并在下载或安装失败后提供重试;wgt 安装完成会立即重启,APK 交由 Android 安装器完成覆盖安装。正式发布要求: 1. 海外 PDA 每次整包发布同时递增 `src/manifest.json` 的 `versionName` 与整数 `versionCode`,使用海外独立 AppID、包名和签名打包 APK。 2. wgt 发布至少递增 `versionName`,并使用与已安装原生包兼容的 uni-app 编译器生成;新增原生模块、权限或不兼容运行时能力时必须发布 APK,不能只发 wgt。 diff --git a/src/utils/app-update.js b/src/utils/app-update.js index d5a96ce..fdb464d 100644 --- a/src/utils/app-update.js +++ b/src/utils/app-update.js @@ -26,10 +26,11 @@ function getRuntimeInfo() { }) } -function normalizeUpdate(payload) { +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() @@ -37,12 +38,18 @@ function normalizeUpdate(payload) { 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() } @@ -177,7 +184,7 @@ export async function checkForAppUpdate({ manual = false } = {}) { }) if (response.statusCode !== 200) throw new Error('更新服务暂时不可用') - const update = normalizeUpdate(response.data?.data ?? response.data) + const update = normalizeUpdate(response.data?.data ?? response.data, runtime) if (update && isNewerUpdate(update, runtime)) { await runUpdate(update) } else if (manual) { diff --git a/tests/app-update.test.mjs b/tests/app-update.test.mjs index efb363d..7f56fd1 100644 --- a/tests/app-update.test.mjs +++ b/tests/app-update.test.mjs @@ -31,6 +31,19 @@ test('overseas online update supports mandatory APK and wgt installation flows', assert.match(updater, /if \(retry\) await runUpdate\(update\)/) }) +test('overseas update manifest rejects a domestic AppID and incompatible WGT base', async () => { + const updater = await read('../src/utils/app-update.js') + const body = updater.match(/function normalizeUpdate\(payload, runtime\) \{[\s\S]*?\n\}/)?.[0] + assert.ok(body) + const normalize = new Function(`${body}; return normalizeUpdate`)() + const runtime = { appid: 'overseas-app', appVersionCode: 3 } + const manifest = { update: true, appid: 'overseas-app', packageType: 'wgt', downloadUrl: 'https://example.com/app.wgt', versionName: '1.0.4', minAppVersionCode: 3 } + assert.equal(normalize(manifest, runtime).minAppVersionCode, 3) + assert.throws(() => normalize({ ...manifest, appid: 'domestic-app' }, runtime), /不匹配/) + assert.throws(() => normalize({ ...manifest, minAppVersionCode: 4 }, runtime), /新版 APK/) + assert.throws(() => normalize({ ...manifest, minAppVersionCode: undefined }, runtime), /新版 APK/) +}) + test('overseas profile exposes manual App update checking and the runtime version', async () => { const profile = await read('../src/pages/profile/profile.vue') diff --git a/vite.config.js b/vite.config.js index 7ee2f8a..d6a0949 100644 --- a/vite.config.js +++ b/vite.config.js @@ -27,6 +27,7 @@ export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '') const domesticTarget = env.API_PROXY_TARGET || 'http://127.0.0.1:9000' const overseasTarget = env.OVERSEAS_API_PROXY_TARGET || 'http://127.0.0.1:9300' + const preserveOverseasPrefix = env.OVERSEAS_API_PROXY_PRESERVE_PREFIX === 'true' return { plugins: [uni()], define: { @@ -41,7 +42,7 @@ export default defineConfig(({ mode }) => { '/overseas-api': loggedProxy('overseas', { target: overseasTarget, changeOrigin: true, - rewrite: (path) => path.replace(/^\/overseas-api/, '/api') + rewrite: (path) => preserveOverseasPrefix ? path : path.replace(/^\/overseas-api/, '/api') }) } },