This commit is contained in:
2026-08-22 11:47:12 +08:00
3 changed files with 56 additions and 17 deletions
@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -254,8 +255,10 @@ public class PdaPickSettlementService {
throw conflict("入仓单缺少真实订单引用");
}
List<OrderFulfillmentSplitClient.QuantityLine> quantities = lines.stream()
.map(line -> new OrderFulfillmentSplitClient.QuantityLine(
line.item().inboundDetailId(), line.item().packageCount()))
.collect(Collectors.groupingBy(line -> line.item().inboundDetailId(), LinkedHashMap::new,
Collectors.summingInt(line -> count(line.item().packageCount(), "测量明细箱数"))))
.entrySet().stream()
.map(entry -> new OrderFulfillmentSplitClient.QuantityLine(entry.getKey(), entry.getValue()))
.toList();
OrderFulfillmentSplitClient.CreateRequest request = new OrderFulfillmentSplitClient.CreateRequest(
source.tenantId(), root.orderId(), source.orderId(), loadingPlanId, containerBookingId,
@@ -333,15 +336,18 @@ public class PdaPickSettlementService {
if (!measuredDetailIds.containsAll(pickedByDetail.keySet())) {
throw conflict("PDA 已拣明细未匹配已审核测量明细");
}
Map<Long, Integer> remainingPickedByDetail = new HashMap<>(pickedByDetail);
List<MeasurementSplitLine> result = new ArrayList<>();
for (MeasurementItemRow item : measurements) {
int measured = count(item.packageCount(), "测量明细箱数");
int picked = pickedByDetail.getOrDefault(item.inboundDetailId(), 0);
if (picked > measured) {
throw conflict("PDA 已拣箱数超过测量明细箱数");
}
int requested = remainingPickedByDetail.getOrDefault(item.inboundDetailId(), 0);
int picked = Math.min(measured, requested);
remainingPickedByDetail.put(item.inboundDetailId(), requested - picked);
result.add(new MeasurementSplitLine(item, picked, measured - picked));
}
if (remainingPickedByDetail.values().stream().anyMatch(remaining -> remaining != 0)) {
throw conflict("PDA 已拣箱数超过测量明细箱数");
}
return result;
}
@@ -259,18 +259,16 @@ public class SmartLoadingService {
if (replay != null) return replay;
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
for (Candidate candidate : candidates(booking.getWarehouseId())) {
if (!Objects.equals(candidate.warehouseId(), booking.getWarehouseId())) continue;
if (!sameChannel(booking, candidate)) continue;
if (!sameCustomerScope(booking, candidate)) continue;
for (InboundCandidate candidate : inboundCandidates(plan.getContainerBookingId(), booking.getWarehouseId())) {
if (StringUtils.hasText(booking.getDestinationWarehouseCode())
&& !Objects.equals(booking.getDestinationWarehouseCode(), candidate.destinationWarehouseCode())) continue;
int count = fittingBoxes(booking, repository.items(planId), candidate);
int count = fittingBoxes(booking, repository.items(planId), candidate.availablePackageCount(),
candidate.averageUnitVolumeCbm(), candidate.unitGrossWeightKg());
if (count > 0) {
plan = repository.plan(planId);
assign(planId, new AssignRequest(candidate.inboundId(), candidate.inboundDetailId(),
count, plan.getVersion(), "REC-" + sha256(operation + '|' + candidate.inboundId()
+ '|' + candidate.inboundDetailId())), operatorId, operatorName);
assignInbound(planId, new AssignInboundRequest(candidate.inboundId(), count,
plan.getVersion(), "REC-INB-" + sha256(operation + '|' + candidate.inboundId())
.substring(0, 48)), operatorId, operatorName);
}
}
LoadingPlanEntity recommended = repository.plan(planId);
@@ -510,14 +508,18 @@ public class SmartLoadingService {
}
}
private int fittingBoxes(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c){
return fittingBoxes(b, lines, c.availablePackageCount(), c.unitVolumeCbm(), c.unitGrossWeightKg());
}
private int fittingBoxes(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,int available,
BigDecimal unitVolume,BigDecimal unitWeight){
if(b.getCapacityCbm()==null||b.getMaxWeightKg()==null)return 0;
BigDecimal usedV=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add);
BigDecimal usedW=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add);
BigDecimal leftV=b.getCapacityCbm().multiply(CAPACITY_LIMIT_RATIO).subtract(usedV);
BigDecimal leftW=b.getMaxWeightKg().multiply(CAPACITY_LIMIT_RATIO).subtract(usedW);
int byV=unit(c.unitVolumeCbm()).signum()==0?c.availablePackageCount():leftV.max(BigDecimal.ZERO).divideToIntegralValue(c.unitVolumeCbm()).intValue();
int byW=unit(c.unitGrossWeightKg()).signum()==0?c.availablePackageCount():leftW.max(BigDecimal.ZERO).divideToIntegralValue(c.unitGrossWeightKg()).intValue();
return Math.max(0,Math.min(c.availablePackageCount(),Math.min(byV,byW)));
int byV=unit(unitVolume).signum()==0?available:leftV.max(BigDecimal.ZERO).divideToIntegralValue(unitVolume).intValue();
int byW=unit(unitWeight).signum()==0?available:leftW.max(BigDecimal.ZERO).divideToIntegralValue(unitWeight).intValue();
return Math.max(0,Math.min(available,Math.min(byV,byW)));
}
private Plan view(LoadingPlanEntity p,ContainerBookingEntity b){
var task = loadingPickService.mutationSnapshot(p.getId());
@@ -79,6 +79,37 @@ class PdaPickSettlementServiceTest {
verify(splits).convergePartialPlanItem(21L, 9L, "operator");
}
@Test
void distributesOnePickedCartonAcrossRepeatedMeasurementRowsOfTheSameInboundDetail() {
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 1, 1, "PICKED");
resolved(List.of(item));
when(splits.lockParent(31L)).thenReturn(parent(2));
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L)).thenReturn(List.of(
new MeasurementItemRow(51L, 61L, 41L, 1),
new MeasurementItemRow(52L, 61L, 41L, 1)));
when(allocations.lockConfirmed(10001L, 31L)).thenAnswer(invocation ->
List.of(allocation(1L, 1, 501L, 2)));
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 1)))));
writes(1, 1);
when(splits.insertSplitItem(anyLong(), eq(10001L), anyLong(), eq(31L), anyLong(), anyInt(),
eq(41L), eq(61L), eq(52L), eq(1))).thenReturn(1);
when(splits.cloneMeasurementItem(eq(10001L), eq(61L), eq(52L), anyLong(),
anyLong(), anyLong(), anyInt(), eq(1))).thenReturn(1);
when(splits.updateMeasurementItemForLoaded(10001L, 52L, 0)).thenReturn(1);
service.settle(1L, 9L, "operator");
verify(splits).updateMeasurementItemForLoaded(10001L, 51L, 1);
verify(splits).updateMeasurementItemForLoaded(10001L, 52L, 0);
verify(splits).cloneMeasurementItem(eq(10001L), eq(61L), eq(52L), anyLong(),
anyLong(), anyLong(), anyInt(), eq(1));
verify(orderSplits).split(eq("internal-token"), eq(9L),
org.mockito.ArgumentMatchers.argThat(request -> request.lines().size() == 1
&& request.lines().get(0).sourceDetailId() == 41L
&& request.lines().get(0).packageCount() == 2));
}
@Test
void reusesWarehouseSettlementTargetWhenRetryingAfterLocalRollback() {
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 3, 1, "PARTIAL");