fix(appointments): expose dispatch cabinet short numbers

This commit is contained in:
2026-10-22 18:03:45 +08:00
parent 9e0266b7c3
commit bfeeef130e
6 changed files with 88 additions and 39 deletions
@@ -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<Source> sourceSnapshots(List<String> 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();
}
}
@@ -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<FleetTripModels.Stop> stops, List<String> containerNumbers, List<String> incompleteReasons) { }
List<FleetTripModels.Stop> stops, List<String> containerNumbers, List<String> cabinetNumbers, List<String> incompleteReasons) { }
public record Result(List<Entry> records, List<Entry> unresolvedRecords) { }
private record ContainerReferences(List<String> containerNumbers, List<String> cabinetNumbers) { }
private static final Set<String> 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<Entry>(); var unresolved=new ArrayList<Entry>();
@@ -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<String,List<String>> containerNumbers(List<FleetTripCalendarRow> rows) {
/** 完整柜号取排车货物快照,柜次短号取同客户真实来源的原装柜事件,分别去空去重。 */
private Map<String,ContainerReferences> containerReferences(List<FleetTripCalendarRow> rows) {
var numbers=rows.stream().map(FleetTripCalendarRow::getDispatchNo).distinct().sorted().toList();
var requested=new HashSet<>(numbers);var grouped=new LinkedHashMap<String,LinkedHashSet<String>>();
var requested=new HashSet<>(numbers);
var containers=new LinkedHashMap<String,LinkedHashSet<String>>();
var cabinets=new LinkedHashMap<String,LinkedHashSet<String>>();
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<String,List<String>>();
grouped.forEach((number,values)->result.put(number,List.copyOf(values)));return Map.copyOf(result);
var result=new LinkedHashMap<String,ContainerReferences>();
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(); }
}
/** 元数据批次必须完整覆盖请求身份,未知地点保持显式未核实。 */
@@ -19,12 +19,14 @@ public interface FleetTripMapper extends BaseMapper<FleetTripEntity> {
ORDER BY COALESCE(t.departed_at,t.planned_departure_at),d.dispatch_no LIMIT 2001
""")
java.util.List<FleetTripCalendarRow> 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("""
<script>
SELECT d.dispatch_no AS dispatchNo,i.source_snapshot_json AS sourceSnapshotJson
SELECT d.dispatch_no AS dispatchNo,i.source_snapshot_json AS sourceSnapshotJson,e.payload_json AS loadingPayloadJson
FROM ll_dispatch_order d JOIN ll_dispatch_item i ON i.dispatch_order_id=d.id
LEFT JOIN ll_order o ON o.id=i.order_id AND o.customer_id=i.customer_id
LEFT JOIN ll_loading_event_inbox e ON e.event_id=o.source_event_id AND e.customer_id=o.customer_id
WHERE d.status='ACTIVE' AND d.dispatch_no IN
<foreach collection='numbers' item='number' open='(' separator=',' close=')'>#{number}</foreach>
ORDER BY d.dispatch_no,i.id