feat: complete tail order controls and terminal drayage
This commit is contained in:
+146
-10
@@ -6,6 +6,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
@@ -23,6 +24,11 @@ import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.common.event.ContainerNumbers;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderCommandRecord;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderCommandRecordMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -31,6 +37,10 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -50,6 +60,64 @@ public class InboundPlanService {
|
||||
@Resource private InboundPlanQueryMapper queryMapper;
|
||||
@Resource private InventoryBalanceMapper inventoryBalanceMapper;
|
||||
@Resource private DropshipOrderService dropshipOrders;
|
||||
@Resource private WarehouseResourceGateway warehouseGateway;
|
||||
@Resource private LastlegOrderCommandRecordMapper commandMapper;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convertManual(ManualInboundPlanRequest request, String idempotencyKey,
|
||||
String operatorId) {
|
||||
String customerId = trimToNull(request.customerId());
|
||||
if (customerId == null || "0".equals(customerId)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "平台人工计划必须指定客户");
|
||||
}
|
||||
String key = trimToNull(idempotencyKey);
|
||||
if (key == null || key.length() > 128) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Idempotency-Key 必须为1至128个字符");
|
||||
}
|
||||
List<SourceOrderSnapshot> snapshots = orderMapper.lockOrder(customerId, request.orderId());
|
||||
if (snapshots == null || snapshots.size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "尾程订单不存在");
|
||||
}
|
||||
SourceOrderSnapshot order = snapshots.get(0);
|
||||
String requestHash = sha256(request);
|
||||
LastlegOrderCommandRecord replay = commandMapper.find(customerId, request.orderId(), "MANUAL_PLAN", operatorId, key);
|
||||
if (replay != null) {
|
||||
if (!MessageDigest.isEqual(replay.getRequestHash().getBytes(StandardCharsets.UTF_8),
|
||||
requestHash.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "同一幂等键不能用于不同请求");
|
||||
}
|
||||
return detailByPlanNo(customerId, replay.getResultRef());
|
||||
}
|
||||
if (planMapper.findBySourceOrder(customerId, request.orderId()) != null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "尾程订单已生成入库计划");
|
||||
}
|
||||
if (order.version() != request.expectedOrderVersion()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "尾程订单版本已变化,请刷新后重试");
|
||||
}
|
||||
if (order.detained()) throw new ResponseStatusException(HttpStatus.CONFLICT, "暂扣尾程订单不能生成入库计划");
|
||||
if (!Objects.equals(trimToNull(order.inboundCode()), trimToNull(request.inboundCode()))) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "入仓码与尾程订单冻结事实不一致");
|
||||
}
|
||||
WarehouseResourceGateway.WarehouseReference warehouse =
|
||||
warehouseGateway.requireActiveSharedWarehouse(request.plannedWarehouseId());
|
||||
if (!Objects.equals(String.valueOf(warehouse.warehouseId()), trimToNull(order.overseasWarehouseId()))
|
||||
|| !Objects.equals(warehouse.warehouseCode(), request.plannedWarehouseCode().trim())
|
||||
|| !Objects.equals(warehouse.warehouseName(), request.plannedWarehouseName().trim())
|
||||
|| !Objects.equals(warehouse.warehouseCode(), trimToNull(order.overseasWarehouseCode()))
|
||||
|| !Objects.equals(warehouse.warehouseName(), trimToNull(order.overseasWarehouseName()))) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "计划入仓仓库不存在、已停用或与订单预分配事实不一致");
|
||||
}
|
||||
validateOrders(snapshots);
|
||||
InboundPlanResponse response = createPlan(customerId, request.orderId(), snapshots, List.of(), null, request);
|
||||
LastlegOrderCommandRecord command = new LastlegOrderCommandRecord();
|
||||
command.setId(IdWorker.getId()); command.setCustomerId(customerId); command.setOrderId(request.orderId());
|
||||
command.setAction("MANUAL_PLAN"); command.setOperatorId(operatorId); command.setIdempotencyKey(key);
|
||||
command.setRequestHash(requestHash); command.setResultRef(response.planNo());
|
||||
command.setResultJson(json(response)); command.setResultVersion(request.expectedOrderVersion() + 1);
|
||||
command.setCreatedAt(LocalDateTime.now()); commandMapper.insert(command);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<InboundPlanListResponse> page(String customerId, InboundPlanRequests.Query query) {
|
||||
@@ -112,12 +180,12 @@ public class InboundPlanService {
|
||||
return detail(customerId, existing);
|
||||
}
|
||||
validateOrders(orders);
|
||||
return createPlan(customerId, selectedOrderId, orders, sourcePalletNos, wholePallets);
|
||||
return createPlan(customerId, selectedOrderId, orders, sourcePalletNos, wholePallets, null);
|
||||
}
|
||||
|
||||
private InboundPlanResponse createPlan(String customerId, Long selectedOrderId,
|
||||
List<SourceOrderSnapshot> orders, List<String> sourcePalletNos,
|
||||
List<WholePalletInput> wholePallets) {
|
||||
List<WholePalletInput> wholePallets, ManualInboundPlanRequest manual) {
|
||||
String containerNo = requiredContainerNo(orders.get(0).containerNo());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InboundPlan plan = new InboundPlan();
|
||||
@@ -136,6 +204,13 @@ public class InboundPlanService {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "接柜仓ID格式无效", exception);
|
||||
}
|
||||
plan.setStatus("PENDING_ASSIGNMENT");
|
||||
plan.setPlannedArrivalDate(manual == null ? null : manual.plannedArrivalDate());
|
||||
plan.setPalletType(manual == null ? null : manual.palletType().trim().toUpperCase());
|
||||
plan.setAssignedFlag(manual != null && manual.assigned());
|
||||
plan.setMergeRequired(manual != null && manual.mergeRequired());
|
||||
plan.setUrgentFlag(manual != null && manual.urgent());
|
||||
plan.setCreationMode(manual == null ? "AUTO" : "MANUAL");
|
||||
plan.setPlanRemark(manual == null ? null : trimToNull(manual.remark()));
|
||||
plan.setLineCount(orders.size());
|
||||
plan.setTotalVolumeCbm(orders.stream().map(SourceOrderSnapshot::volumeCbm)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
@@ -145,16 +220,20 @@ public class InboundPlanService {
|
||||
Map<Long, SourceOrderSnapshot> orderById = orders.stream()
|
||||
.collect(Collectors.toMap(SourceOrderSnapshot::orderId, value -> value));
|
||||
Map<Long, InboundPlanLine> lineByOrder = createLines(plan, orders);
|
||||
PalletAllocator.Result allocation = wholePallets == null ? PalletAllocator.allocate(orders.stream()
|
||||
PalletAllocator.Result allocation = wholePallets == null && manual == null ? PalletAllocator.allocate(orders.stream()
|
||||
.map(order -> new PalletAllocator.LineInput(order.orderId(), warehouseKey(order), volume(order),
|
||||
order.packageCount(), order.recommendedPalletCount())).toList()) : null;
|
||||
if (wholePallets != null) validateWholePallets(orders, wholePallets);
|
||||
plan.setPalletCount(wholePallets == null ? allocation.pallets().size() : wholePallets.size());
|
||||
plan.setPalletCount(manual != null ? manual.palletCount()
|
||||
: wholePallets == null ? allocation.pallets().size() : wholePallets.size());
|
||||
planMapper.insert(plan);
|
||||
|
||||
lineByOrder.values().forEach(lineMapper::insert);
|
||||
Map<Long, Integer> palletCounts;
|
||||
if (wholePallets == null) {
|
||||
if (manual != null) {
|
||||
createManualPallets(plan, orders.get(0), lineByOrder.get(orders.get(0).orderId()), manual);
|
||||
palletCounts = Map.of(orders.get(0).orderId(), manual.palletCount());
|
||||
} else if (wholePallets == null) {
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation,
|
||||
orderById, sourcePalletNos == null ? List.of() : sourcePalletNos);
|
||||
createAllocations(plan, allocation, lineByOrder, persistedPallets);
|
||||
@@ -171,7 +250,11 @@ public class InboundPlanService {
|
||||
InboundPlanLine line = lineByOrder.get(order.orderId());
|
||||
line.setPlannedPalletCount(count);
|
||||
lineMapper.updateById(line);
|
||||
if (orderMapper.markPlanned(customerId, order.orderId(), plan.getPlanNo(), count, now) != 1) {
|
||||
int updated = manual == null
|
||||
? orderMapper.markPlanned(customerId, order.orderId(), plan.getPlanNo(), count, now)
|
||||
: orderMapper.markPlannedManual(customerId, order.orderId(), plan.getPlanNo(), count,
|
||||
manual.mergeRequired(), manual.expectedOrderVersion(), now);
|
||||
if (updated != 1) {
|
||||
throw new IllegalStateException("Failed to update order " + order.orderId());
|
||||
}
|
||||
inventoryBalanceMapper.bindInboundPlan(customerId, order.orderId(), order.inboundCode(),
|
||||
@@ -248,7 +331,7 @@ public class InboundPlanService {
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
|
||||
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index), List.of(),
|
||||
wholePallets.get(index)));
|
||||
wholePallets.get(index), null));
|
||||
}
|
||||
return List.copyOf(created);
|
||||
}
|
||||
@@ -257,6 +340,43 @@ public class InboundPlanService {
|
||||
return new WholePalletInput(pallet.sourcePalletNo(), Math.toIntExact(pallet.packageCount()));
|
||||
}
|
||||
|
||||
private void createManualPallets(InboundPlan plan, SourceOrderSnapshot order, InboundPlanLine line,
|
||||
ManualInboundPlanRequest request) {
|
||||
if (order.packageCount() > 0 && request.palletCount() > order.packageCount()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "人工计划托盘数不能大于订单箱数");
|
||||
}
|
||||
BigDecimal remainingVolume = volume(order);
|
||||
int remainingPackages = order.packageCount();
|
||||
for (int index = 0; index < request.palletCount(); index++) {
|
||||
int slots = request.palletCount() - index;
|
||||
int packages = slots == 1 ? remainingPackages : remainingPackages / slots;
|
||||
BigDecimal palletVolume = slots == 1 ? remainingVolume
|
||||
: remainingVolume.divide(BigDecimal.valueOf(slots), 4, java.math.RoundingMode.HALF_UP);
|
||||
InboundPlanPallet pallet = new InboundPlanPallet();
|
||||
pallet.setId(IdWorker.getId()); pallet.setCustomerId(plan.getCustomerId()); pallet.setPlanId(plan.getId());
|
||||
pallet.setPalletNo(plan.getPlanNo() + "-P" + String.format("%03d", index + 1));
|
||||
pallet.setPalletType(request.palletType().trim().toUpperCase());
|
||||
pallet.setDestinationWarehouseId(order.destinationWarehouseId());
|
||||
pallet.setDestinationWarehouseCode(order.destinationWarehouseCode());
|
||||
pallet.setDestinationWarehouseName(order.destinationWarehouseName());
|
||||
pallet.setCapacityCbm(PalletAllocator.PALLET_CAPACITY_CBM); pallet.setAllocatedVolumeCbm(palletVolume);
|
||||
pallet.setSequenceNo(index + 1); pallet.setAssignmentStatus("UNASSIGNED");
|
||||
pallet.setActualVolumeCbm(BigDecimal.ZERO); pallet.setActualPackageCount(0); palletMapper.insert(pallet);
|
||||
|
||||
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation();
|
||||
allocation.setId(IdWorker.getId()); allocation.setCustomerId(plan.getCustomerId()); allocation.setPlanId(plan.getId());
|
||||
allocation.setLineId(line.getId()); allocation.setPalletId(pallet.getId());
|
||||
allocation.setAllocatedVolumeCbm(palletVolume); allocation.setPlannedPackageCount(packages);
|
||||
allocation.setCountedPackageCount(0); allocation.setPickedPackageCount(0);
|
||||
allocation.setActualVolumeCbm(BigDecimal.ZERO); allocation.setStatus("PENDING_COUNT"); allocation.setVersion(0);
|
||||
allocationMapper.insert(allocation);
|
||||
remainingPackages -= packages; remainingVolume = remainingVolume.subtract(palletVolume);
|
||||
}
|
||||
if (remainingPackages != 0 || remainingVolume.signum() != 0) {
|
||||
throw new IllegalStateException("人工计划托盘分配未保持箱数或体积守恒");
|
||||
}
|
||||
}
|
||||
|
||||
private String requiredContainerNo(String value) {
|
||||
try {
|
||||
return ContainerNumbers.requireCanonical(value);
|
||||
@@ -395,7 +515,7 @@ public class InboundPlanService {
|
||||
plan.setPalletCount(plan.getPalletCount() + 1); plan.setUpdatedAt(LocalDateTime.now()); planMapper.updateById(plan);
|
||||
return new InboundPlanResponse.Pallet(pallet.getId(), pallet.getPalletNo(), pallet.getDestinationWarehouseId(),
|
||||
pallet.getDestinationWarehouseCode(), pallet.getDestinationWarehouseName(), pallet.getCapacityCbm(),
|
||||
pallet.getAllocatedVolumeCbm(), pallet.getSequenceNo(), null, null, null, pallet.getAssignmentStatus(),
|
||||
pallet.getAllocatedVolumeCbm(), pallet.getSequenceNo(), null, null, null, pallet.getPalletType(), pallet.getAssignmentStatus(),
|
||||
null, pallet.getActualVolumeCbm(), pallet.getActualPackageCount());
|
||||
}
|
||||
|
||||
@@ -533,12 +653,14 @@ public class InboundPlanService {
|
||||
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.getPalletType(), 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(),
|
||||
plan.getVersion() == null ? 0 : plan.getVersion());
|
||||
plan.getVersion() == null ? 0 : plan.getVersion(), plan.getPlannedArrivalDate(), plan.getPalletType(),
|
||||
Boolean.TRUE.equals(plan.getAssignedFlag()), Boolean.TRUE.equals(plan.getMergeRequired()),
|
||||
Boolean.TRUE.equals(plan.getUrgentFlag()), plan.getCreationMode(), plan.getPlanRemark());
|
||||
}
|
||||
|
||||
private BigDecimal volume(SourceOrderSnapshot order) {
|
||||
@@ -554,4 +676,18 @@ public class InboundPlanService {
|
||||
private static String trimToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
||||
private String sha256(Object value) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(objectMapper.writeValueAsBytes(value)));
|
||||
} catch (NoSuchAlgorithmException | JsonProcessingException exception) {
|
||||
throw new IllegalStateException("无法生成命令摘要", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private String json(Object value) {
|
||||
try { return objectMapper.writeValueAsString(value); }
|
||||
catch (JsonProcessingException exception) { throw new IllegalStateException("无法保存命令结果", exception); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user