Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java
#	lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java
#	warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletBatchService.java
This commit is contained in:
2026-09-09 18:20:40 +08:00
58 changed files with 1787 additions and 932 deletions
@@ -147,13 +147,20 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Reserved pallet customer binding is invalid");
}
WarehousePalletData pallet = lookupPallet(palletCode).data();
if (pallet.id() != palletId || pallet.warehouseId() != warehouseId || pallet.locationId() != locationId
WarehousePalletReference pallet;
try {
pallet = reference(lookupPallet(palletCode).data());
} catch (ResponseStatusException exception) {
if (exception.getStatusCode().value() != 404) throw exception;
// 已生成计划可能保留早期展示码;确认过的资源绑定仍是唯一可信物理身份。
pallet = resolvePallet(palletId);
}
if (pallet.palletId() != palletId || pallet.warehouseId() != warehouseId || pallet.locationId() != locationId
|| !"OCCUPIED".equals(pallet.status())) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Scanned pallet does not match the reserved inbound plan resource");
}
return reference(pallet);
return pallet;
}
@Override
@@ -58,7 +58,8 @@ public interface WarehouseResourceGateway {
java.math.BigDecimal maxLoadKg, String status) { }
record PalletBatchEnsure(long warehouseId, String batchKey, List<PalletBatchItemRequest> items) { }
record PalletBatchItemRequest(long customerId, String businessKey, String inboundPlanNo,
int sequenceNo, String palletType, String fbaWarehouseCode) { }
int sequenceNo, String palletType, String fbaWarehouseCode,
String palletCode) { }
record PalletBatchCommand(long warehouseId, String batchKey, String commandKey) { }
record PalletBatchCompensation(long warehouseId, String batchKey, String commandKey, String reason) { }
record PalletBatchResponse(long batchId, String batchKey, String status, int attemptNo,
@@ -117,7 +117,7 @@ public class ContainerInboundPlanConversionCoordinator {
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());
item.fbaWarehouseCode(), item.palletCode())).toList());
WarehouseResourceGateway.PalletBatchResponse reservation;
try {
reservation = warehouse.ensurePalletBatch(ensure);
@@ -207,10 +207,15 @@ public class ContainerInboundPlanConversionCoordinator {
.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);
List<String> sourcePalletNos = plans.sourcePalletNos(
order.customerId(), Long.parseLong(order.orderId()), order.orderType());
if (sourcePalletNos.size() != order.suggestedPalletCount()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "头程托盘码数量与柜级预览不一致");
}
for (int localSequence = 1; localSequence <= order.suggestedPalletCount(); localSequence++) {
items.add(new PreparedItem(order.customerId(), Long.parseLong(order.orderId()), planId, planNo,
localSequence, ++globalSequence, "CIP:" + generationId + ":" + globalSequence,
trimToNull(order.fbaWarehouseCode())));
trimToNull(order.fbaWarehouseCode()), sourcePalletNos.get(localSequence - 1)));
}
}
Preparation preparation = new Preparation(warehouseId, List.copyOf(items));
@@ -373,5 +378,5 @@ public class ContainerInboundPlanConversionCoordinator {
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) { }
String fbaWarehouseCode, String palletCode) { }
}
@@ -27,6 +27,7 @@ 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.mapper.LastlegSourcePalletMapper;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -65,8 +66,16 @@ public class InboundPlanService {
@Resource private DropshipOrderService dropshipOrders;
@Resource private WarehouseResourceGateway warehouseGateway;
@Resource private LastlegOrderCommandRecordMapper commandMapper;
@Resource private LastlegSourcePalletMapper sourcePalletMapper;
@Resource private ObjectMapper objectMapper;
public List<String> sourcePalletNos(String customerId, long orderId, String orderType) {
return "ONE_CLICK_DROPSHIP".equals(orderType)
? dropshipOrders.palletSnapshots(customerId, orderId).stream()
.map(DropshipPalletSnapshot::sourcePalletNo).toList()
: sourcePalletMapper.palletNos(customerId, orderId);
}
@Transactional
public InboundPlanResponse convertManual(ManualInboundPlanRequest request, String idempotencyKey,
String operatorId) {
@@ -393,6 +402,10 @@ public class InboundPlanService {
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 (allocation != null && sourcePalletNos != null && !sourcePalletNos.isEmpty()
&& sourcePalletNos.size() != allocation.pallets().size()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "头程托盘码数量与尾程计划托盘数不一致");
}
if (wholePallets != null) validateWholePallets(orders, wholePallets);
plan.setPalletCount(manual != null ? manual.palletCount()
: wholePallets == null ? allocation.pallets().size() : wholePallets.size());
@@ -479,6 +492,7 @@ public class InboundPlanService {
List<Integer> pendingIndexes = new ArrayList<>();
List<List<SourceOrderSnapshot>> snapshots = new ArrayList<>();
List<List<WholePalletInput>> wholePallets = new ArrayList<>();
List<List<String>> standardPalletNos = new ArrayList<>();
for (int index = 0; index < refs.size(); index++) {
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
InboundPlan existing = existingPlans.get(index);
@@ -517,6 +531,11 @@ public class InboundPlanService {
validateWholePallets(orders, sourcePallets);
}
wholePallets.add(sourcePallets);
List<String> standardNos = "ONE_CLICK_DROPSHIP".equals(ref.orderType()) ? List.of()
: sourcePalletMapper.palletNos(ref.customerId(), ref.orderId());
if (sourcePallets == null && standardNos.isEmpty()) throw new ResponseStatusException(
HttpStatus.CONFLICT, "散货订单缺少头程托盘码快照");
standardPalletNos.add(standardNos);
}
requireSingleWarehouseId(warehouseIds);
@@ -531,7 +550,7 @@ public class InboundPlanService {
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(),
created.put(refIndex, createPlan(ref.customerId(), ref.orderId(), snapshots.get(pendingIndex), standardPalletNos.get(pendingIndex),
wholePallets.get(pendingIndex), null, null, prepared, reserved));
}
List<InboundPlanResponse> result = new ArrayList<>(refs.size());
@@ -0,0 +1,27 @@
package com.baiyue.lastleg.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface LastlegSourcePalletMapper {
@Insert("""
INSERT INTO ll_order_source_pallet
(id,customer_id,order_id,source_pallet_no,sequence_no,source_event_id,created_at)
VALUES (#{id},#{customerId},#{orderId},#{palletNo},#{sequenceNo},#{eventId},CURRENT_TIMESTAMP(3))
""")
int insert(@Param("id") long id, @Param("customerId") String customerId,
@Param("orderId") long orderId, @Param("palletNo") String palletNo,
@Param("sequenceNo") int sequenceNo, @Param("eventId") String eventId);
@Select("""
SELECT source_pallet_no FROM ll_order_source_pallet
WHERE customer_id=#{customerId} AND order_id=#{orderId}
ORDER BY sequence_no,id
""")
List<String> palletNos(@Param("customerId") String customerId, @Param("orderId") long orderId);
}
@@ -45,6 +45,7 @@ public class LoadingCompletedConsumer {
@Resource private DropshipOrderService dropshipOrders;
@Resource private InventoryCommandService inventory;
@Resource private ContainerShippingGateMapper shippingGates;
@Resource private LastlegSourcePalletMapper sourcePallets;
@Transactional(isolation = Isolation.READ_COMMITTED)
public int consume(EventEnvelope envelope) {
@@ -278,6 +279,20 @@ public class LoadingCompletedConsumer {
Map<String, List<String>> dropshipLabels = "ONE_CLICK_DROPSHIP".equals(orderType)
&& "OVERSEAS".equals(destinationType) ? dropshipReceivingLabels(source, rawSource) : Map.of();
if ("FIRST_LEG".equals(orderType) && event.eventVersion() >= 2) {
List<FirstLegLoadingPlanCompletedV1.PalletSnapshot> pallets = source.pallets() == null ? List.of() : source.pallets();
Set<String> palletNos = new HashSet<>();
for (int palletIndex = 0; palletIndex < pallets.size(); palletIndex++) {
String palletNo = pallets.get(palletIndex).palletNo();
if (palletNo == null || palletNo.isBlank() || palletNo.length() > 128 || !palletNos.add(palletNo)) {
throw new IllegalArgumentException("散货来源托盘码为空、重复或过长");
}
sourcePallets.insert(IdWorker.getId(), customerId, order.getId(), palletNo.trim(), palletIndex + 1, event.eventId());
}
if (!pallets.isEmpty() && order.getPalletCount() > 0 && pallets.size() != order.getPalletCount()) {
throw new IllegalArgumentException("散货来源托盘码数量与托盘数不一致");
}
}
for(var item:source.items()) {
LastlegOrderDetail detail=new LastlegOrderDetail();
detail.setCustomerId(customerId);