优化航班号处理逻辑,添加航司二三字码映射功能,增强数据解析能力
This commit is contained in:
parent
88f4c0be8b
commit
3ef7ef33a7
@ -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
|
||||
|
||||
@ -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:<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());
|
||||
}
|
||||
|
||||
23
命令.md
23
命令.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=<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#
|
||||
|
||||
Loading…
Reference in New Issue
Block a user