| @ -0,0 +1,16 @@ | |||||
| package com.inspect.simulator.naritech.config; | |||||
| import lombok.Data; | |||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
| import org.springframework.stereotype.Component; | |||||
| @Data | |||||
| @Component | |||||
| @ConfigurationProperties(prefix = "naritech.auth") | |||||
| public class AuthProperties { | |||||
| private String grantType; | |||||
| private String scope; | |||||
| private String clientId; | |||||
| private String clientSecret; | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| package com.inspect.simulator.naritech.config; | |||||
| import lombok.Data; | |||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
| import org.springframework.stereotype.Component; | |||||
| @Data | |||||
| @Component | |||||
| @ConfigurationProperties(prefix = "naritech.api") | |||||
| public class NaritechProperties { | |||||
| private String baseUrl; | |||||
| private String tokenPath; | |||||
| private String patrolReceivePath; | |||||
| public String getFullTokenUrl() { | |||||
| return baseUrl + tokenPath; | |||||
| } | |||||
| public String getFullPatrolReceiveUrl() { | |||||
| return baseUrl + patrolReceivePath; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,50 @@ | |||||
| package com.inspect.simulator.naritech.config; | |||||
| import org.apache.http.config.Registry; | |||||
| import org.apache.http.config.RegistryBuilder; | |||||
| import org.apache.http.conn.socket.ConnectionSocketFactory; | |||||
| import org.apache.http.conn.socket.PlainConnectionSocketFactory; | |||||
| import org.apache.http.conn.ssl.NoopHostnameVerifier; | |||||
| import org.apache.http.conn.ssl.SSLConnectionSocketFactory; | |||||
| import org.apache.http.impl.client.CloseableHttpClient; | |||||
| import org.apache.http.impl.client.HttpClients; | |||||
| import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; | |||||
| import org.apache.http.ssl.SSLContexts; | |||||
| import org.springframework.context.annotation.Bean; | |||||
| import org.springframework.context.annotation.Configuration; | |||||
| import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; | |||||
| import org.springframework.web.client.RestTemplate; | |||||
| import javax.net.ssl.SSLContext; | |||||
| import java.security.KeyManagementException; | |||||
| import java.security.KeyStoreException; | |||||
| import java.security.NoSuchAlgorithmException; | |||||
| @Configuration | |||||
| public class RestTemplateConfig { | |||||
| @Bean | |||||
| public RestTemplate restTemplate() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { | |||||
| SSLContext sslContext = SSLContexts.custom() | |||||
| .loadTrustMaterial(null, (chain, authType) -> true) | |||||
| .build(); | |||||
| SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( | |||||
| sslContext, NoopHostnameVerifier.INSTANCE); | |||||
| Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() | |||||
| .register("http", PlainConnectionSocketFactory.getSocketFactory()) | |||||
| .register("https", sslSocketFactory) | |||||
| .build(); | |||||
| PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry); | |||||
| connectionManager.setMaxTotal(50); | |||||
| connectionManager.setDefaultMaxPerRoute(20); | |||||
| CloseableHttpClient httpClient = HttpClients.custom() | |||||
| .setConnectionManager(connectionManager) | |||||
| .build(); | |||||
| return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,91 @@ | |||||
| package com.inspect.simulator.naritech.controller; | |||||
| import com.inspect.simulator.naritech.dto.NaritechPatrolData; | |||||
| import com.inspect.simulator.naritech.dto.NaritechPatrolResponse; | |||||
| import com.inspect.simulator.naritech.service.NaritechPatrolDataService; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import javax.annotation.Resource; | |||||
| import java.time.LocalDateTime; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| import java.util.stream.Collectors; | |||||
| @Slf4j | |||||
| @RestController | |||||
| @RequestMapping("/naritech") | |||||
| public class NaritechController { | |||||
| private static final int BATCH_SIZE = 500; | |||||
| @Resource | |||||
| private NaritechPatrolDataService naritechPatrolDataService; | |||||
| @GetMapping("/uploadPatrolData") | |||||
| public void uploadPatrolData() { | |||||
| log.info("===== 巡视数据上送任务开始, 当前时间: {} =====", LocalDateTime.now()); | |||||
| try { | |||||
| List<NaritechPatrolData> dataList = naritechPatrolDataService.collectPatrolData(); | |||||
| if (dataList.isEmpty()) { | |||||
| log.info("无待上传数据,任务结束"); | |||||
| return; | |||||
| } | |||||
| int total = dataList.size(); | |||||
| int totalBatches = (total - 1) / BATCH_SIZE + 1; | |||||
| log.info("本次共需上送 {} 条数据, 共 {} 个批次, 每批 {} 条", total, totalBatches, BATCH_SIZE); | |||||
| List<Long> uploadedIds = new ArrayList<>(); | |||||
| for (int i = 0; i < total; i+=BATCH_SIZE) { | |||||
| int end = Math.min(i + BATCH_SIZE, total); | |||||
| List<NaritechPatrolData> subDataList = dataList.subList(i, end); | |||||
| int batchNum = (i / BATCH_SIZE) + 1; | |||||
| try { | |||||
| NaritechPatrolResponse response = naritechPatrolDataService.uploadPatrolData(subDataList); | |||||
| if (response != null && Boolean.TRUE.equals(response.getResult())) { | |||||
| List<Long> ids = dataList.stream().map(NaritechPatrolData::getLogId).collect(Collectors.toList()); | |||||
| naritechPatrolDataService.recordUploadSuccess(ids); | |||||
| uploadedIds.addAll(ids); | |||||
| log.info("第 {}/{} 批上送成功, {} 条", batchNum, totalBatches, subDataList.size()); | |||||
| } else { | |||||
| log.error("第 {}/{} 批上送失败, 远端响应: {}", | |||||
| batchNum, totalBatches, | |||||
| response != null ? response.getMessage() : "无"); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| log.error("第 {}/{} 批上送异常", batchNum, totalBatches, e); | |||||
| } | |||||
| } | |||||
| log.info("===== 任务结束, 成功: {}/{}, 失败: {} =====", uploadedIds.size(), total, total - uploadedIds.size()); | |||||
| } catch (Exception e) { | |||||
| log.error("===== 巡视数据上送任务异常 =====", e); | |||||
| } | |||||
| } | |||||
| @GetMapping("/test") | |||||
| public void test() { | |||||
| log.info("===== 巡视数据测试上送任务开始, 当前时间: {} =====", LocalDateTime.now()); | |||||
| try { | |||||
| List<NaritechPatrolData> dataList = new ArrayList<>(); | |||||
| NaritechPatrolData data = NaritechPatrolData.builder() | |||||
| .logId(1L) | |||||
| .lineId(12736374) | |||||
| .areaName("极Ⅱ") | |||||
| .deviceName("极Ⅱ高Y/Y-C相") | |||||
| .patroldeviceName("") | |||||
| .patrolpointName("极Ⅱ高Y/Y-C相电容器与管母接头测温") | |||||
| .value("36.7℃") | |||||
| .time("2026-05-26 01:01:03") | |||||
| .unit("℃") | |||||
| .build(); | |||||
| dataList.add(data); | |||||
| NaritechPatrolResponse response = naritechPatrolDataService.uploadPatrolData(dataList); | |||||
| log.info("===== 巡视数据测试上送任务完成, 结果: {} =====", | |||||
| response != null ? response.getMessage() : "无数据"); | |||||
| } catch (Exception e) { | |||||
| log.error("===== 巡视数据测试上送任务异常 =====", e); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,42 @@ | |||||
| package com.inspect.simulator.naritech.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonInclude; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Builder; | |||||
| import lombok.Data; | |||||
| import lombok.ToString; | |||||
| @Data | |||||
| @Builder | |||||
| @ToString | |||||
| @JsonInclude(JsonInclude.Include.NON_NULL) | |||||
| public class NaritechPatrolData { | |||||
| /** result_analysis 主键(用于记录,不上送) */ | |||||
| @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) | |||||
| private Long logId; | |||||
| /** 唯一标识ID */ | |||||
| private Integer lineId; | |||||
| /** 区域 */ | |||||
| private String areaName; | |||||
| /** 设备 */ | |||||
| private String deviceName; | |||||
| /** 相别 */ | |||||
| private String patroldeviceName; | |||||
| /** 点位 */ | |||||
| private String patrolpointName; | |||||
| /** 值 */ | |||||
| private String value; | |||||
| /** 生成时间(格式:yyyy-MM-dd HH:mm:ss) */ | |||||
| private String time; | |||||
| /** 单位(用于SQL映射,不上送) */ | |||||
| @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) | |||||
| private String unit; | |||||
| } | |||||
| @ -0,0 +1,15 @@ | |||||
| package com.inspect.simulator.naritech.dto; | |||||
| import lombok.Data; | |||||
| @Data | |||||
| public class NaritechPatrolResponse { | |||||
| private Boolean result; | |||||
| private Integer code; | |||||
| private String message; | |||||
| private String body; | |||||
| } | |||||
| @ -0,0 +1,32 @@ | |||||
| package com.inspect.simulator.naritech.dto; | |||||
| import com.fasterxml.jackson.annotation.JsonProperty; | |||||
| import lombok.Data; | |||||
| @Data | |||||
| public class NaritechTokenResponse { | |||||
| @JsonProperty("access_token") | |||||
| private String accessToken; | |||||
| private String ver; | |||||
| private String code; | |||||
| private String scope; | |||||
| private String iss; | |||||
| @JsonProperty("token_type") | |||||
| private String tokenType; | |||||
| private String message; | |||||
| @JsonProperty("expires_in") | |||||
| private Integer expiresIn; | |||||
| @JsonProperty("client_id") | |||||
| private String clientId; | |||||
| private String jti; | |||||
| } | |||||
| @ -0,0 +1,140 @@ | |||||
| package com.inspect.simulator.naritech.service; | |||||
| import com.inspect.simulator.mapper.ResultAnalysisMapper; | |||||
| import com.inspect.simulator.naritech.config.NaritechProperties; | |||||
| import com.inspect.simulator.naritech.dto.NaritechPatrolData; | |||||
| import com.inspect.simulator.naritech.dto.NaritechPatrolResponse; | |||||
| import com.inspect.simulator.utils.StringUtils; | |||||
| import org.apache.commons.lang3.math.NumberUtils; | |||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import org.springframework.http.HttpEntity; | |||||
| import org.springframework.http.HttpHeaders; | |||||
| import org.springframework.http.MediaType; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.web.client.RestTemplate; | |||||
| import java.time.LocalDateTime; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| @Service | |||||
| public class NaritechPatrolDataService { | |||||
| private static final Logger log = LoggerFactory.getLogger(NaritechPatrolDataService.class); | |||||
| private final RestTemplate restTemplate; | |||||
| private final NaritechProperties apiProperties; | |||||
| private final NaritechTokenService tokenService; | |||||
| private final ResultAnalysisMapper resultAnalysisMapper; | |||||
| public NaritechPatrolDataService(RestTemplate restTemplate, | |||||
| NaritechProperties apiProperties, | |||||
| NaritechTokenService tokenService, | |||||
| ResultAnalysisMapper resultAnalysisMapper) { | |||||
| this.restTemplate = restTemplate; | |||||
| this.apiProperties = apiProperties; | |||||
| this.tokenService = tokenService; | |||||
| this.resultAnalysisMapper = resultAnalysisMapper; | |||||
| } | |||||
| /** | |||||
| * 收集巡视数据。 | |||||
| * 此处需要根据实际业务替换为真实的数据来源(数据库查询、文件读取等)。 | |||||
| */ | |||||
| public List<NaritechPatrolData> collectPatrolData() { | |||||
| // 查询当天未上报的红外算法巡视记录 | |||||
| // infrared: filter=1 其他算法: filter=0 | |||||
| List<NaritechPatrolData> dataList = resultAnalysisMapper.selectNaritechNotUploaded(); | |||||
| // 去掉读数单位 | |||||
| if (dataList != null && !dataList.isEmpty()) { | |||||
| dataList.forEach(NaritechPatrolDataService::removeUnitFromValue); | |||||
| } | |||||
| return dataList; | |||||
| } | |||||
| /** | |||||
| * 去掉读数单位 | |||||
| */ | |||||
| private static void removeUnitFromValue(NaritechPatrolData data) { | |||||
| String value = data.getValue(); | |||||
| if (StringUtils.isEmpty(value)) { | |||||
| return; | |||||
| } | |||||
| String unit = data.getUnit(); | |||||
| String[] vals = value.split(",", -1); | |||||
| List<String> cleanedValues = new ArrayList<>(); | |||||
| for (String val : vals) { | |||||
| if (val.endsWith(unit)) { | |||||
| val = val.substring(0, val.length() - unit.length()); | |||||
| } | |||||
| // 非数字值,直接返回空值 | |||||
| if (!NumberUtils.isCreatable(val)) { | |||||
| data.setValue(""); | |||||
| return; | |||||
| } | |||||
| cleanedValues.add(val); | |||||
| } | |||||
| data.setValue(String.join(",", cleanedValues)); | |||||
| } | |||||
| /** | |||||
| * 上送巡视数据到远端接口 | |||||
| */ | |||||
| public NaritechPatrolResponse uploadPatrolData(List<NaritechPatrolData> dataList) { | |||||
| if (dataList == null || dataList.isEmpty()) { | |||||
| log.warn("无巡视数据需要上送"); | |||||
| return null; | |||||
| } | |||||
| String token = tokenService.getAccessToken(); | |||||
| HttpHeaders headers = new HttpHeaders(); | |||||
| headers.setContentType(MediaType.APPLICATION_JSON); | |||||
| headers.setBearerAuth(token); | |||||
| HttpEntity<List<NaritechPatrolData>> requestEntity = new HttpEntity<>(dataList, headers); | |||||
| String url = apiProperties.getFullPatrolReceiveUrl(); | |||||
| log.info("上送巡视数据到: {}, 数据条数: {}", url, dataList.size()); | |||||
| try { | |||||
| NaritechPatrolResponse response = restTemplate.postForObject(url, requestEntity, NaritechPatrolResponse.class); | |||||
| if (response != null && Boolean.TRUE.equals(response.getResult())) { | |||||
| log.info("巡视数据上送成功: {}", response.getMessage()); | |||||
| } else { | |||||
| log.error("巡视数据上送失败: {}", response != null ? response.getMessage() : "无响应"); | |||||
| } | |||||
| return response; | |||||
| } catch (Exception e) { | |||||
| log.error("巡视数据上送异常", e); | |||||
| throw new RuntimeException("巡视数据上送失败: " + e.getMessage(), e); | |||||
| } | |||||
| } | |||||
| public void recordUploadSuccess(List<Long> logIds) { | |||||
| if (logIds == null || logIds.isEmpty()) { | |||||
| log.warn("无巡视数据需要上送"); | |||||
| return; | |||||
| } | |||||
| resultAnalysisMapper.batchInsertUploadLog(logIds, "naritech#infrared", LocalDateTime.now()); | |||||
| log.info("已记录 {} 条上传成功数据", logIds.size()); | |||||
| } | |||||
| public static void main(String[] args) { | |||||
| NaritechPatrolData data = NaritechPatrolData.builder() | |||||
| .lineId(12736374) | |||||
| .areaName("极Ⅱ") | |||||
| .deviceName("极Ⅱ高Y/Y-C相") | |||||
| .patroldeviceName("") | |||||
| .patrolpointName("极Ⅱ高Y/Y-C相电容器与管母接头测温") | |||||
| .value("27,2℃") | |||||
| .time("2026-05-26 01:01:03") | |||||
| .unit("℃") | |||||
| .build(); | |||||
| removeUnitFromValue(data); | |||||
| System.out.println(data); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,77 @@ | |||||
| package com.inspect.simulator.naritech.service; | |||||
| import com.inspect.simulator.naritech.config.AuthProperties; | |||||
| import com.inspect.simulator.naritech.config.NaritechProperties; | |||||
| import com.inspect.simulator.naritech.dto.NaritechTokenResponse; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.util.LinkedMultiValueMap; | |||||
| import org.springframework.util.MultiValueMap; | |||||
| import org.springframework.web.client.RestTemplate; | |||||
| import java.time.Instant; | |||||
| @Slf4j | |||||
| @Service | |||||
| public class NaritechTokenService { | |||||
| private final RestTemplate restTemplate; | |||||
| private final NaritechProperties apiProperties; | |||||
| private final AuthProperties authProperties; | |||||
| private volatile String cachedToken; | |||||
| private volatile long tokenExpireTime = 0; | |||||
| public NaritechTokenService(RestTemplate restTemplate, | |||||
| NaritechProperties apiProperties, | |||||
| AuthProperties authProperties) { | |||||
| this.restTemplate = restTemplate; | |||||
| this.apiProperties = apiProperties; | |||||
| this.authProperties = authProperties; | |||||
| } | |||||
| public synchronized String getAccessToken() { | |||||
| // Token未过期则复用(提前5分钟刷新) | |||||
| if (cachedToken != null && System.currentTimeMillis() < tokenExpireTime - 300_000) { | |||||
| return cachedToken; | |||||
| } | |||||
| return fetchToken(); | |||||
| } | |||||
| private String fetchToken() { | |||||
| log.info("开始获取访问Token..."); | |||||
| try { | |||||
| MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); | |||||
| body.add("grant_type", authProperties.getGrantType()); | |||||
| body.add("scope", authProperties.getScope()); | |||||
| body.add("client_id", authProperties.getClientId()); | |||||
| body.add("client_secret", authProperties.getClientSecret()); | |||||
| String url = apiProperties.getFullTokenUrl(); | |||||
| log.info("请求Token接口: {}", url); | |||||
| NaritechTokenResponse response = restTemplate.postForObject(url, body, NaritechTokenResponse.class); | |||||
| if (response != null && response.getAccessToken() != null) { | |||||
| cachedToken = response.getAccessToken(); | |||||
| if (response.getExpiresIn() != null && response.getExpiresIn() > 0) { | |||||
| tokenExpireTime = System.currentTimeMillis() + response.getExpiresIn() * 1000L; | |||||
| } else { | |||||
| // 默认24小时过期 | |||||
| tokenExpireTime = System.currentTimeMillis() + 86400_000L; | |||||
| } | |||||
| log.info("Token获取成功, 过期时间: {}", Instant.ofEpochMilli(tokenExpireTime)); | |||||
| return cachedToken; | |||||
| } | |||||
| log.error("Token获取失败: 响应为空或缺少access_token"); | |||||
| throw new RuntimeException("Token获取失败"); | |||||
| } catch (Exception e) { | |||||
| log.error("Token获取异常", e); | |||||
| throw new RuntimeException("Token获取失败: " + e.getMessage(), e); | |||||
| } | |||||
| } | |||||
| public void clearToken() { | |||||
| cachedToken = null; | |||||
| tokenExpireTime = 0; | |||||
| } | |||||
| } | |||||