diff --git a/accoutn.md b/accout.md similarity index 98% rename from accoutn.md rename to accout.md index 900bc84..1d7b33e 100644 --- a/accoutn.md +++ b/accout.md @@ -18,7 +18,7 @@ (4)Z31020**&d1231=+33 4.测试平台-- 10.100.23.10 (1)root -①Huawei@123 +Dianxin@222 5.红绿灯测试平台-- 10.98.23.111 windows系统 (1)账密 ①administrator @@ -40,3 +40,5 @@ mem_clients_normal:5146661 /home/project_20250804/qaup/adxp-adapter + + diff --git a/qaup-collision/src/main/java/com/qaup/collision/controller/RunwayPathPlanningDebugController.java b/qaup-collision/src/main/java/com/qaup/collision/controller/RunwayPathPlanningDebugController.java new file mode 100644 index 0000000..7409420 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/controller/RunwayPathPlanningDebugController.java @@ -0,0 +1,163 @@ +package com.qaup.collision.controller; + +import com.qaup.collision.datacollector.dao.DataCollectorDao; +import com.qaup.collision.datacollector.dto.AircraftRouteDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 跑道滑行路由查询调试接口(可配置开关)。 + * + * 目的: + * - 用于联调验证 IN/OUT 两种拼参逻辑 + * - 输出最终透传到机场滑行路由服务的 URL 与参数 + * + * 开关: + * - qaup.debug.runway-path-planning.enabled=true 才会注册该 Controller + */ +@Slf4j +@RestController +@RequestMapping("/debug/runway-path-planning") +@ConditionalOnProperty(name = "qaup.debug.runway-path-planning.enabled", havingValue = "true") +public class RunwayPathPlanningDebugController { + + @Value("${data.collector.airport-api.glide-url}") + private String airportGlideUrl; + + private final DataCollectorDao dataCollectorDao; + + public RunwayPathPlanningDebugController(DataCollectorDao dataCollectorDao) { + this.dataCollectorDao = dataCollectorDao; + } + + /** + * 调试查询:按 IN/OUT 规则拼参并(可选)执行真实调用。 + * + * 规则(按需求最小实现): + * - IN:只透传 inRunway + contactCross + seat(不透传 outRunway) + * - OUT:只透传 outRunway + startSeat(startSeat 缺失时用 seat 兜底;不透传 inRunway) + */ + @GetMapping("/query") + public ResponseEntity> query( + @RequestParam("routeType") String routeType, + @RequestParam(value = "inRunway", required = false) String inRunway, + @RequestParam(value = "outRunway", required = false) String outRunway, + @RequestParam(value = "contactCross", required = false) String contactCross, + @RequestParam(value = "seat", required = false) String seat, + @RequestParam(value = "startSeat", required = false) String startSeat, + @RequestParam(value = "dryRun", defaultValue = "true") boolean dryRun + ) { + Map result = new LinkedHashMap<>(); + List ignoredParams = new ArrayList<>(); + + String normalizedRouteType = normalize(routeType); + if (normalizedRouteType == null) { + return badRequest("routeType为空", result); + } + normalizedRouteType = normalizedRouteType.toUpperCase(); + + String nInRunway = normalize(inRunway); + String nOutRunway = normalize(outRunway); + String nContactCross = normalize(contactCross); + String nSeat = normalize(seat); + String nStartSeat = normalize(startSeat); + + result.put("routeType", normalizedRouteType); + result.put("dryRun", dryRun); + + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(airportGlideUrl); + Map effectiveParams = new LinkedHashMap<>(); + + AircraftRouteDTO routeData = null; + if ("IN".equalsIgnoreCase(normalizedRouteType)) { + if (nInRunway == null) { + return badRequest("IN 路由缺少必填参数: inRunway", result); + } + if (nContactCross == null) { + return badRequest("IN 路由缺少必填参数: contactCross", result); + } + if (nSeat == null) { + return badRequest("IN 路由缺少必填参数: seat", result); + } + + // IN:只传 inRunway + contactCross + seat(不传 outRunway) + ignoredParams.add("outRunway"); + ignoredParams.add("startSeat"); + + builder.path("/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat") + .queryParam("inRunway", nInRunway) + .queryParam("contactCross", nContactCross) + .queryParam("seat", nSeat); + effectiveParams.put("inRunway", nInRunway); + effectiveParams.put("contactCross", nContactCross); + effectiveParams.put("seat", nSeat); + + if (!dryRun) { + routeData = dataCollectorDao.getArrivalRoute(nInRunway, nOutRunway, nContactCross, nSeat); + } + } else if ("OUT".equalsIgnoreCase(normalizedRouteType)) { + if (nOutRunway == null) { + return badRequest("OUT 路由缺少必填参数: outRunway", result); + } + // OUT:startSeat 缺失时允许 seat 兜底 + String effectiveStartSeat = nStartSeat != null ? nStartSeat : nSeat; + if (effectiveStartSeat == null) { + return badRequest("OUT 路由缺少必填参数: startSeat(可用 seat 兜底)", result); + } + + ignoredParams.add("inRunway"); + ignoredParams.add("contactCross"); + + builder.path("/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat") + .queryParam("outRunway", nOutRunway) + .queryParam("startSeat", effectiveStartSeat); + effectiveParams.put("outRunway", nOutRunway); + effectiveParams.put("startSeat", effectiveStartSeat); + if (nStartSeat == null && nSeat != null) { + effectiveParams.put("startSeatFallbackFromSeat", true); + } + + if (!dryRun) { + routeData = dataCollectorDao.getDepartureRoute(nInRunway, nOutRunway, effectiveStartSeat); + } + } else { + return badRequest("routeType只支持 IN 或 OUT", result); + } + + result.put("effectiveParams", effectiveParams); + result.put("ignoredParams", ignoredParams); + result.put("url", builder.toUriString()); + + if (!dryRun) { + result.put("routeData", routeData); + } + + return ResponseEntity.ok(result); + } + + private static ResponseEntity> badRequest(String message, Map body) { + body.put("error", message); + return ResponseEntity.badRequest().body(body); + } + + private static String normalize(String s) { + if (s == null) { + return null; + } + String t = s.trim(); + return t.isEmpty() ? null : t; + } +} + diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/config/RestTemplateConfig.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/config/RestTemplateConfig.java index 66a742c..4daf8ae 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/config/RestTemplateConfig.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/config/RestTemplateConfig.java @@ -4,26 +4,43 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class RestTemplateConfig { -// @Bean -// public RestTemplate restTemplate() { -// return new RestTemplate(); -// } -@Bean -public RestTemplate restTemplate(ObjectMapper restTemplateObjectMapper) { - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getMessageConverters().forEach(converter -> { - if (converter instanceof MappingJackson2HttpMessageConverter) { - ((MappingJackson2HttpMessageConverter) converter).setObjectMapper(restTemplateObjectMapper); - } - }); - return restTemplate; -} + /** + * HTTP 连接超时(毫秒)。 + * 目的:避免上游接口抖动时 RestTemplate 长时间阻塞,导致 @Scheduled 任务堆积、线程/连接被耗尽。 + */ + @Value("${data.collector.http.connect-timeout-ms:1000}") + private int connectTimeoutMs; + + /** + * HTTP 读取超时(毫秒)。 + * 目的:避免请求卡在 read 阶段导致任务挤压。 + */ + @Value("${data.collector.http.read-timeout-ms:2000}") + private int readTimeoutMs; + + @Bean + public RestTemplate restTemplate(ObjectMapper restTemplateObjectMapper) { + // 使用最小改动的 SimpleClientHttpRequestFactory 配置超时(不引入新依赖,不改变调用逻辑) + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(connectTimeoutMs); + requestFactory.setReadTimeout(readTimeoutMs); + + RestTemplate restTemplate = new RestTemplate(requestFactory); + restTemplate.getMessageConverters().forEach(converter -> { + if (converter instanceof MappingJackson2HttpMessageConverter) { + ((MappingJackson2HttpMessageConverter) converter).setObjectMapper(restTemplateObjectMapper); + } + }); + return restTemplate; + } @Bean public ObjectMapper restTemplateObjectMapper() { 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 1ad2d40..8b45872 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 @@ -11,10 +11,13 @@ import com.qaup.collision.datacollector.dto.FlightNotificationDTO; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; @@ -31,9 +34,11 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.PrecisionModel; +import com.qaup.common.core.redis.RedisCache; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @@ -65,11 +70,19 @@ public class DataCollectorDao { private final RestTemplate restTemplate; private final AuthService authService; private final GeometryFactory geometryFactory; + private final ObjectMapper objectMapper; + + /** + * Redis缓存(用于在机场平台缺失 getRouteParams 接口时,聚合上游事件补齐路由参数) + */ + @Autowired(required = false) + private RedisCache redisCache; public DataCollectorDao(RestTemplate restTemplate, AuthService authService) { this.restTemplate = restTemplate; this.authService = authService; this.geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); // SRID 4326 for WGS84 + this.objectMapper = new ObjectMapper(); } @@ -208,7 +221,7 @@ public class DataCollectorDao { * 获取航空器进港路由 * * @param inRunway 进港跑道编号 - * @param outRunway 出港跑道编号 + * @param outRunway 出港跑道编号(机场侧接口要求参数key存在;当缺失时上游会补齐或用占位传递) * @param contactCross 联络道口 * @param seat 目的机位 * @return 进港路由数据 @@ -221,10 +234,14 @@ public class DataCollectorDao { return null; } + // 机场侧接口校验:要求参数 key 存在(Required String parameter ... is not present),但允许空值。 + // 为满足“IN 只关心 inRunway/contactCross/seat”的业务逻辑,同时避免 400,这里对 outRunway 采用空串占位。 + String outRunwayForQuery = (outRunway == null ? "" : outRunway); + String url = UriComponentsBuilder.fromUriString(airportGlideUrl) .path("/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat") .queryParam("inRunway", inRunway) - .queryParam("outRunway", outRunway) + .queryParam("outRunway", outRunwayForQuery) .queryParam("contactCross", contactCross) .queryParam("seat", seat) .toUriString(); @@ -235,24 +252,41 @@ public class DataCollectorDao { HttpEntity requestEntity = new HttpEntity<>(headers); - ResponseEntity> response = restTemplate.exchange( + // 注意:路由服务返回格式在不同环境可能有两种: + // 1) 包装格式:{status,msg,data:{...}} + // 2) 裸对象:{type,status,codes,geoPath,...} + // 这里先取 String,再按 JSON 结构判断解析,避免 UnrecognizedPropertyException(type)。 + ResponseEntity response = restTemplate.exchange( url, HttpMethod.GET, requestEntity, - new ParameterizedTypeReference>() {} + String.class ); - Response responseBody = response.getBody(); - if (responseBody != null && responseBody.getStatus() == 200) { - log.info("成功获取进港路由数据: inRunway={}, outRunway={}, contactCross={}, seat={}", - inRunway, outRunway, contactCross, seat); - return responseBody.getData(); - } else { - log.warn("获取进港路由数据失败: {}", responseBody != null ? responseBody.getMsg() : "未知错误"); + if (!response.getStatusCode().is2xxSuccessful()) { + log.warn("获取进港路由数据请求失败: HTTP {}, inRunway={}, outRunway={}, contactCross={}, seat={}", + response.getStatusCode(), inRunway, outRunway, contactCross, seat); + log.warn("进港路由请求URL: {}", url); return null; } + + String body = response.getBody(); + AircraftRouteDTO route = parseAircraftRouteBody(body); + if (route != null) { + log.info("成功获取进港路由数据: inRunway={}, outRunway={}, contactCross={}, seat={}", + inRunway, outRunway, contactCross, seat); + } else { + log.warn("获取进港路由数据失败: 返回体为空或无法解析, inRunway={}, outRunway={}, contactCross={}, seat={}, url={}, bodySummary={}", + inRunway, outRunway, contactCross, seat, url, summarizeBody(body)); + } + return route; + + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("获取进港路由数据请求失败: status={}, body={}, inRunway={}, outRunway={}, contactCross={}, seat={}", + e.getStatusCode(), e.getResponseBodyAsString(), inRunway, outRunway, contactCross, seat, e); + return null; } catch (Exception e) { - log.error("获取进港路由数据时发生异常: inRunway={}, outRunway={}, contactCross={}, seat={}", + log.error("获取进港路由数据时发生异常: inRunway={}, outRunway={}, contactCross={}, seat={}", inRunway, outRunway, contactCross, seat, e); return null; } @@ -261,7 +295,7 @@ public class DataCollectorDao { /** * 获取航空器出港路由 * - * @param inRunway 进港跑道编号 + * @param inRunway 进港跑道编号(机场侧业务可能依赖该值;当缺失时上游会补齐或用占位传递) * @param outRunway 出港跑道编号 * @param startSeat 起始机位 * @return 出港路由数据 @@ -274,9 +308,13 @@ public class DataCollectorDao { return null; } + // 机场侧接口校验:要求参数 key 存在,但允许空值。 + // 为满足“OUT 只关心 outRunway/startSeat”的业务逻辑,同时避免 400,这里对 inRunway 采用空串占位。 + String inRunwayForQuery = (inRunway == null ? "" : inRunway); + String url = UriComponentsBuilder.fromUriString(airportGlideUrl) .path("/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat") - .queryParam("inRunway", inRunway) + .queryParam("inRunway", inRunwayForQuery) .queryParam("outRunway", outRunway) .queryParam("startSeat", startSeat) .toUriString(); @@ -287,29 +325,95 @@ public class DataCollectorDao { HttpEntity requestEntity = new HttpEntity<>(headers); - ResponseEntity> response = restTemplate.exchange( + // 同 getArrivalRoute:兼容包装/裸对象两种返回格式 + ResponseEntity response = restTemplate.exchange( url, HttpMethod.GET, requestEntity, - new ParameterizedTypeReference>() {} + String.class ); - Response responseBody = response.getBody(); - if (responseBody != null && responseBody.getStatus() == 200) { - log.info("成功获取出港路由数据: inRunway={}, outRunway={}, startSeat={}", - inRunway, outRunway, startSeat); - return responseBody.getData(); - } else { - log.warn("获取出港路由数据失败: {}", responseBody != null ? responseBody.getMsg() : "未知错误"); + if (!response.getStatusCode().is2xxSuccessful()) { + log.warn("获取出港路由数据请求失败: HTTP {}, inRunway={}, outRunway={}, startSeat={}", + response.getStatusCode(), inRunway, outRunway, startSeat); + log.warn("出港路由请求URL: {}", url); return null; } + + String body = response.getBody(); + AircraftRouteDTO route = parseAircraftRouteBody(body); + if (route != null) { + log.info("成功获取出港路由数据: inRunway={}, outRunway={}, startSeat={}", + inRunway, outRunway, startSeat); + } else { + log.warn("获取出港路由数据失败: 返回体为空或无法解析, inRunway={}, outRunway={}, startSeat={}, url={}, bodySummary={}", + inRunway, outRunway, startSeat, url, summarizeBody(body)); + } + return route; + + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("获取出港路由数据请求失败: status={}, body={}, inRunway={}, outRunway={}, startSeat={}", + e.getStatusCode(), e.getResponseBodyAsString(), inRunway, outRunway, startSeat, e); + return null; } catch (Exception e) { - log.error("获取出港路由数据时发生异常: inRunway={}, outRunway={}, startSeat={}", + log.error("获取出港路由数据时发生异常: inRunway={}, outRunway={}, startSeat={}", inRunway, outRunway, startSeat, e); return null; } } + /** + * 解析路由接口返回体,兼容两种格式: + * 1) 包装格式:{status,msg,data:{...}} + * 2) 裸对象:{type,status,codes,geoPath,...} + */ + private AircraftRouteDTO parseAircraftRouteBody(String body) { + try { + if (body == null || body.isBlank()) { + return null; + } + + JsonNode root = objectMapper.readTree(body); + if (root == null || root.isNull()) { + return null; + } + + // 包装格式:优先取 data 节点 + if (root.has("data")) { + JsonNode dataNode = root.get("data"); + if (dataNode == null || dataNode.isNull()) { + return null; + } + return objectMapper.treeToValue(dataNode, AircraftRouteDTO.class); + } + + // 裸对象:直接映射 + return objectMapper.treeToValue(root, AircraftRouteDTO.class); + + } catch (Exception e) { + log.error("解析路由返回体失败: {}", body, e); + return null; + } + } + + /** + * 将返回体摘要化,避免日志刷屏。 + * - body 为空:返回 "" + * - body 非空:返回 "len=, head=<前200字符>" + */ + private static String summarizeBody(String body) { + if (body == null) { + return ""; + } + String b = body.trim(); + if (b.isEmpty()) { + return ""; + } + int len = b.length(); + String head = b.substring(0, Math.min(200, len)); + return "len=" + len + ", head=" + head; + } + /** * 获取航空器状态 * @@ -363,43 +467,186 @@ public class DataCollectorDao { * @return 航班路由参数数据 */ public AircraftRouteParamsDTO getAircraftRouteParams(String flightNo, String routeType) { -// try { -// String token = authService.getToken(airportBaseUrl); -// if (token == null) { -// log.error("无法获取有效的认证token"); -// return null; -// } -// -// String url = UriComponentsBuilder.fromUriString(airportBaseUrl) -// .path("/aircraftRouteParamsController/getRouteParams") -// .queryParam("flightNo", flightNo) -// .queryParam("routeType", routeType) -// .toUriString(); -// -// HttpHeaders headers = new HttpHeaders(); -// headers.set("Authorization", token); -// headers.set("Content-Type", "application/json"); -// -// HttpEntity requestEntity = new HttpEntity<>(headers); -// ResponseEntity> response = restTemplate.exchange( -// url, -// HttpMethod.GET, -// requestEntity, -// new ParameterizedTypeReference>() {} -// ); -// Response responseBody = response.getBody(); -// if (responseBody != null && responseBody.getStatus() == 200) { -// log.info("成功获取航班路由参数: flightNo={}, routeType={}", flightNo, routeType); -// return responseBody.getData(); -// } else { -// log.warn("获取航班路由参数失败: {}", responseBody != null ? responseBody.getMsg() : "未知错误"); -// return null; -// } -// } catch (Exception e) { -// log.error("获取航班路由参数时发生异常: flightNo={}, routeType={}", flightNo, routeType, e); -// return null; -// } - return null; + try { + if (flightNo == null || flightNo.isBlank()) { + log.warn("获取航班路由参数失败:flightNo为空, routeType={}", routeType); + return null; + } + if (routeType == null || routeType.isBlank()) { + log.warn("获取航班路由参数失败:routeType为空, flightNo={}", flightNo); + return null; + } + + // 兼容上游 BizKey 格式:CZ3519-A-20260206111500 + // 机场路由参数接口通常按“纯航班号”查询,因此这里做一次归一化。 + String normalizedFlightNo = flightNo; + if (normalizedFlightNo.contains("-")) { + String[] parts = normalizedFlightNo.split("-", 2); + if (parts.length > 0 && parts[0] != null && !parts[0].isBlank()) { + normalizedFlightNo = parts[0].trim(); + } + } + final String normalizedRouteType = routeType.trim().toUpperCase(); + + String token = authService.getToken(airportBaseUrl); + if (token == null) { + log.error("无法获取有效的认证token,跳过路由参数查询: flightNo={}, routeType={}", normalizedFlightNo, routeType); + return buildRouteParamsFromRedis(normalizedFlightNo, flightNo, normalizedRouteType, "token_null"); + } + + String url = UriComponentsBuilder.fromUriString(airportBaseUrl) + .path("/aircraftRouteParamsController/getRouteParams") + .queryParam("flightNo", normalizedFlightNo) + .queryParam("routeType", normalizedRouteType) + .toUriString(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", token); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity requestEntity = new HttpEntity<>(headers); + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + requestEntity, + new ParameterizedTypeReference>() {} + ); + + Response responseBody = response.getBody(); + if (responseBody != null && responseBody.getStatus() == 200) { + AircraftRouteParamsDTO data = responseBody.getData(); + log.info("成功获取航班路由参数: flightNo={} (raw={}), routeType={}", + normalizedFlightNo, flightNo, routeType); + return data; + } + + log.warn("获取航班路由参数失败: flightNo={} (raw={}), routeType={}, msg={}", + normalizedFlightNo, flightNo, routeType, + responseBody != null ? responseBody.getMsg() : "未知错误"); + return buildRouteParamsFromRedis(normalizedFlightNo, flightNo, normalizedRouteType, "http_non_200"); + + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("获取航班路由参数请求失败: flightNo={}, routeType={}, status={}, body={}", + flightNo, routeType, e.getStatusCode(), e.getResponseBodyAsString(), e); + // 机场平台在正式环境可能未提供 getRouteParams(常见404)。此处兜底从Redis聚合参数,便于继续路由下发或定位缺字段。 + String normalizedFlightNo = flightNo != null ? flightNo.split("-", 2)[0] : null; + String normalizedRouteType = routeType != null ? routeType.trim().toUpperCase() : null; + return buildRouteParamsFromRedis(normalizedFlightNo, flightNo, normalizedRouteType, "http_" + e.getStatusCode()); + } catch (ResourceAccessException e) { + log.error("获取航班路由参数网络异常: flightNo={}, routeType={}, msg={}", + flightNo, routeType, e.getMessage(), e); + String normalizedFlightNo = flightNo != null ? flightNo.split("-", 2)[0] : null; + String normalizedRouteType = routeType != null ? routeType.trim().toUpperCase() : null; + return buildRouteParamsFromRedis(normalizedFlightNo, flightNo, normalizedRouteType, "network_error"); + } catch (Exception e) { + log.error("获取航班路由参数时发生异常: flightNo={}, routeType={}", flightNo, routeType, e); + String normalizedFlightNo = flightNo != null ? flightNo.split("-", 2)[0] : null; + String normalizedRouteType = routeType != null ? routeType.trim().toUpperCase() : null; + return buildRouteParamsFromRedis(normalizedFlightNo, flightNo, normalizedRouteType, "exception"); + } + } + + /** + * 当机场平台缺失 getRouteParams 接口或调用失败时,从 Redis 聚合路由参数。 + * + * Redis key 约定:flight:<纯航班号> + * 字段来源(由上游事件逐步补齐): + * - inRunway/outRunway: RUNWAY/DFIE 等事件写入 + * - contactCross: TISFLIGHT 事件写入 + * - seat: CRAFTSEAT 事件写入 + * - startSeat: 若未单独提供,尝试使用 seat 兜底(出港通常从机位推出) + * + * 设计目标: + * - 字段齐全则返回 DTO,后续可直接查询路由并下发给前端 + * - 字段不全则返回 null,同时输出“缺字段清单 + 当前Redis快照”,方便定位问题所在 + */ + private AircraftRouteParamsDTO buildRouteParamsFromRedis( + String normalizedFlightNo, + String rawFlightNo, + String normalizedRouteType, + String fallbackReason) { + try { + if (redisCache == null) { + // 注意:当前 logback 文件落地只保留 INFO/ERROR,WARN 不会写入 /logs。 + // 为了便于容器环境排障,这里使用 INFO 级别输出关键原因。 + log.info("路由参数Redis兜底不可用(RedisCache未注入),跳过: flightNo={}, routeType={}, reason={}", + rawFlightNo, normalizedRouteType, fallbackReason); + return null; + } + if (normalizedFlightNo == null || normalizedFlightNo.isBlank() + || normalizedRouteType == null || normalizedRouteType.isBlank()) { + log.info("路由参数Redis兜底入参无效,跳过: flightNo={} (normalized={}), routeType={}, reason={}", + rawFlightNo, normalizedFlightNo, normalizedRouteType, fallbackReason); + return null; + } + + String key = "flight:" + normalizedFlightNo.trim(); + Map snapshot = redisCache.getCacheMap(key); + + String inRunway = toNonBlankString(snapshot.get("inRunway")); + String outRunway = toNonBlankString(snapshot.get("outRunway")); + String contactCross = toNonBlankString(snapshot.get("contactCross")); + String seat = toNonBlankString(snapshot.get("seat")); + String startSeat = toNonBlankString(snapshot.get("startSeat")); + + // 出港:如果没有startSeat,尝试用seat兜底(机位号通常就是推出起点) + if ("OUT".equalsIgnoreCase(normalizedRouteType) && (startSeat == null || startSeat.isBlank())) { + if (seat != null && !seat.isBlank()) { + startSeat = seat; + log.info("路由参数Redis兜底:startSeat缺失,使用seat兜底: flightNo={}, seatAsStartSeat={}", + normalizedFlightNo, seat); + } + } + + // 缺字段清单(用于定位数据源/事件是否补齐) + StringBuilder missing = new StringBuilder(); + if (inRunway == null || inRunway.isBlank()) missing.append("inRunway,"); + if (outRunway == null || outRunway.isBlank()) missing.append("outRunway,"); + + if ("IN".equalsIgnoreCase(normalizedRouteType)) { + if (contactCross == null || contactCross.isBlank()) missing.append("contactCross,"); + if (seat == null || seat.isBlank()) missing.append("seat,"); + } else if ("OUT".equalsIgnoreCase(normalizedRouteType)) { + if (startSeat == null || startSeat.isBlank()) missing.append("startSeat,"); + } else { + missing.append("routeType(IN/OUT),"); + } + + if (missing.length() > 0) { + // 去掉末尾逗号 + missing.setLength(missing.length() - 1); + // 重要:字段不全是“路由下发失败”的根因,需要在 /logs/sys-info.log 可见 + log.info("路由参数Redis兜底失败(字段不全): flightNo={} (raw={}), routeType={}, missing=[{}], reason={}, redis={}", + normalizedFlightNo, rawFlightNo, normalizedRouteType, missing, fallbackReason, snapshot); + return null; + } + + AircraftRouteParamsDTO dto = new AircraftRouteParamsDTO(); + dto.setFlightNo(normalizedFlightNo); + dto.setRouteType(normalizedRouteType); + dto.setInRunway(inRunway); + dto.setOutRunway(outRunway); + dto.setContactCross(contactCross); + dto.setSeat(seat); + dto.setStartSeat(startSeat); + dto.setTimestamp(System.currentTimeMillis()); + + log.info("路由参数Redis兜底成功: flightNo={} (raw={}), routeType={}, inRunway={}, outRunway={}, contactCross={}, seat={}, startSeat={}, reason={}", + normalizedFlightNo, rawFlightNo, normalizedRouteType, + inRunway, outRunway, contactCross, seat, startSeat, fallbackReason); + return dto; + + } catch (Exception e) { + log.error("路由参数Redis兜底异常: flightNo={} (raw={}), routeType={}, reason={}", + normalizedFlightNo, rawFlightNo, normalizedRouteType, fallbackReason, e); + return null; + } + } + + private static String toNonBlankString(Object v) { + if (v == null) return null; + String s = String.valueOf(v).trim(); + return s.isEmpty() ? null : s; } /** diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dto/AircraftRouteDTO.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dto/AircraftRouteDTO.java index 91816d7..a620c4b 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dto/AircraftRouteDTO.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dto/AircraftRouteDTO.java @@ -3,6 +3,7 @@ package com.qaup.collision.datacollector.dto; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.util.List; @@ -122,6 +123,28 @@ public class AircraftRouteDTO { /** * 路径段编码 */ - private String code; + private Object code; + + /** + * 将 code 归一化为字符串。 + * + * 背景:机场侧 GeoJSON 的 properties.code 在不同路段可能返回: + * - "B" / "F6" / ""(字符串) + * - {}(空对象) + * + * 如果强类型使用 String,会在遇到 {} 时触发 Jackson 反序列化异常,导致整条路由解析失败。 + */ + @JsonIgnore + public String getCodeAsString() { + if (code == null) { + return ""; + } + if (code instanceof String) { + String s = ((String) code).trim(); + return s.isEmpty() ? "" : s; + } + // 非字符串(如 {} / Map / List)统一视为“无编码” + return ""; + } } } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dto/AircraftRouteParamsDTO.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dto/AircraftRouteParamsDTO.java index 1106491..5d37ebb 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dto/AircraftRouteParamsDTO.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dto/AircraftRouteParamsDTO.java @@ -66,6 +66,23 @@ public class AircraftRouteParamsDTO { @JsonProperty("timestamp") private Long timestamp; + /** + * 标记 inRunway 是否为系统补齐值(true=补齐;false/空=来自报文/Redis原始数据)。 + * + * 说明: + * - 机场路由服务在参数缺失时会 400(Required parameter not present) + * - 即便允许空占位,业务上可能仍需要跑道值才能返回路由 + * - 因此在缺失时使用固定值补齐,并将来源标记下发给前端用于联调核对 + */ + @JsonProperty("inRunwayPatched") + private Boolean inRunwayPatched; + + /** + * 标记 outRunway 是否为系统补齐值(true=补齐;false/空=来自报文/Redis原始数据)。 + */ + @JsonProperty("outRunwayPatched") + private Boolean outRunwayPatched; + /** * 检查是否为有效的路由参数 */ diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/AuthService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/AuthService.java index c3a4ef7..af0d129 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/AuthService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/AuthService.java @@ -13,6 +13,9 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + @Slf4j @Service public class AuthService { @@ -33,8 +36,23 @@ public class AuthService { private String refreshEndpoint; private final RestTemplate restTemplate; - private String token; - private long tokenExpiryTime; + + /** + * 按 baseUrl 维度缓存 token,避免不同服务(例如 8090 位置接口 vs 8099 路由接口)互相串用 token 导致 403。 + * + * key: baseUrl(例如 http://10.32.38.3:8090 或 http://10.32.38.3:8099) + */ + private final Map tokenCache = new ConcurrentHashMap<>(); + + private static class TokenInfo { + private final String token; + private final long tokenExpiryTimeMs; + + private TokenInfo(String token, long tokenExpiryTimeMs) { + this.token = token; + this.tokenExpiryTimeMs = tokenExpiryTimeMs; + } + } public AuthService(RestTemplate restTemplate) { this.restTemplate = restTemplate; @@ -61,10 +79,11 @@ public class AuthService { if (response.getStatusCode().is2xxSuccessful()) { Response responseBody = response.getBody(); if (responseBody != null) { - this.token = responseBody.getData(); - this.tokenExpiryTime = System.currentTimeMillis() + 3600 * 1000; - log.info("Successfully obtained new token"); - return this.token; + String token = responseBody.getData(); + long expiryMs = System.currentTimeMillis() + 3600 * 1000; + tokenCache.put(baseUrl, new TokenInfo(token, expiryMs)); + log.info("Successfully obtained new token for baseUrl={}", baseUrl); + return token; } } } catch (Exception e) { @@ -82,8 +101,15 @@ public class AuthService { .toUriString(); try { - // 创建带有当前token的请求头 - HttpEntity requestEntity = new HttpEntity<>(createAuthHeader()); + TokenInfo current = tokenCache.get(baseUrl); + if (current == null || current.token == null) { + return loginAndGetToken(baseUrl); + } + + // 创建带有当前token的请求头(注意:token 本身已包含 Bearer 前缀时无需额外拼接) + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", current.token); + HttpEntity requestEntity = new HttpEntity<>(headers); ResponseEntity> response = restTemplate.exchange( refreshUrl, @@ -95,10 +121,11 @@ public class AuthService { if (response.getStatusCode().is2xxSuccessful()) { Response responseBody = response.getBody(); if (responseBody != null) { - this.token = responseBody.getData(); - this.tokenExpiryTime = System.currentTimeMillis() + 3600 * 1000; - log.info("Successfully refreshed token"); - return this.token; + String token = responseBody.getData(); + long expiryMs = System.currentTimeMillis() + 3600 * 1000; + tokenCache.put(baseUrl, new TokenInfo(token, expiryMs)); + log.info("Successfully refreshed token for baseUrl={}", baseUrl); + return token; } } } catch (Exception e) { @@ -108,29 +135,24 @@ public class AuthService { return loginAndGetToken(baseUrl); } - //创造带有Token的请求头 - private HttpHeaders createAuthHeader() { - HttpHeaders headers = new HttpHeaders(); - if (token != null) { - headers.set("Authorization", token); - } - return headers; - } - //获取Token public String getToken(String baseUrl) { long currentTime = System.currentTimeMillis(); - if (token == null) { + TokenInfo current = tokenCache.get(baseUrl); + if (current == null || current.token == null) { return loginAndGetToken(baseUrl); } + // 如果token已过期,重新登录 - if (currentTime >= tokenExpiryTime) { + if (currentTime >= current.tokenExpiryTimeMs) { return loginAndGetToken(baseUrl); } + // 如果token即将过期(比如还有10分钟过期),尝试续期 - if (currentTime >= tokenExpiryTime - 600_000) { + if (currentTime >= current.tokenExpiryTimeMs - 600_000) { return refreshToken(baseUrl); } - return token; + + return current.token; } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java index f6e9bbd..fa20a77 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; @@ -73,6 +74,19 @@ public class DataCollectorService { @Value("${data.collector.route.periodic-collection-enabled:false}") private boolean periodicRouteCollectionEnabled; + /** + * 防止高频采集任务重入导致并发堆积、上游接口被打爆、日志/线程资源挤压。 + * 策略:丢弃旧轮次保实时(如果上一轮未完成,本轮直接跳过)。 + * + * 说明: + * - 本类中存在 fixedRate=250ms 的高频任务(且部分标记 @Async)。 + * - 在多线程调度器下,若上游抖动/超时,fixedRate 会产生并发重入。 + * - 这里用 AtomicBoolean 做最小侵入的“防重入”保护,不改变正常情况下的业务逻辑。 + */ + private final AtomicBoolean aircraftCollectInProgress = new AtomicBoolean(false); + private final AtomicBoolean vehicleCollectInProgress = new AtomicBoolean(false); + private final AtomicBoolean unmannedCollectInProgress = new AtomicBoolean(false); + @Autowired private DataCollectorDao dataCollectorDao; @@ -414,7 +428,13 @@ public class DataCollectorService { if (collectorDisabled) { return; } - + + // 丢弃旧轮次:上一轮未完成时跳过,避免任务堆积 + if (!aircraftCollectInProgress.compareAndSet(false, true)) { + log.warn("航空器采集上一轮尚未完成,跳过本轮以避免堆积(保持实时性)"); + return; + } + try { List newAircrafts = dataCollectorDao.collectAircraftData(airportAircraftEndpoint, airportBaseUrl); if (newAircrafts.isEmpty()) { @@ -463,6 +483,8 @@ public class DataCollectorService { } catch (Exception e) { log.error("采集航空器数据异常", e); + } finally { + aircraftCollectInProgress.set(false); } } @@ -482,7 +504,13 @@ public class DataCollectorService { if (collectorDisabled) { return; } - + + // 丢弃旧轮次:上一轮未完成时跳过,避免 fixedRate + @Async 并发堆积 + if (!vehicleCollectInProgress.compareAndSet(false, true)) { + log.warn("机场车辆采集上一轮尚未完成,跳过本轮以避免堆积(保持实时性)"); + return; + } + try { List vehicles = dataCollectorDao.collectVehicleData(airportVehicleEndpoint, airportBaseUrl); if (vehicles.isEmpty()) { @@ -549,6 +577,8 @@ public class DataCollectorService { } catch (Exception e) { log.error("采集机场车辆数据异常", e); + } finally { + vehicleCollectInProgress.set(false); } } @@ -569,6 +599,12 @@ public class DataCollectorService { return; } + // 丢弃旧轮次:上一轮未完成时跳过,避免“多车循环HTTP + fixedRate + @Async”堆积 + if (!unmannedCollectInProgress.compareAndSet(false, true)) { + log.warn("无人车状态采集上一轮尚未完成,跳过本轮以避免堆积(保持实时性)"); + return; + } + try { if (unmannedVehicleIds.isEmpty()) { // 启动时可能尚未刷新到列表,这里做一次快速拉取作为兜底 @@ -723,6 +759,8 @@ public class DataCollectorService { } catch (Exception e) { log.error("采集无人车数据异常", e); + } finally { + unmannedCollectInProgress.set(false); } } 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 5df808f..3cf8cf4 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 @@ -540,11 +540,11 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { String RunwayNum = getTextContent(root, "RunwayNum"); String InOut = getTextContent(root, "InOut"); //分解BizKey - String[] arr = flightNo.split("-", 3); // 最多切 3 段 + String[] arr = (flightNo == null ? new String[0] : flightNo.split("-", 3)); // 最多切 3 段 FlightNotificationDTO dto = new FlightNotificationDTO(); dto.setFlightNo(flightNo); - if (InOut.equals("A")){ + if ("A".equals(InOut)){ dto.setType("IN"); }else { dto.setType("OUT"); @@ -558,10 +558,12 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { redisCache.setCacheMapValue(key, "flightNumber", arr[0]); redisCache.setCacheMapValue(key, "type", arr[1]); redisCache.setCacheMapValue(key, "time", arr[2]); - if (InOut.equals("A")){ - redisCache.setCacheMapValue(key, "inRunway", RunwayNum); - }else { - redisCache.setCacheMapValue(key, "outRunway", RunwayNum); + if (RunwayNum != null && !RunwayNum.isBlank()) { + if ("A".equals(InOut)){ + redisCache.setCacheMapValue(key, "inRunway", RunwayNum); + }else { + redisCache.setCacheMapValue(key, "outRunway", RunwayNum); + } } log.info("成功将航班数据存储到Redis: flightNumber={}, type={}, time={}", arr[0], arr[1], arr[2]); } catch (Exception e) { @@ -580,13 +582,15 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { log.error("inRunway-{}",inRunway); log.error("outRunway-{}",outRunway); //分解BizKey - String[] arr = flightNo.split("-", 3); // 最多切 3 段 + String[] arr = (flightNo == null ? new String[0] : flightNo.split("-", 3)); // 最多切 3 段 FlightNotificationDTO dto = new FlightNotificationDTO(); dto.setFlightNo(flightNo); - if (arr[1].equals("A")){ - dto.setType("IN"); - }else { - dto.setType("OUT"); + if (arr.length >= 2) { + if ("A".equals(arr[1])){ + dto.setType("IN"); + }else { + dto.setType("OUT"); + } } // 存储到Redis if (arr.length >= 3) { @@ -595,10 +599,10 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { redisCache.setCacheMapValue(key, "flightNumber", arr[0]); redisCache.setCacheMapValue(key, "type", arr[1]); redisCache.setCacheMapValue(key, "time", arr[2]); - if (!inRunway.equals("")){ + if (inRunway != null && !inRunway.isBlank()){ redisCache.setCacheMapValue(key, "inRunway", inRunway); } - if (!outRunway.equals("")){ + if (outRunway != null && !outRunway.isBlank()){ redisCache.setCacheMapValue(key, "outRunway", outRunway); } log.info("成功将航班数据存储到Redis: flightNumber={}, type={}, time={}", arr[0], arr[1], arr[2]); @@ -615,7 +619,7 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { String code = getTextContent(root, "Code"); log.error("seat-{}",code); //分解BizKey - String[] arr = flightNo.split("-", 3); // 最多切 3 段 + String[] arr = (flightNo == null ? new String[0] : flightNo.split("-", 3)); // 最多切 3 段 FlightNotificationDTO dto = new FlightNotificationDTO(); dto.setFlightNo(flightNo); // 存储到Redis @@ -625,7 +629,9 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler { redisCache.setCacheMapValue(key, "flightNumber", arr[0]); redisCache.setCacheMapValue(key, "type", arr[1]); redisCache.setCacheMapValue(key, "time", arr[2]); - redisCache.setCacheMapValue(key, "seat", code); + if (code != null && !code.isBlank()) { + redisCache.setCacheMapValue(key, "seat", code); + } log.info("成功将航班数据存储到Redis: flightNumber={}, type={}, time={}", arr[0], arr[1], arr[2]); } catch (Exception e) { log.error("存储航班数据到Redis失败: {}", e.getMessage()); diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java index dc13578..fbebe8f 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java @@ -137,7 +137,9 @@ public class DataProcessingService { try { if (activeMovingObjectsCache == null || activeMovingObjectsCache.isEmpty()) { - log.debug("活跃对象缓存为空,跳过数据处理"); + log.debug("活跃对象缓存为空,仅处理航班通知/路由重试后返回"); + // 航班通知与路由下发不依赖活跃对象列表,避免因为无移动对象而错过路由重试 + processFlightNotificationUpdates(); return; } @@ -518,6 +520,8 @@ public class DataProcessingService { if (flightNotificationCache.isEmpty()) { log.debug("航班通知缓存为空,跳过处理"); + // 仍需要处理待重试的路由查询(可能来自 WS 立即触发但参数未齐全) + processPendingRouteQueries(); return; } @@ -560,6 +564,8 @@ public class DataProcessingService { } log.info("航班通知数据处理完成,处理数量: {}", flightNotificationCache.size()); + // 处理待重试的路由查询 + processPendingRouteQueries(); } catch (Exception e) { log.error("处理航班通知数据异常", e); @@ -1027,9 +1033,42 @@ public class DataProcessingService { // ==================== 航班路由处理相关方法 ==================== - // 路由获取状态缓存:Key=flightNo:type:time, Value=获取时间戳 + /** + * 路由获取状态缓存:Key=flightNo:routeType(IN/OUT), Value=获取时间戳(ms) + * + * 说明: + * - 目前路由“每个航班每种类型只需获取一次”即可满足前端展示/实时引导需求 + * - 因为存在两种触发方式(航班通知统一处理、WS ARR/AXOT 立即触发),这里用 flightNo+routeType 去重 + */ private final Map routeRetrievalCache = new ConcurrentHashMap<>(); + /** + * 待重试的路由查询任务:Key=flightNo:routeType(IN/OUT) + * - 当 Redis 参数尚未齐全或路由接口返回为空时,进入重试队列 + * - 在周期性处理线程中按 backoff 进行重试,避免在 WS 回调里阻塞/刷接口 + */ + private final Map pendingRouteQueries = new ConcurrentHashMap<>(); + + private static final int ROUTE_RETRY_MAX_ATTEMPTS = 20; + private static final long ROUTE_RETRY_MIN_DELAY_MS = 1_000L; // 1s + private static final long ROUTE_RETRY_MAX_DELAY_MS = 30_000L; // 30s + + private static class PendingRouteQuery { + private final String flightNo; + private final String routeType; // IN / OUT + private long nextRetryAtMs; + private int attempts; + private String lastSource; + + private PendingRouteQuery(String flightNo, String routeType, long nextRetryAtMs, int attempts, String lastSource) { + this.flightNo = flightNo; + this.routeType = routeType; + this.nextRetryAtMs = nextRetryAtMs; + this.attempts = attempts; + this.lastSource = lastSource; + } + } + /** * 基于航班通知触发路由查询 * @@ -1046,90 +1085,23 @@ public class DataProcessingService { String flightNo = flightNotification.getFlightNo(); String routeType = flightNotification.getType().name(); - // 检查是否已获取过该航班事件的路由 - if (isRouteAlreadyRetrieved(flightNo, routeType, flightNotification.getEventTime())) { - log.info("🔄 航班路由已获取过,跳过重复查询: 航班号={}, 类型={}, 时间={}", - flightNo, routeType, flightNotification.getEventTime()); + // 检查是否已获取过该航班路由(按 flightNo + routeType 去重) + if (isRouteAlreadyRetrieved(flightNo, routeType)) { + log.info("航班路由已获取过,跳过重复查询: 航班号={}, 类型={}", flightNo, routeType); return; } - log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}, 时间={}", flightNo, routeType, flightNotification.getEventTime()); + log.info("航班通知触发路由查询(仅Redis拼参): 航班号={}, 类型={}, eventTime={}", + flightNo, routeType, flightNotification.getEventTime()); - // 获取DataCollectorDao进行路由数据查询 - DataCollectorDao dataCollectorDao = applicationContext.getBean(DataCollectorDao.class); - - // 步骤1: 查询航班路由参数 - com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams = - dataCollectorDao.getAircraftRouteParams(flightNo, routeType); - - if (routeParams == null || !routeParams.isValid()) { - log.warn("⚠️ 未能获取有效的航班路由参数: flightNo={}, routeType={}", flightNo, routeType); - return; - } - - log.info("✅ 成功获取航班路由参数: flightNo={}, inRunway={}, outRunway={}, contactCross={}, seat/startSeat={}", - routeParams.getFlightNo(), - routeParams.getInRunway(), - routeParams.getOutRunway(), - routeParams.getContactCross(), - routeParams.isArrivalRoute() ? routeParams.getSeat() : routeParams.getStartSeat()); - - // 步骤2: 基于路由参数调用相应的路由查询接口 - AircraftRouteDTO routeData = null; - - if (routeParams.isArrivalRoute()) { - // 进港路由查询 - routeData = dataCollectorDao.getArrivalRoute( - routeParams.getInRunway(), - routeParams.getOutRunway(), - routeParams.getContactCross(), - routeParams.getSeat() - ); - log.info("🛬 查询进港路由: inRunway={}, outRunway={}, contactCross={}, seat={}", - routeParams.getInRunway(), routeParams.getOutRunway(), - routeParams.getContactCross(), routeParams.getSeat()); - } else if (routeParams.isDepartureRoute()) { - // 出港路由查询 - routeData = dataCollectorDao.getDepartureRoute( - routeParams.getInRunway(), - routeParams.getOutRunway(), - routeParams.getStartSeat() - ); - log.info("🛫 查询出港路由: inRunway={}, outRunway={}, startSeat={}", - routeParams.getInRunway(), routeParams.getOutRunway(), routeParams.getStartSeat()); - } - - // 步骤3: 处理路由查询结果 - if (routeData != null) { - log.info("🎯 成功获取{}路由数据: 编码={}, 状态={}", - routeData.getType(), routeData.getCodes(), routeData.getStatus()); - - // 转换DTO为航空器路由对象 - AircraftRoute aircraftRoute = convertToAircraftRoute(routeData); - - if (aircraftRoute != null) { - // 保存路由到数据库 - saveAircraftRouteToDatabase(flightNo, aircraftRoute); - - // 更新缓存中的航空器路由信息 - updateAircraftRouteInCache(flightNo, aircraftRoute, routeParams); - - // 发布WebSocket路由更新事件 - publishAircraftRouteUpdateEvent(flightNo, aircraftRoute, routeParams); - - // 标记该航班事件的路由已获取 - markRouteAsRetrieved(flightNo, routeType, flightNotification.getEventTime()); - - log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}, 时间={}", flightNo, routeType, flightNotification.getEventTime()); - } else { - log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo); - } - } else { - log.warn("⚠️ 未获取到路由数据: flightNo={}, routeType={}", flightNo, routeType); + boolean success = tryQueryAndPublishRouteFromRedis(flightNo, routeType, "FLIGHT_NOTIFICATION"); + if (!success) { + // Redis 字段未齐全或路由接口暂不可用:进入重试队列,下一轮再试 + schedulePendingRouteQuery(flightNo, routeType, "FLIGHT_NOTIFICATION"); } } catch (Exception e) { - log.error("❌ 航班通知触发路由查询异常: flightNo={}", + log.error("航班通知触发路由查询异常: flightNo={}", flightNotification.getFlightNo(), e); } } @@ -1137,20 +1109,273 @@ public class DataProcessingService { /** * 检查指定航班事件的路由是否已经获取过 */ - private boolean isRouteAlreadyRetrieved(String flightNo, String type, Long time) { - String cacheKey = flightNo + ":" + type + ":" + time; + private boolean isRouteAlreadyRetrieved(String flightNo, String routeType) { + String cacheKey = flightNo + ":" + routeType; return routeRetrievalCache.containsKey(cacheKey); } /** * 标记指定航班事件的路由已获取 */ - private void markRouteAsRetrieved(String flightNo, String type, Long time) { - String cacheKey = flightNo + ":" + type + ":" + time; + private void markRouteAsRetrieved(String flightNo, String routeType) { + String cacheKey = flightNo + ":" + routeType; routeRetrievalCache.put(cacheKey, System.currentTimeMillis()); log.debug("标记路由已获取: cacheKey={}, 当前缓存数量={}", cacheKey, routeRetrievalCache.size()); } + /** + * 从 Redis 拼装路由参数并查询路由,成功后发布路由更新事件。 + * + * 约束(按你的要求): + * - 不再调用 /aircraftRouteParamsController/getRouteParams + * - 路由参数只从 Redis(flight:)聚合 + * + * @return true=成功获取并发布;false=参数未齐全或路由接口失败(可进入重试) + */ + private boolean tryQueryAndPublishRouteFromRedis(String flightNo, String routeType, String eventSource) { + try { + if (flightNo == null || flightNo.isBlank() || routeType == null || routeType.isBlank()) { + return false; + } + String normalizedFlightNo = flightNo.contains("-") ? flightNo.split("-", 2)[0] : flightNo; + String normalizedRouteType = routeType.trim().toUpperCase(); + String key = "flight:" + normalizedFlightNo.trim(); + + // 从 Redis 获取参数快照 + // 注意:RedisCache 可能会把字符串序列化为带引号的 JSON 字符串(例如 "35"),甚至返回非 String 类型。 + // 这里统一做归一化,避免 ClassCastException,并保证 queryParam 不携带多余引号。 + String inRunway = normalizeRedisString(redisCache.getCacheMapValue(key, "inRunway")); + String outRunway = normalizeRedisString(redisCache.getCacheMapValue(key, "outRunway")); + String contactCross = normalizeRedisString(redisCache.getCacheMapValue(key, "contactCross")); + String seat = normalizeRedisString(redisCache.getCacheMapValue(key, "seat")); + String startSeat = normalizeRedisString(redisCache.getCacheMapValue(key, "startSeat")); + + // OUT:允许用 seat 兜底 startSeat(机位推出) + if ("OUT".equalsIgnoreCase(normalizedRouteType) && (startSeat == null || startSeat.isBlank())) { + if (seat != null && !seat.isBlank()) { + startSeat = seat; + } + } + + // 跑道补齐策略(按你的要求): + // - OUT 缺少 inRunway:使用固定值 "34" 补齐 + // - IN 缺少 outRunway:使用固定值 "34" 补齐 + // 同时记录是否“补齐”,用于下发给前端联调核对 + final String PATCH_RUNWAY_VALUE = "34"; + boolean inRunwayPatched = false; + boolean outRunwayPatched = false; + if ("OUT".equalsIgnoreCase(normalizedRouteType) && (inRunway == null || inRunway.isBlank())) { + inRunway = PATCH_RUNWAY_VALUE; + inRunwayPatched = true; + } + if ("IN".equalsIgnoreCase(normalizedRouteType) && (outRunway == null || outRunway.isBlank())) { + outRunway = PATCH_RUNWAY_VALUE; + outRunwayPatched = true; + } + + // 参数校验(字段不全:返回 false,交给重试机制) + if ("IN".equalsIgnoreCase(normalizedRouteType)) { + // IN:按“四个参数”拼参:inRunway/outRunway/contactCross/seat + if (inRunway == null || inRunway.isBlank()) { + log.info("进港路由Redis参数未齐全(缺少inRunway),等待补齐后重试: flightNo={}, inRunway={}", + normalizedFlightNo, inRunway); + return false; + } + if (outRunway == null || outRunway.isBlank()) { + log.info("进港路由Redis参数未齐全(缺少outRunway),等待补齐后重试: flightNo={}, outRunway={}", + normalizedFlightNo, outRunway); + return false; + } + if (contactCross == null || contactCross.isBlank() || seat == null || seat.isBlank()) { + // 打印当前已拼装参数,便于定位到底缺了哪些字段以及跑道是否为补齐值 + log.info("进港路由Redis参数未齐全,等待补齐后重试: flightNo={}, inRunway={} (patched={}), outRunway={} (patched={}), contactCross={}, seat={}", + normalizedFlightNo, inRunway, inRunwayPatched, outRunway, outRunwayPatched, contactCross, seat); + return false; + } + } else if ("OUT".equalsIgnoreCase(normalizedRouteType)) { + // OUT:按“四个参数”拼参:inRunway/outRunway/startSeat(startSeat 可由 seat 兜底) + if (inRunway == null || inRunway.isBlank()) { + log.info("出港路由Redis参数未齐全(缺少inRunway),等待补齐后重试: flightNo={}, inRunway={}", + normalizedFlightNo, inRunway); + return false; + } + if (outRunway == null || outRunway.isBlank()) { + log.info("出港路由Redis参数未齐全(缺少outRunway),等待补齐后重试: flightNo={}, outRunway={}", + normalizedFlightNo, outRunway); + return false; + } + if (startSeat == null || startSeat.isBlank()) { + log.info("出港路由Redis参数未齐全,等待补齐后重试: flightNo={}, inRunway={} (patched={}), outRunway={} (patched={}), startSeat={}, seatAsFallback={}", + normalizedFlightNo, inRunway, inRunwayPatched, outRunway, outRunwayPatched, startSeat, seat); + return false; + } + } else { + log.info("未知routeType,跳过路由查询: flightNo={}, routeType={}", normalizedFlightNo, normalizedRouteType); + return false; + } + + // 调用路由接口 + DataCollectorDao dataCollectorDao = applicationContext.getBean(DataCollectorDao.class); + AircraftRouteDTO routeData; + com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams = + new com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO(); + routeParams.setFlightNo(normalizedFlightNo); + routeParams.setRouteType(normalizedRouteType); + routeParams.setInRunway(inRunway); + routeParams.setOutRunway(outRunway); + routeParams.setContactCross(contactCross); + routeParams.setSeat(seat); + routeParams.setStartSeat(startSeat); + routeParams.setTimestamp(System.currentTimeMillis()); + routeParams.setInRunwayPatched(inRunwayPatched); + routeParams.setOutRunwayPatched(outRunwayPatched); + + if ("IN".equalsIgnoreCase(normalizedRouteType)) { + routeData = dataCollectorDao.getArrivalRoute(inRunway, outRunway, contactCross, seat); + } else { + routeData = dataCollectorDao.getDepartureRoute(inRunway, outRunway, startSeat); + } + + if (routeData == null) { + // 路由服务返回空体/无法解析时打印本次请求参数(含补齐标记),便于容器日志直接定位 + log.info("路由接口返回为空,稍后重试: flightNo={}, routeType={}, source={}, inRunway={} (patched={}), outRunway={} (patched={}), contactCross={}, seat={}, startSeat={}", + normalizedFlightNo, normalizedRouteType, eventSource, + inRunway, inRunwayPatched, outRunway, outRunwayPatched, contactCross, seat, startSeat); + return false; + } + + AircraftRoute aircraftRoute = convertToAircraftRoute(routeData); + if (aircraftRoute == null) { + log.info("路由数据转换失败,稍后重试: flightNo={}, routeType={}, source={}", + normalizedFlightNo, normalizedRouteType, eventSource); + return false; + } + + // 保存 + 更新缓存 + 下发前端 + saveAircraftRouteToDatabase(normalizedFlightNo, aircraftRoute); + updateAircraftRouteInCache(normalizedFlightNo, aircraftRoute, routeParams); + publishAircraftRouteUpdateEvent(normalizedFlightNo, aircraftRoute, routeParams, eventSource); + + // 标记已获取,避免两种触发方式重复下发 + markRouteAsRetrieved(normalizedFlightNo, normalizedRouteType); + pendingRouteQueries.remove(normalizedFlightNo + ":" + normalizedRouteType); + + log.info("路由查询与下发成功: flightNo={}, routeType={}, source={}", + normalizedFlightNo, normalizedRouteType, eventSource); + return true; + + } catch (Exception e) { + log.error("从Redis拼参查询路由异常: flightNo={}, routeType={}, source={}", flightNo, routeType, eventSource, e); + return false; + } + } + + /** + * 将 RedisCache 返回值归一化为可用字符串。 + * - 兼容 RedisCache 以 JSON 方式序列化字符串导致的外层引号(例如 "\"35\"" 或 "\"SC4846\"") + * - 兼容非 String 类型(避免 ClassCastException) + * - 统一 trim,空串返回 null + */ + private static String normalizeRedisString(Object v) { + if (v == null) { + return null; + } + String s = String.valueOf(v).trim(); + if (s.isEmpty()) { + return null; + } + + // 去掉一层外部双引号: "35" -> 35 + if (s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) { + s = s.substring(1, s.length() - 1).trim(); + } + + // 去掉一层外部单引号: '35' -> 35(防御性) + if (s.length() >= 2 && s.startsWith("'") && s.endsWith("'")) { + s = s.substring(1, s.length() - 1).trim(); + } + + return s.isEmpty() ? null : s; + } + + private void schedulePendingRouteQuery(String flightNo, String routeType, String source) { + try { + if (flightNo == null || flightNo.isBlank() || routeType == null || routeType.isBlank()) { + return; + } + String normalizedFlightNo = flightNo.contains("-") ? flightNo.split("-", 2)[0] : flightNo; + String normalizedRouteType = routeType.trim().toUpperCase(); + String key = normalizedFlightNo + ":" + normalizedRouteType; + + PendingRouteQuery existing = pendingRouteQueries.get(key); + long now = System.currentTimeMillis(); + + if (existing == null) { + long next = now + ROUTE_RETRY_MIN_DELAY_MS; + pendingRouteQueries.put(key, new PendingRouteQuery(normalizedFlightNo, normalizedRouteType, next, 0, source)); + log.debug("新增待重试路由查询: key={}, nextRetryAtMs={}", key, next); + return; + } + + // 已存在:更新时间与来源(不改变 attempts,避免疯狂回退) + existing.lastSource = source; + if (existing.nextRetryAtMs < now) { + existing.nextRetryAtMs = now + ROUTE_RETRY_MIN_DELAY_MS; + } + } catch (Exception e) { + log.error("加入待重试路由查询失败: flightNo={}, routeType={}, source={}", flightNo, routeType, source, e); + } + } + + private void processPendingRouteQueries() { + try { + if (pendingRouteQueries.isEmpty()) { + return; + } + long now = System.currentTimeMillis(); + + for (PendingRouteQuery pending : pendingRouteQueries.values()) { + if (pending == null) { + continue; + } + if (pending.nextRetryAtMs > now) { + continue; + } + + String key = pending.flightNo + ":" + pending.routeType; + + // 已成功则无需重试 + if (isRouteAlreadyRetrieved(pending.flightNo, pending.routeType)) { + pendingRouteQueries.remove(key); + continue; + } + + // 超过最大次数:停止重试,避免永久占用 + if (pending.attempts >= ROUTE_RETRY_MAX_ATTEMPTS) { + log.info("路由重试达到上限,停止重试: key={}, attempts={}, lastSource={}", + key, pending.attempts, pending.lastSource); + pendingRouteQueries.remove(key); + continue; + } + + boolean ok = tryQueryAndPublishRouteFromRedis(pending.flightNo, pending.routeType, "RETRY(" + pending.lastSource + ")"); + if (ok) { + pendingRouteQueries.remove(key); + continue; + } + + // backoff:1s,2s,4s,... 上限 30s + pending.attempts++; + long delay = ROUTE_RETRY_MIN_DELAY_MS * (1L << Math.min(pending.attempts, 5)); // 1,2,4,8,16,32 -> cap + delay = Math.min(delay, ROUTE_RETRY_MAX_DELAY_MS); + pending.nextRetryAtMs = now + delay; + log.debug("路由重试失败,安排下次重试: key={}, attempts={}, nextDelayMs={}", key, pending.attempts, delay); + } + } catch (Exception e) { + log.error("处理待重试路由查询异常", e); + } + } + /** * 将AircraftRouteDTO转换为AircraftRoute对象 * 使用JTS将多个LineString段合并为单一连续路径 @@ -1179,7 +1404,7 @@ public class DataProcessingService { // 创建路由段 AircraftRoute.RouteSegment segment = AircraftRoute.RouteSegment.builder() - .code(feature.getProperties() != null ? feature.getProperties().getCode() : "") + .code(feature.getProperties() != null ? feature.getProperties().getCodeAsString() : "") .coordinates(points) .build(); routeSegments.add(segment); @@ -1254,6 +1479,10 @@ public class DataProcessingService { */ private void updateAircraftRouteInCache(String flightNo, AircraftRoute route, com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) { + if (activeMovingObjectsCache == null) { + log.debug("activeMovingObjectsCache 未初始化,跳过航空器路由缓存更新: flightNo={}", flightNo); + return; + } MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo); if (cachedAircraft != null && cachedAircraft instanceof com.qaup.collision.common.model.Aircraft) { @@ -1281,9 +1510,11 @@ public class DataProcessingService { /** * 发布航空器路由更新事件 */ - private void publishAircraftRouteUpdateEvent(String flightNo, AircraftRoute route, - com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) { - log.error( "发布航空器路由更新事件: flightNo={}", flightNo); + private void publishAircraftRouteUpdateEvent(String flightNo, + AircraftRoute route, + com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams, + String eventSource) { + log.error("发布航空器路由更新事件: flightNo={}", flightNo); try { // 创建路由更新事件 com.qaup.collision.websocket.event.AircraftRouteUpdateEvent routeUpdateEvent = @@ -1294,15 +1525,19 @@ public class DataProcessingService { .routeCodes(route.getCodes()) .aircraftStatus(routeParams.getRouteType()) // 使用路由参数中的类型 .routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null) + .inRunway(routeParams.getInRunway()) + .outRunway(routeParams.getOutRunway()) + .inRunwayPatched(routeParams.getInRunwayPatched()) + .outRunwayPatched(routeParams.getOutRunwayPatched()) .timestamp(System.currentTimeMillis()) - .eventSource("FLIGHT_NOTIFICATION") // 标识事件来源 + .eventSource(eventSource) // 标识事件来源 .build(); // 发布WebSocket事件 eventPublisher.publishEvent(routeUpdateEvent); - log.info("📡 发布航空器路由更新事件: 航班号={}, 路由类型={}, 事件来源=航班通知", - flightNo, route.getType()); + log.info("📡 发布航空器路由更新事件: 航班号={}, 路由类型={}, 事件来源={}", + flightNo, route.getType(), eventSource); } catch (Exception e) { log.error("❌ 发布航空器路由更新事件失败: flightNo={}", flightNo, e); @@ -1313,84 +1548,25 @@ public class DataProcessingService { * 触发查询航班号对应的路由 */ public void ARR(String flightNo){ - String key = "flight:"+flightNo; - - String time =(String) redisCache.getCacheMapValue(key, "time"); - log.error("time是{}",time); -// String inRunway = "35"; -// String outRunway = "34"; -// String contactCross = "F1"; -// String seat = "138"; - String inRunway = (String) redisCache.getCacheMapValue(key, "inRunway"); - String outRunway = (String) redisCache.getCacheMapValue(key, "outRunway"); - String contactCross = (String) redisCache.getCacheMapValue(key, "contactCross"); - String seat = (String) redisCache.getCacheMapValue(key, "seat"); - - log.error("进港参数是{}",inRunway); - log.error("出港参数是{}",outRunway); - log.error("接触交叉参数是{}",contactCross); - log.error("座位参数是{}",seat); - - // 获取DataCollectorDao进行路由数据查询 - DataCollectorDao dataCollectorDao = applicationContext.getBean(DataCollectorDao.class); - AircraftRouteDTO routeData = null; - // 进港路由查询 - routeData = dataCollectorDao.getArrivalRoute( - inRunway, - outRunway, - contactCross, - seat); - log.error("进港路由数据routeData是{}",routeData); - - // 创建路由参数:目前有用的只有type - com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams = new com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO(); - routeParams.setFlightNo(flightNo); - routeParams.setRouteType("IN"); - routeParams.setInRunway(inRunway); - routeParams.setOutRunway(outRunway); - routeParams.setContactCross(contactCross); - routeParams.setSeat(seat); - - //处理路由数据并发布 - handleRouteData(flightNo, routeData, routeParams); + // 两种触发方式均启用:WS 收到 ARR 立即触发(参数只从 Redis 拼装) + if (isRouteAlreadyRetrieved(flightNo, "IN")) { + return; + } + boolean ok = tryQueryAndPublishRouteFromRedis(flightNo, "IN", "ADXP_WS_ARR"); + if (!ok) { + schedulePendingRouteQuery(flightNo, "IN", "ADXP_WS_ARR"); + } } public void AXOT(String flightNo){ - String key = "flight:"+flightNo; - - String time =(String) redisCache.getCacheMapValue(key, "time"); - log.error("time是{}",time); -// String inRunway = "17"; -// String outRunway = "35"; -// String startSeat = "201"; - String inRunway = (String) redisCache.getCacheMapValue(key, "inRunway"); - String outRunway = (String) redisCache.getCacheMapValue(key, "outRunway"); - String startSeat = (String) redisCache.getCacheMapValue(key, "startSeat"); - - log.error("进港参数是{}",inRunway); - log.error("出港参数是{}",outRunway); - log.error("起始座位参数是{}",startSeat); - - // 获取DataCollectorDao进行路由数据查询 - DataCollectorDao dataCollectorDao = applicationContext.getBean(DataCollectorDao.class); - AircraftRouteDTO routeData = null; - // 进港路由查询 - routeData = dataCollectorDao.getDepartureRoute( - inRunway, - outRunway, - startSeat); - log.error("出港路由数据是routeData是{}",routeData); - - // 创建路由参数:目前有用的只有type - com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams = new com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO(); - routeParams.setFlightNo(flightNo); - routeParams.setRouteType("OUT"); - routeParams.setInRunway(inRunway); - routeParams.setOutRunway(outRunway); - routeParams.setStartSeat(startSeat); - - //处理路由数据并发布 - handleRouteData(flightNo, routeData, routeParams); + // 两种触发方式均启用:WS 收到 AXOT 立即触发(参数只从 Redis 拼装) + if (isRouteAlreadyRetrieved(flightNo, "OUT")) { + return; + } + boolean ok = tryQueryAndPublishRouteFromRedis(flightNo, "OUT", "ADXP_WS_AXOT"); + if (!ok) { + schedulePendingRouteQuery(flightNo, "OUT", "ADXP_WS_AXOT"); + } } private void handleRouteData(String flightNo, AircraftRouteDTO routeData, @@ -1408,7 +1584,8 @@ public class DataProcessingService { saveAircraftRouteToDatabase(flightNo, aircraftRoute); // 发布WebSocket路由更新事件 - publishAircraftRouteUpdateEvent(flightNo, aircraftRoute, routeParams); + // 兼容旧的辅助方法:事件来源标记为 MANUAL + publishAircraftRouteUpdateEvent(flightNo, aircraftRoute, routeParams, "MANUAL"); } else { log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo); } diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/event/AircraftRouteUpdateEvent.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/event/AircraftRouteUpdateEvent.java index 61de284..fb980f9 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/websocket/event/AircraftRouteUpdateEvent.java +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/event/AircraftRouteUpdateEvent.java @@ -49,6 +49,26 @@ public class AircraftRouteUpdateEvent { * 路由几何数据 (WKT格式) */ private String routeGeometry; + + /** + * 进港跑道编号(用于前端联调查看本次路由查询参数) + */ + private String inRunway; + + /** + * 出港跑道编号(用于前端联调查看本次路由查询参数) + */ + private String outRunway; + + /** + * inRunway 是否为系统补齐值(true=补齐;false/空=来自报文/Redis原始值) + */ + private Boolean inRunwayPatched; + + /** + * outRunway 是否为系统补齐值(true=补齐;false/空=来自报文/Redis原始值) + */ + private Boolean outRunwayPatched; /** * 事件时间戳 diff --git a/qaup-framework/src/main/java/com/qaup/framework/config/SecurityConfig.java b/qaup-framework/src/main/java/com/qaup/framework/config/SecurityConfig.java index 62794f0..fc438fb 100644 --- a/qaup-framework/src/main/java/com/qaup/framework/config/SecurityConfig.java +++ b/qaup-framework/src/main/java/com/qaup/framework/config/SecurityConfig.java @@ -3,6 +3,7 @@ package com.qaup.framework.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; @@ -66,6 +67,12 @@ public class SecurityConfig @Autowired private PermitAllUrlProperties permitAllUrl; + /** + * 调试接口开关:仅开启时才放行 debug 路径,避免误暴露。 + */ + @Value("${qaup.debug.runway-path-planning.enabled:false}") + private boolean runwayPathPlanningDebugEnabled; + /** * 身份验证实现 */ @@ -113,6 +120,8 @@ public class SecurityConfig requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() // WebSocket端点,允许匿名访问 .requestMatchers("/collision", "/collision/**", "/VehicleCommandInfo", "/test/websocket/**").permitAll() + // 调试接口:仅在显式开启开关时放行(默认不放行) + .requestMatchers(runwayPathPlanningDebugEnabled ? "/debug/runway-path-planning/**" : "/__debug_disabled__").permitAll() // 静态资源,可匿名访问 .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() diff --git a/verify-jar.py b/verify-jar.py new file mode 100644 index 0000000..ea94e8b --- /dev/null +++ b/verify-jar.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +QAUP 打包验证脚本(Python) + +目的: + 验证 qaup-admin.jar(Spring Boot fat jar)中是否包含“最新改动”的关键特征字符串。 + 通过读取 qaup-admin.jar -> BOOT-INF/lib/qaup-collision*.jar & qaup-framework*.jar + -> 读取指定 .class 条目 -> 在 class 常量池字节中搜索关键字符串(UTF-8)。 + +用法(在项目根目录执行): + python verify-jar.py + python verify-jar.py --jar qaup-admin/target/qaup-admin.jar + +退出码: + 0 = PASS(命中至少一个关键特征) + 2 = FAIL(未命中关键特征,通常表示不是新包) + 3 = FAIL(缺 jar 或缺 class,或不是 Spring Boot fat jar) +""" + +from __future__ import annotations + +import argparse +import io +import os +import sys +import zipfile +from dataclasses import dataclass +from typing import Iterable, List, Optional, Tuple + + +DEFAULT_ADMIN_JAR = os.path.join("qaup-admin", "target", "qaup-admin.jar") + + +NEEDLES: List[str] = [ + # DataCollectorDao: 新增的失败参数打印 + "bodySummary=", + "进港路由请求URL", + "出港路由请求URL", + # 补齐标记字段 + "inRunwayPatched", + "outRunwayPatched", + # DataProcessingService: 新增的失败时参数打印包含 patched= + "patched=", + # SecurityConfig: debug 放行开关 + "qaup.debug.runway-path-planning.enabled", +] + + +@dataclass(frozen=True) +class ClassTarget: + name: str + jar_kind: str # "collision" | "framework" + class_entry: str + + +CLASS_TARGETS: List[ClassTarget] = [ + ClassTarget( + name="DataCollectorDao", + jar_kind="collision", + class_entry="com/qaup/collision/datacollector/dao/DataCollectorDao.class", + ), + ClassTarget( + name="DataProcessingService", + jar_kind="collision", + class_entry="com/qaup/collision/dataprocessing/service/DataProcessingService.class", + ), + ClassTarget( + name="AircraftRouteUpdateEvent", + jar_kind="collision", + class_entry="com/qaup/collision/websocket/event/AircraftRouteUpdateEvent.class", + ), + ClassTarget( + name="SecurityConfig", + jar_kind="framework", + class_entry="com/qaup/framework/config/SecurityConfig.class", + ), +] + + +def eprint(*args: object) -> None: + print(*args, file=sys.stderr) + + +def read_zip_entry(zf: zipfile.ZipFile, entry_name: str) -> bytes: + with zf.open(entry_name, "r") as f: + return f.read() + + +def find_first_entry_by_prefix(zf: zipfile.ZipFile, prefix: str) -> Optional[str]: + for n in zf.namelist(): + if n.startswith(prefix): + return n + return None + + +def find_lib_jar(zf_admin: zipfile.ZipFile, lib_prefix: str) -> Optional[str]: + # BOOT-INF/lib/qaup-collision*.jar + candidates = [n for n in zf_admin.namelist() if n.startswith("BOOT-INF/lib/") and lib_prefix in os.path.basename(n)] + candidates.sort() + return candidates[0] if candidates else None + + +def scan_bytes_for_needles(blob: bytes, needles: Iterable[str]) -> List[str]: + hits: List[str] = [] + for s in needles: + b = s.encode("utf-8", errors="strict") + if b in blob: + hits.append(s) + return hits + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser(description="Verify qaup-admin.jar contains recent route changes.") + parser.add_argument("--jar", dest="jar_path", default=DEFAULT_ADMIN_JAR, help="Path to qaup-admin.jar") + args = parser.parse_args(argv) + + jar_path = os.path.abspath(args.jar_path) + print(f"JarPath: {jar_path}") + if not os.path.exists(jar_path): + eprint(f"ERROR: jar 不存在: {jar_path}") + return 3 + + try: + with zipfile.ZipFile(jar_path, "r") as zf_admin: + lib_dir = find_first_entry_by_prefix(zf_admin, "BOOT-INF/lib/") + if lib_dir is None: + eprint("ERROR: 未发现 BOOT-INF/lib/,可能不是 Spring Boot fat jar。") + return 3 + + collision_jar_entry = find_lib_jar(zf_admin, "qaup-collision") + framework_jar_entry = find_lib_jar(zf_admin, "qaup-framework") + if collision_jar_entry is None: + eprint("ERROR: 未找到 BOOT-INF/lib/qaup-collision*.jar") + return 3 + if framework_jar_entry is None: + eprint("ERROR: 未找到 BOOT-INF/lib/qaup-framework*.jar") + return 3 + + print(f"collisionJarEntry: {collision_jar_entry}") + print(f"frameworkJarEntry: {framework_jar_entry}") + + collision_bytes = read_zip_entry(zf_admin, collision_jar_entry) + framework_bytes = read_zip_entry(zf_admin, framework_jar_entry) + + jars = { + "collision": zipfile.ZipFile(io.BytesIO(collision_bytes), "r"), + "framework": zipfile.ZipFile(io.BytesIO(framework_bytes), "r"), + } + + any_hit = False + missing = False + + print("\n=== Scan class signatures ===") + for t in CLASS_TARGETS: + zf = jars[t.jar_kind] + if t.class_entry not in zf.namelist(): + print(f"MISSING class: {t.name} -> {t.jar_kind}:{t.class_entry}") + missing = True + continue + blob = read_zip_entry(zf, t.class_entry) + hits = scan_bytes_for_needles(blob, NEEDLES) + any_hit = any_hit or bool(hits) + print(f"OK class: {t.name}") + print(" hits: " + (", ".join(hits) if hits else "")) + + for zf in jars.values(): + zf.close() + + print("\n=== Result ===") + if missing: + print("FAIL: 依赖 jar 或 class 缺失,无法验证(可能打包不完整)") + return 3 + if any_hit: + print("PASS: 已命中关键特征字符串(大概率是最新改动包)") + return 0 + + print("FAIL: 未命中任何关键特征字符串(大概率不是最新改动包)") + return 2 + + except zipfile.BadZipFile: + eprint("ERROR: jar 不是有效的 zip/jar 文件") + return 3 + except Exception as ex: + eprint(f"ERROR: 执行失败: {ex}") + return 3 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/命令.md b/命令.md index 7473e3f..0be8581 100644 --- a/命令.md +++ b/命令.md @@ -1,6 +1,6 @@ ### 命令 ``` -mvn -pl qaup-admin -am package -DskipTests +mvn -pl qaup-admin -am clean package -DskipTests ``` ``` @@ -65,11 +65,206 @@ tail -n 200000 /home/project_20250804/qaup/adxp-adapter/logs/adapter-logs.log \ | tail -n 200 -curl -s http://10.64.58.228:8086/api/adxp/status -curl -s http://10.64.58.228:8086/api/adxp/health -curl -s -X POST http://10.64.58.228:8086/api/adxp/reconnect + + docker exec -it qaup-app sh -lc 'grep -nE "连接到ADXP适配器WebSocket服务|已连接到ADXP适配器WebSocket服务|数据中台航班 SDK 配置不完整|接收到 .* 条航班通知|通过WebSocket接收到 .* 条航班进出港通知|数据中台航班 SDK 未启用|未获取到航班进出港通知数据|航班进出港通知数据无效" /logs/sys-info.log /logs/sys-error.log 2>/dev/null | tail -n 200' -docker exec -it qaup-app sh -lc 'grep -nE "接收到 [0-9]+ 条航班通知|通过WebSocket接收到|新增航班通知缓存|合并航班通知缓存|发布航班进出港通知WebSocket事件" /logs/sys-info.log /logs/sys-error.log 2>/dev/null | tail -n 200' \ No newline at end of file +docker exec -it qaup-app sh -lc 'grep -nE "接收到 [0-9]+ 条航班通知|通过WebSocket接收到|新增航班通知缓存|合并航班通知缓存|发布航班进出港通知WebSocket事件" /logs/sys-info.log /logs/sys-error.log 2>/dev/null | tail -n 200' + +``` +(() => { + const url = "ws://10.64.58.228:8080/collision"; + const ws = new WebSocket(url); + window.__qaupWs = ws; + ws.onopen = () => console.log("open", url); + ws.onerror = (e) => console.log("error", e); + ws.onclose = (e) => console.log("close", e.code, e.reason); + ws.onmessage = (e) => { + const s = typeof e.data === "string" ? e.data : ""; + if (s.includes('"FLIGHT_NOTIFICATION"') || s.includes('"aircraftRouteUpdate"')) { + console.log("MATCH", s); + } + }; +})(); +``` +docker exec -it qaup-app sh -lc 'grep -nE "航班通知触发路由查询|成功获取航班路由参数|查询进港路由|查询出港路由|成功获取.*路由数据|发布航空器路由更新事件|发布航空器路由更新事件失败|航空器路由更新事件已通过WebSocket推送|未能获取有效的航班路由参数|未获取到路由数据" /logs/sys-info.log /logs/sys-error.log 2>/dev/null | tail -n 300' + +docker exec -it qaup-app sh -lc 'grep -nE "CZ3519|航班通知触发路由查询|成功获取航班路由参数|未能获取有效的航班路由参数|查询进港路由|查询出港路由|成功获取.*路由数据|未获取到路由数据|发布航空器路由更新事件|发布航空器路由更新事件失败|航空器路由更新事件已通过WebSocket推送" /logs/sys-info.log /logs/sys-error.log 2>/dev/null | tail -n 400' + +docker exec -it qaup-app sh -lc 'grep -nE "CZ3519|未能获取有效的航班路由参数|成功获取航班路由参数|查询进港路由|查询出港路由|成功获取.*路由数据|未获取到路由数据|发布航空器路由更新事件|发布航空器路由更新事件失败|航空器路由更新事件已通过WebSocket推送|航班路由已获取过" /logs/sys-info.log /logs/sys-error.log 2>/dev/null | tail -n 400' + +docker exec -it qaup-app sh -lc 'grep -nE "成功获取航班路由参数|未能获取有效的航班路由参数|查询进港路由|查询出港路由|成功获取.*路由数据|发布航空器路由更新事件|航空器路由更新事件已通过WebSocket推送" /logs/sys-info.log /logs/sys-error.log 2>/dev/null | tail -n 300' +docker logs -f qaup-app | grep "路由" + + +``` +docker exec -it qaup-app sh -lc ' +raw="QW9797-D-20260206204000"; +norm="${raw%%-*}"; +echo "raw=$raw norm=$norm"; + +grep -nE "航班通知触发路由查询|路由Redis参数未齐全|路由接口返回为空|获取(进港|出港)路由数据请求失败|成功获取(进港|出港)路由数据|路由查询与下发成功|发布航空器路由更新事件(失败)?|航空器路由更新事件已通过WebSocket推送|开始保存航空器路由到数据库|成功保存航空器路由到数据库|保存航空器路由到数据库失败|航班路由已获取过" \ + /logs/sys-info.log /logs/sys-error.log 2>/dev/null \ +| grep -E "(${raw}|${norm})" \ +| tail -n 300 +' + + + +docker exec -it qaup-app sh -lc ' +# 1) 找到启动命令里用的 jar(常见路径仅供示例,实际以 ps 输出为准) +ps -ef | grep java | grep -v grep + +# 2) 把上面 java 命令里的 jar 路径复制出来替换 JAR=... +JAR=/app.jar + +# 3) 在 jar 里搜索“缺少跑道”这句旧文案(如果还能搜到,基本就是旧包) +grep -a "缺少跑道" "$JAR" | head -n 5 || true + +# 4) 在 jar 里搜索我改后的新文案(例如 缺少outRunway / 缺少inRunway) +grep -a "缺少outRunway" "$JAR" | head -n 5 || true +grep -a "缺少inRunway" "$JAR" | head -n 5 || true +' +``` + +``` +docker exec -it qaup-app sh -lc 'sha256sum /app.jar' +docker exec -it qaup-app rm /app.jar +docker cp qaup-app.jar qaup-app:/app.jar +docker exec -it qaup-app ls / +docker restart qaup-app +curl -s http://10.64.58.228:8086/api/adxp/status +curl -s http://10.64.58.228:8086/api/adxp/health +curl -s -X POST http://10.64.58.228:8086/api/adxp/reconnect +docker logs -f qaup-app | grep "路由" +``` + +docker logs -f qaup-app | grep -E "航班通知触发路由查询|tryQueryAndPublishRouteFromRedis|路由Redis参数未齐全|新增待重试路由查询|路由重试失败|路由查询与下发成功|发布航空器路由更新事件|处理航空器路由更新事件|aircraftRouteUpdate" + + +docker exec -it qaup-app sh -lc 'javap -classpath /app.jar com.qaup.collision.datacollector.dao.DataCollectorDao | grep -n parseAircraftRouteBody || echo NO_parseAircraftRouteBody' +docker exec -it qaup-app sh -lc 'javap -classpath /app.jar -private com.qaup.collision.datacollector.service.AuthService | grep -n tokenCache || echo NO_tokenCache' + + + + +``` + + +root@root:/home/project_20250804/qaup# TOKEN=$(curl -sS -X POST "http://10.32.38.3:8099/login?username=dianxin&password=dianxin@123" \ + | sed -n 's/.*"data"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + +# OUT:只传 outRunway + startSeat +curl -sS -D - -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat?outRunway=34&startSeat=108" \ +| head -n 120 +HTTP/1.1 400 +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-Type: application/json;charset=UTF-8 +Transfer-Encoding: chunked +Date: Fri, 06 Feb 2026 13:11:15 GMT +Connection: close + +root@root:/home/project_20250804/qaup# TOKEN=$(curl -sS -X POST "http://10.32.38.3:8099/login?username=dianxin&password=dianxin@123" \url -sS -X POST "http://10.32.38.3:8099/login?username=dianxin&password=dianxin@123" \ + | sed -n 's/.*"data"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + +# IN:只传 inRunway + contactCross + seat +curl -sS -D - -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&contactCross=F1&seat=138" \ +| head -n 120 +HTTP/1.1 400 +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-Type: application/json;charset=UTF-8 +Transfer-Encoding: chunked +Date: Fri, 06 Feb 2026 13:12:02 GMT +Connection: close + +{"status":400,"msg":"Required String parameter 'outRunway' is not present","data":null}root@root:/home/project_20250804/qaup# + + + + +TOKEN=$(curl -sS -X POST "http://10.32.38.3:8099/login?username=dianxin&password=dianxin@123" \ + | sed -n 's/.*"data"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + +# A:inRunway 为空(会空体) +curl -sS -i -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat?inRunway=&outRunway=34&startSeat=165" + +# B:inRunway 有值(应返回 JSON) +curl -sS -i -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=34&startSeat=165" + + +root@root:~# TOKEN=$(curl -sS -X POST "http://10.32.38.3:8099/login?username=dianxin&password=dianxin@123" \ + | sed -n 's/.*"data"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +echo "TOKEN=$TOKEN" +TOKEN=Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NzA0NzAzNzksInVzZXJuYW1lIjoiZGlhbnhpbiJ9.28AlOc_FIOTlq-gQyCIZccvJ77SIj9SpZTwft8_QO3M +root@root:~# curl -sS -i -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat?inRunway=&outRunway=34&startSeat=165" +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: Fri, 06 Feb 2026 13:19:53 GMT + +root@root:~# curl -sS -i -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=34&startSeat=165" +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-Type: application/json;charset=UTF-8 +Transfer-Encoding: chunked +Date: Fri, 06 Feb 2026 13:20:07 GMT + +{"type":"OUT","status":"COMPLETE","codes":"165,F6,E,K1","geometry":null,"geoPath":{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050725127929156E7,4025016.291037335],[4.050719290471537E7,4025199.20219628],[4.050719270874638E7,4025199.816246393]]},"properties":{"code":"E"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050748116962414E7,4025469.703529501],[4.05074979933054E7,4025488.192558861],[4.05075125872243E7,4025504.2311054054]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.05074246385629E7,4025407.576552962],[4.050743271111251E7,4025416.448190767],[4.050745290306342E7,4025438.638909177]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050735203651394E7,4025327.78774847],[4.050735208167705E7,4025327.842539575],[4.050737617807948E7,4025354.319047242],[4.050739636994468E7,4025376.509671472]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050721884982213E7,4025249.470063929],[4.050724229779302E7,4025256.953294908],[4.050725675204703E7,4025262.271443918],[4.050726276604679E7,4025264.484172908],[4.050728250037725E7,4025273.770424166],[4.050728527683149E7,4025275.386363579]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050748763844896E7,4025497.614737691],[4.050748562772207E7,4025499.29000884],[4.050748347863713E7,4025500.783658592],[4.050748120755E7,4025502.084319383],[4.050747883174503E7,4025503.182092399],[4.050747636930351E7,4025504.068622925],[4.050747384575138E7,4025504.7787715]]},"properties":{"code":"165"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050755341493501E7,4025437.762839396],[4.050755267530195E7,4025438.435852589],[4.050755045640565E7,4025440.454889537],[4.050752018227805E7,4025468.002180588]]},"properties":{"code":"165"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050721884982213E7,4025249.470063929],[4.050721557490152E7,4025248.265123449],[4.050721241746041E7,4025246.779339884],[4.050720940152883E7,4025245.024020945],[4.050720655005983E7,4025243.012525668],[4.050720388475481E7,4025240.760162748],[4.050720142589837E7,4025238.284074025],[4.050719919220388E7,4025235.603104031],[4.050719720067109E7,4025232.737656565],[4.050719546645679E7,4025229.709539413],[4.05071940027594E7,4025226.541798375],[4.050719282071851E7,4025223.258541874],[4.05071919293302E7,4025219.884757472],[4.050719133537845E7,4025216.446121709],[4.05071910433836E7,4025212.968804676],[4.050719105556791E7,4025209.479270857],[4.050719137183864E7,4025206.004077711],[4.050719198978879E7,4025202.569673557],[4.050719270874638E7,4025199.816246393]]},"properties":{"code":""}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050805943409429E7,4024261.77885819],[4.050860721469813E7,4024436.598641041]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050753083878539E7,4024140.318065187],[4.050753219434276E7,4024136.633778098],[4.050753386584848E7,4024133.081655501],[4.05075358405814E7,4024129.688731194],[4.050753810351257E7,4024126.48082738],[4.050754063741975E7,4024123.482358144],[4.050754342301836E7,4024120.716143647],[4.050754643910833E7,4024118.203236453],[4.050754966273537E7,4024115.962761301],[4.050755306936574E7,4024114.011769562],[4.050755663307294E7,4024112.365109459],[4.0507560326735E7,4024111.035313071],[4.050756412224091E7,4024110.032500952],[4.050756799070459E7,4024109.364305105],[4.05075719026847E7,4024109.035810907],[4.050757582840868E7,4024109.049518396],[4.050757973799943E7,4024109.405323249],[4.050758360170261E7,4024110.100517578],[4.050758739011307E7,4024111.129810532]]},"properties":{"code":""}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050768694328369E7,4024142.90140585],[4.050777363551838E7,4024170.568536703]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050797274185959E7,4024234.111727338],[4.050801598298332E7,4024247.911784893]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050860721469813E7,4024436.598641041],[4.050869390693281E7,4024464.265771887]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050885919377588E7,4024517.015741149],[4.050894588601057E7,4024544.682872001]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050905115515271E7,4024578.27867375],[4.050905544213134E7,4024579.646828572]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050914213436604E7,4024607.313959424],[4.050914523051728E7,4024608.302071239]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050735203651394E7,4025327.78774847],[4.050735119763997E7,4025326.86583577]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050737806158195E7,4024619.030839834],[4.050727560203166E7,4024940.078054441]]},"properties":{"code":"E"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050753083878539E7,4024140.318065187],[4.050740238431432E7,4024542.817881098]]},"properties":{"code":"E"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050745290306342E7,4025438.638909177],[4.050746097762904E7,4025447.512762542],[4.050748116962414E7,4025469.703529501]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050739636994468E7,4025376.509671472],[4.050740444459597E7,4025385.383618986],[4.05074246385629E7,4025407.576552962]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050728527683149E7,4025275.386363579],[4.050728619714092E7,4025275.921997931],[4.050729923445571E7,4025283.567170995]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050758739011307E7,4024111.129810532],[4.050758786644404E7,4024111.281827736],[4.050768694328369E7,4024142.90140585]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050777363551838E7,4024170.568536703],[4.050797274185959E7,4024234.111727338]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050801598298332E7,4024247.911784893],[4.050805943409429E7,4024261.77885819]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.05075125872243E7,4025504.2311054054],[4.050751503282968E7,4025506.918797179],[4.050751814755022E7,4025510.417400318]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050923930588185E7,4024638.325468728],[4.050922363459327E7,4024633.324102767],[4.050919167278586E7,4024623.123748479],[4.050914832666852E7,4024609.290183055]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050751814755022E7,4025510.417400318],[4.05075165083987E7,4025508.370804927],[4.05075156594737E7,4025507.149358111],[4.050751501474968E7,4025506.221718934],[4.050751371408955E7,4025503.950630728],[4.050751261631713E7,4025501.574824661],[4.050751172978711E7,4025499.112382051],[4.050751106124654E7,4025496.582043574],[4.050751061578342E7,4025494.003066634],[4.050751039678796E7,4025491.395078802],[4.050751040592688E7,4025488.77792844],[4.050751064313062E7,4025486.171533643],[4.05075111065939E7,4025483.59573065],[4.050751179278951E7,4025481.070122875],[4.050751269649507E7,4025478.61393172],[4.050751381083285E7,4025476.245850282],[4.050751512732205E7,4025473.983901091],[4.05075166359434E7,4025471.845298945],[4.050751832521537E7,4025469.846319899],[4.050752018227805E7,4025468.002180588]]},"properties":{"code":""}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050869390693281E7,4024464.265771887],[4.050885919377588E7,4024517.015741149]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050894588601057E7,4024544.682872001],[4.050905115515271E7,4024578.27867375]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050905544213134E7,4024579.646828572],[4.050909878824869E7,4024593.480393996],[4.050914213436604E7,4024607.313959424]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050914523051728E7,4024608.302071239],[4.050914832666852E7,4024609.290183055]]},"properties":{"code":"K1"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050735119763997E7,4025326.86583577],[4.050733582978723E7,4025311.39028127],[4.05073201119621E7,4025298.107656442],[4.05073191716309E7,4025297.313013016],[4.050730241032708E7,4025285.489183442],[4.050730238791439E7,4025285.465824347],[4.05073022503873E7,4025285.322489934],[4.050730181388554E7,4025285.068439142],[4.050730132505871E7,4025284.783934349],[4.050729923445571E7,4025283.567170995]]},"properties":{"code":"F6"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.050727560203166E7,4024940.078054441],[4.050726344066161E7,4024978.184545887],[4.050725127929156E7,4025016.291037335]]},"properties":{"code":"E"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[4.docker exec -it qaup-redis redis-cli --raw HMGET flight:GS6512 inRunway outRunway seat startSeatde":"E"}}]}}root@root:~# docker exec -it qaup-redis redis-cli --raw HMGET flight:GS6512 inRunway outRunway seat startSeat + +"34" +"165" + +root@root:~# docker exec -it qaup-redis redis-cli --raw HGETALL flight:GS6512 +flightNumber +"GS6512" +type +"D" +time +"20260206215500" +seat +"165" +outRunway +"34" +``` + +``` +TOKEN=$(curl -sS -X POST "http://10.32.38.3:8099/login?username=dianxin&password=dianxin@123" \ + | sed -n 's/.*"data"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + +curl -sS -D - -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=34&contactCross=F1&seat=138" \ +| head -n 120 + +curl -sS -i -H "Authorization: $TOKEN" \ +"http://10.32.38.3:8099/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=35&startSeat=165" + +``` +