优化航班号处理逻辑,添加航司二三字码映射功能,增强数据解析能力

This commit is contained in:
sladro 2026-02-28 19:11:20 +08:00
parent 88f4c0be8b
commit 3ef7ef33a7
3 changed files with 160 additions and 20 deletions

View File

@ -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<String, String> 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<String, String> loadIcaoToIataPrefixMap() {
Map<String, String> 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<AirportVehicle> collectVehicleData(String endpoint, String baseUrl) {
try {
String url = UriComponentsBuilder

View File

@ -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 关联 FuIdFuId 视作可复用业务键
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:<flightNo>.activeBizKey
// 优先从 flight:<flightNo>.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<String> 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());
}

View File

@ -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=<empty>
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#