fix: isolate invalid container progress snapshots

This commit is contained in:
2026-09-07 16:36:21 +08:00
parent d622ddd37f
commit e0bb7de0cf
2 changed files with 66 additions and 16 deletions
@@ -86,7 +86,7 @@ public class LastlegOrderService {
long total = orderMapper.countContainerProgress(query); long total = orderMapper.countContainerProgress(query);
List<ContainerPlanProgressSummary> records = total == 0 ? List.of() List<ContainerPlanProgressSummary> records = total == 0 ? List.of()
: orderMapper.pageContainerProgress(query, (query.page() - 1) * query.size(), query.size()) : orderMapper.pageContainerProgress(query, (query.page() - 1) * query.size(), query.size())
.stream().map(this::requireFrozenContainer).toList(); .stream().map(this::containerSummaryForRead).toList();
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size(); long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
return new PageResult<>(records, query.page(), query.size(), total, pages); return new PageResult<>(records, query.page(), query.size(), total, pages);
} }
@@ -101,7 +101,7 @@ public class LastlegOrderService {
if (summary == null) { if (summary == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
} }
return new ContainerPlanProgressDetail(requireFrozenContainer(summary), return new ContainerPlanProgressDetail(containerSummaryForRead(summary),
orderMapper.findContainerProgressItems(bookingId)); orderMapper.findContainerProgressItems(bookingId));
} }
@@ -150,17 +150,42 @@ public class LastlegOrderService {
private ContainerPlanProgressSummary requireFrozenContainer(ContainerPlanProgressSummary summary) { private ContainerPlanProgressSummary requireFrozenContainer(ContainerPlanProgressSummary summary) {
if (summary == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在"); if (summary == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
String issue = containerSnapshotIssue(summary);
if (issue != null) throw new ResponseStatusException(HttpStatus.CONFLICT, issue);
return summary;
}
private ContainerPlanProgressSummary containerSummaryForRead(ContainerPlanProgressSummary summary) {
String issue = containerSnapshotIssue(summary);
if (issue == null) return summary;
boolean invalidContainer = !hasCanonicalContainerNo(summary.containerNo());
return new ContainerPlanProgressSummary(summary.sourceContainerBookingId(), summary.containerNo(),
summary.overseasWarehouseId(), summary.overseasWarehouseCode(), summary.overseasWarehouseName(),
summary.assignmentVersion(), invalidContainer ? false : summary.canChangeReceivingWarehouse(),
invalidContainer ? issue : summary.receivingWarehouseLockReason(), summary.totalOrderCount(),
summary.inboundCodeCount(), summary.missingInboundCodeCount(), summary.planCount(),
summary.plannedInboundCodeCount(), summary.pendingPlanCount(), summary.plannedArrivalAt(),
false, issue);
}
private String containerSnapshotIssue(ContainerPlanProgressSummary summary) {
if (!hasCanonicalContainerNo(summary.containerNo())) {
return "来源柜真实集装箱号缺失、格式无效或存在漂移";
}
if (!StringUtils.hasText(summary.overseasWarehouseId())
|| !StringUtils.hasText(summary.overseasWarehouseCode())
|| !StringUtils.hasText(summary.overseasWarehouseName()) || summary.assignmentVersion() < 0) {
return "柜内接柜仓业务快照不一致";
}
return null;
}
private boolean hasCanonicalContainerNo(String containerNo) {
try { try {
ContainerNumbers.requireCanonical(summary.containerNo()); ContainerNumbers.requireCanonical(containerNo);
if (!StringUtils.hasText(summary.overseasWarehouseId()) return true;
|| !StringUtils.hasText(summary.overseasWarehouseCode())
|| !StringUtils.hasText(summary.overseasWarehouseName()) || summary.assignmentVersion() < 0) {
throw new IllegalArgumentException("receiving warehouse assignment drift");
}
return summary;
} catch (IllegalArgumentException exception) { } catch (IllegalArgumentException exception) {
throw new ResponseStatusException(HttpStatus.CONFLICT, return false;
"来源柜真实集装箱号缺失、格式无效或存在漂移", exception);
} }
} }
@@ -30,7 +30,9 @@ import java.time.LocalDateTime;
import java.math.BigDecimal; import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -319,16 +321,39 @@ class LastlegOrderServiceTest {
} }
@Test @Test
void containerProgressDetailFailsClosedWhenContainerNumberFactHasDrifted() { void containerProgressDetailKeepsInvalidHistoricalContainerReadableAndBlocked() {
var drifted = new ContainerPlanProgressSummary("BOOKING-1", null, 2, 2, 0, var drifted = new ContainerPlanProgressSummary("BOOKING-1", null, 2, 2, 0,
2, 2, 0, LocalDateTime.of(2026, 10, 3, 9, 0), false, "柜号缺失或不一致"); 2, 2, 0, LocalDateTime.of(2026, 10, 3, 9, 0), false, "柜号缺失或不一致");
when(orders.findContainerProgress("BOOKING-1")).thenReturn(drifted); when(orders.findContainerProgress("BOOKING-1")).thenReturn(drifted);
when(orders.findContainerProgressItems("BOOKING-1")).thenReturn(List.of());
ResponseStatusException error = assertThrows(ResponseStatusException.class, var result = service.containerProgressDetail("BOOKING-1");
() -> service.containerProgressDetail("BOOKING-1"));
assertEquals(409, error.getStatusCode().value()); assertFalse(result.summary().allPlansReady());
verify(orders, never()).findContainerProgressItems(any()); assertFalse(result.summary().canChangeReceivingWarehouse());
assertEquals("来源柜真实集装箱号缺失、格式无效或存在漂移",
result.summary().blockingReason());
assertEquals(result.summary().blockingReason(), result.summary().receivingWarehouseLockReason());
verify(orders).findContainerProgressItems("BOOKING-1");
}
@Test
void containerProgressPageKeepsMissingWarehouseSnapshotFromBreakingHealthyContainers() {
var query = new LastlegOrderRequests.ContainerProgressQuery(1, 10, null, null, null);
var healthy = summary(false, 3, "存在3票尚未生成有效入库计划");
var missingWarehouse = new ContainerPlanProgressSummary("BOOKING-2", "MSCU6639870",
null, null, null, 0, true, null, 2, 2, 0, 0, 0, 2,
LocalDateTime.of(2026, 10, 4, 9, 0), false, "存在2票尚未生成有效入库计划");
when(orders.countContainerProgress(query)).thenReturn(2L);
when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(healthy, missingWarehouse));
var result = service.containerProgressPage(query);
assertEquals(2, result.records().size());
assertSame(healthy, result.records().get(0));
assertEquals("柜内接柜仓业务快照不一致", result.records().get(1).blockingReason());
assertFalse(result.records().get(1).allPlansReady());
assertTrue(result.records().get(1).canChangeReceivingWarehouse());
} }
private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) { private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) {