From bed917ba6c5be477c917c2ef9f52cadb9b5b0afd Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 11 Aug 2026 20:00:31 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A3=85=E6=9F=9C=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/src/main/resources/application.yml | 2 +- .../repository/InboundOrderRepository.java | 2 +- .../InboundPalletAllocationRepository.java | 14 +++--- .../service/FinanceFeeSourceOutboxRelay.java | 19 ++++---- .../MeasurementLocationOccupationService.java | 6 ++- .../service/UnifiedInboundQueryService.java | 11 +++-- ...surementLocationOccupationServiceTest.java | 2 +- .../client/FirstlegUnifiedOrderClient.java | 3 +- .../order/dto/UnifiedOrderResponse.java | 3 +- .../service/UnifiedOrderQueryService.java | 10 +++-- .../service/UnifiedOrderQueryServiceTest.java | 4 +- .../PalletLocationAllocationService.java | 10 +++-- .../PalletLocationAllocationServiceTest.java | 44 +++++++++++++++++++ 13 files changed, 96 insertions(+), 34 deletions(-) diff --git a/auth/src/main/resources/application.yml b/auth/src/main/resources/application.yml index 3c9e463aa..346a5d5f2 100644 --- a/auth/src/main/resources/application.yml +++ b/auth/src/main/resources/application.yml @@ -34,7 +34,7 @@ baiyue.security: internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long} token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth} issuer: ${JWT_ISSUER:auth-service} - access-token-ttl: ${ACCESS_TOKEN_TTL:30m} + access-token-ttl: ${ACCESS_TOKEN_TTL:24h} refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d} max-login-failures: ${MAX_LOGIN_FAILURES:5} lock-duration: ${LOGIN_LOCK_DURATION:15m} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java index 52c150b75..477d67bc5 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java @@ -247,7 +247,7 @@ public class InboundOrderRepository { Function.identity(), (left, right) -> left)); } - private Map locationCodesMap(Long tenantId, Map tenantByInbound) { + public Map locationCodesMap(Long tenantId, Map tenantByInbound) { if (tenantByInbound.isEmpty()) { return Map.of(); } 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 618375789..ea7358e8f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundPalletAllocationRepository.java @@ -33,10 +33,13 @@ public class InboundPalletAllocationRepository { .eq(InboundPalletAllocationEntity::getInboundId, inboundId)); LocalDateTime now = LocalDateTime.now(); int allocationCount = result.allocations().size(); - int perAllocation = allocationCount == 0 ? 0 - : (int) Math.ceil((double) totalPackageCount / allocationCount); - int remaining = totalPackageCount; - for (var source : result.allocations()) { + 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++) { + var source = result.allocations().get(index); InboundPalletAllocationEntity value = new InboundPalletAllocationEntity(); value.setTenantId(tenantId); value.setInboundId(inboundId); @@ -47,10 +50,9 @@ public class InboundPalletAllocationRepository { value.setPalletCode(source.palletCode()); value.setPalletCapacityCbm(capacity); value.setAllocatedVolumeCbm(source.allocatedVolumeCbm()); - int allocatedPackages = Math.min(perAllocation, remaining); + int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0); value.setAllocatedPackageCount(allocatedPackages); value.setPickedPackageCount(0); - remaining -= allocatedPackages; value.setLocationId(source.locationId()); value.setLocationCode(source.locationCode()); value.setReservationId(source.reservationId()); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelay.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelay.java index 53c60aa92..a13f4180e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelay.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelay.java @@ -7,6 +7,7 @@ import com.baiyue.firstleg.entity.InboundOutboxEntity; import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository; import com.fasterxml.jackson.databind.ObjectMapper; import feign.FeignException; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -32,19 +33,19 @@ public class FinanceFeeSourceOutboxRelay { FinanceFeeSourceOutboxService.ADJUSTED, FinanceFeeSourceOutboxService.VOIDED); - private final FinanceFeeSourceOutboxRepository repository; - private final FinanceFeeSourceEventClient client; - private final ObjectMapper objectMapper; - private final Clock clock; - private final String workerId; + @Resource + private FinanceFeeSourceOutboxRepository repository; + @Resource + private FinanceFeeSourceEventClient client; + @Resource + private ObjectMapper objectMapper; + private Clock clock = Clock.systemDefaultZone(); + private String workerId = UUID.randomUUID().toString(); @Value("${baiyue.security.internal-service-token}") private String internalToken; @Autowired - public FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository, - FinanceFeeSourceEventClient client, - ObjectMapper objectMapper) { - this(repository, client, objectMapper, Clock.systemDefaultZone(), UUID.randomUUID().toString()); + public FinanceFeeSourceOutboxRelay() { } FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository, 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 e3cba658e..1b9afbf20 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.UUID; @Service public class MeasurementLocationOccupationService { @@ -38,7 +39,10 @@ public class MeasurementLocationOccupationService { long warehouseId = requireWarehouse(order); long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID; BigDecimal volume = requireVolume(order); - String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion; + // 补偿释放后的预占记录需要保留审计,失败重试必须使用新的物理预占键; + // 若仍存在活动预占,仓储侧会按业务单据复用,不会创建新的资源占用。 + String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion + + "-" + UUID.randomUUID(); WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve( internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId, SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java index 9fd9ea4ac..bbc021cab 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java @@ -79,7 +79,7 @@ public class UnifiedInboundQueryService { } InboundTransportSnapshotEntity transport = includePhaseThree ? operations.findTransport(tenantId, inbound.getId()) : null; - return new UnifiedInboundDetail(summary(inbound, detail, preload, pick, evidence, deliveryAt, transport), timeline); + return new UnifiedInboundDetail(summary(inbound, detail, preload, pick, evidence, deliveryAt, transport, null), timeline); } private List summaries(List records) { @@ -92,17 +92,20 @@ public class UnifiedInboundQueryService { var evidences = operations.findEvidence(phaseThreeOwners); var deliveries = operations.findDeliveryHistory(phaseThreeOwners); var transports = operations.findTransports(phaseThreeOwners); + Map owners = records.stream().collect(Collectors.toMap( + InboundOrderEntity::getId, InboundOrderEntity::getTenantId, (left, right) -> left)); + var locationCodes = inbounds.locationCodesMap(null, owners); return records.stream().map(inbound -> summary(inbound, null, preloads.get(inbound.getId()), picks.get(inbound.getId()), evidences.getOrDefault(inbound.getId(), List.of()), deliveries.get(inbound.getId()) == null ? null : deliveries.get(inbound.getId()).getOccurredAt(), - transports.get(inbound.getId()))).toList(); + transports.get(inbound.getId()), locationCodes.get(inbound.getId()))).toList(); } private UnifiedInboundSummary summary(InboundOrderEntity inbound, InboundOrderDetail detail, com.baiyue.firstleg.split.entity.PreloadPlanEntity preload, com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity pick, List evidence, LocalDateTime deliveryAt, - InboundTransportSnapshotEntity transport) { + InboundTransportSnapshotEntity transport, String locationCodes) { String mainStatus = mainStatus(inbound.getProcessStatus()); List activeEvidence = evidence.stream() .filter(item -> item.getStatus() == null || "ACTIVE".equals(item.getStatus())).toList(); @@ -121,7 +124,7 @@ public class UnifiedInboundQueryService { "DELIVERED".equals(mainStatus) ? "COMPLETED" : "PENDING", deliveryAt, inbound.getExpressTrackingNo()) : null; var inboundSummary = detail == null - ? com.baiyue.firstleg.dto.InboundOrderSummary.from(inbound, transport, null) : detail.summary(); + ? com.baiyue.firstleg.dto.InboundOrderSummary.from(inbound, transport, locationCodes) : detail.summary(); return new UnifiedInboundSummary(inboundSummary, mainStatus, nextAction(mainStatus, inbound.getProcessStatus(), preload == null ? null : preload.getStatus()), preloadSummary, loadingSummary, deliverySummary); 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 3376805ad..a355c4eb9 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java @@ -58,7 +58,7 @@ class MeasurementLocationOccupationServiceTest { verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture()); assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT); assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND); - assertThat(reserve.getValue().idempotencyKey()).isEqualTo("measure-88-30-3"); + assertThat(reserve.getValue().idempotencyKey()).startsWith("measure-88-30-3-"); ArgumentCaptor confirm = ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class); diff --git a/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java b/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java index 7dd7f1ec8..cabdcfbab 100644 --- a/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java +++ b/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java @@ -46,7 +46,8 @@ public interface FirstlegUnifiedOrderClient { Integer version, Long customerId, String customerName, Long warehouseId, String warehouseName, Long deliveryWarehouseId, String deliveryWarehouseName, String channelName, - Integer totalPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm, + Integer totalPackageCount, String locationCodes, + BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm, BigDecimal chargeableWeightKg, LocalDateTime measureCompletedAt, String loadingStatus, String containerNo, String cabinetNo, String vesselName, String voyageNo, LocalDateTime sailingAt, LocalDateTime portArrivalAt, diff --git a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java index 5776b1f4d..bd7f1e00b 100644 --- a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java +++ b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java @@ -18,7 +18,8 @@ public final class UnifiedOrderResponse { Long receivingWarehouseId, String receivingWarehouseName, Long destinationWarehouseId, String destinationWarehouseName, Long inboundId, Integer inboundVersion, String inboundCode, LocalDate expectedShipDate, - Integer totalPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm, + Integer totalPackageCount, String recommendedPalletType, String locationCodes, + BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm, MeasurementSummary measurement, PreloadSummary preload, LoadingSummary loading, DeliverySummary delivery, TransportSummary transport, String customerRemark, String latestInternalNote, diff --git a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java index b1e7ac7eb..aa95828c4 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -140,7 +140,7 @@ public class UnifiedOrderQueryService { order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(), order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null, order.getInboundCode(), - order.getExpectedShipDate(), null, null, null, null, null, null, null, null, + order.getExpectedShipDate(), null, null, null, null, null, null, null, null, null, null, order.getCustomerRemark(), latestInternalNote, order.getCreatedAt(), order.getUpdatedAt()); } @@ -149,8 +149,9 @@ public class UnifiedOrderQueryService { summary.mainStatus(), summary.nextAction(), order.getStatus(), order.getCustomerId(), order.getCustomerName(), order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(), order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(), - order.getExpectedShipDate(), inbound.totalPackageCount(), inbound.actualGrossWeightKg(), - inbound.actualVolumeCbm(), measurement(inbound), preload(summary.preload()), loading(summary.loading()), + order.getExpectedShipDate(), inbound.totalPackageCount(), "STANDARD", inbound.locationCodes(), + inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(), measurement(inbound), + preload(summary.preload()), loading(summary.loading()), delivery(summary.delivery()), transport(inbound), order.getCustomerRemark(), latestInternalNote, order.getCreatedAt(), order.getUpdatedAt()); } @@ -162,7 +163,8 @@ public class UnifiedOrderQueryService { summary.mainStatus(), summary.nextAction(), parseOrderStatus(inbound.orderStatusSnapshot()), inbound.customerId(), inbound.customerName(), inbound.channelName(), inbound.warehouseId(), inbound.warehouseName(), inbound.deliveryWarehouseId(), inbound.deliveryWarehouseName(), - inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.totalPackageCount(), inbound.actualGrossWeightKg(), + inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.totalPackageCount(), + "STANDARD", inbound.locationCodes(), inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(), measurement(inbound), preload(summary.preload()), loading(summary.loading()), delivery(summary.delivery()), transport(inbound), order == null ? null : order.getCustomerRemark(), latestInternalNote, inbound.createdAt(), inbound.updatedAt()); diff --git a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java index df66eceef..c2ffc4cfe 100644 --- a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java @@ -136,6 +136,8 @@ class UnifiedOrderQueryServiceTest { assertThat(row.loading().loadingVersion()).isEqualTo(3); assertThat(row.loading().pickingVersion()).isEqualTo(3); assertThat(row.inboundCode()).isEqualTo("RK-77"); + assertThat(row.recommendedPalletType()).isEqualTo("STANDARD"); + assertThat(row.locationCodes()).isEqualTo("A0-01, B2-03"); }); verify(orders, never()).unifiedPage(any(), any(), any()); } @@ -244,7 +246,7 @@ class UnifiedOrderQueryServiceTest { private FirstlegUnifiedOrderClient.Summary summary(String status, String action) { var inbound = new FirstlegUnifiedOrderClient.Inbound( 88L, 10001L, 77L, "FL-77", "RK-77", "INBOUNDED", "APPROVED", "APPROVED", 7, - 30001L, "测试客户", 11L, "深圳仓", 22L, "ONT8", "美森", 10, null, null, null, + 30001L, "测试客户", 11L, "深圳仓", 22L, "ONT8", "美森", 10, "A0-01, B2-03", null, null, null, null, null, null, null, null, null, null, null, null, LocalDateTime.of(2026, 8, 6, 10, 0), LocalDateTime.of(2026, 8, 6, 11, 0)); return new FirstlegUnifiedOrderClient.Summary(inbound, status, action, 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 0679b4a73..ff18b7a40 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/service/PalletLocationAllocationService.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/service/PalletLocationAllocationService.java @@ -108,13 +108,15 @@ public class PalletLocationAllocationService { if (active.size() != split.palletCount()) { throw new BizException(ResultCode.CONFLICT, "活动预占数量与托盘拆分数量不一致"); } + if (active.size() > request.packageCount()) { + throw new BizException(ResultCode.CONFLICT, "托盘数量超过入库单总箱数,无法为每个托盘分配至少一箱"); + } active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId)); List targets = new ArrayList<>(active.size()); - int perAllocation = (int) Math.ceil((double) request.packageCount() / active.size()); - int remainingPackages = request.packageCount(); + int basePackageCount = request.packageCount() / active.size(); + int remainder = request.packageCount() % active.size(); for (int index = 0; index < active.size(); index++) { - int allocatedPackages = Math.min(perAllocation, remainingPackages); - remainingPackages -= allocatedPackages; + int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0); targets.add(new LocationDtos.OccupancyTarget(active.get(index).reservationId(), split.allocations().get(index).allocatedVolumeCbm(), allocatedPackages)); } 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 13e037811..81ebd4c76 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/service/PalletLocationAllocationServiceTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/service/PalletLocationAllocationServiceTest.java @@ -21,6 +21,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -140,6 +141,49 @@ class PalletLocationAllocationServiceTest { .isEqualTo(20); } + @Test + void confirmDistributesRemainderWithoutZeroPackageAllocations() { + List resources = new ArrayList<>(); + List occupancyResults = new ArrayList<>(); + for (int index = 1; index <= 10; index++) { + long reservationId = 100L + index; + long palletId = 200L + index; + long locationId = 300L + index; + resources.add(new LocationDtos.ActiveResource(reservationId, null, 20L, palletId, + "TP-" + index, locationId, "A-" + index, false, "RESERVED")); + occupancyResults.add(new LocationDtos.OccupancyResult(400L + index, "OC-" + index, + reservationId, palletId, locationId, "A-" + index, false)); + when(pallets.transitionStatus(10001L, 20L, palletId, List.of("RESERVED"), "OCCUPIED")) + .thenReturn(1); + } + 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)); + + 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()); + } + @Test void shouldReleaseOnlyPalletsReturnedBySelectedLocationRelease() { var resource = new LocationDtos.ExactSelectiveReleaseResult(