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') } export async function run(context) { const marker = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18) 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}`) 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() if (context.options?.fixedCustomerId) await verifyPermissionDenial(context, marker, warehouseId, containerNo) const now = new Date().toISOString().slice(0, 19) await verifyValidationBoundaries(context, warehouseId, marker, now) 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) if (!context.options?.fixedCustomerId) await verifyPermissionDenial(context, marker, warehouseId, containerNo) context.createdResources.domesticContainer = { warehouseId, inventoryId, containerNo, permissions: PERMISSIONS } } export async function verifyPermissionDenial(context, marker, warehouseId, containerNo) { if (context.options?.fixedCustomerId) { const actor = context.options.fixedDomesticDenyActor if (!actor?.username || !actor.password || !/^[1-9]\d*$/.test(id(actor.membershipId))) { throw Error('缺少固定整柜权限拒绝账号,禁止自动创建用户或角色') } let user for (let page = 1; ; page++) { const response = await context.request('GET', `/api/platform/v1/iam/users?page=${page}&size=100&keyword=${encodeURIComponent(actor.username)}`) const result = context.apiSuccess(MODULE, '查询固定权限拒绝账号', response) user = result?.records?.find(item => id(item.membershipId) === id(actor.membershipId) && item.username === actor.username) if (user || !result?.records?.length || page * Number(result.size || 100) >= Number(result.total || 0)) break } if (user?.status !== 'ACTIVE' || id(user.tenantId) !== '0') throw Error('固定权限拒绝账号必须为启用的平台账号') const login = await context.request('POST', '/api/auth/login', { loginId: actor.username, password: actor.password }, '') const session = context.apiSuccess(MODULE, '登录固定权限拒绝账号', login) if (!session?.accessToken || !session.refreshToken) throw Error('固定权限拒绝账号未返回完整会话') try { const denied = await context.request('GET', `${BASE}/inventories?page=1&size=20&warehouseId=${warehouseId}&containerNo=${containerNo}`, undefined, session.accessToken) expectCode(context, '固定无整柜权限用户被拒绝', denied, 'A0403') if (denied.http !== 200 || String(denied.body?.code) !== 'A0403') throw Error('固定探针账号实际权限不符,停止整柜造数') } finally { const logout = await context.request('POST', '/api/auth/logout', { refreshToken: session.refreshToken }, session.accessToken) context.apiSuccess(MODULE, '退出固定权限拒绝账号会话,不停用账号或角色', logout) if (!succeeded(logout)) throw Error('固定权限拒绝账号退出失败') } return } 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 }, '') 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' }) } }