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 f100c04..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 @@ -28,6 +28,24 @@ public class RabbitMQConfig { 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 public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); 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/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
+ * 原理(Word 的"嵌入字体"机制,让查看方电脑无需安装字体):
+ *
+ * 注意: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
index 2198d18..c250944 100644
--- 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
@@ -20,38 +20,24 @@ public class ReportPushProperties {
/** 是否启用自动归档 + 推送 */
private boolean enabled = true;
-// /** 外部平台网关基础路径(/ext_auth/getAuthToken 获取 Joinbright-Token) */
-// private String baseUrl;
-//
-// /** 外部平台 websocket 地址 */
-// private String wsUrl;
-//
-// /** 认证凭据(现场提供) */
-// private String clientId;
-//
-// private String clientSecret;
-//
-// /** token 缓存时长(秒) */
-// private long tokenCacheSeconds = 7200;
-//
-// /** websocket 连接超时(秒) */
-// private int connectTimeoutSeconds = 10;
-//
-// /** 发送后等待时间(毫秒) */
-// private long sendWaitMillis = 500;
-
/** 自动归档审核人 */
private String checkPerson = "admin";
/** 自动归档巡视结论 */
private String taskResult = "巡检完成,自动归档";
- /** 无缺陷数据时是否仍归档(false: 与手动归档一致,无缺陷不归档) */
- private boolean archiveWithoutDefect = false;
-
/** 任务状态更新事务提交等待时间(毫秒) */
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;
+
+/**
+ * 报告推送调试接口(对接外部平台-盐源新能源智慧感知平台《巡检报告接口文档》)
+ *
- * 报告归档(Word)生成完毕后,组装 report_push 消息(沿用 AlarmMessage 结构),
- * 通过 {@link AlarmPublisher} 发布到 alarm.exchange,
- * 由 inspect-main-api 的 AlarmConsumer → {@code AlarmWsConnection} 推送到外部平台
- * (ws://…/jb-base-socket?Joinbright-Token=…)。
+ * 报告归档(Word)生成完毕后,组装 reportInfo 并发布 report.upload.queue 消息,
+ * 消费者下载报告 docx 后 POST /jb-nessp-external/api/uploadReport 上传
+ * (multipart/form-data:file + reportInfo,请求头携带 joinbright-token)。
*
* @author inspect
*/
@@ -33,20 +30,19 @@ import java.util.UUID;
@Service
public class ReportPushService {
- private static final DateTimeFormatter TIME_FORMATTER =
- DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-
private final ReportPushProperties properties;
- private final AlarmPublisher alarmPublisher;
- private final MessageUtils messageUtils;
+ private final ReportPushPublisher reportPushPublisher;
+ private final PatrolTaskMapper patrolTaskMapper;
+ private final PatrolResultMapper patrolResultMapper;
- @Autowired
public ReportPushService(ReportPushProperties properties,
- AlarmPublisher alarmPublisher,
- MessageUtils messageUtils) {
+ ReportPushPublisher reportPushPublisher,
+ PatrolTaskMapper patrolTaskMapper,
+ PatrolResultMapper patrolResultMapper) {
this.properties = properties;
- this.alarmPublisher = alarmPublisher;
- this.messageUtils = messageUtils;
+ this.reportPushPublisher = reportPushPublisher;
+ this.patrolTaskMapper = patrolTaskMapper;
+ this.patrolResultMapper = patrolResultMapper;
}
/**
@@ -55,6 +51,16 @@ public class ReportPushService {
* @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;
}
@@ -62,69 +68,81 @@ public class ReportPushService {
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 {
- AlarmMessage message = AlarmMessage.builder()
- .messageId(UUID.randomUUID().toString().replace("-", ""))
- .messageType(MessageType.REPORT.getCode())
- .timestamp(LocalDateTime.now().format(TIME_FORMATTER))
- .systemCode(SystemCode.PATROL.getCode())
- .data(buildReportData(report))
- .build();
- alarmPublisher.publish(message);
- log.info("[REPORT_PUSH] 报告推送消息已发布: reportId={}, taskName={}, filePath={}",
- report.getLineId(), report.getInspectionTaskName(), report.getFilePath());
+ String stationNo = patrolTaskMapper.selectStationNo();
+ return stationNo == null ? "" : stationNo;
} catch (Exception e) {
- log.error("[REPORT_PUSH] 报告推送失败: reportId={}", report.getLineId(), e);
+ log.warn("[REPORT_PUSH] 查询场站编号失败: {}", e.getMessage());
+ return "";
}
}
/**
- * 组装推送 data 载荷:报告基本信息 + 状态统计
+ * 电站名称:报告记录优先,其次 basedata_station.station_name
*/
- private JSONObject buildReportData(InspectionReport report) {
- JSONObject data = new JSONObject();
- data.put("reportId", report.getLineId());
- data.put("reportName", report.getInspectionTaskName());
- data.put("filePath", report.getFilePath());
- data.put("taskId", report.getTaskId());
- data.put("taskPatrolledId", report.getTaskPatrolledId());
- data.put("stationName", report.getStationName());
- data.put("areaName", report.getAreaName());
- data.put("checkPerson", report.getCheckPerson());
- data.put("description", report.getDescription());
- if (report.getInspectionStartTime() != null) {
- data.put("inspectionStartTime", TIME_FORMATTER.format(
- LocalDateTime.ofInstant(report.getInspectionStartTime().toInstant(),
- java.time.ZoneId.systemDefault())));
+ private String resolveStationName(InspectionReport report) {
+ if (report != null && StringUtils.isNotEmpty(report.getStationName())) {
+ return report.getStationName();
}
- if (report.getInspectionEndTime() != null) {
- data.put("inspectionEndTime", TIME_FORMATTER.format(
- LocalDateTime.ofInstant(report.getInspectionEndTime().toInstant(),
- java.time.ZoneId.systemDefault())));
- }
- // 状态统计
- String defect = messageUtils.get("缺陷");
- String abnormal = messageUtils.get("异常");
- String normal = messageUtils.get("正常");
- int defectCount = 0, abnormalCount = 0, normalCount = 0, total = 0;
- List
+ *
+ * 实现要点:
+ *
+ *
+ */
+@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
+ *
+ */
+@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
index ae48e7e..b58b19e 100644
--- 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
@@ -141,13 +141,6 @@ public class ReportAutoArchiveService {
}
}
- // 无缺陷不归档(与手动归档一致)
-// if (!properties.isArchiveWithoutDefect() && !hasDefect(taskPatrolledIds)) {
-// log.info("[AUTO_ARCHIVE] 任务 {} 无缺陷数据,不归档(archive-without-defect=false)", archiveTaskPatrolledId);
-// releaseLock(lockKey);
-// return;
-// }
-
// 触发归档(动环 Word 报告),报告生成后由 exportDonghuanReport 回调推送
PatrolTaskResultMain resultMain = new PatrolTaskResultMain();
resultMain.setTaskPatrolledIds(taskPatrolledIds);
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
index ce4ea65..75d741e 100644
--- 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
@@ -1,31 +1,28 @@
package com.inspect.reportpush.service;
import com.alibaba.fastjson.JSONObject;
-import com.inspect.api.domain.AlarmMessage;
-import com.inspect.api.enums.MessageType;
-import com.inspect.api.enums.SystemCode;
-import com.inspect.api.service.AlarmPublisher;
+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.insreportdata.domain.InspectionReportData;
-import com.inspect.message.MessageUtils;
+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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
-import org.springframework.util.CollectionUtils;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-import java.util.UUID;
+import java.util.Date;
+import java.util.Map;
/**
- * 巡检报告 websocket 推送
+ * 巡检报告推送服务(先上报 RabbitMQ,由 inspect-main-api 的
+ * {@link com.inspect.api.service.ReportUploadConsumer} 异步 HTTP 上传到外部平台,
+ * 见《盐源新能源智慧感知平台_巡检报告接口文档 v1.0.0》)。
* > saveReportFuture = CompletableFuture.
>supplyAsync(() -> {
List