From 98d15c30bbfee60407753dbb0bf170595eb3e274 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 18 Sep 2026 15:23:36 +0800 Subject: [PATCH] fix: carry partial dropship measurements to outbound --- .../LoadingCompletedOutboxServiceTest.java | 26 +++++++++++ .../delivery/dto/DeliveryResponses.java | 3 +- .../service/DeliveryTransferService.java | 46 ++++++++++++++++--- .../mapper/DropshipOrderProfileMapper.java | 8 +++- .../model/DropshipOutboundCandidate.java | 7 +++ .../openapi/overseas-business-v1.yaml | 5 +- .../service/DeliveryTransferServiceTest.java | 43 ++++++++++++++++- 7 files changed, 127 insertions(+), 11 deletions(-) diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java index 139c3101e..221f91864 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java @@ -203,6 +203,32 @@ class LoadingCompletedOutboxServiceTest { .isEqualTo(second.payload().at("/orders/0/items/0/cartonMarks")); } + @Test void publishesOnlyTheActuallyLoadedFragmentMeasurements() throws Exception { + Row loadedFragment = row(11, 100, 101); + loadedFragment.setPackageCount(4); + loadedFragment.setGrossWeightKg(new BigDecimal("40")); + loadedFragment.setNetWeightKg(new BigDecimal("36")); + loadedFragment.setDimensionalWeightKg(new BigDecimal("80")); + loadedFragment.setVolumeCbm(new BigDecimal("4")); + when(snapshots.rows(90L)).thenReturn(List.of(loadedFragment)); + when(manifests.labelPool(11L, 100L)).thenReturn(List.of( + label(101, "BOX-001"), label(101, "BOX-002"), + label(101, "BOX-003"), label(101, "BOX-004"))); + + service.publish(90L); + + ArgumentCaptor captor = ArgumentCaptor.forClass(InboundOutboxEntity.class); + verify(outbox).insert(captor.capture()); + EventEnvelope event = mapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class); + assertThat(event.payload().at("/orders/0/packageCount").asInt()).isEqualTo(4); + assertThat(event.payload().at("/orders/0/grossWeightKg").decimalValue()).isEqualByComparingTo("40"); + assertThat(event.payload().at("/orders/0/volumeWeightKg").decimalValue()).isEqualByComparingTo("80"); + assertThat(event.payload().at("/orders/0/volumeCbm").decimalValue()).isEqualByComparingTo("4"); + assertThat(event.payload().at("/orders/0/items/0/packageCount").asInt()).isEqualTo(4); + assertThat(event.payload().at("/orders/0/items/0/grossWeightKg").decimalValue()).isEqualByComparingTo("40"); + assertThat(event.payload().at("/orders/0/items/0/volumeCbm").decimalValue()).isEqualByComparingTo("4"); + } + @Test void immutableCustomerNameParticipatesInPayloadHash() { Row value = row(11, 100, 101); when(snapshots.rows(90L)).thenReturn(List.of(value)); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java index c150e4407..3cbd6917a 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java @@ -59,7 +59,8 @@ public final class DeliveryResponses { public record OutboundDetail(OutboundRow outbound, List deliveries, List pallets) {} public record OneClickCandidate(Long inventoryPositionId, String orderNo, String customerId, String customerName, String overseasWarehouseId, String overseasWarehouseName, String palletNo, String inboundCode, - String skuCode, String productName, long availablePackages) {} + String skuCode, String productName, long availablePackages, + BigDecimal grossWeightKg, BigDecimal volumeWeightKg, BigDecimal volumeCbm) {} public record ReturnOutboundCandidate(String returnNo, String overseasWarehouseId, String cartonMark, String fbaCartonMark, String destinationWarehouseCode, String destinationWarehouseName, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java index eed41070d..5cf0b1f75 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java @@ -76,6 +76,7 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import com.baiyue.lastleg.service.LastlegOrderNoGenerator; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.*; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; @@ -322,8 +323,10 @@ public class DeliveryTransferService { if (concurrent == null) throw duplicate; return replayOneClick(concurrent, requestHash); } - selectedWarehouseId = validateOneClickItems(customerId, request.items()); + OneClickValidation validation = validateOneClickItems(customerId, request.items()); + selectedWarehouseId = validation.warehouseId(); Set physicalPalletIds = new HashSet<>(); + List createdMetricItems = new ArrayList<>(); for (DeliveryRequests.OneClickItem input : request.items()) { InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId()); if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found"); @@ -334,10 +337,21 @@ public class DeliveryTransferService { OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId); item.setOutboundOrderId(outbound.getId()); item.setInboundCode(balance.getInboundCode()); physicalPalletIds.add(position.getPalletId()); - item.setWarehousePalletId(position.getPalletId()); item.setPalletNo(position.getPalletNo()); item.setPackageCount(input.packages().intValue()); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK"); item.setSkuCode(input.sku().trim()); item.setCartonMarks(String.join(",", input.cartonMarks() == null ? List.of() : input.cartonMarks())); item.setCreatedAt(now); + DropshipOutboundCandidate candidate = validation.candidatesByRequest().get( + oneClickCandidateKey(input.inventoryPositionId(), input.sku())); + if (candidate == null) throw conflict("一件代发出库重量体积快照缺失"); + item.setWarehousePalletId(position.getPalletId()); item.setPalletNo(position.getPalletNo()); item.setPackageCount(input.packages().intValue()); + item.setGrossWeightKg(allocateMetric(candidate.getSourceGrossWeightKg(), candidate.getSourcePackageCount(), input.packages(), 3)); + item.setVolumeWeightKg(allocateMetric(candidate.getSourceVolumeWeightKg(), candidate.getSourcePackageCount(), input.packages(), 3)); + item.setVolumeCbm(allocateMetric(candidate.getSourceVolumeCbm(), candidate.getSourcePackageCount(), input.packages(), 4)); + item.setPickedPackageCount(0); item.setStatus("WAIT_PICK"); item.setSkuCode(input.sku().trim()); item.setCartonMarks(String.join(",", input.cartonMarks() == null ? List.of() : input.cartonMarks())); item.setCreatedAt(now); outboundPallets.insert(item); + createdMetricItems.add(item); } outbound.setPalletCount(physicalPalletIds.size()); + outbound.setGrossWeightKg(sum(createdMetricItems, OutboundPalletItem::getGrossWeightKg)); + outbound.setVolumeWeightKg(sum(createdMetricItems, OutboundPalletItem::getVolumeWeightKg)); + outbound.setVolumeCbm(sum(createdMetricItems, OutboundPalletItem::getVolumeCbm)); outbound.setOverseasWarehouseId(selectedWarehouseId); outboundOrders.updateById(outbound); OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setOutboundOrderId(outbound.getId()); task.setTaskNo("PDA" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + task.getId()); task.setStatus("PENDING"); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); pdaTasks.insert(task); @@ -558,10 +572,11 @@ public class DeliveryTransferService { return outboundDetail(outbound.getOutboundNo()); } - private String validateOneClickItems(String customerId, List items) { + private OneClickValidation validateOneClickItems(String customerId, List items) { String warehouseId = null; Map requestedPackages = new LinkedHashMap<>(); Map candidates = new HashMap<>(); + Map candidatesByRequest = new HashMap<>(); Set requestedMarks = new HashSet<>(); for (DeliveryRequests.OneClickItem input : items) { String sku = input.sku().trim(); @@ -577,6 +592,7 @@ public class DeliveryTransferService { String key = candidate.getProfileId() + "\u0000" + sku; requestedPackages.merge(key, input.packages(), Long::sum); candidates.put(key, candidate); + candidatesByRequest.put(oneClickCandidateKey(input.inventoryPositionId(), sku), candidate); if (input.cartonMarks() != null && !input.cartonMarks().isEmpty()) { if (input.cartonMarks().size() != input.packages() || new HashSet<>(input.cartonMarks()).size() != input.cartonMarks().size()) { throw conflict("Specified carton marks must be unique and equal the package count"); @@ -602,7 +618,7 @@ public class DeliveryTransferService { throw conflict("一件代发可用箱数不足:" + candidate.getPalletNo() + " / " + sku); } } - return warehouseId; + return new OneClickValidation(warehouseId, Map.copyOf(candidatesByRequest)); } @Transactional @@ -1185,10 +1201,14 @@ public class DeliveryTransferService { item.getVolumeCbm(), value(item.getPickedPackageCount()), item.getStatus()); } private DeliveryResponses.OneClickCandidate oneClickCandidate(DropshipOutboundCandidate candidate) { + long available = value(candidate.getAvailablePackages()); return new DeliveryResponses.OneClickCandidate(candidate.getInventoryPositionId(), candidate.getOrderNo(), candidate.getCustomerId(), candidate.getCustomerName(), candidate.getOverseasWarehouseId(), candidate.getOverseasWarehouseName(), candidate.getPalletNo(), candidate.getInboundCode(), candidate.getSkuCode(), candidate.getProductName(), - value(candidate.getAvailablePackages())); + available, + allocateMetric(candidate.getSourceGrossWeightKg(), candidate.getSourcePackageCount(), available, 3), + allocateMetric(candidate.getSourceVolumeWeightKg(), candidate.getSourcePackageCount(), available, 3), + allocateMetric(candidate.getSourceVolumeCbm(), candidate.getSourcePackageCount(), available, 4)); } private DeliveryResponses.DeliveryRow deliveryRow(DeliveryOrder d) { return new DeliveryResponses.DeliveryRow(d.getId(), d.getCustomerId(), d.getCustomerName(), d.getDeliveryNo(), d.getOutboundNo(), d.getDispatchNo(), d.getOverseasWarehouseId(), d.getOverseasWarehouseName(), d.getStatus(), d.getWaybillNo(), d.getDeliveryMethod(), d.getCarrierName(), d.getPickupMethod(), d.getLoadingType(), d.getVehicleType(), d.getVehicleSize(), d.getVehiclePlate(), d.getDriverName(), d.getDriverPhone(), d.getAppointmentAccount(), d.getSellerName(), value(d.getPalletCount()), value(d.getPackageCount()), d.getTotalWeightKg(), d.getTotalVolumeCbm(), d.getPlannedDeliveryAt(), d.getDispatchedAt(), d.getExpectedArrivalAt(), d.getSignedAt(), d.getRemark(), d.getCreatedAt(), value(d.getVersion()), DeliveryStatusPolicy.allowedActions(d.getStatus()), d.getBolNo(), com.baiyue.lastleg.delivery.model.DeliveryDocumentSnapshot.summary(d.getDocumentsJson()), d.getDestinationWarehouseId(), d.getDestinationWarehouseCode(), d.getDestinationWarehouseName(), d.getDestinationType(), d.getDeliveryAddress(), d.getContactName(), d.getContactPhone(), d.getPostalCode()); } private DeliveryResponses.DeliverySku sku(LastlegOrderDetail d, LastlegOrder order) { return new DeliveryResponses.DeliverySku(order == null ? null : order.getInboundCode(), order == null ? null : order.getCustomerName(), d.getDestinationWarehouseCode(), null, d.getShipmentNo(), d.getFbaTrackingNo(), d.getSkuCode(), firstText(d.getProductNameZh(), d.getProductNameEn()), value(d.getPackageCount()), d.getGrossWeightKg(), d.getVolumeCbm()); } @@ -1234,6 +1254,8 @@ public class DeliveryTransferService { throw new IllegalStateException("无法生成整托拣货幂等号", error); } } + private record OneClickValidation(String warehouseId, + Map candidatesByRequest) {} private record PalletPickPlan(Long itemId, Long positionId, long packages, List cartonMarks, int sequence) {} private record SkuShipment(long packages, long quantity) { @@ -1242,10 +1264,20 @@ public class DeliveryTransferService { Math.addExact(quantity, other.quantity)); } } - private static BigDecimal sum(List items, - java.util.function.Function getter) { + private static BigDecimal sum(List items, + java.util.function.Function getter) { return items.stream().map(getter).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); } + private static BigDecimal allocateMetric(BigDecimal sourceValue, Long sourcePackages, + long selectedPackages, int scale) { + if (sourceValue == null || sourceValue.signum() == 0 || sourcePackages == null + || sourcePackages <= 0 || selectedPackages <= 0) return BigDecimal.ZERO.setScale(scale); + return sourceValue.multiply(BigDecimal.valueOf(selectedPackages)) + .divide(BigDecimal.valueOf(sourcePackages), scale, RoundingMode.HALF_UP); + } + private static String oneClickCandidateKey(Long inventoryPositionId, String sku) { + return inventoryPositionId + "\u0000" + trim(sku); + } private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; } private String hash(Object value) { try { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipOrderProfileMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipOrderProfileMapper.java index 6f434b5d6..19bf1054b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipOrderProfileMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipOrderProfileMapper.java @@ -17,7 +17,13 @@ public interface DropshipOrderProfileMapper extends BaseMapper item = ArgumentCaptor.forClass(OutboundPalletItem.class); + verify(outboundPallets).insert(item.capture()); + assertEquals(new BigDecimal("20.000"), item.getValue().getGrossWeightKg()); + assertEquals(new BigDecimal("40.000"), item.getValue().getVolumeWeightKg()); + assertEquals(new BigDecimal("2.0000"), item.getValue().getVolumeCbm()); + ArgumentCaptor order = ArgumentCaptor.forClass(OutboundOrder.class); + verify(outboundOrders).updateById(order.capture()); + assertEquals(2, order.getValue().getPackageCount()); + assertEquals(new BigDecimal("20.000"), order.getValue().getGrossWeightKg()); + assertEquals(new BigDecimal("40.000"), order.getValue().getVolumeWeightKg()); + assertEquals(new BigDecimal("2.0000"), order.getValue().getVolumeCbm()); + } + @Test void oneClickOutboundRejectsSummarySkuWithoutAvailableDropshipCartons() { var generator = org.mockito.Mockito.mock(LastlegOrderNoGenerator.class); @@ -563,6 +597,9 @@ class DeliveryTransferServiceTest { assertEquals("DS-A002", result.get(0).palletNo()); assertEquals("SKU-1", result.get(0).skuCode()); assertEquals(3L, result.get(0).availablePackages()); + assertEquals(new BigDecimal("30.000"), result.get(0).grossWeightKg()); + assertEquals(new BigDecimal("60.000"), result.get(0).volumeWeightKg()); + assertEquals(new BigDecimal("3.0000"), result.get(0).volumeCbm()); verify(dropshipProfiles).listOutboundCandidates("TENANT-A", "WH-1"); verify(inventoryPositions, never()).selectById(any()); } @@ -981,7 +1018,11 @@ class DeliveryTransferServiceTest { DropshipOutboundCandidate value = new DropshipOutboundCandidate(); value.setInventoryPositionId(positionId); value.setProfileId(profileId); value.setWarehousePalletId(501L); value.setOverseasWarehouseId(warehouseId); value.setPalletNo(palletNo); value.setSkuCode(sku); - value.setAvailablePackages(packages); return value; + value.setAvailablePackages(packages); value.setSourcePackageCount(packages); + value.setSourceGrossWeightKg(BigDecimal.valueOf(packages * 10L)); + value.setSourceVolumeWeightKg(BigDecimal.valueOf(packages * 20L)); + value.setSourceVolumeCbm(BigDecimal.valueOf(packages)); + return value; } private static DropshipCartonSku cartonSku(String sku, long quantity) {