diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index a00cc5ba8..6f03043c4 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -6183,14 +6183,56 @@ ON DUPLICATE KEY UPDATE color = VALUES(color), remark = VALUES(remark); --- 国内作业仓最小库位基线:每仓一个普通 A1 和一个系统共享溢出位 A0。 +-- 义乌仓初始化 A-F 六个库区,每区 A1-A15 共 90 个普通库位。 +-- A0 仍是系统共享溢出位;东莞仓继续保留最小 A1 + A0 基线。 -- 不初始化预占、占用或任何业务单据。 +INSERT INTO wh_location ( + id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name, + location_type, max_weight_kg, shared_overflow, status, remark, created_by, updated_by +) +WITH RECURSIVE + zones AS ( + SELECT 0 AS zone_index + UNION ALL + SELECT zone_index + 1 FROM zones WHERE zone_index < 5 + ), + slots AS ( + SELECT 1 AS slot_number + UNION ALL + SELECT slot_number + 1 FROM slots WHERE slot_number < 15 + ) +SELECT + CAST(193000000000200000 + CASE + WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201 + ELSE 203 + zones.zone_index * 15 + slots.slot_number + END AS UNSIGNED), + 0, + 193000000000200001, + CHAR(65 + zones.zone_index), + CONCAT(CHAR(65 + zones.zone_index), '区'), + CONCAT(CHAR(65 + zones.zone_index), slots.slot_number), + CONCAT(CHAR(65 + zones.zone_index), slots.slot_number, ' 普通库位'), + 'PALLET',1000.000,0,'AVAILABLE',NULL,0,0 +FROM zones +CROSS JOIN slots +ON DUPLICATE KEY UPDATE + tenant_id = 0, + warehouse_id = VALUES(warehouse_id), + zone_code = VALUES(zone_code), + zone_name = VALUES(zone_name), + location_name = VALUES(location_name), + location_type = VALUES(location_type), + max_weight_kg = VALUES(max_weight_kg), + shared_overflow = VALUES(shared_overflow), + status = 'AVAILABLE', + remark = VALUES(remark), + deleted = 0; + INSERT INTO wh_location ( id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name, location_type, max_weight_kg, shared_overflow, status, remark, created_by, updated_by ) VALUES - (193000000000200201,0,193000000000200001,'A','A区','A1','A1 普通库位','PALLET',1000.000,0,'AVAILABLE',NULL,0,0), (193000000000200202,0,193000000000200001,'A','A区','A0','A0 共享溢出位','PALLET',NULL,1,'AVAILABLE','系统共享临时溢出位',0,0), (193000000000200203,0,193000000000200002,'A','A区','A1','A1 普通库位','PALLET',1000.000,0,'AVAILABLE',NULL,0,0), (193000000000200204,0,193000000000200002,'A','A区','A0','A0 共享溢出位','PALLET',NULL,1,'AVAILABLE','系统共享临时溢出位',0,0) @@ -6207,6 +6249,35 @@ ON DUPLICATE KEY UPDATE remark = VALUES(remark), deleted = 0; +DROP PROCEDURE IF EXISTS assert_yiwu_location_seed; +DELIMITER // +CREATE PROCEDURE assert_yiwu_location_seed() +BEGIN + IF (SELECT COUNT(*) + FROM wh_location + WHERE tenant_id = 0 + AND warehouse_id = 193000000000200001 + AND deleted = 0 + AND shared_overflow = 0 + AND zone_code IN ('A','B','C','D','E','F') + AND location_code REGEXP '^[A-F]([1-9]|1[0-5])$' + AND LEFT(location_code, 1) = zone_code) <> 90 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '义乌仓 A-F 每区 15 个普通库位初始化不完整'; + END IF; + IF (SELECT COUNT(*) + FROM wh_location + WHERE tenant_id = 0 + AND warehouse_id = 193000000000200001 + AND location_code = 'A0' + AND shared_overflow = 1 + AND deleted = 0) <> 1 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '义乌仓系统共享溢出位 A0 初始化不完整'; + END IF; +END// +DELIMITER ; +CALL assert_yiwu_location_seed(); +DROP PROCEDURE IF EXISTS assert_yiwu_location_seed; + DROP TEMPORARY TABLE IF EXISTS tmp_wh_fba_public_master_data; CREATE TEMPORARY TABLE tmp_wh_fba_public_master_data ( warehouse_id_seed BIGINT UNSIGNED NOT NULL, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java index d5ccc7dc4..a19a4a705 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java @@ -59,18 +59,19 @@ public interface WarehousePalletLocationClient { record ReserveRequest(long tenantId, long warehouseId, String sourceService, long businessId, String inboundCode, String idempotencyKey, long operatorId, - BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm, String palletType, + BigDecimal totalVolumeCbm, List pallets, LocalDateTime expiresAt, boolean measurementCompleted) {} + record PalletReference(long weightResultId, long palletId) {} record ConfirmRequest(long tenantId, long warehouseId, String sourceService, long businessId, - long operatorId, BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm, - int packageCount) {} + long operatorId, BigDecimal totalVolumeCbm, int packageCount) {} record ReleaseRequest(long tenantId, String sourceService, long businessId, long operatorId, String reason) {} record SelectiveReleaseRequest(long tenantId, String sourceService, long businessId, String idempotencyKey, long operatorId, String reason, List locationIds) {} record AllocationResult(long reservationId, String reservationNo, int allocationNo, - long palletId, String palletCode, long locationId, String locationCode, + long palletId, String palletCode, BigDecimal palletCapacityCbm, + long locationId, String locationCode, BigDecimal allocatedVolumeCbm, String state) {} record BatchResult(int palletCount, List allocations) {} record ActiveResource(long reservationId, long warehouseId, long palletId, String palletCode, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepository.java index 8c094d55e..0252250f9 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepository.java @@ -22,7 +22,7 @@ public class InboundPalletAllocationRepository { @Transactional public void replace(long tenantId, long inboundId, long warehouseId, long warehouseResourceTenantId, - BigDecimal capacity, int totalPackageCount, BatchResult result) { + int totalPackageCount, BatchResult result) { Map reservationNos = mapper.selectList(new LambdaQueryWrapper() .eq(InboundPalletAllocationEntity::getTenantId, tenantId) .eq(InboundPalletAllocationEntity::getInboundId, inboundId)) @@ -33,9 +33,6 @@ public class InboundPalletAllocationRepository { .eq(InboundPalletAllocationEntity::getInboundId, inboundId)); LocalDateTime now = LocalDateTime.now(); int allocationCount = result.allocations().size(); - if (allocationCount > totalPackageCount) { - throw new IllegalArgumentException("托盘数量不能超过入库单总箱数"); - } int basePackageCount = allocationCount == 0 ? 0 : totalPackageCount / allocationCount; int remainder = allocationCount == 0 ? 0 : totalPackageCount % allocationCount; for (int index = 0; index < result.allocations().size(); index++) { @@ -48,7 +45,7 @@ public class InboundPalletAllocationRepository { value.setWarehouseResourceTenantId(warehouseResourceTenantId); value.setPalletId(source.palletId()); value.setPalletCode(source.palletCode()); - value.setPalletCapacityCbm(capacity); + value.setPalletCapacityCbm(source.palletCapacityCbm()); value.setAllocatedVolumeCbm(source.allocatedVolumeCbm()); int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0); value.setAllocatedPackageCount(allocatedPackages); 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 f61879683..ce2c9a87e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -117,6 +117,9 @@ public class InboundMeasurementService { List items = repository.listItems(tenantId, measurement.getId()); List weights = repository.listWeights(tenantId, inboundId); BigDecimal confirmedWeight = confirmedWeightForSubmission(weights); + int confirmedPalletCount = Math.toIntExact(weights.stream() + .filter(weight -> WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) + .count()); if (!alreadyAdvanced) { requireConfirmedPackageTotal(inbound, items); @@ -137,6 +140,7 @@ public class InboundMeasurementService { if (confirmedWeight != null) { inbound.setActualGrossWeightKg(confirmedWeight); } + inbound.setPalletCount(confirmedPalletCount); if (measurement.getTotalVolumeCbm() != null) { inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm()); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java index 1b9afbf20..2df133c29 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java @@ -8,6 +8,8 @@ import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.client.WarehousePalletLocationClient; import com.baiyue.firstleg.config.InboundProperties; import com.baiyue.firstleg.entity.InboundOrderEntity; +import com.baiyue.firstleg.entity.InboundWeightResultEntity; +import com.baiyue.firstleg.repository.InboundMeasurementRepository; import com.baiyue.firstleg.repository.InboundPalletAllocationRepository; import com.baiyue.firstleg.repository.InboundWorkflowRepository; import feign.FeignException; @@ -29,6 +31,8 @@ public class MeasurementLocationOccupationService { @Resource private InboundWorkflowRepository workflow; @Resource + private InboundMeasurementRepository measurements; + @Resource private InboundProperties properties; @Value("${baiyue.security.internal-service-token}") private String internalToken; @@ -39,6 +43,7 @@ public class MeasurementLocationOccupationService { long warehouseId = requireWarehouse(order); long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID; BigDecimal volume = requireVolume(order); + var palletReferences = confirmedPalletReferences(tenantId, inboundId); // 补偿释放后的预占记录需要保留审计,失败重试必须使用新的物理预占键; // 若仍存在活动预占,仓储侧会按业务单据复用,不会创建新的资源占用。 String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion @@ -46,19 +51,16 @@ public class MeasurementLocationOccupationService { WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve( internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId, SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume, - properties.getDefaultPalletCapacityCbm(), "STANDARD", + palletReferences, LocalDateTime.now().plus(properties.getReservationTtl()), true))), "测量完成后库位预占失败"); int packageCount = requirePackageCount(order); - allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, - properties.getDefaultPalletCapacityCbm(), packageCount, reserved); + allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, packageCount, reserved); try { WarehousePalletLocationClient.BatchResult occupied = requireSuccess(call(() -> warehouse.confirm( internalToken, new WarehousePalletLocationClient.ConfirmRequest(resourceTenantId, warehouseId, - SOURCE_SERVICE, inboundId, operatorId, volume, - properties.getDefaultPalletCapacityCbm(), packageCount))), "测量完成后库位占用失败"); - allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, - properties.getDefaultPalletCapacityCbm(), packageCount, occupied); + SOURCE_SERVICE, inboundId, operatorId, volume, packageCount))), "测量完成后库位占用失败"); + allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, packageCount, occupied); } catch (RuntimeException failure) { try { warehouse.release(internalToken, new WarehousePalletLocationClient.ReleaseRequest( @@ -97,6 +99,24 @@ public class MeasurementLocationOccupationService { return order.getTotalPackageCount(); } + private java.util.List confirmedPalletReferences( + long tenantId, long inboundId) { + java.util.List references = measurements + .listWeights(tenantId, inboundId).stream() + .filter(weight -> "CONFIRMED".equals(weight.getRecognitionStatus())) + .filter(weight -> weight.getConfirmedAt() != null && weight.getNetWeightKg() != null) + .filter(weight -> weight.getPalletId() != null && weight.getPalletId() > 0) + .map(weight -> new WarehousePalletLocationClient.PalletReference(weight.getId(), weight.getPalletId())) + .toList(); + if (references.isEmpty()) { + throw new BizException(ResultCode.CONFLICT, "至少需要一张已确认的称重图片才能提交测量"); + } + if (references.size() > 200) { + throw new BizException(ResultCode.VALIDATION_FAILED, "单票最多记录200张已确认称重图片"); + } + return references; + } + private static ApiResponse call(RemoteCall call) { try { return call.invoke(); } catch (FeignException exception) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepositoryTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepositoryTest.java index 9d006173a..b04320e20 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepositoryTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepositoryTest.java @@ -58,12 +58,12 @@ class InboundPalletAllocationRepositoryTest { when(mapper.selectList(any())).thenReturn(List.of(first, second)); var result = new WarehousePalletLocationClient.BatchResult(2, List.of( - new WarehousePalletLocationClient.AllocationResult(91L, null, 1, 501L, "P1", 11L, "A1", + new WarehousePalletLocationClient.AllocationResult(91L, null, 1, 501L, "P1", new BigDecimal("1.2"), 11L, "A1", new BigDecimal("1.2"), "OCCUPIED"), - new WarehousePalletLocationClient.AllocationResult(92L, null, 2, 501L, "P1", 12L, "A2", + new WarehousePalletLocationClient.AllocationResult(92L, null, 2, 501L, "P1", new BigDecimal("1.2"), 12L, "A2", new BigDecimal("1.2"), "OCCUPIED"))); - repository.replace(10001L, 31L, 88L, 0L, new BigDecimal("1.2"), 2, result); + repository.replace(10001L, 31L, 88L, 0L, 2, result); ArgumentCaptor inserted = ArgumentCaptor.forClass(InboundPalletAllocationEntity.class); verify(mapper, org.mockito.Mockito.times(2)).insert(inserted.capture()); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java index d2d85a258..1c2af185a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java @@ -115,21 +115,28 @@ class CartonMeasurementServiceTest { } @Test - void measuringInboundAllowsManualEntryBeforeMeasurementSubmission() { + void measuringInboundWithoutMeasurementAllowsManualEntryBeforeSubmission() { InboundOrderEntity measuring = inbound("MEASURING"); - InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0); when(repository.findInbound(10001L, 70001L)).thenReturn(measuring); when(repository.lockInbound(10001L, 70001L)).thenReturn(measuring); when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(null); - when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement); - when(repository.listItems(10001L, 30001L)).thenAnswer(invocation -> List.of(insertedItem.get())); - when(repository.updateMeasurement(measurement)).thenReturn(true); + when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(null); + when(repository.listItems(org.mockito.ArgumentMatchers.eq(10001L), + org.mockito.ArgumentMatchers.anyLong())).thenAnswer(invocation -> List.of(insertedItem.get())); + when(repository.updateMeasurement(any())).thenReturn(true); CartonMeasurementResult result = service.upsertManualForInbound( 10001L, 90001L, 70001L, acceptanceCommand()); + ArgumentCaptor measurement = + ArgumentCaptor.forClass(InboundMeasurementEntity.class); + verify(repository).insertMeasurement(measurement.capture()); assertThat(result.created()).isTrue(); - assertThat(measurement.getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_MANUAL); + assertThat(result.measurementId()).isEqualTo(measurement.getValue().getId()); + assertThat(measurement.getValue().getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_MANUAL); + assertThat(measurement.getValue().getSourceCode()).isEqualTo("PLATFORM"); + assertThat(measurement.getValue().getStatus()).isEqualTo(MeasurementBatchService.STATUS_RECEIVED); + assertThat(insertedItem.get().getMeasurementId()).isEqualTo(measurement.getValue().getId()); verify(images).upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any()); } 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 d3f975717..231d508d8 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -598,6 +598,7 @@ class InboundMeasurementServiceTest { assertThat(inbound.getMeasureReviewStatus()).isEqualTo("PENDING"); assertThat(measurement.getStatus()).isEqualTo("SUBMITTED"); assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("20.000"); + assertThat(inbound.getPalletCount()).isEqualTo(1); assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("1.2345"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java index a355c4eb9..6f8adca8b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java @@ -4,6 +4,8 @@ import com.baiyue.common.core.api.ApiResponse; import com.baiyue.firstleg.client.WarehousePalletLocationClient; import com.baiyue.firstleg.config.InboundProperties; import com.baiyue.firstleg.entity.InboundOrderEntity; +import com.baiyue.firstleg.entity.InboundWeightResultEntity; +import com.baiyue.firstleg.repository.InboundMeasurementRepository; import com.baiyue.firstleg.repository.InboundPalletAllocationRepository; import com.baiyue.firstleg.repository.InboundWorkflowRepository; import jakarta.annotation.Resource; @@ -39,6 +41,7 @@ class MeasurementLocationOccupationServiceTest { @MockBean private WarehousePalletLocationClient warehouse; @MockBean private InboundPalletAllocationRepository allocations; @MockBean private InboundWorkflowRepository workflow; + @MockBean private InboundMeasurementRepository measurements; @Resource private InboundProperties properties; @Resource private MeasurementLocationOccupationService service; @@ -46,6 +49,7 @@ class MeasurementLocationOccupationServiceTest { void platformWarehouseResourcesUseZeroWhileSnapshotsRemainInBusinessTenant() { properties.setWarehouseResourceTenantId(90001L); when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound()); + when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights()); when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"), org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED"))); when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"), @@ -59,6 +63,9 @@ class MeasurementLocationOccupationServiceTest { assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT); assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND); assertThat(reserve.getValue().idempotencyKey()).startsWith("measure-88-30-3-"); + assertThat(reserve.getValue().pallets()) + .extracting(WarehousePalletLocationClient.PalletReference::palletId) + .containsExactly(601L, 601L, 601L); ArgumentCaptor confirm = ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class); @@ -72,7 +79,6 @@ class MeasurementLocationOccupationServiceTest { org.mockito.ArgumentMatchers.eq(INBOUND), org.mockito.ArgumentMatchers.eq(WAREHOUSE), org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT), - org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")), org.mockito.ArgumentMatchers.eq(20), org.mockito.ArgumentMatchers.any()); } @@ -81,6 +87,7 @@ class MeasurementLocationOccupationServiceTest { void defaultResourceTenantRemainsPlatformZero() { properties.setWarehouseResourceTenantId(0L); when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound()); + when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights()); when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"), org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED"))); when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"), @@ -104,7 +111,6 @@ class MeasurementLocationOccupationServiceTest { org.mockito.ArgumentMatchers.eq(INBOUND), org.mockito.ArgumentMatchers.eq(WAREHOUSE), org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT), - org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")), org.mockito.ArgumentMatchers.eq(20), org.mockito.ArgumentMatchers.any()); } @@ -113,6 +119,7 @@ class MeasurementLocationOccupationServiceTest { void confirmFailureReleasesReservationInResourceTenantAndKeepsOriginalFailure() { properties.setWarehouseResourceTenantId(90001L); when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound()); + when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights()); when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"), org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED"))); IllegalStateException failure = new IllegalStateException("confirm failed"); @@ -130,7 +137,7 @@ class MeasurementLocationOccupationServiceTest { assertThat(release.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT); assertThat(release.getValue().businessId()).isEqualTo(INBOUND); verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE, PLATFORM_RESOURCE_TENANT, - new BigDecimal("1.200000"), 20, batch("RESERVED")); + 20, batch("RESERVED")); } private static InboundProperties properties() { @@ -159,9 +166,22 @@ class MeasurementLocationOccupationServiceTest { } private static WarehousePalletLocationClient.BatchResult batch(String state) { - return new WarehousePalletLocationClient.BatchResult(1, List.of( - new WarehousePalletLocationClient.AllocationResult( - 501L, "RES-001", 1, 601L, "PL-001", 701L, "A-001", - new BigDecimal("1.0000"), state))); + return new WarehousePalletLocationClient.BatchResult(3, java.util.stream.IntStream.rangeClosed(1, 3) + .mapToObj(index -> new WarehousePalletLocationClient.AllocationResult( + 500L + index, "RES-00" + index, index, 601L, "PL-001", new BigDecimal("1.200000"), + 700L + index, "A-00" + index, new BigDecimal("0.333333"), state)) + .toList()); + } + + private static List confirmedWeights() { + return java.util.stream.LongStream.rangeClosed(1, 3).mapToObj(index -> { + InboundWeightResultEntity weight = new InboundWeightResultEntity(); + weight.setId(800L + index); + weight.setPalletId(601L); + weight.setRecognitionStatus("CONFIRMED"); + weight.setConfirmedAt(java.time.LocalDateTime.now()); + weight.setNetWeightKg(new BigDecimal("10")); + return weight; + }).toList(); } } diff --git a/warehouse/src/main/java/com/baiyue/warehouse/dto/LocationDtos.java b/warehouse/src/main/java/com/baiyue/warehouse/dto/LocationDtos.java index d187bfc4c..318a65e18 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/dto/LocationDtos.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/dto/LocationDtos.java @@ -104,7 +104,7 @@ public final class LocationDtos { public record OccupancyTarget( @Min(1) long reservationId, @NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal allocatedVolumeCbm, - @Min(1) Integer allocatedPackageCount + @Min(0) Integer allocatedPackageCount ) { } diff --git a/warehouse/src/main/java/com/baiyue/warehouse/dto/PalletAllocationDtos.java b/warehouse/src/main/java/com/baiyue/warehouse/dto/PalletAllocationDtos.java index 979918d4f..641cf82a0 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/dto/PalletAllocationDtos.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/dto/PalletAllocationDtos.java @@ -25,13 +25,19 @@ public final class PalletAllocationDtos { @NotBlank @Size(max = 128) String idempotencyKey, @Min(1) long operatorId, @NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6) BigDecimal totalVolumeCbm, - @NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6) BigDecimal palletCapacityCbm, - @Size(max = 32) String palletType, + @NotNull @Size(min = 1, max = 200) List<@NotNull @jakarta.validation.Valid PalletReference> pallets, @NotNull LocalDateTime expiresAt, @NotNull Boolean measurementCompleted ) { } + /** 一张已确认称重图片对应一托;同一托盘主数据允许重复引用。 */ + public record PalletReference( + @Min(1) long weightResultId, + @Min(1) long palletId + ) { + } + public record ConfirmRequest( @Min(0) long tenantId, @Min(1) long warehouseId, @@ -39,7 +45,6 @@ public final class PalletAllocationDtos { @Min(1) long businessId, @Min(1) long operatorId, @NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6) BigDecimal totalVolumeCbm, - @NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6) BigDecimal palletCapacityCbm, @Min(1) int packageCount ) { } @@ -193,6 +198,7 @@ public final class PalletAllocationDtos { int allocationNo, long palletId, String palletCode, + BigDecimal palletCapacityCbm, long locationId, String locationCode, BigDecimal allocatedVolumeCbm, diff --git a/warehouse/src/main/java/com/baiyue/warehouse/repository/PalletRepository.java b/warehouse/src/main/java/com/baiyue/warehouse/repository/PalletRepository.java index 9f44088dd..eb759f73b 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/repository/PalletRepository.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/repository/PalletRepository.java @@ -62,16 +62,6 @@ public class PalletRepository { .ne(excludedId != null, PalletEntity::getId, excludedId)) > 0; } - public List findReusable(long tenantId, long warehouseId, String palletType, - java.math.BigDecimal capacityCbm) { - return mapper.selectList(new LambdaQueryWrapper() - .eq(PalletEntity::getTenantId, tenantId) - .eq(PalletEntity::getWarehouseId, warehouseId) - .eq(StringUtils.hasText(palletType), PalletEntity::getPalletType, palletType) - .eq(capacityCbm != null, PalletEntity::getCapacityCbm, capacityCbm) - .orderByAsc(PalletEntity::getPalletCode)); - } - /** * 生成平台作用域内下一个编码。编码序列跨仓库共用,且包含逻辑删除记录,确保编码永不复用。 */ diff --git a/warehouse/src/main/java/com/baiyue/warehouse/service/PalletLocationAllocationService.java b/warehouse/src/main/java/com/baiyue/warehouse/service/PalletLocationAllocationService.java index 4d940a3ce..fc41096d5 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/service/PalletLocationAllocationService.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/service/PalletLocationAllocationService.java @@ -32,8 +32,6 @@ import java.security.NoSuchAlgorithmException; */ @Service public class PalletLocationAllocationService { - /** 测量完成后默认推荐的普通托盘类型编码。 */ - public static final String DEFAULT_PALLET_TYPE = "STANDARD"; /** 仓库、托盘和库位属于平台公共资源域。 */ private static final long PLATFORM_RESOURCE_TENANT_ID = 0L; /** 仅头程服务可结算头程入库单占用的仓储资源。 */ @@ -41,8 +39,6 @@ public class PalletLocationAllocationService { @Resource private PalletRepository palletRepository; @Resource - private PalletVolumeSplitter splitter; - @Resource private LocationResourceService locationResourceService; @Resource private PalletLocationTransferOperationRepository transferOperations; @@ -59,74 +55,78 @@ public class PalletLocationAllocationService { if (!Boolean.TRUE.equals(request.measurementCompleted())) { throw new BizException(ResultCode.CONFLICT, "只能使用已完成测量的总体积拆分托盘并占用库位"); } - var split = splitter.split(request.totalVolumeCbm(), request.palletCapacityCbm()); List existing = active(request.tenantId(), request.sourceService(), request.businessId()); if (!existing.isEmpty()) { - return fromActive(existing, split); + return fromActive(request.tenantId(), request.warehouseId(), existing, request.totalVolumeCbm()); } - String palletType = org.springframework.util.StringUtils.hasText(request.palletType()) - ? request.palletType() : DEFAULT_PALLET_TYPE; - List pallets = palletRepository.findReusable(request.tenantId(), request.warehouseId(), - palletType, request.palletCapacityCbm()); - if (pallets.isEmpty()) { - throw new BizException(ResultCode.NOT_FOUND, "未配置匹配类型和容量的托盘主数据"); + List pallets = new ArrayList<>(request.pallets().size()); + for (PalletAllocationDtos.PalletReference reference : request.pallets()) { + PalletEntity pallet = palletRepository.findOwned(request.tenantId(), request.warehouseId(), reference.palletId()); + if (pallet == null) { + throw new BizException(ResultCode.NOT_FOUND, "称重图片关联的托盘主数据不存在或不属于当前仓库"); + } + pallets.add(pallet); } - List targets = new ArrayList<>(split.palletCount()); - for (int index = 0; index < split.palletCount(); index++) { - PalletEntity pallet = pallets.get(index % pallets.size()); + List targets = new ArrayList<>(pallets.size()); + for (int index = 0; index < pallets.size(); index++) { + PalletEntity pallet = pallets.get(index); targets.add(new LocationDtos.ReservationTarget(pallet.getId(), pallet.getPalletCode(), - request.idempotencyKey() + ":" + (index + 1))); + request.idempotencyKey() + ":" + request.pallets().get(index).weightResultId())); } List reservations = locationResourceService.reserveAvailable( new LocationDtos.ReserveCommand(request.tenantId(), request.warehouseId(), request.sourceService(), request.businessId(), request.inboundCode(), request.operatorId(), request.expiresAt(), targets)); - if (reservations.size() != split.palletCount()) { - throw new BizException(ResultCode.CONFLICT, "库位预占结果数量不一致"); + if (reservations.size() != pallets.size()) { + throw new BizException(ResultCode.CONFLICT, "库位预占结果数量与已确认称重图片数量不一致"); } List results = new ArrayList<>(reservations.size()); for (int index = 0; index < reservations.size(); index++) { var item = reservations.get(index); results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), item.reservationNo(), index + 1, - item.palletId(), item.palletCode(), item.locationId(), item.locationCode(), - split.allocations().get(index).allocatedVolumeCbm(), "RESERVED")); + item.palletId(), item.palletCode(), pallets.get(index).getCapacityCbm(), + item.locationId(), item.locationCode(), allocatedVolume(request.totalVolumeCbm(), reservations.size(), index), + "RESERVED")); } - return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results)); + return new PalletAllocationDtos.BatchResult(results.size(), List.copyOf(results)); } @Transactional @BusinessAudit(event = "PALLET_LOCATION_CONFIRM", resourceType = "INBOUND", resourceId = "#request.businessId", description = "确认库位占用") public PalletAllocationDtos.BatchResult confirm(PalletAllocationDtos.ConfirmRequest request) { - var split = splitter.split(request.totalVolumeCbm(), request.palletCapacityCbm()); List active = active(request.tenantId(), request.sourceService(), request.businessId()); - if (active.size() != split.palletCount()) { - throw new BizException(ResultCode.CONFLICT, "活动预占数量与托盘拆分数量不一致"); - } - if (active.size() > request.packageCount()) { - throw new BizException(ResultCode.CONFLICT, "托盘数量超过入库单总箱数,无法为每个托盘分配至少一箱"); + if (active.isEmpty()) { + throw new BizException(ResultCode.CONFLICT, "入库单没有可确认的活动库位预占"); } active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId)); List targets = new ArrayList<>(active.size()); int basePackageCount = request.packageCount() / active.size(); - int remainder = request.packageCount() % active.size(); + int packageRemainder = request.packageCount() % active.size(); for (int index = 0; index < active.size(); index++) { - int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0); targets.add(new LocationDtos.OccupancyTarget(active.get(index).reservationId(), - split.allocations().get(index).allocatedVolumeCbm(), allocatedPackages)); + allocatedVolume(request.totalVolumeCbm(), active.size(), index), + basePackageCount + (index < packageRemainder ? 1 : 0))); } List occupied = locationResourceService.confirmOccupied( new LocationDtos.ConfirmCommand(request.tenantId(), request.warehouseId(), request.sourceService(), request.businessId(), request.operatorId(), targets)); + if (occupied.size() != active.size()) { + throw new BizException(ResultCode.CONFLICT, "库位占用结果数量与已确认称重图片数量不一致"); + } List results = new ArrayList<>(occupied.size()); for (int index = 0; index < occupied.size(); index++) { var item = occupied.get(index); + PalletEntity pallet = palletRepository.findOwnedIncludingDeleted(request.tenantId(), request.warehouseId(), item.palletId()); + if (pallet == null) { + throw new BizException(ResultCode.NOT_FOUND, "库位占用引用的托盘主数据不存在"); + } results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), active.get(index).reservationNo(), index + 1, - item.palletId(), active.get(index).palletCode(), item.locationId(), item.locationCode(), - split.allocations().get(index).allocatedVolumeCbm(), "OCCUPIED")); + item.palletId(), active.get(index).palletCode(), pallet.getCapacityCbm(), item.locationId(), item.locationCode(), + allocatedVolume(request.totalVolumeCbm(), occupied.size(), index), "OCCUPIED")); } - return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results)); + return new PalletAllocationDtos.BatchResult(results.size(), List.copyOf(results)); } @Transactional @@ -528,19 +528,27 @@ public class PalletLocationAllocationService { return locationResourceService.findOccupiedResources(tenantId, sourceService, businessId); } - private static PalletAllocationDtos.BatchResult fromActive(List active, - PalletVolumeSplitter.SplitResult split) { - if (active.size() != split.palletCount()) { - throw new BizException(ResultCode.CONFLICT, "幂等请求与已有托盘拆分结果不一致"); - } + private PalletAllocationDtos.BatchResult fromActive(long tenantId, long warehouseId, + List active, + java.math.BigDecimal totalVolumeCbm) { active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId)); List results = new ArrayList<>(active.size()); for (int index = 0; index < active.size(); index++) { var item = active.get(index); + PalletEntity pallet = palletRepository.findOwnedIncludingDeleted( + tenantId, warehouseId, item.palletId()); + if (pallet == null) { + throw new BizException(ResultCode.NOT_FOUND, "已有库位分配引用的托盘主数据不存在"); + } results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), item.reservationNo(), index + 1, - item.palletId(), item.palletCode(), item.locationId(), item.locationCode(), - split.allocations().get(index).allocatedVolumeCbm(), item.state())); + item.palletId(), item.palletCode(), pallet.getCapacityCbm(), item.locationId(), item.locationCode(), + allocatedVolume(totalVolumeCbm, active.size(), index), item.state())); } - return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results)); + return new PalletAllocationDtos.BatchResult(results.size(), List.copyOf(results)); + } + + private static java.math.BigDecimal allocatedVolume(java.math.BigDecimal total, int count, int index) { + java.math.BigDecimal base = total.divide(java.math.BigDecimal.valueOf(count), 6, java.math.RoundingMode.DOWN); + return index == count - 1 ? total.subtract(base.multiply(java.math.BigDecimal.valueOf(count - 1L))) : base; } } diff --git a/warehouse/src/main/java/com/baiyue/warehouse/service/PalletVolumeSplitter.java b/warehouse/src/main/java/com/baiyue/warehouse/service/PalletVolumeSplitter.java deleted file mode 100644 index f2bcd730a..000000000 --- a/warehouse/src/main/java/com/baiyue/warehouse/service/PalletVolumeSplitter.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.baiyue.warehouse.service; - -import com.baiyue.common.core.api.ResultCode; -import com.baiyue.common.core.exception.BizException; -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.List; - -/** 按托盘容积向上拆分入库总体积,不涉及SKU库存。 */ -@Component -public class PalletVolumeSplitter { - static final int MAX_PALLET_COUNT = 200; - - public SplitResult split(BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm) { - requirePositive(totalVolumeCbm, "总体积必须大于0"); - requirePositive(palletCapacityCbm, "托盘容积必须大于0"); - final int palletCount; - try { - palletCount = totalVolumeCbm.divide(palletCapacityCbm, 0, RoundingMode.CEILING).intValueExact(); - } catch (ArithmeticException exception) { - throw new BizException(ResultCode.VALIDATION_FAILED, "托盘拆分数量超出系统处理范围"); - } - if (palletCount > MAX_PALLET_COUNT) { - throw new BizException(ResultCode.VALIDATION_FAILED, "单次最多拆分200个托盘"); - } - - List allocations = new ArrayList<>(palletCount); - for (int index = 1; index <= palletCount; index++) { - BigDecimal allocated = index == palletCount - ? totalVolumeCbm.subtract(palletCapacityCbm.multiply(BigDecimal.valueOf(palletCount - 1L))) - : palletCapacityCbm; - allocations.add(new Allocation(index, allocated)); - } - return new SplitResult(totalVolumeCbm, palletCapacityCbm, palletCount, List.copyOf(allocations)); - } - - private static void requirePositive(BigDecimal value, String message) { - if (value == null || value.signum() <= 0) { - throw new BizException(ResultCode.VALIDATION_FAILED, message); - } - } - - public record Allocation(int allocationNo, BigDecimal allocatedVolumeCbm) { - } - - public record SplitResult(BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm, - int palletCount, List allocations) { - } -} diff --git a/warehouse/src/main/resources/db/phase2-pallet-seed.sql b/warehouse/src/main/resources/db/phase2-pallet-seed.sql index 507565fb5..85f68321a 100644 --- a/warehouse/src/main/resources/db/phase2-pallet-seed.sql +++ b/warehouse/src/main/resources/db/phase2-pallet-seed.sql @@ -16,14 +16,56 @@ ON DUPLICATE KEY UPDATE color=VALUES(color), remark=VALUES(remark); --- 每个国内作业仓初始化一个普通库位和一个系统共享溢出位 A0。 +-- 义乌仓初始化 A-F 六个库区,每区 A1-A15 共 90 个普通库位。 +-- A0 仍是系统共享溢出位;东莞仓继续保留最小 A1 + A0 基线。 -- 这里只创建公共资源,不初始化预占、占用或业务单据。 +INSERT INTO wh_location( + id,tenant_id,warehouse_id,zone_code,zone_name,location_code,location_name, + location_type,max_weight_kg,shared_overflow,status,remark,created_by,updated_by +) +WITH RECURSIVE + zones AS ( + SELECT 0 AS zone_index + UNION ALL + SELECT zone_index + 1 FROM zones WHERE zone_index < 5 + ), + slots AS ( + SELECT 1 AS slot_number + UNION ALL + SELECT slot_number + 1 FROM slots WHERE slot_number < 15 + ) +SELECT + CAST(190000000000000000 + CASE + WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201 + ELSE 203 + zones.zone_index * 15 + slots.slot_number + END AS UNSIGNED), + 0, + 190000000000000001, + CHAR(65 + zones.zone_index), + CONCAT(CHAR(65 + zones.zone_index), '区'), + CONCAT(CHAR(65 + zones.zone_index), slots.slot_number), + CONCAT(CHAR(65 + zones.zone_index), slots.slot_number, ' 普通库位'), + 'PALLET',1000.000,0,'AVAILABLE',NULL,0,0 +FROM zones +CROSS JOIN slots +ON DUPLICATE KEY UPDATE + tenant_id=0, + warehouse_id=VALUES(warehouse_id), + zone_code=VALUES(zone_code), + zone_name=VALUES(zone_name), + location_name=VALUES(location_name), + location_type=VALUES(location_type), + max_weight_kg=VALUES(max_weight_kg), + shared_overflow=VALUES(shared_overflow), + status='AVAILABLE', + remark=VALUES(remark), + deleted=0; + INSERT INTO wh_location( id,tenant_id,warehouse_id,zone_code,zone_name,location_code,location_name, location_type,max_weight_kg,shared_overflow,status,remark,created_by,updated_by ) VALUES - (190000000000000201,0,190000000000000001,'A','A区','A1','A1 普通库位','PALLET',1000.000,0,'AVAILABLE',NULL,0,0), (190000000000000202,0,190000000000000001,'A','A区','A0','A0 共享溢出位','PALLET',NULL,1,'AVAILABLE','系统共享临时溢出位',0,0), (190000000000000203,0,190000000000000002,'A','A区','A1','A1 普通库位','PALLET',1000.000,0,'AVAILABLE',NULL,0,0), (190000000000000204,0,190000000000000002,'A','A区','A0','A0 共享溢出位','PALLET',NULL,1,'AVAILABLE','系统共享临时溢出位',0,0) @@ -39,3 +81,32 @@ ON DUPLICATE KEY UPDATE status='AVAILABLE', remark=VALUES(remark), deleted=0; + +DROP PROCEDURE IF EXISTS assert_yiwu_location_seed; +DELIMITER // +CREATE PROCEDURE assert_yiwu_location_seed() +BEGIN + IF (SELECT COUNT(*) + FROM wh_location + WHERE tenant_id=0 + AND warehouse_id=190000000000000001 + AND deleted=0 + AND shared_overflow=0 + AND zone_code IN ('A','B','C','D','E','F') + AND location_code REGEXP '^[A-F]([1-9]|1[0-5])$' + AND LEFT(location_code,1)=zone_code) <> 90 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='义乌仓 A-F 每区 15 个普通库位初始化不完整'; + END IF; + IF (SELECT COUNT(*) + FROM wh_location + WHERE tenant_id=0 + AND warehouse_id=190000000000000001 + AND location_code='A0' + AND shared_overflow=1 + AND deleted=0) <> 1 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='义乌仓系统共享溢出位 A0 初始化不完整'; + END IF; +END// +DELIMITER ; +CALL assert_yiwu_location_seed(); +DROP PROCEDURE IF EXISTS assert_yiwu_location_seed; diff --git a/warehouse/src/test/java/com/baiyue/warehouse/dto/PalletAllocationDtosValidationTest.java b/warehouse/src/test/java/com/baiyue/warehouse/dto/PalletAllocationDtosValidationTest.java index 5d6a0b88e..c162e1f2d 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/dto/PalletAllocationDtosValidationTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/dto/PalletAllocationDtosValidationTest.java @@ -18,9 +18,10 @@ class PalletAllocationDtosValidationTest { LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(30); assertThat(validator.validate(new PalletAllocationDtos.ReserveRequest(0L, 20L, "FIRSTLEG", 88L, - "RK-001", "measure-88", 9L, BigDecimal.ONE, BigDecimal.ONE, "STANDARD", expiresAt, true))).isEmpty(); + "RK-001", "measure-88", 9L, BigDecimal.ONE, + List.of(new PalletAllocationDtos.PalletReference(7001L, 30L)), expiresAt, true))).isEmpty(); assertThat(validator.validate(new PalletAllocationDtos.ConfirmRequest(0L, 20L, "FIRSTLEG", 88L, - 9L, BigDecimal.ONE, BigDecimal.ONE, 20))).isEmpty(); + 9L, BigDecimal.ONE, 20))).isEmpty(); assertThat(validator.validate(new PalletAllocationDtos.ReleaseRequest(0L, "FIRSTLEG", 88L, 9L, "入库补偿"))).isEmpty(); assertThat(validator.validate(new PalletAllocationDtos.SelectiveReleaseRequest(0L, "FIRSTLEG", 88L, diff --git a/warehouse/src/test/java/com/baiyue/warehouse/repository/InitialWarehouseResourceSeedSqlContractTest.java b/warehouse/src/test/java/com/baiyue/warehouse/repository/InitialWarehouseResourceSeedSqlContractTest.java index d6f2b911d..3683b2811 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/repository/InitialWarehouseResourceSeedSqlContractTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/repository/InitialWarehouseResourceSeedSqlContractTest.java @@ -10,7 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat; class InitialWarehouseResourceSeedSqlContractTest { @Test - void initializesReusablePalletMasterDataAndAvailableLocationsForBothDomesticWarehouses() throws Exception { + void initializesReusablePalletsAndYiwuLocationGrid() throws Exception { assertModuleSeed(Files.readString(Path.of("src", "main", "resources", "db", "phase2-pallet-seed.sql"), StandardCharsets.UTF_8)); assertUnifiedSeed(Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"), @@ -30,7 +30,15 @@ class InitialWarehouseResourceSeedSqlContractTest { assertThat(sql) .contains("0," + yiwuId + ",'TP-001'") .contains("0," + dongguanId + ",'TP-002'") - .contains("(" + idPrefix + "201,0," + yiwuId + ",'A','A区','A1'") + .contains("WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201") + .contains("ELSE 203 + zones.zone_index * 15 + slots.slot_number") + .contains("SELECT zone_index + 1 FROM zones WHERE zone_index < 5") + .contains("SELECT slot_number + 1 FROM slots WHERE slot_number < 15") + .contains("CONCAT(CHAR(65 + zones.zone_index), slots.slot_number)") + .contains("CROSS JOIN slots") + .contains("location_code REGEXP '^[A-F]([1-9]|1[0-5])$'") + .contains("<> 90 THEN") + .contains("义乌仓 A-F 每区 15 个普通库位初始化不完整") .contains("(" + idPrefix + "202,0," + yiwuId + ",'A','A区','A0'") .contains("(" + idPrefix + "203,0," + dongguanId + ",'A','A区','A1'") .contains("(" + idPrefix + "204,0," + dongguanId + ",'A','A区','A0'") diff --git a/warehouse/src/test/java/com/baiyue/warehouse/service/PalletLocationAllocationServiceTest.java b/warehouse/src/test/java/com/baiyue/warehouse/service/PalletLocationAllocationServiceTest.java index 042cfbc12..490898b73 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/service/PalletLocationAllocationServiceTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/service/PalletLocationAllocationServiceTest.java @@ -47,50 +47,48 @@ class PalletLocationAllocationServiceTest { @Configuration(proxyBeanMethods = false) static class TestConfig { - @Bean PalletVolumeSplitter splitter() { return new PalletVolumeSplitter(); } @Bean ObjectMapper objectMapper() { return new ObjectMapper(); } } @Test - void reusesOnePalletMasterForMultipleAllocations() { + void oneConfirmedWeightImageCreatesOneAllocationWithoutCapacityMatching() { when(locations.findActiveResources(10001, "FIRSTLEG", 88)).thenReturn(List.of()); - when(pallets.findReusable(10001, 20, "木托盘", new BigDecimal("2"))) - .thenReturn(List.of(pallet(30, "TP-001"))); + when(pallets.findOwned(10001, 20, 30)).thenReturn(pallet(30, "TP-001")); when(locations.reserveAvailable(any())).thenReturn(List.of( new LocationDtos.ReservationResult(101, "LR101", 30, "TP-001", 40, "A-01", false, - LocalDateTime.now().plusMinutes(30)), - new LocationDtos.ReservationResult(102, "LR102", 30, "TP-001", 41, "A-02", false, LocalDateTime.now().plusMinutes(30)))); var request = new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", 88, "RK-001", "measure-001", - 9001, new BigDecimal("4"), new BigDecimal("2"), "木托盘", LocalDateTime.now().plusMinutes(30), true); - assertThat(service.reserve(request).allocations()).hasSize(2) - .extracting(PalletAllocationDtos.AllocationResult::palletId) - .containsOnly(30L); + 9001, new BigDecimal("4"), List.of(new PalletAllocationDtos.PalletReference(7001, 30)), + LocalDateTime.now().plusMinutes(30), true); + assertThat(service.reserve(request).allocations()).singleElement().satisfies(item -> { + assertThat(item.palletId()).isEqualTo(30L); + assertThat(item.allocatedVolumeCbm()).isEqualByComparingTo("4"); + assertThat(item.palletCapacityCbm()).isEqualByComparingTo("1.2"); + }); org.mockito.ArgumentCaptor command = org.mockito.ArgumentCaptor.forClass(LocationDtos.ReserveCommand.class); verify(locations).reserveAvailable(command.capture()); - assertThat(command.getValue().targets()).hasSize(2) - .extracting(LocationDtos.ReservationTarget::palletId) - .containsOnly(30L); + assertThat(command.getValue().targets()).singleElement() + .extracting(LocationDtos.ReservationTarget::palletId).isEqualTo(30L); } @Test void shouldHandleCase02() { when(locations.findActiveResources(10001, "FIRSTLEG", 99)).thenReturn(List.of()); - when(pallets.findReusable(10001, 20, "STANDARD", new BigDecimal("2"))) - .thenReturn(List.of(pallet(30, "TP-001"))); + when(pallets.findOwned(10001, 20, 30)).thenReturn(pallet(30, "TP-001")); when(locations.reserveAvailable(any())).thenReturn(List.of( new LocationDtos.ReservationResult(101, "LR101", 30, "TP-001", 40, "A0", true, LocalDateTime.now().plusMinutes(30)))); var request = new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", 99, "RK-001", "measure-99", - 9001, new BigDecimal("2"), new BigDecimal("2"), null, LocalDateTime.now().plusMinutes(30), true); + 9001, new BigDecimal("2"), List.of(new PalletAllocationDtos.PalletReference(7001, 30)), + LocalDateTime.now().plusMinutes(30), true); var result = service.reserve(request); assertThat(result.allocations().get(0).locationCode()).isEqualTo("A0"); - verify(pallets).findReusable(10001, 20, "STANDARD", new BigDecimal("2")); + verify(pallets).findOwned(10001, 20, 30); } @Test @@ -129,7 +127,7 @@ class PalletLocationAllocationServiceTest { assertThat(result.palletCount()).isEqualTo(1); assertThat(result.allocations().get(0).state()).isEqualTo("RESERVED"); - verify(pallets, never()).findReusable(any(Long.class), any(Long.class), any(), any()); + verify(pallets, never()).findOwned(anyLong(), anyLong(), anyLong()); } @Test @@ -139,9 +137,10 @@ class PalletLocationAllocationServiceTest { when(locations.findActiveResources(10001L, "FIRSTLEG", 88L)).thenReturn(List.of(resource)); when(locations.confirmOccupied(any())).thenReturn(List.of(new LocationDtos.OccupancyResult( 301L, "OC-301", 101L, 30L, 40L, "A-01", false))); + when(pallets.findOwnedIncludingDeleted(10001L, 20L, 30L)).thenReturn(pallet(30L, "TP-001")); service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L, "FIRSTLEG", 88L, - 9001L, new BigDecimal("1.0"), new BigDecimal("2.0"), 20)); + 9001L, new BigDecimal("1.0"), 20)); org.mockito.ArgumentCaptor command = org.mockito.ArgumentCaptor.forClass(LocationDtos.ConfirmCommand.class); @@ -152,10 +151,10 @@ class PalletLocationAllocationServiceTest { } @Test - void confirmDistributesRemainderWithoutZeroPackageAllocations() { + void confirmedImageCountDefinesPalletCountEvenWhenItExceedsPackageCount() { List resources = new ArrayList<>(); List occupancyResults = new ArrayList<>(); - for (int index = 1; index <= 10; index++) { + for (int index = 1; index <= 3; index++) { long reservationId = 100L + index; long palletId = 200L + index; long locationId = 300L + index; @@ -163,33 +162,21 @@ class PalletLocationAllocationServiceTest { "TP-" + index, locationId, "A-" + index, false, "RESERVED")); occupancyResults.add(new LocationDtos.OccupancyResult(400L + index, "OC-" + index, reservationId, palletId, locationId, "A-" + index, false)); + PalletEntity pallet = pallet(palletId, "TP-" + index); + when(pallets.findOwnedIncludingDeleted(10001L, 20L, palletId)).thenReturn(pallet); } when(locations.findActiveResources(10001L, "FIRSTLEG", 89L)).thenReturn(resources); when(locations.confirmOccupied(any())).thenReturn(occupancyResults); service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L, "FIRSTLEG", 89L, - 9001L, new BigDecimal("12.0"), new BigDecimal("1.2"), 12)); + 9001L, new BigDecimal("12.0"), 2)); org.mockito.ArgumentCaptor command = org.mockito.ArgumentCaptor.forClass(LocationDtos.ConfirmCommand.class); verify(locations).confirmOccupied(command.capture()); assertThat(command.getValue().targets()) .extracting(LocationDtos.OccupancyTarget::allocatedPackageCount) - .containsExactly(2, 2, 1, 1, 1, 1, 1, 1, 1, 1); - } - - @Test - void confirmRejectsMorePalletsThanPackages() { - List resources = List.of( - new LocationDtos.ActiveResource(101L, null, 20L, 30L, "TP-001", 40L, "A-01", false, "RESERVED"), - new LocationDtos.ActiveResource(102L, null, 20L, 31L, "TP-002", 41L, "A-02", false, "RESERVED")); - when(locations.findActiveResources(10001L, "FIRSTLEG", 90L)).thenReturn(resources); - - assertThatThrownBy(() -> service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L, - "FIRSTLEG", 90L, 9001L, new BigDecimal("2.0"), new BigDecimal("1.0"), 1))) - .isInstanceOf(BizException.class) - .hasMessageContaining("托盘数量超过入库单总箱数"); - verify(locations, never()).confirmOccupied(any()); + .containsExactly(1, 1, 0); } @Test @@ -490,13 +477,15 @@ class PalletLocationAllocationServiceTest { private static PalletAllocationDtos.ReserveRequest reserve(long businessId) { return new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", businessId, "RK-001", "measure-001", - 9001, new BigDecimal("2"), new BigDecimal("2"), "木托盘", LocalDateTime.now().plusMinutes(30), true); + 9001, new BigDecimal("2"), List.of(new PalletAllocationDtos.PalletReference(7001, 30)), + LocalDateTime.now().plusMinutes(30), true); } private static PalletEntity pallet(long id, String code) { PalletEntity entity = new PalletEntity(); entity.setId(id); entity.setPalletCode(code); + entity.setCapacityCbm(new BigDecimal("1.2")); return entity; } } diff --git a/warehouse/src/test/java/com/baiyue/warehouse/service/PalletVolumeSplitterTest.java b/warehouse/src/test/java/com/baiyue/warehouse/service/PalletVolumeSplitterTest.java deleted file mode 100644 index fab4a2bcc..000000000 --- a/warehouse/src/test/java/com/baiyue/warehouse/service/PalletVolumeSplitterTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.baiyue.warehouse.service; - -import com.baiyue.common.core.exception.BizException; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class PalletVolumeSplitterTest { - private final PalletVolumeSplitter splitter = new PalletVolumeSplitter(); - - @Test - void shouldHandleCase01() { - var result = splitter.split(new BigDecimal("20"), new BigDecimal("2")); - assertThat(result.palletCount()).isEqualTo(10); - assertThat(result.allocations()).allMatch(item -> item.allocatedVolumeCbm().compareTo(new BigDecimal("2")) == 0); - } - - @Test - void shouldHandleCase02() { - var result = splitter.split(new BigDecimal("20.1"), new BigDecimal("2")); - assertThat(result.palletCount()).isEqualTo(11); - assertThat(result.allocations().get(10).allocatedVolumeCbm()).isEqualByComparingTo("0.1"); - assertThat(result.allocations()).extracting(PalletVolumeSplitter.Allocation::allocationNo) - .containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); - } - - @Test - void shouldHandleCase03() { - assertThatThrownBy(() -> splitter.split(null, BigDecimal.ONE)).isInstanceOf(BizException.class); - assertThatThrownBy(() -> splitter.split(BigDecimal.ZERO, BigDecimal.ONE)).isInstanceOf(BizException.class); - assertThatThrownBy(() -> splitter.split(BigDecimal.ONE, BigDecimal.ZERO)).isInstanceOf(BizException.class); - assertThatThrownBy(() -> splitter.split(new BigDecimal("10001"), BigDecimal.ONE)).isInstanceOf(BizException.class); - } -}