fix(lastleg): keep unconfirmed container resources recoverable

This commit is contained in:
2026-10-05 05:26:01 +08:00
parent 3afb3bcfd1
commit 47acfc991b
10 changed files with 146 additions and 6 deletions
@@ -11,6 +11,14 @@ import java.time.LocalDateTime;
@Mapper
public interface ContainerPlanConversionCommandMapper extends BaseMapper<ContainerPlanConversionCommand> {
// Only plans listed in this durable command are pending; older plans in the same cabinet stay confirmed.
String UNCONFIRMED_PLAN = "EXISTS (SELECT 1 FROM ll_container_plan_conversion_command rc " +
"JOIN JSON_TABLE(rc.preparation_json, '$.items[*]' COLUMNS (" +
"customer_id VARCHAR(20) PATH '$.customerId' ERROR ON EMPTY ERROR ON ERROR," +
"plan_id BIGINT PATH '$.planId' ERROR ON EMPTY ERROR ON ERROR)) resource_item " +
"ON resource_item.plan_id=p.id AND CAST(resource_item.customer_id AS BINARY)=CAST(p.customer_id AS BINARY) " +
"WHERE rc.source_container_booking_id=o.source_container_booking_id AND rc.status='LOCAL_COMMITTED')";
@Select("SELECT * FROM ll_container_plan_conversion_command WHERE source_container_booking_id=#{bookingId} AND idempotency_key=#{key} LIMIT 1")
ContainerPlanConversionCommand find(@Param("bookingId") String bookingId, @Param("key") String key);
@@ -49,6 +49,11 @@ public interface InboundPlanMapper extends BaseMapper<InboundPlan> {
@Select("SELECT * FROM ll_inbound_plan WHERE customer_id=#{customerId} AND source_order_id=#{sourceOrderId} LIMIT 1")
InboundPlan findBySourceOrder(@Param("customerId") String customerId, @Param("sourceOrderId") Long sourceOrderId);
@Select("SELECT COUNT(*) FROM ll_inbound_plan p JOIN ll_order o ON o.customer_id=p.customer_id " +
"AND o.id=p.source_order_id WHERE p.customer_id=#{customerId} AND p.id=#{planId} AND " +
ContainerPlanConversionCommandMapper.UNCONFIRMED_PLAN)
long countUnconfirmedResourcePlans(@Param("customerId") String customerId, @Param("planId") long planId);
@Update("UPDATE ll_inbound_plan SET overseas_warehouse_id=#{warehouseId},updated_at=NOW(3) " +
"WHERE id=#{planId} AND customer_id=#{customerId} " +
"AND (overseas_warehouse_id IS NULL OR overseas_warehouse_id=#{warehouseId})")
@@ -47,7 +47,8 @@ public interface InboundPlanOrderMapper {
"o.detained AS detained,o.version AS version,p.plan_no AS planNo,p.status AS planStatus," +
"p.pallet_count AS plannedPalletCount," +
"(SELECT COUNT(*) FROM ll_inbound_plan_pallet pp WHERE pp.customer_id=p.customer_id " +
"AND pp.plan_id=p.id AND pp.warehouse_pallet_id IS NOT NULL) AS materializedPalletCount," +
"AND pp.plan_id=p.id AND pp.warehouse_pallet_id IS NOT NULL AND NOT " +
ContainerPlanConversionCommandMapper.UNCONFIRMED_PLAN + ") AS materializedPalletCount," +
"(SELECT COUNT(*) FROM ll_order_source_pallet sp WHERE sp.customer_id=o.customer_id " +
"AND sp.order_id=o.id) AS sourcePalletCount " +
"FROM ll_order o LEFT JOIN ll_inbound_plan p ON p.customer_id=o.customer_id AND p.source_order_id=o.id " +
@@ -716,11 +716,14 @@ public class ReceivingContainerService {
&& matchedPlans.stream().allMatch(plan -> List.of("COMPLETED", "PENDING_RECONCILIATION")
.contains(plan.getStatus()));
boolean shippingReady = shippingGates.receivingAvailable(sourceContainerBookingId, containerNo) == 1;
boolean operable = allPlansReady && shippingReady && warehouseSnapshotConsistent;
boolean resourcesConfirmed = matchedPlans.stream().noneMatch(plan ->
plans.countUnconfirmedResourcePlans(plan.getCustomerId(), plan.getId()) > 0);
boolean operable = allPlansReady && resourcesConfirmed && shippingReady && warehouseSnapshotConsistent;
int pending = Math.max(0, totalOrders - plannedOrders);
String blockReason = operable ? null : (!warehouseSnapshotConsistent
? "柜内接柜仓业务快照不一致"
: !allPlansReady ? plannedOrders + "个入仓码已生成计划," + pending + "个订单未操作/缺入仓码"
: !resourcesConfirmed ? "入库计划资源尚未确认,请先在平台恢复原柜级转换命令"
: "柜号尚未达到海外收货可操作节点");
LocalDateTime arrival = orderFacts.stream().map(LastlegOrder::getEstimatedWarehouseAt)
.filter(Objects::nonNull).min(LocalDateTime::compareTo).orElse(null);
@@ -958,6 +961,9 @@ public class ReceivingContainerService {
private void requireMaterializedPlans(List<InboundPlan> containerPlans) {
for (InboundPlan plan : containerPlans) {
if (plans.countUnconfirmedResourcePlans(plan.getCustomerId(), plan.getId()) > 0) {
throw conflict("入库计划资源尚未确认,请先在平台恢复原柜级转换命令");
}
int expected = plan.getPalletCount() == null ? 0 : plan.getPalletCount();
long materialized = pallets.selectCount(new LambdaQueryWrapper<InboundPlanPallet>()
.eq(InboundPlanPallet::getCustomerId, plan.getCustomerId())
@@ -81,9 +81,11 @@ public class ContainerInboundPlanConversionCoordinator {
.anyMatch(order -> "RESOURCE_PENDING".equals(order.readiness()));
if (!preview.ready() || !allLogicalPlansComplete || !needsMaterialization) return manual;
var retry = preview(bookingId).resourceRetry();
List<InboundPlanResponse> materialized = confirm(new ConvertContainerInboundPlanRequest(
bookingId, preview.snapshotVersion(), preview.snapshotHash()),
"AUTO-MANUAL:" + hash(List.of(bookingId, idempotencyKey)));
bookingId, retry == null ? preview.snapshotVersion() : retry.snapshotVersion(),
retry == null ? preview.snapshotHash() : retry.snapshotHash()),
retry == null ? "AUTO-MANUAL:" + hash(List.of(bookingId, idempotencyKey)) : retry.idempotencyKey());
return materialized.stream()
.filter(plan -> Objects.equals(plan.customerId(), manual.customerId())
&& Objects.equals(plan.sourceOrderId(), manual.sourceOrderId()))
@@ -104,6 +106,9 @@ public class ContainerInboundPlanConversionCoordinator {
} else if ("FAILED".equals(resolved.getStatus())) {
resolved = replaceFailedCommand(resolved, bookingId, key, request, requestHash);
} else {
if ("LOCAL_COMMITTED".equals(resolved.getStatus()) && !key.equals(resolved.getIdempotencyKey())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "资源恢复必须使用原柜级转换命令");
}
requireSameRequest(resolved, requestHash);
}
if ("COMPLETED".equals(resolved.getStatus())) {
@@ -71,8 +71,9 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
"THEN 1 ELSE 0 END) AS pendingPlanCount," +
"SUM(CASE WHEN p.id IS NOT NULL AND p.status IN ('PENDING_ASSIGNMENT','SIGNED') " +
"AND COALESCE(p.pallet_count,0)&gt;0 AND " +
"(SELECT COUNT(*) FROM ll_inbound_plan_pallet pp WHERE pp.customer_id=p.customer_id " +
"AND pp.plan_id=p.id AND pp.warehouse_pallet_id IS NOT NULL)&lt;COALESCE(p.pallet_count,0) " +
"(" + com.baiyue.lastleg.inboundplan.mapper.ContainerPlanConversionCommandMapper.UNCONFIRMED_PLAN +
" OR (SELECT COUNT(*) FROM ll_inbound_plan_pallet pp WHERE pp.customer_id=p.customer_id " +
"AND pp.plan_id=p.id AND pp.warehouse_pallet_id IS NOT NULL)&lt;COALESCE(p.pallet_count,0)) " +
"THEN 1 ELSE 0 END) AS resourcePendingPlanCount," +
"CASE WHEN COUNT(o.estimated_warehouse_at)=COUNT(*) " +
"AND COUNT(DISTINCT o.estimated_warehouse_at)=1 THEN MAX(o.estimated_warehouse_at) ELSE NULL END AS plannedArrivalAt," +