From b914bb75322794fee34bde198fd8d5d86b38b508 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 17 Aug 2026 13:47:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/admin-test-env-modules/README.md | 4 +- ...und-measurement-box-code-contract.test.mjs | 13 +++ .../inbound-measurement.mjs | 101 ++++++++++++++---- .../ocean-booking-prototype-contract.test.mjs | 2 +- .../phase3-producer-chain-contract.test.mjs | 6 +- .../full-regression-entry-contract.test.mjs | 2 +- 6 files changed, 102 insertions(+), 26 deletions(-) diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index 838a0849d..d80f18391 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -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`,不得回退历史资源。 部署后需要定向验证该完整链时,模块顺序固定为: diff --git a/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs b/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs index 84f59e8ef..4704d790d 100644 --- a/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs @@ -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\)/) +}) diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index df6dd3f50..0c9877ee9 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -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) } diff --git a/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs b/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs index 2d10f7bc8..2a7f72ff9 100644 --- a/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs +++ b/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs @@ -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\)\}/) diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index 3b46bd76a..e64d77e98 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -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*:/) diff --git a/scripts/full-regression-entry-contract.test.mjs b/scripts/full-regression-entry-contract.test.mjs index cee8c4dcd..048c778c3 100644 --- a/scripts/full-regression-entry-contract.test.mjs +++ b/scripts/full-regression-entry-contract.test.mjs @@ -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')