生产更新:同步已提交的海外仓分析及业务修复(2026-10-16) #27
+15
-2
@@ -15,7 +15,7 @@ public interface AppointmentWorkSourceMapper {
|
||||
public Cargo cargo() { return new Cargo(inboundId,customerId,orderId,orderNo,inboundCode,destinationKind,destinationCode,destinationName,packageCount); }
|
||||
}
|
||||
|
||||
String SOURCES = """
|
||||
String SOURCE_ROWS = """
|
||||
SELECT b.id,b.cabinet_no,b.container_no,b.departure_port_name,b.destination_warehouse_name,
|
||||
MIN(CASE WHEN f.standard_node='DEPARTED_ORIGIN_PORT' THEN f.actual_at END) departed_local_at,
|
||||
MIN(f.actual_at) sailed_evidence_local_at,
|
||||
@@ -25,13 +25,26 @@ public interface AppointmentWorkSourceMapper {
|
||||
WHERE b.record_status='ACTIVE' AND f.actual_at IS NOT NULL
|
||||
AND f.standard_node IN ('DEPARTED_ORIGIN_PORT','IN_TRANSIT','ARRIVED_DESTINATION_PORT',
|
||||
'DISCHARGED_FROM_VESSEL','PICKUP_READY','PICKED_UP','EMPTY_RETURNED')
|
||||
""";
|
||||
String SOURCE_ORDER = """
|
||||
GROUP BY b.id,b.cabinet_no,b.container_no,b.departure_port_name,b.destination_warehouse_name,b.version
|
||||
ORDER BY b.id
|
||||
""";
|
||||
// A complete identity/version manifest also detects deletions or edits between cursor pages.
|
||||
@Select(SOURCES)
|
||||
@Select(SOURCE_ROWS + SOURCE_ORDER)
|
||||
List<Source> sources();
|
||||
|
||||
/** Same live eligibility and aggregate facts as the manifest, restricted before aggregation. */
|
||||
@Select("<script>" + SOURCE_ROWS + """
|
||||
<choose>
|
||||
<when test="ids != null and !ids.isEmpty()">
|
||||
AND b.id IN <foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>
|
||||
</when>
|
||||
<otherwise>AND 1=0</otherwise>
|
||||
</choose>
|
||||
""" + SOURCE_ORDER + "</script>")
|
||||
List<Source> sourcesByIds(@Param("ids") List<Long> ids);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT p.container_booking_id,i.inbound_id, i.account_id customer_id, i.order_id, i.order_no,i.inbound_code,
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ public class AppointmentWorkSourceService {
|
||||
throw bad("事项身份或事实类型无效");
|
||||
ids.add(id(request.containerBookingId()));
|
||||
}
|
||||
var selected=mapper.sources().stream().filter(row->ids.contains(row.id())).toList();
|
||||
var selected=mapper.sourcesByIds(List.copyOf(ids));
|
||||
if(selected.size()!=ids.size()) throw new ResponseStatusException(HttpStatus.NOT_FOUND,"未找到有效已开船柜次");
|
||||
var sources=new LinkedHashMap<Long,ContainerSource>();
|
||||
sources(selected).forEach(source->sources.put(Long.parseLong(source.containerBookingId()),source));
|
||||
|
||||
+13
-3
@@ -18,15 +18,25 @@ class AppointmentWorkSourceMapperTest {
|
||||
s.execute("CREATE TABLE fl_direct_delivery_unloading(container_booking_id BIGINT)");
|
||||
s.execute("INSERT INTO fl_container_booking VALUES(1,'D1',NULL,'上海',NULL,1,'ACTIVE'),(2,'D2','C2','上海','私人仓',1,'ACTIVE'),(3,'D3','C3','上海',NULL,1,'ACTIVE'),(4,'D4','C4','上海',NULL,1,'MERGED')");
|
||||
s.execute("INSERT INTO fl_carrier_schedule_node_fact VALUES(10,1,'DEPARTED_ORIGIN_PORT','2026-10-01'),(20,2,'EMPTY_RETURNED','2026-10-03'),(30,3,'LOADED_ON_VESSEL','2026-10-04'),(40,4,'DEPARTED_ORIGIN_PORT','2026-10-01'),(50,3,'DEPARTED_ORIGIN_PORT',NULL)");
|
||||
s.execute("INSERT INTO fl_direct_delivery_unloading VALUES(2)");
|
||||
s.execute("INSERT INTO fl_direct_delivery_unloading VALUES(2),(2)");
|
||||
s.execute("INSERT INTO fl_carrier_schedule_node_fact VALUES(60,1,'IN_TRANSIT','2026-10-02'),(70,1,'ARRIVED_DESTINATION_PORT','2026-10-03'),(80,2,'EMPTY_RETURNED',NULL)");
|
||||
}
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);
|
||||
config.setEnvironment(new Environment("test",new JdbcTransactionFactory(),ds));config.addMapper(AppointmentWorkSourceMapper.class);
|
||||
try(var session=new MybatisSqlSessionFactoryBuilder().build(config).openSession()) {
|
||||
var rows=session.getMapper(AppointmentWorkSourceMapper.class).sources();
|
||||
var mapper=session.getMapper(AppointmentWorkSourceMapper.class);
|
||||
var rows=mapper.sources();
|
||||
assertEquals(2,rows.size());assertEquals(1,rows.get(0).id());assertEquals(2,rows.get(1).id());
|
||||
assertNull(rows.get(0).destinationWarehouseName());assertEquals(1,rows.get(1).deliveryVersion());
|
||||
assertNull(rows.get(0).destinationWarehouseName());assertEquals(2,rows.get(1).deliveryVersion());
|
||||
assertNull(rows.get(1).departedLocalAt());assertNotNull(rows.get(1).sailedEvidenceLocalAt());
|
||||
assertEquals(70,rows.get(0).factVersion());assertEquals(20,rows.get(1).factVersion());
|
||||
// The optimized path must equal the former full-manifest + Java filter, including
|
||||
// dates, versions, null names, unloading counts, duplicate IDs and ineligible sources.
|
||||
for(var ids:java.util.List.of(java.util.List.of(2L,1L),java.util.List.of(1L,1L),
|
||||
java.util.List.of(2L,3L,4L,999L),java.util.List.of(3L,4L),java.util.List.<Long>of())) {
|
||||
assertEquals(rows.stream().filter(row->ids.contains(row.id())).toList(),mapper.sourcesByIds(ids));
|
||||
}
|
||||
assertTrue(mapper.sourcesByIds(null).isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-6
@@ -52,8 +52,8 @@ class AppointmentWorkSourceServiceTest {
|
||||
when(mapper.sources()).thenReturn(List.of(source(1,1)));
|
||||
assertEquals(503,assertThrows(ResponseStatusException.class,()->service.container(1)).getStatusCode().value());
|
||||
}
|
||||
@Test void factBatchReadsManifestOnceAndPreservesEachContainerIdentity() {
|
||||
when(mapper.sources()).thenReturn(IntStream.rangeClosed(1,100).mapToObj(id->source(id,1)).toList());
|
||||
@Test void factBatchReadsOnlyRequestedSourcesAndPreservesEachContainerIdentity() {
|
||||
when(mapper.sourcesByIds(anyList())).thenReturn(IntStream.rangeClosed(1,100).mapToObj(id->source(id,1)).toList());
|
||||
when(mapper.cargo(anyList())).thenReturn(List.of());
|
||||
stubFlows("UNKNOWN");
|
||||
var fact=mock(Fact.class);
|
||||
@@ -66,19 +66,21 @@ class AppointmentWorkSourceServiceTest {
|
||||
var requests=IntStream.rangeClosed(1,100).boxed().flatMap(id->List.of(
|
||||
request("PICKUP-"+id,"PICKUP",id),request("RETURN-"+id,"EMPTY_RETURN",id)).stream()).toList();
|
||||
assertEquals(200,service.facts(requests).size());
|
||||
verify(mapper,times(1)).sources();
|
||||
verify(mapper,times(1)).sourcesByIds(IntStream.rangeClosed(1,100).mapToObj(Long::valueOf).toList());
|
||||
verify(mapper,never()).sources();
|
||||
verify(mapper,times(1)).cargo(IntStream.rangeClosed(1,100).mapToObj(Long::valueOf).toList());
|
||||
verify(facts,times(1)).establishedFlows(anyList());
|
||||
verify(facts,times(1)).read(any(),any());
|
||||
}
|
||||
@Test void nextFactBatchReadsFreshManifestAndRejectsRemovedContainer() {
|
||||
when(mapper.sources()).thenReturn(List.of(source(1,1)),List.of());
|
||||
@Test void nextFactBatchReadsFreshSourcesAndRejectsRemovedContainer() {
|
||||
when(mapper.sourcesByIds(List.of(1L))).thenReturn(List.of(source(1,1)),List.of());
|
||||
when(mapper.cargo(List.of(1L))).thenReturn(List.of());
|
||||
stubFlows("UNKNOWN");
|
||||
var request=request("PICKUP-1","PICKUP",1);
|
||||
service.facts(List.of(request));
|
||||
assertEquals(404,assertThrows(ResponseStatusException.class,()->service.facts(List.of(request))).getStatusCode().value());
|
||||
verify(mapper,times(2)).sources();
|
||||
verify(mapper,times(2)).sourcesByIds(List.of(1L));
|
||||
verify(mapper,never()).sources();
|
||||
verify(facts,times(1)).read(any(),any());
|
||||
}
|
||||
private void stubFlows(String flow) {
|
||||
|
||||
Reference in New Issue
Block a user