生产更新:同步已提交的海外仓分析及业务修复(2026-10-16) #27

Merged
admin merged 138 commits from test into main 2026-10-16 18:02:59 +08:00
4 changed files with 37 additions and 12 deletions
Showing only changes of commit 4277601999 - Show all commits
@@ -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,
@@ -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));
@@ -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());
}
}
}
@@ -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) {