From 3ef7ef33a7f73e60a9311b56898b690dfbf735ce Mon Sep 17 00:00:00 2001 From: sladro Date: Sat, 28 Feb 2026 19:11:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=88=AA=E7=8F=AD=E5=8F=B7?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=88=AA=E5=8F=B8=E4=BA=8C=E4=B8=89=E5=AD=97=E7=A0=81=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=BC=BA=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=A7=A3=E6=9E=90=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datacollector/dao/DataCollectorDao.java | 72 +++++++++++++++- .../AdxpFlightServiceWebSocketClient.java | 85 +++++++++++++++---- 命令.md | 23 +++++ 3 files changed, 160 insertions(+), 20 deletions(-) diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java index cff8f95..da77fe2 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java @@ -37,14 +37,20 @@ import org.locationtech.jts.geom.PrecisionModel; import com.qaup.common.core.redis.RedisCache; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; @Slf4j @Component public class DataCollectorDao { + private static final Map ICAO_TO_IATA_PREFIX = loadIcaoToIataPrefixMap(); // 机场数据源相关配置 @Value("${data.collector.airport-api.base-url}") @@ -112,9 +118,10 @@ public class DataCollectorDao { log.warn("原始航空器数据缺失必要字段 (flightNo, longitude, latitude),跳过处理: {}", rawData); return null; } + String normalizedFlightNo = normalizeAircraftFlightNo(rawData.getFlightNo()); Aircraft aircraft = new Aircraft(); - aircraft.setObjectId(rawData.getFlightNo()); - aircraft.setObjectName(rawData.getFlightNo()); // 使用 flightNo 作为 objectName + aircraft.setObjectId(normalizedFlightNo); + aircraft.setObjectName(normalizedFlightNo); // 使用归一化后的 flightNo 作为 objectName aircraft.setTrackNumber(rawData.getTrackNumber()); aircraft.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude()))); // 外部API中没有直接的速度和方向,如果需要,可以在DataCollectorService中计算或使用默认值 @@ -136,6 +143,67 @@ public class DataCollectorDao { return Collections.emptyList(); } + private static String normalizeAircraftFlightNo(String rawFlightNo) { + if (rawFlightNo == null) { + return null; + } + String normalized = rawFlightNo.trim().toUpperCase(); + if (normalized.isEmpty()) { + return rawFlightNo; + } + int dash = normalized.indexOf('-'); + if (dash > 0) { + normalized = normalized.substring(0, dash).trim(); + } + if (normalized.length() < 4) { + return normalized; + } + String prefix = normalized.substring(0, 3); + String mappedPrefix = ICAO_TO_IATA_PREFIX.get(prefix); + if (mappedPrefix == null || mappedPrefix.isBlank()) { + return normalized; + } + return mappedPrefix + normalized.substring(3); + } + + private static Map loadIcaoToIataPrefixMap() { + Map map = new HashMap<>(); + try (InputStream in = DataCollectorDao.class.getResourceAsStream("/airline-code-map.csv")) { + if (in == null) { + return Collections.emptyMap(); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + String line; + boolean firstLine = true; + while ((line = reader.readLine()) != null) { + String row = line.trim(); + if (row.isEmpty()) { + continue; + } + if (firstLine) { + firstLine = false; + if (row.toUpperCase().startsWith("IATA,ICAO")) { + continue; + } + } + String[] parts = row.split(",", 2); + if (parts.length != 2) { + continue; + } + String iata = parts[0].trim().toUpperCase(); + String icao = parts[1].trim().toUpperCase(); + if (iata.matches("^[A-Z0-9]{2}$") && icao.matches("^[A-Z0-9]{3}$")) { + map.put(icao, iata); + } + } + } + } catch (Exception e) { + log.warn("加载航司二三字码映射失败,航空器flightNo将保持原值: {}", e.getMessage()); + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(map); + } + public List collectVehicleData(String endpoint, String baseUrl) { try { String url = UriComponentsBuilder diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/websocket/AdxpFlightServiceWebSocketClient.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/websocket/AdxpFlightServiceWebSocketClient.java index 9da4e3e..129df6a 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/websocket/AdxpFlightServiceWebSocketClient.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/websocket/AdxpFlightServiceWebSocketClient.java @@ -24,6 +24,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -563,7 +564,11 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { if (bizKey == null) { return null; } - String normalized = bizKey.trim(); + String normalized = normalizeRedisString(bizKey); + if (normalized == null) { + return null; + } + normalized = normalized.trim(); return normalized.isEmpty() ? null : normalized.toUpperCase(); } @@ -575,10 +580,34 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { if (s.isEmpty()) { return null; } - if (s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) { - s = s.substring(1, s.length() - 1).trim(); + // 循环去除外层引号与转义引号,兼容 "\"MU5595-A-...\"" 这类双层污染值。 + boolean changed = true; + while (changed && s.length() >= 2) { + changed = false; + if (s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) { + s = s.substring(1, s.length() - 1).trim(); + changed = true; + } + if (s.length() >= 2 && s.startsWith("'") && s.endsWith("'")) { + s = s.substring(1, s.length() - 1).trim(); + changed = true; + } + if (s.length() >= 4 && s.startsWith("\\\"") && s.endsWith("\\\"")) { + s = s.substring(2, s.length() - 2).trim(); + changed = true; + } + if (s.length() >= 4 && s.startsWith("\\'") && s.endsWith("\\'")) { + s = s.substring(2, s.length() - 2).trim(); + changed = true; + } } - if (s.length() >= 2 && s.startsWith("'") && s.endsWith("'")) { + if (s.contains("\\\"")) { + s = s.replace("\\\"", "\"").trim(); + } + if (s.contains("\\'")) { + s = s.replace("\\'", "'").trim(); + } + while (s.length() >= 2 && ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")))) { s = s.substring(1, s.length() - 1).trim(); } return s.isEmpty() ? null : s; @@ -614,33 +643,50 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { return null; } - private String resolveTisFlightBizKey(String bizKeyRaw, String fuIdRaw, String flightId, String resolvedFlightNo) { + private String resolveTisFlightBizKey(String bizKeyRaw, String resolvedFlightNo) { String bizNorm = normalizeBizKey(bizKeyRaw); if (bizNorm != null) { return bizNorm; } - // TISFLIGHT 缺 BizKey 时,先用 FlightId 关联 FuId(FuId 视作可复用业务键) - String fuNorm = normalizeBizKey(fuIdRaw); - String normalizedFlightId = normalizeFlightNo(flightId); String normalizedResolvedFlightNo = normalizeFlightNo(resolvedFlightNo); - String flightNoForMatch = normalizedFlightId != null ? normalizedFlightId : normalizedResolvedFlightNo; - if (fuNorm != null && flightNoForMatch != null) { - String fuFlightNo = firstSegmentFromBizLike(fuNorm); - if (flightNoForMatch.equalsIgnoreCase(fuFlightNo)) { - return fuNorm; - } + if (normalizedResolvedFlightNo == null) { + return null; } - // FlightId-FuId 关联失败时,按航班号回退到 flight:.activeBizKey + // 优先从 flight:.activeBizKey 拿当前活跃业务键 String flightKey = buildFlightRedisKey(resolvedFlightNo); if (flightKey != null) { Object activeBizRaw = redisCache.getCacheMapValue(flightKey, "activeBizKey"); String activeBizKey = normalizeBizKey(activeBizRaw == null ? null : String.valueOf(activeBizRaw)); if (activeBizKey != null) { + // 读到脏值时,顺便回写干净值,避免后续再次受污染影响 + redisCache.setCacheMapValue(flightKey, "activeBizKey", activeBizKey); return activeBizKey; } } + + // activeBizKey 不存在时,按航班号从 flightBiz 命名空间反查 + Collection bizRedisKeys = redisCache.keys("flightBiz:" + normalizedResolvedFlightNo + "-*"); + if (bizRedisKeys != null && !bizRedisKeys.isEmpty()) { + String prefix = "flightBiz:"; + for (String redisKey : bizRedisKeys) { + if (redisKey == null || !redisKey.startsWith(prefix)) { + continue; + } + String candidateBizKey = normalizeBizKey(redisKey.substring(prefix.length())); + if (candidateBizKey == null) { + continue; + } + String candidateFlightNo = firstSegmentFromBizLike(candidateBizKey); + if (candidateFlightNo != null && candidateFlightNo.equalsIgnoreCase(normalizedResolvedFlightNo)) { + if (flightKey != null) { + updateActiveBizKey(flightKey, candidateBizKey, System.currentTimeMillis()); + } + return candidateBizKey; + } + } + } return null; } @@ -1115,19 +1161,22 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { dto.setContactCross(contactCross); } + String normalizedBizKey = resolveTisFlightBizKey(bizKey, resolvedFlightNo); String flightKey = buildFlightRedisKey(resolvedFlightNo); + String bizRedisKey = buildBizRedisKey(normalizedBizKey); if (flightKey != null) { try { redisCache.setCacheMapValue(flightKey, "flightNumber", resolvedFlightNo); + updateActiveBizKey(flightKey, normalizedBizKey, nowMs); // contactCross 仅对进港有效,避免进出港参数串用 if ("IN".equalsIgnoreCase(routeType) && contactCross != null && !contactCross.isBlank()) { - redisCache.setCacheMapValue(flightKey, "contactCross", contactCross); - redisCache.setCacheMapValue(flightKey, "contactCrossTs", String.valueOf(nowMs)); + setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "contactCross", contactCross); + setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "contactCrossTs", String.valueOf(nowMs)); } log.info("TISFLIGHT航班号解析(已应用三字转二字): flNoRaw={}, flightIdRaw={}, fuId={}, bizKey={}, resolvedFlightNo={}", - flightNumberRaw, flightIdRaw, fuId, bizKey, resolvedFlightNo); + flightNumberRaw, flightIdRaw, fuId, normalizedBizKey, resolvedFlightNo); } catch (Exception e) { log.error("存储航班TISFLIGHT数据到Redis失败: {}", e.getMessage()); } diff --git a/命令.md b/命令.md index 5d6e792..17c7a28 100644 --- a/命令.md +++ b/命令.md @@ -350,4 +350,27 @@ root@root:/home/project_20250804/qaup# for i in $(seq 1 20); do TS=$(date '+%H ``` +18:19:48.319 [ScheduledTask-4] INFO c.q.c.d.s.DataProcessingService - [tryQueryAndPublishRouteFromRedis,1240] - 路由接口返回为空,稍后重试: flightNo=CA4293, routeType=IN, source=RETRY(FLIGHT_NOTIFICATION), inRunway=17, outRunway=17, contactCross=K1, seat=150, startSeat=null +curl -sS -D - -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=35&contactCross=F1&seat=138" \ +| head -n 120 + + +18:53:52.400 [ScheduledTask-4] WARN c.q.c.d.d.DataCollectorDao - [getArrivalRoute,299] - 获取进港路由数据失败: 返回体为空或无法解析, inRunway=17, outRunway=17, contactCross=K1, seat=143, url=http://10.32.38.3:8099/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat?inRunway=17&outRunway=17&contactCross=K1&seat=143, bodySummary= +18:53:52.400 [ScheduledTask-4] INFO c.q.c.d.s.DataProcessingService - [tryQueryAndPublishRouteFromRedis,1240] - 路由接口返回为空,稍后重试: flightNo=SC4772, routeType=IN, source=RETRY(FLIGHT_NOTIFICATION), inRunway=17, outRunway=17, contactCross=K1, seat=143, startSeat=null +^C +root@root:/home/project_20250804/qaup# curl -sS -D - -H "Authorization: $TOKEN" "http://10.32.38.3:8099/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat?inRunway=17&outRunway=17&contactCross=K1&seat=110" | head -n 120 +HTTP/1.1 200 +X-Content-Type-Options: nosniff +X-XSS-Protection: 1; mode=block +Cache-Control: no-cache, no-store, max-age=0, must-revalidate +Pragma: no-cache +Expires: 0 +X-Frame-Options: DENY +Content-Length: 0 +Date: Sat, 28 Feb 2026 10:56:18 GMT + +root@root:/home/project_20250804/qaup# echo $TOKEN +Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NzIzNjA0NTYsInVzZXJuYW1lIjoiZGlhbnhpbiJ9.kTPxkoFR64eJT7eZOZWSN_ed-qvWbMFqr2WlGofBE60 +root@root:/home/project_20250804/qaup#