diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarRepository.java index 3e877bc27..21f0e00c2 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarRepository.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarRepository.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository; /** 日历只读访问保留真实主单范围,整批读取原货物快照而不逐车查询。 */ @Repository public class FleetTripCalendarRepository { - public record Source(String dispatchNo, String sourceSnapshotJson) { } + public record Source(String dispatchNo, String sourceSnapshotJson, String loadingPayloadJson) { } private final FleetTripMapper mapper; /** 装配现有车次Mapper,日历服务不直接访问SQL。 */ @@ -18,10 +18,10 @@ public class FleetTripCalendarRepository { return mapper.calendar(from, to); } - /** 一次读取指定主单的全部货物快照,空窗口不发送空IN查询。 */ + /** 一次读取指定主单的全部货物与原装柜快照,空窗口不发送空IN查询。 */ public List sourceSnapshots(List dispatchNumbers) { if (dispatchNumbers.isEmpty()) return List.of(); return mapper.calendarSources(dispatchNumbers).stream() - .map(row -> new Source(row.dispatchNo(), row.sourceSnapshotJson())).toList(); + .map(row -> new Source(row.dispatchNo(), row.sourceSnapshotJson(), row.loadingPayloadJson())).toList(); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarService.java index caeabfde1..8c70177db 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarService.java @@ -19,8 +19,9 @@ public class FleetTripCalendarService { String originWarehouseCode, String originWarehouseName, String originCountryCode, String originTimezone, String originWarehouseStatus, Instant plannedDepartureAt, Instant expectedDriverEndAt, Instant departedAt, Instant completedAt, - List stops, List containerNumbers, List incompleteReasons) { } + List stops, List containerNumbers, List cabinetNumbers, List incompleteReasons) { } public record Result(List records, List unresolvedRecords) { } + private record ContainerReferences(List containerNumbers, List cabinetNumbers) { } private static final Set COUNTRIES = Set.of(Locale.getISOCountries()); private final FleetTripCalendarRepository trips; private final FleetWarehouseClient warehouses; @@ -41,7 +42,7 @@ public class FleetTripCalendarService { var rows=trips.calendar(LocalDateTime.ofInstant(from,ZoneOffset.UTC),LocalDateTime.ofInstant(to,ZoneOffset.UTC)); if (rows.size()>2000) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"排车过多,请缩小查询日期范围"); if (rows.isEmpty()) return new Result(List.of(),List.of()); - var containers=containerNumbers(rows); + var containers=containerReferences(rows); var ids=rows.stream().map(FleetTripEntity::getWarehouseId).filter(Objects::nonNull).filter(id->id>0).distinct().sorted().toList(); var metadata=metadata(ids); var records=new ArrayList(); var unresolved=new ArrayList(); @@ -64,35 +65,50 @@ public class FleetTripCalendarService { throw new IllegalArgumentException("incomplete route"); stops=List.copyOf(stops); } catch (Exception invalid) { stops=List.of(); reasons.add("ROUTE_INCOMPLETE"); } + var references=containers.getOrDefault(row.getDispatchNo(),new ContainerReferences(List.of(),List.of())); var entry=new Entry("DISPATCH",row.getDispatchNo(),row.getStatus(),row.getWarehouseId()==null?null:row.getWarehouseId().toString(), warehouse==null?row.getWarehouseCode():warehouse.warehouseCode(),warehouse==null?row.getWarehouseName():warehouse.warehouseName(), country,zone,warehouse==null?null:warehouse.status(),instant(row.getPlannedDepartureAt()),instant(row.getExpectedDriverEndAt()), - instant(row.getDepartedAt()),instant(row.getCompletedAt()),stops,containers.getOrDefault(row.getDispatchNo(),List.of()),List.copyOf(reasons)); + instant(row.getDepartedAt()),instant(row.getCompletedAt()),stops,references.containerNumbers(),references.cabinetNumbers(),List.copyOf(reasons)); (unresolvedOrigin?unresolved:records).add(entry); } return new Result(List.copyOf(records),List.copyOf(unresolved)); } - /** 柜号只取排车建单时正式保存的SourceDisplay.containerNo,不反查或推测其他来源。 */ - private Map> containerNumbers(List rows) { + /** 完整柜号取排车货物快照,柜次短号取同客户真实来源的原装柜事件,分别去空去重。 */ + private Map containerReferences(List rows) { var numbers=rows.stream().map(FleetTripCalendarRow::getDispatchNo).distinct().sorted().toList(); - var requested=new HashSet<>(numbers);var grouped=new LinkedHashMap>(); + var requested=new HashSet<>(numbers); + var containers=new LinkedHashMap>(); + var cabinets=new LinkedHashMap>(); for(var source:trips.sourceSnapshots(numbers)) { if(source==null || !requested.contains(source.dispatchNo()) || source.sourceSnapshotJson()==null || source.sourceSnapshotJson().isBlank()) throw sourceUnavailable(); - try { - var snapshot=json.reader().with(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_TRAILING_TOKENS) - .readTree(source.sourceSnapshotJson()); - if(snapshot==null || !snapshot.isObject())throw sourceUnavailable(); - var value=snapshot.get("containerNo"); - if(value==null || value.isNull())continue; - if(!value.isTextual())throw sourceUnavailable(); - var number=value.textValue().strip(); - if(!number.isEmpty())grouped.computeIfAbsent(source.dispatchNo(),ignored->new LinkedHashSet<>()).add(number); - } catch(com.fasterxml.jackson.core.JsonProcessingException invalid) { throw sourceUnavailable(); } + var container=snapshotReference(source.sourceSnapshotJson(),"containerNo"); + if(container!=null)containers.computeIfAbsent(source.dispatchNo(),ignored->new LinkedHashSet<>()).add(container); + // 非头程来源没有装柜事件,保持短号为空,不从完整柜号或内部ID猜测。 + if(source.loadingPayloadJson()!=null) { + var cabinet=snapshotReference(source.loadingPayloadJson(),"cabinetNo"); + if(cabinet!=null)cabinets.computeIfAbsent(source.dispatchNo(),ignored->new LinkedHashSet<>()).add(cabinet); + } } - var result=new LinkedHashMap>(); - grouped.forEach((number,values)->result.put(number,List.copyOf(values)));return Map.copyOf(result); + var result=new LinkedHashMap(); + for(var number:numbers)result.put(number,new ContainerReferences( + List.copyOf(containers.getOrDefault(number,new LinkedHashSet<>())),List.copyOf(cabinets.getOrDefault(number,new LinkedHashSet<>())))); + return Map.copyOf(result); + } + + /** 只接受正式JSON对象中的文本业务标识,缺失留空,损坏资料明确报错。 */ + private String snapshotReference(String payload,String field) { + try { + var snapshot=json.reader().with(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_TRAILING_TOKENS).readTree(payload); + if(snapshot==null || !snapshot.isObject())throw sourceUnavailable(); + var value=snapshot.get(field); + if(value==null || value.isNull())return null; + if(!value.isTextual())throw sourceUnavailable(); + var number=value.textValue().strip(); + return number.isEmpty()?null:number; + } catch(com.fasterxml.jackson.core.JsonProcessingException invalid) { throw sourceUnavailable(); } } /** 元数据批次必须完整覆盖请求身份,未知地点保持显式未核实。 */ diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripMapper.java index 391401a36..d335449ba 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripMapper.java @@ -19,12 +19,14 @@ public interface FleetTripMapper extends BaseMapper { ORDER BY COALESCE(t.departed_at,t.planned_departure_at),d.dispatch_no LIMIT 2001 """) java.util.List calendar(@Param("from") java.time.LocalDateTime from,@Param("to") java.time.LocalDateTime to); - record CalendarSource(String dispatchNo, String sourceSnapshotJson) { } - /** 主查询限额之后批量读取原货物快照,不以货物JOIN放大整车记录或截断多柜。 */ + record CalendarSource(String dispatchNo, String sourceSnapshotJson, String loadingPayloadJson) { } + /** 主查询限额后批读货物快照及同客户原装柜事件,柜次短号只沿真实订单关联读取。 */ @Select("""