| @ -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; | |||
| } | |||
| } | |||