This commit is contained in:
2026-08-28 10:38:58 +08:00
parent b34fbb2bd1
commit 1e5ce54858
37 changed files with 256 additions and 159 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ export async function run(context) {
表中的 `*-fixture.mjs` 是被业务模块复用的测试数据前置,不是新增的独立全量入口。执行 HTTP 时仍从 `../admin-test-env-acceptance.mjs` 进入;`*contract.test.mjs` 只做离线脚本结构和数据归属回归。
四期 HTTP 验收使用国内登录响应同时签发的短时海外 Token。总入口通过 `--gateway` 访问国内 Gateway,通过 `--overseas-gateway` 访问海外 Gateway。海外仓、海外业务和海外主数据模块只对带本轮 marker 的资源执行已确认的新增、编辑、状态与导出,并在结束前按契约停用;PDA 收货模块使用本轮临时 WAREHOUSE_PICKER 海外 Token,只调用 `/api/pda/v1/last-leg/receiving-containers/**` 柜号收货入口,不接受 `customerId`、`planNo` 或分配 ID。收货成功后由相邻的 `phase4-overseas-pda-operations` 消费仅存在于进程内存、不会进入 JSON 报告的 Token 与库存事实,执行盘点、整托移库、托盘操作和换标;新模块无论成功失败都在 finally 停用两个 picker 并清空临时凭据。两模块均不调用出库、退货出库、派送或内部接口;任何真实柜、箱唛、托盘、库存或换标前置缺失都必须形成 deployment blocker,不能按空场景通过;船节点禁止手工创建任务。
四期 HTTP 验收使用国内登录响应同时签发的短时海外 Token。总入口通过 `--gateway` 访问国内 Gateway,通过 `--overseas-gateway` 访问海外 Gateway。海外仓、海外业务和海外主数据模块只对带本轮 marker 的资源执行已确认的新增、编辑、状态与导出,并在结束前按契约停用;PDA 收货模块使用本轮临时 OVERSEAS_WAREHOUSE_PDA_OPERATOR 海外 Token,只调用 `/api/pda/v1/last-leg/receiving-containers/**` 柜号收货入口,不接受 `customerId`、`planNo` 或分配 ID。收货成功后由相邻的 `phase4-overseas-pda-operations` 消费仅存在于进程内存、不会进入 JSON 报告的 Token 与库存事实,执行盘点、整托移库、托盘操作和换标;新模块无论成功失败都在 finally 停用两个 picker 并清空临时凭据。两模块均不调用出库、退货出库、派送或内部接口;任何真实柜、箱唛、托盘、库存或换标前置缺失都必须形成 deployment blocker,不能按空场景通过;船节点禁止手工创建任务。
节点全链定向入口为 `--modules phase4-overseas-pda`。依赖展开固定经过本轮 `LOADED` 柜次的 `DESTINATION_CLEARANCE`、`CUSTOMS_DECLARATION`、`DELIVERY_ORDER`、`PICKUP_ORDER` 四类资料生命周期,再确认国内出仓、核对自动创建的 `OUTBOUND_CONFIRM` 船节点任务、人工补录至实际 `PICKUP_READY`,最后验证海外 PDA 候选可见。四类资料模块只写本轮 marker 柜次,覆盖上传/完成重放、同键异载荷 `A0409`、冲突后零漂移和聚合回读;任一 fixture 或 Trace 缺失均为 deployment blocker。
@@ -36,22 +36,30 @@ export async function run(context) {
const rolesResponse = await context.request('GET', '/api/platform/v1/iam/roles')
const roles = context.apiSuccess(MODULE, '查询三期仓库内置角色', rolesResponse, '', OPTIONS)
const warehouseRoles = new Map((Array.isArray(roles) ? roles : [])
.filter((item) => ['WAREHOUSE_OPERATOR', 'WAREHOUSE_PICKER', 'WAREHOUSE_TALLY'].includes(item.roleCode))
.filter((item) => ['WAREHOUSE_OPERATOR', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', 'WAREHOUSE_TALLY', 'CN_FIRSTLEG_PDA_OPERATOR'].includes(item.roleCode))
.map((item) => [item.roleCode, item]))
context.assert(MODULE, '操作员、拣货员、理货员三个系统角色已启用', rolesResponse,
warehouseRoles.size === 3 && [...warehouseRoles.values()]
context.assert(MODULE, '国内后台、国内PDA、海外PDA、理货复核四个系统角色已启用', rolesResponse,
warehouseRoles.size === 4 && [...warehouseRoles.values()]
.every((role) => role.roleType === 'SYSTEM' && role.status === 'ACTIVE'),
`roles=${[...warehouseRoles.keys()].join(',')}`, OPTIONS)
const roleCodes = (roleCode) => new Set((warehouseRoles.get(roleCode)?.permissionIds || [])
.map((permissionId) => permissionById.get(id(permissionId))).filter(Boolean))
const pickerCodes = roleCodes('WAREHOUSE_PICKER')
context.assert(MODULE, '拣货员只具备国内PDA查询作业与海外PDA入仓操作权限', rolesResponse,
pickerCodes.size === 3
&& pickerCodes.has('platform:firstleg:pda:view')
&& pickerCodes.has('platform:firstleg:pda:work')
&& pickerCodes.has('lastleg:pda:operate'),
`permissions=${[...pickerCodes].sort().join(',')}`, OPTIONS)
const domesticPdaCodes = roleCodes('CN_FIRSTLEG_PDA_OPERATOR')
context.assert(MODULE, '国内头程PDA角色只具备国内任务查询与作业权限', rolesResponse,
domesticPdaCodes.size === 2
&& domesticPdaCodes.has('platform:firstleg:pda:view')
&& domesticPdaCodes.has('platform:firstleg:pda:work')
&& !domesticPdaCodes.has('lastleg:pda:operate'),
`permissions=${[...domesticPdaCodes].sort().join(',')}`, OPTIONS)
const overseasPdaCodes = roleCodes('OVERSEAS_WAREHOUSE_PDA_OPERATOR')
context.assert(MODULE, '海外仓PDA角色只具备海外PDA作业权限', rolesResponse,
overseasPdaCodes.size === 1
&& overseasPdaCodes.has('lastleg:pda:operate')
&& !overseasPdaCodes.has('platform:firstleg:pda:view')
&& !overseasPdaCodes.has('platform:firstleg:pda:work'),
`permissions=${[...overseasPdaCodes].sort().join(',')}`, OPTIONS)
const operatorCodes = roleCodes('WAREHOUSE_OPERATOR')
context.assert(MODULE, '操作员具备国内入仓权限与四期十项只读权限', rolesResponse,
@@ -88,7 +96,7 @@ export async function run(context) {
`phase4ReadCodes=${PHASE4_READ_CODES.length}`, OPTIONS)
context.assert(MODULE, '仓库系统角色不包含任何客户侧权限', rolesResponse,
[...operatorCodes, ...pickerCodes, ...tallyCodes]
[...operatorCodes, ...domesticPdaCodes, ...overseasPdaCodes, ...tallyCodes]
.every((code) => code.startsWith('platform:') || code === 'lastleg:pda:operate'),
'warehouse roles may use platform:* plus the single overseas service authority lastleg:pda:operate', OPTIONS)
@@ -32,10 +32,11 @@ test('warehouse operator and tally use the exact phase-four list/detail permissi
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'/)
test('domestic and overseas PDA operators use isolated authorities', () => {
assert.match(acceptance, /domesticPdaCodes\.size === 2/)
assert.match(acceptance, /overseasPdaCodes\.size === 1/)
assert.match(acceptance, /overseasPdaCodes\.has\('lastleg:pda:operate'\)/)
assert.match(iamSql, /role\.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'/)
assert.match(iamSql, /permission\.permission_code='lastleg:pda:operate'/)
})
@@ -33,7 +33,7 @@ test('PDA module covers the container-number receiving contract and excludes out
assert.match(source, /同键异载荷冲突/)
assert.match(source, /跨柜切换锁返回409/)
assert.match(source, /第二仓库操作员抢占同柜返回409/)
assert.match(source, /WAREHOUSE_PICKER/)
assert.match(source, /OVERSEAS_WAREHOUSE_PDA_OPERATOR/)
assert.match(source, /lastleg:pda:operate/)
assert.match(source, /安全投影已收敛/)
assert.match(source, /projectionReady = success\(projectionResponse\) \|\| notFound\(projectionResponse\)/)
@@ -157,7 +157,7 @@ test('PDA receiving lifecycle is ordered from candidate claim through photos, mo
]) assert.ok(source.includes(token), token)
})
test('missing WAREHOUSE_PICKER overseas authority is a deployment blocker', async () => {
test('missing OVERSEAS_WAREHOUSE_PDA_OPERATOR overseas authority is a deployment blocker', async () => {
const calls = []; const gaps = []; const steps = []
const context = {
marker: 'PDA-CONTRACT', context: {}, createdResources: {},
@@ -192,7 +192,7 @@ test('empty candidate response cannot become a zero-business-request pass', asyn
async request(method, requestPath, body) {
calls.push({ channel: 'domestic', method, requestPath })
if (method === 'GET' && requestPath.endsWith('/roles')) return response([
{ id: roleId, roleCode: 'WAREHOUSE_PICKER', status: 'ACTIVE', permissionIds: [permissionId] },
{ id: roleId, roleCode: 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', status: 'ACTIVE', permissionIds: [permissionId] },
], requestPath, method)
if (method === 'GET' && requestPath.endsWith('/permissions')) return response([
{ id: permissionId, permissionCode: 'lastleg:pda:operate' },
@@ -90,15 +90,15 @@ async function loginMerchantForNegativeProbe(context) {
async function resolvePickerAccess(context) {
const rolesResponse = await context.request('GET', '/api/platform/v1/iam/roles')
const roles = context.apiSuccess(MODULE, '查询海外PDA仓库操作员角色', rolesResponse, '', OPTIONS)
const role = (Array.isArray(roles) ? roles : []).find((item) => item?.roleCode === 'WAREHOUSE_PICKER' && item?.status === 'ACTIVE')
const role = (Array.isArray(roles) ? roles : []).find((item) => item?.roleCode === 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' && item?.status === 'ACTIVE')
const permissionsResponse = await context.request('GET', '/api/platform/v1/iam/permissions')
const permissions = context.apiSuccess(MODULE, '查询海外PDA权限资源', permissionsResponse, '', OPTIONS)
const operate = (Array.isArray(permissions) ? permissions : []).find((item) => item?.permissionCode === 'lastleg:pda:operate')
const assigned = Boolean(role && operate && (role.permissionIds || []).map(id).includes(id(operate.id)))
context.assert(MODULE, 'WAREHOUSE_PICKER精确具备海外PDA权限', rolesResponse, assigned,
context.assert(MODULE, 'OVERSEAS_WAREHOUSE_PDA_OPERATOR精确具备海外PDA权限', rolesResponse, assigned,
`roleId=${id(role?.id) || 'missing'}, permissionId=${id(operate?.id) || 'missing'}`, OPTIONS)
if (!assigned) {
blockerGap(context, '海外PDA仓库操作员权限', 'WAREHOUSE_PICKER未获得lastleg:pda:operate;必须先执行最新国内统一增量SQL并部署IAM')
blockerGap(context, '海外PDA仓库操作员权限', 'OVERSEAS_WAREHOUSE_PDA_OPERATOR未获得lastleg:pda:operate;必须先执行最新国内统一增量SQL并部署IAM')
return null
}
return { role, operate }
@@ -662,7 +662,7 @@ export async function run(context) {
}
try {
if (!primary?.token || !secondary?.token) {
blockerGap(context, '海外PDA双操作员账号', '未能创建并登录两个WAREHOUSE_PICKER平台账号,无法验证全局一柜锁')
blockerGap(context, '海外PDA双操作员账号', '未能创建并登录两个OVERSEAS_WAREHOUSE_PDA_OPERATOR平台账号,无法验证全局一柜锁')
return
}
const matrix = context.createdResources?.phase4InboundProducerFixtures