修复:完善装填状态与确认权限

This commit is contained in:
焦龙言
2026-07-27 10:12:05 +08:00
parent a223971a3c
commit 4340d03a70
4 changed files with 111 additions and 51 deletions
@@ -1075,6 +1075,13 @@ test('入库页面只允许 DRAFT 变更并以 authoritative 详情和独立确
assertButtonsUsePermission(source, 'handleConfirm', 'business:inbound:confirm')
assert.match(source, /proxy\.\$auth\.hasPermiAnd\(\['business:inbound:query', 'business:inbound:add'\]\)/)
assert.match(source, /proxy\.\$auth\.hasPermiAnd\(\['business:inbound:query', 'business:inbound:edit'\]\)/)
const confirmButton = extractButtonBlocks(source, 'handleConfirm')[0]
assert.match(confirmButton, /v-if="scope\.row\.status === 'DRAFT'"/)
assert.doesNotMatch(source, /canConfirmInbound/)
assert.doesNotMatch(
source,
/proxy\.\$auth\.hasPermiAnd\(\['business:inbound:query', 'business:inbound:confirm'\]\)/
)
const addButton = extractButtonBlocks(source, 'handleAdd')[0]
assert.match(addButton, /:disabled="[^"]*operationPending[^"]*optionLoading[^"]*detailLoading[^"]*open[^"]*"/)
const addSource = extractNamedFunction(source, 'handleAdd')
@@ -1143,6 +1150,7 @@ test('入库页面只允许 DRAFT 变更并以 authoritative 详情和独立确
assert.match(ensureSource, /createIdempotencyKey\(\)/)
assert.match(ensureSource, /confirmIdempotency\.key/)
const confirmSource = extractNamedFunction(source, 'handleConfirm')
assert.doesNotMatch(confirmSource, /canConfirmInbound|business:inbound:query/)
assert.ok(
confirmSource.indexOf('confirmLoading.value = true') < confirmSource.indexOf('proxy.$modal.confirm('),
'入库确认必须在打开二次确认框前加锁'
@@ -1155,6 +1163,16 @@ test('入库页面只允许 DRAFT 变更并以 authoritative 详情和独立确
assert.doesNotMatch(confirmCatch, /clearConfirmIdempotency\(\)[\s\S]*?(?:loadError|msgError)/)
})
test('装填扫码 pending 期间禁用全部业务输入', async () => {
const source = await readFile(path.join(frontendRoot, businessPageContracts.filling.relativePath), 'utf8')
for (const field of ['strainId', 'batchNo', 'packageSpec', 'initialPackages']) {
const businessControl = source.match(new RegExp(`<[^>]+v-model="form\\.${field}"[^>]*>`))?.[0]
assert.ok(businessControl, `缺少装填 ${field} 业务控件`)
assert.match(businessControl, /:disabled="[^"]*scanLoading[^"]*"/, `装填扫码 pending 时必须禁用 ${field}`)
}
})
test('装填页面锁定扫码上下文、校验正整数并在失败重试时保留幂等键', async () => {
const source = await readFile(path.join(frontendRoot, businessPageContracts.filling.relativePath), 'utf8')
@@ -1166,42 +1184,59 @@ test('装填页面锁定扫码上下文、校验正整数并在失败重试时
assert.match(source, /pallet\.palletCode \|\| `#\$\{pallet\.palletId\}`/)
assert.match(source, /getFillingStrainOptions\(\)/)
const clearPayloadSource = extractNamedFunction(source, 'clearFillingPayload')
assert.match(clearPayloadSource, /clearFillingIdempotencyKey\(\)/)
assert.match(clearPayloadSource, /scannedPallet\.value = null/)
for (const [field, emptyValue] of [
['qrToken', "''"],
['warehouseId', 'undefined'],
['strainId', 'undefined'],
['batchNo', "''"],
['packageSpec', "''"],
['initialPackages', 'undefined']
]) {
const fieldPattern = new RegExp(`(?:form\\.value\\.${field}\\s*=\\s*${emptyValue}|${field}:\\s*${emptyValue})`)
assert.match(clearPayloadSource, fieldPattern, `装填载荷清理必须重置 ${field}`)
}
assert.match(extractNamedFunction(source, 'resetFillingOperation'), /clearFillingPayload\(\)/)
const scanSource = extractNamedFunction(source, 'handleFillingScan')
const scanCallIndex = scanSource.indexOf('scanFillingPallet(')
assert.ok(scanCallIndex >= 0, '装填扫码必须调用 scanFillingPallet')
const scanPrefix = scanSource.slice(0, scanCallIndex)
const clearKeyIndex = scanPrefix.indexOf('clearFillingIdempotencyKey()')
const clearPayloadIndex = scanPrefix.indexOf('clearFillingPayload()')
const scanPendingIndex = scanPrefix.indexOf('scanLoading.value = true')
assert.ok(
clearKeyIndex >= 0 && scanPendingIndex >= 0 && clearKeyIndex < scanPendingIndex,
'新装填扫码必须在进入 pending 前明确清除旧幂等键'
clearPayloadIndex >= 0 && scanPendingIndex >= 0 && clearPayloadIndex < scanPendingIndex,
'新装填扫码必须在进入 pending 前清除旧业务载荷'
)
const scanSuccessSource = extractPromiseCallback(scanSource, 'then')
const scanSuccessGuardIndex = scanSuccessSource.indexOf('if (!isCurrentScanRequest(scanContext)) return')
const invalidStateIndex = scanSuccessSource.indexOf("if (pallet.flowState !== 'EMPTY_IN_WAREHOUSE')")
const invalidStateClearIndex = scanSuccessSource.indexOf('clearFillingPayload()', invalidStateIndex)
const invalidStateErrorIndex = scanSuccessSource.indexOf("scanError.value = '该托盘当前不可装填'", invalidStateIndex)
assert.ok(
scanSuccessGuardIndex >= 0 &&
invalidStateIndex > scanSuccessGuardIndex &&
invalidStateClearIndex > invalidStateIndex &&
invalidStateErrorIndex > invalidStateClearIndex,
'不可装填响应必须在当前请求门禁后、写入错误状态前清除业务载荷'
)
const scanCatchSource = extractPromiseCallback(scanSource, 'catch')
const scanCatchGuardIndex = scanCatchSource.indexOf('if (!isCurrentScanRequest(scanContext)) return')
const scanCatchClearIndex = scanCatchSource.indexOf('clearFillingPayload()')
const scanCatchErrorIndex = scanCatchSource.indexOf("scanError.value = '托盘扫码校验失败,请检查后重试'")
assert.ok(
scanCatchGuardIndex >= 0 && scanCatchClearIndex > scanCatchGuardIndex && scanCatchErrorIndex > scanCatchClearIndex,
'装填扫码异常必须在当前请求门禁后、写入错误状态前清除业务载荷'
)
assert.match(scanPrefix, /scannedPallet\.value = null/, '新装填扫码前必须清除旧托盘')
for (const [field, emptyValue] of [
['qrToken', "''"],
['warehouseId', 'undefined'],
['strainId', 'undefined'],
['batchNo', "''"],
['packageSpec', "''"],
['initialPackages', 'undefined']
]) {
const fieldPattern = new RegExp(`(?:form\\.value\\.${field}\\s*=\\s*${emptyValue}|${field}:\\s*${emptyValue})`)
assert.match(scanPrefix, fieldPattern, `新装填扫码前必须清除旧 ${field}`)
}
const invalidSource = extractNamedFunction(source, 'handleScanInvalid')
assert.match(invalidSource, /clearFillingIdempotencyKey\(\)/)
assert.match(invalidSource, /scannedPallet\.value = null/)
for (const [field, emptyValue] of [
['qrToken', "''"],
['warehouseId', 'undefined'],
['strainId', 'undefined'],
['batchNo', "''"],
['packageSpec', "''"],
['initialPackages', 'undefined']
]) {
const fieldPattern = new RegExp(`(?:form\\.value\\.${field}\\s*=\\s*${emptyValue}|${field}:\\s*${emptyValue})`)
assert.match(invalidSource, fieldPattern, `装填扫码输入失败后必须清除旧 ${field}`)
}
const invalidClearIndex = invalidSource.indexOf('clearFillingPayload()')
const invalidErrorIndex = invalidSource.indexOf('scanError.value = message')
assert.ok(
invalidClearIndex >= 0 && invalidErrorIndex > invalidClearIndex,
'装填扫码输入失败后必须先清除业务载荷再展示错误'
)
const fillingSubmitButton = extractButtonBlocks(source, 'submitFilling')[0]
assert.match(fillingSubmitButton, /:disabled="[^"]*!scannedPallet[^"]*"/)
@@ -1225,6 +1260,18 @@ test('装填页面锁定扫码上下文、校验正整数并在失败重试时
assert.match(source, /watch\([\s\S]*?if \(!submitLoading\.value\) clearFillingIdempotencyKey\(\)/)
})
test('发货详情仅在 remainingPackages 缺失时回退快照并保留 null', async () => {
const source = await readFile(path.join(frontendRoot, businessPageContracts.shipment.relativePath), 'utf8')
const detailSuccessSource = extractPromiseCallback(extractNamedFunction(source, 'handleUpdate'), 'then')
assert.match(
detailSuccessSource,
/remainingPackages:\s*item\.remainingPackages\s*===\s*undefined\s*\?\s*\(?\s*item\.packageQty\s*\?\?\s*null\s*\)?\s*:\s*item\.remainingPackages/,
'发货详情仅在 remainingPackages 缺失时回退确认快照 packageQty,并保留 null'
)
assert.doesNotMatch(detailSuccessSource, /item\.remainingPackages\s*\?\?\s*item\.packageQty/)
})
test('发货页面只允许 DRAFT 变更、展示只读余量并复用确认幂等键', async () => {
const source = await readFile(path.join(frontendRoot, businessPageContracts.shipment.relativePath), 'utf8')
@@ -1235,6 +1282,13 @@ test('发货页面只允许 DRAFT 变更、展示只读余量并复用确认幂
assertButtonsUsePermission(source, 'handleConfirm', 'business:shipment:confirm')
assert.match(source, /proxy\.\$auth\.hasPermiAnd\(\['business:shipment:query', 'business:shipment:add'\]\)/)
assert.match(source, /proxy\.\$auth\.hasPermiAnd\(\['business:shipment:query', 'business:shipment:edit'\]\)/)
const confirmButton = extractButtonBlocks(source, 'handleConfirm')[0]
assert.match(confirmButton, /v-if="scope\.row\.status === 'DRAFT'"/)
assert.doesNotMatch(source, /canConfirmShipment/)
assert.doesNotMatch(
source,
/proxy\.\$auth\.hasPermiAnd\(\['business:shipment:query', 'business:shipment:confirm'\]\)/
)
const addButton = extractButtonBlocks(source, 'handleAdd')[0]
assert.match(addButton, /:disabled="[^"]*operationPending[^"]*optionLoading[^"]*detailLoading[^"]*open[^"]*"/)
const addSource = extractNamedFunction(source, 'handleAdd')
@@ -1298,17 +1352,13 @@ test('发货页面只允许 DRAFT 变更、展示只读余量并复用确认幂
assert.match(source, /const detail = response\.data;/)
assert.match(source, /\(Array\.isArray\(detail\.items\) \? detail\.items : \[\]\)\.map\(\(item\) => \(\{[\s\S]*?palletId: item\.palletId/)
assert.match(source, /item\.palletCode \|\| `#\$\{item\.palletId\}`/)
assert.match(
detailSuccessSource,
/remainingPackages:\s*item\.remainingPackages\s*\?\?\s*item\.packageQty\s*\?\?\s*null/,
'发货详情必须优先使用当前余量,仅以确认快照 packageQty 回退'
)
assert.doesNotMatch(source, /remainingPackages\s*[-+]=|Math\.(?:max|min)\([^)]*remainingPackages/)
assert.match(extractNamedFunction(source, 'handleShipmentScan'), /remainingPackages:\s*pallet\.remainingPackages/)
const ensureSource = extractNamedFunction(source, 'ensureConfirmIdempotencyKey')
assert.match(ensureSource, /createIdempotencyKey\(\)/)
const confirmSource = extractNamedFunction(source, 'handleConfirm')
assert.doesNotMatch(confirmSource, /canConfirmShipment|business:shipment:query/)
assert.ok(
confirmSource.indexOf('confirmLoading.value = true') < confirmSource.indexOf('proxy.$modal.confirm('),
'发货确认必须在打开二次确认框前加锁'