装柜修改

This commit is contained in:
2026-08-11 20:00:31 +08:00
parent 48bb752007
commit bed917ba6c
13 changed files with 96 additions and 34 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ baiyue.security:
internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long} 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} token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth}
issuer: ${JWT_ISSUER:auth-service} 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} refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d}
max-login-failures: ${MAX_LOGIN_FAILURES:5} max-login-failures: ${MAX_LOGIN_FAILURES:5}
lock-duration: ${LOGIN_LOCK_DURATION:15m} lock-duration: ${LOGIN_LOCK_DURATION:15m}
@@ -247,7 +247,7 @@ public class InboundOrderRepository {
Function.identity(), (left, right) -> left)); Function.identity(), (left, right) -> left));
} }
private Map<Long, String> locationCodesMap(Long tenantId, Map<Long, Long> tenantByInbound) { public Map<Long, String> locationCodesMap(Long tenantId, Map<Long, Long> tenantByInbound) {
if (tenantByInbound.isEmpty()) { if (tenantByInbound.isEmpty()) {
return Map.of(); return Map.of();
} }
@@ -33,10 +33,13 @@ public class InboundPalletAllocationRepository {
.eq(InboundPalletAllocationEntity::getInboundId, inboundId)); .eq(InboundPalletAllocationEntity::getInboundId, inboundId));
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
int allocationCount = result.allocations().size(); int allocationCount = result.allocations().size();
int perAllocation = allocationCount == 0 ? 0 if (allocationCount > totalPackageCount) {
: (int) Math.ceil((double) totalPackageCount / allocationCount); throw new IllegalArgumentException("托盘数量不能超过入库单总箱数");
int remaining = totalPackageCount; }
for (var source : result.allocations()) { 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(); InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
value.setTenantId(tenantId); value.setTenantId(tenantId);
value.setInboundId(inboundId); value.setInboundId(inboundId);
@@ -47,10 +50,9 @@ public class InboundPalletAllocationRepository {
value.setPalletCode(source.palletCode()); value.setPalletCode(source.palletCode());
value.setPalletCapacityCbm(capacity); value.setPalletCapacityCbm(capacity);
value.setAllocatedVolumeCbm(source.allocatedVolumeCbm()); value.setAllocatedVolumeCbm(source.allocatedVolumeCbm());
int allocatedPackages = Math.min(perAllocation, remaining); int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0);
value.setAllocatedPackageCount(allocatedPackages); value.setAllocatedPackageCount(allocatedPackages);
value.setPickedPackageCount(0); value.setPickedPackageCount(0);
remaining -= allocatedPackages;
value.setLocationId(source.locationId()); value.setLocationId(source.locationId());
value.setLocationCode(source.locationCode()); value.setLocationCode(source.locationCode());
value.setReservationId(source.reservationId()); value.setReservationId(source.reservationId());
@@ -7,6 +7,7 @@ import com.baiyue.firstleg.entity.InboundOutboxEntity;
import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository; import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException; import feign.FeignException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -32,19 +33,19 @@ public class FinanceFeeSourceOutboxRelay {
FinanceFeeSourceOutboxService.ADJUSTED, FinanceFeeSourceOutboxService.ADJUSTED,
FinanceFeeSourceOutboxService.VOIDED); FinanceFeeSourceOutboxService.VOIDED);
private final FinanceFeeSourceOutboxRepository repository; @Resource
private final FinanceFeeSourceEventClient client; private FinanceFeeSourceOutboxRepository repository;
private final ObjectMapper objectMapper; @Resource
private final Clock clock; private FinanceFeeSourceEventClient client;
private final String workerId; @Resource
private ObjectMapper objectMapper;
private Clock clock = Clock.systemDefaultZone();
private String workerId = UUID.randomUUID().toString();
@Value("${baiyue.security.internal-service-token}") @Value("${baiyue.security.internal-service-token}")
private String internalToken; private String internalToken;
@Autowired @Autowired
public FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository, public FinanceFeeSourceOutboxRelay() {
FinanceFeeSourceEventClient client,
ObjectMapper objectMapper) {
this(repository, client, objectMapper, Clock.systemDefaultZone(), UUID.randomUUID().toString());
} }
FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository, FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository,
@@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID;
@Service @Service
public class MeasurementLocationOccupationService { public class MeasurementLocationOccupationService {
@@ -38,7 +39,10 @@ public class MeasurementLocationOccupationService {
long warehouseId = requireWarehouse(order); long warehouseId = requireWarehouse(order);
long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID; long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID;
BigDecimal volume = requireVolume(order); 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( WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve(
internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId, internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId,
SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume, SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume,
@@ -79,7 +79,7 @@ public class UnifiedInboundQueryService {
} }
InboundTransportSnapshotEntity transport = includePhaseThree InboundTransportSnapshotEntity transport = includePhaseThree
? operations.findTransport(tenantId, inbound.getId()) : null; ? 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<UnifiedInboundSummary> summaries(List<InboundOrderEntity> records) { private List<UnifiedInboundSummary> summaries(List<InboundOrderEntity> records) {
@@ -92,17 +92,20 @@ public class UnifiedInboundQueryService {
var evidences = operations.findEvidence(phaseThreeOwners); var evidences = operations.findEvidence(phaseThreeOwners);
var deliveries = operations.findDeliveryHistory(phaseThreeOwners); var deliveries = operations.findDeliveryHistory(phaseThreeOwners);
var transports = operations.findTransports(phaseThreeOwners); var transports = operations.findTransports(phaseThreeOwners);
Map<Long, Long> 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()), return records.stream().map(inbound -> summary(inbound, null, preloads.get(inbound.getId()),
picks.get(inbound.getId()), evidences.getOrDefault(inbound.getId(), List.of()), picks.get(inbound.getId()), evidences.getOrDefault(inbound.getId(), List.of()),
deliveries.get(inbound.getId()) == null ? null : deliveries.get(inbound.getId()).getOccurredAt(), 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, private UnifiedInboundSummary summary(InboundOrderEntity inbound, InboundOrderDetail detail,
com.baiyue.firstleg.split.entity.PreloadPlanEntity preload, com.baiyue.firstleg.split.entity.PreloadPlanEntity preload,
com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity pick, com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity pick,
List<LoadingEvidenceEntity> evidence, LocalDateTime deliveryAt, List<LoadingEvidenceEntity> evidence, LocalDateTime deliveryAt,
InboundTransportSnapshotEntity transport) { InboundTransportSnapshotEntity transport, String locationCodes) {
String mainStatus = mainStatus(inbound.getProcessStatus()); String mainStatus = mainStatus(inbound.getProcessStatus());
List<LoadingEvidenceEntity> activeEvidence = evidence.stream() List<LoadingEvidenceEntity> activeEvidence = evidence.stream()
.filter(item -> item.getStatus() == null || "ACTIVE".equals(item.getStatus())).toList(); .filter(item -> item.getStatus() == null || "ACTIVE".equals(item.getStatus())).toList();
@@ -121,7 +124,7 @@ public class UnifiedInboundQueryService {
"DELIVERED".equals(mainStatus) ? "COMPLETED" : "PENDING", deliveryAt, "DELIVERED".equals(mainStatus) ? "COMPLETED" : "PENDING", deliveryAt,
inbound.getExpressTrackingNo()) : null; inbound.getExpressTrackingNo()) : null;
var inboundSummary = detail == 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, return new UnifiedInboundSummary(inboundSummary, mainStatus,
nextAction(mainStatus, inbound.getProcessStatus(), preload == null ? null : preload.getStatus()), nextAction(mainStatus, inbound.getProcessStatus(), preload == null ? null : preload.getStatus()),
preloadSummary, loadingSummary, deliverySummary); preloadSummary, loadingSummary, deliverySummary);
@@ -58,7 +58,7 @@ class MeasurementLocationOccupationServiceTest {
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture()); verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT); assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND); 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<WarehousePalletLocationClient.ConfirmRequest> confirm = ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class); ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
@@ -46,7 +46,8 @@ public interface FirstlegUnifiedOrderClient {
Integer version, Integer version,
Long customerId, String customerName, Long warehouseId, String warehouseName, Long customerId, String customerName, Long warehouseId, String warehouseName,
Long deliveryWarehouseId, String deliveryWarehouseName, String channelName, Long deliveryWarehouseId, String deliveryWarehouseName, String channelName,
Integer totalPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm, Integer totalPackageCount, String locationCodes,
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
BigDecimal chargeableWeightKg, LocalDateTime measureCompletedAt, BigDecimal chargeableWeightKg, LocalDateTime measureCompletedAt,
String loadingStatus, String containerNo, String cabinetNo, String vesselName, String voyageNo, String loadingStatus, String containerNo, String cabinetNo, String vesselName, String voyageNo,
LocalDateTime sailingAt, LocalDateTime portArrivalAt, LocalDateTime sailingAt, LocalDateTime portArrivalAt,
@@ -18,7 +18,8 @@ public final class UnifiedOrderResponse {
Long receivingWarehouseId, String receivingWarehouseName, Long receivingWarehouseId, String receivingWarehouseName,
Long destinationWarehouseId, String destinationWarehouseName, Long destinationWarehouseId, String destinationWarehouseName,
Long inboundId, Integer inboundVersion, String inboundCode, LocalDate expectedShipDate, 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, MeasurementSummary measurement, PreloadSummary preload,
LoadingSummary loading, DeliverySummary delivery, TransportSummary transport, LoadingSummary loading, DeliverySummary delivery, TransportSummary transport,
String customerRemark, String latestInternalNote, String customerRemark, String latestInternalNote,
@@ -140,7 +140,7 @@ public class UnifiedOrderQueryService {
order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(), order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null, order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null,
order.getInboundCode(), 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.getCustomerRemark(), latestInternalNote,
order.getCreatedAt(), order.getUpdatedAt()); order.getCreatedAt(), order.getUpdatedAt());
} }
@@ -149,8 +149,9 @@ public class UnifiedOrderQueryService {
summary.mainStatus(), summary.nextAction(), order.getStatus(), order.getCustomerId(), order.getCustomerName(), summary.mainStatus(), summary.nextAction(), order.getStatus(), order.getCustomerId(), order.getCustomerName(),
order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(), order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(), order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(),
order.getExpectedShipDate(), inbound.totalPackageCount(), inbound.actualGrossWeightKg(), order.getExpectedShipDate(), inbound.totalPackageCount(), "STANDARD", inbound.locationCodes(),
inbound.actualVolumeCbm(), measurement(inbound), preload(summary.preload()), loading(summary.loading()), inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(), measurement(inbound),
preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), order.getCustomerRemark(), latestInternalNote, delivery(summary.delivery()), transport(inbound), order.getCustomerRemark(), latestInternalNote,
order.getCreatedAt(), order.getUpdatedAt()); order.getCreatedAt(), order.getUpdatedAt());
} }
@@ -162,7 +163,8 @@ public class UnifiedOrderQueryService {
summary.mainStatus(), summary.nextAction(), parseOrderStatus(inbound.orderStatusSnapshot()), summary.mainStatus(), summary.nextAction(), parseOrderStatus(inbound.orderStatusSnapshot()),
inbound.customerId(), inbound.customerName(), inbound.channelName(), inbound.warehouseId(), inbound.customerId(), inbound.customerName(), inbound.channelName(), inbound.warehouseId(),
inbound.warehouseName(), inbound.deliveryWarehouseId(), inbound.deliveryWarehouseName(), 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()), inbound.actualVolumeCbm(), measurement(inbound), preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), order == null ? null : order.getCustomerRemark(), latestInternalNote, delivery(summary.delivery()), transport(inbound), order == null ? null : order.getCustomerRemark(), latestInternalNote,
inbound.createdAt(), inbound.updatedAt()); inbound.createdAt(), inbound.updatedAt());
@@ -136,6 +136,8 @@ class UnifiedOrderQueryServiceTest {
assertThat(row.loading().loadingVersion()).isEqualTo(3); assertThat(row.loading().loadingVersion()).isEqualTo(3);
assertThat(row.loading().pickingVersion()).isEqualTo(3); assertThat(row.loading().pickingVersion()).isEqualTo(3);
assertThat(row.inboundCode()).isEqualTo("RK-77"); 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()); verify(orders, never()).unifiedPage(any(), any(), any());
} }
@@ -244,7 +246,7 @@ class UnifiedOrderQueryServiceTest {
private FirstlegUnifiedOrderClient.Summary summary(String status, String action) { private FirstlegUnifiedOrderClient.Summary summary(String status, String action) {
var inbound = new FirstlegUnifiedOrderClient.Inbound( var inbound = new FirstlegUnifiedOrderClient.Inbound(
88L, 10001L, 77L, "FL-77", "RK-77", "INBOUNDED", "APPROVED", "APPROVED", 7, 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, null, null, null, null, null, null, null, null, null,
LocalDateTime.of(2026, 8, 6, 10, 0), LocalDateTime.of(2026, 8, 6, 11, 0)); LocalDateTime.of(2026, 8, 6, 10, 0), LocalDateTime.of(2026, 8, 6, 11, 0));
return new FirstlegUnifiedOrderClient.Summary(inbound, status, action, return new FirstlegUnifiedOrderClient.Summary(inbound, status, action,
@@ -108,13 +108,15 @@ public class PalletLocationAllocationService {
if (active.size() != split.palletCount()) { if (active.size() != split.palletCount()) {
throw new BizException(ResultCode.CONFLICT, "活动预占数量与托盘拆分数量不一致"); throw new BizException(ResultCode.CONFLICT, "活动预占数量与托盘拆分数量不一致");
} }
if (active.size() > request.packageCount()) {
throw new BizException(ResultCode.CONFLICT, "托盘数量超过入库单总箱数,无法为每个托盘分配至少一箱");
}
active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId)); active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId));
List<LocationDtos.OccupancyTarget> targets = new ArrayList<>(active.size()); List<LocationDtos.OccupancyTarget> targets = new ArrayList<>(active.size());
int perAllocation = (int) Math.ceil((double) request.packageCount() / active.size()); int basePackageCount = request.packageCount() / active.size();
int remainingPackages = request.packageCount(); int remainder = request.packageCount() % active.size();
for (int index = 0; index < active.size(); index++) { for (int index = 0; index < active.size(); index++) {
int allocatedPackages = Math.min(perAllocation, remainingPackages); int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0);
remainingPackages -= allocatedPackages;
targets.add(new LocationDtos.OccupancyTarget(active.get(index).reservationId(), targets.add(new LocationDtos.OccupancyTarget(active.get(index).reservationId(),
split.allocations().get(index).allocatedVolumeCbm(), allocatedPackages)); split.allocations().get(index).allocatedVolumeCbm(), allocatedPackages));
} }
@@ -21,6 +21,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -140,6 +141,49 @@ class PalletLocationAllocationServiceTest {
.isEqualTo(20); .isEqualTo(20);
} }
@Test
void confirmDistributesRemainderWithoutZeroPackageAllocations() {
List<LocationDtos.ActiveResource> resources = new ArrayList<>();
List<LocationDtos.OccupancyResult> 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<LocationDtos.ConfirmCommand> 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<LocationDtos.ActiveResource> 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 @Test
void shouldReleaseOnlyPalletsReturnedBySelectedLocationRelease() { void shouldReleaseOnlyPalletsReturnedBySelectedLocationRelease() {
var resource = new LocationDtos.ExactSelectiveReleaseResult( var resource = new LocationDtos.ExactSelectiveReleaseResult(