feat(appointments): expose authoritative arrival forecast observations
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.firstleg.portschedule.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.portschedule.service.AppointmentForecastService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/firstleg/appointment-forecasts")
|
||||
public class InternalAppointmentForecastController {
|
||||
public record Request(List<Long> containerBookingIds) { }
|
||||
private final AppointmentForecastService service;
|
||||
private final String token;
|
||||
public InternalAppointmentForecastController(AppointmentForecastService service,
|
||||
@Value("${baiyue.security.internal-service-token:}") String token){this.service=service;this.token=token;}
|
||||
@PostMapping("/batch")
|
||||
public ApiResponse<List<AppointmentForecastService.Forecast>> batch(
|
||||
@RequestHeader(value="X-Internal-Service-Token",required=false) String supplied,@RequestBody Request request) {
|
||||
if(token==null || token.isBlank() || supplied==null || !MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8),supplied.getBytes(StandardCharsets.UTF_8)))
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,"内部身份无效");
|
||||
return ApiResponse.success(service.batch(request==null?null:request.containerBookingIds()));
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.firstleg.portschedule.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AppointmentForecastMapper {
|
||||
record Fact(Long containerBookingId,Long factId,String providerCode,String sourceReference,
|
||||
LocalDateTime observedAt,LocalDateTime estimatedAt,LocalDateTime actualAt) { }
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT container_booking_id,fact_id,provider_code,source_reference,observed_at,estimated_at,actual_at
|
||||
FROM (
|
||||
SELECT f.container_booking_id,f.id AS fact_id,f.provider_code,f.source_reference,
|
||||
f.observed_at,f.estimated_at,f.actual_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY f.container_booking_id ORDER BY f.observed_at DESC,f.id DESC) AS position
|
||||
FROM fl_carrier_schedule_node_fact f JOIN fl_container_booking c ON c.id=f.container_booking_id
|
||||
WHERE c.record_status='ACTIVE' AND f.standard_node='ARRIVED_DESTINATION_PORT'
|
||||
AND f.container_booking_id IN
|
||||
<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>
|
||||
) ranked WHERE position<=2 ORDER BY container_booking_id,position
|
||||
</script>
|
||||
""")
|
||||
List<Fact> latest(@Param("ids") List<Long> containerIds);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.firstleg.portschedule.service;
|
||||
|
||||
import com.baiyue.firstleg.portschedule.mapper.AppointmentForecastMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Forecast facts are read from their owning service, never inferred from an order snapshot. */
|
||||
@Service
|
||||
public class AppointmentForecastService {
|
||||
public record Forecast(Long containerBookingId,AppointmentForecastMapper.Fact latest,
|
||||
AppointmentForecastMapper.Fact previous,String timeBasis) { }
|
||||
private final AppointmentForecastMapper mapper;
|
||||
public AppointmentForecastService(AppointmentForecastMapper mapper){this.mapper=mapper;}
|
||||
public List<Forecast> batch(List<Long> ids) {
|
||||
if(ids==null || ids.isEmpty() || ids.size()>200 || ids.stream().anyMatch(id->id==null || id<=0)
|
||||
|| new HashSet<>(ids).size()!=ids.size()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"每批须提供1至200个不重复的柜次");
|
||||
var facts=mapper.latest(ids).stream().collect(Collectors.groupingBy(AppointmentForecastMapper.Fact::containerBookingId));
|
||||
return ids.stream().map(id->{
|
||||
var rows=facts.getOrDefault(id,List.of());
|
||||
return new Forecast(id,rows.isEmpty()?null:rows.get(0),rows.size()<2?null:rows.get(1),"SOURCE_LOCAL_UNSPECIFIED");
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user