feat(appointments): expose bounded terminal pickup calendar

This commit is contained in:
2026-10-10 11:43:47 +08:00
parent 47dc9e8bf6
commit 46ae8ab1c6
9 changed files with 271 additions and 0 deletions
@@ -12,6 +12,11 @@ import java.util.List;
@FeignClient(name = "warehouse-service", contextId = "firstlegWarehouseMasterDataClient",
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouses")
public interface WarehouseMasterDataClient {
@GetMapping("/order-catalog")
ApiResponse<List<CatalogWarehouse>> catalog(@RequestHeader("X-Internal-Service-Token") String token);
record CatalogWarehouse(Long id, String warehouseCode, String warehouseName, String warehouseType, String countryCode) { }
@GetMapping("/{tenantId}/{warehouseId}/active")
ApiResponse<Detail> active(@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long tenantId,
@@ -41,6 +41,17 @@ import java.util.List;
@RequestMapping("/api/platform/v1/terminal-drayages")
public class PlatformTerminalDrayageController {
@Resource private TerminalDrayageMailService mails;
@Resource private com.baiyue.firstleg.terminaldrayage.service.TerminalDrayageCalendarService calendar;
@GetMapping("/calendar")
@PreAuthorize("hasAuthority('platform:appointment:list') and hasAuthority('platform:firstleg:terminal-drayage:list')")
public ApiResponse<com.baiyue.firstleg.terminaldrayage.service.TerminalDrayageCalendarService.Calendar> calendar(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @org.springframework.format.annotation.DateTimeFormat(iso=org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate from,
@RequestParam @org.springframework.format.annotation.DateTimeFormat(iso=org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate to) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(calendar.calendar(from,to));
}
@PostMapping("/{containerBookingId}/pickup-email")
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:fleet:edit')")
@@ -0,0 +1,8 @@
package com.baiyue.firstleg.terminaldrayage.dto;
import java.time.LocalDateTime;
/** Original pickup appointment wall time; the source does not establish a port time zone. */
public record PickupCalendarEntry(Long containerBookingId, String bookingNo, String containerNo,
String destinationPortName, String destinationWarehouseCode, String fleetSupplierName,
LocalDateTime pickupAppointmentAt, LocalDateTime actualPickupAt) { }
@@ -14,6 +14,19 @@ import java.util.List;
@Mapper
public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity> {
@Select("""
SELECT c.id containerBookingId,c.booking_no bookingNo,c.container_no containerNo,
c.destination_port_name destinationPortName,c.destination_warehouse_code destinationWarehouseCode,
t.fleet_supplier_name fleetSupplierName,t.pickup_appointment_at pickupAppointmentAt,NULL actualPickupAt
FROM fl_terminal_drayage t JOIN fl_container_booking c ON c.id=t.container_booking_id
WHERE c.record_status='ACTIVE' AND t.pickup_appointment_at>=#{from} AND t.pickup_appointment_at<#{to}
AND EXISTS(SELECT 1 FROM fl_outbound_dispatch d WHERE d.container_booking_id=c.id
AND d.confirmed_at IS NOT NULL AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING')
ORDER BY t.pickup_appointment_at,c.id LIMIT #{limit}
""")
List<com.baiyue.firstleg.terminaldrayage.dto.PickupCalendarEntry> calendar(
@Param("from") LocalDateTime from,@Param("to") LocalDateTime to,@Param("limit") int limit);
@Select("""
SELECT b.id AS containerBookingId,b.container_no AS containerNo,b.ocean_booking_id AS oceanBookingId,
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt
@@ -0,0 +1,57 @@
package com.baiyue.firstleg.terminaldrayage.service;
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
import com.baiyue.firstleg.terminaldrayage.dto.PickupCalendarEntry;
import com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper;
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.*;
@Service
public class TerminalDrayageCalendarService {
private final TerminalDrayageMapper mapper;
private final WarehouseMasterDataClient warehouses;
private final String token;
private final CarrierScheduleQueryService shipping;
public TerminalDrayageCalendarService(TerminalDrayageMapper mapper,WarehouseMasterDataClient warehouses,
CarrierScheduleQueryService shipping,
@Value("${baiyue.security.internal-service-token}") String token) {this.mapper=mapper;this.warehouses=warehouses;this.shipping=shipping;this.token=token;}
public record Calendar(List<PickupCalendarEntry> records,boolean truncated,int unclassifiedCount,
String scopeBasis,String timeBasis) { }
public Calendar calendar(LocalDate from,LocalDate to) {
if(from==null || to==null || !to.isAfter(from) || ChronoUnit.DAYS.between(from,to)>42)
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"日历查询范围必须为1至42天");
var rows=mapper.calendar(from.atStartOfDay(),to.atStartOfDay(),1001);
if(rows.isEmpty()) return result(List.of(),false,0);
final Map<String,String> countries=new HashMap<>();
try {
var response=warehouses.catalog(token);
if(response==null || !response.successful() || response.data()==null) throw new IllegalStateException();
for(var warehouse:response.data()) if(warehouse!=null && warehouse.warehouseCode()!=null && warehouse.countryCode()!=null)
countries.put(warehouse.warehouseCode(),warehouse.countryCode());
} catch(RuntimeException error) {throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"提柜目的仓范围暂不可核实",error);}
var records=new ArrayList<PickupCalendarEntry>();int unknown=0;
for(var row:rows.stream().limit(1000).toList()) {
var country=countries.get(row.destinationWarehouseCode());
if(country==null || country.isBlank()) unknown++;
else if("US".equals(country)) records.add(row);
}
var actual=new HashMap<Long,java.time.LocalDateTime>();
var ids=records.stream().map(PickupCalendarEntry::containerBookingId).distinct().toList();
for(int start=0;start<ids.size();start+=200) {
for(var fact:shipping.containerActualFacts(ids.subList(start,Math.min(start+200,ids.size()))))
if(fact.pickedUpAt()!=null) actual.put(fact.containerBookingId(),fact.pickedUpAt());
}
var enriched=records.stream().map(row->new PickupCalendarEntry(row.containerBookingId(),row.bookingNo(),row.containerNo(),
row.destinationPortName(),row.destinationWarehouseCode(),row.fleetSupplierName(),row.pickupAppointmentAt(),actual.get(row.containerBookingId()))).toList();
return result(enriched,rows.size()>1000,unknown);
}
private Calendar result(List<PickupCalendarEntry> records,boolean truncated,int unknown) {
return new Calendar(List.copyOf(records),truncated,unknown,"US_DESTINATION_WAREHOUSE","SOURCE_LOCAL_UNSPECIFIED");
}
}