|
|
@@ -0,0 +1,598 @@
|
|
|
+package com.ymwk.supervisionserver.controller;
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+import com.ymwk.supervisionserver.component.EndpointValidator;
|
|
|
+import com.ymwk.supervisionserver.component.ResponseDTO;
|
|
|
+import com.ymwk.supervisionserver.entity.Api;
|
|
|
+import com.ymwk.supervisionserver.entity.Regulated;
|
|
|
+import com.ymwk.supervisionserver.model.*;
|
|
|
+
|
|
|
+import com.ymwk.supervisionserver.service.ApiService;
|
|
|
+import com.ymwk.supervisionserver.service.RegulatedService;
|
|
|
+import jakarta.annotation.Resource;
|
|
|
+
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestBody;
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+import org.springframework.web.client.RestClientException;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+import org.springframework.web.util.UriComponentsBuilder;
|
|
|
+import tools.jackson.core.JacksonException;
|
|
|
+import tools.jackson.databind.JsonNode;
|
|
|
+import tools.jackson.databind.ObjectMapper;
|
|
|
+import tools.jackson.databind.node.ObjectNode;
|
|
|
+
|
|
|
+import java.net.URI;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
+import java.util.concurrent.CompletionException;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.Executors;
|
|
|
+
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/v1/remote")
|
|
|
+public class RemoteController {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private RegulatedService regulatedService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ApiService apiService;
|
|
|
+ @Resource
|
|
|
+ private RestTemplate restTemplate;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ObjectMapper objectMapper;
|
|
|
+
|
|
|
+ //查询
|
|
|
+ @PostMapping("/dynamic")
|
|
|
+ public ResponseDTO<SystemInfo> dynamic(@RequestBody DynamicRequest request) {
|
|
|
+
|
|
|
+ Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
|
|
|
+ if (regulated == null) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的密码设施");
|
|
|
+ }
|
|
|
+ // 目前仅处理类型 1,其他类型可按需扩展
|
|
|
+ if (!Integer.valueOf(1).equals(regulated.getType())) {
|
|
|
+ System.out.println("暂不支持的密码设施类型:" + regulated.getType());
|
|
|
+ return ResponseDTO.error(100001,"暂不支持的密码设施类型:" + regulated.getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并发请求该设施下所有启用的 apiType=1 采集接口,停用的不请求
|
|
|
+ ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 1);
|
|
|
+ if (fetched.getCode() != ResponseDTO.OK_CODE) {
|
|
|
+ return ResponseDTO.error(fetched);
|
|
|
+ }
|
|
|
+ List<JsonNode> dataNodes = fetched.getData();
|
|
|
+
|
|
|
+ // 将各接口返回的数据分片按字段合并:字段以第一个返回的非空来源为准,重复字段不覆盖
|
|
|
+ ObjectNode merged = objectMapper.createObjectNode();
|
|
|
+ for (JsonNode dataNode : dataNodes) {
|
|
|
+ if (!dataNode.isObject()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, JsonNode> entry : dataNode.properties()) {
|
|
|
+ JsonNode value = entry.getValue();
|
|
|
+ if (value != null && !value.isNull() && !merged.has(entry.getKey())) {
|
|
|
+ merged.set(entry.getKey(), value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (merged.isEmpty()) {
|
|
|
+ return ResponseDTO.error(100001,"采集接口返回数据为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ SystemInfo systemInfo = objectMapper.treeToValue(merged, SystemInfo.class);
|
|
|
+ return ResponseDTO.ok(systemInfo, fetched.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并发请求该密码设施下指定类型的全部启用采集接口;单个接口失败仅跳过并记录原因
|
|
|
+ // 返回值约定:code=100000 时 data 为成功解析的 data 节点列表,message 可能附带部分失败的接口地址;
|
|
|
+ // 全部失败或未配置接口时返回 code=100001 的错误提示
|
|
|
+ private ResponseDTO<List<JsonNode>> fetchApiDataNodes(Regulated regulated, Integer apiType) {
|
|
|
+ List<Api> apis = apiService.listByIdenAndType(regulated.getIden(), apiType);
|
|
|
+ if (apis.isEmpty()) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的采集接口配置");
|
|
|
+ }
|
|
|
+
|
|
|
+ ExecutorService executor = Executors.newFixedThreadPool(apis.size());
|
|
|
+ List<JsonNode> dataNodes;
|
|
|
+ List<String> failedEndpoints = new ArrayList<>();
|
|
|
+ try {
|
|
|
+ List<CompletableFuture<JsonNode>> futures = new ArrayList<>(apis.size());
|
|
|
+ for (Api api : apis) {
|
|
|
+ futures.add(CompletableFuture.supplyAsync(() -> requestApiDataNode(api), executor));
|
|
|
+ }
|
|
|
+
|
|
|
+ dataNodes = new ArrayList<>(apis.size());
|
|
|
+ for (int i = 0; i < futures.size(); i++) {
|
|
|
+ Api api = apis.get(i);
|
|
|
+ try {
|
|
|
+ JsonNode dataNode = futures.get(i).join();
|
|
|
+ if (dataNode != null) {
|
|
|
+ dataNodes.add(dataNode);
|
|
|
+ }
|
|
|
+ } catch (CompletionException e) {
|
|
|
+ // 详细失败原因只记录到服务端日志,返回给前端的提示保持简洁
|
|
|
+ System.out.println("调用采集接口异常:" + api.getApiEndpoint() + ":" + e.getCause().getMessage());
|
|
|
+ failedEndpoints.add(api.getApiEndpoint());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ executor.shutdown();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dataNodes.isEmpty()) {
|
|
|
+ return ResponseDTO.error(100001,"调用采集接口失败:" + String.join("、", failedEndpoints)
|
|
|
+ + ",请检查该接口是否畅通");
|
|
|
+ }
|
|
|
+ if (failedEndpoints.isEmpty()) {
|
|
|
+ return ResponseDTO.ok(dataNodes);
|
|
|
+ }
|
|
|
+ return ResponseDTO.ok(dataNodes, "操作成功(部分采集接口调用失败:" + String.join("、", failedEndpoints)
|
|
|
+ + ",请检查该接口是否畅通,已返回其余采集结果)");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按接口配置的请求方法/参数调用采集接口,逐层剥离 data 包装,返回最内层的 data 节点
|
|
|
+ private JsonNode requestApiDataNode(Api api) {
|
|
|
+ ResponseEntity<String> responseEntity = sendApiRequest(api, parseApiParams(api));
|
|
|
+
|
|
|
+ if (!responseEntity.getStatusCode().is2xxSuccessful() || responseEntity.getBody() == null) {
|
|
|
+ throw new RestClientException("调用采集接口失败,HTTP 状态:" + responseEntity.getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 兼容直接返回、单层 data 包装以及多层 data 包装的响应。
|
|
|
+ JsonNode root = objectMapper.readTree(responseEntity.getBody());
|
|
|
+ JsonNode dataNode = root;
|
|
|
+ while (dataNode.isObject() && dataNode.has("data")) {
|
|
|
+ dataNode = dataNode.get("data");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dataNode == null || dataNode.isNull()) {
|
|
|
+ throw new RestClientException("采集接口返回数据为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ return dataNode;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析接口配置的请求参数,未配置时返回空对象
|
|
|
+ private ObjectNode parseApiParams(Api api) {
|
|
|
+ String raw = api.getParams();
|
|
|
+ if (raw == null || raw.isBlank()) {
|
|
|
+ return objectMapper.createObjectNode();
|
|
|
+ }
|
|
|
+ JsonNode parsed;
|
|
|
+ try {
|
|
|
+ parsed = objectMapper.readTree(raw);
|
|
|
+ } catch (JacksonException e) {
|
|
|
+ throw new RestClientException("请求参数配置不是合法的JSON对象");
|
|
|
+ }
|
|
|
+ if (!parsed.isObject()) {
|
|
|
+ throw new RestClientException("请求参数配置不是合法的JSON对象");
|
|
|
+ }
|
|
|
+ return (ObjectNode) parsed;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按接口配置的请求方法和参数提交方式发送请求
|
|
|
+ // GET 固定将参数拼成 query string;POST 支持 json body / form 表单 / query string 三种方式
|
|
|
+ private ResponseEntity<String> sendApiRequest(Api api, ObjectNode params) {
|
|
|
+ String endpoint = api.getApiEndpoint() == null ? "" : api.getApiEndpoint().trim();
|
|
|
+ // 提前拦截非法地址,避免底层 HTTP 客户端抛出无法捕获的异常污染前端
|
|
|
+ if (!EndpointValidator.isValid(endpoint)) {
|
|
|
+ throw new RestClientException("接口地址不是合法的 http/https 地址:" + endpoint);
|
|
|
+ }
|
|
|
+
|
|
|
+ String method = api.getMethod() == null || api.getMethod().isBlank()
|
|
|
+ ? Api.METHOD_GET
|
|
|
+ : api.getMethod().trim().toUpperCase();
|
|
|
+
|
|
|
+ if (Api.METHOD_GET.equals(method)) {
|
|
|
+ URI uri = buildUriWithParams(endpoint, params);
|
|
|
+ System.out.println("调用接口请求 >> GET " + uri);
|
|
|
+ return restTemplate.getForEntity(uri, String.class);
|
|
|
+ }
|
|
|
+ if (!Api.METHOD_POST.equals(method)) {
|
|
|
+ throw new RestClientException("不支持的请求方法:" + api.getMethod());
|
|
|
+ }
|
|
|
+
|
|
|
+ String paramType = api.getParamType() == null || api.getParamType().isBlank()
|
|
|
+ ? Api.PARAM_TYPE_JSON
|
|
|
+ : api.getParamType().trim().toLowerCase();
|
|
|
+
|
|
|
+ if (Api.PARAM_TYPE_FORM.equals(paramType)) {
|
|
|
+ // 按 application/x-www-form-urlencoded 规范编码参数
|
|
|
+ StringBuilder form = new StringBuilder();
|
|
|
+ params.properties().forEach(entry -> {
|
|
|
+ if (form.length() > 0) {
|
|
|
+ form.append('&');
|
|
|
+ }
|
|
|
+ form.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8))
|
|
|
+ .append('=')
|
|
|
+ .append(URLEncoder.encode(paramToString(entry.getValue()), StandardCharsets.UTF_8));
|
|
|
+ });
|
|
|
+ System.out.println("调用接口请求 >> POST " + endpoint
|
|
|
+ + " ct=application/x-www-form-urlencoded form=" + form);
|
|
|
+ return restTemplate.postForEntity(endpoint,
|
|
|
+ byteEntity(form.toString(), MediaType.APPLICATION_FORM_URLENCODED), String.class);
|
|
|
+ }
|
|
|
+ if (Api.PARAM_TYPE_QUERY.equals(paramType)) {
|
|
|
+ URI uri = buildUriWithParams(endpoint, params);
|
|
|
+ System.out.println("调用接口请求 >> POST " + uri + " 无body");
|
|
|
+ return restTemplate.postForEntity(uri, null, String.class);
|
|
|
+ }
|
|
|
+ if (!Api.PARAM_TYPE_JSON.equals(paramType)) {
|
|
|
+ throw new RestClientException("不支持的参数提交方式:" + api.getParamType());
|
|
|
+ }
|
|
|
+ String json = params.toString();
|
|
|
+ System.out.println("调用接口请求 >> POST " + endpoint + " ct=application/json json=" + json);
|
|
|
+ return restTemplate.postForEntity(endpoint,
|
|
|
+ byteEntity(json, MediaType.APPLICATION_JSON), String.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 以 byte[] 构造请求体并显式声明 Content-Length:
|
|
|
+ // 直接以 ObjectNode/对象作为 body 时 Jackson 转换器不设置 Content-Length,
|
|
|
+ // RestTemplate 会以 chunked 方式发送,部分服务端(如被监管平台)在 chunked 请求下解析不到参数;
|
|
|
+ // 参数值含中文时也会按 UTF-8 正确发送
|
|
|
+ private HttpEntity<byte[]> byteEntity(String body, MediaType contentType) {
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.setContentType(contentType);
|
|
|
+ headers.setContentLength(body.getBytes(StandardCharsets.UTF_8).length);
|
|
|
+ return new HttpEntity<>(body.getBytes(StandardCharsets.UTF_8), headers);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将请求参数拼接到 endpoint 的 query string 上
|
|
|
+ private URI buildUriWithParams(String endpoint, ObjectNode params) {
|
|
|
+ UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(endpoint);
|
|
|
+ params.properties().forEach(entry -> builder.queryParam(entry.getKey(), paramToString(entry.getValue())));
|
|
|
+ return builder.build().encode().toUri();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标量直接取文本,对象/数组序列化为 JSON 文本
|
|
|
+ private String paramToString(JsonNode value) {
|
|
|
+ return value.isValueNode() ? value.asText() : value.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @PostMapping("/total")
|
|
|
+ public ResponseDTO<CryptoStatInfo> total(@RequestBody DynamicRequest request) {
|
|
|
+
|
|
|
+ Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
|
|
|
+ if (regulated == null) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的密码设施");
|
|
|
+ }
|
|
|
+ // 目前仅处理类型 1,其他类型可按需扩展
|
|
|
+ if (!Integer.valueOf(1).equals(regulated.getType())) {
|
|
|
+ System.out.println("暂不支持的密码设施类型:" + regulated.getType());
|
|
|
+ return ResponseDTO.error(100001,"暂不支持的密码设施类型:" + regulated.getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并发请求该设施下所有启用的 apiType=2 采集接口,停用的不请求
|
|
|
+ ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 2);
|
|
|
+ if (fetched.getCode() != ResponseDTO.OK_CODE) {
|
|
|
+ return ResponseDTO.error(fetched);
|
|
|
+ }
|
|
|
+ List<JsonNode> dataNodes = fetched.getData();
|
|
|
+
|
|
|
+ // 将各接口返回的数据分片按字段合并:字段以第一个返回的非空来源为准,重复字段不覆盖
|
|
|
+ // 例如一个源返回 signCount,另一个源返回 verifyCount,合并后补全为完整对象
|
|
|
+ ObjectNode merged = objectMapper.createObjectNode();
|
|
|
+ for (JsonNode dataNode : dataNodes) {
|
|
|
+ if (!dataNode.isObject()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, JsonNode> entry : dataNode.properties()) {
|
|
|
+ JsonNode value = entry.getValue();
|
|
|
+ if (value != null && !value.isNull() && !merged.has(entry.getKey())) {
|
|
|
+ merged.set(entry.getKey(), value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (merged.isEmpty()) {
|
|
|
+ return ResponseDTO.error(100001,"采集接口返回数据为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ CryptoStatInfo cryptoStatInfo = objectMapper.treeToValue(merged, CryptoStatInfo.class);
|
|
|
+ return ResponseDTO.ok(cryptoStatInfo, fetched.getMessage());
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/recentWeek")
|
|
|
+ public ResponseDTO<List<CryptoStatItem>> recentWeek(@RequestBody DynamicRequest request) {
|
|
|
+
|
|
|
+ Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
|
|
|
+ if (regulated == null) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的密码设施");
|
|
|
+ }
|
|
|
+ // 目前仅处理类型 1,其他类型可按需扩展
|
|
|
+ if (!Integer.valueOf(1).equals(regulated.getType())) {
|
|
|
+ System.out.println("暂不支持的密码设施类型:" + regulated.getType());
|
|
|
+ return ResponseDTO.error(100001,"暂不支持的密码设施类型:" + regulated.getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并发请求该设施下所有启用的 apiType=4 采集接口,停用的不请求
|
|
|
+ ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 4);
|
|
|
+ if (fetched.getCode() != ResponseDTO.OK_CODE) {
|
|
|
+ return ResponseDTO.error(fetched);
|
|
|
+ }
|
|
|
+ List<JsonNode> dataNodes = fetched.getData();
|
|
|
+
|
|
|
+ // 按日期(statAt)归组,同一天的数据分片按字段合并:字段以第一个返回的非空来源为准
|
|
|
+ // 例如一个源只返回同一天的 signCount,另一个源只返回同一天的 verifyCount,合并后补全为整行
|
|
|
+ Map<String, ObjectNode> mergedByStatAt = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ for (JsonNode dataNode : dataNodes) {
|
|
|
+ List<JsonNode> items = new ArrayList<>();
|
|
|
+ if (dataNode.isArray()) {
|
|
|
+ dataNode.forEach(items::add);
|
|
|
+ } else {
|
|
|
+ items.add(dataNode);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (JsonNode item : items) {
|
|
|
+ if (!item.isObject()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 没有 statAt 的数据无法关联到具体日期,直接跳过
|
|
|
+ String statAt = item.path("statAt").asText(null);
|
|
|
+ if (statAt == null || statAt.isBlank()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ ObjectNode target = mergedByStatAt
|
|
|
+ .computeIfAbsent(statAt, k -> objectMapper.createObjectNode().put("statAt", k));
|
|
|
+ for (Map.Entry<String, JsonNode> entry : item.properties()) {
|
|
|
+ JsonNode value = entry.getValue();
|
|
|
+ if (value != null && !value.isNull() && !target.has(entry.getKey())) {
|
|
|
+ target.set(entry.getKey(), value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mergedByStatAt.isEmpty()) {
|
|
|
+ return ResponseDTO.error(100001,"采集接口返回数据为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<CryptoStatItem> cryptoStatItems = new ArrayList<>(mergedByStatAt.size());
|
|
|
+ for (ObjectNode row : mergedByStatAt.values()) {
|
|
|
+ cryptoStatItems.add(objectMapper.treeToValue(row, CryptoStatItem.class));
|
|
|
+ }
|
|
|
+ return ResponseDTO.ok(cryptoStatItems, fetched.getMessage());
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/static")
|
|
|
+ public ResponseDTO<Map<String, ComputerInfo>> staticInfo(@RequestBody DynamicRequest request) {
|
|
|
+
|
|
|
+ Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
|
|
|
+ if (regulated == null) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的密码设施");
|
|
|
+ }
|
|
|
+ // 目前仅处理类型 1,其他类型可按需扩展
|
|
|
+ if (!Integer.valueOf(1).equals(regulated.getType())) {
|
|
|
+ System.out.println("暂不支持的密码设施类型:" + regulated.getType());
|
|
|
+ return ResponseDTO.error(100001,"暂不支持的密码设施类型:" + regulated.getType());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并发请求该设施下所有启用的 apiType=3 采集接口,停用的不请求
|
|
|
+ ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 3);
|
|
|
+ if (fetched.getCode() != ResponseDTO.OK_CODE) {
|
|
|
+ return ResponseDTO.error(fetched);
|
|
|
+ }
|
|
|
+ List<JsonNode> dataNodes = fetched.getData();
|
|
|
+
|
|
|
+ // 将各接口返回的数据分片按字段合并:字段以第一个返回的非空来源为准,重复字段不覆盖
|
|
|
+ // 若某个源返回数组,取第一个元素参与合并
|
|
|
+ ObjectNode merged = objectMapper.createObjectNode();
|
|
|
+ for (JsonNode dataNode : dataNodes) {
|
|
|
+ if (dataNode.isArray()) {
|
|
|
+ if (dataNode.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ dataNode = dataNode.get(0);
|
|
|
+ }
|
|
|
+ if (!dataNode.isObject()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, JsonNode> entry : dataNode.properties()) {
|
|
|
+ JsonNode value = entry.getValue();
|
|
|
+ if (value != null && !value.isNull() && !merged.has(entry.getKey())) {
|
|
|
+ merged.set(entry.getKey(), value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (merged.isEmpty()) {
|
|
|
+ return ResponseDTO.error(100001,"采集接口返回数据为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ ComputerInfo computerInfo = objectMapper.treeToValue(merged, ComputerInfo.class);
|
|
|
+ return ResponseDTO.ok(Map.of("data", computerInfo), fetched.getMessage());
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ //应用接口列表采集
|
|
|
+ @PostMapping("/appApiList")
|
|
|
+ public ResponseDTO<Map<String, List<AppApiItem>>> appApiList(@RequestBody DynamicRequest request) {
|
|
|
+
|
|
|
+ Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
|
|
|
+ if (regulated == null) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的密码设施");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并发请求该设施下所有启用的 apiType=5 采集接口,停用的不请求
|
|
|
+ ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 5);
|
|
|
+ if (fetched.getCode() != ResponseDTO.OK_CODE) {
|
|
|
+ return ResponseDTO.error(fetched);
|
|
|
+ }
|
|
|
+ List<JsonNode> dataNodes = fetched.getData();
|
|
|
+
|
|
|
+ // 展开各来源的条目:兼容 {"list":[...]} 包装、直接数组以及单个对象
|
|
|
+ List<JsonNode> rawItems = new ArrayList<>();
|
|
|
+ for (JsonNode dataNode : dataNodes) {
|
|
|
+ JsonNode rowNode = dataNode;
|
|
|
+ if (rowNode.isObject() && rowNode.has("list") && rowNode.get("list").isArray()) {
|
|
|
+ rowNode = rowNode.get("list");
|
|
|
+ }
|
|
|
+ if (rowNode.isArray()) {
|
|
|
+ rowNode.forEach(rawItems::add);
|
|
|
+ } else {
|
|
|
+ rawItems.add(rowNode);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按 id 归组去重合并:字段以第一个返回的非空来源为准;无 id 的条目按整条内容去重
|
|
|
+ Map<Object, ObjectNode> keyed = new LinkedHashMap<>();
|
|
|
+ for (JsonNode item : rawItems) {
|
|
|
+ if (!item.isObject()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ JsonNode idNode = item.path("id");
|
|
|
+ Object key = !idNode.isMissingNode() && !idNode.isNull()
|
|
|
+ ? "id:" + idNode.asText()
|
|
|
+ : item.toString();
|
|
|
+ ObjectNode target = keyed.computeIfAbsent(key, k -> objectMapper.createObjectNode());
|
|
|
+ for (Map.Entry<String, JsonNode> entry : item.properties()) {
|
|
|
+ JsonNode value = entry.getValue();
|
|
|
+ if (value != null && !value.isNull() && !target.has(entry.getKey())) {
|
|
|
+ target.set(entry.getKey(), value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (keyed.isEmpty()) {
|
|
|
+ return ResponseDTO.error(100001,"采集接口返回数据为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<AppApiItem> items = new ArrayList<>(keyed.size());
|
|
|
+ for (ObjectNode row : keyed.values()) {
|
|
|
+ items.add(objectMapper.treeToValue(row, AppApiItem.class));
|
|
|
+ }
|
|
|
+ return ResponseDTO.ok(Map.of("list", items), fetched.getMessage());
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ //应用接口启停
|
|
|
+ @PostMapping("/appApiToggle")
|
|
|
+ public ResponseDTO<Void> appApiToggle(@RequestBody AppApiToggleRequest request) {
|
|
|
+
|
|
|
+ if (request.getId() == null) {
|
|
|
+ return ResponseDTO.error(100001,"id 不能为空");
|
|
|
+ }
|
|
|
+ Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
|
|
|
+ if (regulated == null) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的密码设施");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 启停接口应唯一;存在多条启用配置时直接报错,避免调用到错误的接口
|
|
|
+ List<Api> apis = apiService.listByIdenAndType(request.getRegulatedIden(), 6);
|
|
|
+ if (apis.isEmpty()) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的采集接口配置");
|
|
|
+ }
|
|
|
+ if (apis.size() > 1) {
|
|
|
+ return ResponseDTO.error(100001,"存在多个启用的启停接口配置,请仅保留一条");
|
|
|
+ }
|
|
|
+ Api api = apis.get(0);
|
|
|
+
|
|
|
+ try {
|
|
|
+ ObjectNode params = parseApiParams(api);
|
|
|
+ // id 与配置的请求参数合并,同名参数以本次指令为准
|
|
|
+ params.put("id", request.getId());
|
|
|
+
|
|
|
+ ResponseEntity<String> responseEntity = sendApiRequest(api, params);
|
|
|
+
|
|
|
+ if (!responseEntity.getStatusCode().is2xxSuccessful() || responseEntity.getBody() == null) {
|
|
|
+ return ResponseDTO.error(100001,"下发启停指令失败:" + api.getApiEndpoint()
|
|
|
+ + ",请检查该接口是否畅通");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验采集端返回的包装响应:code=100000 成功,100001 失败
|
|
|
+ JsonNode root = objectMapper.readTree(responseEntity.getBody());
|
|
|
+ JsonNode codeNode = root.path("code");
|
|
|
+ if (!root.isObject() || !codeNode.isNumber()) {
|
|
|
+ return ResponseDTO.error(100001,"启停接口返回数据异常:" + api.getApiEndpoint());
|
|
|
+ }
|
|
|
+ int code = codeNode.asInt();
|
|
|
+ if (code == ResponseDTO.OK_CODE) {
|
|
|
+ return ResponseDTO.okMsg("操作成功");
|
|
|
+ }
|
|
|
+ // 非成功码统一透出对端的 message,便于定位问题
|
|
|
+ String remoteMsg = root.path("message").asText(null);
|
|
|
+ String detail = remoteMsg != null && !remoteMsg.isBlank() ? remoteMsg : "返回码:" + code;
|
|
|
+ return ResponseDTO.error(100001,"下发启停指令失败:" + detail);
|
|
|
+ } catch (RestClientException e) {
|
|
|
+ System.out.println("下发启停指令异常:" + api.getApiEndpoint() + ":" + e.getMessage());
|
|
|
+ return ResponseDTO.error(100001,"下发启停指令异常:" + api.getApiEndpoint()
|
|
|
+ + ",请检查该接口是否畅通");
|
|
|
+ } catch (JacksonException e) {
|
|
|
+ System.out.println("下发启停指令响应解析异常:" + e.getMessage());
|
|
|
+ return ResponseDTO.error(100001,"启停接口返回数据解析异常:" + api.getApiEndpoint());
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ //连通性测试:按 t_api 配置的 method/paramType/params 实际调用一次该接口,返回连通结果
|
|
|
+ @PostMapping("/appApiTest")
|
|
|
+ public ResponseDTO<Map<String, Object>> appApiTest(@RequestBody AppApiTestRequest request) {
|
|
|
+
|
|
|
+ if (request.getId() == null) {
|
|
|
+ return ResponseDTO.error(100001,"id 不能为空");
|
|
|
+ }
|
|
|
+ Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
|
|
|
+ if (regulated == null) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的密码设施");
|
|
|
+ }
|
|
|
+ Api api = apiService.getApiById(request.getId());
|
|
|
+ // 该接口配置必须属于传入的密码设施,避免跨设施测试
|
|
|
+ if (api == null || !api.getRegulatedIden().equals(regulated.getIden())) {
|
|
|
+ return ResponseDTO.error(100001,"未找到对应的接口配置");
|
|
|
+ }
|
|
|
+ if (!EndpointValidator.isValid(api.getApiEndpoint())) {
|
|
|
+ return ResponseDTO.error(100001,"接口地址不是合法的 http/https 地址");
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("method", api.getMethod());
|
|
|
+ result.put("paramType", api.getParamType());
|
|
|
+ result.put("endpoint", api.getApiEndpoint());
|
|
|
+
|
|
|
+ long start = System.currentTimeMillis();
|
|
|
+ try {
|
|
|
+ ResponseEntity<String> responseEntity = sendApiRequest(api, parseApiParams(api));
|
|
|
+ result.put("costMs", System.currentTimeMillis() - start);
|
|
|
+ int httpStatus = responseEntity.getStatusCode().value();
|
|
|
+ result.put("httpStatus", httpStatus);
|
|
|
+ boolean success = responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.getBody() != null;
|
|
|
+ result.put("success", success);
|
|
|
+ result.put("message", success ? null : "HTTP 状态异常:" + httpStatus);
|
|
|
+ return ResponseDTO.ok(result);
|
|
|
+ } catch (RestClientException e) {
|
|
|
+ System.out.println("接口连通性测试异常:" + api.getApiEndpoint() + ":" + e.getMessage());
|
|
|
+ result.put("costMs", System.currentTimeMillis() - start);
|
|
|
+ result.put("success", false);
|
|
|
+ result.put("message", e.getMessage());
|
|
|
+ return ResponseDTO.ok(result);
|
|
|
+ } catch (JacksonException e) {
|
|
|
+ System.out.println("接口连通性测试请求构造异常:" + e.getMessage());
|
|
|
+ result.put("costMs", System.currentTimeMillis() - start);
|
|
|
+ result.put("success", false);
|
|
|
+ result.put("message", "请求参数配置异常:" + e.getMessage());
|
|
|
+ return ResponseDTO.ok(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|