权限:移除业务物理删除入口

This commit is contained in:
焦龙言
2026-09-24 08:13:16 +08:00
parent 4706c4afeb
commit ae8c4c8c38
35 changed files with 668 additions and 445 deletions
@@ -17,21 +17,18 @@ const expectedRequests = {
{ url: "'/master/strain/' + strainId", method: 'get' },
{ url: "'/master/strain'", method: 'post' },
{ url: "'/master/strain'", method: 'put' },
{ url: "'/master/strain/' + strainIds", method: 'delete' }
],
'src/api/master/customer.js': [
{ url: "'/master/customer/list'", method: 'get' },
{ url: "'/master/customer/' + customerId", method: 'get' },
{ url: "'/master/customer'", method: 'post' },
{ url: "'/master/customer'", method: 'put' },
{ url: "'/master/customer/' + customerIds", method: 'delete' }
],
'src/api/master/site.js': [
{ url: "'/master/site/list'", method: 'get' },
{ url: "'/master/site/' + siteId", method: 'get' },
{ url: "'/master/site'", method: 'post' },
{ url: "'/master/site'", method: 'put' },
{ url: "'/master/site/' + siteIds", method: 'delete' },
{ url: "'/master/site/' + siteId + '/users'", method: 'get' },
{ url: "'/master/site/' + siteId + '/users'", method: 'put' }
],
@@ -40,14 +37,12 @@ const expectedRequests = {
{ url: "'/master/line/' + lineId", method: 'get' },
{ url: "'/master/line'", method: 'post' },
{ url: "'/master/line'", method: 'put' },
{ url: "'/master/line/' + lineIds", method: 'delete' }
],
'src/api/master/warehouse.js': [
{ url: "'/master/warehouse/list'", method: 'get' },
{ url: "'/master/warehouse/' + warehouseId", method: 'get' },
{ url: "'/master/warehouse'", method: 'post' },
{ url: "'/master/warehouse'", method: 'put' },
{ url: "'/master/warehouse/' + warehouseIds", method: 'delete' },
{ url: "'/master/warehouse/' + warehouseId + '/users'", method: 'get' },
{ url: "'/master/warehouse/' + warehouseId + '/users'", method: 'put' }
],
@@ -57,7 +52,6 @@ const expectedRequests = {
{ url: "'/master/pallet/' + palletId + '/qrcode'", method: 'get' },
{ url: "'/master/pallet'", method: 'post' },
{ url: "'/master/pallet'", method: 'put' },
{ url: "'/master/pallet/' + palletIds", method: 'delete' }
],
'src/api/business/inbound.js': [
{ url: "'/business/inbound/list'", method: 'get' },
@@ -66,7 +60,6 @@ const expectedRequests = {
{ url: "'/business/inbound/' + inboundId", method: 'get' },
{ url: "'/business/inbound'", method: 'post' },
{ url: "'/business/inbound'", method: 'put' },
{ url: "'/business/inbound/' + inboundIds", method: 'delete' },
{ url: "'/business/inbound/' + inboundId + '/confirm'", method: 'put' }
],
'src/api/business/filling.js': [
@@ -84,7 +77,6 @@ const expectedRequests = {
{ url: "'/business/shipment/' + shipmentId", method: 'get' },
{ url: "'/business/shipment'", method: 'post' },
{ url: "'/business/shipment'", method: 'put' },
{ url: "'/business/shipment/' + shipmentIds", method: 'delete' },
{ url: "'/business/shipment/' + shipmentId + '/confirm'", method: 'put' }
],
'src/api/business/usage.js': [
@@ -134,43 +126,43 @@ const expectedReportExports = {
const masterPageContracts = {
strain: {
relativePath: 'src/views/master/strain/index.vue',
apiNames: ['addStrain', 'delStrain', 'getStrain', 'listStrain', 'updateStrain'],
permissions: ['add', 'edit', 'list', 'query', 'remove'],
apiNames: ['addStrain', 'getStrain', 'listStrain', 'updateStrain'],
permissions: ['add', 'edit', 'list', 'query'],
addPayloadKeys: ['remark', 'status', 'strainCode', 'strainName'],
editPayloadKeys: ['remark', 'status', 'strainCode', 'strainId', 'strainName']
},
customer: {
relativePath: 'src/views/master/customer/index.vue',
apiNames: ['addCustomer', 'delCustomer', 'getCustomer', 'listCustomer', 'updateCustomer'],
permissions: ['add', 'edit', 'list', 'query', 'remove'],
apiNames: ['addCustomer', 'getCustomer', 'listCustomer', 'updateCustomer'],
permissions: ['add', 'edit', 'list', 'query'],
addPayloadKeys: ['customerCode', 'customerName', 'remark', 'status'],
editPayloadKeys: ['customerCode', 'customerId', 'customerName', 'remark', 'status']
},
site: {
relativePath: 'src/views/master/site/index.vue',
apiNames: ['addSite', 'delSite', 'getSite', 'listSite', 'updateSite'],
permissions: ['add', 'assign', 'edit', 'list', 'query', 'remove'],
apiNames: ['addSite', 'getSite', 'listSite', 'updateSite'],
permissions: ['add', 'assign', 'edit', 'list', 'query'],
addPayloadKeys: ['address', 'customerId', 'remark', 'siteCode', 'siteName', 'status'],
editPayloadKeys: ['address', 'customerId', 'remark', 'siteCode', 'siteId', 'siteName', 'status']
},
line: {
relativePath: 'src/views/master/line/index.vue',
apiNames: ['addLine', 'delLine', 'getLine', 'listLine', 'updateLine'],
permissions: ['add', 'edit', 'list', 'query', 'remove'],
apiNames: ['addLine', 'getLine', 'listLine', 'updateLine'],
permissions: ['add', 'edit', 'list', 'query'],
addPayloadKeys: ['lineCode', 'lineName', 'remark', 'siteId', 'status'],
editPayloadKeys: ['lineCode', 'lineId', 'lineName', 'remark', 'siteId', 'status']
},
warehouse: {
relativePath: 'src/views/master/warehouse/index.vue',
apiNames: ['addWarehouse', 'delWarehouse', 'getWarehouse', 'listWarehouse', 'updateWarehouse'],
permissions: ['add', 'assign', 'edit', 'list', 'query', 'remove'],
apiNames: ['addWarehouse', 'getWarehouse', 'listWarehouse', 'updateWarehouse'],
permissions: ['add', 'assign', 'edit', 'list', 'query'],
addPayloadKeys: ['address', 'remark', 'status', 'warehouseCode', 'warehouseName'],
editPayloadKeys: ['address', 'remark', 'status', 'warehouseCode', 'warehouseId', 'warehouseName']
},
pallet: {
relativePath: 'src/views/master/pallet/index.vue',
apiNames: ['addPallet', 'delPallet', 'getPallet', 'getPalletQrcode', 'listPallet', 'updatePallet'],
permissions: ['add', 'edit', 'list', 'print', 'query', 'remove'],
apiNames: ['addPallet', 'getPallet', 'getPalletQrcode', 'listPallet', 'updatePallet'],
permissions: ['add', 'edit', 'list', 'print', 'query'],
addPayloadKeys: ['capacityPackages', 'palletCode', 'remark'],
editPayloadKeys: ['assetStatus', 'capacityPackages', 'palletId', 'remark']
}
@@ -182,14 +174,13 @@ const businessPageContracts = {
apiNames: [
'addInbound',
'confirmInbound',
'delInbound',
'getInbound',
'getInboundWarehouseOptions',
'listInbound',
'scanInboundPallet',
'updateInbound'
],
permissions: ['add', 'confirm', 'edit', 'list', 'query', 'remove'],
permissions: ['add', 'confirm', 'edit', 'list', 'query'],
payloads: {
addInbound: ['palletIds', 'warehouseId'],
updateInbound: ['inboundId', 'palletIds', 'warehouseId'],
@@ -217,7 +208,6 @@ const businessPageContracts = {
apiNames: [
'addShipment',
'confirmShipment',
'delShipment',
'getShipment',
'getShipmentSiteOptions',
'getShipmentWarehouseOptions',
@@ -225,7 +215,7 @@ const businessPageContracts = {
'scanShipmentPallet',
'updateShipment'
],
permissions: ['add', 'confirm', 'edit', 'list', 'query', 'remove'],
permissions: ['add', 'confirm', 'edit', 'list', 'query'],
payloads: {
addShipment: ['palletIds', 'sourceWarehouseId', 'targetSiteId'],
updateShipment: ['palletIds', 'shipmentId', 'sourceWarehouseId', 'targetSiteId'],
@@ -361,47 +351,40 @@ const expectedButtonPermissions = [
'master:strain:query',
'master:strain:add',
'master:strain:edit',
'master:strain:remove',
'master:customer:query',
'master:customer:add',
'master:customer:edit',
'master:customer:remove',
'master:site:query',
'master:site:add',
'master:site:edit',
'master:site:remove',
'master:site:assign',
'master:line:query',
'master:line:add',
'master:line:edit',
'master:line:remove',
'master:warehouse:query',
'master:warehouse:add',
'master:warehouse:edit',
'master:warehouse:remove',
'master:warehouse:assign',
'master:pallet:query',
'master:pallet:add',
'master:pallet:edit',
'master:pallet:remove',
'master:pallet:print',
'business:inbound:query',
'business:inbound:add',
'business:inbound:edit',
'business:inbound:remove',
'business:inbound:confirm',
'business:filling:query',
'business:filling:add',
'business:shipment:query',
'business:shipment:add',
'business:shipment:edit',
'business:shipment:remove',
'business:shipment:confirm',
'business:usage:query',
'business:usage:add',
'business:return:query',
'business:return:request',
'business:return:confirm'
'business:return:confirm',
'system:testbatch:adopt'
]
const expectedPermissions = [...expectedPagePermissions, ...expectedButtonPermissions].sort()
@@ -536,7 +519,7 @@ function parseStaticPermissionArray(expression) {
assert.ok(index === ranges.length - 1, '权限门禁数组禁止稀疏元素,仅允许单个尾逗号')
continue
}
const literal = /^(['"])((?:master|business|report):[a-z]+:[a-z]+)\1$/.exec(item)
const literal = /^(['"])((?:master|business|report|system):[a-z]+:[a-z]+)\1$/.exec(item)
assert.ok(literal, '权限门禁数组禁止动态表达式或非法字面量')
permissions.push(literal[2])
}
@@ -1147,8 +1130,8 @@ test('窄屏模式控件仍保留正式退出与测试批次切换入口', async
test('17 个页面的真实权限门禁逐项匹配独立硬编码契约', async () => {
assert.equal(expectedPagePermissions.length, 17)
assert.equal(expectedButtonPermissions.length, 44)
assert.equal(new Set(expectedPermissions).size, 61)
assert.equal(expectedButtonPermissions.length, 37)
assert.equal(new Set(expectedPermissions).size, 54)
const relativePaths = [
...Object.values(masterPageContracts),
...Object.values(businessPageContracts),
@@ -1450,7 +1433,7 @@ test('首次入库确认失败后取消重试不丢弃原幂等键', async () =>
test('17 个 API 文件逐一匹配硬编码请求契约', async (t) => {
assert.equal(Object.keys(expectedRequests).length, 17)
assert.equal(Object.values(expectedRequests).flat().length, 74)
assert.equal(Object.values(expectedRequests).flat().length, 66)
for (const [relativePath, expected] of Object.entries(expectedRequests)) {
await t.test(relativePath, async () => {
@@ -1647,7 +1630,6 @@ test('六个基础资料页面逐页满足 API、权限、分页和安全删除
assertButtonsUsePermission(source, 'submitCreate', `${permissionPrefix}:add`)
assertButtonsUsePermission(source, 'handleUpdate', `${permissionPrefix}:edit`)
assertButtonsUsePermission(source, 'submitUpdate', `${permissionPrefix}:edit`)
assertButtonsUsePermission(source, 'handleDelete', `${permissionPrefix}:remove`)
for (const button of extractButtonBlocks(source, 'handleUpdate')) {
assert.match(
@@ -1708,20 +1690,10 @@ test('六个基础资料页面逐页满足 API、权限、分页和安全删除
assert.doesNotMatch(selectionColumn, /:selectable=/)
assert.doesNotMatch(source, /function isRowSelectable\(/)
assert.match(source, /const selectedIds = ref\(\[\]\)/)
assert.match(source, /const removableIds = ref\(\[\]\)/)
const selectionSource = source.slice(source.indexOf('function handleSelectionChange('), source.indexOf('function handleAdd('))
assert.match(selectionSource, new RegExp(`selectedIds\\.value = selection\\.map\\(\\(item\\) => item\\.${idKey}\\)`))
assert.match(selectionSource, new RegExp(`removableIds\\.value = selection\\s*\\.filter\\(\\(item\\) => item\\.canRemove === true\\)\\s*\\.map\\(\\(item\\) => item\\.${idKey}\\)`))
assert.match(selectionSource, /single\.value = selectedIds\.value\.length !== 1/)
assert.match(selectionSource, /multiple\.value = removableIds\.value\.length === 0/)
assert.match(detailSource, new RegExp(`const ${idKey} = row\\.${idKey} \\|\\| selectedIds\\.value\\[0\\]`))
assert.match(source, new RegExp(`const targetIds = row\\?\\.${idKey} \\? \\(row\\.canRemove === true \\? \\[row\\.${idKey}\\] : \\[\\]\\) : removableIds\\.value`))
const rowDeleteButtons = extractButtonBlocks(source, 'handleDelete')
.filter((button) => button.includes('handleDelete(scope.row)'))
assert.ok(rowDeleteButtons.length > 0, `${resource} 缺少行删除按钮`)
for (const button of rowDeleteButtons) {
assert.match(button, /v-if="scope\.row\.canRemove === true"/)
}
const rowUpdateButtons = extractButtonBlocks(source, 'handleUpdate')
.filter((button) => button.includes('handleUpdate(scope.row)'))
assert.ok(rowUpdateButtons.length > 0, `${resource} 缺少行修改按钮`)
@@ -1980,7 +1952,6 @@ test('入库页面只允许 DRAFT 变更并以 authoritative 详情和独立确
assertButtonsUsePermission(source, 'handleAdd', 'business:inbound:add')
assertButtonsUsePermission(source, 'submitInbound', 'business:inbound:add')
assertButtonsUsePermission(source, 'handleUpdate', 'business:inbound:edit')
assertButtonsUsePermission(source, 'handleDelete', 'business:inbound:remove')
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'\]\)/)
@@ -1999,9 +1970,8 @@ test('入库页面只允许 DRAFT 变更并以 authoritative 详情和独立确
/^function handleAdd\(\)\s*\{\s*if \(!canAddInbound\.value \|\| operationPending\.value \|\| optionLoading\.value \|\| detailLoading\.value \|\| open\.value\) return/
)
assert.match(source, /scope\.row\.status === ['"]DRAFT['"][\s\S]*?handleUpdate\(scope\.row\)/)
assert.match(source, /scope\.row\.status === ['"]DRAFT['"][\s\S]*?handleDelete\(scope\.row\)/)
assert.match(source, /scope\.row\.status === ['"]DRAFT['"][\s\S]*?handleConfirm\(scope\.row\)/)
for (const functionName of ['handleUpdate', 'handleDelete', 'handleConfirm']) {
for (const functionName of ['handleUpdate', 'handleConfirm']) {
assert.match(extractNamedFunction(source, functionName), /status !== ['"]DRAFT['"]/)
}
@@ -2041,11 +2011,6 @@ test('入库页面只允许 DRAFT 变更并以 authoritative 详情和独立确
)
assert.match(submitSource, /if \(!valid\) \{\s*submitLoading\.value = false\s*return/)
assert.match(submitSource, /\.finally\(\(\) => \{\s*submitLoading\.value = false/)
const deleteSource = extractNamedFunction(source, 'handleDelete')
assert.ok(
deleteSource.indexOf('deleteLoading.value = true') < deleteSource.indexOf('proxy.$modal.confirm('),
'入库删除必须在打开二次确认框前加锁'
)
assert.deepEqual(extractSecondCallObjectKeys(source, 'scanInboundPallet'), ['warehouseId'])
const scanSource = extractNamedFunction(source, 'handleInboundScan')
@@ -2187,7 +2152,6 @@ test('发货页面只允许 DRAFT 变更、展示只读余量并复用确认幂
assertButtonsUsePermission(source, 'handleAdd', 'business:shipment:add')
assertButtonsUsePermission(source, 'submitShipment', 'business:shipment:add')
assertButtonsUsePermission(source, 'handleUpdate', 'business:shipment:edit')
assertButtonsUsePermission(source, 'handleDelete', 'business:shipment:remove')
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'\]\)/)
@@ -2206,9 +2170,8 @@ test('发货页面只允许 DRAFT 变更、展示只读余量并复用确认幂
/^function handleAdd\(\)\s*\{\s*if \(!canAddShipment\.value \|\| operationPending\.value \|\| optionLoading\.value \|\| detailLoading\.value \|\| open\.value\) return/
)
assert.match(source, /scope\.row\.status === ['"]DRAFT['"][\s\S]*?handleUpdate\(scope\.row\)/)
assert.match(source, /scope\.row\.status === ['"]DRAFT['"][\s\S]*?handleDelete\(scope\.row\)/)
assert.match(source, /scope\.row\.status === ['"]DRAFT['"][\s\S]*?handleConfirm\(scope\.row\)/)
for (const functionName of ['handleUpdate', 'handleDelete', 'handleConfirm']) {
for (const functionName of ['handleUpdate', 'handleConfirm']) {
assert.match(extractNamedFunction(source, functionName), /status !== ['"]DRAFT['"]/)
}
@@ -2250,11 +2213,6 @@ test('发货页面只允许 DRAFT 变更、展示只读余量并复用确认幂
)
assert.match(submitSource, /if \(!valid\) \{\s*submitLoading\.value = false\s*return/)
assert.match(submitSource, /\.finally\(\(\) => \{\s*submitLoading\.value = false/)
const deleteSource = extractNamedFunction(source, 'handleDelete')
assert.ok(
deleteSource.indexOf('deleteLoading.value = true') < deleteSource.indexOf('proxy.$modal.confirm('),
'发货删除必须在打开二次确认框前加锁'
)
assert.deepEqual(extractSecondCallObjectKeys(source, 'scanShipmentPallet'), ['sourceWarehouseId', 'targetSiteId'])
assert.match(source, /label="[^"]*只读[^"]*"[^>]*prop="remainingPackages"|prop="remainingPackages"[\s\S]*?不可修改/)
@@ -2534,6 +2492,7 @@ test('测试批次管理与旧数据纳入遵守 root、权限和预览提交契
assert.match(source, /useUserStore\(\)/, `${rootType} 必须读取 admin 可见性`)
assert.match(source, /useTestBatchStore\(\)/, `${rootType} 必须读取测试模式`)
assert.match(source, /userStore\.admin === true\s*&&\s*!testBatchStore\.isTestMode/, `${rootType} 纳入入口必须只在管理员正式模式显示`)
assert.match(source, /v-hasPermi="\['system:testbatch:adopt'\]"/, `${rootType} 纳入入口必须校验批次纳入权限`)
assert.match(source, new RegExp(`<test-batch-adoption-dialog\\b[\\s\\S]*?:root-type="['\"]${rootType}['\"][\\s\\S]*?:resource-ids="selectedIds"`))
assert.match(source, new RegExp(`selectedIds\\.value\\s*=\\s*selection\\.map\\(\\(item\\)\\s*=>\\s*item\\.${contract.id}\\)`), `${rootType} 必须传递正整数根 ID`)
}