diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java index b5a77648f..d833afbbc 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -123,11 +123,13 @@ public class InboundMeasurementService { long tenantId, long inboundId, long operatorId, InboundMeasurementRequests.SubmitRequest request) { InboundOrderEntity inbound = requireInbound(tenantId, inboundId); + if (!Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED") + .contains(inbound.getProcessStatus())) { + throw new BizException(ResultCode.CONFLICT, "测量已提交或审核完成,不能重复提交或修改冻结结果"); + } InboundMeasurementEntity measurement = requireMeasurement(tenantId, inboundId, request.measurementId()); requireVersion(request.version(), measurement.getVersion()); - boolean alreadyAdvanced = !Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED") - .contains(inbound.getProcessStatus()); - if (!alreadyAdvanced && !Set.of(MeasurementBatchService.STATUS_DRAFT, MeasurementBatchService.STATUS_RECEIVED) + if (!Set.of(MeasurementBatchService.STATUS_DRAFT, MeasurementBatchService.STATUS_RECEIVED) .contains(measurement.getStatus())) { throw new BizException(ResultCode.CONFLICT, "当前测量批次不能重复提交审核"); } @@ -135,19 +137,15 @@ public class InboundMeasurementService { List weights = repository.listWeights(tenantId, inboundId); ConfirmedWeightTotals confirmedTotals = confirmedWeightTotals(weights); - if (!alreadyAdvanced) { - requireConfirmedPackageTotal(inbound, items); - measurement.setStatus(STATUS_SUBMITTED); - if (!repository.updateMeasurement(measurement)) { - throw concurrentModification(); - } + requireConfirmedPackageTotal(inbound, items); + measurement.setStatus(STATUS_SUBMITTED); + if (!repository.updateMeasurement(measurement)) { + throw concurrentModification(); } - if (!alreadyAdvanced) { - inbound.setProcessStatus("MEASURE_PENDING"); - inbound.setMeasureReviewStatus("PENDING"); - inbound.setMeasureCompletedAt(LocalDateTime.now()); - } + inbound.setProcessStatus("MEASURE_PENDING"); + inbound.setMeasureReviewStatus("PENDING"); + inbound.setMeasureCompletedAt(LocalDateTime.now()); if (measurement.getTotalPackageCount() != null) { inbound.setTotalPackageCount(measurement.getTotalPackageCount()); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java index 6700292b4..6ec6882ef 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java @@ -72,8 +72,14 @@ public class MeasurementReviewService { } if (approved) { var items = repository.listMeasurementItems(tenantId, measurement.getId()); + if (items.isEmpty() || items.stream().anyMatch(item -> item.getPackageCount() == null + || item.getPackageCount() <= 0 || item.getTotalVolumeCbm() == null + || item.getTotalVolumeCbm().signum() <= 0 || item.getVolumeWeightKg() == null + || item.getVolumeWeightKg().signum() <= 0)) { + throw new BizException(ResultCode.VALIDATION_FAILED, + "测量明细箱数、体积和体积重必须完整且大于零,才能审核通过"); + } long confirmed = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getPackageCount) - .filter(java.util.Objects::nonNull) .mapToLong(Integer::longValue) .sum(); if (order.getTotalPackageCount() == null || confirmed != order.getTotalPackageCount()) { @@ -97,13 +103,23 @@ public class MeasurementReviewService { .map(com.baiyue.firstleg.entity.InboundWeightResultEntity::getNetWeightKg) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal volume = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getTotalVolumeCbm) - .filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal volumeWeight = items.stream() + .map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getVolumeWeightKg) + .reduce(BigDecimal.ZERO, BigDecimal::add); if (order.getActualGrossWeightKg() == null || order.getActualNetWeightKg() == null || order.getActualGrossWeightKg().compareTo(grossWeight) != 0 || order.getActualNetWeightKg().compareTo(netWeight) != 0 - || volume.signum() <= 0) { + || order.getActualVolumeCbm() == null || order.getActualVolumeCbm().compareTo(volume) != 0 + || order.getVolumeWeightKg() == null || order.getVolumeWeightKg().compareTo(volumeWeight) != 0 + || measurement.getTotalPackageCount() == null + || measurement.getTotalPackageCount() != confirmed + || measurement.getTotalVolumeCbm() == null + || measurement.getTotalVolumeCbm().compareTo(volume) != 0 + || measurement.getVolumeWeightKg() == null + || measurement.getVolumeWeightKg().compareTo(volumeWeight) != 0) { throw new BizException(ResultCode.VALIDATION_FAILED, - "审核通过前称重毛重、货物净重汇总和体积必须完整且一致"); + "审核通过前箱数、称重、体积和体积重必须在明细、测量批次与入库汇总间完整且一致"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index f411552d7..0d43b500d 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -234,6 +234,38 @@ class InboundMeasurementServiceTest { assertThat(result.confidence()).isEqualByComparingTo("0.50972"); } + @Test + void ocrFailureCreatesManualWeightThatCanBeAdjustedAndConfirmed() { + InboundOrderEntity inbound = inbound("WAIT_MEASURE"); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + + InboundMeasurementResponses.WeightResult created = service.createWeightResult( + 1L, 10L, 9L, + new InboundMeasurementRequests.CreateWeightResultRequest( + 120L, 501L, "FILE_WEIGHT_OCR", null, null, null, null)); + + assertThat(created.recognitionStatus()).isEqualTo("NEEDS_MANUAL"); + ArgumentCaptor saved = + ArgumentCaptor.forClass(InboundWeightResultEntity.class); + verify(repository).insertWeight(saved.capture()); + InboundWeightResultEntity weight = saved.getValue(); + weight.setId(20L); + when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight); + when(repository.updateWeight(weight)).thenReturn(true); + when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight)); + when(repository.updateInboundWeightSummary(inbound)).thenReturn(true); + + InboundMeasurementResponses.WeightResult confirmed = service.confirmWeightResult( + 1L, 10L, 20L, 9L, "操作员", + new InboundMeasurementRequests.ConfirmWeightResultRequest( + 0, new BigDecimal("30.000"), "KG", "OCR失败后人工确认")); + + assertThat(confirmed.recognitionStatus()).isEqualTo("CONFIRMED"); + assertThat(confirmed.grossWeightKg()).isEqualByComparingTo("30.000"); + assertThat(confirmed.netWeightKg()).isEqualByComparingTo("11.500"); + verify(repository, org.mockito.Mockito.times(2)).insertRevision(any(InboundWeightRevisionEntity.class)); + } + @Test void weightPalletOptionsUsesInboundWarehouseWithoutCustomerScope() { InboundOrderEntity inbound = inbound("WAIT_MEASURE"); @@ -799,9 +831,13 @@ class InboundMeasurementServiceTest { } @Test - void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() { + void approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals() { InboundOrderEntity inbound = inbound("WAIT_SPLIT"); inbound.setMeasureReviewStatus("APPROVED"); + inbound.setActualGrossWeightKg(new BigDecimal("40.000")); + inbound.setActualNetWeightKg(new BigDecimal("30.000")); + inbound.setActualVolumeCbm(new BigDecimal("2.0000")); + inbound.setVolumeWeightKg(new BigDecimal("334.000")); InboundMeasurementEntity measurement = new InboundMeasurementEntity(); measurement.setId(30L); measurement.setStatus("APPROVED"); @@ -815,14 +851,17 @@ class InboundMeasurementServiceTest { when(repository.listWeights(1L, 10L)).thenReturn(List.of()); when(repository.updateInbound(inbound)).thenReturn(true); - InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L, - new InboundMeasurementRequests.SubmitRequest(30L, 2)); + assertThatThrownBy(() -> service.submit(1L, 10L, 9L, + new InboundMeasurementRequests.SubmitRequest(30L, 2))) + .isInstanceOf(BizException.class) + .hasMessageContaining("审核完成"); - assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT"); - assertThat(result.measureReviewStatus()).isEqualTo("APPROVED"); - assertThat(measurement.getStatus()).isEqualTo("APPROVED"); + assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("40.000"); + assertThat(inbound.getActualNetWeightKg()).isEqualByComparingTo("30.000"); + assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("2.0000"); + assertThat(inbound.getVolumeWeightKg()).isEqualByComparingTo("334.000"); verify(repository, never()).updateMeasurement(any()); - verify(repository).updateInbound(inbound); + verify(repository, never()).updateInbound(any()); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java index 54792c723..78aa5cca2 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java @@ -91,6 +91,7 @@ class MeasurementReviewServiceTest { InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); item.setPackageCount(5); item.setTotalVolumeCbm(new BigDecimal("0.7480")); + item.setVolumeWeightKg(new BigDecimal("124.916")); when(fixture.repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item)); when(fixture.repository.transitionReview(10001L, 10L, 9L, 3, true, 20L)).thenReturn(1); when(fixture.repository.transitionMeasurement(10001L, 10L, 20L, "APPROVED")).thenReturn(1); @@ -118,6 +119,40 @@ class MeasurementReviewServiceTest { verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong()); } + @Test + @DisplayName("多SKU测量行的箱数、重量、体积和体积重守恒后才能进入WAIT_SPLIT") + void shouldApproveMultipleCargoLinesOnlyWhenAllFrozenTotalsMatch() { + Fixture fixture = fixture(false); + InboundMeasurementItemEntity first = measurementItem(2, "0.3000", "50.100"); + InboundMeasurementItemEntity second = measurementItem(3, "0.4480", "74.816"); + when(fixture.repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(first, second)); + when(fixture.repository.transitionReview(10001L, 10L, 9L, 3, true, 20L)).thenReturn(1); + when(fixture.repository.transitionMeasurement(10001L, 10L, 20L, "APPROVED")).thenReturn(1); + + fixture.service.approve(10001L, 9L, "审核员", 10L, "approve-multi-sku", + new MeasurementReviewRequest(20L, 3, null)); + + ArgumentCaptor history = + ArgumentCaptor.forClass(InboundStatusHistoryEntity.class); + verify(fixture.repository).insertHistory(history.capture()); + assertThat(history.getValue().getToStatus()).isEqualTo("WAIT_SPLIT"); + } + + @Test + @DisplayName("体积或体积重任一层汇总不一致时审核零写入") + void shouldRejectApprovalWhenVolumeSnapshotDoesNotConserve() { + Fixture fixture = fixture(false); + fixture.order.setActualVolumeCbm(new BigDecimal("0.7479")); + + assertThatThrownBy(() -> fixture.service.approve(10001L, 9L, "审核员", 10L, + "approve-volume-mismatch", new MeasurementReviewRequest(20L, 3, null))) + .isInstanceOf(BizException.class) + .hasMessageContaining("体积和体积重"); + + verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong()); + verify(fixture.repository, never()).transitionMeasurement(anyLong(), anyLong(), anyLong(), anyString()); + } + @Test @DisplayName("驳回原因必填且状态推进为测量驳回") void shouldRequireRejectReason() { @@ -217,13 +252,15 @@ class MeasurementReviewServiceTest { order.setTotalPackageCount(5); order.setActualGrossWeightKg(new BigDecimal("1787.500")); order.setActualNetWeightKg(new BigDecimal("1747.500")); + order.setActualVolumeCbm(new BigDecimal("0.7480")); + order.setVolumeWeightKg(new BigDecimal("124.916")); InboundMeasurementEntity measurement = new InboundMeasurementEntity(); measurement.setId(20L); measurement.setStatus("SUBMITTED"); - InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); - item.setPackageCount(5); - item.setTotalVolumeCbm(new BigDecimal("0.7480")); - item.setVolumeWeightKg(new BigDecimal("124.916")); + measurement.setTotalPackageCount(5); + measurement.setTotalVolumeCbm(new BigDecimal("0.7480")); + measurement.setVolumeWeightKg(new BigDecimal("124.916")); + InboundMeasurementItemEntity item = measurementItem(5, "0.7480", "124.916"); when(repository.findOwned(10001L, 10L)).thenReturn(order); when(repository.findMeasurement(10001L, 10L, 20L)).thenReturn(measurement); when(repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item)); @@ -236,6 +273,14 @@ class MeasurementReviewServiceTest { return new Fixture(service, repository, idempotency, order, measurement); } + private InboundMeasurementItemEntity measurementItem(int packages, String volume, String volumeWeight) { + InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); + item.setPackageCount(packages); + item.setTotalVolumeCbm(new BigDecimal(volume)); + item.setVolumeWeightKg(new BigDecimal(volumeWeight)); + return item; + } + private InboundWeightResultEntity confirmedWeight(String grossWeightKg, String netWeightKg) { InboundWeightResultEntity weight = new InboundWeightResultEntity(); weight.setRecognitionStatus("CONFIRMED"); 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 394982e55..d89ddff9c 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 @@ -3,6 +3,11 @@ import { readFileSync } from 'node:fs' import test from 'node:test' const source = readFileSync(new URL('inbound-measurement.mjs', import.meta.url), 'utf8') +const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8') +const labelTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java') +const measurementTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java') +const cartonTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java') +const reviewTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java') test('measurement acceptance scans the stable box code frozen by first print', () => { assert.match(source, /const boxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-1`/) @@ -32,3 +37,37 @@ test('measurement fixture derives pallet resources from confirmed image count', test('volume ceiling removes binary floating point noise before scaling', () => { assert.match(source, /Number\.EPSILON \* Math\.max\(1, Math\.abs\(value\)\) \* 4/) }) + +test('demo package freezes repeated label printing and Mojia dimension replacement', () => { + assert.match(labelTests, /repeatedAndPartialPrintsReadFrozenMappingWithoutReallocation/) + assert.match(labelTests, /changedCargoKeepsTheFirstFrozenBoxCodes/) + assert.match(cartonTests, /repeatedScanReplacesDeviceValuesButPreservesManualCountAndOldImageIsDeleted/) + assert.match(source, /增量 fixture 形成动态真实独立货物测量明细/) +}) + +test('demo package covers OCR failure, manual confirmation, aggregation and stale versions', () => { + assert.match(source, /OCR无有效重量时转人工确认/) + assert.match(source, /称重旧版本确认被拒绝且不覆盖已确认结果/) + assert.match(source, /图片张数决定托数且净重汇总已落地/) + assert.match(measurementTests, /ocrFailureCreatesManualWeightThatCanBeAdjustedAndConfirmed/) + assert.match(measurementTests, /confirmingFifthPalletSeparatelyAggregatesGrossAndCargoNetWeight/) + assert.match(measurementTests, /confirmingWithStaleVersionDoesNotTreatRequestAsIdempotent/) +}) + +test('demo package keeps pallet pages bounded and historical snapshots read-only', () => { + assert.match(source, /weight-pallet-options[^\n]+page=1&size=20&keyword=/) + assert.match(source, /records\.length <= 20/) + assert.match(measurementTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/) +}) + +test('approval freezes measurement and requires quantity weight and volume conservation', () => { + assert.match(source, /审核进入WAIT_SPLIT后箱码尺寸与图片快照不可再覆盖/) + assert.match(measurementTests, /approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals/) + assert.match(reviewTests, /shouldApproveMultipleCargoLinesOnlyWhenAllFrozenTotalsMatch/) + assert.match(reviewTests, /shouldRejectApprovalWhenVolumeSnapshotDoesNotConserve/) + assert.match(source, /actualPackageCount[^\n]+originalPackageCount/) + assert.match(source, /actualGrossWeightKg[^\n]+weightEvidence\.grossWeightKg/) + assert.match(source, /actualNetWeightKg[^\n]+weightEvidence\.netWeightKg/) + assert.match(source, /actualVolumeCbm[^\n]+totalVolume/) + assert.match(source, /volumeWeightKg[^\n]+volumeWeight/) +}) diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index 1171ab60e..856c8594e 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -84,7 +84,7 @@ async function waitForCompletedMeasurement(context, measurementPath, attempts = return { response, measurement, attempts } } -async function provisionWarehouseResources(context, { inboundId, warehouseId, palletCount }) { +async function provisionWarehouseResources(context, { inboundId, tenantId, warehouseId, palletCount }) { if (!warehouseId || !Number.isInteger(palletCount) || palletCount < 1) return null const resources = [] @@ -92,6 +92,20 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, pa if (!candidates) return null const palletIds = randomlyReusePalletIds(candidates, palletCount, context.context?.waitSplitVariant?.randomValue) + const candidateKeyword = candidates.find((item) => item.id === palletIds[0])?.code || '' + const candidateResponse = await context.request('GET', + `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-pallet-options?${query(tenantId)}&page=1&size=20&keyword=${encodeURIComponent(candidateKeyword)}`) + const candidatePage = context.apiSuccess(MODULE, '按关键词分页查询当前入库仓称重托盘候选', candidateResponse, + `warehouseId=${warehouseId}, keyword=${candidateKeyword}`, OPTIONS) + const pageControlled = Array.isArray(candidatePage?.records) && candidatePage.records.length <= 20 + && Number(candidatePage?.page) === 1 && Number(candidatePage?.size) === 20 + && Number(candidatePage?.total) >= candidatePage.records.length + && candidatePage.records.some((item) => asId(item?.palletId) === palletIds[0]) + context.assert(MODULE, '托盘候选保持服务端关键词分页且不返回无界数组', candidateResponse, + pageControlled, + `records=${candidatePage?.records?.length ?? 'missing'}, page=${candidatePage?.page ?? 'missing'}, size=${candidatePage?.size ?? 'missing'}, total=${candidatePage?.total ?? 'missing'}`, + OPTIONS) + if (!pageControlled) return null for (let index = 1; index <= palletCount; index += 1) { const palletId = palletIds[index - 1] context.assert(MODULE, `随机复用原有称重托盘 ${index}/${palletCount}`, null, @@ -134,6 +148,10 @@ async function createConfirmedWeightEvidence(context, { const created = context.apiSuccess(MODULE, `登记称重图片与托盘快照 ${index + 1}/${palletIds.length}`, createResponse, `fileId=${fileId}, palletId=${palletId}`, OPTIONS) if (!created?.id) return null + context.assert(MODULE, `OCR无有效重量时转人工确认 ${index + 1}/${palletIds.length}`, createResponse, + created.recognitionStatus === 'NEEDS_MANUAL' && created.ocrWeightValue == null, + `recognitionStatus=${created.recognitionStatus || 'missing'}, ocrWeightValue=${created.ocrWeightValue ?? 'null'}`, + OPTIONS) const grossWeight = Number(grossWeightPerPalletKg) + index * 5 const confirmResponse = await context.request('POST', @@ -148,6 +166,16 @@ async function createConfirmedWeightEvidence(context, { if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) { return null } + const staleConfirmResponse = await context.request('POST', + `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, { + version: Number(created.version), adjustedWeightValue: grossWeight + 1, + adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 旧版本冲突验证`, + }) + context.assert(MODULE, `称重旧版本确认被拒绝且不覆盖已确认结果 ${index + 1}/${palletIds.length}`, + staleConfirmResponse, + staleConfirmResponse.http === 200 && String(staleConfirmResponse.body?.code) === 'A0409', + `HTTP=${staleConfirmResponse.http}, code=${staleConfirmResponse.body?.code ?? 'missing'}, requestId=${staleConfirmResponse.requestId || 'missing'}`, + OPTIONS) confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg }) } @@ -161,7 +189,9 @@ async function createConfirmedWeightEvidence(context, { const netWeightKg = confirmedResults.reduce((sum, result) => sum + result.netWeightKg, 0) const persisted = confirmedRows.length === palletIds.length && confirmedResults.every((expected) => confirmedRows.some((row) => - asId(row?.id) === expected.id && asId(row?.palletId) === expected.palletId)) + asId(row?.id) === expected.id && asId(row?.palletId) === expected.palletId + && String(row?.palletCode || '').trim() && String(row?.palletType || '').trim() + && Number.isFinite(Number(row?.palletTareWeightKg)))) && closeTo(workspace?.actualGrossWeightKg, grossWeightKg, 0.001) && closeTo(workspace?.actualNetWeightKg, netWeightKg, 0.001) context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted, @@ -533,7 +563,7 @@ export async function run(context) { const palletCount = Math.max(1, Number(context.context?.waitSplitVariant?.palletCount || 1)) const warehouseResourcesReady = await provisionWarehouseResources(context, { - inboundId, warehouseId: asId(summary?.warehouseId), palletCount, + inboundId, tenantId, warehouseId: asId(summary?.warehouseId), palletCount, }) context.assert(MODULE, '测量提交前按称重图片数准备真实库位', draftResponse, Boolean(warehouseResourcesReady), @@ -606,6 +636,15 @@ export async function run(context) { && closeTo(finalMeasurement?.volumeWeightKg, volumeWeight), `processStatus=${finalMeasurement?.processStatus ?? 'missing'}, measurementStatus=${finalMeasurement?.measurementStatus ?? 'missing'}, packages=${finalMeasurement?.actualPackageCount ?? 'missing'}, pallets=${weightEvidence.count}, grossWeight=${finalMeasurement?.actualGrossWeightKg ?? 'missing'}, netWeight=${finalMeasurement?.actualNetWeightKg ?? 'missing'}`, OPTIONS) + const approvedScanResponse = await multipartRequest(context, scanPath, { + boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-AFTER-APPROVAL', + measuredAt: new Date().toISOString().slice(0, 19), + }) + context.assert(MODULE, '审核进入WAIT_SPLIT后箱码尺寸与图片快照不可再覆盖', approvedScanResponse, + approvedScanResponse.http === 200 && String(approvedScanResponse.body?.code) === 'A0409', + `HTTP=${approvedScanResponse.http}, code=${approvedScanResponse.body?.code ?? 'missing'}, requestId=${approvedScanResponse.requestId || 'missing'}`, + OPTIONS) + context.createdResources.inboundMeasurement = { marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId), boxCode, finalStatus: finalMeasurement?.measurementStatus || null,