Files
baiyue-firstleg/scripts/admin-test-env-modules/domestic-container.mjs
T

258 lines
19 KiB
JavaScript
Raw Normal View History

2026-08-13 11:57:33 +08:00
const MODULE = 'domestic-container'
const BASE = '/api/platform/v1/domestic-containers'
const PERMISSIONS = ['platform:domestic-container:list', 'platform:domestic-container:inbound:create',
'platform:domestic-container:outbound:create']
function id(value) { return value == null ? '' : String(value) }
function succeeded(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
function traced(context, label, response) {
context.assert(MODULE, `${label}返回链路标识`, response, Boolean(response?.requestId) && Boolean(response?.traceId),
`requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`)
}
function expectCode(context, label, response, code) {
context.assert(MODULE, label, response, response?.http === 200 && String(response?.body?.code) === code,
`HTTP=${response?.http}, code=${response?.body?.code || 'missing'}`)
traced(context, label, response)
}
async function api(context, method, path, body, token = context.token) {
const response = await context.request(method, path, body, token)
traced(context, `${method} ${path}`, response)
return response
}
async function verifyValidationBoundaries(context, warehouseId, marker, now) {
const valid = {
warehouseId, cargoName: marker, containerNo: `DCV${marker}`.slice(0, 32).toUpperCase(), boxCount: 1,
volumeCbm: 1, inboundTime: now, status: 'COMPLETED', remark: marker,
}
const cases = [
['集装箱号格式非法被拒绝', { ...valid, containerNo: 'bad!', idempotencyKey: `${marker}-invalid-container` }],
['零箱入库被拒绝', { ...valid, boxCount: 0, idempotencyKey: `${marker}-zero-boxes` }],
['零体积入库被拒绝', { ...valid, volumeCbm: 0, idempotencyKey: `${marker}-zero-volume` }],
['非法入库状态被拒绝', { ...valid, status: 'UNKNOWN', idempotencyKey: `${marker}-invalid-status` }],
['空幂等键被拒绝', { ...valid, idempotencyKey: ' ' }],
['超长备注被拒绝', { ...valid, remark: 'R'.repeat(501), idempotencyKey: `${marker}-long-remark` }],
]
for (const [label, body] of cases) {
const response = await api(context, 'POST', `${BASE}/inbounds`, body)
expectCode(context, label, response, 'A0422')
}
expectCode(context, '非法入库分页大小被拒绝',
await api(context, 'GET', `${BASE}/inbounds?page=1&size=201`), 'A0422')
expectCode(context, '非法库存状态筛选被拒绝',
await api(context, 'GET', `${BASE}/inventories?page=1&size=20&status=UNKNOWN`), 'A0422')
}
async function verifyExtendedInventoryScenarios(context, warehouseId, marker, now) {
const containerNo = `DCX${marker}`.slice(0, 32).toUpperCase()
const cargoName = `${marker}-extended`.slice(0, 120)
const firstBody = { warehouseId, cargoName, containerNo, boxCount: 40, volumeCbm: 20,
inboundTime: now, status: 'COMPLETED', remark: `${marker}-extended-1`, idempotencyKey: `${marker}-extended-1` }
context.apiSuccess(MODULE, '扩展场景首次完成入库', await api(context, 'POST', `${BASE}/inbounds`, firstBody))
context.apiSuccess(MODULE, '同仓同柜同货名追加入库', await api(context, 'POST', `${BASE}/inbounds`, {
...firstBody, cargoName: ` ${cargoName} `, containerNo: containerNo.toLowerCase(), boxCount: 10, volumeCbm: 5,
remark: `${marker}-extended-2`, idempotencyKey: `${marker}-extended-2`,
}))
expectCode(context, '同仓同柜不同货名禁止合并库存', await api(context, 'POST', `${BASE}/inbounds`, {
...firstBody, cargoName: `${cargoName}-other`, boxCount: 1, volumeCbm: 1,
idempotencyKey: `${marker}-cargo-conflict`,
}), 'A0409')
const filteredResponse = await api(context, 'GET', `${BASE}/inventories?page=1&size=1&warehouseId=${warehouseId}&containerNo=${containerNo.toLowerCase()}&cargoName=${encodeURIComponent(cargoName)}&status=IN_STOCK`)
const filtered = context.apiSuccess(MODULE, '组合条件筛选扩展库存', filteredResponse)
const inventory = filtered?.records?.[0]
context.assert(MODULE, '筛选汇总不受分页大小影响且柜号已标准化', filteredResponse,
Number(filtered?.total) === 1 && filtered?.records?.length === 1 && inventory?.containerNo === containerNo
&& Number(filtered?.summary?.inboundBoxes) === 50 && Number(filtered?.summary?.availableBoxes) === 50
&& Number(filtered?.summary?.inboundVolumeCbm) === 25,
`total=${filtered?.total}, records=${filtered?.records?.length}, containerNo=${inventory?.containerNo}, summary=${JSON.stringify(filtered?.summary)}`)
if (!inventory?.id) throw new Error('扩展库存未创建成功')
const inboundListResponse = await api(context, 'GET', `${BASE}/inbounds?page=1&size=1&warehouseId=${warehouseId}&containerNo=${containerNo}&cargoName=${encodeURIComponent(cargoName)}&status=COMPLETED`)
const inboundList = context.apiSuccess(MODULE, '组合条件筛选扩展入库单', inboundListResponse)
context.assert(MODULE, '入库单筛选总数与分页记录正确', inboundListResponse,
Number(inboundList?.total) === 2 && inboundList?.records?.length === 1
&& inboundList.records[0]?.status === 'COMPLETED' && inboundList.records[0]?.businessType === 'INBOUND',
`total=${inboundList?.total}, records=${inboundList?.records?.length}, status=${inboundList?.records?.[0]?.status}`)
const beforeMissingVersion = await api(context, 'GET', `${BASE}/inventories/${inventory.id}`)
const beforeDetail = context.apiSuccess(MODULE, '缺失版本测试前读取库存详情', beforeMissingVersion)
expectCode(context, '完成出库必须携带库存版本', await api(context, 'POST', `${BASE}/outbounds`, {
warehouseId, containerNo, boxCount: 1, outboundTime: now, status: 'COMPLETED',
idempotencyKey: `${marker}-missing-version`,
}), 'A0422')
const afterMissingVersion = context.apiSuccess(MODULE, '缺失版本请求后回查库存',
await api(context, 'GET', `${BASE}/inventories/${inventory.id}`))
context.assert(MODULE, '缺失库存版本的请求不得扣减库存', beforeMissingVersion,
Number(afterMissingVersion?.inventory?.availableBoxes) === Number(beforeDetail?.inventory?.availableBoxes),
`before=${beforeDetail?.inventory?.availableBoxes}, after=${afterMissingVersion?.inventory?.availableBoxes}`)
const currentVersion = Number(afterMissingVersion?.inventory?.version)
const availableBeforeRace = Number(afterMissingVersion?.inventory?.availableBoxes)
const raceBodies = [1, 2].map((sequence) => ({ warehouseId, containerNo, boxCount: 7, outboundTime: now,
status: 'COMPLETED', inventoryVersion: currentVersion, idempotencyKey: `${marker}-race-${sequence}` }))
const raceResponses = await Promise.all(raceBodies.map((body) => context.request('POST', `${BASE}/outbounds`, body, context.token)))
raceResponses.forEach((response, index) => traced(context, `并发出库请求${index + 1}`, response))
const winners = raceResponses.filter(succeeded)
const conflicts = raceResponses.filter((response) => response?.http === 200 && String(response?.body?.code) === 'A0409')
context.assert(MODULE, '同一库存版本并发出库仅一个成功', raceResponses[0], winners.length === 1 && conflicts.length === 1,
`success=${winners.length}, conflict=${conflicts.length}`)
const afterRaceResponse = await api(context, 'GET', `${BASE}/inventories/${inventory.id}`)
const afterRace = context.apiSuccess(MODULE, '并发出库后回查库存', afterRaceResponse)
context.assert(MODULE, '并发出库只扣减一次并只新增一条流水', afterRaceResponse,
Number(afterRace?.inventory?.availableBoxes) === availableBeforeRace - 7
&& afterRace?.transactions?.length === 3 && Number(afterRace?.outboundOrderCount) === 1,
`before=${availableBeforeRace}, after=${afterRace?.inventory?.availableBoxes}, transactions=${afterRace?.transactions?.length}, outboundOrders=${afterRace?.outboundOrderCount}`)
const winnerIndex = raceResponses.findIndex(succeeded)
if (winnerIndex >= 0) {
const firstWinner = raceResponses[winnerIndex]?.body?.data
const replay = await api(context, 'POST', `${BASE}/outbounds`, raceBodies[winnerIndex])
const replayData = context.apiSuccess(MODULE, '并发成功请求使用原幂等键重放', replay)
context.assert(MODULE, '并发成功请求重放不重复扣减', replay,
replayData?.replayed === true && id(replayData?.id) === id(firstWinner?.id),
`replayed=${replayData?.replayed}, firstId=${id(firstWinner?.id)}, replayId=${id(replayData?.id)}`)
}
expectCode(context, '不存在的整柜库存详情返回未找到',
await api(context, 'GET', `${BASE}/inventories/1`), 'A0404')
}
2026-08-13 11:57:33 +08:00
export async function run(context) {
const marker = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18)
2026-08-13 17:24:40 +08:00
const initialInboundsResponse = await api(context, 'GET', `${BASE}/inbounds?page=1&size=20`)
const initialInbounds = context.apiSuccess(MODULE, '无筛选查询整柜入库单列表', initialInboundsResponse)
context.assert(MODULE, '无筛选整柜入库列表分页结构正确', initialInboundsResponse,
Array.isArray(initialInbounds?.records) && Number(initialInbounds?.page) === 1 && Number(initialInbounds?.size) === 20,
`records=${initialInbounds?.records?.length ?? 'missing'}, page=${initialInbounds?.page}, size=${initialInbounds?.size}`)
2026-08-13 11:57:33 +08:00
const warehousesResponse = await api(context, 'GET', '/api/platform/v1/warehouse-admin?page=1&size=200&warehouseType=DOMESTIC&status=ACTIVE')
const warehouses = context.apiSuccess(MODULE, '查询平台启用国内仓', warehousesResponse)
const warehouse = warehouses?.records?.[0]
context.assert(MODULE, '平台国内仓候选有效', warehousesResponse, id(warehouse?.id) && warehouse?.warehouseType === 'DOMESTIC',
`warehouseId=${id(warehouse?.id) || 'missing'}, type=${warehouse?.warehouseType || 'missing'}`)
if (!warehouse) throw new Error('缺少启用的平台国内仓,整柜验收无法继续')
const warehouseId = id(warehouse.id)
const containerNo = `DCA${marker}`.slice(0, 32).toUpperCase()
const now = new Date().toISOString().slice(0, 19)
await verifyValidationBoundaries(context, warehouseId, marker, now)
2026-08-13 11:57:33 +08:00
const invalidWarehouse = await api(context, 'POST', `${BASE}/inbounds`, {
warehouseId: '1', cargoName: marker, containerNo: `${containerNo}X`, boxCount: 1, volumeCbm: 1,
inboundTime: now, status: 'COMPLETED', remark: marker, idempotencyKey: `${marker}-invalid-warehouse`,
})
expectCode(context, '非平台国内仓禁止整柜入库', invalidWarehouse, 'A0404')
const pendingInboundBody = { warehouseId, cargoName: marker, containerNo, boxCount: 100, volumeCbm: 50,
inboundTime: now, status: 'PENDING', remark: `${marker}-pending-in`, idempotencyKey: `${marker}-pending-in` }
const pendingInbound = await api(context, 'POST', `${BASE}/inbounds`, pendingInboundBody)
context.apiSuccess(MODULE, '创建待入库整柜单', pendingInbound)
const beforeCompleted = await api(context, 'GET', `${BASE}/inventories?page=1&size=20&warehouseId=${warehouseId}&containerNo=${containerNo}`)
const beforePage = context.apiSuccess(MODULE, '待入库后查询库存', beforeCompleted)
context.assert(MODULE, 'PENDING 入库不记账', beforeCompleted, Number(beforePage?.total) === 0 && Number(beforePage?.summary?.availableBoxes) === 0,
`total=${beforePage?.total}, available=${beforePage?.summary?.availableBoxes}`)
const inboundBody = { ...pendingInboundBody, status: 'COMPLETED', remark: `${marker}-completed-in`, idempotencyKey: `${marker}-completed-in` }
const inbound = await api(context, 'POST', `${BASE}/inbounds`, inboundBody)
const inboundCreated = context.apiSuccess(MODULE, '完成整柜入库', inbound)
context.assert(MODULE, 'COMPLETED 入库返回新单', inbound, id(inboundCreated?.id) && inboundCreated?.replayed === false,
`orderId=${id(inboundCreated?.id) || 'missing'}`)
const replayInbound = await api(context, 'POST', `${BASE}/inbounds`, inboundBody)
const replayed = context.apiSuccess(MODULE, '同幂等键同请求重放', replayInbound)
context.assert(MODULE, '幂等重放返回首次单据', replayInbound,
id(replayed?.id) === id(inboundCreated?.id) && replayed?.replayed === true, `replayed=${replayed?.replayed}`)
const conflictingInbound = await api(context, 'POST', `${BASE}/inbounds`, { ...inboundBody, boxCount: 101 })
expectCode(context, '同幂等键不同请求冲突', conflictingInbound, 'A0409')
const inventoryResponse = await api(context, 'GET', `${BASE}/inventories?page=1&size=20&warehouseId=${warehouseId}&containerNo=${containerNo}`)
const inventoryPage = context.apiSuccess(MODULE, '查询完成入库库存', inventoryResponse)
let inventory = inventoryPage?.records?.[0]
context.assert(MODULE, '库存及汇总与入库一致', inventoryResponse,
Number(inventory?.inboundBoxes) === 100 && Number(inventory?.availableBoxes) === 100
&& inventory?.inventoryStatus === 'IN_STOCK' && Number(inventoryPage?.summary?.inboundBoxes) === 100,
`in=${inventory?.inboundBoxes}, available=${inventory?.availableBoxes}, status=${inventory?.inventoryStatus}`)
const inventoryId = id(inventory?.id)
if (!inventoryId) throw new Error('完成入库后未形成库存记录')
const pendingOutbound = await api(context, 'POST', `${BASE}/outbounds`, { warehouseId, containerNo, boxCount: 5,
outboundTime: now, status: 'PENDING', remark: `${marker}-pending-out`, idempotencyKey: `${marker}-pending-out` })
context.apiSuccess(MODULE, '创建待出库整柜单', pendingOutbound)
const afterPending = await api(context, 'GET', `${BASE}/inventories/${inventoryId}`)
const afterPendingDetail = context.apiSuccess(MODULE, '待出库后查询详情', afterPending)
context.assert(MODULE, 'PENDING 出库不记账', afterPending,
Number(afterPendingDetail?.inventory?.availableBoxes) === 100 && afterPendingDetail?.transactions?.length === 1,
`available=${afterPendingDetail?.inventory?.availableBoxes}, transactions=${afterPendingDetail?.transactions?.length}`)
const partial = await api(context, 'POST', `${BASE}/outbounds`, { warehouseId, containerNo, boxCount: 30,
outboundTime: now, status: 'COMPLETED', remark: `${marker}-partial`, idempotencyKey: `${marker}-partial`,
inventoryVersion: Number(afterPendingDetail.inventory.version) })
context.apiSuccess(MODULE, '完成部分出库', partial)
let detailResponse = await api(context, 'GET', `${BASE}/inventories/${inventoryId}`)
let detail = context.apiSuccess(MODULE, '查询部分出库详情和流水', detailResponse)
context.assert(MODULE, '部分出库库存和交易流水正确', detailResponse,
detail?.inventory?.inventoryStatus === 'PARTIALLY_OUTBOUND' && Number(detail?.inventory?.availableBoxes) === 70
&& detail?.transactions?.length === 2 && detail?.transactions?.[0]?.businessType === 'OUTBOUND',
`available=${detail?.inventory?.availableBoxes}, status=${detail?.inventory?.inventoryStatus}, tx=${detail?.transactions?.length}`)
const staleVersion = await api(context, 'POST', `${BASE}/outbounds`, { warehouseId, containerNo, boxCount: 1,
outboundTime: now, status: 'COMPLETED', idempotencyKey: `${marker}-stale`,
inventoryVersion: Number(afterPendingDetail.inventory.version) })
expectCode(context, '过期库存版本冲突', staleVersion, 'A0409')
const insufficient = await api(context, 'POST', `${BASE}/outbounds`, { warehouseId, containerNo, boxCount: 71,
outboundTime: now, status: 'COMPLETED', idempotencyKey: `${marker}-insufficient`,
inventoryVersion: Number(detail.inventory.version) })
expectCode(context, '库存不足禁止出库', insufficient, 'A0409')
const full = await api(context, 'POST', `${BASE}/outbounds`, { warehouseId, containerNo, boxCount: 70,
outboundTime: now, status: 'COMPLETED', remark: `${marker}-full`, idempotencyKey: `${marker}-full`,
inventoryVersion: Number(detail.inventory.version) })
context.apiSuccess(MODULE, '完成全部出库', full)
detailResponse = await api(context, 'GET', `${BASE}/inventories/${inventoryId}`)
detail = context.apiSuccess(MODULE, '查询全部出库详情', detailResponse)
context.assert(MODULE, '全部出库库存和三条交易流水正确', detailResponse,
detail?.inventory?.inventoryStatus === 'OUTBOUND' && Number(detail?.inventory?.availableBoxes) === 0
&& Number(detail?.inventory?.outboundBoxes) === 100 && detail?.transactions?.length === 3,
`available=${detail?.inventory?.availableBoxes}, outbound=${detail?.inventory?.outboundBoxes}, tx=${detail?.transactions?.length}`)
const inboundsList = await api(context, 'GET', `${BASE}/inbounds?page=1&size=20&warehouseId=${warehouseId}&containerNo=${containerNo}`)
const inboundRows = context.apiSuccess(MODULE, '筛选整柜入库单列表', inboundsList)
const outboundsList = await api(context, 'GET', `${BASE}/outbounds?page=1&size=20&warehouseId=${warehouseId}&containerNo=${containerNo}`)
const outboundRows = context.apiSuccess(MODULE, '筛选整柜出库单列表', outboundsList)
context.assert(MODULE, '列表覆盖待单和已完成单', outboundsList,
inboundRows?.records?.length === 2 && outboundRows?.records?.length === 3,
`inbounds=${inboundRows?.records?.length}, outbounds=${outboundRows?.records?.length}`)
await verifyExtendedInventoryScenarios(context, warehouseId, marker, now)
2026-08-13 11:57:33 +08:00
await verifyPermissionDenial(context, marker, warehouseId, containerNo)
context.createdResources.domesticContainer = { warehouseId, inventoryId, containerNo, permissions: PERMISSIONS }
}
async function verifyPermissionDenial(context, marker, warehouseId, containerNo) {
const phone = await context.findAvailablePhone(MODULE)
const roleCode = `DC_DENY_${marker}`.slice(0, 64)
const username = `dc_deny_${marker}`.slice(0, 64).toLowerCase()
const password = `Dc${marker.slice(-8)}A1!`
const roleResponse = await context.request('POST', '/api/platform/v1/iam/roles', {
roleCode, roleName: `整柜权限拒绝-${marker}`, description: marker,
})
const role = context.apiSuccess(MODULE, '创建无整柜权限角色', roleResponse)
const roleId = id(role?.id)
if (!roleId) throw new Error('无法创建权限拒绝验收角色')
const userResponse = await context.request('POST', '/api/platform/v1/iam/users', {
username, displayName: `整柜权限拒绝-${marker}`, phone, email: `${username}@example.com`, tenantId: '0',
roleIds: [roleId], password, status: 'ACTIVE',
})
const user = context.apiSuccess(MODULE, '创建无整柜权限用户', userResponse)
const membershipId = id(user?.membershipId)
if (!membershipId) throw new Error('无法创建权限拒绝验收用户')
try {
const loginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password }, '')
2026-08-13 11:57:33 +08:00
const token = context.apiSuccess(MODULE, '无整柜权限用户登录', loginResponse)?.accessToken
if (!token) throw new Error('权限拒绝验收用户登录失败')
const denied = await context.request('GET', `${BASE}/inventories?page=1&size=20&warehouseId=${warehouseId}&containerNo=${containerNo}`, undefined, token)
expectCode(context, '无整柜权限用户被拒绝', denied, 'A0403')
} finally {
await context.request('PUT', `/api/platform/v1/iam/users/${membershipId}/status`, { status: 'DISABLED' })
await context.request('PUT', `/api/platform/v1/iam/roles/${roleId}/status`, { status: 'DISABLED' })
}
}