吉安感知网项目-后端
rain
2026-01-22 092c6b67baa9b42069ceb202cadf97a0cd467c03
任务成果导出
3 files modified
2 files added
954 ■■■■■ changed files
drone-service/drone-gd/pom.xml 79 ●●●●● patch | view | raw | blame | history
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/IGdPatrolTaskService.java 9 ●●●●● patch | view | raw | blame | history
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/service/impl/GdPatrolTaskServiceImpl.java 71 ●●●●● patch | view | raw | blame | history
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdPatrolReportWordUtil.java 656 ●●●●● patch | view | raw | blame | history
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdQrCodeUtil.java 139 ●●●●● patch | view | raw | blame | history
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>
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);
}
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;
    }
}
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdPatrolReportWordUtil.java
New file
@@ -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);
        }
    }
}
drone-service/drone-gd/src/main/java/org/sxkj/gd/workorder/utils/GdQrCodeUtil.java
New file
@@ -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();
        }
    }
}