diff --git a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/mapper/CarrierScheduleQueryMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/mapper/CarrierScheduleQueryMapper.java index f256e9c9a..79334b6b9 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/mapper/CarrierScheduleQueryMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/mapper/CarrierScheduleQueryMapper.java @@ -63,6 +63,7 @@ public interface CarrierScheduleQueryMapper extends BaseMapper SELECT b.id AS containerBookingId,q.id AS queryId,q.version AS queryVersion, @@ -70,7 +71,7 @@ public interface CarrierScheduleQueryMapper extends BaseMapper#{id} GROUP BY b.id,q.id,q.version,q.status,current.standard_node,current.actual_at diff --git a/firstleg/src/test/java/com/baiyue/firstleg/portschedule/repository/CarrierScheduleQuerySqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/portschedule/repository/CarrierScheduleQuerySqlContractTest.java index 93ddbab9f..13ff0dbed 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/portschedule/repository/CarrierScheduleQuerySqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/portschedule/repository/CarrierScheduleQuerySqlContractTest.java @@ -44,8 +44,9 @@ class CarrierScheduleQuerySqlContractTest { } } + /** 保留批量读取,实际到港与日历采用相同的最新观测顺序。 */ @Test - void batchesActualContainerFactsWithoutEstimatedOrPerNodeQueries() throws Exception { + void batchesActualContainerFactsWithLatestObservedArrivalAndWithoutEstimatedTimes() throws Exception { String sql = String.join("\n", CarrierScheduleQueryMapper.class .getMethod("selectContainerActualFacts", List.class) .getAnnotation(Select.class).value()); @@ -55,13 +56,16 @@ class CarrierScheduleQuerySqlContractTest { .contains("f.actual_at IS NOT NULL") .contains("MAX(CASE WHEN f.standard_node='DEPARTED_WAREHOUSE'") .contains("MAX(CASE WHEN f.standard_node='DEPARTED_ORIGIN_PORT'") - .contains("MAX(CASE WHEN f.standard_node='ARRIVED_DESTINATION_PORT'") + .contains("MAX(arrival.actual_at) AS arrivedDestinationPortAt") + .contains("a.standard_node='ARRIVED_DESTINATION_PORT' AND a.actual_at IS NOT NULL") + .contains("ORDER BY a.observed_at DESC,a.id DESC LIMIT 1") + .doesNotContain("MAX(CASE WHEN f.standard_node='ARRIVED_DESTINATION_PORT'") .contains("MAX(CASE WHEN f.standard_node='PICKUP_READY'") .contains("MAX(CASE WHEN f.standard_node='PICKED_UP'") .contains("MAX(CASE WHEN f.standard_node='EMPTY_RETURNED'") .contains("ORDER BY b.id") .doesNotContain("estimated_at"); - assertThat(count(sql, "fl_carrier_schedule_node_fact")).isEqualTo(3); + assertThat(count(sql, "fl_carrier_schedule_node_fact")).isEqualTo(5); assertThat(count(sql, "SELECT f2.id FROM fl_carrier_schedule_node_fact f2")).isEqualTo(1); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/mapper/ContainerCalendarSourceMapperTest.java b/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/mapper/ContainerCalendarSourceMapperTest.java index c5695c497..6b5a89190 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/mapper/ContainerCalendarSourceMapperTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/terminaldrayage/mapper/ContainerCalendarSourceMapperTest.java @@ -2,6 +2,7 @@ package com.baiyue.firstleg.terminaldrayage.mapper; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder; +import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleQueryMapper; import org.apache.ibatis.mapping.Environment; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.h2.jdbcx.JdbcDataSource; @@ -10,6 +11,51 @@ import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.*; class ContainerCalendarSourceMapperTest { + /** 后续观测可将实际到港修正为更早日期,两入口须取同一条最新实际记录。 */ + @Test + void usesLatestObservedArrivalIncludingSameObservationIdTieBreakAcrossBothEntrypoints() throws Exception { + var ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:arrival-source-correction;MODE=MySQL;DB_CLOSE_DELAY=-1"); + try (var connection = ds.getConnection(); var sql = connection.createStatement()) { + sql.execute("CREATE TABLE fl_container_booking(id BIGINT PRIMARY KEY,cabinet_no VARCHAR,container_no VARCHAR," + + "vessel_name VARCHAR,voyage_no VARCHAR,destination_port_name VARCHAR,destination_warehouse_code VARCHAR," + + "actual_port_arrival_at TIMESTAMP,estimated_port_arrival_at TIMESTAMP,unloading_completed_at TIMESTAMP,record_status VARCHAR)"); + sql.execute("CREATE TABLE fl_terminal_drayage(container_booking_id BIGINT,actual_empty_return_at TIMESTAMP)"); + sql.execute("CREATE TABLE fl_carrier_schedule_query(id BIGINT,version INT,status VARCHAR)"); + sql.execute("CREATE TABLE fl_carrier_schedule_node_fact(id BIGINT,query_id BIGINT,container_booking_id BIGINT," + + "standard_node VARCHAR,actual_at TIMESTAMP,observed_at TIMESTAMP)"); + sql.execute("CREATE TABLE fl_direct_delivery_unloading(container_booking_id BIGINT,actual_at TIMESTAMP)"); + sql.execute("CREATE TABLE fl_outbound_dispatch(container_booking_id BIGINT,confirmed_at TIMESTAMP,reversed_at TIMESTAMP,status VARCHAR)"); + sql.execute("INSERT INTO fl_container_booking(id,cabinet_no,estimated_port_arrival_at,record_status) VALUES " + + "(1,'D1','2026-10-29 08:00:00','ACTIVE'),(2,'D2','2026-10-29 08:00:00','ACTIVE')"); + sql.execute("INSERT INTO fl_carrier_schedule_query VALUES(1,0,'READY'),(2,0,'READY')"); + sql.execute("INSERT INTO fl_outbound_dispatch SELECT id,'2026-10-01 00:00:00',NULL,'WAITING_FOR_LOADING' FROM fl_container_booking"); + sql.execute("INSERT INTO fl_carrier_schedule_node_fact VALUES " + + "(1,1,1,'ARRIVED_DESTINATION_PORT','2026-10-23 14:00:00','2026-10-24 14:00:00')," + + "(2,1,1,'ARRIVED_DESTINATION_PORT','2026-10-21 14:00:00','2026-10-25 14:00:00')," + + "(3,1,1,'ARRIVED_DESTINATION_PORT',NULL,'2026-10-26 14:00:00')," + + "(4,2,2,'ARRIVED_DESTINATION_PORT','2026-10-24 14:00:00','2026-10-25 14:00:00')," + + "(5,2,2,'ARRIVED_DESTINATION_PORT','2026-10-22 14:00:00','2026-10-25 14:00:00')"); + } + var config = new MybatisConfiguration(); + config.setEnvironment(new Environment("test", new JdbcTransactionFactory(), ds)); + config.addMapper(ContainerCalendarSourceMapper.class); + config.addMapper(CarrierScheduleQueryMapper.class); + try (var session = new MybatisSqlSessionFactoryBuilder().build(config).openSession()) { + var from = LocalDateTime.parse("2026-10-01T00:00:00"); + var calendar = session.getMapper(ContainerCalendarSourceMapper.class).sources(from, from.minusDays(2), from.minusDays(4), from.plusMonths(1)); + var workbench = session.getMapper(CarrierScheduleQueryMapper.class).selectContainerActualFacts(java.util.List.of(1L, 2L)); + assertEquals(2, calendar.size()); + assertEquals(2, workbench.size()); + for (int index = 0; index < calendar.size(); index++) { + var expected = LocalDateTime.parse(index == 0 ? "2026-10-21T14:00:00" : "2026-10-22T14:00:00"); + assertEquals(expected, calendar.get(index).arrivalAt()); + assertEquals("ACTUAL", calendar.get(index).arrivalType()); + assertEquals(expected, workbench.get(index).arrivedDestinationPortAt()); + assertEquals(calendar.get(index).arrivalAt().plusDays(2).toLocalDate(), workbench.get(index).arrivedDestinationPortAt().plusDays(2).toLocalDate()); + } + } + } /** 使用实际 Mapper 核对跨月默认日期候选、缺日期与真实出仓边界。 */ @Test void selectsShiftedArrivalUndatedAndOlderReturnCandidatesWithoutInactiveOrUndepartedContainers() throws Exception {