diff --git a/pom.xml b/pom.xml
index 7428a99..54ac37e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -157,7 +157,15 @@
mybatis-plus-boot-starter
3.4.3
-
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ com.github.pagehelper
+ pagehelper-spring-boot-starter
+ 1.4.1
+
net.java.jna
jna
diff --git a/src/main/java/com/inspect/simulator/mapper/ResultAnalysisMapper.java b/src/main/java/com/inspect/simulator/mapper/ResultAnalysisMapper.java
index f247611..ad3ae90 100644
--- a/src/main/java/com/inspect/simulator/mapper/ResultAnalysisMapper.java
+++ b/src/main/java/com/inspect/simulator/mapper/ResultAnalysisMapper.java
@@ -1,11 +1,12 @@
package com.inspect.simulator.mapper;
-import com.inspect.simulator.domain.analysis.vo.AnalysisResult;
import com.inspect.simulator.domain.bigmodelr.ContentJson;
import com.inspect.simulator.domain.visual.VisualJson;
+import com.inspect.simulator.naritech.dto.NaritechPatrolData;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
+import java.time.LocalDateTime;
import java.util.List;
@Mapper
@@ -20,4 +21,10 @@ public interface ResultAnalysisMapper {
int addDmtModelInfo(@Param("list") List contentJsonList);
int addVisualModelInfo(@Param("list") List visualJsonList);
+
+ List selectNaritechNotUploaded();
+
+ int batchInsertUploadLog(@Param("logIds") List logIds,
+ @Param("type") String type,
+ @Param("uploadTime") LocalDateTime uploadTime);
}
diff --git a/src/main/java/com/inspect/simulator/naritech/config/AuthProperties.java b/src/main/java/com/inspect/simulator/naritech/config/AuthProperties.java
new file mode 100644
index 0000000..36bb5bb
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/config/AuthProperties.java
@@ -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;
+}
diff --git a/src/main/java/com/inspect/simulator/naritech/config/NaritechProperties.java b/src/main/java/com/inspect/simulator/naritech/config/NaritechProperties.java
new file mode 100644
index 0000000..2238646
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/config/NaritechProperties.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/inspect/simulator/naritech/config/RestTemplateConfig.java b/src/main/java/com/inspect/simulator/naritech/config/RestTemplateConfig.java
new file mode 100644
index 0000000..c2cb63a
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/config/RestTemplateConfig.java
@@ -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 registry = RegistryBuilder.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));
+ }
+}
diff --git a/src/main/java/com/inspect/simulator/naritech/controller/NaritechController.java b/src/main/java/com/inspect/simulator/naritech/controller/NaritechController.java
new file mode 100644
index 0000000..a4afe51
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/controller/NaritechController.java
@@ -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 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 uploadedIds = new ArrayList<>();
+
+ for (int i = 0; i < total; i+=BATCH_SIZE) {
+ int end = Math.min(i + BATCH_SIZE, total);
+ List 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 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 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);
+ }
+ }
+}
diff --git a/src/main/java/com/inspect/simulator/naritech/dto/NaritechPatrolData.java b/src/main/java/com/inspect/simulator/naritech/dto/NaritechPatrolData.java
new file mode 100644
index 0000000..e4909bb
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/dto/NaritechPatrolData.java
@@ -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;
+}
diff --git a/src/main/java/com/inspect/simulator/naritech/dto/NaritechPatrolResponse.java b/src/main/java/com/inspect/simulator/naritech/dto/NaritechPatrolResponse.java
new file mode 100644
index 0000000..6b4d114
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/dto/NaritechPatrolResponse.java
@@ -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;
+}
diff --git a/src/main/java/com/inspect/simulator/naritech/dto/NaritechTokenResponse.java b/src/main/java/com/inspect/simulator/naritech/dto/NaritechTokenResponse.java
new file mode 100644
index 0000000..472f392
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/dto/NaritechTokenResponse.java
@@ -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;
+}
diff --git a/src/main/java/com/inspect/simulator/naritech/service/NaritechPatrolDataService.java b/src/main/java/com/inspect/simulator/naritech/service/NaritechPatrolDataService.java
new file mode 100644
index 0000000..13ff52c
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/service/NaritechPatrolDataService.java
@@ -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 collectPatrolData() {
+ // 查询当天未上报的红外算法巡视记录
+ // infrared: filter=1 其他算法: filter=0
+ List 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 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 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> 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 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);
+ }
+}
diff --git a/src/main/java/com/inspect/simulator/naritech/service/NaritechTokenService.java b/src/main/java/com/inspect/simulator/naritech/service/NaritechTokenService.java
new file mode 100644
index 0000000..a36d63f
--- /dev/null
+++ b/src/main/java/com/inspect/simulator/naritech/service/NaritechTokenService.java
@@ -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 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;
+ }
+}
diff --git a/src/main/resources/mapper/ResultAnalysisMapper.xml b/src/main/resources/mapper/ResultAnalysisMapper.xml
index 20eac38..2d7f31b 100644
--- a/src/main/resources/mapper/ResultAnalysisMapper.xml
+++ b/src/main/resources/mapper/ResultAnalysisMapper.xml
@@ -73,4 +73,47 @@
+
+
+
+ INSERT INTO patrol_upload_log (log_id, type, upload_time)
+ VALUES
+
+ (#{id}, #{type}, #{uploadTime})
+
+ ON DUPLICATE KEY UPDATE upload_time = VALUES(upload_time)
+
\ No newline at end of file