feat: gate overseas receiving and harden phase4 workflows

This commit is contained in:
2026-08-25 10:31:04 +08:00
parent cb78bc6b41
commit 52b9fc382f
93 changed files with 2779 additions and 201 deletions
@@ -1,5 +1,18 @@
const MODULE = 'phase3-warehouse-rbac'
const OPTIONS = { scenario: 'warehouse-role-boundary', deploymentBlocker: true }
const PHASE4_READ_CODES = [
'platform:overseas-warehouse:last-mile-order:list',
'platform:overseas-warehouse:inbound-order:list',
'platform:overseas-warehouse:relabel-order:list',
'platform:overseas-warehouse:inventory:list',
'platform:overseas-warehouse:warehouse:list',
'platform:overseas-warehouse:location:list',
'platform:overseas-warehouse:pallet:list',
'platform:overseas-business:purchase:list',
'platform:overseas-business:truck:list',
'platform:overseas-business:looseboard:list',
]
const PHASE4_WRITE_SUFFIXES = [':create', ':edit', ':status', ':export', ':adjust', ':delete', ':review']
function id(value) { return value == null ? '' : String(value) }
@@ -41,14 +54,17 @@ export async function run(context) {
`permissions=${[...pickerCodes].sort().join(',')}`, OPTIONS)
const operatorCodes = roleCodes('WAREHOUSE_OPERATOR')
context.assert(MODULE, '操作员仅具备入仓查看和后台证据维护权限', rolesResponse,
context.assert(MODULE, '操作员具备国内入仓权限与四期十项只读权限', rolesResponse,
operatorCodes.has('platform:firstleg:inbound:loading:view')
&& operatorCodes.has('platform:firstleg:inbound:loading:edit')
&& operatorCodes.size === 5
&& operatorCodes.size === 15
&& PHASE4_READ_CODES.every((code) => operatorCodes.has(code))
&& !operatorCodes.has('platform:firstleg:inbound:loading:complete')
&& !operatorCodes.has('platform:firstleg:pda:assign')
&& !operatorCodes.has('platform:firstleg:outbound-dispatch:confirm')
&& !operatorCodes.has('platform:firstleg:outbound-dispatch:reverse'),
&& !operatorCodes.has('platform:firstleg:outbound-dispatch:reverse')
&& [...operatorCodes].filter((code) => code.startsWith('platform:overseas-'))
.every((code) => !PHASE4_WRITE_SUFFIXES.some((suffix) => code.endsWith(suffix))),
`permissionCount=${operatorCodes.size}`, OPTIONS)
const tallyCodes = roleCodes('WAREHOUSE_TALLY')
@@ -56,10 +72,21 @@ export async function run(context) {
tallyCodes.has('platform:firstleg:inbound:loading:view')
&& tallyCodes.has('platform:firstleg:inbound:loading:edit')
&& tallyCodes.has('platform:firstleg:pda:review')
&& tallyCodes.size === 22
&& PHASE4_READ_CODES.every((code) => tallyCodes.has(code))
&& !tallyCodes.has('platform:firstleg:inbound:loading:complete')
&& !tallyCodes.has('platform:firstleg:outbound-dispatch:confirm'),
&& !tallyCodes.has('platform:firstleg:outbound-dispatch:confirm')
&& [...tallyCodes].filter((code) => code.startsWith('platform:overseas-'))
.every((code) => !PHASE4_WRITE_SUFFIXES.some((suffix) => code.endsWith(suffix))),
`permissionCount=${tallyCodes.size}`, OPTIONS)
context.assert(MODULE, '四期list权限同时用于列表与详情且不隐含写权限', rolesResponse,
PHASE4_READ_CODES.every((code) => code.endsWith(':list'))
&& [...operatorCodes, ...tallyCodes]
.filter((code) => code.startsWith('platform:overseas-'))
.every((code) => PHASE4_READ_CODES.includes(code)),
`phase4ReadCodes=${PHASE4_READ_CODES.length}`, OPTIONS)
context.assert(MODULE, '仓库系统角色不包含任何客户侧权限', rolesResponse,
[...operatorCodes, ...pickerCodes, ...tallyCodes]
.every((code) => code.startsWith('platform:') || code === 'lastleg:pda:operate'),
@@ -0,0 +1,39 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const acceptance = readFileSync(new URL('./phase3-warehouse-rbac.mjs', import.meta.url), 'utf8')
const iamSql = readFileSync(new URL('../../iam/src/main/resources/db/lastleg-permissions.sql', import.meta.url), 'utf8')
const listCodes = [
'platform:overseas-warehouse:last-mile-order:list',
'platform:overseas-warehouse:inbound-order:list',
'platform:overseas-warehouse:relabel-order:list',
'platform:overseas-warehouse:inventory:list',
'platform:overseas-warehouse:warehouse:list',
'platform:overseas-warehouse:location:list',
'platform:overseas-warehouse:pallet:list',
'platform:overseas-business:purchase:list',
'platform:overseas-business:truck:list',
'platform:overseas-business:looseboard:list',
]
test('warehouse operator and tally use the exact phase-four list/detail permission set', () => {
for (const code of listCodes) {
assert.match(acceptance, new RegExp(code.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
assert.match(iamSql, new RegExp(`'WAREHOUSE_OPERATOR','${code}'`))
assert.match(iamSql, new RegExp(`'WAREHOUSE_TALLY','${code}'`))
}
assert.match(acceptance, /operatorCodes\.size === 15/)
assert.match(acceptance, /tallyCodes\.size === 22/)
assert.match(acceptance, /PHASE4_WRITE_SUFFIXES/)
assert.match(iamSql, /assert_phase4_warehouse_role_minimum/)
assert.match(iamSql, /permission_code REGEXP ':\(create\|edit\|status\|export\|adjust\|delete\|review\)\$'/)
})
test('picker stays isolated to the dedicated overseas PDA authority', () => {
assert.match(acceptance, /pickerCodes\.size === 3/)
assert.match(acceptance, /pickerCodes\.has\('lastleg:pda:operate'\)/)
assert.match(iamSql, /role\.role_code='WAREHOUSE_PICKER'/)
assert.match(iamSql, /permission\.permission_code='lastleg:pda:operate'/)
})
+9
View File
@@ -3765,3 +3765,12 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
'ALTER TABLE ll_overseas_procurement_status_history MODIFY COLUMN change_detail JSON NULL COMMENT ''字段变更前后值JSON''',
'ALTER TABLE ll_overseas_procurement_status_history ADD COLUMN change_detail JSON NULL COMMENT ''字段变更前后值JSON'' AFTER operator_id'));
PREPARE phase4_schema_028 FROM @ddl; EXECUTE phase4_schema_028; DEALLOCATE PREPARE phase4_schema_028;
-- 四期货物废弃审核幂等快照;存量表按字段存在性幂等补齐。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_cargo_discard_application' AND column_name='review_idempotency_key'),
'SELECT 1', 'ALTER TABLE ll_cargo_discard_application ADD COLUMN review_idempotency_key VARCHAR(255) NULL COMMENT ''审核幂等键'' AFTER request_hash'));
PREPARE phase4_discard_review_stmt FROM @ddl; EXECUTE phase4_discard_review_stmt; DEALLOCATE PREPARE phase4_discard_review_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_cargo_discard_application' AND column_name='review_request_hash'),
'SELECT 1', 'ALTER TABLE ll_cargo_discard_application ADD COLUMN review_request_hash CHAR(64) NULL COMMENT ''审核请求内容哈希'' AFTER review_idempotency_key'));
PREPARE phase4_discard_review_stmt FROM @ddl; EXECUTE phase4_discard_review_stmt; DEALLOCATE PREPARE phase4_discard_review_stmt;