fix: carry partial dropship measurements to outbound

This commit is contained in:
2026-09-18 15:23:36 +08:00
parent c8a99f9fff
commit 98d15c30bb
7 changed files with 127 additions and 11 deletions
@@ -59,7 +59,8 @@ public final class DeliveryResponses {
public record OutboundDetail(OutboundRow outbound, List<DeliveryRow> deliveries, List<OutboundPalletRow> 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,
@@ -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<Long> physicalPalletIds = new HashSet<>();
List<OutboundPalletItem> 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<DeliveryRequests.OneClickItem> items) {
private OneClickValidation validateOneClickItems(String customerId, List<DeliveryRequests.OneClickItem> items) {
String warehouseId = null;
Map<String, Long> requestedPackages = new LinkedHashMap<>();
Map<String, DropshipOutboundCandidate> candidates = new HashMap<>();
Map<String, DropshipOutboundCandidate> candidatesByRequest = new HashMap<>();
Set<String> 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<String, DropshipOutboundCandidate> candidatesByRequest) {}
private record PalletPickPlan(Long itemId, Long positionId, long packages,
List<String> 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<DispatchItem> items,
java.util.function.Function<DispatchItem, BigDecimal> getter) {
private static <T> BigDecimal sum(List<T> items,
java.util.function.Function<T, BigDecimal> 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 {