feat(lastleg): reserve pallets for container plans

This commit is contained in:
2026-09-09 00:15:23 +08:00
parent cf8053b7ce
commit 7b47de47c6
15 changed files with 700 additions and 18 deletions
@@ -0,0 +1,289 @@
package com.baiyue.lastleg.inboundplan.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.inboundplan.dto.ContainerInboundPlanPreviewResponse;
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
import com.baiyue.lastleg.inboundplan.entity.ContainerPlanConversionCommand;
import com.baiyue.lastleg.inboundplan.mapper.ContainerPlanConversionCommandMapper;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
public class ContainerInboundPlanConversionCoordinator {
private static final java.time.Duration APPLYING_LEASE = java.time.Duration.ofMinutes(2);
@Resource private InboundPlanService plans;
@Resource private WarehouseResourceGateway warehouse;
@Resource private ContainerPlanConversionCommandMapper commands;
@Resource private LastlegOrderNoGenerator numbers;
@Resource private ObjectMapper objectMapper;
@Resource private PlatformTransactionManager transactionManager;
public List<InboundPlanResponse> confirm(ConvertContainerInboundPlanRequest request, String idempotencyKey) {
String bookingId = required(request.sourceContainerBookingId(), 128, "来源柜预订 ID 不能为空");
String key = required(idempotencyKey, 128, "Idempotency-Key 必须为1至128个字符");
String requestHash = hash(List.of(bookingId, request.snapshotVersion(), request.snapshotHash()));
ContainerPlanConversionCommand resolved = commands.find(bookingId, key);
if (resolved == null) resolved = commands.findByBooking(bookingId);
if (resolved == null) resolved = createCommand(bookingId, key, request, requestHash);
final ContainerPlanConversionCommand command = resolved;
requireSameRequest(command, requestHash);
Preparation preparation = read(command.getPreparationJson(), Preparation.class);
if ("COMPLETED".equals(command.getStatus())) return result(command);
if ("APPLYING".equals(command.getStatus())) {
LocalDateTime now = LocalDateTime.now();
if (command.getUpdatedAt() == null || command.getUpdatedAt().plus(APPLYING_LEASE).isAfter(now)
|| commands.reclaimStale(command.getId(), command.getUpdatedAt(), now) != 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "该柜正在生成入库计划,请勿重复提交");
}
command.setUpdatedAt(now);
}
if (!"LOCAL_COMMITTED".equals(command.getStatus())) {
if (commands.claim(command.getId(), "PREPARED", "APPLYING", LocalDateTime.now()) != 1) {
ContainerPlanConversionCommand current = commands.findByBooking(bookingId);
if (current != null && "COMPLETED".equals(current.getStatus())) return result(current);
throw new ResponseStatusException(HttpStatus.CONFLICT, "该柜正在生成入库计划,请勿重复提交");
}
command.setStatus("APPLYING");
}
if (preparation.items().isEmpty()) {
try {
List<InboundPlanResponse> existing = inTransaction(() -> {
List<InboundPlanResponse> converted = plans.convertContainerReserved(bookingId,
request.snapshotVersion(), request.snapshotHash(), List.of());
state(command.getId(), "APPLYING", "COMPLETED", write(converted), null);
return converted;
});
command.setStatus("COMPLETED");
return existing;
} catch (RuntimeException failure) {
resetForRetry(command, failure);
throw failure;
}
}
WarehouseResourceGateway.PalletBatchEnsure ensure = new WarehouseResourceGateway.PalletBatchEnsure(
preparation.warehouseId(), command.getBatchKey(), preparation.items().stream().map(item ->
new WarehouseResourceGateway.PalletBatchItemRequest(Long.parseLong(item.customerId()), item.businessKey(),
item.planNo(), item.globalSequenceNo(), "STANDARD", item.fbaWarehouseCode())).toList());
WarehouseResourceGateway.PalletBatchResponse reservation;
try {
reservation = warehouse.ensurePalletBatch(ensure);
requireBatch(reservation, command.getBatchKey(), preparation.items().size(), "RESERVED", "CONFIRMED");
} catch (RuntimeException failure) {
resetForRetry(command, failure);
throw failure;
}
List<InboundPlanResponse> localResult;
if ("LOCAL_COMMITTED".equals(command.getStatus())) {
localResult = result(command);
} else {
try {
List<InboundPlanService.PreparedPlan> preparedPlans = bind(preparation, reservation);
localResult = inTransaction(() -> {
List<InboundPlanResponse> created = plans.convertContainerReserved(bookingId,
request.snapshotVersion(), request.snapshotHash(), preparedPlans);
state(command.getId(), "APPLYING", "LOCAL_COMMITTED", write(created), null);
return created;
});
command.setStatus("LOCAL_COMMITTED");
} catch (RuntimeException failure) {
try {
warehouse.compensatePalletBatch(new WarehouseResourceGateway.PalletBatchCompensation(
preparation.warehouseId(), command.getBatchKey(), "COMPENSATE:" + command.getId(),
"Lastleg local transaction did not commit"));
} catch (RuntimeException compensationFailure) {
failure.addSuppressed(compensationFailure);
}
resetForRetry(command, failure);
throw failure;
}
}
WarehouseResourceGateway.PalletBatchResponse confirmed = warehouse.confirmPalletBatch(
new WarehouseResourceGateway.PalletBatchCommand(preparation.warehouseId(), command.getBatchKey(),
"CONFIRM:" + command.getId()));
requireBatch(confirmed, command.getBatchKey(), preparation.items().size(), "CONFIRMED");
complete(command, localResult);
return localResult;
}
private ContainerPlanConversionCommand createCommand(String bookingId, String key,
ConvertContainerInboundPlanRequest request,
String requestHash) {
ContainerInboundPlanPreviewResponse preview = plans.previewContainer(bookingId);
if (preview.snapshotVersion() != request.snapshotVersion()
|| !Objects.equals(preview.snapshotHash(), request.snapshotHash()) || !preview.ready()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜级预览已变化或存在阻断,请刷新预览后重试");
}
long warehouseId;
try { warehouseId = Long.parseLong(preview.overseasWarehouseId()); }
catch (RuntimeException exception) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜级接柜仓 ID 无效", exception);
}
long commandId = IdWorker.getId();
List<PreparedItem> items = new ArrayList<>();
int globalSequence = 0;
for (ContainerInboundPlanPreviewResponse.Order order : preview.groups().stream()
.flatMap(group -> group.orders().stream()).filter(item -> "READY".equals(item.readiness())).toList()) {
long planId = IdWorker.getId();
String planNo = numbers.next("INBOUND_PLAN", order.destinationWarehouseCode(), planId);
for (int localSequence = 1; localSequence <= order.suggestedPalletCount(); localSequence++) {
items.add(new PreparedItem(order.customerId(), Long.parseLong(order.orderId()), planId, planNo,
localSequence, ++globalSequence, "CIP:" + commandId + ":" + globalSequence,
"FBA".equals(order.destinationType()) ? order.destinationWarehouseCode() : null));
}
}
Preparation preparation = new Preparation(warehouseId, List.copyOf(items));
ContainerPlanConversionCommand command = new ContainerPlanConversionCommand();
command.setId(commandId); command.setSourceContainerBookingId(bookingId); command.setIdempotencyKey(key);
command.setRequestHash(requestHash); command.setSnapshotVersion(request.snapshotVersion());
command.setSnapshotHash(request.snapshotHash()); command.setWarehouseId(warehouseId);
command.setBatchKey("CIP:" + hash(bookingId) + ":" + commandId); command.setStatus("PREPARED");
command.setPreparationJson(write(preparation)); command.setCreatedAt(LocalDateTime.now());
command.setUpdatedAt(command.getCreatedAt());
try { inTransaction(() -> { commands.insert(command); return null; }); }
catch (DuplicateKeyException concurrent) {
ContainerPlanConversionCommand existing = commands.findByBooking(bookingId);
if (existing == null) throw concurrent;
return existing;
}
return command;
}
private List<InboundPlanService.PreparedPlan> bind(Preparation preparation,
WarehouseResourceGateway.PalletBatchResponse response) {
Map<String, WarehouseResourceGateway.PalletBatchItem> byKey = new LinkedHashMap<>();
for (WarehouseResourceGateway.PalletBatchItem item : response.items()) {
if (item.businessKey() == null || byKey.putIfAbsent(item.businessKey(), item) != null) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "海外仓托盘批次包含重复业务键");
}
}
if (response.items().stream().map(WarehouseResourceGateway.PalletBatchItem::palletId).distinct().count() != response.items().size()
|| response.items().stream().map(WarehouseResourceGateway.PalletBatchItem::resourceLockId).distinct().count() != response.items().size()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "海外仓托盘批次包含重复托盘或资源锁");
}
return preparation.items().stream().collect(Collectors.groupingBy(PreparedItem::orderId)).values().stream()
.map(items -> {
PreparedItem first = items.get(0);
return new InboundPlanService.PreparedPlan(first.planId(), first.planNo(), first.customerId(),
first.orderId(), items.stream().sorted(Comparator.comparingInt(PreparedItem::localSequenceNo))
.map(item -> new InboundPlanService.PreparedPallet(item.localSequenceNo(), item.businessKey(),
requiredReservation(byKey.get(item.businessKey()), item))).toList());
}).toList();
}
private WarehouseResourceGateway.PalletBatchItem requiredReservation(
WarehouseResourceGateway.PalletBatchItem value, PreparedItem expected) {
if (value == null || value.customerId() != Long.parseLong(expected.customerId())
|| !Objects.equals(value.businessKey(), expected.businessKey())
|| !Objects.equals(value.inboundPlanNo(), expected.planNo())
|| value.sequenceNo() != expected.globalSequenceNo() || value.palletId() <= 0
|| value.locationId() <= 0 || value.resourceLockId() <= 0
|| value.palletCode() == null || value.palletCode().isBlank()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "海外仓托盘预留响应与计划快照不一致");
}
return value;
}
private void requireBatch(WarehouseResourceGateway.PalletBatchResponse response, String batchKey, int size,
String... acceptedStatuses) {
if (response == null || !Objects.equals(batchKey, response.batchKey()) || response.items() == null
|| response.items().size() != size || java.util.Arrays.stream(acceptedStatuses)
.noneMatch(status -> status.equals(response.status()))) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "海外仓托盘批次响应不完整或状态不一致");
}
}
private void requireSameRequest(ContainerPlanConversionCommand command, String requestHash) {
if (!MessageDigest.isEqual(command.getRequestHash().getBytes(StandardCharsets.UTF_8),
requestHash.getBytes(StandardCharsets.UTF_8))) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "同一幂等键不能用于不同柜级确认请求");
}
}
private List<InboundPlanResponse> result(ContainerPlanConversionCommand command) {
if (command.getResultJson() == null) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "柜级转换结果尚未可用");
}
return read(command.getResultJson(), new TypeReference<List<InboundPlanResponse>>() { });
}
private void complete(ContainerPlanConversionCommand command, List<InboundPlanResponse> result) {
inTransaction(() -> { state(command.getId(), "LOCAL_COMMITTED", "COMPLETED", write(result), null); return null; });
command.setStatus("COMPLETED");
}
private void resetForRetry(ContainerPlanConversionCommand command, RuntimeException failure) {
if ("APPLYING".equals(command.getStatus())) {
state(command.getId(), "APPLYING", "PREPARED", null, safeMessage(failure));
command.setStatus("PREPARED");
}
}
private void state(long id, String expectedStatus, String status, String resultJson, String lastError) {
if (commands.updateState(id, expectedStatus, status, resultJson, lastError, LocalDateTime.now()) != 1) {
throw new IllegalStateException("无法更新柜级转换命令状态");
}
}
private <T> T inTransaction(java.util.function.Supplier<T> action) {
return new TransactionTemplate(transactionManager).execute(status -> action.get());
}
private String hash(Object value) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(write(value).getBytes(StandardCharsets.UTF_8))); }
catch (NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
}
private String write(Object value) {
try { return objectMapper.writeValueAsString(value); }
catch (JsonProcessingException exception) { throw new IllegalStateException("无法序列化柜级转换快照", exception); }
}
private <T> T read(String value, Class<T> type) {
try { return objectMapper.readValue(value, type); }
catch (JsonProcessingException exception) { throw new IllegalStateException("无法读取柜级转换快照", exception); }
}
private <T> T read(String value, TypeReference<T> type) {
try { return objectMapper.readValue(value, type); }
catch (JsonProcessingException exception) { throw new IllegalStateException("无法读取柜级转换结果", exception); }
}
private static String required(String value, int max, String message) {
String normalized = value == null || value.isBlank() ? null : value.trim();
if (normalized == null || normalized.length() > max) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
return normalized;
}
private static String safeMessage(Throwable value) {
String message = value.getMessage();
return message == null ? value.getClass().getSimpleName() : message.substring(0, Math.min(1000, message.length()));
}
public record Preparation(long warehouseId, List<PreparedItem> items) { }
public record PreparedItem(String customerId, long orderId, long planId, String planNo,
int localSequenceNo, int globalSequenceNo, String businessKey,
String fbaWarehouseCode) { }
}
@@ -231,6 +231,7 @@ public class InboundPlanService {
|| row.packageCount() > 0 && row.volumeCbm().signum() == 0) blockers.add("CARGO_FACT_INVALID");
}
Integer suggested = suggestedPalletCount(row, blockers);
if (!planned && (suggested == null || suggested <= 0)) blockers.add("PALLET_COUNT_NOT_READY");
String destinationType = destinationType(row);
String destinationCode = "FBA".equals(destinationType) && trimToNull(row.fbaWarehouseCode()) != null
? row.fbaWarehouseCode().trim() : row.destinationWarehouseCode();
@@ -335,11 +336,23 @@ public class InboundPlanService {
List<SourceOrderSnapshot> orders, List<String> sourcePalletNos,
List<WholePalletInput> wholePallets, ManualInboundPlanRequest manual,
WarehouseResourceGateway.WarehouseReference receivingWarehouse) {
return createPlan(customerId, selectedOrderId, orders, sourcePalletNos, wholePallets, manual,
receivingWarehouse, null, Map.of());
}
private InboundPlanResponse createPlan(String customerId, Long selectedOrderId,
List<SourceOrderSnapshot> orders, List<String> sourcePalletNos,
List<WholePalletInput> wholePallets, ManualInboundPlanRequest manual,
WarehouseResourceGateway.WarehouseReference receivingWarehouse,
PreparedPlan preparedPlan,
Map<Integer, WarehouseResourceGateway.PalletBatchItem> reservedPallets) {
String containerNo = requiredContainerNo(orders.get(0).containerNo());
LocalDateTime now = LocalDateTime.now();
InboundPlan plan = new InboundPlan();
plan.setId(IdWorker.getId());
plan.setPlanNo(orderNoGenerator.next("INBOUND_PLAN", orders.get(0).destinationWarehouseCode(), plan.getId()));
plan.setId(preparedPlan == null ? IdWorker.getId() : preparedPlan.planId());
plan.setPlanNo(preparedPlan == null
? orderNoGenerator.next("INBOUND_PLAN", orders.get(0).destinationWarehouseCode(), plan.getId())
: preparedPlan.planNo());
plan.setCustomerId(customerId);
plan.setSourceOrderId(selectedOrderId);
plan.setContainerNo(containerNo);
@@ -384,14 +397,14 @@ public class InboundPlanService {
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);
orderById, sourcePalletNos == null ? List.of() : sourcePalletNos, reservedPallets);
createAllocations(plan, allocation, lineByOrder, persistedPallets);
palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
PalletAllocator.AllocationDraft::orderId, Collectors.collectingAndThen(Collectors.mapping(
item -> item.pallet().sequenceNo(), Collectors.toSet()), java.util.Set::size)));
} else {
SourceOrderSnapshot order = orders.get(0);
createWholePallets(plan, order, lineByOrder.get(order.orderId()), wholePallets);
createWholePallets(plan, order, lineByOrder.get(order.orderId()), wholePallets, reservedPallets);
palletCounts = Map.of(order.orderId(), wholePallets.size());
}
for (SourceOrderSnapshot order : orders) {
@@ -417,10 +430,32 @@ public class InboundPlanService {
/** 同一来源柜的每个尾程订单独立建计划;整个柜级批次共用一个事务。 */
@Transactional
public List<InboundPlanResponse> convertContainer(String sourceContainerBookingId) {
return convertContainerInternal(sourceContainerBookingId, null, null, Map.of());
}
@Transactional
public List<InboundPlanResponse> convertContainerReserved(String sourceContainerBookingId,
long snapshotVersion, String snapshotHash,
List<PreparedPlan> preparedPlans) {
Map<Long, PreparedPlan> prepared = preparedPlans.stream().collect(Collectors.toMap(
PreparedPlan::orderId, value -> value));
return convertContainerInternal(sourceContainerBookingId, snapshotVersion, snapshotHash, prepared);
}
private List<InboundPlanResponse> convertContainerInternal(String sourceContainerBookingId,
Long snapshotVersion, String snapshotHash,
Map<Long, PreparedPlan> preparedPlans) {
String bookingId = trimToNull(sourceContainerBookingId);
if (bookingId == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订 ID 不能为空");
List<InboundPlanOrderMapper.ContainerOrderRef> refs = orderMapper.lockContainerOrders(bookingId);
if (refs.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜没有尾程订单");
if (snapshotVersion != null) {
ContainerInboundPlanPreviewResponse current = previewContainer(bookingId);
if (current.snapshotVersion() != snapshotVersion || !Objects.equals(current.snapshotHash(), snapshotHash)
|| !current.ready()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜级预览已变化或存在阻断,请刷新预览后重试");
}
}
String containerNo = null;
for (InboundPlanOrderMapper.ContainerOrderRef ref : refs) {
String current = requiredContainerNo(ref.containerNo());
@@ -481,8 +516,15 @@ public class InboundPlanService {
for (int pendingIndex = 0; pendingIndex < pendingIndexes.size(); pendingIndex++) {
int refIndex = pendingIndexes.get(pendingIndex);
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(refIndex);
PreparedPlan prepared = preparedPlans.get(ref.orderId());
if (snapshotVersion != null && (prepared == null || !Objects.equals(prepared.customerId(), ref.customerId()))) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜级托盘预留与待生成计划不一致");
}
Map<Integer, WarehouseResourceGateway.PalletBatchItem> reserved = prepared == null ? Map.of()
: prepared.pallets().stream().collect(Collectors.toMap(PreparedPallet::sequenceNo,
PreparedPallet::reservation));
created.put(refIndex, createPlan(ref.customerId(), ref.orderId(), snapshots.get(pendingIndex), List.of(),
wholePallets.get(pendingIndex), null, null));
wholePallets.get(pendingIndex), null, null, prepared, reserved));
}
List<InboundPlanResponse> result = new ArrayList<>(refs.size());
for (int index = 0; index < refs.size(); index++) {
@@ -492,6 +534,11 @@ public class InboundPlanService {
return List.copyOf(result);
}
public record PreparedPlan(long planId, String planNo, String customerId, long orderId,
List<PreparedPallet> pallets) { }
public record PreparedPallet(int sequenceNo, String businessKey,
WarehouseResourceGateway.PalletBatchItem reservation) { }
private WholePalletInput wholePallet(DropshipPalletSnapshot pallet) {
return new WholePalletInput(pallet.sourcePalletNo(), Math.toIntExact(pallet.packageCount()));
}
@@ -569,7 +616,8 @@ public class InboundPlanService {
}
private void createWholePallets(InboundPlan plan, SourceOrderSnapshot order, InboundPlanLine line,
List<WholePalletInput> sourcePallets) {
List<WholePalletInput> sourcePallets,
Map<Integer, WarehouseResourceGateway.PalletBatchItem> reservedPallets) {
BigDecimal remainingVolume = volume(order);
int remainingPackages = order.packageCount();
for (int index = 0; index < sourcePallets.size(); index++) {
@@ -586,6 +634,7 @@ public class InboundPlanService {
pallet.setDestinationWarehouseName(order.destinationWarehouseName());
pallet.setCapacityCbm(PalletAllocator.PALLET_CAPACITY_CBM); pallet.setAllocatedVolumeCbm(palletVolume);
pallet.setSequenceNo(index + 1); pallet.setAssignmentStatus("UNASSIGNED");
applyReservedPallet(pallet, reservedPallets.get(index + 1));
pallet.setActualVolumeCbm(BigDecimal.ZERO); pallet.setActualPackageCount(0); palletMapper.insert(pallet);
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation();
@@ -707,7 +756,7 @@ public class InboundPlanService {
private Map<PalletAllocator.PalletDraft, InboundPlanPallet> createPallets(
InboundPlan plan, PalletAllocator.Result allocation, Map<Long, SourceOrderSnapshot> orderById,
List<String> sourcePalletNos) {
List<String> sourcePalletNos, Map<Integer, WarehouseResourceGateway.PalletBatchItem> reservedPallets) {
Map<String, SourceOrderSnapshot> warehouseSamples = orderById.values().stream()
.collect(Collectors.toMap(this::warehouseKey, value -> value, (left, right) -> left));
Map<PalletAllocator.PalletDraft, InboundPlanPallet> result = new HashMap<>();
@@ -729,6 +778,7 @@ public class InboundPlanService {
pallet.setAllocatedVolumeCbm(draft.allocatedVolumeCbm());
pallet.setSequenceNo(result.size() + 1);
pallet.setAssignmentStatus("UNASSIGNED");
applyReservedPallet(pallet, reservedPallets.get(pallet.getSequenceNo()));
pallet.setActualVolumeCbm(BigDecimal.ZERO);
pallet.setActualPackageCount(0);
palletMapper.insert(pallet);
@@ -737,6 +787,20 @@ public class InboundPlanService {
return result;
}
private void applyReservedPallet(InboundPlanPallet pallet,
WarehouseResourceGateway.PalletBatchItem reserved) {
if (reserved == null) return;
if (trimToNull(reserved.palletCode()) == null || reserved.palletCode().length() > 128) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "海外仓返回的真实托盘码无效");
}
pallet.setPalletNo(reserved.palletCode());
pallet.setWarehousePalletId(reserved.palletId());
pallet.setWarehouseLocationId(reserved.locationId());
pallet.setWarehouseResourceLockId(reserved.resourceLockId());
pallet.setAssignmentStatus("RESERVED");
pallet.setLockedAt(LocalDateTime.now());
}
private void createAllocations(InboundPlan plan, PalletAllocator.Result allocation,
Map<Long, InboundPlanLine> lineByOrder,
Map<PalletAllocator.PalletDraft, InboundPlanPallet> pallets) {