From c680e9ae370e75fbf90d54192d0f6cf2e8fc619a Mon Sep 17 00:00:00 2001
From: xiebin <vip_xiaobin810@163.com>
Date: Thu, 22 Jan 2026 16:58:33 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'
---
drone-service/drone-gd/pom.xml | 79 ++++
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/IGdPatrolTaskService.java | 9
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdPatrolReportWordUtil.java | 656 +++++++++++++++++++++++++++++++++++++++++
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdQrCodeUtil.java | 139 ++++++++
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/impl/GdPatrolTaskServiceImpl.java | 71 ++++
5 files changed, 951 insertions(+), 3 deletions(-)
diff --git a/drone-service/drone-gd/pom.xml b/drone-service/drone-gd/pom.xml
index 9edbe5d..48880e4 100644
--- a/drone-service/drone-gd/pom.xml
+++ b/drone-service/drone-gd/pom.xml
@@ -13,6 +13,37 @@
<name>${project.artifactId}</name>
<packaging>jar</packaging>
+ <properties>
+ <poi.version>5.2.5</poi.version>
+ <commons-compress.version>1.25.0</commons-compress.version>
+ <xmlbeans.version>5.2.0</xmlbeans.version>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi</artifactId>
+ <version>${poi.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi-ooxml</artifactId>
+ <version>${poi.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-compress</artifactId>
+ <version>${commons-compress.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.xmlbeans</groupId>
+ <artifactId>xmlbeans</artifactId>
+ <version>${xmlbeans.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
<dependencies>
<dependency>
<groupId>org.springblade</groupId>
@@ -25,6 +56,28 @@
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-excel</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi-ooxml</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi-ooxml-schemas</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-compress</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.xmlbeans</groupId>
+ <artifactId>xmlbeans</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
@@ -34,6 +87,32 @@
<groupId>org.springblade</groupId>
<artifactId>drone-system-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.poi</groupId>
+ <artifactId>poi-ooxml</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.xmlbeans</groupId>
+ <artifactId>xmlbeans</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-compress</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.zxing</groupId>
+ <artifactId>core</artifactId>
+ <version>3.5.3</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.zxing</groupId>
+ <artifactId>javase</artifactId>
+ <version>3.5.3</version>
+ </dependency>
</dependencies>
<build>
diff --git a/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/IGdPatrolTaskService.java b/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/IGdPatrolTaskService.java
index 9050522..bf470d7 100644
--- a/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/IGdPatrolTaskService.java
+++ b/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/IGdPatrolTaskService.java
@@ -25,6 +25,7 @@
import org.sxkj.gd.workorder.excel.GdPatrolTaskExcel;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
+import java.io.File;
import java.util.List;
/**
@@ -73,4 +74,12 @@
*/
boolean republishPatrolTask(GdPatrolTaskEntity taskEntity);
+ /**
+ * 生成巡查报告
+ *
+ * @param patrolTaskId 巡查任务主键
+ * @return 报告文件
+ */
+ File exportPatrolReport(Long patrolTaskId);
+
}
diff --git a/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/impl/GdPatrolTaskServiceImpl.java b/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/impl/GdPatrolTaskServiceImpl.java
index d6fce0c..eb7fc38 100644
--- a/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/impl/GdPatrolTaskServiceImpl.java
+++ b/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/impl/GdPatrolTaskServiceImpl.java
@@ -28,7 +28,11 @@
import org.springframework.transaction.annotation.Transactional;
import org.sxkj.common.constant.WordOrderConstant;
import org.sxkj.common.utils.OrderNumUtils;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.constant.BladeConstant;
+import org.springblade.core.tool.utils.StringUtil;
import org.sxkj.gd.workorder.entity.GdPatrolTaskEntity;
+import org.sxkj.gd.workorder.entity.GdTaskResultEntity;
import org.sxkj.gd.workorder.entity.GdWorkOrderEntity;
import org.sxkj.gd.workorder.entity.GdWorkOrderFlowEntity;
import org.sxkj.gd.workorder.enums.PatrolTaskStatusEnum;
@@ -39,9 +43,16 @@
import org.sxkj.gd.workorder.param.GdPatrolTaskAuditParam;
import org.sxkj.gd.workorder.param.GdPatrolTaskPageParam;
import org.sxkj.gd.workorder.service.IGdPatrolTaskService;
+import org.sxkj.gd.workorder.service.IGdTaskResultService;
import org.sxkj.gd.workorder.service.IGdWorkOrderFlowService;
+import org.sxkj.gd.workorder.utils.GdPatrolReportWordUtil;
import org.sxkj.gd.workorder.vo.GdPatrolTaskVO;
+import org.sxkj.system.entity.Dept;
+import org.sxkj.system.entity.User;
+import org.sxkj.system.feign.ISysClient;
+import org.sxkj.system.feign.IUserClient;
+import java.io.File;
import java.util.Date;
import java.util.List;
@@ -59,7 +70,12 @@
// @Autowired
// private IGdWorkOrderService gdWorkOrderService;
-
+ @Autowired
+ private ISysClient sysClient;
+ @Autowired
+ private IUserClient userClient;
+ @Autowired
+ private IGdTaskResultService gdTaskResultService;
@Override
public IPage<GdPatrolTaskVO> selectGdPatrolTaskPage(IPage<GdPatrolTaskVO> page, GdPatrolTaskPageParam gdPatrolTask, List<Long> deptIdList) {
return page.setRecords(baseMapper.selectGdPatrolTaskPage(page, gdPatrolTask, deptIdList));
@@ -277,10 +293,10 @@
@Override
@Transactional(rollbackFor = Exception.class)
- public boolean republishPatrolTask(GdPatrolTaskEntity taskEntity) {
+ public boolean republishPatrolTask(GdPatrolTaskEntity taskEntity) {
if (taskEntity == null || taskEntity.getId() == null) {
throw new RuntimeException("任务信息或ID不能为空");
- }
+ }
// 1. 验证任务是否存在
GdPatrolTaskEntity existingTask = getById(taskEntity.getId());
@@ -336,6 +352,27 @@
return gdWorkOrderFlowService.save(flowEntity);
}
+
+ @Override
+ public File exportPatrolReport(Long patrolTaskId) {
+ if (patrolTaskId == null) {
+ throw new RuntimeException("巡查任务主键不能为空");
+ }
+ GdPatrolTaskEntity taskEntity = getById(patrolTaskId);
+ if (taskEntity == null) {
+ throw new RuntimeException("巡查任务不存在");
+ }
+ List<GdTaskResultEntity> resultList = gdTaskResultService.list(Wrappers.<GdTaskResultEntity>lambdaQuery()
+ .eq(GdTaskResultEntity::getPatrolTaskId, patrolTaskId)
+ .eq(GdTaskResultEntity::getIsDeleted, BladeConstant.DB_NOT_DELETED));
+ String creatorName = getUserName(taskEntity.getCreateUser());
+ String deptName = getDeptName(taskEntity.getCreateDept());
+ try {
+ return GdPatrolReportWordUtil.generateReportFile(taskEntity, resultList, creatorName, deptName);
+ } catch (Exception e) {
+ throw new RuntimeException("生成巡查报告失败", e);
+ }
+ }
/**
@@ -411,5 +448,33 @@
return baseDesc + newStatus.getDesc();
}
+ private String getDeptName(Long deptId) {
+ if (deptId == null) {
+ return "/";
+ }
+ R<Dept> deptResult = sysClient.getDept(deptId);
+ Dept dept = deptResult != null ? deptResult.getData() : null;
+ if (dept == null || StringUtil.isBlank(dept.getDeptName())) {
+ return "/";
+ }
+ return dept.getDeptName();
+ }
+
+ private String getUserName(Long userId) {
+ if (userId == null) {
+ return "/";
+ }
+ R<User> userResult = userClient.userInfoById(userId);
+ User user = userResult != null ? userResult.getData() : null;
+ if (user == null) {
+ return "/";
+ }
+ String realName = user.getRealName();
+ if (StringUtil.isBlank(realName)) {
+ realName = user.getName();
+ }
+ return StringUtil.isBlank(realName) ? "/" : realName;
+ }
+
}
diff --git a/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdPatrolReportWordUtil.java b/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdPatrolReportWordUtil.java
new file mode 100644
index 0000000..62aecde
--- /dev/null
+++ b/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdPatrolReportWordUtil.java
@@ -0,0 +1,656 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.sxkj.gd.workorder.utils;
+
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.apache.poi.util.Units;
+import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFRun;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
+import org.apache.poi.xwpf.usermodel.XWPFTableRow;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcBorders;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTextDirection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springblade.core.tool.utils.StringUtil;
+import org.sxkj.gd.workorder.entity.GdPatrolTaskEntity;
+import org.sxkj.gd.workorder.entity.GdTaskResultEntity;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+public final class GdPatrolReportWordUtil {
+
+ private static final Logger logger = LoggerFactory.getLogger(GdPatrolReportWordUtil.class);
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
+ private static final int CONNECT_TIMEOUT = 3000;
+ private static final int READ_TIMEOUT = 8000;
+
+ private static final ExecutorService RESOURCE_EXECUTOR = new ThreadPoolExecutor(
+ 8, 16, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(200),
+ new ThreadPoolExecutor.CallerRunsPolicy()
+ );
+
+ private GdPatrolReportWordUtil() {
+ }
+
+ public static File generateReportFile(GdPatrolTaskEntity taskEntity,
+ List<GdTaskResultEntity> resultList,
+ String creatorName,
+ String deptName)
+ throws IOException, InvalidFormatException, InterruptedException, ExecutionException {
+ String title = StringUtil.isBlank(taskEntity.getPatrolTaskName()) ? "巡查任务" : taskEntity.getPatrolTaskName();
+ List<GdTaskResultEntity> results = resultList == null ? Collections.emptyList() : resultList;
+ File reportFile = createReportTempFile(taskEntity);
+
+ try (XWPFDocument document = new XWPFDocument();
+ FileOutputStream fos = new FileOutputStream(reportFile)) {
+ CompletableFuture<Map<String, byte[]>> imageFuture = CompletableFuture.supplyAsync(
+ () -> preloadImages(results), RESOURCE_EXECUTOR
+ );
+ CompletableFuture<Map<String, byte[]>> qrCodeFuture = CompletableFuture.supplyAsync(
+ () -> preloadQrCodes(results), RESOURCE_EXECUTOR
+ );
+ CompletableFuture.allOf(imageFuture, qrCodeFuture).join();
+ Map<String, byte[]> imageCache = imageFuture.get();
+ Map<String, byte[]> qrCodeCache = qrCodeFuture.get();
+
+ createTitle(document, title);
+ createSectionHeader(document, "一、任务详情");
+ addTaskDetails(document, taskEntity, creatorName, deptName);
+
+ int resultCount = results.size();
+ createSectionHeader(document, "二、巡查结果(" + resultCount + "件)");
+ addResultDetails(document, results, imageCache, qrCodeCache);
+
+ document.write(fos);
+ return reportFile;
+ }
+ }
+
+ private static File createReportTempFile(GdPatrolTaskEntity taskEntity) throws IOException {
+ String taskNo = taskEntity != null ? taskEntity.getTaskNo() : null;
+ String safePrefix = buildSafeFilePrefix(taskNo);
+ return File.createTempFile(safePrefix, ".docx");
+ }
+
+ private static String buildSafeFilePrefix(String text) {
+ String baseName = StringUtil.isBlank(text) ? "gd_patrol_report" : "gd_patrol_report_" + text;
+ String safeName = baseName.replaceAll("[^a-zA-Z0-9_-]", "_");
+ if (safeName.length() < 3) {
+ safeName = "gd_patrol_report";
+ }
+ if (safeName.length() > 50) {
+ safeName = safeName.substring(0, 50);
+ }
+ return safeName + "_";
+ }
+
+ private static void addTaskDetails(XWPFDocument document,
+ GdPatrolTaskEntity taskEntity,
+ String creatorName,
+ String deptName) {
+ XWPFTable table = document.createTable(4, 2);
+ table.setWidth("100%");
+ table.getCTTbl().addNewTblGrid().addNewGridCol().setW(BigInteger.valueOf(3000));
+ table.getCTTbl().getTblGrid().addNewGridCol().setW(BigInteger.valueOf(6000));
+ table.getCTTbl().addNewTblPr().addNewTblBorders();
+
+ setTableCellWithGrayBackground(table, 0, 0, "任务编号");
+ setTableCellWithSpacing(table, 0, 1, formatString(taskEntity.getTaskNo()));
+
+ setTableCellWithGrayBackground(table, 1, 0, "创建人");
+ setTableCellWithSpacing(table, 1, 1, formatString(creatorName));
+
+ setTableCellWithGrayBackground(table, 2, 0, "所属部门");
+ setTableCellWithSpacing(table, 2, 1, formatString(deptName));
+
+ setTableCellWithGrayBackground(table, 3, 0, "任务时间");
+ Date taskTime = taskEntity.getExecuteTime() != null ? taskEntity.getExecuteTime() : taskEntity.getCreateTime();
+ setTableCellWithSpacing(table, 3, 1, formatDate(taskTime));
+ }
+
+ private static void addResultDetails(XWPFDocument document,
+ List<GdTaskResultEntity> results,
+ Map<String, byte[]> imageCache,
+ Map<String, byte[]> qrCodeCache)
+ throws IOException, InvalidFormatException {
+ if (results == null || results.isEmpty()) {
+ addDetailItem(document, "/");
+ return;
+ }
+
+ int totalRows = results.size() * 4 + (results.size() - 1);
+ XWPFTable resultTable = document.createTable(totalRows, 3);
+
+ CTTbl ctTbl = resultTable.getCTTbl();
+ CTTblGrid tblGrid = ctTbl.getTblGrid();
+ if (tblGrid == null) {
+ tblGrid = ctTbl.addNewTblGrid();
+ }
+ while (tblGrid.sizeOfGridColArray() < 3) {
+ tblGrid.addNewGridCol();
+ }
+ tblGrid.getGridColArray(0).setW(BigInteger.valueOf(600));
+ tblGrid.getGridColArray(1).setW(BigInteger.valueOf(1500));
+ tblGrid.getGridColArray(2).setW(BigInteger.valueOf(3900));
+
+ XWPFTableRow firstRow = resultTable.getRow(0);
+ firstRow.getCell(0).setWidth("600");
+ firstRow.getCell(1).setWidth("1500");
+ firstRow.getCell(2).setWidth("3900");
+
+ CTTblPr tblPr = ctTbl.getTblPr();
+ if (tblPr == null) {
+ tblPr = ctTbl.addNewTblPr();
+ }
+ CTTblWidth tblWidth = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
+ tblWidth.setW(BigInteger.valueOf(6000));
+ tblWidth.setType(STTblWidth.DXA);
+
+ int currentRow = 0;
+ for (int i = 0; i < results.size(); i++) {
+ GdTaskResultEntity result = results.get(i);
+ String resultPrefix = String.format("%02d", i + 1);
+
+ setTableCellWithSequence(resultTable, currentRow, 0, resultPrefix, 4);
+ setTableCellWithBoldAndCenter(resultTable, currentRow, 1, "成果编号");
+ setTableCellWithSpacing(resultTable, currentRow, 2, formatString(result.getResultCode()));
+ currentRow++;
+
+ setTableCellWithBoldAndCenter(resultTable, currentRow, 1, "成果地址");
+ setTableCellWithSpacing(resultTable, currentRow, 2, "/");
+ currentRow++;
+
+ setTableCellWithBoldAndCenter(resultTable, currentRow, 1, "成果定位");
+ XWPFTableCell navCell = resultTable.getRow(currentRow).getCell(2);
+ addLocationInfoToCell(navCell, result, qrCodeCache);
+ currentRow++;
+
+ setTableCellWithBoldAndCenter(resultTable, currentRow, 1, "成果图片");
+ XWPFTableCell imageCell = resultTable.getRow(currentRow).getCell(2);
+ addImageToCell(imageCell, result.getResultUrl(), imageCache);
+ currentRow++;
+
+ if (i < results.size() - 1) {
+ createUnifiedSeparatorRow(resultTable, currentRow);
+ currentRow++;
+ }
+ }
+ }
+
+ private static void createUnifiedSeparatorRow(XWPFTable table, int row) {
+ XWPFTableCell cell0 = table.getRow(row).getCell(0);
+ XWPFTableCell cell1 = table.getRow(row).getCell(1);
+ XWPFTableCell cell2 = table.getRow(row).getCell(2);
+
+ clearCellParagraphs(cell0);
+ clearCellParagraphs(cell1);
+ clearCellParagraphs(cell2);
+
+ XWPFParagraph para0 = cell0.addParagraph();
+ XWPFParagraph para1 = cell1.addParagraph();
+ XWPFParagraph para2 = cell2.addParagraph();
+
+ para0.setSpacingBefore(80);
+ para0.setSpacingAfter(80);
+ para1.setSpacingBefore(80);
+ para1.setSpacingAfter(80);
+ para2.setSpacingBefore(80);
+ para2.setSpacingAfter(80);
+
+ cell0.setColor("C0C0C0");
+ cell1.setColor("C0C0C0");
+ cell2.setColor("C0C0C0");
+
+ setCellBorders(cell0, true, false, true, true);
+ setCellBorders(cell1, false, false, true, true);
+ setCellBorders(cell2, false, true, true, true);
+
+ para0.createRun().setText("");
+ para1.createRun().setText("");
+ para2.createRun().setText("");
+ }
+
+ private static void setCellBorders(XWPFTableCell cell, boolean left, boolean right, boolean top, boolean bottom) {
+ CTTcPr tcPr = cell.getCTTc().getTcPr();
+ if (tcPr == null) {
+ tcPr = cell.getCTTc().addNewTcPr();
+ }
+ CTTcBorders borders = tcPr.isSetTcBorders() ? tcPr.getTcBorders() : tcPr.addNewTcBorders();
+
+ if (borders.getLeft() == null) {
+ borders.addNewLeft();
+ }
+ borders.getLeft().setVal(left ? STBorder.SINGLE : STBorder.NIL);
+
+ if (borders.getRight() == null) {
+ borders.addNewRight();
+ }
+ borders.getRight().setVal(right ? STBorder.SINGLE : STBorder.NIL);
+
+ if (borders.getTop() == null) {
+ borders.addNewTop();
+ }
+ borders.getTop().setVal(top ? STBorder.SINGLE : STBorder.NIL);
+
+ if (borders.getBottom() == null) {
+ borders.addNewBottom();
+ }
+ borders.getBottom().setVal(bottom ? STBorder.SINGLE : STBorder.NIL);
+ }
+
+ private static void setTableCellWithSequence(XWPFTable table, int row, int col, String text, int rowSpan) {
+ XWPFTableCell cell = table.getRow(row).getCell(col);
+ clearCellParagraphs(cell);
+ XWPFParagraph para = cell.addParagraph();
+ para.setAlignment(ParagraphAlignment.CENTER);
+ cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
+ para.setWordWrapped(false);
+
+ if (cell.getCTTc().getTcPr() == null) {
+ cell.getCTTc().addNewTcPr();
+ }
+ cell.getCTTc().getTcPr().addNewNoWrap();
+ CTTcPr tcPr = cell.getCTTc().getTcPr();
+ if (tcPr.getTextDirection() == null) {
+ tcPr.addNewTextDirection();
+ }
+ tcPr.getTextDirection().setVal(STTextDirection.LR_TB);
+
+ XWPFRun run = para.createRun();
+ run.setText(text);
+ run.setBold(true);
+
+ if (rowSpan > 1) {
+ CTTcPr mergeTcPr = cell.getCTTc().getTcPr();
+ if (mergeTcPr == null) {
+ mergeTcPr = cell.getCTTc().addNewTcPr();
+ }
+ mergeTcPr.addNewVMerge().setVal(STMerge.RESTART);
+ for (int i = 1; i < rowSpan; i++) {
+ XWPFTableCell mergeCell = table.getRow(row + i).getCell(col);
+ CTTcPr mergeCellPr = mergeCell.getCTTc().getTcPr();
+ if (mergeCellPr == null) {
+ mergeCellPr = mergeCell.getCTTc().addNewTcPr();
+ }
+ mergeCellPr.addNewVMerge().setVal(STMerge.CONTINUE);
+ mergeCell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
+ if (!mergeCell.getParagraphs().isEmpty()) {
+ mergeCell.getParagraphs().get(0).setWordWrapped(false);
+ }
+ }
+ }
+ }
+
+ private static void setTableCellWithBoldAndCenter(XWPFTable table, int row, int col, String text) {
+ XWPFTableCell cell = table.getRow(row).getCell(col);
+ clearCellParagraphs(cell);
+ XWPFParagraph para = cell.addParagraph();
+ para.setAlignment(ParagraphAlignment.CENTER);
+ cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
+ para.setWordWrapped(false);
+
+ if (cell.getCTTc().getTcPr() == null) {
+ cell.getCTTc().addNewTcPr();
+ }
+ cell.getCTTc().getTcPr().addNewNoWrap();
+
+ XWPFRun run = para.createRun();
+ run.setText(formatString(text));
+ run.setBold(true);
+ }
+
+ private static void setTableCellWithGrayBackground(XWPFTable table, int row, int col, String text) {
+ XWPFTableCell cell = table.getRow(row).getCell(col);
+ clearCellParagraphs(cell);
+ XWPFParagraph para = cell.addParagraph();
+ para.setAlignment(ParagraphAlignment.CENTER);
+ cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
+ para.setWordWrapped(false);
+
+ if (cell.getCTTc().getTcPr() == null) {
+ cell.getCTTc().addNewTcPr();
+ }
+ cell.getCTTc().getTcPr().addNewNoWrap();
+
+ XWPFRun run = para.createRun();
+ run.setText(formatString(text));
+ cell.setColor("D0D0D0");
+ }
+
+ private static void setTableCellWithSpacing(XWPFTable table, int row, int col, String text) {
+ XWPFTableCell cell = table.getRow(row).getCell(col);
+ clearCellParagraphs(cell);
+ XWPFParagraph para = cell.addParagraph();
+ cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
+ para.setWordWrapped(false);
+
+ if (cell.getCTTc().getTcPr() == null) {
+ cell.getCTTc().addNewTcPr();
+ }
+ cell.getCTTc().getTcPr().addNewNoWrap();
+
+ XWPFRun run = para.createRun();
+ String formattedText = (text != null && !"/".equals(text)) ? " " + text : formatString(text);
+ run.setText(formattedText);
+ }
+
+ private static void addLocationInfoToCell(XWPFTableCell cell,
+ GdTaskResultEntity result,
+ Map<String, byte[]> qrCodeCache)
+ throws IOException, InvalidFormatException {
+ clearCellParagraphs(cell);
+ XWPFParagraph para = cell.addParagraph();
+ cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
+ para.setWordWrapped(false);
+ para.setAlignment(ParagraphAlignment.LEFT);
+
+ if (result.getLongitude() != null && result.getLatitude() != null) {
+ String longitude = formatCoordinateValue(result.getLongitude());
+ String latitude = formatCoordinateValue(result.getLatitude());
+ if (StringUtil.isBlank(longitude) || StringUtil.isBlank(latitude)) {
+ para.setAlignment(ParagraphAlignment.CENTER);
+ para.createRun().setText("/");
+ return;
+ }
+
+ XWPFRun coordinateRun = para.createRun();
+ coordinateRun.setText(longitude + "," + latitude + " ");
+ coordinateRun.setFontSize(10);
+
+ String qrKey = longitude + "," + latitude;
+ byte[] qrCodeData = qrCodeCache.get(qrKey);
+ if (qrCodeData != null && qrCodeData.length > 0) {
+ try {
+ para.setIndentationLeft(100);
+ para.setIndentationRight(100);
+ para.setSpacingBefore(50);
+ para.setSpacingAfter(50);
+
+ XWPFRun qrRun = para.createRun();
+ qrRun.addPicture(
+ new ByteArrayInputStream(qrCodeData),
+ XWPFDocument.PICTURE_TYPE_PNG,
+ "qrcode.png",
+ Units.toEMU(80),
+ Units.toEMU(80)
+ );
+ } catch (Exception e) {
+ logger.error("二维码插入失败,成果编号:{}", result.getResultCode(), e);
+ XWPFRun errorRun = para.createRun();
+ errorRun.setText("(二维码生成失败)");
+ }
+ } else {
+ XWPFRun errorRun = para.createRun();
+ errorRun.setText("(二维码生成失败)");
+ }
+ } else {
+ para.setAlignment(ParagraphAlignment.CENTER);
+ para.createRun().setText("/");
+ }
+ }
+
+ private static void addImageToCell(XWPFTableCell cell, String imageUrl, Map<String, byte[]> imageCache)
+ throws IOException, InvalidFormatException {
+ clearCellParagraphs(cell);
+ XWPFParagraph para = cell.addParagraph();
+ para.setAlignment(ParagraphAlignment.CENTER);
+ cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
+ para.setWordWrapped(false);
+
+ XWPFRun run = para.createRun();
+ if (StringUtil.isBlank(imageUrl)) {
+ run.setText("(无图片)");
+ return;
+ }
+
+ byte[] imageData = imageCache.get(imageUrl);
+ if (imageData == null || imageData.length == 0) {
+ run.setText("(图片加载失败)");
+ return;
+ }
+
+ try {
+ int format = findImageFormat(imageUrl);
+ para.setSpacingBefore(150);
+ para.setSpacingAfter(100);
+ para.setIndentationLeft(50);
+ para.setIndentationRight(50);
+
+ run.addPicture(new ByteArrayInputStream(imageData), format, "image.tmp",
+ Units.toEMU(320), Units.toEMU(180));
+ } catch (Exception e) {
+ logger.error("图片插入失败,地址:{}", imageUrl, e);
+ run.setText("(图片插入失败)");
+ }
+ }
+
+ private static Map<String, byte[]> preloadImages(List<GdTaskResultEntity> results) {
+ Set<String> uniqueUrls = new HashSet<>();
+ if (results != null) {
+ results.stream()
+ .map(GdTaskResultEntity::getResultUrl)
+ .filter(url -> !StringUtil.isBlank(url))
+ .forEach(uniqueUrls::add);
+ }
+ if (uniqueUrls.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, byte[]> resultMap = new ConcurrentHashMap<>();
+ List<CompletableFuture<Void>> futures = uniqueUrls.stream()
+ .map(url -> CompletableFuture.runAsync(() -> {
+ try {
+ byte[] data = downloadImage(url);
+ if (data != null && data.length > 0) {
+ resultMap.put(url, data);
+ }
+ } catch (Exception e) {
+ logger.error("图片下载失败,地址:{}", url, e);
+ }
+ }, RESOURCE_EXECUTOR))
+ .collect(Collectors.toList());
+
+ try {
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+ } catch (Exception e) {
+ logger.error("等待图片下载完成时发生异常", e);
+ }
+ return resultMap;
+ }
+
+ private static Map<String, byte[]> preloadQrCodes(List<GdTaskResultEntity> results) {
+ if (results == null || results.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ List<GdTaskResultEntity> needQrCodeResults = results.stream()
+ .filter(result -> result.getLongitude() != null && result.getLatitude() != null)
+ .collect(Collectors.toList());
+ if (needQrCodeResults.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, byte[]> qrCodeCache = new ConcurrentHashMap<>();
+ List<CompletableFuture<Void>> futures = needQrCodeResults.stream()
+ .map(result -> CompletableFuture.runAsync(() -> {
+ String longitude = formatCoordinateValue(result.getLongitude());
+ String latitude = formatCoordinateValue(result.getLatitude());
+ if (StringUtil.isBlank(longitude) || StringUtil.isBlank(latitude)) {
+ return;
+ }
+ String destination = StringUtil.isBlank(result.getResultCode()) ? "巡查结果" : result.getResultCode();
+ String navUrl = GdQrCodeUtil.generateAmapNavigationUrl(longitude, latitude, destination);
+ if (StringUtil.isBlank(navUrl)) {
+ return;
+ }
+ try {
+ byte[] qrCode = GdQrCodeUtil.generateQrCodeImageBytes(navUrl, 100, 100);
+ if (qrCode != null && qrCode.length > 0) {
+ qrCodeCache.put(longitude + "," + latitude, qrCode);
+ }
+ } catch (Exception e) {
+ logger.error("二维码生成失败,成果编号:{}", result.getResultCode(), e);
+ }
+ }, RESOURCE_EXECUTOR))
+ .collect(Collectors.toList());
+
+ try {
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+ } catch (Exception e) {
+ logger.error("等待二维码生成完成时发生异常", e);
+ }
+ return qrCodeCache;
+ }
+
+ private static byte[] downloadImage(String urlString) throws IOException {
+ URL url = new URL(urlString);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ try {
+ conn.setConnectTimeout(CONNECT_TIMEOUT);
+ conn.setReadTimeout(READ_TIMEOUT);
+ conn.setRequestMethod("GET");
+ conn.setRequestProperty("User-Agent", "Mozilla/5.0");
+ conn.setInstanceFollowRedirects(false);
+
+ int responseCode = conn.getResponseCode();
+ if (responseCode >= 300 && responseCode < 400) {
+ String newUrl = conn.getHeaderField("Location");
+ if (!StringUtil.isBlank(newUrl)) {
+ conn.disconnect();
+ return downloadImage(newUrl);
+ }
+ }
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ throw new IOException("图片请求失败,状态码:" + responseCode);
+ }
+
+ try (InputStream is = conn.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[16384];
+ int bytesRead;
+ while ((bytesRead = is.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesRead);
+ }
+ return baos.toByteArray();
+ }
+ } finally {
+ conn.disconnect();
+ }
+ }
+
+ private static void createTitle(XWPFDocument document, String text) {
+ XWPFParagraph p = document.createParagraph();
+ p.setAlignment(ParagraphAlignment.CENTER);
+ XWPFRun run = p.createRun();
+ run.setText(text);
+ run.setBold(true);
+ run.setFontSize(20);
+ }
+
+ private static void createSectionHeader(XWPFDocument document, String text) {
+ XWPFParagraph p = document.createParagraph();
+ XWPFRun run = p.createRun();
+ run.setText(text);
+ run.setBold(true);
+ run.setFontSize(16);
+ }
+
+ private static void addDetailItem(XWPFDocument document, String text) {
+ document.createParagraph().createRun().setText(text);
+ }
+
+ private static String formatString(String text) {
+ return !StringUtil.isBlank(text) ? text : "/";
+ }
+
+ private static String formatDate(Date date) {
+ if (date == null) {
+ return "/";
+ }
+ return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().format(DATE_TIME_FORMATTER);
+ }
+
+ private static String formatCoordinateValue(Double coordinate) {
+ if (coordinate == null) {
+ return "";
+ }
+ return String.format(Locale.CHINA, "%.6f", coordinate);
+ }
+
+ private static int findImageFormat(String url) {
+ String cleanUrl = url;
+ int queryIndex = url.indexOf("?");
+ if (queryIndex > -1) {
+ cleanUrl = url.substring(0, queryIndex);
+ }
+ String lowerUrl = cleanUrl.toLowerCase(Locale.ROOT);
+ if (lowerUrl.endsWith(".jpeg") || lowerUrl.endsWith(".jpg")) {
+ return XWPFDocument.PICTURE_TYPE_JPEG;
+ }
+ if (lowerUrl.endsWith(".png")) {
+ return XWPFDocument.PICTURE_TYPE_PNG;
+ }
+ if (lowerUrl.endsWith(".gif")) {
+ return XWPFDocument.PICTURE_TYPE_GIF;
+ }
+ return XWPFDocument.PICTURE_TYPE_JPEG;
+ }
+
+ private static void clearCellParagraphs(XWPFTableCell cell) {
+ int paragraphCount = cell.getParagraphs().size();
+ for (int i = paragraphCount - 1; i >= 0; i--) {
+ cell.removeParagraph(i);
+ }
+ }
+}
diff --git a/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdQrCodeUtil.java b/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdQrCodeUtil.java
new file mode 100644
index 0000000..cb24a5e
--- /dev/null
+++ b/drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdQrCodeUtil.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.sxkj.gd.workorder.utils;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.WriterException;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class GdQrCodeUtil {
+
+ private static final double PI = 3.1415926535897932384626;
+ private static final double A = 6378245.0;
+ private static final double EE = 0.00669342162296594323;
+
+ private GdQrCodeUtil() {
+ }
+
+ /**
+ * 生成高德地图导航链接,自动将 WGS84 坐标转换为 GCJ-02 坐标。
+ *
+ * @param longitude 目的地经度(WGS84)
+ * @param latitude 目的地纬度(WGS84)
+ * @param destination 目的地名称
+ * @return 编码后的高德导航链接
+ */
+ public static String generateAmapNavigationUrl(String longitude, String latitude, String destination) {
+ try {
+ double wgsLon = Double.parseDouble(longitude);
+ double wgsLat = Double.parseDouble(latitude);
+
+ double[] gcjCoords = wgs84ToGcj02(wgsLon, wgsLat);
+ String encodedDestination = URLEncoder.encode(destination, StandardCharsets.UTF_8.name());
+
+ return String.format(
+ "https://uri.amap.com/marker?position=%s,%s&name=%s&from=ztzf",
+ gcjCoords[0], gcjCoords[1], encodedDestination
+ );
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ /**
+ * WGS84 坐标转换为 GCJ-02 坐标。
+ *
+ * @param wgsLon WGS84 经度
+ * @param wgsLat WGS84 纬度
+ * @return [经度, 纬度] GCJ-02 坐标
+ */
+ public static double[] wgs84ToGcj02(double wgsLon, double wgsLat) {
+ if (outOfChina(wgsLon, wgsLat)) {
+ return new double[]{wgsLon, wgsLat};
+ }
+
+ double dLat = transformLat(wgsLon - 105.0, wgsLat - 35.0);
+ double dLon = transformLon(wgsLon - 105.0, wgsLat - 35.0);
+ double radLat = wgsLat / 180.0 * PI;
+ double magic = Math.sin(radLat);
+ magic = 1 - EE * magic * magic;
+ double sqrtMagic = Math.sqrt(magic);
+
+ dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI);
+ dLon = (dLon * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI);
+
+ double gcjLat = wgsLat + dLat;
+ double gcjLon = wgsLon + dLon;
+
+ return new double[]{gcjLon, gcjLat};
+ }
+
+ private static boolean outOfChina(double lon, double lat) {
+ return (lon < 72.004 || lon > 137.8347) || (lat < 0.8293 || lat > 55.8271);
+ }
+
+ private static double transformLat(double x, double y) {
+ double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
+ + 0.2 * Math.sqrt(Math.abs(x));
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
+ ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
+ ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
+ return ret;
+ }
+
+ private static double transformLon(double x, double y) {
+ double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y
+ + 0.1 * Math.sqrt(Math.abs(x));
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
+ ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
+ ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
+ return ret;
+ }
+
+ /**
+ * 生成二维码图片字节数组(PNG)。
+ *
+ * @param text 编码文本
+ * @param width 宽度
+ * @param height 高度
+ * @return 二维码图片字节数组
+ */
+ public static byte[] generateQrCodeImageBytes(String text, int width, int height) throws WriterException, IOException {
+ QRCodeWriter qrCodeWriter = new QRCodeWriter();
+ Map<EncodeHintType, Object> hints = new HashMap<>();
+ hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
+ hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
+ hints.put(EncodeHintType.MARGIN, 1);
+
+ BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ MatrixToImageWriter.writeToStream(bitMatrix, "PNG", baos);
+ return baos.toByteArray();
+ }
+ }
+}
--
Gitblit v1.9.3