From 895d0f763988cbf114593e7b862d9f283577ce9c Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 18 Sep 2026 14:14:10 +0800 Subject: [PATCH] fix(lastleg): accept cabinet pallet identities --- ...ainerInboundPlanConversionCoordinator.java | 13 +++++---- ...rInboundPlanConversionCoordinatorTest.java | 8 +++--- .../fulfillment-list-field-contract.mjs | 2 +- ...t-prototype-field-matrix-contract.test.mjs | 2 +- .../phase3-loading-finalize.mjs | 3 ++- .../admin-test-env-modules/phase3-loading.mjs | 3 ++- ...-phase4-producer-fixture-contract.test.mjs | 3 +++ .../phase4-overseas-pda-contract.test.mjs | 3 +++ .../phase4-overseas-pda.mjs | 27 +++++++++++++++++++ 9 files changed, 49 insertions(+), 15 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java index c95a433ac..45f2f0447 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java @@ -255,7 +255,7 @@ public class ContainerInboundPlanConversionCoordinator { String fbaCode = trimToNull(order.fbaWarehouseCode()); if (fbaCode != null && !"ONE_CLICK_DROPSHIP".equals(order.orderType())) { for (String palletCode : sourcePalletNos) { - PalletCodeParts parts = palletCodeParts(preview.containerNo(), palletCode); + PalletCodeParts parts = palletCodeParts(palletCode); String previousPrefix = prefixByFba.putIfAbsent(fbaCode, parts.prefix()); if (previousPrefix != null && !previousPrefix.equals(parts.prefix())) { throw new ResponseStatusException(HttpStatus.CONFLICT, "同一 FBA 仓的头程托盘码内部编码不一致"); @@ -487,17 +487,16 @@ public class ContainerInboundPlanConversionCoordinator { int localSequenceNo, int globalSequenceNo, String businessKey, String fbaWarehouseCode, String palletCode) { } - private static PalletCodeParts palletCodeParts(String containerNo, String palletCode) { - String stableContainerNo = trimToNull(containerNo); + private static PalletCodeParts palletCodeParts(String palletCode) { String stablePalletCode = trimToNull(palletCode); - if (stableContainerNo == null || stablePalletCode == null) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "集装箱号或头程托盘码缺失"); + if (stablePalletCode == null) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "头程托盘码缺失"); } - Matcher matcher = Pattern.compile("^(" + Pattern.quote(stableContainerNo) + "-.+)-(\\d{3})$") + Matcher matcher = Pattern.compile("^(D\\d{4,}-.+)-(\\d{3})$") .matcher(stablePalletCode); if (!matcher.matches()) { throw new ResponseStatusException(HttpStatus.CONFLICT, - "头程托盘码不符合‘集装箱号-FBA内部编码-下标’规则"); + "头程托盘码不符合‘柜次号-FBA内部编码-下标’规则"); } return new PalletCodeParts(matcher.group(1), Integer.parseInt(matcher.group(2))); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java index 1838590df..fc143f7b2 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java @@ -345,7 +345,7 @@ class ContainerInboundPlanConversionCoordinatorTest { when(plans.previewContainer(BOOKING_ID)).thenReturn(preview); when(numbers.next(eq("INBOUND_PLAN"), any(String.class), any(Long.class))).thenReturn("IP-ACY2"); when(plans.sourcePalletNos("100", 11L, "FIRST_LEG")).thenReturn(List.of( - "MSCU6639870-101-001", "MSCU6639870-101-002")); + "D0004-101-001", "D0004-101-002")); ContainerPlanConversionCommand command = ReflectionTestUtils.invokeMethod(coordinator, "prepareCommand", 1L, 2L, BOOKING_ID, "FIRST-KEY", request, @@ -359,7 +359,7 @@ class ContainerInboundPlanConversionCoordinatorTest { throw new AssertionError(exception); } - assertEquals(List.of("MSCU6639870-101-001", "MSCU6639870-101-002", "MSCU6639870-101-003"), + assertEquals(List.of("D0004-101-001", "D0004-101-002", "D0004-101-003"), preparation.items().stream().map(ContainerInboundPlanConversionCoordinator.PreparedItem::palletCode).toList()); } @@ -376,7 +376,7 @@ class ContainerInboundPlanConversionCoordinatorTest { 1, 1, 3, 2, 1, 3, 3, 3, List.of(order)))); when(plans.previewContainer(BOOKING_ID)).thenReturn(preview); when(plans.sourcePalletNos("100", 11L, "FIRST_LEG")).thenReturn(List.of( - "MSCU6639870-101-001", "MSCU6639870-101-002")); + "D10000-101-001", "D10000-101-002")); when(plans.existingPlanIdentity("100", 11L)) .thenReturn(new InboundPlanService.PlanIdentity(21L, "IP-MANUAL", 3)); @@ -434,7 +434,7 @@ class ContainerInboundPlanConversionCoordinatorTest { private InboundPlanResponse plan(String planNo, List pallets) { return new InboundPlanResponse(21L, planNo, "100", 11L, "FL-WH-1", "验收客户", "ONE_CLICK_DROPSHIP", "MSCU6639870", "US-LAX-01", - "Los Angeles", "PENDING_ASSIGNMENT", 1, 1, BigDecimal.ONE, 10, 0, + "Los Angeles", "PENDING_ASSIGNMENT", 1, 1, BigDecimal.TEN, new BigDecimal("8"), BigDecimal.ONE, 10, 0, List.of(), pallets, List.of(), 1, LocalDate.of(2026, 9, 10), "WOOD", false, false, false, "MANUAL", "自动生成", null, null); } diff --git a/scripts/admin-test-env-modules/fulfillment-list-field-contract.mjs b/scripts/admin-test-env-modules/fulfillment-list-field-contract.mjs index 3ea3488da..a02650a34 100644 --- a/scripts/admin-test-env-modules/fulfillment-list-field-contract.mjs +++ b/scripts/admin-test-env-modules/fulfillment-list-field-contract.mjs @@ -30,7 +30,7 @@ export function containerFieldGaps(record, { requireContainerNo = false } = {}) cabinetNo: isNonBlank, warehouseId: isPositiveId, warehouseName: isNonBlank, - channelCode: isNonBlank, + channelGroupCode: isNonBlank, containerType: isNonBlank, capacityCbm: (value) => Number(value) > 0, maxWeightKg: (value) => Number(value) > 0, diff --git a/scripts/admin-test-env-modules/fulfillment-prototype-field-matrix-contract.test.mjs b/scripts/admin-test-env-modules/fulfillment-prototype-field-matrix-contract.test.mjs index b5d98d353..059d2c520 100644 --- a/scripts/admin-test-env-modules/fulfillment-prototype-field-matrix-contract.test.mjs +++ b/scripts/admin-test-env-modules/fulfillment-prototype-field-matrix-contract.test.mjs @@ -82,7 +82,7 @@ test('正式 DTO/OpenAPI 暴露矩阵使用的稳定字段', () => { test('字段校验器接受正式完整投影并精确暴露缺字段', () => { const samples = [ - [containerFieldGaps, { id: ids.container, cabinetNo: 'CAB-1', containerNo: 'MSCU1234567', warehouseId: ids.id, warehouseName: '宁波仓', channelCode: 'EXX', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500, planStatus: 'EDITING' }, { requireContainerNo: true }, 'warehouseName'], + [containerFieldGaps, { id: ids.container, cabinetNo: 'CAB-1', containerNo: 'MSCU1234567', warehouseId: ids.id, warehouseName: '宁波仓', channelGroupCode: 'EXX', containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500, planStatus: 'EDITING' }, { requireContainerNo: true }, 'warehouseName'], [preloadFieldGaps, { planId: ids.plan, containerBookingId: ids.container, preloadNo: 'PL-1', cabinetNo: 'CAB-1', customers: [{ accountId: ids.id, customerName: '测试客户' }], warehouseName: '宁波仓', containerType: '40HQ', status: 'EDITING', skuCount: 1, totalPackageCount: 1, totalVolumeCbm: 1, totalGrossWeightKg: 10, createdAt: at }, undefined, 'customers'], [loadingListFieldGaps, { planId: ids.plan, containerBookingId: ids.container, loadingListNo: 'LL-1', containerNo: 'MSCU1234567', cabinetNo: 'CAB-1', warehouseName: '宁波仓', status: 'COMPLETED', totalPackageCount: 1, totalGrossWeightKg: 10, totalNetWeightKg: 9, totalVolumeCbm: 1, totalQuantity: 2, createdAt: at }, undefined, 'containerNo'], [oceanBookingFieldGaps, { id: ids.id, bookingNo: 'BK-1', status: 'RELEASED', vesselName: 'VESSEL', voyageNo: 'V1', estimatedSailingAt: at, estimatedArrivalAt: at, customerId: ids.id, customerName: '测试客户', shippingCompanyId: null, shippingCompanyName: null, bookingAgentId: ids.id, bookingAgentName: '代理' }, undefined, 'voyageNo'], diff --git a/scripts/admin-test-env-modules/phase3-loading-finalize.mjs b/scripts/admin-test-env-modules/phase3-loading-finalize.mjs index 750fbf15f..f00f230bd 100644 --- a/scripts/admin-test-env-modules/phase3-loading-finalize.mjs +++ b/scripts/admin-test-env-modules/phase3-loading-finalize.mjs @@ -110,7 +110,7 @@ async function verifyPalletLabels(context, fixture, loadingPlanId) { const index = Number(label?.palletIndex) return id(label?.containerNo) === id(fixture.containerNo) && id(label?.destinationWarehouseInternalCode) - && id(label?.palletCode) === `${fixture.containerNo}-${label.destinationWarehouseInternalCode}-${String(index).padStart(3, '0')}` + && id(label?.palletCode) === `${fixture.cabinetNo}-${label.destinationWarehouseInternalCode}-${String(index).padStart(3, '0')}` }) const indexesValid = (manifest?.groups || []).every((group) => (group?.labels || []).every((label, index) => Number(label?.palletIndex) === index + 1)) @@ -580,6 +580,7 @@ export async function run(context) { destinationWarehouseId: id(fixture.destinationWarehouseId), destinationWarehouseCode: fixture.destinationWarehouseCode || null, palletLabelManifest, + cabinetNo: fixture.cabinetNo || null, containerNo: fixture.containerNo || null, sourceStatus: 'LOADED', verification: 'PRODUCED_IN_COMPLETE_TRANSACTION_PENDING_BRIDGE_CONSUMER_HTTP', diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index c25df145c..ee0b43833 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -1715,7 +1715,8 @@ export async function run(context) { missingTypeVoidedEvidenceId: id(missingTypeTarget?.id), replacementEvidenceFileId: replacementFileId, immutableEvidenceId: id(immutablePhoto?.id), immutableEvidenceVersion: Number(immutablePhoto?.version), postLoadedFileId, - oceanBookingId: bookingId, loadingPlanId, containerBookingId, containerNo, warehouseId, + oceanBookingId: bookingId, loadingPlanId, containerBookingId, + cabinetNo: simulation.cabinetNo, containerNo, warehouseId, canonicalPdaTaskId: canonicalTaskId, pickingTaskId: canonicalTaskId, inboundVersion: loadingVersion, atomicBatchVerified: awaitingReview && atomicBatchReplayVerified, diff --git a/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs b/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs index be5a6a9c1..ae312ffab 100644 --- a/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs @@ -81,10 +81,13 @@ test('measurement and loading pass destination identity into the LOADED producer const finalize = read('phase3-loading-finalize.mjs') assert.match(measurement, /destinationType: context\.createdResources\?\.orders\?\.destinationType/) assert.match(loading, /destinationType: initial\?\.summary\?\.destinationType \|\| source\?\.destinationType/) + assert.match(loading, /cabinetNo: simulation\.cabinetNo, containerNo/) assert.match(loading, /fulfillmentType: source\?\.fulfillmentType \|\| 'STANDARD'/) assert.match(loading, /Array\.isArray\(incrementalFixture\?\.skuCodes\)/) assert.match(loading, /Array\.isArray\(incrementalFixture\?\.skuLines\)/) assert.match(finalize, /destinationType: fixture\.destinationType \|\| null/) + assert.match(finalize, /cabinetNo: fixture\.cabinetNo \|\| null/) + assert.match(finalize, /`\$\{fixture\.cabinetNo\}-\$\{label\.destinationWarehouseInternalCode\}-/) assert.match(finalize, /fulfillmentType: fixture\.fulfillmentType \|\| 'STANDARD'/) assert.match(finalize, /skuCodes: Array\.isArray\(fixture\.skuCodes\) \? fixture\.skuCodes : \[\]/) assert.match(finalize, /skuLines: Array\.isArray\(fixture\.skuLines\) \? fixture\.skuLines : \[\]/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs index 79db67ffe..3b581ae11 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs @@ -103,6 +103,9 @@ test('PDA module covers the container-number receiving contract and excludes out for (const forbidden of ['/outbound', '/dispatch', '/delivery', '/internal/']) assert.equal(source.includes(forbidden), false, forbidden) assert.match(source, /同键同载荷重放/) assert.match(source, /inbound-plans\/container-preview/) + assert.match(source, /orders\/container-arrival-times\/query/) + assert.match(source, /container-summaries\/\$\{encodeURIComponent\(bookingId\)\}\/expected-arrival/) + assert.match(source, /estimatedWarehouseArrivalAt, expectedVersion: Number\(arrivalTime\.version\)/) assert.match(source, /snapshotVersion: Number\(preview\.snapshotVersion\)/) assert.match(source, /snapshotHash: String\(preview\.snapshotHash\)/) assert.match(source, /rememberGeneratedPlans\(context, plans\)/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs index 7cf858e55..10e9012f7 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs @@ -336,6 +336,33 @@ async function convertReceivingContainers(context, producers) { `sourceContainerBookingId=${id(summary?.sourceContainerBookingId) || 'missing'}, allPlansReady=${summary?.allPlansReady === true}, orders=${totalOrders}, inboundCodes=${inboundCodes}, plans=${existingPlans}, pending=${pendingPlans}; ${trace(summaryResponse)}`, OPTIONS) if (!convertible) return false const bookingId = id(summary.sourceContainerBookingId) + const arrivalQueryResponse = await context.requestOverseas('POST', + '/api/platform/v1/last-leg/orders/container-arrival-times/query', + { sourceContainerBookingIds: [bookingId] }, {}, context.overseasToken) + const arrivalTimes = success(arrivalQueryResponse) && Array.isArray(arrivalQueryResponse.body?.data) + ? arrivalQueryResponse.body.data : [] + let arrivalTime = arrivalTimes.find((item) => id(item?.sourceContainerBookingId) === bookingId) + context.addStep(MODULE, `读取整柜预计到仓时间 ${containerNo}`, arrivalQueryResponse, + Boolean(arrivalTime) && Number.isInteger(Number(arrivalTime?.version)), + `sourceContainerBookingId=${bookingId}, estimatedWarehouseArrivalAt=${arrivalTime?.estimatedWarehouseArrivalAt || 'missing'}, version=${arrivalTime?.version ?? 'missing'}; ${trace(arrivalQueryResponse)}`, + OPTIONS) + if (!arrivalTime) return false + if (!nonBlank(arrivalTime.estimatedWarehouseArrivalAt)) { + const estimatedWarehouseArrivalAt = new Date(Date.now() + 2 * 86400000).toISOString().slice(0, 19) + const arrivalUpdateResponse = await context.requestOverseas('PUT', + `/api/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(bookingId)}/expected-arrival`, + { estimatedWarehouseArrivalAt, expectedVersion: Number(arrivalTime.version), + remark: `${context.marker}-overseas-inbound-plan-prerequisite` }, + { 'Idempotency-Key': `${context.marker}-expected-arrival-${containerNo}`.slice(0, 128) }, + context.overseasToken) + arrivalTime = success(arrivalUpdateResponse) ? arrivalUpdateResponse.body?.data : null + context.addStep(MODULE, `维护整柜预计到仓时间 ${containerNo}`, arrivalUpdateResponse, + id(arrivalTime?.sourceContainerBookingId) === bookingId + && nonBlank(arrivalTime?.estimatedWarehouseArrivalAt), + `sourceContainerBookingId=${bookingId}, estimatedWarehouseArrivalAt=${arrivalTime?.estimatedWarehouseArrivalAt || 'missing'}, version=${arrivalTime?.version ?? 'missing'}; ${trace(arrivalUpdateResponse)}`, + OPTIONS) + if (!arrivalTime || !nonBlank(arrivalTime.estimatedWarehouseArrivalAt)) return false + } const previewPath = `/api/platform/v1/last-leg/inbound-plans/container-preview?sourceContainerBookingId=${encodeURIComponent(bookingId)}` const previewResponse = await context.requestOverseas('GET', previewPath, undefined, {}, context.overseasToken)