fix(firstleg): project latest port schedule in container reads
This commit is contained in:
@@ -53,29 +53,8 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
SELECT 1 FROM fl_loading_plan_item active_item
|
||||
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0
|
||||
) THEN 'WAIT_LOADING'
|
||||
ELSE p.plan_status END AS plan_status,
|
||||
COALESCE(schedule.vessel_name,b.vessel_name) AS vessel_name,
|
||||
COALESCE(schedule.voyage_no,b.voyage_no) AS voyage_no,
|
||||
COALESCE(schedule.etd_at,b.estimated_sailing_at) AS estimated_sailing_at,
|
||||
COALESCE(schedule.eta_at,b.estimated_port_arrival_at) AS estimated_port_arrival_at
|
||||
ELSE p.plan_status END AS plan_status
|
||||
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
|
||||
LEFT JOIN (
|
||||
SELECT ranked.customer_id,ranked.ocean_booking_id,ranked.container_booking_id,
|
||||
ranked.vessel_name,ranked.voyage_no,ranked.etd_at,ranked.eta_at
|
||||
FROM (
|
||||
SELECT candidate.customer_id,candidate.ocean_booking_id,candidate.container_booking_id,
|
||||
candidate.vessel_name,candidate.voyage_no,candidate.etd_at,candidate.eta_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY candidate.customer_id,candidate.ocean_booking_id,candidate.container_booking_id
|
||||
ORDER BY candidate.observed_at DESC,candidate.updated_at DESC,candidate.id DESC
|
||||
) AS schedule_rank
|
||||
FROM fl_port_schedule candidate
|
||||
) ranked
|
||||
WHERE ranked.schedule_rank=1
|
||||
) schedule
|
||||
ON schedule.customer_id=b.customer_id
|
||||
AND schedule.ocean_booking_id=b.ocean_booking_id
|
||||
AND schedule.container_booking_id=b.id
|
||||
WHERE b.record_status = 'ACTIVE'
|
||||
AND (#{query.containerNo} IS NULL OR b.container_no = #{query.containerNo})
|
||||
AND (#{query.cabinetNo} IS NULL OR b.cabinet_no = #{query.cabinetNo})
|
||||
@@ -160,29 +139,8 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
SELECT 1 FROM fl_loading_plan_item active_item
|
||||
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0
|
||||
) THEN 'WAIT_LOADING'
|
||||
ELSE p.plan_status END AS plan_status,
|
||||
COALESCE(schedule.vessel_name,b.vessel_name) AS vessel_name,
|
||||
COALESCE(schedule.voyage_no,b.voyage_no) AS voyage_no,
|
||||
COALESCE(schedule.etd_at,b.estimated_sailing_at) AS estimated_sailing_at,
|
||||
COALESCE(schedule.eta_at,b.estimated_port_arrival_at) AS estimated_port_arrival_at
|
||||
ELSE p.plan_status END AS plan_status
|
||||
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
|
||||
LEFT JOIN (
|
||||
SELECT ranked.customer_id,ranked.ocean_booking_id,ranked.container_booking_id,
|
||||
ranked.vessel_name,ranked.voyage_no,ranked.etd_at,ranked.eta_at
|
||||
FROM (
|
||||
SELECT candidate.customer_id,candidate.ocean_booking_id,candidate.container_booking_id,
|
||||
candidate.vessel_name,candidate.voyage_no,candidate.etd_at,candidate.eta_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY candidate.customer_id,candidate.ocean_booking_id,candidate.container_booking_id
|
||||
ORDER BY candidate.observed_at DESC,candidate.updated_at DESC,candidate.id DESC
|
||||
) AS schedule_rank
|
||||
FROM fl_port_schedule candidate
|
||||
) ranked
|
||||
WHERE ranked.schedule_rank=1
|
||||
) schedule
|
||||
ON schedule.customer_id=b.customer_id
|
||||
AND schedule.ocean_booking_id=b.ocean_booking_id
|
||||
AND schedule.container_booking_id=b.id
|
||||
WHERE b.id = #{id} AND b.record_status = 'ACTIVE'
|
||||
""")
|
||||
ContainerBookingEntity selectBooking(@Param("id") long id);
|
||||
|
||||
@@ -6,19 +6,22 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.Query;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
|
||||
import com.baiyue.firstleg.portschedule.entity.PortScheduleEntity;
|
||||
import com.baiyue.firstleg.portschedule.mapper.PortScheduleMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ContainerRepository {
|
||||
@Resource private ContainerBookingMapper mapper;
|
||||
@Resource private PortScheduleMapper schedules;
|
||||
public PageResult<ContainerBookingEntity> page(String workbench, Query query) {
|
||||
Page<ContainerBookingEntity> result = mapper.selectBookingPage(
|
||||
new Page<>(query.page(), query.size()), query, workbench);
|
||||
result.getRecords().forEach(this::resolveLoadingTime);
|
||||
result.getRecords().forEach(this::resolveReadModel);
|
||||
return PageResult.from(result);
|
||||
}
|
||||
public ContainerBookingEntity find(long id) { return resolveLoadingTime(mapper.selectBooking(id)); }
|
||||
public ContainerBookingEntity find(long id) { return resolveReadModel(mapper.selectBooking(id)); }
|
||||
public ContainerBookingEntity findForUpdate(long id) { return mapper.selectBookingForUpdate(id); }
|
||||
public ContainerBookingEntity findActiveForUpdate(long id) { return mapper.selectActiveBookingForUpdate(id); }
|
||||
public boolean departed(long id) { return mapper.hasActiveOutboundDeparture(id) > 0; }
|
||||
@@ -62,6 +65,21 @@ public class ContainerRepository {
|
||||
estimatedSailingAt, operatorId, operatorName);
|
||||
}
|
||||
|
||||
private ContainerBookingEntity resolveReadModel(ContainerBookingEntity value) {
|
||||
resolveCurrentPortSchedule(value);
|
||||
return resolveLoadingTime(value);
|
||||
}
|
||||
|
||||
private void resolveCurrentPortSchedule(ContainerBookingEntity value) {
|
||||
if (value == null || value.getCustomerId() == null || value.getOceanBookingId() == null) return;
|
||||
PortScheduleEntity schedule = schedules.latestForDispatch(value.getCustomerId(), value.getOceanBookingId(), value.getId());
|
||||
if (schedule == null) return;
|
||||
value.setVesselName(schedule.getVesselName());
|
||||
value.setVoyageNo(schedule.getVoyageNo());
|
||||
value.setEstimatedSailingAt(schedule.getEtdAt());
|
||||
value.setEstimatedPortArrivalAt(schedule.getEtaAt());
|
||||
}
|
||||
|
||||
private ContainerBookingEntity resolveLoadingTime(ContainerBookingEntity value) {
|
||||
if (value != null && value.getLoadingAt() == null
|
||||
&& "COMPLETED".equals(value.getPlanStatus())) {
|
||||
|
||||
-20
@@ -57,26 +57,6 @@ class ContainerBookingMapperContractTest {
|
||||
.contains("customer_item.account_id = #{query.customerId}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("平台货柜读模型投影最新港务船期但保留订舱事实")
|
||||
void projectsLatestPortScheduleForPlatformReads() throws Exception {
|
||||
String detail = String.join("\n", ContainerBookingMapper.class
|
||||
.getDeclaredMethod("selectBooking", long.class).getAnnotation(Select.class).value());
|
||||
String page = String.join("\n", ContainerBookingMapper.class
|
||||
.getDeclaredMethod("selectBookingPage", com.baomidou.mybatisplus.extension.plugins.pagination.Page.class,
|
||||
com.baiyue.firstleg.dto.ContainerDtos.Query.class, String.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
for (String sql : java.util.List.of(detail, page)) {
|
||||
assertThat(sql)
|
||||
.contains("fl_port_schedule", "ROW_NUMBER() OVER", "PARTITION BY candidate.customer_id",
|
||||
"COALESCE(schedule.vessel_name,b.vessel_name)",
|
||||
"COALESCE(schedule.voyage_no,b.voyage_no)",
|
||||
"COALESCE(schedule.etd_at,b.estimated_sailing_at)",
|
||||
"COALESCE(schedule.eta_at,b.estimated_port_arrival_at)")
|
||||
.doesNotContain("actual_sailing_at=schedule", "actual_port_arrival_at=schedule");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("关联订舱 SQL 的预计到港参数与 Mapper 参数名一致")
|
||||
void linkReleasedBookingUsesDeclaredEstimatedArrivalParameter() {
|
||||
|
||||
+29
@@ -2,6 +2,8 @@ package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
|
||||
import com.baiyue.firstleg.portschedule.entity.PortScheduleEntity;
|
||||
import com.baiyue.firstleg.portschedule.mapper.PortScheduleMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -16,6 +18,7 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ContainerRepositoryLoadingTimeTest {
|
||||
@Mock private ContainerBookingMapper mapper;
|
||||
@Mock private PortScheduleMapper schedules;
|
||||
@InjectMocks private ContainerRepository repository;
|
||||
|
||||
@Test
|
||||
@@ -44,4 +47,30 @@ class ContainerRepositoryLoadingTimeTest {
|
||||
|
||||
assertThat(result.getLoadingAt()).isEqualTo(plannedAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void projectsLatestPortScheduleWithoutChangingContainerIdentity() {
|
||||
ContainerBookingEntity booking = new ContainerBookingEntity();
|
||||
booking.setId(8L);
|
||||
booking.setCustomerId(9L);
|
||||
booking.setOceanBookingId(10L);
|
||||
booking.setContainerNo("TSTU0000001");
|
||||
booking.setVesselName("OLD-VESSEL");
|
||||
booking.setVoyageNo("OLD-V01");
|
||||
PortScheduleEntity schedule = new PortScheduleEntity();
|
||||
schedule.setVesselName("NEW-VESSEL");
|
||||
schedule.setVoyageNo("NEW-V01");
|
||||
schedule.setEtdAt(LocalDateTime.of(2026, 10, 7, 8, 0));
|
||||
schedule.setEtaAt(LocalDateTime.of(2026, 10, 27, 8, 0));
|
||||
when(mapper.selectBooking(8L)).thenReturn(booking);
|
||||
when(schedules.latestForDispatch(9L, 10L, 8L)).thenReturn(schedule);
|
||||
|
||||
ContainerBookingEntity result = repository.find(8L);
|
||||
|
||||
assertThat(result.getContainerNo()).isEqualTo("TSTU0000001");
|
||||
assertThat(result.getVesselName()).isEqualTo("NEW-VESSEL");
|
||||
assertThat(result.getVoyageNo()).isEqualTo("NEW-V01");
|
||||
assertThat(result.getEstimatedSailingAt()).isEqualTo(schedule.getEtdAt());
|
||||
assertThat(result.getEstimatedPortArrivalAt()).isEqualTo(schedule.getEtaAt());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user