重构数据采集和路由查询逻辑,增强超时处理和并发控制。新增Redis缓存管理,优化航班路由参数获取,确保系统稳定性和性能。更新相关文档以反映最新功能和接口调用示例。路由第一次下发成功
This commit is contained in:
parent
dee9bc4420
commit
aa13f343ea
@ -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
|
||||
|
||||
|
||||
@ -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<Map<String, Object>> 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<String, Object> result = new LinkedHashMap<>();
|
||||
List<String> 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<String, Object> 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<Map<String, Object>> badRequest(String message, Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,19 +4,36 @@ 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();
|
||||
// }
|
||||
/**
|
||||
* 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) {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
// 使用最小改动的 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);
|
||||
|
||||
@ -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,22 +252,39 @@ public class DataCollectorDao {
|
||||
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<Response<AircraftRouteDTO>> response = restTemplate.exchange(
|
||||
// 注意:路由服务返回格式在不同环境可能有两种:
|
||||
// 1) 包装格式:{status,msg,data:{...}}
|
||||
// 2) 裸对象:{type,status,codes,geoPath,...}
|
||||
// 这里先取 String,再按 JSON 结构判断解析,避免 UnrecognizedPropertyException(type)。
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
new ParameterizedTypeReference<Response<AircraftRouteDTO>>() {}
|
||||
String.class
|
||||
);
|
||||
|
||||
Response<AircraftRouteDTO> 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={}",
|
||||
inRunway, outRunway, contactCross, seat, e);
|
||||
@ -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,22 +325,36 @@ public class DataCollectorDao {
|
||||
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<Response<AircraftRouteDTO>> response = restTemplate.exchange(
|
||||
// 同 getArrivalRoute:兼容包装/裸对象两种返回格式
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
new ParameterizedTypeReference<Response<AircraftRouteDTO>>() {}
|
||||
String.class
|
||||
);
|
||||
|
||||
Response<AircraftRouteDTO> 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={}",
|
||||
inRunway, outRunway, startSeat, e);
|
||||
@ -310,6 +362,58 @@ public class DataCollectorDao {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析路由接口返回体,兼容两种格式:
|
||||
* 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 为空:返回 "<empty>"
|
||||
* - body 非空:返回 "len=<n>, head=<前200字符>"
|
||||
*/
|
||||
private static String summarizeBody(String body) {
|
||||
if (body == null) {
|
||||
return "<empty>";
|
||||
}
|
||||
String b = body.trim();
|
||||
if (b.isEmpty()) {
|
||||
return "<empty>";
|
||||
}
|
||||
int len = b.length();
|
||||
String head = b.substring(0, Math.min(200, len));
|
||||
return "len=" + len + ", head=" + head;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取航空器状态
|
||||
*
|
||||
@ -363,44 +467,187 @@ 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<String> requestEntity = new HttpEntity<>(headers);
|
||||
// ResponseEntity<Response<AircraftRouteParamsDTO>> response = restTemplate.exchange(
|
||||
// url,
|
||||
// HttpMethod.GET,
|
||||
// requestEntity,
|
||||
// new ParameterizedTypeReference<Response<AircraftRouteParamsDTO>>() {}
|
||||
// );
|
||||
// Response<AircraftRouteParamsDTO> 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;
|
||||
// }
|
||||
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<String> requestEntity = new HttpEntity<>(headers);
|
||||
ResponseEntity<Response<AircraftRouteParamsDTO>> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
new ParameterizedTypeReference<Response<AircraftRouteParamsDTO>>() {}
|
||||
);
|
||||
|
||||
Response<AircraftRouteParamsDTO> 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<String, Object> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取航班进出港通知
|
||||
|
||||
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 检查是否为有效的路由参数
|
||||
*/
|
||||
|
||||
@ -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<String, TokenInfo> 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<String> 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<Void> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<Response<String>> response = restTemplate.exchange(
|
||||
refreshUrl,
|
||||
@ -95,10 +121,11 @@ public class AuthService {
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
Response<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -415,6 +429,12 @@ public class DataCollectorService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 丢弃旧轮次:上一轮未完成时跳过,避免任务堆积
|
||||
if (!aircraftCollectInProgress.compareAndSet(false, true)) {
|
||||
log.warn("航空器采集上一轮尚未完成,跳过本轮以避免堆积(保持实时性)");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<Aircraft> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,6 +505,12 @@ public class DataCollectorService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 丢弃旧轮次:上一轮未完成时跳过,避免 fixedRate + @Async 并发堆积
|
||||
if (!vehicleCollectInProgress.compareAndSet(false, true)) {
|
||||
log.warn("机场车辆采集上一轮尚未完成,跳过本轮以避免堆积(保持实时性)");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<AirportVehicle> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,11 +558,13 @@ 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")){
|
||||
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) {
|
||||
log.error("存储航班数据到Redis失败: {}", e.getMessage());
|
||||
@ -580,14 +582,16 @@ 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")){
|
||||
if (arr.length >= 2) {
|
||||
if ("A".equals(arr[1])){
|
||||
dto.setType("IN");
|
||||
}else {
|
||||
dto.setType("OUT");
|
||||
}
|
||||
}
|
||||
// 存储到Redis
|
||||
if (arr.length >= 3) {
|
||||
try {
|
||||
@ -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]);
|
||||
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());
|
||||
|
||||
@ -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<String, Long> routeRetrievalCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 待重试的路由查询任务:Key=flightNo:routeType(IN/OUT)
|
||||
* - 当 Redis 参数尚未齐全或路由接口返回为空时,进入重试队列
|
||||
* - 在周期性处理线程中按 backoff 进行重试,避免在 WS 回调里阻塞/刷接口
|
||||
*/
|
||||
private final Map<String, PendingRouteQuery> 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 去重)
|
||||
if (isRouteAlreadyRetrieved(flightNo, routeType)) {
|
||||
log.info("航班路由已获取过,跳过重复查询: 航班号={}, 类型={}", flightNo, routeType);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("航班通知触发路由查询(仅Redis拼参): 航班号={}, 类型={}, eventTime={}",
|
||||
flightNo, routeType, flightNotification.getEventTime());
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}, 时间={}", 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:<flightNo>)聚合
|
||||
*
|
||||
* @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,8 +1510,10 @@ public class DataProcessingService {
|
||||
/**
|
||||
* 发布航空器路由更新事件
|
||||
*/
|
||||
private void publishAircraftRouteUpdateEvent(String flightNo, AircraftRoute route,
|
||||
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
|
||||
private void publishAircraftRouteUpdateEvent(String flightNo,
|
||||
AircraftRoute route,
|
||||
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams,
|
||||
String eventSource) {
|
||||
log.error("发布航空器路由更新事件: flightNo={}", flightNo);
|
||||
try {
|
||||
// 创建路由更新事件
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -50,6 +50,26 @@ public class AircraftRouteUpdateEvent {
|
||||
*/
|
||||
private String routeGeometry;
|
||||
|
||||
/**
|
||||
* 进港跑道编号(用于前端联调查看本次路由查询参数)
|
||||
*/
|
||||
private String inRunway;
|
||||
|
||||
/**
|
||||
* 出港跑道编号(用于前端联调查看本次路由查询参数)
|
||||
*/
|
||||
private String outRunway;
|
||||
|
||||
/**
|
||||
* inRunway 是否为系统补齐值(true=补齐;false/空=来自报文/Redis原始值)
|
||||
*/
|
||||
private Boolean inRunwayPatched;
|
||||
|
||||
/**
|
||||
* outRunway 是否为系统补齐值(true=补齐;false/空=来自报文/Redis原始值)
|
||||
*/
|
||||
private Boolean outRunwayPatched;
|
||||
|
||||
/**
|
||||
* 事件时间戳
|
||||
*/
|
||||
|
||||
@ -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()
|
||||
|
||||
191
verify-jar.py
Normal file
191
verify-jar.py
Normal file
@ -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 "<none>"))
|
||||
|
||||
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())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user