diff --git a/README.md b/README.md
index d22491f..5a9293e 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,7 @@ Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查国内 PDA 的独
```json
{
"update": true,
+ "appid": "<国内PDA AppID>",
"packageType": "apk",
"versionName": "0.0.2",
"versionCode": 2,
@@ -99,7 +100,7 @@ Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查国内 PDA 的独
}
```
-`packageType` 只允许 `apk` 或 `wgt`。`mandatory=true` 时员工不能跳过,并在下载或安装失败后提供重试;wgt 安装完成会立即重启,APK 交由 Android 安装器完成覆盖安装。正式发布要求:
+`packageType` 只允许 `apk` 或 `wgt`,`appid` 必须与当前安装包一致。WGT 清单还须提供 `minAppVersionCode`(最低兼容 APK 整数版本),不满足时客户端拒绝安装并提示升级 APK。`mandatory=true` 时员工不能跳过,并在下载或安装失败后提供重试;wgt 安装完成会立即重启,APK 交由 Android 安装器完成覆盖安装。正式发布要求:
1. 国内 PDA 每次整包发布同时递增 `manifest.json` 的 `versionName` 与整数 `versionCode`,使用国内独立 AppID、包名和签名打包 APK。
2. wgt 发布至少递增 `versionName`,并使用与已安装原生包兼容的 uni-app 编译器生成;新增原生模块、权限或不兼容运行时能力时必须发布 APK,不能只发 wgt。
diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue
index 5d8c823..e68e4fd 100644
--- a/pages/picking/picking.vue
+++ b/pages/picking/picking.vue
@@ -101,10 +101,10 @@
-
+
-
+
已同步服务端
@@ -469,12 +469,13 @@
if (!this.partialTargetItem) return false
const value = parseInt(this.partialInput, 10)
const available = this.availablePickQuantity(this.partialTargetItem)
- return Number.isInteger(value) && value > 0 && value < this.partialTargetItem.qty && (available === null || value <= available)
+ return Number.isInteger(value) && value > 0 && value >= Number(this.partialTargetItem.pickedPackageCount || 0) && value < this.partialTargetItem.qty && (available === null || value <= available)
},
partialValidationMessage() {
if (!this.partialInput || !this.partialTargetItem) return ''
const value = parseInt(this.partialInput, 10)
if (!Number.isInteger(value) || value <= 0) return '请输入大于 0 的整数'
+ if (value < Number(this.partialTargetItem.pickedPackageCount || 0)) return `实际拣出总数不能少于已拣的 ${this.partialTargetItem.pickedPackageCount} 箱`
if (value >= this.partialTargetItem.qty) return `部分拣货必须少于 ${this.partialTargetItem.qty},整单请返回后点击“确认拣出”`
const available = this.availablePickQuantity(this.partialTargetItem)
if (available !== null && value > available) return `超过库存/库位可用量,可拣 ${available}`
@@ -1527,7 +1528,7 @@
const item = this.currentBox.items.find((entry) => entry.id === id)
if (isSealedPalletItem(item)) { this.showToast(`整托货物不能部分拣货,请扫描 ${item.palletNo}`, 'warning'); return }
this.partialTargetId = id
- this.partialInput = ''
+ this.partialInput = Number(item?.pickedPackageCount || 0) > 0 ? String(item.pickedPackageCount) : ''
this.openOverlay('partial')
this.$nextTick(() => {
this.partialFocused = true
diff --git a/tests/app-update.test.mjs b/tests/app-update.test.mjs
index 7b0d8e6..9e04dad 100644
--- a/tests/app-update.test.mjs
+++ b/tests/app-update.test.mjs
@@ -31,6 +31,19 @@ test('online update supports mandatory APK and wgt installation flows', async ()
assert.match(updater, /if \(retry\) await runUpdate\(update\)/)
})
+test('update manifest rejects a different AppID and incompatible WGT base', async () => {
+ const updater = await read('../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: 'domestic-app', appVersionCode: 3 }
+ const manifest = { update: true, appid: 'domestic-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: 'overseas-app' }, runtime), /不匹配/)
+ assert.throws(() => normalize({ ...manifest, minAppVersionCode: 4 }, runtime), /新版 APK/)
+ assert.throws(() => normalize({ ...manifest, minAppVersionCode: undefined }, runtime), /新版 APK/)
+})
+
test('profile exposes manual App update checking and the runtime version', async () => {
const profile = await read('../pages/profile/profile.vue')
diff --git a/tests/picking-partial-actions.test.mjs b/tests/picking-partial-actions.test.mjs
new file mode 100644
index 0000000..aa8972a
--- /dev/null
+++ b/tests/picking-partial-actions.test.mjs
@@ -0,0 +1,22 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+
+const page = readFileSync(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
+
+test('ordinary partially picked items retain full and partial picking actions, but not skip', () => {
+ assert.match(page, /v-else-if="!isSealedPalletItem\(item\) && \[ITEM_STATUS.PENDING, ITEM_STATUS.PARTIAL\].includes\(itemState\(item\)\)"/)
+ assert.match(page, /v-if="itemState\(item\) === ITEM_STATUS.PENDING" class="pick-btn pick-btn-skip"/)
+ assert.match(page, /const remaining = Number\(item\?\.qty \|\| 0\) - Number\(item\?\.pickedPackageCount \|\| 0\)/)
+ assert.match(page, /'PICK', \{ pickedPackageCount: remaining, source \}/)
+})
+
+test('partial quantity is a cumulative total and cannot erase boxes already picked', () => {
+ const body = page.match(/partialInputValid\(\) \{([\s\S]*?)\n\t\t\t\},/)[1]
+ const valid = new Function(body)
+ const context = { partialTargetItem: { qty: 20, pickedPackageCount: 3 }, availablePickQuantity: () => 20 }
+ for (const [input, expected] of [['2', false], ['3', true], ['12', true], ['20', false]]) {
+ assert.equal(valid.call({ ...context, partialInput: input }), expected, input)
+ }
+ assert.match(page, /this.partialInput = Number\(item\?\.pickedPackageCount \|\| 0\) > 0 \? String\(item.pickedPackageCount\) : ''/)
+})
diff --git a/utils/app-update.js b/utils/app-update.js
index 41c4316..33d7c7b 100644
--- a/utils/app-update.js
+++ b/utils/app-update.js
@@ -30,10 +30,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()
@@ -41,12 +42,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()
}
@@ -181,7 +188,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) {