fix: close phase4 overseas acceptance gaps

This commit is contained in:
2026-08-24 21:00:22 +08:00
parent 005c3f54e2
commit 3607bebbfd
42 changed files with 860 additions and 203 deletions
@@ -35,20 +35,58 @@ async function expected(context, token, method, name, path, body, predicate) {
return response
}
async function loginPda(context) {
async function loginMerchantForNegativeProbe(context) {
const fixture = context.context?.merchantQualificationFixture
if (!fixture?.username || !fixture?.password) {
blockerGap(context, 'PDA客户账号', '缺少 merchant-qualification-fixture 生成的本轮客户账号;请保留默认全量前置模块')
return null
}
if (!fixture?.username || !fixture?.password) return null
const response = await context.request('POST', '/api/auth/login', { username: fixture.username, password: fixture.password }, '')
const data = success(response) ? response.body?.data : null
context.addStep(MODULE, '本轮客户登录并取得海外PDA令牌', response,
context.addStep(MODULE, '本轮客户登录取得海外令牌用于PDA越权反证', response,
success(response) && nonBlank(data?.overseasAccessToken),
`overseasTokenPresent=${nonBlank(data?.overseasAccessToken)}; ${trace(response)}`, OPTIONS)
return nonBlank(data?.overseasAccessToken) ? data.overseasAccessToken : null
}
async function createPicker(context, index) {
const rolesResponse = await context.request('GET', '/api/platform/v1/iam/roles')
const roles = context.apiSuccess(MODULE, `查询海外PDA仓库操作员角色${index}`, rolesResponse, '', OPTIONS)
const role = (Array.isArray(roles) ? roles : []).find((item) => item?.roleCode === 'WAREHOUSE_PICKER' && item?.status === 'ACTIVE')
const permissionsResponse = await context.request('GET', '/api/platform/v1/iam/permissions')
const permissions = context.apiSuccess(MODULE, `查询海外PDA权限资源${index}`, 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权限${index}`, 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')
return null
}
const suffix = markerSuffix(context)
const username = `pda_overseas_${suffix.toLowerCase()}_${index}`.slice(0, 64)
const password = `Pda${suffix.replace(/[^A-Za-z0-9]/g, '')}A${index}9`
const phone = await context.findAvailablePhone(MODULE)
const payload = { username, displayName: `海外PDA验收操作员${index}-${suffix}`, phone,
email: `${username}@example.com`, tenantId: '0', roleIds: [id(role.id)], password, status: 'ACTIVE' }
const createdResponse = await context.request('POST', '/api/platform/v1/iam/users', payload)
const created = context.apiSuccess(MODULE, `创建海外PDA仓库操作员${index}`, createdResponse, '', OPTIONS)
const membershipId = id(created?.membershipId)
context.assert(MODULE, `海外PDA操作员${index}冻结平台范围与角色`, createdResponse,
longId(membershipId) && id(created?.tenantId) === '0' && (created?.roleIds || []).map(id).includes(id(role.id)),
`membershipId=${membershipId || 'missing'}, tenantId=${id(created?.tenantId) || 'missing'}`, OPTIONS)
if (!longId(membershipId)) return null
const login = await context.request('POST', '/api/auth/login', { username, password }, '')
const token = success(login) ? login.body?.data?.overseasAccessToken : null
context.addStep(MODULE, `海外PDA操作员${index}登录取得双区域海外令牌`, login, nonBlank(token),
`overseasTokenPresent=${nonBlank(token)}; ${trace(login)}`, OPTIONS)
return { membershipId, username, token: nonBlank(token) ? token : null }
}
async function disablePicker(context, picker, index) {
if (!longId(picker?.membershipId)) return
const response = await context.request('PUT', `/api/platform/v1/iam/users/${picker.membershipId}/status`, { status: 'DISABLED' })
const disabled = success(response) && response.body?.data?.status === 'DISABLED'
context.addStep(MODULE, `finally停用海外PDA仓库操作员${index}`, response, disabled, trace(response), OPTIONS)
}
async function createPallet(context, inbound, index) {
const warehouseId = id(inbound?.destinationWarehouseId)
if (!longId(warehouseId)) return null
@@ -160,17 +198,14 @@ async function receiveOverseas(context, token, inbound, pallet) {
return true
}
export async function run(context) {
context.createdResources.phase4OverseasPda = {
marker: context.marker, containerNo: null, customerId: context.context?.merchantQualificationFixture?.tenantId || null,
palletIds: [], locationIds: [], finalStatus: null,
cleanup: 'Pallets and locations used by completed inventory are retained as acceptance evidence',
}
const token = await loginPda(context)
if (!token) return
async function runReceiving(context, token, secondaryToken, merchantToken) {
if (context.overseasToken) {
const denied = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, context.overseasToken)
context.addStep(MODULE, '平台海外令牌不能冒充客户PDA账号', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
context.addStep(MODULE, '平台管理员海外令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
}
if (merchantToken) {
const denied = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, merchantToken)
context.addStep(MODULE, '客户海外令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
}
const before = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/current', undefined, {}, token)
const noCurrent = notFound(before)
@@ -203,6 +238,11 @@ export async function run(context) {
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`, prepareBody)
context.assert(MODULE, '选择柜号幂等结果一致', prepareReplay.response,
success(prepareReplay.response) && prepareReplay.data?.containerNo === containerNo && prepareReplay.data?.status === 'WAIT_SIGN', trace(prepareReplay.response), OPTIONS)
if (secondaryToken) {
await expected(context, secondaryToken, 'POST', '第二仓库操作员抢占同柜返回409',
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`,
{ idempotencyKey: key(context, 'secondary-prepare') }, conflict)
}
const other = candidates.data.find((item) => item.containerNo !== containerNo)
if (other) await expected(context, token, 'GET', '当前账号跨柜切换锁返回409',
`/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(other.containerNo)}`, undefined, conflict)
@@ -268,3 +308,27 @@ export async function run(context) {
context.addStep(MODULE, '完成后current返回404正常空态', after, notFound(after), trace(after), OPTIONS)
context.createdResources.phase4OverseasPda.finalStatus = completed.data?.status || null
}
export async function run(context) {
context.createdResources.phase4OverseasPda = {
marker: context.marker, containerNo: null, customerId: null,
palletIds: [], locationIds: [], finalStatus: null, pickerMembershipIds: [],
cleanup: 'Pallets and locations used by completed inventory are retained as acceptance evidence',
}
const merchantToken = await loginMerchantForNegativeProbe(context)
const primary = await createPicker(context, 1)
const secondary = primary?.token ? await createPicker(context, 2) : null
for (const picker of [primary, secondary]) if (picker?.membershipId) {
context.createdResources.phase4OverseasPda.pickerMembershipIds.push(picker.membershipId)
}
try {
if (!primary?.token || !secondary?.token) {
blockerGap(context, '海外PDA双操作员账号', '未能创建并登录两个WAREHOUSE_PICKER平台账号,无法验证全局一柜锁')
return
}
await runReceiving(context, primary.token, secondary.token, merchantToken)
} finally {
await disablePicker(context, secondary, 2)
await disablePicker(context, primary, 1)
}
}