From 808fabaf419d7c836ae93a75054caa0cad024012 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 13 Aug 2026 19:19:29 +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 --- .../firstleg/service/ContainerService.java | 3 + .../firstleg/service/LoadingPickService.java | 15 ++++- .../openapi/phase3-pda-convergence-v1.yaml | 2 +- .../PdaConvergenceOpenApiContractTest.java | 1 + .../service/ContainerServiceTest.java | 21 ++++++ .../service/LoadingPickServiceTest.java | 64 +++++++++++++++++++ scripts/admin-test-env-acceptance.mjs | 9 ++- .../admin-test-env-modules/ocean-bookings.mjs | 25 +++++--- .../phase3-container-loading.mjs | 4 +- .../admin-test-env-modules/phase3-loading.mjs | 64 ++++++++++++++----- .../phase3-producer-chain-contract.test.mjs | 5 +- 11 files changed, 176 insertions(+), 37 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java index d0b61ee26..e9954f131 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java @@ -114,6 +114,9 @@ public class ContainerService { if (containerNo == null) return; LoadingPlanEntity plan = smartLoadingRepository.planByBooking(booking.getId()); if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return; + // The PDA task and its item versions are stable work identities. A later transport-number + // correction only changes the booking display value and must not resynchronize the task. + if (loadingPickRepository.taskByPlan(plan.getId()) != null) return; loadingPickService.begin(plan.getId(), containerNo, operatorId, operatorName, smartLoadingRepository.items(plan.getId())); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java index 90103a9f3..c2ab1987d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java @@ -423,8 +423,21 @@ public class LoadingPickService { if (item.getPickedPackageCount() > planItem.getPackageCount()) { throw conflict("已拣货箱数不能低于计划箱数"); } + int pickedPackageCount = count(item.getPickedPackageCount()); + String itemStatus = pickedPackageCount == 0 ? "PENDING" + : pickedPackageCount == planItem.getPackageCount() ? "PICKED" : "PICKING"; + if (Objects.equals(item.getPickTaskId(), taskId) + && Objects.equals(item.getInboundId(), planItem.getInboundId()) + && Objects.equals(item.getInboundDetailId(), planItem.getInboundDetailId()) + && Objects.equals(item.getPlannedPackageCount(), planItem.getPackageCount()) + && Objects.equals(item.getItemStatus(), itemStatus)) { + return; + } + item.setPickTaskId(taskId); + item.setInboundId(planItem.getInboundId()); + item.setInboundDetailId(planItem.getInboundDetailId()); item.setPlannedPackageCount(planItem.getPackageCount()); - item.setItemStatus(item.getPickedPackageCount().equals(planItem.getPackageCount()) ? "PICKED" : "PICKING"); + item.setItemStatus(itemStatus); repository.update(item); } diff --git a/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml b/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml index bcfa0bccc..aebdd7383 100644 --- a/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml +++ b/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml @@ -128,7 +128,7 @@ components: loadingPlanId: {type: integer, format: int64} containerBookingId: {type: integer, format: int64} cabinetNo: {type: string} - currentContainerNo: {type: string, nullable: true, description: Mutable transport display field; never task identity} + currentContainerNo: {type: string, nullable: true, description: Mutable transport display field; changing it never resynchronizes the task or item versions and it is never task identity} taskStatus: {type: string, enum: [LOADING, COMPLETED]} workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, COMPLETED]} version: {type: integer, minimum: 0} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PdaConvergenceOpenApiContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PdaConvergenceOpenApiContractTest.java index f642ce24a..58af100bc 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PdaConvergenceOpenApiContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PdaConvergenceOpenApiContractTest.java @@ -28,6 +28,7 @@ class PdaConvergenceOpenApiContractTest { .contains("BatchRequest") .contains("inventorySnapshotToken") .contains("InventorySnapshot") + .contains("changing it never resynchronizes the task or item versions") .contains("expectedTaskVersion") .contains("CompleteLoadingRequest"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java index 8a4b579dd..19066d250 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java @@ -148,6 +148,27 @@ class ContainerServiceTest { org.mockito.ArgumentMatchers.eq("admin"), any()); } + @Test + @DisplayName("PDA任务创建后替换真实柜号不重同步任务明细") + void doesNotResynchronizeExistingPdaTaskWhenContainerNumberChanges() { + ContainerBookingEntity current = booking("EDITING", 2); + LoadingPlanEntity plan = new LoadingPlanEntity(); + plan.setId(31L); + plan.setPlanStatus("EDITING"); + plan.setPreloadPushedAt(LocalDateTime.of(2026, 8, 11, 20, 0)); + when(repository.find(11L)).thenReturn(current); + when(repository.updateTransport(11L, "MSCU7654321", "V002", 2, 9L, "admin")) + .thenReturn(1); + when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan); + when(loadingPickRepository.taskByPlan(31L)).thenReturn(new LoadingPickTaskEntity()); + + service.updateTransport(11L, 9L, "admin", + new TransportUpdateRequest("MSCU7654321", "V002", 2)); + + verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), any(), any()); + verify(smartLoadingRepository, never()).items(31L); + } + @Test @DisplayName("PDA任务创建后禁止清空真实集装箱号") void rejectsClearingContainerNumberAfterPdaTaskStarts() { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java index 083e30121..73251d400 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java @@ -16,6 +16,7 @@ import com.baiyue.firstleg.entity.LoadingPickHistoryEntity; import com.baiyue.firstleg.entity.LoadingPickItemEntity; import com.baiyue.firstleg.entity.LoadingPickOperationEntity; import com.baiyue.firstleg.entity.LoadingPickTaskEntity; +import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import com.baiyue.firstleg.repository.LoadingPickRepository; import com.baiyue.firstleg.repository.SmartLoadingRepository; import jakarta.annotation.Resource; @@ -352,6 +353,69 @@ class LoadingPickServiceTest { verify(repository, never()).touchTask(any(Long.class), any(Integer.class)); } + @Test + void staleTaskVersionDoesNotDriftItemAndFollowingValidBatchSucceeds() { + LoadingPickTaskEntity task = task(); + LoadingPickItemEntity item = item(); + when(repository.taskForUpdate(20L)).thenReturn(task); + when(repository.task(20L)).thenReturn(task); + when(repository.assignment(20L)).thenReturn(assignment()); + when(repository.items(20L)).thenReturn(List.of(item)); + when(repository.items(20L, 101L)).thenReturn(List.of(item)); + when(repository.pick(30L, 2, "MANUAL", "[]", 0, 101L)).thenReturn(1); + when(repository.allTaskItemsResolved(20L)).thenReturn(true); + when(repository.touchTask(20L, 0)).thenReturn(1); + when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn( + ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource( + 71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2)))); + String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken(); + BatchItemRequest line = new BatchItemRequest( + 30L, "PICK", 2, null, "MANUAL", List.of(), 0, null); + + assertThatThrownBy(() -> service.batch(20L, "batch-stale", new BatchRequest( + 101L, 10L, 99L, "CSLU1234567", 1, snapshotToken, List.of(line)), 9L, "operator")) + .isInstanceOf(BizException.class) + .hasMessageContaining("任务版本已变化"); + + assertThat(item.getVersion()).isZero(); + assertThat(item.getPickedPackageCount()).isZero(); + service.batch(20L, "batch-valid", new BatchRequest( + 101L, 10L, 99L, "CSLU1234567", 0, snapshotToken, List.of(line)), 9L, "operator"); + + verify(repository, times(1)).pick(30L, 2, "MANUAL", "[]", 0, 101L); + verify(repository, times(1)).touchTask(20L, 0); + } + + @Test + void repeatedTaskBeginDoesNotChangePendingItemBeforeRejectedStaleBatch() { + LoadingPickTaskEntity task = task(); + LoadingPickItemEntity pending = item(); + var planItem = new LoadingPlanItemEntity(); + planItem.setId(40L); + planItem.setInboundId(50L); + planItem.setInboundDetailId(60L); + planItem.setPackageCount(2); + when(repository.taskByPlan(10L)).thenReturn(task); + when(repository.itemByPlanItem(40L)).thenReturn(pending); + when(repository.taskForUpdate(20L)).thenReturn(task); + + service.begin(10L, "CSLU1234567", 9L, "operator", List.of(planItem)); + + BatchRequest staleRequest = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 1, + "stale-token", List.of( + new BatchItemRequest(30L, "PICK", 2, null, "AUTO", null, 0, null))); + assertThatThrownBy(() -> service.batch(20L, "batch-stale-after-resync", staleRequest, + 9L, "operator")).isInstanceOf(BizException.class); + + assertThat(pending.getVersion()).isZero(); + assertThat(pending.getPickedPackageCount()).isZero(); + assertThat(pending.getItemStatus()).isEqualTo("PENDING"); + verify(repository, never()).update(any(LoadingPickItemEntity.class)); + verify(repository, never()).pick(any(Long.class), any(Integer.class), any(), any(), + any(Integer.class), any(Long.class)); + verify(repository, never()).touchTask(any(Long.class), any(Integer.class)); + } + private void preparePick(LoadingPickTaskEntity task, LoadingPickItemEntity item, String operationId) { when(repository.history(operationId)).thenReturn(null); when(repository.task(20L)).thenReturn(task); diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index acd2d0ce3..a338f7e97 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -521,10 +521,8 @@ export class AcceptanceRun { async preparePhase3ContainerFixture() { const originalMarker = this.marker - // Keep the shared orchestration marker stable. Downstream loading, - // overview and finance modules select fixtures by context.marker; using a - // private -CAB suffix here made the producer invisible to those modules. - const fixtureMarker = originalMarker + // Isolate order idempotency, then publish the fixture under the run marker. + const fixtureMarker = `${originalMarker}-CAB` const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement'] const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound'] const createdSnapshot = new Map(createdKeys.map((key) => [key, { @@ -546,7 +544,8 @@ export class AcceptanceRun { this.createdResources.orchestrationFixtures = { ...(this.createdResources.orchestrationFixtures || {}), phase3Container: fixture ? { - marker: fixtureMarker, + marker: originalMarker, + sourceMarker: fixtureMarker, tenantId: fixture.tenantId, orderId: fixture.orderId || this.createdResources.orders?.orderId, inboundId: fixture.inboundId, diff --git a/scripts/admin-test-env-modules/ocean-bookings.mjs b/scripts/admin-test-env-modules/ocean-bookings.mjs index e31e2adab..5165568c0 100644 --- a/scripts/admin-test-env-modules/ocean-bookings.mjs +++ b/scripts/admin-test-env-modules/ocean-bookings.mjs @@ -70,15 +70,22 @@ export async function run(context) { context.assert(MODULE, '订舱存在匹配本轮柜型的箱型需求', changed, Boolean(requirement?.id), `requirementId=${id(requirement?.id) || 'missing'}`, OPTIONS) if (!requirement?.id || id(containerFlow?.tenantId) !== id(changeConfirmed?.oceanBooking?.customerId)) return - const allocation = await request(context, 'POST', `${path}/allocations`, { - requirementId: id(requirement.id), containerBookingId: id(containerFlow.containerBookingId), expectedVersion: version, - }, { 'Idempotency-Key': `${context.marker}-ocean-container-allocation`.slice(0, 128) }) - const allocated = context.apiSuccess(MODULE, '放舱前分配本轮柜次', allocation, '', OPTIONS) - version = Number(allocated?.oceanBooking?.version) - context.assert(MODULE, '订舱柜次分配持久化且版本递增', allocation, - allocated?.containerAllocations?.some?.((item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId)) - && Number.isInteger(version), - `containerBookingId=${id(containerFlow.containerBookingId)}, version=${version}`, OPTIONS) + const existingAllocation = changeConfirmed?.containerAllocations?.find?.( + (item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId)) + if (existingAllocation) { + context.assert(MODULE, '确认订舱已自动关联本轮柜次', changed, true, + `containerBookingId=${id(containerFlow.containerBookingId)}`, OPTIONS) + } else { + const allocation = await request(context, 'POST', `${path}/allocations`, { + requirementId: id(requirement.id), containerBookingId: id(containerFlow.containerBookingId), expectedVersion: version, + }, { 'Idempotency-Key': `${context.marker}-ocean-container-allocation`.slice(0, 128) }) + const allocated = context.apiSuccess(MODULE, '放舱前分配本轮柜次', allocation, '', OPTIONS) + version = Number(allocated?.oceanBooking?.version) + context.assert(MODULE, '订舱柜次分配持久化且版本递增', allocation, + allocated?.containerAllocations?.some?.((item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId)) + && Number.isInteger(version), + `containerBookingId=${id(containerFlow.containerBookingId)}, version=${version}`, OPTIONS) + } } const released = await request(context, 'POST', `${path}/release`, { expectedVersion: version, billOfLadingNo: `BL-${suffix}`, vesselName: `放舱船-${suffix}`, diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index a3466b2f6..f0649f719 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -145,7 +145,7 @@ export async function run(context) { expectedVersion: Number(foreignPlan?.version), operationId: `${context.marker}-other-channel-assign`.slice(0, 128), }) context.addStep(MODULE, '异渠道入库单拒绝装入当前柜次', foreignAssignResponse, - foreignAssignResponse.http === 200 && ['A0400', '400'].includes(codeOf(foreignAssignResponse)), + foreignAssignResponse.http === 200 && ['A0422', '422'].includes(codeOf(foreignAssignResponse)), `HTTP=${foreignAssignResponse.http}, code=${codeOf(foreignAssignResponse)}`, OPTIONS) const containerDetailResponse = await context.request('GET', `${CONTAINERS}/${containerId}`) @@ -275,7 +275,7 @@ export async function run(context) { version: Number(replaced?.version), reason: `${context.marker}-clear-container-no`, }) context.addStep(MODULE, 'PDA 任务创建后清空真实柜号失败', clearContainerNoResponse, - clearContainerNoResponse.http === 200 && ['A0400', '400'].includes(codeOf(clearContainerNoResponse)), + clearContainerNoResponse.http === 200 && ['A0409', '409'].includes(codeOf(clearContainerNoResponse)), `HTTP=${clearContainerNoResponse.http}, code=${codeOf(clearContainerNoResponse)}`, OPTIONS) const replacedTaskResponse = await context.request('GET', diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index adece17fd..bc13d56bc 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -117,7 +117,38 @@ export async function run(context) { const warehouseId = id(initial?.summary?.warehouseId) const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading' const pdaBase = '/api/platform/v1/first-leg/pda-loading' - const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?warehouseId=${encodeURIComponent(warehouseId)}`) + const bookingDraft = { + customerId: tenantId, customerName: initial?.summary?.customerName, + warehouseId, warehouseName: initial?.summary?.warehouseName, + channelCode: initial?.summary?.channelCode, channelName: initial?.summary?.channelName, + shippingCompanyName: `P3-SHIPPING-${suffix}`, bookingAgentName: `P3-AGENT-${suffix}`, + departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', + destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY/CY', freightTerm: 'PREPAID', + remark: context.marker, + parties: [{ partyType: 'SHIPPER', partyName: `P3-SHIPPER-${suffix}` }, + { partyType: 'CONSIGNEE', partyName: `P3-CONSIGNEE-${suffix}` }, + { partyType: 'NOTIFY_PARTY', partyName: `P3-NOTIFY-${suffix}` }], + containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 100, + maxWeightKg: 100000, quantity: 1 }], + } + const bookingCreateResponse = await request(context, 'POST', '/api/platform/v1/ocean-bookings', + { expectedVersion: 0, data: bookingDraft }, + { 'Idempotency-Key': `${context.marker}-p3-booking-create`.slice(0, 128) }) + const bookingCreated = context.apiSuccess(MODULE, '创建带渠道仓库快照的三期订舱', bookingCreateResponse, '', OPTIONS) + const bookingId = id(bookingCreated?.oceanBooking?.id) + if (!bookingId) return + const bookingSubmitResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/submit`, + { expectedVersion: Number(bookingCreated.oceanBooking.version), reason: context.marker }, + { 'Idempotency-Key': `${context.marker}-p3-booking-submit`.slice(0, 128) }) + const bookingSubmitted = context.apiSuccess(MODULE, '提交三期订舱', bookingSubmitResponse, '', OPTIONS) + const bookingConfirmResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/confirm`, + { expectedVersion: Number(bookingSubmitted?.oceanBooking?.version), reason: context.marker }, + { 'Idempotency-Key': `${context.marker}-p3-booking-confirm`.slice(0, 128) }) + const bookingConfirmed = context.apiSuccess(MODULE, '确认订舱并自动生成三期柜次', bookingConfirmResponse, '', OPTIONS) + const containerBookingId = id(bookingConfirmed?.containerAllocations?.[0]?.containerBookingId) + if (!containerBookingId) return + + const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS) const candidate = Array.isArray(candidates) ? candidates.find((item) => id(item.inboundId) === inboundId && Number(item.availablePackageCount) > 0) : null @@ -128,21 +159,6 @@ export async function run(context) { if (!candidate?.inboundDetailId || !warehouseId) return const containerNo = `P3B-${suffix}` - const containerResponse = await request(context, 'POST', '/api/platform/v1/containers', { - containerNo, cabinetNo: `P3BI-${suffix}`, warehouseId, - warehouseName: initial?.summary?.warehouseName || null, billOfLadingNo: `P3BL-${suffix}`, - vesselName: `P3-BRIDGE-${suffix}`, voyageNo: `P3V-${suffix}`, containerType: '40HQ', - shippingCompanyName: `P3-SHIPPING-${suffix}`, departurePortCode: 'CNSZX', departurePortName: '深圳', - destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', - destinationWarehouseCode: candidate.destinationWarehouseCode || null, - destinationWarehouseName: candidate.destinationWarehouseName || null, - capacityCbm: 100, maxWeightKg: 100000, loadingAt: '2026-09-02T08:00:00', - estimatedSailingAt: '2026-09-03T08:00:00', estimatedPortArrivalAt: '2026-09-20T08:00:00', - estimatedWarehouseArrivalAt: '2026-09-21T08:00:00', - }) - const container = context.apiSuccess(MODULE, '创建与入库预装串联的柜次', containerResponse, '', OPTIONS) - const containerBookingId = id(container?.id) - if (!containerBookingId) return const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, { bookingId: containerBookingId, remark: context.marker, @@ -203,6 +219,20 @@ export async function run(context) { && Number(pushedLoadingPlan?.version) === Number(link.loadingPlanVersion) + 1, `status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS) + const containerBeforeTransportResponse = await context.request('GET', + `/api/platform/v1/containers/${containerBookingId}`) + const containerBeforeTransport = context.apiSuccess(MODULE, '读取补真实柜号前柜次版本', + containerBeforeTransportResponse, '', OPTIONS) + const transportResponse = await request(context, 'PATCH', + `/api/platform/v1/containers/${containerBookingId}/transport`, { + containerNo, voyageNo: `P3V-${suffix}`, version: Number(containerBeforeTransport?.version), + reason: `${context.marker}-bind-container-no`, + }) + const transported = context.apiSuccess(MODULE, '补真实柜号并开放三期 PDA 任务', transportResponse, '', OPTIONS) + context.assert(MODULE, '真实柜号绑定到稳定柜次且计划身份不变', transportResponse, + transported?.containerNo === containerNo && id(transported?.id) === containerBookingId, + `containerNo=${transported?.containerNo || 'missing'}, containerBookingId=${id(transported?.id) || 'missing'}`, OPTIONS) + const blockedBeforePreloadConfirm = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`, { expectedVersion: initialVersion, remark: 'must confirm preload first' }, { 'Idempotency-Key': `${context.marker}-p3-complete-before-preload-confirm`.slice(0, 128) }) @@ -211,7 +241,7 @@ export async function run(context) { `HTTP=${blockedBeforePreloadConfirm.http}, code=${codeOf(blockedBeforePreloadConfirm)}`, OPTIONS) const canonicalTaskResponse = await context.request('GET', - `${pdaBase}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`) + `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const canonicalTask = context.apiSuccess(MODULE, '读取柜次级 PDA 唯一事实任务', canonicalTaskResponse, '', OPTIONS) const canonicalTaskId = id(canonicalTask?.id) const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId) 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 a901e3b8e..2dec25d6e 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 @@ -298,9 +298,10 @@ test('phase3 loading provisions its own marker-isolated WAIT_SPLIT fixture', asy assert.equal(run.marker.includes('-LOAD'), false) }) -test('container fixture keeps the run marker and does not publish incomplete WAIT_PRICING resources', () => { +test('container fixture isolates order idempotency while publishing the run marker', () => { const entry = source('../admin-test-env-acceptance.mjs') - assert.match(entry, /const fixtureMarker = originalMarker/) + assert.match(entry, /const fixtureMarker = `\$\{originalMarker\}-CAB`/) + assert.match(entry, /marker: originalMarker,\s*\n\s*sourceMarker: fixtureMarker/) assert.match(entry, /fixture\.finalStatus !== 'APPROVED'/) assert.match(entry, /禁止回退使用 WAIT_PRICING 订单/) })