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