Test #1

Merged
admin merged 1726 commits from test into main 2026-09-17 11:52:45 +08:00
6 changed files with 102 additions and 26 deletions
Showing only changes of commit b914bb7532 - Show all commits
+2 -2
View File
@@ -62,9 +62,9 @@ export async function run(context) {
默认 Admin 全量入口已注册并按同一 `context.marker` 串行执行货柜主链。冻结时序为:
`WAIT_SPLIT -> 内部模拟柜 -> 智能排货 -> 无集装箱号 push-preload -> exact replay/冲突零漂移 -> 预装 EDITING 补集装箱号 -> 关联 CONFIRMED 订舱 -> RELEASED -> 计划级原子确认 -> 唯一 PDA task -> claim 后 LOADING -> 原子批量拣货/PDA complete -> 五类证据 -> 报关及下游门禁 -> loading complete -> LOADED`。
`WAIT_SPLIT -> SIMULATION/ACTIVE 内部模拟柜 -> 智能排货 -> 无集装箱号 push-preload -> exact replay/冲突零漂移 -> 订舱逐柜登记 OCEAN_BOOKING/ACTIVE 真实柜 -> 按 realContainerBookingId 原地合并(来源 MERGED,模拟柜身份存活)-> RELEASED -> 计划级原子确认 -> 唯一 PDA task -> claim 后 LOADING -> 原子批量拣货/PDA complete -> 五类证据 -> 报关及下游门禁 -> loading complete -> LOADED`。
其中,无号 push 只冻结装柜计划、建立 bridge 并使预装可见,不得创建 PDA task;未补号、订舱未关联或未 `RELEASED`、整份计划未确认时,PDA 查询必须返回业务码 `A0404`。唯一确认入口为 `POST /api/platform/v1/first-leg/loading-plans/{loadingPlanId}/confirm`,请求体只含当前计划 `expectedVersion` 并携带 `Idempotency-Key`;成功必须返回 `confirmationStatus=CONFIRMED`、`readiness.readyForPda=true` 和唯一 `readiness.pdaTaskId`。验收脚本禁止调用旧入库单级 `/preload/confirming`、`/preload/confirm`。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。
其中,无号 push 只冻结装柜计划、建立 bridge 并使预装可见,不得创建 PDA task;真实柜号由合并动作从未消费的订舱真实柜带入,合并前、订舱未 `RELEASED` 或整份计划未确认时,PDA 查询必须返回业务码 `A0404`。合并后正常列表只保留模拟柜原地转成的一个 ACTIVE 操作身份,PDA 禁止使用 MERGED 来源 ID。唯一确认入口为 `POST /api/platform/v1/first-leg/loading-plans/{loadingPlanId}/confirm`,请求体只含当前计划 `expectedVersion` 并携带 `Idempotency-Key`;成功必须返回 `confirmationStatus=CONFIRMED`、`readiness.readyForPda=true` 和唯一 `readiness.pdaTaskId`。验收脚本禁止调用旧入库单级 `/preload/confirming`、`/preload/confirm`。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。
部署后需要定向验证该完整链时,模块顺序固定为:
@@ -13,3 +13,16 @@ test('reprint and Mojia scan share the same frozen box code variable', () => {
assert.match(source, /重复整单打印复用已冻结箱码映射[\s\S]*boxCode=\$\{boxCode\}/)
assert.match(source, /const firstScanResponse = await multipartRequest[\s\S]*boxCode, \.\.\.firstDimensions/)
})
test('measurement submission confirms weight evidence before submit', () => {
assert.match(source, /files\/measurements\/\$\{tenantId\}\/\$\{inboundId\}/)
assert.match(source, /\/weight-results\?\$\{query\(tenantId\)\}/)
assert.match(source, /\/weight-results\/\$\{created\.id\}\/confirm\?\$\{query\(tenantId\)\}/)
assert.match(source, /createConfirmedWeightEvidence[\s\S]*\/measurement\/submit/)
})
test('measurement fixture derives pallet resources from confirmed image count', () => {
assert.match(source, /palletCount: WEIGHING_IMAGE_COUNT/)
assert.doesNotMatch(source, /Math\.ceil\(Number\(totalVolumeCbm\)/)
assert.doesNotMatch(source, /Math\.ceil\(totalVolume \/ DEFAULT_PALLET_CAPACITY_CBM\)/)
})
@@ -3,7 +3,8 @@ import { readFile } from 'node:fs/promises'
const MODULE = 'inbound-measurement'
const SCENARIO = 'mojia-carton-measurement'
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
const DEFAULT_PALLET_CAPACITY_CBM = 1.2
const WEIGHING_IMAGE_COUNT = 1
const FIXTURE_PALLET_CAPACITY_CBM = 1.2
function asId(value) { return value == null ? '' : String(value) }
function asNumber(value) { return Number(value) }
@@ -37,21 +38,21 @@ async function waitForCompletedMeasurement(context, measurementPath, attempts =
return { response, measurement, attempts }
}
async function provisionWarehouseResources(context, { inboundId, warehouseId, totalVolumeCbm }) {
const palletCount = Math.ceil(Number(totalVolumeCbm) / DEFAULT_PALLET_CAPACITY_CBM)
if (!warehouseId || !Number.isFinite(palletCount) || palletCount < 1) return false
async function provisionWarehouseResources(context, { inboundId, warehouseId, palletCount }) {
if (!warehouseId || !Number.isInteger(palletCount) || palletCount < 1) return null
const suffix = `${String(context.marker).replace(/\D/g, '').slice(-8)}${String(inboundId).slice(-6)}`
const palletResponse = await context.request('POST', '/api/platform/v1/pallets?tenantId=0', {
warehouseId, palletType: 'STANDARD', capacityCbm: FIXTURE_PALLET_CAPACITY_CBM,
weightKg: 8, color: '蓝色', remark: `${context.marker}-INBOUND-${inboundId}`,
})
const palletId = asId(context.apiSuccess(MODULE, '创建称重托盘主数据', palletResponse,
`warehouseId=${warehouseId}`, OPTIONS))
if (!palletId) return null
const resources = []
for (let index = 1; index <= palletCount; index += 1) {
const resourceSuffix = `${suffix}${String(index).padStart(2, '0')}`
const palletResponse = await context.request('POST', '/api/platform/v1/pallets?tenantId=0', {
warehouseId, palletType: 'STANDARD', capacityCbm: DEFAULT_PALLET_CAPACITY_CBM,
weightKg: 8, color: '蓝色', remark: `${context.marker}-INBOUND-${inboundId}`,
})
const palletId = asId(context.apiSuccess(MODULE, `创建测量占用托盘 ${index}/${palletCount}`,
palletResponse, `warehouseId=${warehouseId}`, OPTIONS))
let locationId = null
if (context.context?.phase3LoadingRequireSharedOverflow !== true) {
const locationResponse = await context.request('POST', '/api/platform/v1/locations?tenantId=0', {
@@ -63,16 +64,64 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, to
locationId = asId(context.apiSuccess(MODULE, `创建测量占用库位 ${index}/${palletCount}`,
locationResponse, `warehouseId=${warehouseId}`, OPTIONS))
}
if (!palletId || (context.context?.phase3LoadingRequireSharedOverflow !== true && !locationId)) return false
if (context.context?.phase3LoadingRequireSharedOverflow !== true && !locationId) return null
resources.push({ palletId, locationId })
}
context.createdResources.inboundWarehouseResources ??= {}
context.createdResources.inboundWarehouseResources[inboundId] = {
marker: context.marker, tenantId: '0', warehouseId: asId(warehouseId), palletCount,
palletCapacityCbm: DEFAULT_PALLET_CAPACITY_CBM, resources,
palletCapacityCbm: FIXTURE_PALLET_CAPACITY_CBM, resources,
}
return true
return { palletId, palletCount, resources }
}
async function createConfirmedWeightEvidence(context, { inboundId, tenantId, palletId }) {
const image = await readFile(new URL('../../../apps/pda-uniapp/static/c7.png', import.meta.url))
const uploadResponse = await context.requestMultipart('POST',
`/api/platform/v1/files/measurements/${tenantId}/${inboundId}`, {
content: image, type: 'image/png', name: `weight-${inboundId}.png`,
})
const upload = context.apiSuccess(MODULE, '上传称重图片并执行 OCR', uploadResponse,
`inboundId=${inboundId}`, OPTIONS)
const fileId = asId(upload?.fileId)
if (!fileId) return null
const createResponse = await context.request('POST',
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results?${query(tenantId)}`, {
fileId, palletId, ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null,
ocrWeightValue: null, ocrWeightUnit: null, confidence: null,
})
const created = context.apiSuccess(MODULE, '登记称重图片与托盘快照', createResponse,
`fileId=${fileId}, palletId=${palletId}`, OPTIONS)
if (!created?.id) return null
const confirmResponse = await context.request('POST',
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, {
version: Number(created.version), adjustedWeightValue: 100,
adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 自动化人工确认称重`,
})
const confirmed = context.apiSuccess(MODULE, '人工确认称重图片净重', confirmResponse,
`resultId=${created.id}, palletId=${palletId}`, OPTIONS)
const netWeightKg = Number(confirmed?.netWeightKg)
if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) {
return null
}
const workspaceResponse = await context.request('GET',
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement?${query(tenantId)}`)
const workspace = context.apiSuccess(MODULE, '回查已确认称重图片和货物总重量', workspaceResponse,
`resultId=${created.id}`, OPTIONS)
const confirmedRows = (workspace?.weightResults || [])
.filter((result) => result?.recognitionStatus === 'CONFIRMED')
const persisted = confirmedRows.length === WEIGHING_IMAGE_COUNT
&& asId(confirmedRows[0]?.id) === asId(created.id)
&& asId(confirmedRows[0]?.palletId) === palletId
&& closeTo(workspace?.actualGrossWeightKg, netWeightKg, 0.001)
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
`confirmedImages=${confirmedRows.length}, expectedPallets=${WEIGHING_IMAGE_COUNT}, netWeight=${netWeightKg}, actual=${workspace?.actualGrossWeightKg ?? 'missing'}`,
OPTIONS)
return persisted ? { count: confirmedRows.length, netWeightKg } : null
}
function findIsolatedInbound(context) {
@@ -364,17 +413,29 @@ export async function run(context) {
}
const warehouseResourcesReady = await provisionWarehouseResources(context, {
inboundId, warehouseId: asId(summary?.warehouseId), totalVolumeCbm: totalVolume,
inboundId, warehouseId: asId(summary?.warehouseId), palletCount: WEIGHING_IMAGE_COUNT,
})
context.assert(MODULE, '测量提交前已创建足量真实托盘和库位', draftResponse,
warehouseResourcesReady,
`warehouseId=${asId(summary?.warehouseId) || 'missing'}, palletCount=${Math.ceil(totalVolume / DEFAULT_PALLET_CAPACITY_CBM)}`,
context.assert(MODULE, '测量提交前按称重图片数准备真实库位', draftResponse,
Boolean(warehouseResourcesReady),
`warehouseId=${asId(summary?.warehouseId) || 'missing'}, palletCount=${WEIGHING_IMAGE_COUNT}`,
OPTIONS)
if (!warehouseResourcesReady) {
finishScenario(context, start)
return
}
const weightEvidence = await createConfirmedWeightEvidence(context, {
inboundId, tenantId, palletId: warehouseResourcesReady.palletId,
})
context.assert(MODULE, '测量提交前已通过正式接口确认称重图片', draftResponse,
Boolean(weightEvidence),
`confirmedImages=${weightEvidence?.count ?? 0}, expected=${WEIGHING_IMAGE_COUNT}`,
OPTIONS)
if (!weightEvidence) {
finishScenario(context, start)
return
}
const submitResponse = await context.request('POST',
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/submit?${query(tenantId)}`,
{ measurementId: draft.measurementId, version: Number(draft.version) })
@@ -418,14 +479,16 @@ export async function run(context) {
finalMeasurement?.processStatus === 'WAIT_SPLIT' && finalMeasurement?.measureReviewStatus === 'APPROVED'
&& finalMeasurement?.measurementStatus === 'APPROVED'
&& Number(finalMeasurement?.actualPackageCount) === originalPackageCount
&& closeTo(finalMeasurement?.actualGrossWeightKg, weightEvidence.netWeightKg, 0.001)
&& closeTo(finalMeasurement?.actualVolumeCbm, totalVolume)
&& closeTo(finalMeasurement?.volumeWeightKg, volumeWeight),
`processStatus=${finalMeasurement?.processStatus ?? 'missing'}, measurementStatus=${finalMeasurement?.measurementStatus ?? 'missing'}, packages=${finalMeasurement?.actualPackageCount ?? 'missing'}`, OPTIONS)
`processStatus=${finalMeasurement?.processStatus ?? 'missing'}, measurementStatus=${finalMeasurement?.measurementStatus ?? 'missing'}, packages=${finalMeasurement?.actualPackageCount ?? 'missing'}, pallets=${weightEvidence.count}, cargoWeight=${finalMeasurement?.actualGrossWeightKg ?? 'missing'}`, OPTIONS)
context.createdResources.inboundMeasurement = {
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
processStatus: finalMeasurement?.processStatus || null,
palletCount: weightEvidence.count, actualGrossWeightKg: weightEvidence.netWeightKg,
}
finishScenario(context, start)
}
@@ -90,7 +90,7 @@ test('HTTP fixtures cover unassigned CONFIRMED persistence and assigned customer
assert.match(fixture, /serviceTerm: 'CY-CY', freightTerm: 'NINGBO'/)
assert.doesNotMatch(fixture, /serviceTerm: 'CY\/CY'|freightTerm: 'PREPAID'/)
assert.match(bookingModule, /createdResources\.oceanBookingUnassignedCustomer/)
assert.match(bookingModule, /未指定客户订舱保持空客户且 CONFIRMED 不自动生成内部柜/)
assert.match(bookingModule, /未指定客户订舱保持空客户且原子生成一条真实柜/)
assert.match(bookingModule, /按真实订舱号查询未指定客户订舱/)
assert.match(bookingModule, /keyword=\$\{encodeURIComponent\(bookingNo\)\}/)
assert.doesNotMatch(unassignedFlow, /keyword=\$\{encodeURIComponent\(context\.marker\)\}/)
@@ -110,14 +110,15 @@ test('bridge loading confirms preload before creating and claiming the PDA task'
assert.match(value, /计划级确认同 key 异参返回冲突/)
assert.match(value, /已确认计划使用新 key 重复确认返回冲突/)
assert.doesNotMatch(value, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
assert.match(value, /loading-plans\/\$\{loadingPlanId\}\/transport\?warehouseId=/)
assert.doesNotMatch(value.slice(value.indexOf('无真实柜号推送预装成功并冻结柜次明细'), value.indexOf('const linkResponse')),
/\/transport/)
assert.match(value, /missingContainerTaskResponse\.http === 200 && codeOf\(missingContainerTaskResponse\) === 'A0404'/)
assert.doesNotMatch(value, /无真实柜号拒绝推送柜次计划|PDA complete后补确认/)
assert.match(value, /billOfLadingNo:[\s\S]*vesselName:[\s\S]*voyageNo:[\s\S]*estimatedSailingAt:[\s\S]*estimatedArrivalAt:/)
assert.doesNotMatch(value, /P3-SHIPPING|P3-AGENT/)
assert.match(value, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
assert.match(value, /const frozenTransportMatches = frozenTransportValid[\s\S]*transportFields\.every/)
assert.match(value, /CONFIRMED 订舱回显完整且冻结五项运输快照[\s\S]*if \(!bookingId \|\| !requirement\?\.id \|\| !frozenTransportMatches\) return[\s\S]*三期订舱携带已冻结运输快照并放舱/)
assert.match(value, /CONFIRMED 订舱回显完整且冻结五项运输快照[\s\S]*if \(!bookingId \|\| !requirement\?\.containerBookingId \|\| !frozenTransportMatches\) return[\s\S]*三期订舱携带已冻结运输快照并放舱/)
assert.match(value, /\.\.\.frozenTransport, reason: context\.marker/)
assert.doesNotMatch(value, /new Date\(Date\.now\(\)/)
assert.match(value, /bookingReleased\?\.oceanBooking\?\.status === 'RELEASED'/)
@@ -246,7 +247,6 @@ test('container loading arranges a simulation before linking a confirmed real bo
"const bookingCreate = await request",
"const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`",
])
assert.match(value, /channelCode, warehouseId, warehouseName/)
assert.match(value, /publishPlatformLogisticsChannels\(context, warehouseId, MODULE\)/)
const draft = value.slice(value.indexOf('const bookingDraft = {'), value.indexOf('context.assert(MODULE, \'订舱渠道与仓库快照完整\''))
assert.doesNotMatch(draft, /warehouseName\s*:/)
@@ -69,7 +69,7 @@ test('test-environment loading chains push without a number and open PDA only af
const bridgeLoading = readFileSync(
resolve(scriptsDir, 'admin-test-env-modules', 'phase3-loading.mjs'), 'utf8')
assert.match(containerLoading, /expectedVersion: Number\(postPushPlan\.version\), operationReason: context\.marker/)
assert.match(containerLoading, /realContainerBookingId: id\(requirement\.containerBookingId\)/)
const pushIndex = bridgeLoading.indexOf('无真实柜号推送预装成功并冻结柜次明细')
const absentIndex = bridgeLoading.indexOf('无号 push 成功后 PDA 查询明确 A0404')