暂存提交
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointment;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentQueryMapper;
|
||||
import com.baiyue.lastleg.inboundplan.model.FbaAppointmentLineSnapshot;
|
||||
import com.baiyue.lastleg.inboundplan.model.FbaAppointmentStatus;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class InboundFbaAppointmentService {
|
||||
@Resource private InboundFbaAppointmentMapper appointmentMapper;
|
||||
@Resource private InboundFbaAppointmentItemMapper itemMapper;
|
||||
@Resource private InboundFbaAppointmentQueryMapper queryMapper;
|
||||
|
||||
@Transactional
|
||||
public FbaAppointmentResponse batchCreate(String customerId, String operatorId,
|
||||
BatchFbaAppointmentRequest request) {
|
||||
List<Long> lineIds = List.copyOf(request.lineIds());
|
||||
if (new HashSet<>(lineIds).size() != lineIds.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Selected inbound lines contain duplicates");
|
||||
}
|
||||
String appointmentNo = requiredTrimmed(request.appointmentNo(), "Appointment number is required");
|
||||
String status = normalizeStatus(request.appointmentStatus());
|
||||
String remark = trimToNull(request.appointmentRemark());
|
||||
|
||||
InboundFbaAppointment existing = find(customerId, appointmentNo);
|
||||
if (existing != null) {
|
||||
return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark);
|
||||
}
|
||||
|
||||
List<FbaAppointmentLineSnapshot> lines = queryMapper.lockLines(customerId, lineIds);
|
||||
if (lines.size() != lineIds.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
||||
"One or more selected inbound lines do not exist in this tenant");
|
||||
}
|
||||
String fbaWarehouseCode = commonFbaWarehouse(lines);
|
||||
|
||||
// The row locks serialize concurrent submissions selecting the same inbound lines.
|
||||
existing = find(customerId, appointmentNo);
|
||||
if (existing != null) {
|
||||
return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark);
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InboundFbaAppointment appointment = new InboundFbaAppointment();
|
||||
appointment.setId(IdWorker.getId());
|
||||
appointment.setCustomerId(customerId);
|
||||
appointment.setAppointmentNo(appointmentNo);
|
||||
appointment.setAppointmentAt(request.appointmentAt());
|
||||
appointment.setAppointmentStatus(status);
|
||||
appointment.setAppointmentRemark(remark);
|
||||
appointment.setFbaWarehouseCode(fbaWarehouseCode);
|
||||
appointment.setSelectedCount(lines.size());
|
||||
appointment.setCreatedBy(trimToNull(operatorId));
|
||||
appointment.setCreatedAt(now);
|
||||
appointment.setUpdatedAt(now);
|
||||
try {
|
||||
appointmentMapper.insert(appointment);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InboundFbaAppointment concurrent = find(customerId, appointmentNo);
|
||||
if (concurrent != null) {
|
||||
return verifyReplay(concurrent, lineIds, request.appointmentAt(), status, remark);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
|
||||
for (FbaAppointmentLineSnapshot line : lines) {
|
||||
InboundFbaAppointmentItem item = new InboundFbaAppointmentItem();
|
||||
item.setId(IdWorker.getId());
|
||||
item.setAppointmentId(appointment.getId());
|
||||
item.setPlanId(line.planId());
|
||||
item.setLineId(line.lineId());
|
||||
item.setOrderId(line.orderId());
|
||||
item.setInboundCode(line.inboundCode());
|
||||
item.setFbaWarehouseCode(line.fbaWarehouseCode());
|
||||
item.setCreatedAt(now);
|
||||
itemMapper.insert(item);
|
||||
}
|
||||
return response(appointment, items(appointment.getId()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FbaAppointmentResponse detail(String customerId, String appointmentNo) {
|
||||
InboundFbaAppointment appointment = find(customerId,
|
||||
requiredTrimmed(appointmentNo, "Appointment number is required"));
|
||||
if (appointment == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "FBA appointment not found");
|
||||
}
|
||||
return response(appointment, items(appointment.getId()));
|
||||
}
|
||||
|
||||
private FbaAppointmentResponse verifyReplay(InboundFbaAppointment existing, List<Long> requestedLineIds,
|
||||
LocalDateTime appointmentAt, String status, String remark) {
|
||||
List<InboundFbaAppointmentItem> existingItems = items(existing.getId());
|
||||
Set<Long> existingLineIds = existingItems.stream()
|
||||
.map(InboundFbaAppointmentItem::getLineId).collect(java.util.stream.Collectors.toSet());
|
||||
if (!Objects.equals(existing.getAppointmentAt(), appointmentAt)
|
||||
|| !Objects.equals(existing.getAppointmentStatus(), status)
|
||||
|| !Objects.equals(existing.getAppointmentRemark(), remark)
|
||||
|| existingLineIds.size() != requestedLineIds.size()
|
||||
|| !existingLineIds.equals(new HashSet<>(requestedLineIds))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Appointment number already exists with different appointment data");
|
||||
}
|
||||
return response(existing, existingItems);
|
||||
}
|
||||
|
||||
private String commonFbaWarehouse(List<FbaAppointmentLineSnapshot> lines) {
|
||||
List<String> warehouses = lines.stream().map(FbaAppointmentLineSnapshot::fbaWarehouseCode)
|
||||
.map(InboundFbaAppointmentService::trimToNull).toList();
|
||||
if (warehouses.stream().anyMatch(Objects::isNull) || new HashSet<>(warehouses).size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Selected inbound lines must belong to the same non-empty FBA warehouse");
|
||||
}
|
||||
return warehouses.get(0);
|
||||
}
|
||||
|
||||
private InboundFbaAppointment find(String customerId, String appointmentNo) {
|
||||
return appointmentMapper.selectOne(new LambdaQueryWrapper<InboundFbaAppointment>()
|
||||
.eq(InboundFbaAppointment::getCustomerId, customerId)
|
||||
.eq(InboundFbaAppointment::getAppointmentNo, appointmentNo));
|
||||
}
|
||||
|
||||
private List<InboundFbaAppointmentItem> items(Long appointmentId) {
|
||||
return itemMapper.selectList(new LambdaQueryWrapper<InboundFbaAppointmentItem>()
|
||||
.eq(InboundFbaAppointmentItem::getAppointmentId, appointmentId)
|
||||
.orderByAsc(InboundFbaAppointmentItem::getLineId));
|
||||
}
|
||||
|
||||
private FbaAppointmentResponse response(InboundFbaAppointment appointment,
|
||||
List<InboundFbaAppointmentItem> items) {
|
||||
return new FbaAppointmentResponse(appointment.getId(), appointment.getCustomerId(),
|
||||
appointment.getAppointmentNo(), appointment.getAppointmentAt(), appointment.getAppointmentStatus(),
|
||||
appointment.getAppointmentRemark(), appointment.getFbaWarehouseCode(), appointment.getSelectedCount(),
|
||||
appointment.getCreatedBy(), appointment.getCreatedAt(), appointment.getUpdatedAt(),
|
||||
items.stream().map(item -> new FbaAppointmentResponse.Item(item.getId(), item.getPlanId(),
|
||||
item.getLineId(), item.getOrderId(), item.getInboundCode(), item.getFbaWarehouseCode())).toList());
|
||||
}
|
||||
|
||||
private static String normalizeStatus(String value) {
|
||||
String normalized = requiredTrimmed(value, "Appointment status is required").toUpperCase(Locale.ROOT);
|
||||
try {
|
||||
return FbaAppointmentStatus.valueOf(normalized).name();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Appointment status must be PENDING, SUCCESS, FAILED or CANCELLED");
|
||||
}
|
||||
}
|
||||
|
||||
private static String requiredTrimmed(String value, String message) {
|
||||
String normalized = trimToNull(value);
|
||||
if (normalized == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanOrderMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
||||
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class InboundPlanService {
|
||||
@Resource private InboundPlanMapper planMapper;
|
||||
@Resource private InboundPlanLineMapper lineMapper;
|
||||
@Resource private InboundPlanPalletMapper palletMapper;
|
||||
@Resource private InboundPlanPalletAllocationMapper allocationMapper;
|
||||
@Resource private InboundPlanOrderMapper orderMapper;
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
|
||||
String containerNo = trimToNull(orderMapper.findContainerNo(customerId, selectedOrderId));
|
||||
if (containerNo == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Order does not exist in this tenant or container number is empty");
|
||||
}
|
||||
|
||||
List<SourceOrderSnapshot> orders = orderMapper.lockContainerOrders(customerId, containerNo);
|
||||
if (orders.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Container orders not found");
|
||||
}
|
||||
InboundPlan existing = planMapper.findByContainer(customerId, containerNo);
|
||||
if (existing != null) {
|
||||
return detail(customerId, existing);
|
||||
}
|
||||
validateOrders(orders);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setId(IdWorker.getId());
|
||||
plan.setPlanNo("LIP" + plan.getId());
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setContainerNo(containerNo);
|
||||
plan.setStatus("PENDING_ASSIGNMENT");
|
||||
plan.setLineCount(orders.size());
|
||||
plan.setTotalVolumeCbm(orders.stream().map(SourceOrderSnapshot::volumeCbm)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
plan.setCreatedAt(now);
|
||||
plan.setUpdatedAt(now);
|
||||
|
||||
Map<Long, SourceOrderSnapshot> orderById = orders.stream()
|
||||
.collect(Collectors.toMap(SourceOrderSnapshot::orderId, value -> value));
|
||||
Map<Long, InboundPlanLine> lineByOrder = createLines(plan, orders);
|
||||
PalletAllocator.Result allocation = PalletAllocator.allocate(orders.stream()
|
||||
.map(order -> new PalletAllocator.LineInput(order.orderId(), warehouseKey(order), volume(order),
|
||||
order.packageCount()))
|
||||
.toList());
|
||||
plan.setPalletCount(allocation.pallets().size());
|
||||
planMapper.insert(plan);
|
||||
|
||||
lineByOrder.values().forEach(lineMapper::insert);
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation, orderById);
|
||||
createAllocations(plan, allocation, lineByOrder, persistedPallets);
|
||||
|
||||
Map<Long, Integer> palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
|
||||
PalletAllocator.AllocationDraft::orderId,
|
||||
Collectors.collectingAndThen(Collectors.mapping(item -> item.pallet().sequenceNo(), Collectors.toSet()), java.util.Set::size)));
|
||||
for (SourceOrderSnapshot order : orders) {
|
||||
int count = palletCounts.getOrDefault(order.orderId(), 0);
|
||||
InboundPlanLine line = lineByOrder.get(order.orderId());
|
||||
line.setPlannedPalletCount(count);
|
||||
lineMapper.updateById(line);
|
||||
if (orderMapper.markPlanned(customerId, order.orderId(), plan.getPlanNo(), count, now) != 1) {
|
||||
throw new IllegalStateException("Failed to update order " + order.orderId());
|
||||
}
|
||||
orderMapper.insertHistory(order.orderId(), order.currentStatus(),
|
||||
"Container converted to inbound plan " + plan.getPlanNo(), now);
|
||||
}
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getCustomerId, customerId).eq(InboundPlan::getPlanNo, planNo));
|
||||
if (plan == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
}
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
private Map<Long, InboundPlanLine> createLines(InboundPlan plan, List<SourceOrderSnapshot> orders) {
|
||||
Map<Long, InboundPlanLine> result = new HashMap<>();
|
||||
for (SourceOrderSnapshot order : orders) {
|
||||
InboundPlanLine line = new InboundPlanLine();
|
||||
line.setId(IdWorker.getId());
|
||||
line.setPlanId(plan.getId());
|
||||
line.setOrderId(order.orderId());
|
||||
line.setInboundCode(order.inboundCode());
|
||||
line.setFbaWarehouseCode(order.fbaWarehouseCode());
|
||||
line.setDestinationWarehouseId(order.destinationWarehouseId());
|
||||
line.setDestinationWarehouseCode(order.destinationWarehouseCode());
|
||||
line.setDestinationWarehouseName(order.destinationWarehouseName());
|
||||
line.setVolumeCbm(volume(order));
|
||||
line.setRequiredPackageCount(order.packageCount());
|
||||
line.setPlannedPalletCount(0);
|
||||
result.put(order.orderId(), line);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<PalletAllocator.PalletDraft, InboundPlanPallet> createPallets(
|
||||
InboundPlan plan, PalletAllocator.Result allocation, Map<Long, SourceOrderSnapshot> orderById) {
|
||||
Map<String, SourceOrderSnapshot> warehouseSamples = orderById.values().stream()
|
||||
.collect(Collectors.toMap(this::warehouseKey, value -> value, (left, right) -> left));
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> result = new HashMap<>();
|
||||
for (PalletAllocator.PalletDraft draft : allocation.pallets()) {
|
||||
SourceOrderSnapshot sample = warehouseSamples.get(draft.warehouseKey());
|
||||
InboundPlanPallet pallet = new InboundPlanPallet();
|
||||
pallet.setId(IdWorker.getId());
|
||||
pallet.setPlanId(plan.getId());
|
||||
pallet.setPalletNo(plan.getPlanNo() + "-P" + String.format("%03d", result.size() + 1));
|
||||
pallet.setDestinationWarehouseId(sample.destinationWarehouseId());
|
||||
pallet.setDestinationWarehouseCode(sample.destinationWarehouseCode());
|
||||
pallet.setDestinationWarehouseName(sample.destinationWarehouseName());
|
||||
pallet.setCapacityCbm(PalletAllocator.PALLET_CAPACITY_CBM);
|
||||
pallet.setAllocatedVolumeCbm(draft.allocatedVolumeCbm());
|
||||
pallet.setSequenceNo(result.size() + 1);
|
||||
pallet.setAssignmentStatus("UNASSIGNED");
|
||||
pallet.setActualVolumeCbm(BigDecimal.ZERO);
|
||||
pallet.setActualPackageCount(0);
|
||||
palletMapper.insert(pallet);
|
||||
result.put(draft, pallet);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void createAllocations(InboundPlan plan, PalletAllocator.Result allocation,
|
||||
Map<Long, InboundPlanLine> lineByOrder,
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> pallets) {
|
||||
for (PalletAllocator.AllocationDraft draft : allocation.allocations()) {
|
||||
InboundPlanPalletAllocation item = new InboundPlanPalletAllocation();
|
||||
item.setId(IdWorker.getId());
|
||||
item.setPlanId(plan.getId());
|
||||
item.setLineId(lineByOrder.get(draft.orderId()).getId());
|
||||
item.setPalletId(pallets.get(draft.pallet()).getId());
|
||||
item.setAllocatedVolumeCbm(draft.allocatedVolumeCbm());
|
||||
item.setPlannedPackageCount(draft.plannedPackageCount());
|
||||
item.setCountedPackageCount(0);
|
||||
item.setPickedPackageCount(0);
|
||||
item.setActualVolumeCbm(BigDecimal.ZERO);
|
||||
item.setStatus("PENDING_COUNT");
|
||||
item.setVersion(0);
|
||||
allocationMapper.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOrders(List<SourceOrderSnapshot> orders) {
|
||||
for (SourceOrderSnapshot order : orders) {
|
||||
if (!"PENDING_ASSIGNMENT".equals(order.currentStatus())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Inbound code " + order.inboundCode() + " is not ready for an inbound plan");
|
||||
}
|
||||
if (order.destinationCount() != 1 || order.destinationMismatchCount() != 0 || warehouseKey(order) == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Inbound code " + order.inboundCode()
|
||||
+ " must have one destination warehouse consistent with all cargo details");
|
||||
}
|
||||
if (order.inboundOrderNo() != null && !order.inboundOrderNo().isBlank()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Inbound code " + order.inboundCode() + " already belongs to an inbound plan");
|
||||
}
|
||||
if (volume(order).signum() < 0) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Inbound code " + order.inboundCode() + " has negative volume");
|
||||
}
|
||||
if (order.packageCount() < 0 || (order.packageCount() > 0 && volume(order).signum() == 0)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Inbound code " + order.inboundCode() + " has invalid package count or volume");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private InboundPlanResponse detail(String customerId, InboundPlan plan) {
|
||||
if (!customerId.equals(plan.getCustomerId())) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
}
|
||||
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getPlanId, plan.getId()).orderByAsc(InboundPlanLine::getId));
|
||||
List<InboundPlanPallet> pallets = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
|
||||
.eq(InboundPlanPallet::getPlanId, plan.getId()).orderByAsc(InboundPlanPallet::getSequenceNo));
|
||||
List<InboundPlanPalletAllocation> allocations = allocationMapper.selectList(
|
||||
new LambdaQueryWrapper<InboundPlanPalletAllocation>()
|
||||
.eq(InboundPlanPalletAllocation::getPlanId, plan.getId())
|
||||
.orderByAsc(InboundPlanPalletAllocation::getId));
|
||||
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getContainerNo(),
|
||||
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), plan.getTotalVolumeCbm(),
|
||||
lines.stream().map(line -> new InboundPlanResponse.Line(line.getId(), line.getOrderId(), line.getInboundCode(), line.getFbaWarehouseCode(),
|
||||
line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(), line.getDestinationWarehouseName(),
|
||||
line.getVolumeCbm(), line.getRequiredPackageCount(), line.getPlannedPalletCount())).toList(),
|
||||
pallets.stream().map(pallet -> new InboundPlanResponse.Pallet(pallet.getId(), pallet.getPalletNo(),
|
||||
pallet.getDestinationWarehouseId(), pallet.getDestinationWarehouseCode(), pallet.getDestinationWarehouseName(),
|
||||
pallet.getCapacityCbm(), pallet.getAllocatedVolumeCbm(), pallet.getSequenceNo(),
|
||||
pallet.getWarehousePalletId(), pallet.getWarehouseLocationId(), pallet.getWarehouseResourceLockId(),
|
||||
pallet.getAssignmentStatus(), pallet.getLockedAt(), pallet.getActualVolumeCbm(),
|
||||
pallet.getActualPackageCount())).toList(),
|
||||
allocations.stream().map(item -> new InboundPlanResponse.Allocation(item.getLineId(), item.getPalletId(),
|
||||
item.getAllocatedVolumeCbm(), item.getPlannedPackageCount(), item.getCountedPackageCount(),
|
||||
item.getPickedPackageCount(), item.getActualVolumeCbm(), item.getStatus(), item.getVersion())).toList());
|
||||
}
|
||||
|
||||
private BigDecimal volume(SourceOrderSnapshot order) {
|
||||
return Objects.requireNonNullElse(order.volumeCbm(), BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
private String warehouseKey(SourceOrderSnapshot order) {
|
||||
String id = trimToNull(order.destinationWarehouseId());
|
||||
String code = trimToNull(order.destinationWarehouseCode());
|
||||
return id != null ? "ID:" + id : code == null ? null : "CODE:" + code;
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PalletAllocator {
|
||||
public static final BigDecimal PALLET_CAPACITY_CBM = new BigDecimal("2.0000");
|
||||
|
||||
private PalletAllocator() {
|
||||
}
|
||||
|
||||
public static Result allocate(List<LineInput> lines) {
|
||||
Map<String, List<LineInput>> groups = new LinkedHashMap<>();
|
||||
for (LineInput line : lines) {
|
||||
if (line.volumeCbm() == null || line.volumeCbm().signum() < 0) {
|
||||
throw new IllegalArgumentException("Volume must be zero or positive");
|
||||
}
|
||||
if (line.packageCount() < 0) {
|
||||
throw new IllegalArgumentException("Package count must be zero or positive");
|
||||
}
|
||||
if (line.volumeCbm().signum() == 0 && line.packageCount() > 0) {
|
||||
throw new IllegalArgumentException("Positive package count requires positive volume");
|
||||
}
|
||||
groups.computeIfAbsent(line.warehouseKey(), ignored -> new ArrayList<>()).add(line);
|
||||
}
|
||||
|
||||
List<PalletDraft> pallets = new ArrayList<>();
|
||||
List<AllocationDraft> allocations = new ArrayList<>();
|
||||
for (List<LineInput> group : groups.values()) {
|
||||
PalletDraft current = null;
|
||||
int sequence = 0;
|
||||
for (LineInput line : group) {
|
||||
BigDecimal remaining = line.volumeCbm();
|
||||
while (remaining.signum() > 0) {
|
||||
if (current == null || current.remainingCapacity().signum() == 0) {
|
||||
current = new PalletDraft(line.warehouseKey(), ++sequence, BigDecimal.ZERO);
|
||||
pallets.add(current);
|
||||
}
|
||||
BigDecimal allocated = remaining.min(current.remainingCapacity());
|
||||
current.add(allocated);
|
||||
allocations.add(new AllocationDraft(line.orderId(), current, allocated, 0));
|
||||
remaining = remaining.subtract(allocated);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Result(List.copyOf(pallets), distributePackages(lines, allocations));
|
||||
}
|
||||
|
||||
private static List<AllocationDraft> distributePackages(List<LineInput> lines,
|
||||
List<AllocationDraft> allocations) {
|
||||
Map<Long, LineInput> linesByOrder = new LinkedHashMap<>();
|
||||
for (LineInput line : lines) {
|
||||
if (linesByOrder.put(line.orderId(), line) != null) {
|
||||
throw new IllegalArgumentException("Duplicate order id " + line.orderId());
|
||||
}
|
||||
}
|
||||
Map<Long, List<AllocationDraft>> allocationsByOrder = new LinkedHashMap<>();
|
||||
for (AllocationDraft allocation : allocations) {
|
||||
allocationsByOrder.computeIfAbsent(allocation.orderId(), ignored -> new ArrayList<>()).add(allocation);
|
||||
}
|
||||
|
||||
List<AllocationDraft> result = new ArrayList<>(allocations.size());
|
||||
Map<AllocationDraft, AllocationDraft> replacements = new LinkedHashMap<>();
|
||||
for (LineInput line : lines) {
|
||||
List<AllocationDraft> orderAllocations = allocationsByOrder.getOrDefault(line.orderId(), List.of());
|
||||
int assigned = 0;
|
||||
for (int index = 0; index < orderAllocations.size(); index++) {
|
||||
AllocationDraft allocation = orderAllocations.get(index);
|
||||
int packages = index == orderAllocations.size() - 1
|
||||
? line.packageCount() - assigned
|
||||
: allocation.allocatedVolumeCbm().multiply(BigDecimal.valueOf(line.packageCount()))
|
||||
.divide(line.volumeCbm(), 0, RoundingMode.DOWN).intValueExact();
|
||||
assigned += packages;
|
||||
replacements.put(allocation, new AllocationDraft(allocation.orderId(), allocation.pallet(),
|
||||
allocation.allocatedVolumeCbm(), packages));
|
||||
}
|
||||
}
|
||||
for (AllocationDraft allocation : allocations) {
|
||||
result.add(replacements.get(allocation));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
public record LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount) {
|
||||
}
|
||||
|
||||
public static final class PalletDraft {
|
||||
private final String warehouseKey;
|
||||
private final int sequenceNo;
|
||||
private BigDecimal allocatedVolumeCbm;
|
||||
|
||||
private PalletDraft(String warehouseKey, int sequenceNo, BigDecimal allocatedVolumeCbm) {
|
||||
this.warehouseKey = warehouseKey;
|
||||
this.sequenceNo = sequenceNo;
|
||||
this.allocatedVolumeCbm = allocatedVolumeCbm;
|
||||
}
|
||||
|
||||
private void add(BigDecimal volume) {
|
||||
allocatedVolumeCbm = allocatedVolumeCbm.add(volume);
|
||||
}
|
||||
|
||||
public String warehouseKey() {
|
||||
return warehouseKey;
|
||||
}
|
||||
|
||||
public int sequenceNo() {
|
||||
return sequenceNo;
|
||||
}
|
||||
|
||||
public BigDecimal allocatedVolumeCbm() {
|
||||
return allocatedVolumeCbm;
|
||||
}
|
||||
|
||||
public BigDecimal remainingCapacity() {
|
||||
return PALLET_CAPACITY_CBM.subtract(allocatedVolumeCbm);
|
||||
}
|
||||
}
|
||||
|
||||
public record AllocationDraft(Long orderId, PalletDraft pallet, BigDecimal allocatedVolumeCbm,
|
||||
int plannedPackageCount) {
|
||||
}
|
||||
|
||||
public record Result(List<PalletDraft> pallets, List<AllocationDraft> allocations) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user