diff --git a/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/RedisConst.java b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/RedisConst.java
index 9561e4e..cd82dd3 100644
--- a/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/RedisConst.java
+++ b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/RedisConst.java
@@ -13,4 +13,5 @@ public class RedisConst {
public static final String TASK_RECORD_CNT = "TASK_RECORD_CNT";
public static final String TASK_CURRENT_CODE = "TASK_CURRENT_CODE@";
public static final String IMMEDIATELY_EXEC_TASK_TIME = "IMMEDIATELY_EXEC_TASK_TIME";
+ public static final String REPORT_PUSH_LOCK = "REPORT_PUSH_LOCK@";//任务完成自动归档+报告推送 防重锁
}
diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/config/RabbitMQConfig.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/config/RabbitMQConfig.java
index 4c11d4a..2700d94 100644
--- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/config/RabbitMQConfig.java
+++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/config/RabbitMQConfig.java
@@ -25,7 +25,25 @@ public class RabbitMQConfig {
@Bean
public Binding alarmBinding() {
- return BindingBuilder.bind(alarmQueue()).to(alarmExchange()).with(YanyuanConstants.EXCHANGE);
+ return BindingBuilder.bind(alarmQueue()).to(alarmExchange()).with(YanyuanConstants.ROUTING_KEY);
+ }
+
+ // ============================ 巡检报告上传推送 MQ ============================
+
+ @Bean
+ public TopicExchange reportExchange() {
+ return new TopicExchange(YanyuanConstants.REPORT_EXCHANGE, true, false);
+ }
+
+ @Bean("reportUploadQueue")
+ public Queue reportUploadQueue() {
+ return new Queue(YanyuanConstants.REPORT_QUEUE, true);
+ }
+
+ @Bean
+ public Binding reportUploadBinding() {
+ return BindingBuilder.bind(reportUploadQueue()).to(reportExchange())
+ .with(YanyuanConstants.REPORT_ROUTING_KEY);
}
@Bean
diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/constant/YanyuanConstants.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/constant/YanyuanConstants.java
index ec7758e..67bb27e 100644
--- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/constant/YanyuanConstants.java
+++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/constant/YanyuanConstants.java
@@ -7,4 +7,9 @@ public class YanyuanConstants {
public static final String EXCHANGE = "alarm.exchange";
public static final String QUEUE = "alarm.queue";
public static final String ROUTING_KEY = "alarm.routing-key";
+
+ /** 巡检报告上传推送(先上报MQ,再由消费者HTTP上传外部平台) */
+ public static final String REPORT_EXCHANGE = "report.exchange";
+ public static final String REPORT_QUEUE = "report.upload.queue";
+ public static final String REPORT_ROUTING_KEY = "report.upload.routing-key";
}
diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/AlarmMessage.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/AlarmMessage.java
index 5c0fd73..d3e6425 100644
--- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/AlarmMessage.java
+++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/AlarmMessage.java
@@ -6,6 +6,8 @@ import lombok.*;
@ToString
@Data
@Builder
+@NoArgsConstructor
+@AllArgsConstructor
public class AlarmMessage extends BaseEntity {
private String messageId;
private String messageType;
diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ReportPushMessage.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ReportPushMessage.java
new file mode 100644
index 0000000..6361247
--- /dev/null
+++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ReportPushMessage.java
@@ -0,0 +1,26 @@
+package com.inspect.api.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 巡检报告上传推送消息(先上报 report.upload.queue,由消费者异步 HTTP 上传外部平台)
+ */
+@Data
+public class ReportPushMessage implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 报告ID(inspection_report.line_id) */
+ private Long reportId;
+
+ /** 报告文件路径(SFTP,docx) */
+ private String filePath;
+
+ /** 报告详情(reportInfo JSON 字符串,发布时组装,消费者只负责下载文件并上传) */
+ private String reportInfo;
+
+ /** 消费重试次数(上传失败后 MQ 重投递计数) */
+ private int retryCount;
+}
diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportPushPublisher.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportPushPublisher.java
new file mode 100644
index 0000000..79506d6
--- /dev/null
+++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportPushPublisher.java
@@ -0,0 +1,34 @@
+package com.inspect.api.service;
+
+import com.inspect.api.constant.YanyuanConstants;
+import com.inspect.api.domain.ReportPushMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.stereotype.Service;
+
+/**
+ * 巡检报告上传消息发布者:报告生成后先发布到 report.exchange,
+ * 由 {@link ReportUploadConsumer} 异步 HTTP 上传到外部平台。
+ */
+@Slf4j
+@Service
+public class ReportPushPublisher {
+
+ private final RabbitTemplate rabbitTemplate;
+
+ public ReportPushPublisher(RabbitTemplate rabbitTemplate) {
+ this.rabbitTemplate = rabbitTemplate;
+ }
+
+ public void publish(ReportPushMessage message) {
+ try {
+ rabbitTemplate.convertAndSend(YanyuanConstants.REPORT_EXCHANGE,
+ YanyuanConstants.REPORT_ROUTING_KEY, message);
+ log.info("[REPORT_PUSH_MQ] 报告上传消息已发布: reportId={}, filePath={}",
+ message.getReportId(), message.getFilePath());
+ } catch (Exception e) {
+ log.error("[REPORT_PUSH_MQ] 报告上传消息发布失败: reportId={}, err: {}",
+ message.getReportId(), e.getMessage(), e);
+ }
+ }
+}
diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadConsumer.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadConsumer.java
new file mode 100644
index 0000000..0bec7a2
--- /dev/null
+++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadConsumer.java
@@ -0,0 +1,137 @@
+package com.inspect.api.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.inspect.api.domain.ReportPushMessage;
+import com.inspect.base.core.sftp.SftpClient;
+import com.inspect.base.core.utils.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+/**
+ * 巡检报告上传消费者:从 report.upload.queue 取消息,下载报告 docx,
+ * 通过 {@link ReportUploadService} HTTP 上传到外部平台(POST /jb-nessp-external/api/uploadReport)。
+ *
+ * 上传失败抛出异常使消息重投递(重试次数由消息 retryCount 控制,超过 {@code mq-max-retries} 丢弃)。
+ */
+@Slf4j
+@Component
+public class ReportUploadConsumer {
+
+ /** 上传失败最大重试次数(超过后丢弃,避免无限重投) */
+ @Value("${inspect.report-push.mq-max-retries:3}")
+ private int maxRetries;
+
+ /** 报告文件(SFTP)下载重试次数/间隔(毫秒),防止异步上传尚未完成 */
+ @Value("${inspect.report-push.download-retry-times:3}")
+ private int downloadRetryTimes;
+
+ @Value("${inspect.report-push.download-retry-interval-millis:1000}")
+ private long downloadRetryIntervalMillis;
+
+ private final ReportUploadService reportUploadService;
+ private final SftpClient sftpClient;
+
+ public ReportUploadConsumer(ReportUploadService reportUploadService, SftpClient sftpClient) {
+ this.reportUploadService = reportUploadService;
+ this.sftpClient = sftpClient;
+ }
+
+ @RabbitListener(queues = "#{reportUploadQueue.name}")
+ public void onReport(ReportPushMessage message) {
+ if (message == null || message.getReportId() == null) {
+ log.warn("[REPORT_PUSH_MQ] 报告上传消息为空,丢弃");
+ return;
+ }
+ try {
+ boolean ok = upload(message);
+ if (ok) {
+ log.info("[REPORT_PUSH_MQ] 报告上传成功: reportId={}", message.getReportId());
+ } else {
+ throw new RuntimeException("report upload failed, will be requeued");
+ }
+ } catch (Exception e) {
+ message.setRetryCount(message.getRetryCount() + 1);
+ if (message.getRetryCount() > maxRetries) {
+ log.error("[REPORT_PUSH_MQ] 报告上传超过最大重试次数({}), 丢弃: reportId={}, err: {}",
+ maxRetries, message.getReportId(), e.getMessage());
+ return;
+ }
+ log.warn("[REPORT_PUSH_MQ] 报告上传失败(第{}次), 消息重投递: reportId={}, err: {}",
+ message.getRetryCount(), message.getReportId(), e.getMessage());
+ throw new RuntimeException("report upload failed, will be requeued", e);
+ }
+ }
+
+ /**
+ * 下载报告 docx + HTTP 上传(reportInfo 由发布方组装好随消息传递)
+ */
+ private boolean upload(ReportPushMessage message) throws Exception {
+ String filePath = message.getFilePath();
+ if (StringUtils.isEmpty(filePath)) {
+ log.warn("[REPORT_PUSH_MQ] 报告文件路径为空, 丢弃: reportId={}", message.getReportId());
+ return true;
+ }
+ byte[] fileBytes = downloadFromSftp(filePath);
+ if (fileBytes == null || fileBytes.length == 0) {
+ log.warn("[REPORT_PUSH_MQ] 下载报告文件失败: {}", filePath);
+ return false;
+ }
+ JSONObject reportInfo = JSONObject.parseObject(message.getReportInfo());
+ if (reportInfo == null) {
+ log.warn("[REPORT_PUSH_MQ] reportInfo 为空, 丢弃: reportId={}", message.getReportId());
+ return true;
+ }
+ String fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
+ boolean ok = reportUploadService.uploadReport(fileBytes, fileName, reportInfo);
+ log.info("[REPORT_PUSH_MQ] 报告上传结果: {}, reportId={}, fileName={}",
+ ok, message.getReportId(), fileName);
+ return ok;
+ }
+
+ /**
+ * 从 SFTP 下载报告文件(导出为异步上传,下载失败自动重试等待)
+ */
+ private byte[] downloadFromSftp(String filePath) {
+ for (int i = 0; i < Math.max(1, downloadRetryTimes); i++) {
+ try {
+ final byte[][] holder = new byte[1][];
+ sftpClient.downLoad(filePath, inputStream -> holder[0] = readAll(inputStream));
+ if (holder[0] != null && holder[0].length > 0) {
+ return holder[0];
+ }
+ log.warn("[REPORT_PUSH_MQ] 报告文件尚未就绪, 重试 {}/{}: {}", i + 1, downloadRetryTimes, filePath);
+ } catch (Exception e) {
+ log.warn("[REPORT_PUSH_MQ] 下载报告文件异常, 重试 {}/{}: {}, err: {}",
+ i + 1, downloadRetryTimes, filePath, e.getMessage());
+ }
+ if (i < Math.max(1, downloadRetryTimes) - 1 && downloadRetryIntervalMillis > 0) {
+ try {
+ Thread.sleep(downloadRetryIntervalMillis);
+ } catch (InterruptedException ignore) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }
+ return null;
+ }
+
+ private byte[] readAll(InputStream inputStream) throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ byte[] buf = new byte[8192];
+ int n;
+ try {
+ while ((n = inputStream.read(buf)) != -1) {
+ bos.write(buf, 0, n);
+ }
+ } finally {
+ inputStream.close();
+ }
+ return bos.toByteArray();
+ }
+}
diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadService.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadService.java
new file mode 100644
index 0000000..f34f5de
--- /dev/null
+++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadService.java
@@ -0,0 +1,182 @@
+package com.inspect.api.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.inspect.api.domain.ApiResult;
+import com.inspect.api.domain.AuthTokenData;
+import com.inspect.base.core.utils.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * 巡检报告上传服务(外部平台《巡检报告接口文档 v1.0.0》)
+ *
+ * 1. Joinbright-Token 复用 {@link ExtAuthService}(GET /ext_auth/getAuthToken,配置 yanyuan.auth.*)
+ * 2. POST {upload-url} 上传巡检报告:
+ * - 请求头携带 joinbright-token
+ * - multipart/form-data:file(报告文件) + reportInfo(报告详情 JSON 字符串)
+ */
+@Slf4j
+@Service
+public class ReportUploadService {
+
+ /** Token 请求头名称(外部平台固定) */
+ private static final String TOKEN_HEADER = "joinbright-token";
+
+ /** 是否启用报告上传推送 */
+ @Value("${inspect.report-push.enabled:true}")
+ private boolean enabled;
+
+ /** 巡检报告上传接口完整地址 */
+ @Value("${inspect.report-push.upload-url:http://192.168.129.2:32799/jb-gateway/jb-nessp-external/api/uploadReport}")
+ private String uploadUrl;
+
+ /** http 连接/读取超时(秒) */
+ @Value("${inspect.report-push.http-timeout-seconds:60}")
+ private int httpTimeoutSeconds;
+
+ private final ExtAuthService extAuthService;
+
+ public ReportUploadService(ExtAuthService extAuthService) {
+ this.extAuthService = extAuthService;
+ }
+
+ /**
+ * 获取 Joinbright-Token
+ */
+ public String getJoinbrightToken() {
+ try {
+ ApiResult result = extAuthService.getAuthToken();
+ if (result == null || !result.isSuccess() || result.getData() == null) {
+ log.error("[REPORT-PUSH] 获取 Joinbright-Token 失败: {}", result);
+ return null;
+ }
+ String token = result.getData().getAccess_token();
+ log.info("[REPORT-PUSH] 获取 Joinbright-Token 成功: {}...",
+ StringUtils.isEmpty(token) ? "" : token.substring(0, Math.min(12, token.length())));
+ return token;
+ } catch (Exception e) {
+ log.error("[REPORT-PUSH] 获取 Joinbright-Token 异常", e);
+ return null;
+ }
+ }
+
+ /**
+ * 上传巡检报告文件及元数据到外部平台
+ *
+ * @param fileBytes 报告文件字节(docx)
+ * @param fileName 报告文件名(含扩展名)
+ * @param reportInfo 报告详情对象(序列化为 JSON 字符串传递)
+ * @return 是否上传成功
+ */
+ public boolean uploadReport(byte[] fileBytes, String fileName, JSONObject reportInfo) {
+ if (!enabled) {
+ log.warn("[REPORT-PUSH] 报告上传未启用(inspect.report-push.enabled=false)");
+ return false;
+ }
+ if (fileBytes == null || fileBytes.length == 0) {
+ log.warn("[REPORT-PUSH] 报告文件为空, 无法上传");
+ return false;
+ }
+ if (reportInfo == null) {
+ log.warn("[REPORT-PUSH] reportInfo 为空, 无法上传");
+ return false;
+ }
+ // 上传失败时(token可能失效/超时)重新获取token重试一次
+ String token = getJoinbrightToken();
+ boolean ok = doUpload(token, fileBytes, fileName, reportInfo);
+ if (!ok && StringUtils.isNotEmpty(token)) {
+ log.warn("[REPORT-PUSH] 首次上传失败, 重新获取token后重试");
+ ok = doUpload(getJoinbrightToken(), fileBytes, fileName, reportInfo);
+ }
+ return ok;
+ }
+
+ private boolean doUpload(String token, byte[] fileBytes, String fileName, JSONObject reportInfo) {
+ if (StringUtils.isEmpty(token)) {
+ log.error("[REPORT-PUSH] Joinbright-Token 为空, 无法上传");
+ return false;
+ }
+ String boundary = "----InspectReportBoundary" + System.currentTimeMillis();
+ HttpURLConnection conn = null;
+ try {
+ URL url = new URL(uploadUrl);
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setDoInput(true);
+ conn.setConnectTimeout(httpTimeoutSeconds * 1000);
+ conn.setReadTimeout(httpTimeoutSeconds * 1000);
+ conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
+ conn.setRequestProperty(TOKEN_HEADER, token);
+
+ String reportInfoJson = reportInfo.toJSONString();
+ try (DataOutputStream out = new DataOutputStream(conn.getOutputStream())) {
+ // reportInfo 字段
+ out.writeBytes("--" + boundary + "\r\n");
+ out.writeBytes("Content-Disposition: form-data; name=\"reportInfo\"\r\n");
+ out.writeBytes("Content-Type: text/plain; charset=UTF-8\r\n\r\n");
+ out.write(reportInfoJson.getBytes("UTF-8"));
+ out.writeBytes("\r\n");
+ // file 字段
+ out.writeBytes("--" + boundary + "\r\n");
+ out.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"\r\n");
+ out.writeBytes("Content-Type: application/octet-stream\r\n\r\n");
+ try (InputStream in = new ByteArrayInputStream(fileBytes)) {
+ byte[] buf = new byte[8192];
+ int n;
+ while ((n = in.read(buf)) != -1) {
+ out.write(buf, 0, n);
+ }
+ }
+ out.writeBytes("\r\n");
+ out.writeBytes("--" + boundary + "--\r\n");
+ out.flush();
+ }
+
+ int responseCode = conn.getResponseCode();
+ String response = readResponse(conn, responseCode);
+ log.info("[REPORT-PUSH] 上传响应 code: {}, body: {}", responseCode, response);
+ if (responseCode == 200) {
+ JSONObject resp = JSONObject.parseObject(response);
+ if (resp != null && Boolean.TRUE.equals(resp.getBoolean("success"))) {
+ return true;
+ }
+ log.error("[REPORT-PUSH] 上传业务失败: {}", response);
+ } else {
+ log.error("[REPORT-PUSH] 上传HTTP失败: {}, body: {}", responseCode, response);
+ }
+ return false;
+ } catch (Exception e) {
+ log.error("[REPORT-PUSH] 报告上传异常, url: {}", uploadUrl, e);
+ return false;
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+
+ private String readResponse(HttpURLConnection conn, int responseCode) throws Exception {
+ InputStream stream = responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream();
+ if (stream == null) {
+ return "";
+ }
+ StringBuilder result = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ result.append(line);
+ }
+ }
+ return result.toString();
+ }
+}
diff --git a/inspect-main/inspect-main-start/src/main/java/com/inspect/InspectStartApplication.java b/inspect-main/inspect-main-start/src/main/java/com/inspect/InspectStartApplication.java
index eb0c44d..4a1927d 100644
--- a/inspect-main/inspect-main-start/src/main/java/com/inspect/InspectStartApplication.java
+++ b/inspect-main/inspect-main-start/src/main/java/com/inspect/InspectStartApplication.java
@@ -18,7 +18,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
@EnableRyFeignClients
@EnableDiscoveryClient
@SpringBootApplication
-@EnableAspectJAutoProxy
+@EnableAspectJAutoProxy(exposeProxy = true)
@EnableAsync
public class InspectStartApplication {
private final static Logger log = LoggerFactory.getLogger(InspectStartApplication.class);
diff --git a/inspect-main/inspect-main-start/src/main/resources/application.yml b/inspect-main/inspect-main-start/src/main/resources/application.yml
index b46691d..8eab957 100644
--- a/inspect-main/inspect-main-start/src/main/resources/application.yml
+++ b/inspect-main/inspect-main-start/src/main/resources/application.yml
@@ -64,4 +64,43 @@ sftp:
port: 21
username: ftpuser
password: atia2018
- isImplicit: 1
\ No newline at end of file
+ isImplicit: 1
+
+# 盐源外部平台认证/推送(复用 inspect-main-api 的 ExtAuthService / AlarmWsConnection)
+# client-id/client-secret 为现场认证凭据,请按现场配置填写(可在nacos配置中心覆盖)
+yanyuan:
+ auth:
+ base-url: http://192.168.129.2:32799/jb-gateway/jb-base
+ ws-url: ws://192.168.129.2:32799/jb-gateway/jb-base/jb-base-socket
+ client-id: ""
+ client-secret: ""
+
+# 任务巡检完成后自动归档 + 生成报告 + 推送(先上报 RabbitMQ,再由消费者 HTTP 上传外部平台)
+# 见《盐源新能源智慧感知平台_巡检报告接口文档 v1.0.0》:POST /jb-nessp-external/api/uploadReport
+inspect:
+ report-push:
+ # 是否启用自动归档+推送
+ enabled: true
+ # 巡检报告上传接口完整地址
+ upload-url: http://192.168.129.2:32799/jb-gateway/jb-nessp-external/api/uploadReport
+ # http 连接/读取超时(秒)
+ http-timeout-seconds: 60
+ # 自动归档审核人(报告发起人 username 为空时的兜底)
+ check-person: admin
+ # 自动归档巡视结论
+ task-result: 巡检完成,自动归档
+ # 任务状态更新事务提交等待时间(毫秒)
+ after-commit-delay-millis: 2000
+ # 归档防重锁过期时间(秒)
+ lock-expire-seconds: 86400
+ # 上传报告的场站ID(为空时取 basedata_station.station_no)
+ station-id: ""
+ # 上传报告的巡检类型(风机巡检/光伏巡检/线路巡检/山火巡检/机器人巡检;为空时按任务预置位类型推断)
+ inspect-type: ""
+ # 上传报告的巡检设备名称(机库名称、机器人名称、摄像头名称,可选)
+ inspect-device-name: ""
+ # 报告文件(SFTP)下载重试次数/间隔(毫秒)
+ download-retry-times: 10
+ download-retry-interval-millis: 1000
+ # 上传失败 MQ 重投递最大次数(超过后丢弃,避免无限重试)
+ mq-max-retries: 3
\ No newline at end of file
diff --git a/inspect-main/inspect-main-task/pom.xml b/inspect-main/inspect-main-task/pom.xml
index a2f28dd..1a32223 100644
--- a/inspect-main/inspect-main-task/pom.xml
+++ b/inspect-main/inspect-main-task/pom.xml
@@ -13,6 +13,11 @@
inspect-main-task
3.4.0
+
+ UTF-8
+ UTF-8
+
+
org.springframework.boot
@@ -63,10 +68,13 @@
+
org.apache.poi
poi-ooxml
- 5.2.3
+ 4.1.2
org.redisson
@@ -79,5 +87,11 @@
metadata-extractor
2.18.0
+
+
+ com.inspect
+ inspect-main-api
+ 3.4.0
+
\ No newline at end of file
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/controller/ResultAnalysisController.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/controller/ResultAnalysisController.java
index 1fa2e42..2a2b265 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/controller/ResultAnalysisController.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/controller/ResultAnalysisController.java
@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.PageInfo;
import com.inspect.analysis.domain.*;
import com.inspect.analysis.service.IResultAnalysisService;
+import com.inspect.analysis.service.DefectListAggregator;
import com.inspect.base.core.constant.AlgConstants;
import com.inspect.base.core.domain.Response;
import com.inspect.base.core.enums.PointStatusEnum;
@@ -884,6 +885,30 @@ public class ResultAnalysisController extends BaseController {
return rspData;
}
+ /**
+ * 缺陷列表查询:同一任务、点位对象和算法聚合为一条记录。
+ */
+ @GetMapping({"/defect/summary"})
+ public TableDataInfo defectSummary(DefectListQuery query) {
+ logger.info("LIST_DEFECT_SUMMARY param: {}", query);
+
+ List candidates = patrolResultService.selectDefectList(query);
+ logger.info("缺陷汇总原始数据 candidates size:{}",candidates == null ? 0 : candidates.size());
+ List algInfos = patrolResultService.selectAlgInfo(new PatrolResult());
+ List resultList = DefectListAggregator.aggregate(candidates, algInfos);
+ logger.info("缺陷汇总 resultList size:{}",resultList.size());
+ PageDomain pageDomain = TableSupport.buildPageRequest();
+ List pageList = DefectListAggregator.page(
+ resultList, pageDomain.getPageNum(), pageDomain.getPageSize());
+
+ TableDataInfo response = new TableDataInfo();
+ response.setCode(200);
+ response.setRows(pageList);
+ response.setTotal(resultList.size());
+ response.setMsg("查询成功");
+ return response;
+ }
+
private static class GroupKey {
private final String objectId;
private final String algType;
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/DefectListQuery.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/DefectListQuery.java
new file mode 100644
index 0000000..c6fe00d
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/DefectListQuery.java
@@ -0,0 +1,52 @@
+package com.inspect.analysis.domain;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.List;
+
+/**
+ * 缺陷列表查询条件。
+ */
+@Getter
+@Setter
+@ToString
+public class DefectListQuery {
+ /** 巡视任务执行 ID。 */
+ private String taskPatrolledId;
+ /** 服务层根据主任务展开后的任务执行 ID。 */
+ private List taskPatrolIds;
+ /** 结果来源:0-大模型,1-初筛。 */
+ private String filter;
+ /** 算法类型。 */
+ private String algType;
+ /** 区域 ID。 */
+ private String areaId;
+ /** 主设备 ID。 */
+ private String deviceId;
+ /** 点位 ID。 */
+ private String pointId;
+ /** 任务编码。 */
+ private String taskCode;
+ /** 点位名称。 */
+ private String pointName;
+ /** 任务名称。 */
+ private String taskName;
+ /** 主设备名称。 */
+ private String deviceName;
+ /** 相机/巡检设备名称。 */
+ private String channelName;
+ /** 系统/设备来源。 */
+ private String deviceSource;
+ /** 点位状态。 */
+ private String pointStatus;
+ /** 缺陷描述关键字。 */
+ private String desc;
+ /** 开始日期,格式 yyyy-MM-dd。 */
+ private String beginTime;
+ /** 结束日期,格式 yyyy-MM-dd。 */
+ private String endTime;
+ /** 预置位类型:0-视频,其他-机器人/无人机;未传时按视频处理。 */
+ private String posType = "0";
+}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/DefectListAggregator.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/DefectListAggregator.java
new file mode 100644
index 0000000..c6defbc
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/DefectListAggregator.java
@@ -0,0 +1,205 @@
+package com.inspect.analysis.service;
+
+import com.inspect.analysis.domain.AlgValue;
+import com.inspect.base.core.constant.AlgConstants;
+import com.inspect.base.core.utils.StringUtils;
+import com.inspect.partrolresult.domain.AlgInfo;
+import com.inspect.task.domain.PatrolData;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 将缺陷分析明细按任务、对象和算法聚合为列表展示记录。
+ */
+public final class DefectListAggregator {
+
+ private DefectListAggregator() {
+ }
+
+ public static List aggregate(List source, List algInfos) {
+ if (source == null || source.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ Map algNameMap = algInfos == null ? Collections.emptyMap() : algInfos.stream()
+ .filter(Objects::nonNull)
+ .filter(item -> StringUtils.isNotEmpty(item.getAlgSubtypeCode()))
+ .collect(Collectors.toMap(AlgInfo::getAlgSubtypeCode, AlgInfo::getAlgSubtypeName,
+ (first, ignored) -> first));
+
+ Map> groups = new LinkedHashMap<>();
+ for (PatrolData item : source) {
+ if (item == null || !"0".equals(item.getWarnStatus())
+ || AlgConstants.CORRECTION.equals(item.getAlgType())) {
+ continue;
+ }
+ item.setAlgName(algNameMap.getOrDefault(item.getAlgType(), ""));
+ prepareItem(item);
+ GroupKey key = new GroupKey(item.getTaskPatrolId(), item.getObjectId(), item.getAlgType());
+ groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(item);
+ }
+
+ List result = new ArrayList<>();
+ for (Map.Entry> entry : groups.entrySet()) {
+ result.add(merge(entry.getKey(), entry.getValue()));
+ }
+ result.sort(Comparator.comparingInt(item -> statusRank(item.getResStatus())));
+ return result;
+ }
+
+ public static List page(List source, Integer pageNum, Integer pageSize) {
+ if (source == null || source.isEmpty()) {
+ return new ArrayList<>();
+ }
+ int safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
+ int safePageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
+ long from = (long) (safePageNum - 1) * safePageSize;
+ if (from >= source.size()) {
+ return new ArrayList<>();
+ }
+ int fromIndex = (int) from;
+ long to = Math.min((long) source.size(), from + safePageSize);
+ int toIndex = (int) to;
+ return source.subList(fromIndex, toIndex);
+ }
+
+ private static void prepareItem(PatrolData item) {
+ if ("1".equals(item.getFilter()) && StringUtils.isNotEmpty(item.getImageNormalUrlPath())) {
+ item.setImg(item.getImageNormalUrlPath());
+ }
+ if (item.getResValue() != null && "meter".equals(item.getAlgType())) {
+ String value = new AlgValue().formatValue(item.getResValue());
+ if ("-1".equals(value)) {
+ item.setResValue("空值");
+ }
+ }
+ }
+
+ private static PatrolData merge(GroupKey key, List values) {
+ PatrolData result = new PatrolData();
+ result.setTaskPatrolId(key.taskPatrolId);
+ result.setObjectId(key.objectId);
+ result.setAlgType(key.algType);
+ result.setDesc(joinDistinct(values, PatrolData::getDesc));
+
+ for (PatrolData item : values) {
+ if ("1".equals(item.getFilter())) {
+// if (StringUtils.isEmpty(result.getImgAnalyse())) {
+// result.setImgAnalyse(item.getImgAnalyse());
+// }
+ if (StringUtils.isEmpty(result.getImg())) {
+ result.setImg(item.getImg());
+ }
+ }
+ if ("0".equals(item.getFilter())) {
+ if (StringUtils.isNotEmpty(item.getImgAnalyse())) {
+ result.setImgAnalyse(item.getImgAnalyse());
+ }
+// if (StringUtils.isNotEmpty(item.getImg())) {
+// result.setImg(item.getImg());
+// }
+ }
+// if (StringUtils.isEmpty(result.getImg())) {
+// if (StringUtils.isNotEmpty(item.getImgAnalyse())) {
+// result.setImgAnalyse(item.getImgAnalyse());
+// }
+// if (StringUtils.isNotEmpty(item.getImg())) {
+// result.setImg(item.getImg());
+// }
+// }
+ }
+
+ result.setImageNormalUrlPath(firstNonEmpty(values, PatrolData::getImageNormalUrlPath));
+ result.setFilePath(firstNonEmpty(values, PatrolData::getFilePath));
+ result.setAlgName(firstNonEmpty(values, PatrolData::getAlgName));
+ result.setAreaName(firstNonEmpty(values, PatrolData::getAreaName));
+// result.setBrightDesc(firstNonEmpty(values, PatrolData::getBrightDesc));
+// result.setBrightImgAnalyse(firstNonEmpty(values, PatrolData::getBrightImgAnalyse));
+// result.setBrightResStatus(firstNonEmpty(values, PatrolData::getBrightResStatus));
+ result.setChannelName(firstNonEmpty(values, PatrolData::getChannelName));
+ result.setDataType(firstNonEmpty(values, PatrolData::getDataType));
+ result.setDeviceName(firstNonEmpty(values, PatrolData::getDeviceName));
+ result.setDeviceSource(firstNonEmpty(values, PatrolData::getDeviceSource));
+ result.setImgType(firstNonEmpty(values, PatrolData::getImgType));
+ result.setLineId(joinDistinct(values, PatrolData::getLineId));
+ result.setPatrolTime(firstNonEmpty(values, PatrolData::getPatrolTime));
+ result.setPointId(firstNonEmpty(values, PatrolData::getPointId));
+ result.setPointName(firstNonEmpty(values, PatrolData::getPointName));
+ result.setPointStatus(firstNonEmpty(values, PatrolData::getPointStatus));
+ result.setRequestId(firstNonEmpty(values, PatrolData::getRequestId));
+ result.setResValue(firstNonEmpty(values, PatrolData::getResValue));
+ result.setResStatus(firstNonEmpty(values, PatrolData::getResStatus));
+ result.setSuggestion(firstNonEmpty(values, PatrolData::getSuggestion));
+ result.setWarnStatus("0");
+ return result;
+ }
+
+ private static String firstNonEmpty(List values, Function getter) {
+ return values.stream().map(getter).filter(StringUtils::isNotEmpty).findFirst().orElse("");
+ }
+
+ private static String joinDistinct(List values, Function getter) {
+ Set distinct = new LinkedHashSet<>();
+ for (PatrolData value : values) {
+ String item = getter.apply(value);
+ if (StringUtils.isNotEmpty(item)) {
+ distinct.add(item);
+ }
+ }
+ return String.join(",", distinct);
+ }
+
+ private static int statusRank(String status) {
+ if ("1".equals(status)) {
+ return 1;
+ }
+ if ("3".equals(status)) {
+ return 2;
+ }
+ if ("2".equals(status)) {
+ return 3;
+ }
+ return 10;
+ }
+
+ private static final class GroupKey {
+ private final String taskPatrolId;
+ private final String objectId;
+ private final String algType;
+
+ private GroupKey(String taskPatrolId, String objectId, String algType) {
+ this.taskPatrolId = taskPatrolId;
+ this.objectId = objectId;
+ this.algType = algType;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof GroupKey)) {
+ return false;
+ }
+ GroupKey that = (GroupKey) other;
+ return Objects.equals(taskPatrolId, that.taskPatrolId)
+ && Objects.equals(objectId, that.objectId)
+ && Objects.equals(algType, that.algType);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(taskPatrolId, objectId, algType);
+ }
+ }
+}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java
index ff74319..3ad7b0c 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java
@@ -508,7 +508,7 @@ public class ResultAnalysisUtils {
if (!NumberUtils.isCreatable(val)) {
return val;
}
-
+ // 红外算法,保留1位小数(四舍五入)
if (AlgConstants.ALG_INFRARED_LIST.contains(algType)) {
String formatted = BigDecimalUtil.toOneDecimal(val, 1);
return shouldAddUnit ? formatted + unit : formatted;
@@ -521,7 +521,7 @@ public class ResultAnalysisUtils {
}
return val;
}
-
+ // 所有值都加单位(如果有单位的话)
return shouldAddUnit ? val + unit : val;
})
.collect(Collectors.joining(","));
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/aspect/TaskStatusChangeAspect.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/aspect/TaskStatusChangeAspect.java
index 34cf3ba..7128c37 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/aspect/TaskStatusChangeAspect.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/aspect/TaskStatusChangeAspect.java
@@ -1,6 +1,7 @@
package com.inspect.aspect;
import com.inspect.annotation.TaskStatusChange;
+import com.inspect.reportpush.service.ReportAutoArchiveService;
import com.inspect.task.service.IPatrolTaskService;
import com.inspect.taskstatus.domain.PatrolTaskStatus;
import lombok.extern.slf4j.Slf4j;
@@ -18,6 +19,9 @@ public class TaskStatusChangeAspect {
@Autowired
private IPatrolTaskService patrolTaskService;
+ @Autowired
+ private ReportAutoArchiveService reportAutoArchiveService;
+
/**
* 拦截带有 @TaskStatusChange 注解的方法
@@ -46,8 +50,9 @@ public class TaskStatusChangeAspect {
// 执行切面逻辑
if (taskStatusChange.async()) {
- log.info("异步执行区域任务合并报告,任务执行id: {}", taskStatus.getTaskPatrolledId());
- patrolTaskService.executeAsync(taskStatus);
+ log.info("异步执行自动归档,任务执行id: {}", taskStatus.getTaskPatrolledId());
+ // 任务巡检完成自动归档 + 生成报告 + websocket 推送(盐源外部平台)
+ reportAutoArchiveService.onTaskCompleted(taskStatus);
} else {
executeSync(taskStatus);
}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/img/domain/InspectionReportImg.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/img/domain/InspectionReportImg.java
index 46208aa..d73f148 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/img/domain/InspectionReportImg.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/img/domain/InspectionReportImg.java
@@ -28,6 +28,9 @@ public class InspectionReportImg extends BaseEntity {
name = "缩略图路径"
)
private String imgSrc;
+
+ private String imageNormalUrlPath;
+
@JsonFormat(
pattern = "yyyy-MM-dd"
)
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/insreport/domain/InspectionReport.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/insreport/domain/InspectionReport.java
index 2ac2ce5..053aa21 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/insreport/domain/InspectionReport.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/insreport/domain/InspectionReport.java
@@ -92,6 +92,8 @@ public class InspectionReport extends BaseEntity {
private String filePath;
private String taskId;
private String taskResultId;
+ /** 位置(省份 + 地市拼接,如 四川省成都市) */
+ private String location;
private String areaName;
private String areaType;
private String deviceType;
@@ -118,12 +120,12 @@ public class InspectionReport extends BaseEntity {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
InspectionReport that = (InspectionReport) object;
- return Objects.equals(lineId, that.lineId) && Objects.equals(stationName, that.stationName) && Objects.equals(voltLevel, that.voltLevel) && Objects.equals(stationType, that.stationType) && Objects.equals(inspectionDate, that.inspectionDate) && Objects.equals(inspectionTaskName, that.inspectionTaskName) && Objects.equals(envInfo, that.envInfo) && Objects.equals(inspectionStartTime, that.inspectionStartTime) && Objects.equals(inspectionEndTime, that.inspectionEndTime) && Objects.equals(patrolStatistics, that.patrolStatistics) && Objects.equals(checkPerson, that.checkPerson) && Objects.equals(checkTime, that.checkTime) && Objects.equals(description, that.description) && Objects.equals(filePath, that.filePath) && Objects.equals(taskId, that.taskId) && Objects.equals(taskResultId, that.taskResultId) && Objects.equals(areaName, that.areaName) && Objects.equals(areaType, that.areaType) && Objects.equals(deviceType, that.deviceType) && Objects.equals(type, that.type) && Objects.equals(cycleStartTime, that.cycleStartTime) && Objects.equals(cycleEndTime, that.cycleEndTime) && Objects.equals(taskPatrolledId, that.taskPatrolledId) && Objects.equals(ReportDatalist, that.ReportDatalist) && Objects.equals(info, that.info);
+ return Objects.equals(lineId, that.lineId) && Objects.equals(stationName, that.stationName) && Objects.equals(voltLevel, that.voltLevel) && Objects.equals(stationType, that.stationType) && Objects.equals(inspectionDate, that.inspectionDate) && Objects.equals(inspectionTaskName, that.inspectionTaskName) && Objects.equals(envInfo, that.envInfo) && Objects.equals(inspectionStartTime, that.inspectionStartTime) && Objects.equals(inspectionEndTime, that.inspectionEndTime) && Objects.equals(patrolStatistics, that.patrolStatistics) && Objects.equals(checkPerson, that.checkPerson) && Objects.equals(checkTime, that.checkTime) && Objects.equals(description, that.description) && Objects.equals(filePath, that.filePath) && Objects.equals(taskId, that.taskId) && Objects.equals(taskResultId, that.taskResultId) && Objects.equals(location, that.location) && Objects.equals(areaName, that.areaName) && Objects.equals(areaType, that.areaType) && Objects.equals(deviceType, that.deviceType) && Objects.equals(type, that.type) && Objects.equals(cycleStartTime, that.cycleStartTime) && Objects.equals(cycleEndTime, that.cycleEndTime) && Objects.equals(taskPatrolledId, that.taskPatrolledId) && Objects.equals(ReportDatalist, that.ReportDatalist) && Objects.equals(info, that.info);
}
@Override
public int hashCode() {
- return Objects.hash(lineId, stationName, voltLevel, stationType, inspectionDate, inspectionTaskName, envInfo, inspectionStartTime, inspectionEndTime, patrolStatistics, checkPerson, checkTime, description, filePath, taskId, taskResultId, areaName, areaType, deviceType, type, cycleStartTime, cycleEndTime, taskPatrolledId, ReportDatalist, info);
+ return Objects.hash(lineId, stationName, voltLevel, stationType, inspectionDate, inspectionTaskName, envInfo, inspectionStartTime, inspectionEndTime, patrolStatistics, checkPerson, checkTime, description, filePath, taskId, taskResultId, location, areaName, areaType, deviceType, type, cycleStartTime, cycleEndTime, taskPatrolledId, ReportDatalist, info);
}
@Override
@@ -145,6 +147,7 @@ public class InspectionReport extends BaseEntity {
", filePath='" + filePath + '\'' +
", taskId='" + taskId + '\'' +
", taskResultId='" + taskResultId + '\'' +
+ ", location='" + location + '\'' +
", areaName='" + areaName + '\'' +
", areaType='" + areaType + '\'' +
", deviceType='" + deviceType + '\'' +
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/insreportdata/domain/InspectionReportData.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/insreportdata/domain/InspectionReportData.java
index 3a40037..3597643 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/insreportdata/domain/InspectionReportData.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/insreportdata/domain/InspectionReportData.java
@@ -58,6 +58,11 @@ public class InspectionReportData extends BaseEntity {
name = "巡视结果"
)
private String inspectionResults;
+
+ @Excel(
+ name = "巡视读数"
+ )
+ private String inspectionValue;
@Excel(
name = "点位状态:正常、异常、待人工确 认三种状态"
)
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/message/MessageUtils.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/message/MessageUtils.java
index 31339bc..7b89677 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/message/MessageUtils.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/message/MessageUtils.java
@@ -235,6 +235,8 @@ public class MessageUtils {
return "summary";
case "缺陷":
return "defect";
+ case "本次巡检未发现缺陷项":
+ return "No defects were found during this inspection.";
case"分析失败":
return "analysis failed";
default:
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/mapper/PatrolResultMapper.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/mapper/PatrolResultMapper.java
index f8ae020..97e1f68 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/mapper/PatrolResultMapper.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/mapper/PatrolResultMapper.java
@@ -1,6 +1,7 @@
package com.inspect.partrolresult.mapper;
import com.inspect.analysis.domain.ResultAnalysis;
+import com.inspect.analysis.domain.DefectListQuery;
import com.inspect.partrolresult.domain.*;
import com.inspect.statistics.domain.TaskStatisticsConfig;
import com.inspect.system.base.openDomain.BasedataEqpBookMoMain;
@@ -34,6 +35,8 @@ public interface PatrolResultMapper {
List selectPatrolDataResultByTaskCodeV2(ResultAnalysis resultAnalysis);
+ List selectDefectList(DefectListQuery query);
+
List selectPatrolDataResultByTaskCodeList(PatrolTaskStatus patrolTaskStatus);
List selectBasedataEqpBookList(BasedataEqpBookMoMain basedataEqpBookMoMain);
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/service/IPatrolResultService.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/service/IPatrolResultService.java
index ee14a13..5b3a40c 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/service/IPatrolResultService.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/service/IPatrolResultService.java
@@ -1,6 +1,7 @@
package com.inspect.partrolresult.service;
import com.inspect.analysis.domain.ResultAnalysis;
+import com.inspect.analysis.domain.DefectListQuery;
import com.inspect.insreport.domain.InspectionReport;
import com.inspect.partrolresult.domain.*;
import com.inspect.resultmain.domain.PatrolTaskResultMain;
@@ -30,6 +31,8 @@ public interface IPatrolResultService {
List selectPatrolDataResultByTaskCodeV2(ResultAnalysis resultAnalysis);
+ List selectDefectList(DefectListQuery query);
+
List selectPatrolDataResultByTaskCodeList(PatrolTaskStatus patrolTaskStatus);
List selectBasedataEqpBookList(BasedataEqpBookMoMain basedataEqpbookMomain);
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/service/impl/PatrolResultServiceImpl.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/service/impl/PatrolResultServiceImpl.java
index e7de160..9e52a3f 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/service/impl/PatrolResultServiceImpl.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/partrolresult/service/impl/PatrolResultServiceImpl.java
@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.inspect.analysis.domain.AlgValue;
import com.inspect.analysis.domain.AnalyseResPoint;
+import com.inspect.analysis.domain.DefectListQuery;
import com.inspect.analysis.domain.ResultAnalysis;
import com.inspect.analysis.mapper.ResultAnalysisMapper;
import com.inspect.analysis.utils.ResultAnalysisUtils;
@@ -147,6 +148,11 @@ public class PatrolResultServiceImpl implements IPatrolResultService {
return this.patrolResultMapper.selectPatrolDataResultByTaskCodeV3(resultAnalysis);
}
+ @Override
+ public List selectDefectList(DefectListQuery query) {
+ return patrolResultMapper.selectDefectList(query);
+ }
+
public List selectPatrolDataResultByTaskCodeList(PatrolTaskStatus patrolTaskStatus) {
return patrolResultMapper.selectPatrolDataResultByTaskCodeList(patrolTaskStatus);
}
@@ -528,6 +534,8 @@ public class PatrolResultServiceImpl implements IPatrolResultService {
String stationName = stationMap.get("station_name");
String stationType = stationMap.get("station_type");
String voltLevel = stationMap.get("volt_level");
+ String cityName = stationMap.get("city_name"); // 地市
+ String provinceName = stationMap.get("province_name"); // 省份
startTime = PrintUtil.useTime("导出报告:查询站点信息", startTime);
AjaxResult ajaxResult = feignBasedataAreaService.evnList();
@@ -778,6 +786,8 @@ public class PatrolResultServiceImpl implements IPatrolResultService {
report.setVoltLevel(StringUtils.isEmpty(voltLevel) ? voltage : voltLevel);
report.setFilter("0");
report.setStationType(stationType);
+ // 位置 = 省份 + 地市(如 四川省成都市)
+ report.setLocation(buildLocation(provinceName, cityName));
inspectionReportMapper.insertInspectionReport(report);
// 为每个报告生成数据
batchInsertReportData_shaoxing(String.valueOf(report.getLineId()), patrolResultRefs, basePointAreaInfoList, lineIds);
@@ -791,6 +801,18 @@ public class PatrolResultServiceImpl implements IPatrolResultService {
return reportIds;
}
+ /** 拼接报告位置:省份 + 地市(如 四川省成都市;缺省时只显示存在的部分) */
+ private String buildLocation(String provinceName, String cityName) {
+ StringBuilder sb = new StringBuilder();
+ if (StringUtils.isNotEmpty(provinceName)) {
+ sb.append(provinceName);
+ }
+ if (StringUtils.isNotEmpty(cityName)) {
+ sb.append(cityName);
+ }
+ return sb.toString();
+ }
+
@Transactional
public List saveReportV2(PatrolTaskResultMain resultMain, List resultList) {
long startTime = System.currentTimeMillis();
@@ -1321,31 +1343,33 @@ public class PatrolResultServiceImpl implements IPatrolResultService {
reportData.setTaskName(resultTaskName);
reportData.setTaskPatrolledId(taskPatrolledId);
reportData.setTaskResultId(String.valueOf(mainId));
- if ("1".equals(patrolResultRef.getResultType()) && validAlgTypes.contains(patrolResultRef.getAlgType())) {
- reportData.setInspectionResults(patrolResultRef.getValue());
- } else {
- reportData.setInspectionResults(patrolResultRef.getDescription());
- }
+// if ("1".equals(patrolResultRef.getResultType()) && validAlgTypes.contains(patrolResultRef.getAlgType())) {
+// reportData.setInspectionResults(patrolResultRef.getValue());
+// } else {
+// reportData.setInspectionResults(patrolResultRef.getDescription());
+// }
+ reportData.setInspectionResults(patrolResultRef.getDescription());
+ reportData.setInspectionValue(patrolResultRef.getValue());
reportData.setAlgName(patrolResultRef.getAlgName());
- String threshold = patrolResultRef.getThreshold();
- if (StringUtils.isBlank(threshold)) {
- threshold = "";
- }
- if ("1".equals(patrolResultRef.getResultType()) && threshold.contains("sub_53")) {
- reportData.setInspectionResults(patrolResultRef.getValue());
- }
-
- if ("1".equals(patrolResultRef.getResultType()) && (threshold.contains("sub_52") || threshold.contains("sub_53"))
- && !"0".equals(patrolResultRef.getValue())
- && !"1".equals(patrolResultRef.getValue())
- && !"-1".equals(patrolResultRef.getValue())
- && !"check image".equals(patrolResultRef.getValue())) {
- reportData.setInspectionResults(patrolResultRef.getValue());
- }
+// String threshold = patrolResultRef.getThreshold();
+// if (StringUtils.isBlank(threshold)) {
+// threshold = "";
+// }
+// if ("1".equals(patrolResultRef.getResultType()) && threshold.contains("sub_53")) {
+// reportData.setInspectionResults(patrolResultRef.getValue());
+// }
+//
+// if ("1".equals(patrolResultRef.getResultType()) && (threshold.contains("sub_52") || threshold.contains("sub_53"))
+// && !"0".equals(patrolResultRef.getValue())
+// && !"1".equals(patrolResultRef.getValue())
+// && !"-1".equals(patrolResultRef.getValue())
+// && !"check image".equals(patrolResultRef.getValue())) {
+// reportData.setInspectionResults(patrolResultRef.getValue());
+// }
// 给读数增加单位
- String resValue = resultAnalysisUtils.addUnit2Value(reportData.getInspectionResults(), patrolResultRef.getAlgType(), patrolResultRef.getUnit());
- reportData.setInspectionResults(resValue);
+ String resValue = resultAnalysisUtils.addUnit2Value(reportData.getInspectionValue(), patrolResultRef.getAlgType(), patrolResultRef.getUnit());
+ reportData.setInspectionValue(resValue);
String pointStatus = patrolResultRef.getPointStatus();
String resultType = patrolResultRef.getResultType();
@@ -1395,16 +1419,20 @@ public class PatrolResultServiceImpl implements IPatrolResultService {
//List results = JSONObject.parseObject(resultContent).getJSONArray("resultList").toJavaList(resultList.class);
//String imgUrl = results.get(0).getResults().get(0).getResImageUrl();
AnalyseResPoint analyseResPoint = JSONObject.parseObject(resultContent, AnalyseResPoint.class);
- String imgUrl = analyseResPoint.getResImageUrl();
+ String resImageUrl = analyseResPoint.getResImageUrl();
+ String imageNormalUrlPath = analyseResPoint.getImageNormalUrlPath();
- if (StringUtils.isEmpty(imgUrl)) {
- imgUrl = patrolResultRef.getFilePath().split(StringUtils.COMMA)[0];
+ if (StringUtils.isEmpty(resImageUrl)) {
+ resImageUrl = patrolResultRef.getFilePath().split(StringUtils.COMMA)[0];
+ }
+ if (StringUtils.isEmpty(imageNormalUrlPath)) {
+ imageNormalUrlPath = patrolResultRef.getFilePath().split(StringUtils.COMMA)[0];
}
-
InspectionReportImg reportImg = new InspectionReportImg();
reportImg.setReportInfoId(reportDataId);
- reportImg.setImg(imgUrl);
- reportImg.setImgSrc(imgUrl);
+ reportImg.setImg(resImageUrl);
+ reportImg.setImgSrc(patrolResultRef.getFilePath().split(StringUtils.COMMA)[0]);
+ reportImg.setImageNormalUrlPath(imageNormalUrlPath);
reportImg.setImgType("0");
reportImg.setCreatTime(new Date());
reportImages.add(reportImg);
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportfont/ReportFontEmbedder.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportfont/ReportFontEmbedder.java
new file mode 100644
index 0000000..bc6034b
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportfont/ReportFontEmbedder.java
@@ -0,0 +1,281 @@
+package com.inspect.reportfont;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ooxml.POIXMLDocumentPart;
+import org.apache.poi.openxml4j.opc.OPCPackage;
+import org.apache.poi.openxml4j.opc.PackagePart;
+import org.apache.poi.openxml4j.opc.PackageRelationship;
+import org.apache.poi.openxml4j.opc.PackagingURIHelper;
+import org.apache.poi.openxml4j.opc.TargetMode;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFSettings;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTLanguage;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSettings;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 动环巡检报告 Word 字体嵌入。
+ *
+ * 原理(Word 的"嵌入字体"机制,让查看方电脑无需安装字体):
+ *
+ * - docx 包内的字体"注册表"是 {@code word/fontTable.xml}:按字体名(w:name)列出字体,
+ * 每个条目通过 {@code embedRegular/embedBold} 属性(r:id)引用一个字体部件;
+ * - 字体部件({@code word/fonts/*.odttf})是混淆后的字体数据——按 OOXML §17.8.1,
+ * 字体前 32 字节与混淆 key 异或,key 由条目中的 {@code fontKey}(GUID)推导;
+ * Word 打开文档时按同样算法还原后使用;
+ * - 正文 run 只写字体名(w:eastAsia),Word 按名字查 fontTable 命中嵌入字体。
+ *
+ * 实现要点:
+ *
+ * - {@code fontTable.xml} 直接使用 Word 自身生成的模板(fontKey 固定),
+ * 兼容性最稳(实测动态生成的 fontTable Word 不识别嵌入字体);
+ * - 字体部件按模板固定的 font1..font5 命名,混淆 key 与模板条目一一对应,
+ * 且按序 addRelationship 保证 rId 映射正确。
+ *
+ */
+@Slf4j
+@Component
+public class ReportFontEmbedder {
+
+ // ============================ 部件/关系类型常量 ============================
+ private static final String REL_FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font";
+ private static final String REL_FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
+ private static final String REL_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
+ private static final String REL_THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
+ private static final String REL_WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings";
+
+ private static final String CT_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml";
+ private static final String CT_FONT = "application/vnd.openxmlformats-officedocument.obfuscatedFont";
+ private static final String CT_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml";
+ private static final String CT_THEME = "application/vnd.openxmlformats-officedocument.theme+xml";
+ private static final String CT_WEB_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml";
+
+ /** 模板文件所在 classpath 目录 */
+ private static final String TEMPLATE_DIR = "report/wordparts/";
+
+ /**
+ * 报告用到的 5 个嵌入字体部件(与 fontTable 模板的条目一一对应):
+ * 部件名 -> {字体文件, 混淆 key}。字体文件位于 classpath fonts/ 或外部目录。
+ * 注意:黑体在模板中有 regular/bold/italic 三个样式条目,均引用同一份黑体;
+ * 方正小标宋、仿宋各只嵌 bold(模板如此,标题/表头均加粗使用)。
+ */
+ private static final List EMBEDDED_FONTS = Arrays.asList(
+ new EmbeddedFont("font1", "simhei.ttf", "DAF739B1-18F1-4906-820A-C106608C2B73"),
+ new EmbeddedFont("font2", "simhei.ttf", "3039BACA-5140-4C4C-8AFB-F7B61E374105"),
+ new EmbeddedFont("font3", "simhei.ttf", "73410ACE-E9FA-46ED-BA3D-9ECB6C9FEF4E"),
+ new EmbeddedFont("font4", "方正小标宋简体.ttf", "4D5F1B72-2D28-435A-B7BC-848B95F30EBB"),
+ new EmbeddedFont("font5", "simfang.ttf", "818E8C25-A7E2-4610-9234-360D3B3511E5"));
+
+ /**
+ * 外部字体目录({@code report.donghuan.fonts-dir}),
+ * 留空则从 classpath {@code fonts/} 加载(随 jar 打包)。
+ */
+ @Value("${report.donghuan.fonts-dir:}")
+ private String fontsDir;
+
+ /** 一个待嵌入字体:部件名 + 字体文件 + 混淆 key(fontKey) */
+ private static final class EmbeddedFont {
+ final String partName;
+ final String fileName;
+ final String fontKey;
+
+ EmbeddedFont(String partName, String fileName, String fontKey) {
+ this.partName = partName;
+ this.fileName = fileName;
+ this.fontKey = fontKey;
+ }
+ }
+
+ /**
+ * 把报告所需字体嵌入 docx(在生成正文内容前调用)。
+ * 字体缺失时不阻断归档,报告退化为不带嵌入字体。
+ */
+ public void embed(XWPFDocument doc) {
+ try {
+ OPCPackage pkg = doc.getPackage();
+ PackagePart documentPart = pkg.getPart(PackagingURIHelper.createPartName("/word/document.xml"));
+
+ // 1. fontTable.xml(静态模板,含字体注册信息)
+ addPart(pkg, "/word/fontTable.xml", CT_FONT_TABLE, "fontTable.xml");
+ // 2. 字体部件 font1..font5.odttf,关系挂到 fontTable 上(rId 按序分配,与模板一致)
+ embedFontParts(pkg, pkg.getPart(PackagingURIHelper.createPartName("/word/fontTable.xml")));
+ // 3. 补齐 Word 常规部件(styles/theme/webSettings,缺了 Word 不解析嵌入字体)
+ addPart(pkg, "/word/styles.xml", CT_STYLES, "styles.xml");
+ addPart(pkg, "/word/theme/theme1.xml", CT_THEME, "theme1.xml");
+ addPart(pkg, "/word/webSettings.xml", CT_WEB_SETTINGS, "webSettings.xml");
+ // 4. document.xml 引用 fontTable/styles/theme/webSettings
+ addDocumentRelations(documentPart, pkg);
+ // 5. settings.xml 写入嵌入字体标志
+ addSettingsFlags(doc);
+ log.info("[FONT_EMBED] 报告字体嵌入完成");
+ } catch (Exception e) {
+ log.error("[FONT_EMBED] 字体嵌入失败,本次报告不带嵌入字体", e);
+ }
+ }
+
+ /** 从模板目录创建部件并写入内容,同时挂到 document.xml */
+ private PackagePart addPart(OPCPackage pkg, String partName, String contentType, String templateName) throws Exception {
+ PackagePart part = pkg.createPart(PackagingURIHelper.createPartName(partName), contentType);
+ byte[] bytes = readTemplate(templateName);
+ if (bytes != null) {
+ try (OutputStream os = part.getOutputStream()) {
+ os.write(bytes);
+ }
+ }
+ log.info("[FONT_EMBED] 已创建部件: {} ({} bytes)", partName, bytes == null ? 0 : bytes.length);
+ return part;
+ }
+
+ /** 把 document.xml 与 fontTable/styles/theme/webSettings 建立关系 */
+ private void addDocumentRelations(PackagePart documentPart, OPCPackage pkg) throws Exception {
+ documentPart.addRelationship(
+ PackagingURIHelper.createPartName("/word/fontTable.xml"), TargetMode.INTERNAL, REL_FONT_TABLE);
+ documentPart.addRelationship(
+ PackagingURIHelper.createPartName("/word/styles.xml"), TargetMode.INTERNAL, REL_STYLES);
+ documentPart.addRelationship(
+ PackagingURIHelper.createPartName("/word/theme/theme1.xml"), TargetMode.INTERNAL, REL_THEME);
+ documentPart.addRelationship(
+ PackagingURIHelper.createPartName("/word/webSettings.xml"), TargetMode.INTERNAL, REL_WEB_SETTINGS);
+ }
+
+ /**
+ * 依次创建 font1..font5.odttf 部件(完整字体按固定 key 混淆),
+ * 并把 font 关系挂到 fontTable 部件(addRelationship 按序生成 rId1..rId5,
+ * 与静态模板中的引用一一对应)。
+ */
+ private void embedFontParts(OPCPackage pkg, PackagePart fontTablePart) throws Exception {
+ for (EmbeddedFont font : EMBEDDED_FONTS) {
+ byte[] fontBytes = loadFontBytes(font.fileName);
+ if (fontBytes == null) {
+ log.warn("[FONT_EMBED] 字体文件缺失,跳过部件 {}: {}", font.partName, font.fileName);
+ continue;
+ }
+ PackagePart fontPart = pkg.createPart(
+ PackagingURIHelper.createPartName("/word/fonts/" + font.partName + ".odttf"), CT_FONT);
+ try (OutputStream os = fontPart.getOutputStream()) {
+ os.write(obfuscateFont(fontBytes, font.fontKey));
+ }
+ fontTablePart.addRelationship(fontPart.getPartName(), TargetMode.INTERNAL, REL_FONT);
+ log.info("[FONT_EMBED] 已嵌入字体部件: {} <- {} ({} bytes)", font.partName, font.fileName, fontBytes.length);
+ }
+ }
+
+ /**
+ * 在 settings.xml 写入嵌入字体标志,Word 靠它识别"文档带嵌入字体":
+ * {@code }、{@code }、
+ * {@code }。
+ *
+ * 注意:POI 4.1.2 保存 settings 时从内部缓存的 CTSettings 写回(直接改部件内容会被覆盖),
+ * 且不能整体替换缓存对象(类型不兼容),因此反射取出缓存的 CTSettings 用其 API 添加标志。
+ */
+ private void addSettingsFlags(XWPFDocument doc) {
+ try {
+ for (POIXMLDocumentPart part : doc.getRelations()) {
+ if (part instanceof XWPFSettings) {
+ java.lang.reflect.Field ctField = XWPFSettings.class.getDeclaredField("ctSettings");
+ ctField.setAccessible(true);
+ CTSettings ctSettings = (CTSettings) ctField.get(part);
+ if (!ctSettings.isSetEmbedTrueTypeFonts()) {
+ ctSettings.addNewEmbedTrueTypeFonts();
+ }
+ if (!ctSettings.isSetSaveSubsetFonts()) {
+ ctSettings.addNewSaveSubsetFonts();
+ }
+ if (!ctSettings.isSetThemeFontLang()) {
+ CTLanguage lang = ctSettings.addNewThemeFontLang();
+ lang.setVal("en-US");
+ lang.setEastAsia("zh-CN");
+ }
+ log.info("[FONT_EMBED] settings.xml 已写入嵌入字体标志");
+ return;
+ }
+ }
+ log.warn("[FONT_EMBED] 未找到 settings 部件,跳过嵌入字体标志");
+ } catch (Exception e) {
+ log.warn("[FONT_EMBED] 写入 settings.xml 嵌入字体标志失败", e);
+ }
+ }
+
+ /** 读取 classpath 模板内容(report/wordparts/) */
+ private byte[] readTemplate(String name) {
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream(TEMPLATE_DIR + name)) {
+ if (is == null) {
+ log.warn("[FONT_EMBED] 模板缺失: {}", TEMPLATE_DIR + name);
+ return null;
+ }
+ return readAll(is);
+ } catch (Exception e) {
+ log.warn("[FONT_EMBED] 读取模板失败: {}", name, e);
+ return null;
+ }
+ }
+
+ /**
+ * 加载字体文件:优先外部目录(report.donghuan.fonts-dir),
+ * 其次 classpath {@code fonts/}(随 jar 打包)。
+ */
+ private byte[] loadFontBytes(String fileName) {
+ try {
+ if (fontsDir != null && !fontsDir.isEmpty()) {
+ Path fontPath = Paths.get(fontsDir, fileName);
+ if (Files.exists(fontPath)) {
+ return Files.readAllBytes(fontPath);
+ }
+ }
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream("fonts/" + fileName)) {
+ if (is != null) {
+ return readAll(is);
+ }
+ }
+ log.warn("[FONT_EMBED] 字体文件不存在: {} (fontsDir={}, classpath=fonts/)", fileName, fontsDir);
+ } catch (Exception e) {
+ log.warn("[FONT_EMBED] 读取字体文件失败: {}", fileName, e);
+ }
+ return null;
+ }
+
+ /**
+ * OOXML §17.8.1 字体混淆(ODTTF):字体前 32 字节与混淆 key 异或。
+ * key 由 fontKey(GUID 去掉横线、32 位十六进制)每 2 字符转 1 字节后整体反转得到。
+ * Word 打开文档时按同样算法还原字体。
+ */
+ private byte[] obfuscateFont(byte[] fontBytes, String fontKey) {
+ byte[] out = fontBytes.clone();
+ byte[] key = buildObfuscationKey(fontKey);
+ int len = Math.min(32, out.length);
+ for (int i = 0; i < len; i++) {
+ out[i] ^= key[i % 16];
+ }
+ return out;
+ }
+
+ /** 由 fontKey(GUID)生成 16 字节混淆 key:十六进制转字节后整体反转 */
+ private byte[] buildObfuscationKey(String fontKey) {
+ String hex = fontKey.replace("-", "");
+ byte[] key = new byte[16];
+ for (int i = 0; i < 16; i++) {
+ key[i] = (byte) Integer.parseInt(hex.substring((15 - i) * 2, (16 - i) * 2), 16);
+ }
+ return key;
+ }
+
+ private byte[] readAll(InputStream is) throws java.io.IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ byte[] buf = new byte[8192];
+ int n;
+ while ((n = is.read(buf)) != -1) {
+ bos.write(buf, 0, n);
+ }
+ return bos.toByteArray();
+ }
+}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/config/ReportPushProperties.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/config/ReportPushProperties.java
new file mode 100644
index 0000000..c250944
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/config/ReportPushProperties.java
@@ -0,0 +1,43 @@
+package com.inspect.reportpush.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 任务巡检完成自动归档 + 生成报告 + websocket 推送配置
+ *
+ * 对应 application.yml 中 {@code inspect.report-push.*} 配置块
+ * (可在 nacos 配置中心按现场覆盖)。
+ *
+ * @author inspect
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "inspect.report-push")
+public class ReportPushProperties {
+
+ /** 是否启用自动归档 + 推送 */
+ private boolean enabled = true;
+
+ /** 自动归档审核人 */
+ private String checkPerson = "admin";
+
+ /** 自动归档巡视结论 */
+ private String taskResult = "巡检完成,自动归档";
+
+ /** 任务状态更新事务提交等待时间(毫秒) */
+ private long afterCommitDelayMillis = 2000;
+
+ /** 归档防重锁过期时间(秒) */
+ private long lockExpireSeconds = 86400;
+
+ /** 上传报告的场站ID(为空时取 basedata_station.station_no) */
+ private String stationId = "";
+
+ /** 上传报告的巡检类型(风机巡检/光伏巡检/线路巡检/山火巡检/机器人巡检) */
+ private String inspectType = "动环智能巡检";
+
+ /** 上传报告的巡检设备名称(机库名称、机器人名称、摄像头名称,可选) */
+ private String inspectDeviceName = "";
+}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/controller/ReportPushController.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/controller/ReportPushController.java
new file mode 100644
index 0000000..6cd7215
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/controller/ReportPushController.java
@@ -0,0 +1,64 @@
+package com.inspect.reportpush.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.inspect.api.service.ReportUploadService;
+import com.inspect.base.core.utils.DateUtils;
+import com.inspect.base.core.utils.StringUtils;
+import com.inspect.base.core.web.controller.BaseController;
+import com.inspect.base.core.web.domain.AjaxResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+/**
+ * 报告推送调试接口(对接外部平台-盐源新能源智慧感知平台《巡检报告接口文档》)
+ *
+ * - GET /reportpush/getToken 获取 Joinbright-Token(复用 ExtAuthService,验证认证连通性)
+ * - GET /reportpush/test 上传一条测试报告(验证 /uploadReport 上传接口连通性)
+ *
+ */
+@Slf4j
+@RestController
+@RequestMapping("/reportpush")
+public class ReportPushController extends BaseController {
+
+ @Autowired
+ private ReportUploadService reportUploadService;
+
+ /**
+ * 获取 Joinbright-Token(调试用)
+ */
+ @GetMapping("/getToken")
+ public AjaxResult getToken() {
+ String token = reportUploadService.getJoinbrightToken();
+ return StringUtils.isNotEmpty(token) ? AjaxResult.success(token) : AjaxResult.error("获取Joinbright-Token失败");
+ }
+
+ /**
+ * 上传测试报告(调试用)
+ */
+ @GetMapping("/test")
+ public AjaxResult test(String message) {
+ byte[] dummyFile = "inspect report upload test".getBytes(StandardCharsets.UTF_8);
+ String now = DateUtils.parseDateToStr(DateUtils.yyyyMMddHHmmss2, new Date());
+ JSONObject reportInfo = new JSONObject();
+ reportInfo.put("id", "TEST-" + System.currentTimeMillis());
+ reportInfo.put("name", "测试报告");
+ reportInfo.put("fileType", "docx");
+ reportInfo.put("stationId", "");
+ reportInfo.put("stationName", "测试电站");
+ reportInfo.put("inspectTime", now);
+ reportInfo.put("createTime", now);
+ reportInfo.put("inspectType", "机器人巡检");
+ reportInfo.put("inspectContent", StringUtils.isEmpty(message) ? "巡检报告上传接口联调测试" : message);
+ reportInfo.put("taskId", "TEST");
+ reportInfo.put("taskName", "测试任务");
+ boolean ok = reportUploadService.uploadReport(dummyFile, "test_report.docx", reportInfo);
+ return ok ? AjaxResult.success("上传成功") : AjaxResult.error("上传失败, 请查看日志");
+ }
+}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportAutoArchiveService.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportAutoArchiveService.java
new file mode 100644
index 0000000..b58b19e
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportAutoArchiveService.java
@@ -0,0 +1,190 @@
+package com.inspect.reportpush.service;
+
+import com.inspect.base.core.constant.RedisConst;
+import com.inspect.base.core.enums.TaskStatus;
+import com.inspect.base.core.enums.TaskType;
+import com.inspect.base.core.utils.StringUtils;
+import com.inspect.base.redis.service.RedisService;
+import com.inspect.analysis.domain.ResultAnalysis;
+import com.inspect.analysis.service.IResultAnalysisService;
+import com.inspect.reportpush.config.ReportPushProperties;
+import com.inspect.resultmain.controller.DonghuanReportExportController;
+import com.inspect.resultmain.domain.PatrolTaskResultMain;
+import com.inspect.resultmain.service.IPatrolTaskResultMainService;
+import com.inspect.taskstatus.domain.PatrolTaskStatus;
+import com.inspect.taskstatus.service.IPatrolTaskStatusService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 任务巡检完成 → 自动归档 + 生成报告(动环 Word) → websocket 推送
+ *
+ * 由 {@code TaskStatusChangeAspect} 在任务状态变更为「已完成(1)」时异步触发,
+ * 仅处理主任务/普通任务(子任务由主任务统一归档),
+ * 通过 Redis 防重锁避免重复归档。
+ *
+ * @author inspect
+ */
+@Slf4j
+@Service
+public class ReportAutoArchiveService {
+
+ private final ReportPushProperties properties;
+ private final RedisService redisService;
+ private final IPatrolTaskStatusService patrolTaskStatusService;
+ private final IPatrolTaskResultMainService patrolTaskResultMainService;
+ private final IResultAnalysisService resultAnalysisService;
+ private final DonghuanReportExportController donghuanReportExportController;
+
+ public ReportAutoArchiveService(ReportPushProperties properties,
+ RedisService redisService,
+ IPatrolTaskStatusService patrolTaskStatusService,
+ IPatrolTaskResultMainService patrolTaskResultMainService,
+ IResultAnalysisService resultAnalysisService,
+ DonghuanReportExportController donghuanReportExportController) {
+ this.properties = properties;
+ this.redisService = redisService;
+ this.patrolTaskStatusService = patrolTaskStatusService;
+ this.patrolTaskResultMainService = patrolTaskResultMainService;
+ this.resultAnalysisService = resultAnalysisService;
+ this.donghuanReportExportController = donghuanReportExportController;
+ }
+
+ /**
+ * 任务状态变更为已完成时触发(异步)。
+ *
+ * @param taskStatus 变更后的任务状态(切面入参)
+ */
+ @Async
+ public void onTaskCompleted(PatrolTaskStatus taskStatus) {
+ try {
+ if (!properties.isEnabled()) {
+ log.info("[AUTO_ARCHIVE] 未启用 inspect.report-push.enabled,跳过自动归档: taskPatrolledId={}",
+ taskStatus == null ? null : taskStatus.getTaskPatrolledId());
+ return;
+ }
+ if (taskStatus == null || StringUtils.isEmpty(taskStatus.getTaskPatrolledId())) {
+ log.warn("[AUTO_ARCHIVE] 任务状态参数为空,跳过自动归档");
+ return;
+ }
+ // 子任务不单独归档,由主任务统一归档
+ if (TaskType.SUB.getCode().equals(taskStatus.getTaskType())) {
+ log.debug("[AUTO_ARCHIVE] 子任务不触发归档: {}", taskStatus.getTaskPatrolledId());
+ return;
+ }
+
+ // 归档单元:主任务(联合任务)或普通任务自身
+ PatrolTaskStatus mainTask = patrolTaskStatusService.selectPatrolTaskStatusMain(taskStatus.getTaskPatrolledId());
+ String archiveTaskPatrolledId = mainTask != null
+ ? mainTask.getTaskPatrolledId() : taskStatus.getTaskPatrolledId();
+ if (StringUtils.isEmpty(archiveTaskPatrolledId)) {
+ log.warn("[AUTO_ARCHIVE] 无法确定归档任务ID,跳过: {}", taskStatus.getTaskPatrolledId());
+ return;
+ }
+
+ // 防重锁:同一归档单元只允许一次自动归档
+ String lockKey = RedisConst.REPORT_PUSH_LOCK + archiveTaskPatrolledId;
+ Boolean locked = redisService.redisTemplate.opsForValue()
+ .setIfAbsent(lockKey, archiveTaskPatrolledId,
+ properties.getLockExpireSeconds(), TimeUnit.SECONDS);
+ if (!Boolean.TRUE.equals(locked)) {
+ log.info("[AUTO_ARCHIVE] 任务 {} 已在归档/推送中(防重锁),跳过", archiveTaskPatrolledId);
+ return;
+ }
+ log.info("[AUTO_ARCHIVE] 获取归档防重锁: {}", lockKey);
+
+ // 等待任务状态事务提交、结果数据落库
+ long waitMillis = properties.getAfterCommitDelayMillis();
+ if (waitMillis > 0) {
+ try {
+ Thread.sleep(waitMillis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ releaseLock(lockKey);
+ return;
+ }
+ }
+
+ // 二次校验:任务仍为已完成状态
+ PatrolTaskStatus freshTask = patrolTaskStatusService.selectPatrolTaskStatusByTaskPatrolledId(archiveTaskPatrolledId);
+ if (freshTask == null || !TaskStatus.DONE.getCode().equals(freshTask.getTaskState())) {
+ log.info("[AUTO_ARCHIVE] 任务 {} 当前状态 {},非已完成,跳过自动归档",
+ archiveTaskPatrolledId, freshTask == null ? "null" : freshTask.getTaskState());
+ releaseLock(lockKey);
+ return;
+ }
+
+ // 二次校验:已归档(file_status=1)的任务不重复归档
+ PatrolTaskResultMain existMain = patrolTaskResultMainService
+ .selectPatrolTaskResultMainByTaskPatrolledId(archiveTaskPatrolledId);
+ if (existMain != null && "1".equals(existMain.getFileStatus())) {
+ log.info("[AUTO_ARCHIVE] 任务 {} 已归档(fileStatus=1),跳过", archiveTaskPatrolledId);
+ releaseLock(lockKey);
+ return;
+ }
+
+ // 归档范围:主任务 + 其下所有子任务(联合任务)
+ List taskPatrolledIds = new ArrayList<>();
+ taskPatrolledIds.add(archiveTaskPatrolledId);
+ List subTasks = patrolTaskStatusService.selectPatrolTaskStatusListByMainId(archiveTaskPatrolledId);
+ if (subTasks != null) {
+ for (PatrolTaskStatus subTask : subTasks) {
+ if (StringUtils.isNotEmpty(subTask.getTaskPatrolledId())) {
+ taskPatrolledIds.add(subTask.getTaskPatrolledId());
+ }
+ }
+ }
+
+ // 触发归档(动环 Word 报告),报告生成后由 exportDonghuanReport 回调推送
+ PatrolTaskResultMain resultMain = new PatrolTaskResultMain();
+ resultMain.setTaskPatrolledIds(taskPatrolledIds);
+ resultMain.setCheckPerson(properties.getCheckPerson());
+ resultMain.setTaskResult(properties.getTaskResult());
+ log.info("[AUTO_ARCHIVE] 触发自动归档: taskPatrolledIds={}, checkPerson={}, taskResult={}",
+ taskPatrolledIds, resultMain.getCheckPerson(), resultMain.getTaskResult());
+ donghuanReportExportController.updateInfo_donghuan(resultMain);
+ } catch (Exception e) {
+ log.error("[AUTO_ARCHIVE] 自动归档异常: taskPatrolledId={}",
+ taskStatus == null ? null : taskStatus.getTaskPatrolledId(), e);
+ }
+ }
+
+ /**
+ * 校验任务(含子任务)是否存在缺陷数据(初筛缺陷 + 大模型缺陷)
+ */
+ private boolean hasDefect(List taskPatrolledIds) {
+ try {
+ Map abnormalMap = resultAnalysisService.selectTaskAbnormalDataBatch(taskPatrolledIds);
+ if (abnormalMap == null || abnormalMap.isEmpty()) {
+ return false;
+ }
+ int csQxNum = 0, aiQxNum = 0;
+ for (ResultAnalysis analysis : abnormalMap.values()) {
+ if (analysis == null) {
+ continue;
+ }
+ csQxNum += analysis.getCsQxNum();
+ aiQxNum += analysis.getAiQxNum();
+ }
+ return csQxNum + aiQxNum > 0;
+ } catch (Exception e) {
+ log.error("[AUTO_ARCHIVE] 查询缺陷数据异常,按存在缺陷处理: taskPatrolledIds={}", taskPatrolledIds, e);
+ return true;
+ }
+ }
+
+ private void releaseLock(String lockKey) {
+ try {
+ redisService.deleteObject(lockKey);
+ } catch (Exception e) {
+ log.warn("[AUTO_ARCHIVE] 释放防重锁失败: {}", lockKey, e);
+ }
+ }
+
+}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportPushService.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportPushService.java
new file mode 100644
index 0000000..75d741e
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportPushService.java
@@ -0,0 +1,148 @@
+package com.inspect.reportpush.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.inspect.api.domain.ReportPushMessage;
+import com.inspect.api.service.ReportPushPublisher;
+import com.inspect.base.core.utils.DateUtils;
+import com.inspect.base.core.utils.StringUtils;
+import com.inspect.insreport.domain.InspectionReport;
+import com.inspect.partrolresult.mapper.PatrolResultMapper;
+import com.inspect.reportpush.config.ReportPushProperties;
+import com.inspect.task.mapper.PatrolTaskMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 巡检报告推送服务(先上报 RabbitMQ,由 inspect-main-api 的
+ * {@link com.inspect.api.service.ReportUploadConsumer} 异步 HTTP 上传到外部平台,
+ * 见《盐源新能源智慧感知平台_巡检报告接口文档 v1.0.0》)。
+ *
+ * 报告归档(Word)生成完毕后,组装 reportInfo 并发布 report.upload.queue 消息,
+ * 消费者下载报告 docx 后 POST /jb-nessp-external/api/uploadReport 上传
+ * (multipart/form-data:file + reportInfo,请求头携带 joinbright-token)。
+ *
+ * @author inspect
+ */
+@Slf4j
+@Service
+public class ReportPushService {
+
+ private final ReportPushProperties properties;
+ private final ReportPushPublisher reportPushPublisher;
+ private final PatrolTaskMapper patrolTaskMapper;
+ private final PatrolResultMapper patrolResultMapper;
+
+ public ReportPushService(ReportPushProperties properties,
+ ReportPushPublisher reportPushPublisher,
+ PatrolTaskMapper patrolTaskMapper,
+ PatrolResultMapper patrolResultMapper) {
+ this.properties = properties;
+ this.reportPushPublisher = reportPushPublisher;
+ this.patrolTaskMapper = patrolTaskMapper;
+ this.patrolResultMapper = patrolResultMapper;
+ }
+
+ /**
+ * 报告生成并上传完成后推送。自动归档与手动归档均会触发。
+ *
+ * @param report 已保存 filePath 的巡检报告
+ */
+ public void pushReport(InspectionReport report) {
+ pushReport(report, null);
+ }
+
+ /**
+ * 报告生成并上传完成后推送(带发起人)。
+ *
+ * @param report 已保存 filePath 的巡检报告
+ * @param username 报告发起人(当前登录账号,可为空)
+ */
+ public void pushReport(InspectionReport report, String username) {
+ if (report == null) {
+ return;
+ }
+ if (!properties.isEnabled()) {
+ log.info("[REPORT_PUSH] 未启用 report-push 配置,跳过报告推送: reportId={}", report.getLineId());
+ return;
+ }
+ // 报告文件路径(动环导出保存的是 .docx,兼容 .xls 结尾路径)
+ String filePath = report.getFilePath();
+ if (StringUtils.isEmpty(filePath)) {
+ log.warn("[REPORT_PUSH] 报告文件路径为空, 无法推送: reportId={}", report.getLineId());
+ return;
+ }
+ String fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
+ // 报告元数据(《盐源新能源智慧感知平台_巡检报告接口文档_v1.0.0(2)》reportInfo 字段,发布时组装)
+ JSONObject reportInfo = new JSONObject();
+ reportInfo.put("id", report.getLineId() == null
+ ? String.valueOf(System.currentTimeMillis()) : String.valueOf(report.getLineId()));
+ reportInfo.put("name", StringUtils.isNotEmpty(fileName)
+ ? fileName : "巡检报告");
+ reportInfo.put("fileType", "docx");
+ reportInfo.put("stationId", resolveStationId());
+ reportInfo.put("stationName", resolveStationName(report));
+ reportInfo.put("inspectTime", report.getInspectionStartTime() == null ? null
+ : DateUtils.parseDateToStr(DateUtils.yyyyMMddHHmmss2, report.getInspectionStartTime()));
+ reportInfo.put("createTime", DateUtils.parseDateToStr(DateUtils.yyyyMMddHHmmss2, new Date()));
+ reportInfo.put("inspectType", resolveInspectType());
+ reportInfo.put("inspectDeviceName", properties.getInspectDeviceName());
+ reportInfo.put("inspectContent", StringUtils.isNotEmpty(report.getPatrolStatistics())
+ ? report.getPatrolStatistics() : report.getInspectionTaskName());
+ reportInfo.put("taskId", StringUtils.isNotEmpty(report.getTaskId()) ? report.getTaskId() : "");
+ reportInfo.put("taskName", report.getInspectionTaskName());
+ // 报告发起人:归档入口捕获的当前登录账号,为空时取配置审核人
+ reportInfo.put("username", StringUtils.isNotEmpty(username) ? username : properties.getCheckPerson());
+
+ ReportPushMessage message = new ReportPushMessage();
+ message.setReportId(report.getLineId());
+ message.setFilePath(filePath);
+ message.setReportInfo(reportInfo.toJSONString());
+ reportPushPublisher.publish(message);
+ log.info("[REPORT_PUSH] 报告推送消息已发布: reportId={}, filePath={}, username={}",
+ report.getLineId(), filePath, username);
+ }
+
+ /**
+ * 场站ID:配置优先,其次取 basedata_station.station_no
+ */
+ private String resolveStationId() {
+ if (StringUtils.isNotEmpty(properties.getStationId())) {
+ return properties.getStationId();
+ }
+ try {
+ String stationNo = patrolTaskMapper.selectStationNo();
+ return stationNo == null ? "" : stationNo;
+ } catch (Exception e) {
+ log.warn("[REPORT_PUSH] 查询场站编号失败: {}", e.getMessage());
+ return "";
+ }
+ }
+
+ /**
+ * 电站名称:报告记录优先,其次 basedata_station.station_name
+ */
+ private String resolveStationName(InspectionReport report) {
+ if (report != null && StringUtils.isNotEmpty(report.getStationName())) {
+ return report.getStationName();
+ }
+ try {
+ Map stationMap = patrolResultMapper.selectBasedataStation();
+ if (stationMap != null && stationMap.get("station_name") != null) {
+ return stationMap.get("station_name");
+ }
+ } catch (Exception e) {
+ log.warn("[REPORT_PUSH] 查询电站名称失败: {}", e.getMessage());
+ }
+ return "";
+ }
+
+ /**
+ * 巡检类型:配置优先,默认动环智能巡检
+ */
+ private String resolveInspectType() {
+ return StringUtils.isNotEmpty(properties.getInspectType()) ? properties.getInspectType() : "动环智能巡检";
+ }
+}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/DonghuanReportExportController.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/DonghuanReportExportController.java
new file mode 100644
index 0000000..965ac11
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/DonghuanReportExportController.java
@@ -0,0 +1,1221 @@
+package com.inspect.resultmain.controller;
+
+import com.inspect.base.core.sftp.SftpClient;
+import com.inspect.base.core.sftp.SftpUploadEntity;
+import com.inspect.base.core.utils.DateUtils;
+import com.inspect.base.core.utils.StringUtils;
+import com.inspect.base.core.web.controller.BaseController;
+import com.inspect.base.core.web.domain.AjaxResult;
+import com.inspect.common.security.utils.SecurityUtils;
+import com.inspect.img.domain.InspectionReportImg;
+import com.inspect.img.service.IInspectionReportImgService;
+import com.inspect.insreport.domain.InspectionReport;
+import com.inspect.insreport.service.IInspectionReportService;
+import com.inspect.insreportdata.domain.InspectionReportData;
+import com.inspect.insreportdata.service.IInspectionReportDataService;
+import com.inspect.message.MessageUtils;
+import com.inspect.partrolresult.domain.PatrolResult;
+import com.inspect.partrolresult.service.IPatrolResultService;
+import com.inspect.partrolresult.util.PrintUtil;
+import com.inspect.reportfont.ReportFontEmbedder;
+import com.inspect.reportpush.service.ReportPushService;
+import com.inspect.resultmain.domain.PatrolTaskResultMain;
+import com.inspect.resultmain.service.IPatrolTaskResultMainService;
+import org.apache.commons.io.function.IOConsumer;
+import org.apache.poi.xwpf.usermodel.*;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * 动环智能巡检报告 —— 归档导出(Word 版)
+ *
+ * 逻辑流程对标 {@link PatrolTaskResultMainController#updateInfo_shaoxing} /
+ * {@code exportExcelWordAndZipShaoxing}(数据准备、保存、异步导出完全一致),
+ * 仅 Word 生成改为动环模板样式(模板:动环智能巡检报告_20260817.docx)。
+ *
+ * 样式要点(与模板逐项对齐):
+ *
+ * - 页面:A4(11906×16838 twips),版心 上2098/右1474/下1984/左1587
+ * - 封面:42pt 仿宋大标题 + 分页;第二页 logo + 方正小标宋 22pt 深蓝标题
+ * + 仿宋 16pt 浅蓝副标题 + 封面签署表(初审/校对/审核/巡检日期)
+ * - 标题:黑体 16pt 加粗;表头:深蓝 1F4E79 底白字加粗;正文:仿宋 12pt
+ * - 表格边框:B4C6E7 细线(sz=0),单元格垂直居中,行高不低于 454 twips
+ * - 巡检详情:每个点位一张 8 行 × 4 列详情表,末行基准图/缺陷图
+ *
+ *
+ * 已知不确定点(待归档时调整,均以 TODO 标注):
+ * 位置字段来源、巡检点位/设备数量口径、审核意见来源、巡检读数、封面 logo 路径、
+ * 基准图/缺陷图选择规则、详情表范围(全部点位 or 仅缺陷)、Excel 导出。
+ *
+ * @author inspect
+ */
+@RestController
+@RefreshScope
+@RequestMapping({"/resultmain"})
+public class DonghuanReportExportController extends BaseController {
+
+ // ============================ 样式常量(与模板一致) ============================
+ /** 正文 */
+ private static final String FONT_BODY = "仿宋";
+ /** 一级小标题 */
+ private static final String FONT_HEAD = "黑体";
+ /** 报告标题 */
+ private static final String FONT_TITLE = "方正小标宋简体";
+ /** 深蓝:标题 / 表头底色 */
+ private static final String COLOR_DARK = "1F4E79";
+ /** 浅蓝:封面副标题 */
+ private static final String COLOR_LIGHT = "2E75B6";
+ /** 表格边框浅蓝 */
+ private static final String COLOR_BORDER = "B4C6E7";
+ /** 封面占位值灰色 */
+ private static final String COLOR_GRAY = "999999";
+ /** 表头白字 */
+ private static final String COLOR_WHITE = "FFFFFF";
+
+ /** 正文 12pt(半磅 24) */
+ private static final int SZ_BODY = 24;
+ /** 小标题 14pt(半磅 28) 四号 */
+ private static final int SZ_HEAD_FOURTH = 28;
+ /** 小标题 16pt(半磅 32)三号 */
+ private static final int SZ_HEAD_THIRD = 32;
+ /** 行高 atLeast(twips) */
+ private static final int ROW_H = 454;
+ /** 单元格左右边距(twips) */
+ private static final int CELL_MAR = 108;
+
+ /** 巡检详情图片尺寸(EMU,与模板一致) */
+ private static final long IMG_PATROL_W = 1711325L, IMG_PATROL_H = 960755L;
+ private static final long IMG_DEFECT_W = 1629410L, IMG_DEFECT_H = 916940L;
+
+ @Value("${sftp.basePath}")
+ private String basePath;
+
+ @Value("${sftp.stationCode}")
+ private String stationCode;
+
+ /**
+ * 封面 logo 图片:默认从 classpath 加载(图片位于
+ * inspect-main-task/src/main/resources/report/logo.png)
+ */
+ @Value("${report.donghuan.logo:report/logo.png}")
+ private String logoPath;
+
+ private final IPatrolTaskResultMainService patrolTaskResultMainService;
+ private final IPatrolResultService patrolResultService;
+ private final IInspectionReportDataService inspectionReportDataService;
+ private final IInspectionReportImgService inspectionReportImgService;
+ private final IInspectionReportService inspectionReportService;
+ private final SftpClient sftpClient;
+ private final ReportPushService reportPushService;
+ private final ReportFontEmbedder reportFontEmbedder;
+
+ private static final ExecutorService executor = Executors.newFixedThreadPool(10);
+
+ @Autowired
+ private MessageUtils messageUtils;
+
+ public DonghuanReportExportController(IPatrolTaskResultMainService patrolTaskResultMainService,
+ IPatrolResultService patrolResultService,
+ IInspectionReportDataService inspectionReportDataService,
+ IInspectionReportImgService inspectionReportImgService,
+ IInspectionReportService inspectionReportService,
+ SftpClient sftpClient,
+ ReportPushService reportPushService,
+ ReportFontEmbedder reportFontEmbedder) {
+ this.patrolTaskResultMainService = patrolTaskResultMainService;
+ this.patrolResultService = patrolResultService;
+ this.inspectionReportDataService = inspectionReportDataService;
+ this.inspectionReportImgService = inspectionReportImgService;
+ this.inspectionReportService = inspectionReportService;
+ this.sftpClient = sftpClient;
+ this.reportPushService = reportPushService;
+ this.reportFontEmbedder = reportFontEmbedder;
+ }
+
+ // ============================================================================
+ // 一、入口:动环归档
+ // ============================================================================
+ @PostMapping({"/updateInfo_shaoxing"})
+ public AjaxResult updateInfo_donghuan(@RequestBody PatrolTaskResultMain resultMain) {
+ long startTime = System.currentTimeMillis();
+ // 报告发起人:当前登录账号
+ final String username = SecurityUtils.getUsername();
+ logger.info("[ARCHIVE] 动环归档触发, 发起人: {}", username);
+ CompletableFuture> saveReportFuture = CompletableFuture.>supplyAsync(() -> {
+ List taskPatrolledIds = resultMain.getTaskPatrolledIds();
+ logger.info("-----------patrolTaskResultMains taskPatrolledIds: {}", taskPatrolledIds);
+ List patrolTaskResultMains =
+ patrolTaskResultMainService.selectPatrolTaskResultMainByTaskPatrolledIds(taskPatrolledIds);
+ if (patrolTaskResultMains == null || patrolTaskResultMains.isEmpty()) {
+ logger.error("[归档]失败, 数据采集中...");
+ return Collections.emptyList();
+ }
+ List lineIds = patrolTaskResultMains.stream()
+ .map(PatrolTaskResultMain::getLineId).collect(Collectors.toList());
+ List longs = patrolTaskResultMainService.selectLineIdsByList(lineIds);
+ if (longs != null && !longs.isEmpty()) {
+ lineIds.addAll(longs);
+ }
+ lineIds = lineIds.stream().distinct().collect(Collectors.toList());
+ List resultList = patrolResultService.selectPatrolResultListByMainIds(lineIds);
+ if (resultList == null || resultList.isEmpty()) {
+ logger.error("[归档]失败, 数据采集中...");
+ return Collections.emptyList();
+ }
+ PrintUtil.useTime("查询统计数据", startTime);
+ logger.info("[ARCHIVE] Start saving reports (donghuan)");
+ return patrolResultService.saveReportShaoxing(resultMain, lineIds, resultList);
+ }, executor).exceptionally(ex -> {
+ logger.error("Report saving failed", ex);
+ return Collections.emptyList();
+ });
+ PrintUtil.useTime("保存报告数据", startTime);
+
+ // 保存完成后异步导出(动环 Word 版)
+ saveReportFuture.thenAcceptAsync(reportIds -> {
+ logger.info("-----------[ARCHIVE] Saved export reportIds: {}", reportIds);
+ if (reportIds.isEmpty()) {
+ logger.warn("No reports to export");
+ return;
+ }
+ CompletableFuture.runAsync(() -> exportDonghuanReport(reportIds, username), executor)
+ .whenComplete((v, ex) -> {
+ if (ex != null) {
+ logger.error("Async export failed for reportIds: {}", reportIds, ex);
+ }
+ });
+ }, executor).exceptionally(ex -> {
+ logger.error("Error occurred during saveReport: ", ex);
+ return null;
+ });
+ return toAjax(1);
+ }
+
+ // ============================================================================
+ // 二、导出:动环 Word + 图片 ZIP
+ // ============================================================================
+ public void exportDonghuanReport(List reportIds, String username) {
+ // 合并报告,所有报告内容一致,取第一个导出即可
+ Long lineId = reportIds.get(0);
+ List tempFiles = new ArrayList<>();
+ Map tempFileMap = new LinkedHashMap<>();
+ XWPFDocument wordDocument = null;
+ try {
+ InspectionReport inspectionReport =
+ inspectionReportService.selectInspectionReportByLineId(lineId);
+ if (inspectionReport == null) {
+ logger.warn("--exportDonghuanReport----report lineId:{} not exist!", lineId);
+ return;
+ }
+ InspectionReportData query = new InspectionReportData();
+ query.setReportId(String.valueOf(inspectionReport.getLineId()));
+ List dataList =
+ inspectionReportDataService.selectInspectionReportDataList(query);
+ if (dataList == null || dataList.isEmpty()) {
+ logger.info("--exportDonghuanReport------report lineId:{},filter:{} no report data!",
+ lineId, inspectionReport.getFilter());
+ return;
+ }
+ inspectionReport.setReportDatalist(dataList);
+
+ // 图片映射:imgSrc -> 唯一文件名
+ Map imgSrcToFileNameMap = new HashMap<>();
+ List images = new ArrayList<>();
+ for (InspectionReportData reportData : dataList) {
+ if (!messageUtils.get("缺陷").equals(reportData.getPointStatus())) {
+ continue;
+ }
+ InspectionReportImg imgQuery = new InspectionReportImg();
+ imgQuery.setReportInfoId(reportData.getLineId());
+ List reportImgList =
+ inspectionReportImgService.selectInspectionReportImgList(imgQuery);
+ reportData.setReportImgList(reportImgList);
+
+ int imgIndex = 1;
+ for (InspectionReportImg reportImg : reportImgList) {
+ // 原图
+ String originalImgSrc = reportImg.getImgSrc();
+ // 基准图
+ String imageNormalUrlPath = reportImg.getImageNormalUrlPath();
+ // 结果图
+ String img = reportImg.getImg();
+ images.add(originalImgSrc);
+ images.add(imageNormalUrlPath);
+ images.add(img);
+ String pointName = reportData.getPointName() != null
+ ? reportData.getPointName() : "unnamed";
+
+ // 原图
+ if (originalImgSrc != null) {
+ String extension = "";
+ int dotIndex = originalImgSrc.lastIndexOf('.');
+ if (dotIndex > 0) {
+ extension = originalImgSrc.substring(dotIndex);
+ }
+ String uniqueFileName = String.format("%s_%s_%s_%d%s",
+ originalImgSrc.split("_")[2],
+ pointName.replaceAll("[\\\\/:*?\"<>|]", "")
+ .replaceAll("\\s+", ""),
+ reportData.getLineId(), imgIndex, extension);
+ imgSrcToFileNameMap.put(originalImgSrc, uniqueFileName);
+ imgIndex++;
+ }
+ // 结果图
+ if (img != null) {
+ String extension = "";
+ int dotIndex = img.lastIndexOf('.');
+ if (dotIndex > 0) {
+ extension = img.substring(dotIndex);
+ }
+ String uniqueImgName = String.format("%s_%s_%s_%d%s",
+ img.split("_")[2],
+ pointName.replaceAll("[\\\\/:*?\"<>|]", "")
+ .replaceAll("\\s+", ""),
+ reportData.getLineId(), imgIndex, extension);
+ imgSrcToFileNameMap.put(img, uniqueImgName);
+ imgIndex++;
+ }
+ // 基准图
+ if (imageNormalUrlPath != null) {
+ String extension = "";
+ int dotIndex = imageNormalUrlPath.lastIndexOf('.');
+ if (dotIndex > 0) {
+ extension = imageNormalUrlPath.substring(dotIndex);
+ }
+ String uniqueImgNormalPathName = String.format("%s_%s_%s_%d%s",
+ imageNormalUrlPath.split("_")[2],
+ pointName.replaceAll("[\\\\/:*?\"<>|]", "")
+ .replaceAll("\\s+", ""),
+ reportData.getLineId(), imgIndex, extension);
+ imgSrcToFileNameMap.put(imageNormalUrlPath, uniqueImgNormalPathName);
+ imgIndex++;
+ }
+ }
+ }
+
+ // 图片下载到临时文件,避免内存驻留大量图片字节
+ for (String imgSrc : images) {
+ try {
+ Path tempFile = downloadImageToTemp(imgSrc);
+ if (tempFile != null) {
+ tempFileMap.put(imgSrc, tempFile);
+ tempFiles.add(tempFile);
+ }
+ } catch (Exception e) {
+ logger.warn("报告归档下载图片失败: {}", imgSrc, e);
+ }
+ }
+
+ // 生成动环样式 Word
+ wordDocument = getXWPFDocumentDonghuan(inspectionReport, tempFileMap);
+
+ String basePath = this.basePath + stationCode + "/Model/";
+ Date startTime = inspectionReport.getInspectionStartTime() != null
+ ? inspectionReport.getInspectionStartTime() : new Date();
+ String timestamp = DateUtils.format(DateUtils.yyyyMMddHHmmss2, startTime)
+ + "_" + System.currentTimeMillis();
+ String namePrefix = "";
+// if ("1".equals(inspectionReport.getFilter())) {
+// namePrefix = messageUtils.get("初筛") + "_";
+// } else if ("0".equals(inspectionReport.getFilter())) {
+// namePrefix = messageUtils.get("大模型") + "_";
+// }
+ String reportName = inspectionReport.getInspectionTaskName() + "_" + timestamp;
+
+ // 上传图片 ZIP
+ CompletableFuture zipUploadFuture = CompletableFuture.runAsync(() -> {
+ uploadFile(basePath, reportName, ".zip", outputStream -> {
+ try (ZipOutputStream zipOut = new ZipOutputStream(outputStream)) {
+ for (Map.Entry entry : tempFileMap.entrySet()) {
+ String pointFileName = imgSrcToFileNameMap
+ .getOrDefault(entry.getKey(), "unnamed");
+ ZipEntry zipEntry = new ZipEntry(pointFileName);
+ zipOut.putNextEntry(zipEntry);
+ Files.copy(entry.getValue(), zipOut);
+ zipOut.closeEntry();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("生成ZIP文件失败", e);
+ }
+ });
+ }, executor);
+
+ // TODO 归档时调整:如需 Excel 导出,可复用
+ // PatrolTaskResultMainController.getSXSSFWorkbookShaoxing(inspectionReport, tempFileMap)
+ // 并在此上传 .xls(本类暂只导出 Word + 图片 ZIP)
+
+ // 上传 Word
+ XWPFDocument finalWordDocument = wordDocument;
+ CompletableFuture wordUploadFuture = CompletableFuture.runAsync(() -> {
+ uploadFile(basePath, reportName, ".docx", finalWordDocument::write);
+ }, executor);
+
+ CompletableFuture.allOf(zipUploadFuture, wordUploadFuture)
+ .whenComplete((v, t) -> {
+ try {
+ inspectionReport.setFilePath(basePath + reportName + ".docx");
+ inspectionReport.setLineIds(reportIds);
+ inspectionReportService.updateInspectionReportFilePath(inspectionReport);
+ // 报告生成并上传完成后,发布消息到 RabbitMQ,由消费者异步 HTTP 上传外部平台(自动归档与手动归档均推送)
+ reportPushService.pushReport(inspectionReport, username);
+ } finally {
+ closeResources(finalWordDocument, tempFiles);
+ }
+ });
+ } catch (Throwable e) {
+ logger.error("生成动环巡视文件异常", e);
+ closeResources(wordDocument, tempFiles);
+ }
+ }
+
+ // ============================================================================
+ // 三、动环 Word 构建
+ // ============================================================================
+ private XWPFDocument getXWPFDocumentDonghuan(InspectionReport report,
+ Map tempFileMap) {
+ XWPFDocument doc = new XWPFDocument();
+ // 嵌入报告字体(仿宋/黑体/方正小标宋),docx 自带字体,查看方无需安装
+ reportFontEmbedder.embed(doc);
+ setupPage(doc);
+ buildCoverPage1(doc); // 第1页:42pt 大标题 + 分页
+ buildCoverPage2(doc, report); // 第2页:logo + 报告标题 + 副标题 + 签署表
+ buildSectionBasic(doc, report); // 一、巡检基本情况
+ buildSectionStatus(doc); // 二、巡检状态说明
+ buildSectionOverview(doc, report); // 三、巡检结果概览
+ buildSectionDetail(doc, report, tempFileMap); // 四、巡检详情
+ return doc;
+ }
+
+ /** 段落 keepNext:标题与后续表格/内容保持同页,避免标题孤行 */
+ private void setKeepNext(XWPFParagraph p) {
+ CTPPr pPr = p.getCTP().isSetPPr() ? p.getCTP().getPPr() : p.getCTP().addNewPPr();
+ if (!pPr.isSetKeepNext()) {
+ pPr.addNewKeepNext();
+ }
+ }
+
+ /**
+ * 设置段落首行缩进 1 字符。
+ * 写 w:firstLineChars=100(Word/WPS 原生「首行缩进:1 字符」,随字号自动折算 1 个汉字宽),
+ * 同时按段落字号(halfPoints,半磅)回填 w:firstLine(twips = halfPoints × 10),
+ * 兼容不识别 firstLineChars 的阅读器(如 LibreOffice)。
+ */
+ private void setFirstLineIndentOneChar(XWPFParagraph p, int halfPoints) {
+ p.setIndentationFirstLine(halfPoints * 10);
+ CTPPr pPr = p.getCTP().isSetPPr() ? p.getCTP().getPPr() : p.getCTP().addNewPPr();
+ CTInd ind = pPr.isSetInd() ? pPr.getInd() : pPr.addNewInd();
+ ind.setFirstLineChars(BigInteger.valueOf(100));
+ }
+
+ /** 页面设置:A4 + 模板版心 + 文档网格(与模板 sectPr 一致) */
+ private void setupPage(XWPFDocument doc) {
+ CTBody body = doc.getDocument().getBody();
+ CTSectPr sectPr = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
+ CTPageSz pgSz = sectPr.isSetPgSz() ? sectPr.getPgSz() : sectPr.addNewPgSz();
+ pgSz.setW(BigInteger.valueOf(11906));
+ pgSz.setH(BigInteger.valueOf(16838));
+ CTPageMar pgMar = sectPr.isSetPgMar() ? sectPr.getPgMar() : sectPr.addNewPgMar();
+ pgMar.setTop(BigInteger.valueOf(2098));
+ pgMar.setRight(BigInteger.valueOf(1474));
+ pgMar.setBottom(BigInteger.valueOf(1984));
+ pgMar.setLeft(BigInteger.valueOf(1587));
+ pgMar.setHeader(BigInteger.valueOf(851));
+ pgMar.setFooter(BigInteger.valueOf(992));
+ pgMar.setGutter(BigInteger.ZERO);
+ CTColumns cols = sectPr.isSetCols() ? sectPr.getCols() : sectPr.addNewCols();
+ cols.setNum(BigInteger.ONE);
+ cols.setSpace(BigInteger.valueOf(720));
+ // 文档网格 linePitch=360(每行 18pt):模板封面第二页正是靠它撑满一页,
+ // 否则空行按 12pt 排版,内容变矮,「一、巡检基本情况」会被挤到封面页
+ CTDocGrid grid = sectPr.isSetDocGrid() ? sectPr.getDocGrid() : sectPr.addNewDocGrid();
+ grid.setLinePitch(BigInteger.valueOf(360));
+ grid.setCharSpace(BigInteger.ZERO);
+ }
+
+ /** 第1页封面:42pt 仿宋大标题 + 分页符 */
+ private void buildCoverPage1(XWPFDocument doc) {
+ XWPFParagraph p = addPara(doc, "#动环智能巡检", FONT_BODY, 84, false, false, null,
+ ParagraphAlignment.CENTER, 120, 120);
+ p.createRun().addBreak(BreakType.PAGE);
+ }
+
+ /** 第2页:logo + 报告标题 + 副标题 + 空行 + 封面签署表 */
+ private void buildCoverPage2(XWPFDocument doc, InspectionReport report) {
+ // logo:优先 classpath 资源(src/main/resources/report/logo.png),
+ // 兼容本地绝对/相对路径
+ if (StringUtils.isNotEmpty(logoPath)) {
+ try (InputStream is = openLogoStream(logoPath)) {
+ if (is != null) {
+ XWPFParagraph logoPara = doc.createParagraph();
+ XWPFRun logoRun = logoPara.createRun();
+ // 与模板一致:3203575 × 541020 EMU(POI 5.x 的 addPicture 仅 int 尺寸重载)
+ logoRun.addPicture(is, XWPFDocument.PICTURE_TYPE_PNG, "logo.png",
+ (int) 3203575L, (int) 541020L);
+ }
+ } catch (Exception e) {
+ logger.warn("插入封面 logo 失败: {}", logoPath, e);
+ }
+ }
+ addPara(doc, "", null, SZ_BODY, false, false, null, null, 600, null);
+ addPara(doc, "", null, SZ_BODY, false, false, null, null, 200, null);
+
+ // 报告标题:站名 + 巡检报告(方正小标宋简体 22pt 加粗 深蓝)
+ // TODO 归档时调整:站名是否已含「场站/电站」后缀
+ // 站名为空时标题不拼占位符(标题本身不会空白)
+ String station = report.getStationName();
+ String reportTitle = (StringUtils.isEmpty(station) ? "" : station) + "巡检报告";
+ addPara(doc, reportTitle, FONT_TITLE, 44, true, false, COLOR_DARK,
+ ParagraphAlignment.CENTER, 0, 100);
+ // 副标题
+ addPara(doc, "【巡检类型:动环智能巡检】", FONT_BODY, SZ_HEAD_THIRD, false, false,
+ COLOR_LIGHT, ParagraphAlignment.CENTER, 200, 100);
+
+ // 空行(撑出版面,与模板一致)
+ for (int i = 0; i < 11; i++) {
+ addPara(doc, "", null, SZ_BODY, false, false, null,
+ ParagraphAlignment.CENTER, 200, 100);
+ }
+ addPara(doc, "", null, SZ_BODY, false, false, null,
+ ParagraphAlignment.CENTER, 400, null);
+
+ // 封面签署表:初审/校对/审核/巡检日期(4行×2列,宽4686 twips)
+ XWPFTable table = addTable(doc, 4, 2, new int[]{1800, 2886}, 4686, true);
+ String[] labels = {"初审", "校对", "审核", "巡检日期"};
+ // TODO 归档时调整:初审/校对/审核 人员数据来源暂用占位符;巡检日期取巡视开始日期
+ String[] values = {"[填写]", "[填写]", "[填写]",
+ report.getInspectionStartTime() != null
+ ? DateUtils.format(DateUtils.yyyyMMdd2, report.getInspectionStartTime()) : "[填写]"};
+ for (int i = 0; i < labels.length; i++) {
+ setRow(table.getRow(i), null, true);
+ setRowJcCenter(table.getRow(i));
+ // 标签:仿宋 12pt 加粗 深蓝 居中
+ styleCell(table.getRow(i).getCell(0), labels[i], FONT_BODY, SZ_BODY, true, false,
+ COLOR_DARK, false, ParagraphAlignment.CENTER, 0, 0, 1800, null,
+ 60, 150, 60, 120);
+ // 值:仿宋 12pt 灰色斜体 居中
+ styleCell(table.getRow(i).getCell(1), values[i], FONT_BODY, SZ_BODY, false, true,
+ COLOR_GRAY, false, ParagraphAlignment.CENTER, 0, 0, 2886, null,
+ 60, 150, 60, 120);
+ }
+ }
+
+ /** 一、巡检基本情况 */
+ private void buildSectionBasic(XWPFDocument doc, InspectionReport report) {
+ XWPFParagraph h = addPara(doc, "一、巡检基本情况", FONT_HEAD, SZ_HEAD_FOURTH, true, false,
+ null, null, 160, 80);
+ setKeepNext(h);
+ setFirstLineIndentOneChar(h, SZ_HEAD_FOURTH); // 标题首行缩进 1 字符
+
+ XWPFTable t = addTable(doc, 7, 4, new int[]{1827, 2655, 1920, 2659}, 9061, false);
+ List dataList = report.getReportDatalist();
+
+ // R0 表头(跨4列,深蓝底白字)
+ setRow(t.getRow(0), ROW_H, false);
+ mergeRowCells(t, 0, 0, 3);
+ styleCell(t.getRow(0).getCell(0), "场站基本信息", FONT_BODY, SZ_BODY, true, false,
+ COLOR_WHITE, true, ParagraphAlignment.CENTER, 0, 0, 9061, 4,
+ 0, CELL_MAR, 0, CELL_MAR);
+
+ // R1 电站名称 | 白杨坪 | 位置 | 四川省成都市
+ setRow(t.getRow(1), ROW_H, false);
+ bodyCell(t.getRow(1).getCell(0), "电站名称", 1827);
+ bodyCell(t.getRow(1).getCell(1), val(report.getStationName()), 2655);
+ bodyCell(t.getRow(1).getCell(2), "位置", 1920);
+ bodyCell(t.getRow(1).getCell(3), val(report.getLocation()), 2659);
+
+ // ==================== R2:巡视任务基本信息(新增表头) ====================
+ setRow(t.getRow(2), ROW_H, false);
+ mergeRowCells(t, 2, 0, 3);
+ styleCell(t.getRow(2).getCell(0), "巡视任务基本信息", FONT_BODY, SZ_BODY, true, false,
+ COLOR_WHITE, true, ParagraphAlignment.CENTER, 0, 0, 9061, 4,
+ 0, CELL_MAR, 0, CELL_MAR);
+
+ // R3 巡检任务 | xxx(跨3列)
+ setRow(t.getRow(3), ROW_H, false);
+ bodyCell(t.getRow(3).getCell(0), "巡检任务", 1827);
+ mergeRowCells(t, 3, 1, 3);
+ styleCell(t.getRow(3).getCell(1), val(report.getInspectionTaskName()), FONT_BODY, SZ_BODY,
+ false, false, null, false, ParagraphAlignment.LEFT, 0, 0,
+ 1827 + 2655 + 1920 + 2659 - 1827, 3, 0, CELL_MAR, 0, CELL_MAR);
+
+ // R4 巡检开始时间 | xx | 巡检结束时间 | xx
+ setRow(t.getRow(4), ROW_H, false);
+ bodyCell(t.getRow(4).getCell(0), "巡检开始时间", 1827);
+ bodyCell(t.getRow(4).getCell(1), val(formatDateTime(report.getInspectionStartTime())), 2655);
+ bodyCell(t.getRow(4).getCell(2), "巡检结束时间", 1920);
+ bodyCell(t.getRow(4).getCell(3), val(formatDateTime(report.getInspectionEndTime())), 2659);
+
+ // R5 巡检点位数量(个) | n | 巡检设备数量(个) | m
+ // TODO 归档时调整:数量口径(按点位名去重 / 按设备名去重)
+ setRow(t.getRow(5), ROW_H, false);
+ bodyCell(t.getRow(5).getCell(0), "巡检点位数量(个)", 1827);
+ bodyCell(t.getRow(5).getCell(1), String.valueOf(distinctCount(dataList,
+ InspectionReportData::getPointName)), 2655);
+ bodyCell(t.getRow(5).getCell(2), "巡检设备数量(个)", 1920);
+ bodyCell(t.getRow(5).getCell(3), String.valueOf(distinctCount(dataList,
+ InspectionReportData::getEqName)), 2659);
+
+ // R6 审核意见 | (跨3列,可空)
+ // TODO 归档时调整:审核意见来源(暂用 report.description,可改为 checkPerson 或空)
+ setRow(t.getRow(6), ROW_H, false);
+ bodyCell(t.getRow(6).getCell(0), "审核意见", 1827);
+ mergeRowCells(t, 6, 1, 3);
+ styleCell(t.getRow(6).getCell(1), val(report.getDescription()), FONT_BODY, SZ_BODY,
+ false, false, null, false, ParagraphAlignment.LEFT, 0, 0,
+ 1827 + 2655 + 1920 + 2659 - 1827, 3, 0, CELL_MAR, 0, CELL_MAR);
+ }
+
+ /** 二、巡检状态说明(静态文案,与模板一致) */
+ private void buildSectionStatus(XWPFDocument doc) {
+ XWPFParagraph h = addPara(doc, "二、巡检状态说明", FONT_HEAD, SZ_HEAD_FOURTH, true, false,
+ null, null, 160, 80);
+ setKeepNext(h);
+ setFirstLineIndentOneChar(h, SZ_HEAD_FOURTH); // 标题首行缩进 1 字符
+
+ XWPFTable t = addTable(doc, 4, 3, new int[]{1831, 4115, 3114}, 9060, false);
+ String[][] rows = {
+ {"状态", "说明", "典型场景"},
+ {"缺陷", "巡检结果表明设备存在异常状态或潜在运行风险",
+ "表计超限、温度过高、表面油污、设备部件破损"},
+ {"异常", "巡检数据未满足有效判定条件,无法准确判断设备当前状态",
+ "图像模糊、目标遮挡、通信异常、传感器数据异常"},
+ {"正常", "巡检数据有效,检测结果符合设备正常运行要求,未发现异常状态或运行风险",
+ "表计读数正常、温度正常、设备外观正常"},
+ };
+ for (int i = 0; i < rows.length; i++) {
+ setRow(t.getRow(i), ROW_H, false);
+ if (i == 0) {
+ headerCell(t.getRow(i).getCell(0), rows[i][0], 1831);
+ headerCell(t.getRow(i).getCell(1), rows[i][1], 4115);
+ headerCell(t.getRow(i).getCell(2), rows[i][2], 3114);
+ } else {
+ bodyCell(t.getRow(i).getCell(0), rows[i][0], 1831, true);
+ bodyCell(t.getRow(i).getCell(1), rows[i][1], 4115, false);
+ bodyCell(t.getRow(i).getCell(2), rows[i][2], 3114, false);
+ }
+ }
+ }
+
+ /** 三、巡检结果概览:(一)巡检统计 + (二)缺陷汇总表 */
+ private void buildSectionOverview(XWPFDocument doc, InspectionReport report) {
+ XWPFParagraph h = addPara(doc, "三、巡检结果概览", FONT_HEAD, SZ_HEAD_FOURTH, true, false,
+ null, null, 160, 80);
+ setKeepNext(h);
+ setFirstLineIndentOneChar(h, SZ_HEAD_FOURTH); // 标题首行缩进 1 字符
+
+ List dataList = report.getReportDatalist() != null
+ ? report.getReportDatalist() : Collections.emptyList();
+ String statusDefect = messageUtils.get("缺陷");
+ String statusAbnormal = messageUtils.get("异常");
+ String statusNormal = messageUtils.get("正常");
+
+ // (一)巡检统计
+ XWPFParagraph h1 = addPara(doc, "(一)巡检统计", FONT_BODY, SZ_HEAD_THIRD, true, false,
+ null, null, 80, 40);
+ setKeepNext(h1);
+ setFirstLineIndentOneChar(h1, SZ_HEAD_THIRD); // 标题首行缩进 1 字符
+ XWPFTable statTable = addTable(doc, 5, 2, new int[]{4296, 4713}, 9008, true);
+ setRow(statTable.getRow(0), ROW_H, false);
+ headerCell(statTable.getRow(0).getCell(0), "巡检状态", 4296);
+ headerCell(statTable.getRow(0).getCell(1), "数量(个)", 4713);
+ String[][] statRows = {
+ {statusDefect, String.valueOf(countByStatus(dataList, statusDefect))},
+ {statusAbnormal, String.valueOf(countByStatus(dataList, statusAbnormal))},
+ {statusNormal, String.valueOf(countByStatus(dataList, statusNormal))},
+ {"合计", String.valueOf(dataList.size())},
+ };
+ for (int i = 0; i < statRows.length; i++) {
+ setRow(statTable.getRow(i + 1), ROW_H, false);
+ bodyCell(statTable.getRow(i + 1).getCell(0), statRows[i][0], 4296, true);
+ bodyCell(statTable.getRow(i + 1).getCell(1), statRows[i][1], 4713, true);
+ }
+
+ // (二)缺陷汇总表
+ XWPFParagraph h2 = addPara(doc, "(二)缺陷汇总表", FONT_BODY, SZ_HEAD_THIRD, true, false,
+ null, null, 80, 40);
+ setKeepNext(h2);
+ setFirstLineIndentOneChar(h2, SZ_HEAD_THIRD); // 标题首行缩进 1 字符
+ List defectList = dataList.stream()
+ .filter(d -> statusDefect.equals(d.getPointStatus()))
+ .collect(Collectors.toList());
+ int[] summaryWidths = {717, 1200, 1230, 1245, 1215, 1710, 990, 727};
+ int summaryTotalWidth = Arrays.stream(summaryWidths).sum();
+ // 无缺陷数据时保留一行(合并整行输出文字说明),避免表格空白
+ int summaryRows = 1 + Math.max(defectList.size(), 1);
+ XWPFTable summaryTable = addTable(doc, summaryRows, 8, summaryWidths, 9034, false);
+ String[] summaryHeaders = {"序号", "区域名称", "间隔名称", "设备名称", "部件名称",
+ "巡检点位名称", "巡检", "状态"};
+ setRow(summaryTable.getRow(0), ROW_H, true);
+ for (int i = 0; i < summaryHeaders.length; i++) {
+ headerCell(summaryTable.getRow(0).getCell(i), summaryHeaders[i], summaryWidths[i]);
+ }
+ // 「巡检 / 结果」表头换行
+ XWPFParagraph headP = summaryTable.getRow(0).getCell(6).getParagraphs().get(0);
+ setRunFont(headP.createRun(), "结果", FONT_BODY, SZ_BODY, true, false, COLOR_WHITE, true);
+ int num = 1;
+ for (int ri = 0; ri < defectList.size(); ri++) {
+ InspectionReportData d = defectList.get(ri);
+ setRow(summaryTable.getRow(ri + 1), ROW_H, true);
+ String[] vals = {String.valueOf(num++), val(d.getArea()), val(d.getTimeInterval()),
+ val(d.getEqName()), val(d.getParts()), val(d.getPointName()),
+ val(d.getInspectionResults()), val(d.getPointStatus())};
+ for (int ci = 0; ci < vals.length; ci++) {
+ bodyCell(summaryTable.getRow(ri + 1).getCell(ci), vals[ci],
+ summaryWidths[ci], false, 120, 120);
+ }
+ }
+ if (defectList.isEmpty()) {
+ // 无缺陷数据时:整行合并输出文字说明(原占位横杠改为「本次巡检未发现缺陷项」)
+ setRow(summaryTable.getRow(1), ROW_H, true);
+ mergeRowCells(summaryTable, 1, 0, 7);
+ styleCell(summaryTable.getRow(1).getCell(0),
+ messageUtils.get("本次巡检未发现缺陷项"), FONT_BODY, SZ_BODY,
+ false, false, null, false, ParagraphAlignment.CENTER, 120, 120,
+ summaryTotalWidth, 8, 0, CELL_MAR, 0, CELL_MAR);
+ }
+ }
+
+ /** 四、巡检详情:每个点位一张 8 行 × 4 列详情表 */
+ private void buildSectionDetail(XWPFDocument doc, InspectionReport report,
+ Map tempFileMap) {
+ XWPFParagraph h = addPara(doc, "四、巡检详情", FONT_HEAD, SZ_HEAD_FOURTH, true, false,
+ null, null, 160, 80);
+ setKeepNext(h);
+ setFirstLineIndentOneChar(h, SZ_HEAD_FOURTH); // 标题首行缩进 1 字符
+
+ List dataList = report.getReportDatalist() != null
+ ? report.getReportDatalist().stream().filter(item -> messageUtils.get("缺陷").equals(item.getPointStatus())).collect(Collectors.toList()) : Collections.emptyList();
+ // TODO 归档时调整:详情表范围(全部点位 or 仅缺陷点位,如需仅缺陷请在此过滤)
+ if (dataList.isEmpty()) {
+ // 无缺陷数据时输出文字说明,避免「四、巡检详情」章节空白
+ addPara(doc, messageUtils.get("本次巡检未发现缺陷项"), FONT_BODY, SZ_BODY,
+ false, false, null, ParagraphAlignment.LEFT, 160, 80);
+ return;
+ }
+ for (InspectionReportData d : dataList) {
+ XWPFTable t = addTable(doc, 8, 4, new int[]{1707, 2891, 1665, 2785}, 9048, false);
+ int[] grid = {1707, 2891, 1665, 2785};
+
+ String[][] pairs = {
+ {"巡检点位名称", val(d.getPointName()), "巡检时间", val(formatDateTime(d.getAcquisitionTime()))},
+ {"区域名称", val(d.getArea()), "间隔名称", val(d.getTimeInterval())},
+ {"设备名称", val(d.getEqName()), "部件名称", val(d.getParts())},
+ {"数据来源", val(d.getDataSources()), "算法名称", val(d.getAlgName())},
+ {"巡检读数", val(d.getInspectionValue()), "巡检结果", val(d.getInspectionResults())},
+ };
+ for (int ri = 0; ri < pairs.length; ri++) {
+ setRow(t.getRow(ri), ROW_H, false);
+ bodyCell(t.getRow(ri).getCell(0), pairs[ri][0], grid[0], false, 120, 120);
+ bodyCell(t.getRow(ri).getCell(1), pairs[ri][1], grid[1], false, 120, 120);
+ bodyCell(t.getRow(ri).getCell(2), pairs[ri][2], grid[2], false, 120, 120);
+ bodyCell(t.getRow(ri).getCell(3), pairs[ri][3], grid[3], false, 120, 120);
+ }
+
+ // R5 巡检状态 | 状态(跨3列)
+ setRow(t.getRow(5), ROW_H, false);
+ bodyCell(t.getRow(5).getCell(0), "巡检状态", grid[0], false, 120, 120);
+ mergeRowCells(t, 5, 1, 3);
+ styleCell(t.getRow(5).getCell(1), val(d.getPointStatus()), FONT_BODY, SZ_BODY,
+ false, false, null, false, ParagraphAlignment.LEFT, 120, 120,
+ grid[1] + grid[2] + grid[3], 3, 0, CELL_MAR, 0, CELL_MAR);
+
+ // R6 巡检原图 | 图片文件名(跨3列)
+ String imgName = firstImageName(d);
+ setRow(t.getRow(6), ROW_H, false);
+ bodyCell(t.getRow(6).getCell(0), "巡检原图", grid[0], false, 120, 120);
+ mergeRowCells(t, 6, 1, 3);
+ styleCell(t.getRow(6).getCell(1), val(imgName), FONT_BODY, SZ_BODY,
+ false, false, null, false, ParagraphAlignment.LEFT, 120, 120,
+ grid[1] + grid[2] + grid[3], 3, 0, CELL_MAR, 0, CELL_MAR);
+
+ // R7 基准图 | 巡检原图照片 | 缺陷图 | 缺陷图照片
+ // 图片列表为空或路径缺失时置 null,由 imageCell 输出横杠占位,避免空白/空指针
+ setRow(t.getRow(7), ROW_H, false);
+ Path patrolPath = null;
+ Path defectPath = null;
+ if (d.getReportImgList() != null && !d.getReportImgList().isEmpty()) {
+ InspectionReportImg inspectionReportImg = d.getReportImgList().get(0);
+ patrolPath = tempFileMap.get(inspectionReportImg.getImageNormalUrlPath());
+ defectPath = tempFileMap.get(inspectionReportImg.getImg());
+ }
+ bodyCell(t.getRow(7).getCell(0), "基准图", grid[0], false, 120, 120);
+ imageCell(t.getRow(7).getCell(1), patrolPath,
+ IMG_PATROL_W, IMG_PATROL_H, grid[1]);
+ bodyCell(t.getRow(7).getCell(2), "缺陷图", grid[2], false, 120, 120);
+ imageCell(t.getRow(7).getCell(3), defectPath,
+ IMG_DEFECT_W, IMG_DEFECT_H, grid[3]);
+
+ // 表间间距
+ addPara(doc, "", null, SZ_BODY, false, false, null, null, 120, 120);
+ }
+ }
+
+ // ============================================================================
+ // 四、POI 样式工具方法
+ // ============================================================================
+
+ /** 新建段落并写文字;halfPoints=字号半磅;before/after 单位 twips */
+ private XWPFParagraph addPara(XWPFDocument doc, String text, String font, int halfPoints,
+ boolean bold, boolean italic, String color,
+ ParagraphAlignment align, Integer before, Integer after) {
+ XWPFParagraph p = doc.createParagraph();
+ if (align != null) {
+ p.setAlignment(align);
+ }
+ if (before != null) {
+ p.setSpacingBefore(before);
+ }
+ if (after != null) {
+ p.setSpacingAfter(after);
+ }
+ // 正文段落行距 276 auto(1.15 倍),与模板 Normal 样式一致:
+ // 模板封面第二页正是靠它把空行撑满一页;若按默认单倍行距,封面页变矮,
+ // 「一、巡检基本情况」会被挤到封面页(表格内段落走 styleCell,显式 240)
+ CTPPr pPr = p.getCTP().isSetPPr() ? p.getCTP().getPPr() : p.getCTP().addNewPPr();
+ CTSpacing spacing = pPr.isSetSpacing() ? pPr.getSpacing() : pPr.addNewSpacing();
+ spacing.setLine(BigInteger.valueOf(276));
+ spacing.setLineRule(STLineSpacingRule.AUTO);
+ if (text != null && !text.isEmpty()) {
+ XWPFRun run = p.createRun();
+ setRunFont(run, text, font, halfPoints, bold, italic, color, false);
+ }
+ return p;
+ }
+
+ /** 设置 run 字体(含中文 eastAsia)、字号、加粗、斜体、颜色 */
+ private void setRunFont(XWPFRun run, String text, String font, int halfPoints,
+ boolean bold, boolean italic, String color, boolean lineBreak) {
+ if (lineBreak) {
+ run.addBreak(); // 换行符在前,实现「巡检⏎结果」这类两行表头
+ }
+ if (text != null) {
+ run.setText(text);
+ }
+ // 字体/字号直接写 w:rPr XML:
+ // - setFontSize(double) 仅 POI 5.x 有,而运行环境是 poi-ooxml 4.1.2(target/lib),
+ // 调用会抛 NoSuchMethodError
+ // - w:sz 单位固定为半磅(24 = 12pt),所有 POI 版本一致
+ CTRPr rPr = run.getCTR().isSetRPr() ? run.getCTR().getRPr() : run.getCTR().addNewRPr();
+ if (font != null) {
+ run.setFontFamily(font);
+ CTFonts fonts = rPr.isSetRFonts() ? rPr.getRFonts() : rPr.addNewRFonts();
+ fonts.setEastAsia(font);
+ fonts.setCs(font);
+ // 注意:嵌入字体的引用写在 fontTable.xml(w:font w:name + w:embedRegular),
+ // run 这里只需写字体名,Word 打开时按名字查 fontTable 使用嵌入字体
+ }
+ CTHpsMeasure sz = rPr.isSetSz() ? rPr.getSz() : rPr.addNewSz();
+ sz.setVal(BigInteger.valueOf(halfPoints));
+ CTHpsMeasure szCs = rPr.isSetSzCs() ? rPr.getSzCs() : rPr.addNewSzCs();
+ szCs.setVal(BigInteger.valueOf(halfPoints));
+ run.setBold(bold);
+ run.setItalic(italic);
+ if (color != null) {
+ run.setColor(color);
+ }
+ }
+
+ /** 建表:设置 tblW / 边框 / autofit / 网格列宽 */
+ private XWPFTable addTable(XWPFDocument doc, int rows, int cols, int[] gridWidths,
+ int tblWidth, boolean center) {
+ XWPFTable table = doc.createTable(rows, cols);
+ CTTbl tbl = table.getCTTbl();
+ CTTblPr tblPr = tbl.getTblPr();
+ if (tblPr == null) {
+ tblPr = tbl.addNewTblPr();
+ }
+ CTTblWidth tblW = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
+ tblW.setW(BigInteger.valueOf(tblWidth));
+ tblW.setType(STTblWidth.DXA);
+
+ if (center) {
+ CTJc jc = tblPr.isSetJc() ? tblPr.getJc() : tblPr.addNewJc();
+ jc.setVal(STJc.CENTER);
+ }
+
+ CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
+ setBorders(borders);
+
+ CTTblLayoutType layout = tblPr.isSetTblLayout() ? tblPr.getTblLayout() : tblPr.addNewTblLayout();
+ layout.setType(STTblLayoutType.AUTOFIT);
+
+ CTTblCellMar cellMar = tblPr.isSetTblCellMar() ? tblPr.getTblCellMar() : tblPr.addNewTblCellMar();
+ setCellMar(cellMar, 0, CELL_MAR, 0, CELL_MAR);
+
+ // 网格列宽
+ CTTblGrid grid = tbl.getTblGrid();
+ if (grid == null) {
+ grid = tbl.addNewTblGrid();
+ }
+ for (int i = grid.sizeOfGridColArray() - 1; i >= 0; i--) {
+ grid.removeGridCol(i);
+ }
+ for (int w : gridWidths) {
+ grid.addNewGridCol().setW(BigInteger.valueOf(w));
+ }
+ return table;
+ }
+
+ private void setBorders(CTTblBorders borders) {
+ CTBorder top = borders.isSetTop() ? borders.getTop() : borders.addNewTop();
+ CTBorder left = borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft();
+ CTBorder bottom = borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom();
+ CTBorder right = borders.isSetRight() ? borders.getRight() : borders.addNewRight();
+ CTBorder insideH = borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH();
+ CTBorder insideV = borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV();
+ for (CTBorder b : new CTBorder[]{top, left, bottom, right, insideH, insideV}) {
+ b.setVal(STBorder.SINGLE);
+ b.setColor(COLOR_BORDER);
+ b.setSz(BigInteger.ZERO);
+ b.setSpace(BigInteger.ZERO);
+ }
+ }
+
+ private void setBorders(CTTcBorders borders) {
+ CTBorder top = borders.isSetTop() ? borders.getTop() : borders.addNewTop();
+ CTBorder left = borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft();
+ CTBorder bottom = borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom();
+ CTBorder right = borders.isSetRight() ? borders.getRight() : borders.addNewRight();
+ for (CTBorder b : new CTBorder[]{top, left, bottom, right}) {
+ b.setVal(STBorder.SINGLE);
+ b.setColor(COLOR_BORDER);
+ b.setSz(BigInteger.ZERO);
+ b.setSpace(BigInteger.ZERO);
+ }
+ }
+
+ private void setCellMar(CTTblCellMar mar, int top, int left, int bottom, int right) {
+ CTTblWidth t = mar.isSetTop() ? mar.getTop() : mar.addNewTop();
+ t.setW(BigInteger.valueOf(top));
+ t.setType(STTblWidth.DXA);
+ CTTblWidth l = mar.isSetLeft() ? mar.getLeft() : mar.addNewLeft();
+ l.setW(BigInteger.valueOf(left));
+ l.setType(STTblWidth.DXA);
+ CTTblWidth b = mar.isSetBottom() ? mar.getBottom() : mar.addNewBottom();
+ b.setW(BigInteger.valueOf(bottom));
+ b.setType(STTblWidth.DXA);
+ CTTblWidth r = mar.isSetRight() ? mar.getRight() : mar.addNewRight();
+ r.setW(BigInteger.valueOf(right));
+ r.setType(STTblWidth.DXA);
+ }
+
+ private void setCellMar(CTTcMar mar, int top, int left, int bottom, int right) {
+ CTTblWidth t = mar.isSetTop() ? mar.getTop() : mar.addNewTop();
+ t.setW(BigInteger.valueOf(top));
+ t.setType(STTblWidth.DXA);
+ CTTblWidth l = mar.isSetLeft() ? mar.getLeft() : mar.addNewLeft();
+ l.setW(BigInteger.valueOf(left));
+ l.setType(STTblWidth.DXA);
+ CTTblWidth b = mar.isSetBottom() ? mar.getBottom() : mar.addNewBottom();
+ b.setW(BigInteger.valueOf(bottom));
+ b.setType(STTblWidth.DXA);
+ CTTblWidth r = mar.isSetRight() ? mar.getRight() : mar.addNewRight();
+ r.setW(BigInteger.valueOf(right));
+ r.setType(STTblWidth.DXA);
+ }
+
+ /** 行高(atLeast)+ 可选 cantSplit */
+ private void setRow(XWPFTableRow row, Integer height, boolean cantSplit) {
+ // 旧版 poi-ooxml-schemas 中 w:tr 的类型是 CTRow(非 CTr),
+ // 且 trPr 内元素为数组式访问器(sizeOfXxxArray/getXxxArray/addNewXxx)
+ CTRow ctRow = row.getCtRow();
+ CTTrPr trPr = ctRow.isSetTrPr() ? ctRow.getTrPr() : ctRow.addNewTrPr();
+ if (cantSplit && trPr.sizeOfCantSplitArray() == 0) {
+ trPr.addNewCantSplit();
+ }
+ if (height != null) {
+ CTHeight h = trPr.sizeOfTrHeightArray() > 0 ? trPr.getTrHeightArray(0) : trPr.addNewTrHeight();
+ h.setVal(BigInteger.valueOf(height));
+ h.setHRule(STHeightRule.AT_LEAST);
+ }
+ }
+
+ /** 行内水平居中(封面签署表用) */
+ private void setRowJcCenter(XWPFTableRow row) {
+ CTRow ctRow = row.getCtRow();
+ CTTrPr trPr = ctRow.isSetTrPr() ? ctRow.getTrPr() : ctRow.addNewTrPr();
+ CTJc jc = trPr.sizeOfJcArray() > 0 ? trPr.getJcArray(0) : trPr.addNewJc();
+ jc.setVal(STJc.CENTER);
+ }
+
+ /** 水平合并 row 第 from..to 列(gridSpan + 移除多余单元格) */
+ private void mergeRowCells(XWPFTable table, int rowIdx, int from, int to) {
+ XWPFTableRow row = table.getRow(rowIdx);
+ XWPFTableCell first = row.getCell(from);
+ CTTcPr tcPr = first.getCTTc().isSetTcPr() ? first.getCTTc().getTcPr() : first.getCTTc().addNewTcPr();
+ CTDecimalNumber gs = tcPr.isSetGridSpan() ? tcPr.getGridSpan() : tcPr.addNewGridSpan();
+ gs.setVal(BigInteger.valueOf(to - from + 1));
+ List tcs = row.getCtRow().getTcList();
+ for (int i = to; i > from; i--) {
+ tcs.remove(i);
+ }
+ }
+
+ /** 表头单元格:深蓝底 + 白字 + 加粗 + 居中 */
+ private void headerCell(XWPFTableCell cell, String text, int tcw) {
+ styleCell(cell, text, FONT_BODY, SZ_BODY, true, false, COLOR_WHITE, true,
+ ParagraphAlignment.CENTER, 0, 0, tcw, null, 0, CELL_MAR, 0, CELL_MAR);
+ }
+
+ /** 正文单元格:仿宋 12pt,可选居中/行前段后间距 */
+ private void bodyCell(XWPFTableCell cell, String text, int tcw, boolean center,
+ int before, int after) {
+ styleCell(cell, text, FONT_BODY, SZ_BODY, false, false, null, false,
+ center ? ParagraphAlignment.CENTER : ParagraphAlignment.LEFT,
+ before, after, tcw, null, 0, CELL_MAR, 0, CELL_MAR);
+ }
+
+ private void bodyCell(XWPFTableCell cell, String text, int tcw, boolean center) {
+ bodyCell(cell, text, tcw, center, 0, 0);
+ }
+
+ private void bodyCell(XWPFTableCell cell, String text, int tcw) {
+ bodyCell(cell, text, tcw, false, 0, 0);
+ }
+
+ /** 图片单元格:细边框 + 垂直居中 + 图片(保持模板尺寸);无图时输出横杠占位 */
+ private void imageCell(XWPFTableCell cell, Path imagePath, long w, long h, int tcw) {
+ styleCell(cell, "", FONT_BODY, SZ_BODY, false, false, null, false,
+ ParagraphAlignment.LEFT, 120, 120, tcw, null, 0, CELL_MAR, 0, CELL_MAR);
+ XWPFParagraph p = cell.getParagraphs().get(0);
+ if (imagePath == null) {
+ // 无图片时以横杠占位,避免空白单元格
+ XWPFRun dashRun = p.createRun();
+ setRunFont(dashRun, "--", FONT_BODY, SZ_BODY, false, false, null, false);
+ return;
+ }
+ XWPFRun run = p.createRun();
+ try (InputStream is = Files.newInputStream(imagePath)) {
+ run.addPicture(is, XWPFDocument.PICTURE_TYPE_JPEG, "img.jpg", (int) w, (int) h);
+ } catch (Exception e) {
+ logger.warn("插入巡检图片失败: {}", imagePath, e);
+ }
+ }
+
+ /**
+ * 通用单元格样式:B4C6E7 细边框(sz=0)、垂直居中、可选深蓝底、
+ * 列宽、gridSpan、单元格边距
+ */
+ private void styleCell(XWPFTableCell cell, String text, String font, int halfPoints,
+ boolean bold, boolean italic, String color, boolean shade,
+ ParagraphAlignment align, int before, int after,
+ Integer tcw, Integer gridSpan,
+ int marTop, int marLeft, int marBottom, int marRight) {
+ CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
+
+ if (tcw != null) {
+ CTTblWidth w = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
+ w.setW(BigInteger.valueOf(tcw));
+ w.setType(STTblWidth.DXA);
+ }
+ if (gridSpan != null && gridSpan > 1) {
+ CTDecimalNumber gs = tcPr.isSetGridSpan() ? tcPr.getGridSpan() : tcPr.addNewGridSpan();
+ gs.setVal(BigInteger.valueOf(gridSpan));
+ }
+ CTTcBorders borders = tcPr.isSetTcBorders() ? tcPr.getTcBorders() : tcPr.addNewTcBorders();
+ setBorders(borders);
+ if (shade) {
+ CTShd shd = tcPr.isSetShd() ? tcPr.getShd() : tcPr.addNewShd();
+ shd.setVal(STShd.CLEAR);
+ shd.setColor("auto");
+ shd.setFill(COLOR_DARK);
+ }
+ CTTcMar tcMar = tcPr.isSetTcMar() ? tcPr.getTcMar() : tcPr.addNewTcMar();
+ setCellMar(tcMar, marTop, marLeft, marBottom, marRight);
+ CTVerticalJc vAlign = tcPr.isSetVAlign() ? tcPr.getVAlign() : tcPr.addNewVAlign();
+ vAlign.setVal(STVerticalJc.CENTER);
+
+ // 段落:清空默认段落,写入第一个段落
+ List ctPList = cell.getCTTc().getPList();
+ for (int i = ctPList.size() - 1; i > 0; i--) {
+ cell.getCTTc().removeP(i);
+ }
+ XWPFParagraph p = cell.getParagraphs().get(0);
+ for (int i = p.getRuns().size() - 1; i >= 0; i--) {
+ p.removeRun(i);
+ }
+ p.setAlignment(align);
+ p.setSpacingBefore(before);
+ p.setSpacingAfter(after);
+ // 表格内段落与模板一致:单倍行距(w:line=240 auto),不受 Normal 样式 1.15 倍影响
+ CTPPr cellPPr = p.getCTP().isSetPPr() ? p.getCTP().getPPr() : p.getCTP().addNewPPr();
+ CTSpacing cellSpacing = cellPPr.isSetSpacing() ? cellPPr.getSpacing() : cellPPr.addNewSpacing();
+ cellSpacing.setLine(BigInteger.valueOf(240));
+ cellSpacing.setLineRule(STLineSpacingRule.AUTO);
+ if (text != null && !text.isEmpty()) {
+ XWPFRun run = p.createRun();
+ setRunFont(run, text, font, halfPoints, bold, italic, color, false);
+ }
+ }
+
+ // ============================================================================
+ // 五、数据小工具
+ // ============================================================================
+
+ /**
+ * 空值/空白字符串转为占位横杠(--),避免报告中出现空白单元格;
+ * 非空则原样返回。
+ */
+ private String val(String s) {
+ return (s == null || s.trim().isEmpty()) ? "--" : s;
+ }
+
+ private long countByStatus(List list, String status) {
+ if (list == null) {
+ return 0;
+ }
+ return list.stream().filter(d -> status.equals(d.getPointStatus())).count();
+ }
+
+ private long distinctCount(List list,
+ java.util.function.Function getter) {
+ if (list == null) {
+ return 0;
+ }
+ return list.stream().map(getter).filter(StringUtils::isNotEmpty).distinct().count();
+ }
+
+ /** 点位第一张图片的文件名(imgType=0),无则空串 */
+ private String firstImageName(InspectionReportData d) {
+ if (d.getReportImgList() == null) {
+ return "";
+ }
+ for (InspectionReportImg img : d.getReportImgList()) {
+ if ("0".equals(img.getImgType()) && StringUtils.isNotEmpty(img.getImgSrc())) {
+ String src = img.getImgSrc();
+ int idx = src.lastIndexOf('/');
+ return idx >= 0 ? src.substring(idx + 1) : src;
+ }
+ }
+ return "";
+ }
+
+ /** 点位第 idx 张图片(imgType=0)的临时文件,无则 null */
+ private Path firstImagePath(InspectionReportData d, Map tempFileMap, int idx) {
+ if (d.getReportImgList() == null) {
+ return null;
+ }
+ int found = 0;
+ for (InspectionReportImg img : d.getReportImgList()) {
+ if ("0".equals(img.getImgType()) && StringUtils.isNotEmpty(img.getImgSrc())) {
+ if (found == idx) {
+ return tempFileMap.get(img.getImgSrc());
+ }
+ found++;
+ }
+ }
+ return null;
+ }
+
+ /** 点位第 idx 张图片(imgType=0)的临时文件,无则 null */
+ private Path getImagePath(InspectionReportData d, Map tempFileMap, int idx) {
+ if (d.getReportImgList() == null) {
+ return null;
+ }
+ int found = 0;
+ for (InspectionReportImg img : d.getReportImgList()) {
+ if ("0".equals(img.getImgType()) && StringUtils.isNotEmpty(img.getImgSrc())) {
+ if (found == idx) {
+ return tempFileMap.get(img.getImgSrc());
+ }
+ found++;
+ }
+ }
+ return null;
+ }
+
+
+ private String formatDateTime(Date date) {
+ return date != null ? DateUtils.format(DateUtils.yyyyMMddHHmmss2, date) : "";
+ }
+
+ // ============================================================================
+ // 六、上传 / 资源工具(与 PatrolTaskResultMainController 保持一致)
+ // ============================================================================
+
+ private void uploadFile(String basePath, String fileName, String suffix,
+ IOConsumer writer) {
+ SftpUploadEntity sftpUploadEntity = new SftpUploadEntity();
+ sftpUploadEntity.setFilePath(basePath);
+ sftpUploadEntity.setFileName(fileName);
+ sftpUploadEntity.setSuffix(suffix);
+ try {
+ String filePath = sftpClient.upload(sftpUploadEntity, outputStream -> {
+ writer.accept(outputStream);
+ outputStream.flush();
+ });
+ logger.info("文件上传成功: {}", filePath);
+ } catch (Exception e) {
+ logger.error("文件上传失败", e);
+ }
+ }
+
+ private Path downloadImageToTemp(String imagePath) {
+ try {
+ if (imagePath == null) {
+ return null;
+ }
+ Path tempFile = Files.createTempFile("inspect_img_", ".jpg");
+ sftpClient.downLoad(imagePath, inputStream ->
+ Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING));
+ return tempFile;
+ } catch (Exception e) {
+ logger.warn("图片下载到临时文件失败: {}", imagePath, e);
+ return null;
+ }
+ }
+
+ /**
+ * 打开 logo 图片流:优先按 classpath 资源加载(相对路径,图片随包发布),
+ * 找不到时兼容本地绝对/相对文件路径;两者都无则返回 null。
+ */
+ private InputStream openLogoStream(String logoPath) throws IOException {
+ InputStream is = getClass().getClassLoader().getResourceAsStream(logoPath);
+ if (is != null) {
+ return is;
+ }
+ Path p = Paths.get(logoPath);
+ if (Files.exists(p)) {
+ return Files.newInputStream(p);
+ }
+ logger.warn("logo 资源不存在: classpath={}, file={}", logoPath, p.toAbsolutePath());
+ return null;
+ }
+
+ private void closeResources(XWPFDocument wordDocument, List tempFiles) {
+ if (wordDocument != null) {
+ try {
+ wordDocument.close();
+ logger.info("XWPFDocument 已关闭");
+ } catch (IOException e) {
+ logger.error("关闭XWPFDocument失败", e);
+ }
+ }
+ for (Path tempFile : tempFiles) {
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (IOException e) {
+ logger.warn("删除临时文件失败: {}", tempFile, e);
+ }
+ }
+ }
+}
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/PatrolTaskResultMainController.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/PatrolTaskResultMainController.java
index a9ef223..a6cfa70 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/PatrolTaskResultMainController.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/PatrolTaskResultMainController.java
@@ -263,7 +263,7 @@ public class PatrolTaskResultMainController extends BaseController {
}
- @PostMapping({"/updateInfo_shaoxing"})
+ @PostMapping({"/updateInfo_shaoxing1"})
public AjaxResult updateInfo_shaoxing(
@RequestBody PatrolTaskResultMain resultMain) {
long startTime = System.currentTimeMillis();
diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/taskstatus/service/impl/PatrolTaskStatusServiceImpl.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/taskstatus/service/impl/PatrolTaskStatusServiceImpl.java
index 1652865..fa67491 100644
--- a/inspect-main/inspect-main-task/src/main/java/com/inspect/taskstatus/service/impl/PatrolTaskStatusServiceImpl.java
+++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/taskstatus/service/impl/PatrolTaskStatusServiceImpl.java
@@ -22,6 +22,7 @@ import com.inspect.taskstatus.mapper.PatrolTaskStatusMapper;
import com.inspect.taskstatus.service.IPatrolTaskStatusService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -102,6 +103,7 @@ public class PatrolTaskStatusServiceImpl implements IPatrolTaskStatusService {
return this.patrolTaskStatusMapper.updatePatrolTaskStatusOfTaskState(patrolTaskStatus);
}
+ @TaskStatusChange(targetState = "1", async = true) // "1"表示已完成状态
public int updatePatrolTaskStatusOfTaskStateAndEndTime(PatrolTaskStatus patrolTaskStatus) {
return this.patrolTaskStatusMapper.updatePatrolTaskStatusOfTaskStateAndEndTime(patrolTaskStatus);
}
@@ -119,6 +121,7 @@ public class PatrolTaskStatusServiceImpl implements IPatrolTaskStatusService {
}
@Override
+ @TaskStatusChange(targetState = "1", async = true) // "1"表示已完成状态
public int insertIgnorePatrolTaskStatus(PatrolTaskStatus patrolTaskStatusMain) {
return this.patrolTaskStatusMapper.insertIgnorePatrolTaskStatus(patrolTaskStatusMain);
}
@@ -158,8 +161,8 @@ public class PatrolTaskStatusServiceImpl implements IPatrolTaskStatusService {
patrolTaskStatus.setTaskState(state);
setNormalTaskStatus(patrolTaskStatus);
- // 更新子任务或普通任务
- updatePatrolTaskStatus(patrolTaskStatus);
+ // 更新子任务或普通任务(走代理调用,确保 @TaskStatusChange 切面在状态变为完成时触发)
+ ((IPatrolTaskStatusService) AopContext.currentProxy()).updatePatrolTaskStatus(patrolTaskStatus);
log.info("updateTaskProgress: {}", patrolTaskStatus);
// 如果是子任务,需要同步更新主任务
if (TaskType.SUB.getCode().equals(patrolTaskStatus.getTaskType()) && patrolTaskStatus.getMainId() != null) {
@@ -239,8 +242,8 @@ public class PatrolTaskStatusServiceImpl implements IPatrolTaskStatusService {
mainTask.setTaskEstimatedTime(mainAlgorithmProgress.toString());
setNormalTaskStatus(mainTask);
- // 更新主任务
- patrolTaskStatusMapper.updatePatrolTaskStatus(mainTask);
+ // 更新主任务(走代理调用,确保 @TaskStatusChange 切面在联合任务完成时触发)
+ ((IPatrolTaskStatusService) AopContext.currentProxy()).updatePatrolTaskStatus(mainTask);
}
// 生成巡视归档记录
diff --git a/inspect-main/inspect-main-task/src/main/resources/fonts/simfang.ttf b/inspect-main/inspect-main-task/src/main/resources/fonts/simfang.ttf
new file mode 100644
index 0000000..68334e2
Binary files /dev/null and b/inspect-main/inspect-main-task/src/main/resources/fonts/simfang.ttf differ
diff --git a/inspect-main/inspect-main-task/src/main/resources/fonts/simhei.ttf b/inspect-main/inspect-main-task/src/main/resources/fonts/simhei.ttf
new file mode 100644
index 0000000..5bd4687
Binary files /dev/null and b/inspect-main/inspect-main-task/src/main/resources/fonts/simhei.ttf differ
diff --git a/inspect-main/inspect-main-task/src/main/resources/fonts/方正小标宋简体.ttf b/inspect-main/inspect-main-task/src/main/resources/fonts/方正小标宋简体.ttf
new file mode 100644
index 0000000..0161fcc
Binary files /dev/null and b/inspect-main/inspect-main-task/src/main/resources/fonts/方正小标宋简体.ttf differ
diff --git a/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportDataMapper.xml b/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportDataMapper.xml
index 34cd8b7..ef3bbf0 100644
--- a/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportDataMapper.xml
+++ b/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportDataMapper.xml
@@ -15,6 +15,7 @@
+
@@ -36,6 +37,7 @@
data_sources,
acquisition_time,
inspection_results,
+ inspection_value,
point_status,
inspection_img,
alg_name,
@@ -164,11 +166,11 @@
insert into inspection_report_data
- (line_id,code,report_id,area,time_interval,eq_name,parts,point_name,data_sources,acquisition_time,inspection_results,point_status,inspection_img,alg_name,task_name,task_patrolled_id,task_result_id)
+ (line_id,code,report_id,area,time_interval,eq_name,parts,point_name,data_sources,acquisition_time,inspection_results,inspection_value,point_status,inspection_img,alg_name,task_name,task_patrolled_id,task_result_id)
values
(#{t.lineId}, #{t.code}, #{t.reportId}, #{t.area}, #{t.timeInterval}, #{t.eqName}, #{t.parts},
- #{t.pointName},#{t.dataSources},#{t.acquisitionTime},#{t.inspectionResults},#{t.pointStatus},#{t.inspectionImg},#{t.algName},#{t.taskName},#{t.taskPatrolledId},#{t.taskResultId})
+ #{t.pointName},#{t.dataSources},#{t.acquisitionTime},#{t.inspectionResults},#{t.inspectionValue},#{t.pointStatus},#{t.inspectionImg},#{t.algName},#{t.taskName},#{t.taskPatrolledId},#{t.taskResultId})
diff --git a/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportImgMapper.xml b/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportImgMapper.xml
index 9a0a4e5..360f1cc 100644
--- a/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportImgMapper.xml
+++ b/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportImgMapper.xml
@@ -9,12 +9,13 @@
+
- select line_id, report_info_id, img, img_src, creat_time, img_type
+ select line_id, report_info_id, img, img_src, image_normal_url_path, creat_time, img_type
from inspection_report_img
@@ -87,9 +88,9 @@
- insert into inspection_report_img (line_id, report_info_id, img, img_src, creat_time, img_type) values
+ insert into inspection_report_img (line_id, report_info_id, img, img_src, image_normal_url_path, creat_time, img_type) values
- (#{t.lineId}, #{t.reportInfoId}, #{t.img}, #{t.imgSrc}, #{t.creatTime}, #{t.imgType})
+ (#{t.lineId}, #{t.reportInfoId}, #{t.img}, #{t.imgSrc}, #{t.imageNormalUrlPath}, #{t.creatTime}, #{t.imgType})
diff --git a/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportMapper.xml b/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportMapper.xml
index 19fd2bf..5569dd8 100644
--- a/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportMapper.xml
+++ b/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportMapper.xml
@@ -23,6 +23,7 @@
+
@@ -34,6 +35,7 @@
station_name,
volt_level,
station_type,
+ location,
inspection_date,
inspection_task_name,
env_info,
@@ -113,6 +115,7 @@
station_name,
volt_level,
station_type,
+ location,
inspection_date,
inspection_task_name,
env_info,
@@ -132,6 +135,7 @@
#{stationName},
#{voltLevel},
#{stationType},
+ #{location},
#{inspectionDate},
#{inspectionTaskName},
#{envInfo},
@@ -155,6 +159,7 @@
station_name = #{stationName},
volt_level = #{voltLevel},
station_type = #{stationType},
+ location = #{location},
inspection_date = #{inspectionDate},
inspection_task_name = #{inspectionTaskName},
env_info = #{envInfo},
diff --git a/inspect-main/inspect-main-task/src/main/resources/mapper/task/PatrolResultMapper.xml b/inspect-main/inspect-main-task/src/main/resources/mapper/task/PatrolResultMapper.xml
index d319d34..641be86 100644
--- a/inspect-main/inspect-main-task/src/main/resources/mapper/task/PatrolResultMapper.xml
+++ b/inspect-main/inspect-main-task/src/main/resources/mapper/task/PatrolResultMapper.xml
@@ -346,6 +346,116 @@
ORDER BY line_id desc
+
+
diff --git a/inspect-main/inspect-main-task/src/main/resources/report/logo.png b/inspect-main/inspect-main-task/src/main/resources/report/logo.png
new file mode 100644
index 0000000..a83f630
Binary files /dev/null and b/inspect-main/inspect-main-task/src/main/resources/report/logo.png differ
diff --git a/inspect-main/inspect-main-task/src/main/resources/report/wordparts/fontTable.xml b/inspect-main/inspect-main-task/src/main/resources/report/wordparts/fontTable.xml
new file mode 100644
index 0000000..637d657
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/resources/report/wordparts/fontTable.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/inspect-main/inspect-main-task/src/main/resources/report/wordparts/styles.xml b/inspect-main/inspect-main-task/src/main/resources/report/wordparts/styles.xml
new file mode 100644
index 0000000..3eb86c3
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/resources/report/wordparts/styles.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/inspect-main/inspect-main-task/src/main/resources/report/wordparts/theme1.xml b/inspect-main/inspect-main-task/src/main/resources/report/wordparts/theme1.xml
new file mode 100644
index 0000000..67f3205
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/resources/report/wordparts/theme1.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/inspect-main/inspect-main-task/src/main/resources/report/wordparts/webSettings.xml b/inspect-main/inspect-main-task/src/main/resources/report/wordparts/webSettings.xml
new file mode 100644
index 0000000..b5d1b1e
--- /dev/null
+++ b/inspect-main/inspect-main-task/src/main/resources/report/wordparts/webSettings.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/inspect-metadata/src/main/java/com/inspect/metadata/eqpbook/domain/BasedataEqpBookChannel.java b/inspect-metadata/src/main/java/com/inspect/metadata/eqpbook/domain/BasedataEqpBookChannel.java
index 90001ad..7824b33 100644
--- a/inspect-metadata/src/main/java/com/inspect/metadata/eqpbook/domain/BasedataEqpBookChannel.java
+++ b/inspect-metadata/src/main/java/com/inspect/metadata/eqpbook/domain/BasedataEqpBookChannel.java
@@ -93,6 +93,8 @@ public class BasedataEqpBookChannel extends BaseEntity {
private String areaName;
+ private Long areaId;
+
public String toString() {
return (new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)).append("channelId", this.getChannelId()).append("channelCode", this.getChannelCode()).append("channelName", this.getChannelName()).append("parentChannelId", this.getParentChannelId()).append("eqpbookId", this.getEqpBookId()).append("channelType", this.getChannelType()).append("onlineState", this.getOnlineState()).append("channelContent", this.getChannelContent()).append("provider", this.getProvider()).append("address", this.getAddress()).append("host", this.getHost()).append("port", this.getPort()).append("createBy", this.getCreateBy()).append("createTime", this.getCreateTime()).append("updateBy", this.getUpdateBy()).append("updateTime", this.getUpdateTime()).append("deleteFlag", this.getDeleteFlag()).append("patroldeviceCode", this.getPatrolDeviceCode()).append("username", this.getUsername()).append("password", this.getPassword()).toString();
}
diff --git a/inspect-metadata/src/main/resources/mapper/eqpbook/BasedataEqpBookChannelMapper.xml b/inspect-metadata/src/main/resources/mapper/eqpbook/BasedataEqpBookChannelMapper.xml
index fe7ed92..e98c59b 100644
--- a/inspect-metadata/src/main/resources/mapper/eqpbook/BasedataEqpBookChannelMapper.xml
+++ b/inspect-metadata/src/main/resources/mapper/eqpbook/BasedataEqpBookChannelMapper.xml
@@ -5,6 +5,10 @@
+
+
+
+
@@ -58,6 +62,10 @@