From 08be30bda841df3af65428547186513a3d7b1f42 Mon Sep 17 00:00:00 2001
From: liding <756868258@qq.com>
Date: 星期二, 27 十月 2026 17:47:56 +0800
Subject: [PATCH] feat(report): 完善报告退回流程及新增 PDF 预览功能 - 新增退回到报告审核环节,支持退回至检验任务或报告审核两种目标

---
 inspect-server/src/main/java/com/ruoyi/inspect/util/ReportSealLayoutUtil.java |  178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 168 insertions(+), 10 deletions(-)

diff --git a/inspect-server/src/main/java/com/ruoyi/inspect/util/ReportSealLayoutUtil.java b/inspect-server/src/main/java/com/ruoyi/inspect/util/ReportSealLayoutUtil.java
index 9791a1e..5f81971 100644
--- a/inspect-server/src/main/java/com/ruoyi/inspect/util/ReportSealLayoutUtil.java
+++ b/inspect-server/src/main/java/com/ruoyi/inspect/util/ReportSealLayoutUtil.java
@@ -1,6 +1,8 @@
 package com.ruoyi.inspect.util;
 
 import com.aspose.words.Document;
+import com.aspose.words.Cell;
+import com.aspose.words.CompositeNode;
 import com.aspose.words.LayoutCollector;
 import com.aspose.words.LayoutEnumerator;
 import com.aspose.words.License;
@@ -11,6 +13,7 @@
 import com.aspose.words.Paragraph;
 import com.aspose.words.RelativeHorizontalPosition;
 import com.aspose.words.RelativeVerticalPosition;
+import com.aspose.words.Run;
 import com.aspose.words.SaveFormat;
 import com.aspose.words.Section;
 import com.aspose.words.Shape;
@@ -75,6 +78,98 @@
         document.save(wordPath, SaveFormat.DOCX);
     }
 
+    /** 浠庡巻鍙叉壒鍑嗙涓竻闄ゆ壒鍑嗗唴瀹癸紝鎭㈠閲嶆柊鎵瑰噯鎵�闇�鐨勫崰浣嶇銆� */
+    public static void restoreForReview(String wordPath) throws Exception {
+        applyAsposeLicense();
+        Document document = new Document(wordPath);
+        List<Shape> shapes = imageShapes(document);
+        Shape signatureShape = null;
+        List<Shape> dateShapes = new ArrayList<>();
+        for (Shape shape : shapes) {
+            if (hasPlaceholderMarker(shape, "ratifyUrl")) {
+                signatureShape = shape;
+                continue;
+            }
+            if (hasPlaceholderMarker(shape, "ratifyDateUrl")) {
+                dateShapes.add(shape);
+                continue;
+            }
+            // 鎵瑰噯鏃舵坊鍔犵殑鎶ュ憡绔犲拰 MA 绔犳寜椤甸潰娴姩瀹氫綅锛屾寜浣嶇疆娓呯悊鑰岄潪姣旇緝鍥剧墖瀛楄妭銆�
+            if (isApprovalSeal(shape)) {
+                shape.remove();
+            }
+        }
+        if (signatureShape != null) {
+            replaceWithPlaceholder(document, signatureShape, "{{@ratifyUrl}}");
+        } else if (!restoreApprovalCellPlaceholder(document)) {
+            throw new IllegalStateException("鏈壘鍒版壒鍑嗘爮锛屾棤娉曟仮澶嶆壒鍑嗙鍚嶅崰浣嶇");
+        }
+        // 閮ㄥ垎鎶ュ憡妯℃澘娌℃湁鎵瑰噯鏃ユ湡鍥剧墖锛屾壒鍑嗘椂闂翠粎瀛樺湪鏁版嵁搴撲腑銆�
+        for (Shape dateShape : dateShapes) {
+            replaceWithPlaceholder(document, dateShape, "{{@ratifyDateUrl}}");
+        }
+        document.save(wordPath, SaveFormat.DOCX);
+    }
+
+    private static boolean isApprovalSeal(Shape shape) throws Exception {
+        return shape.getWrapType() != WrapType.INLINE
+                && shape.getRelativeHorizontalPosition() == RelativeHorizontalPosition.PAGE
+                && shape.getRelativeVerticalPosition() == RelativeVerticalPosition.PAGE
+                && shape.getWidth() >= 50D && shape.getWidth() <= 140D
+                && shape.getHeight() >= 50D && shape.getHeight() <= 140D;
+    }
+
+    private static boolean restoreApprovalCellPlaceholder(Document document) throws Exception {
+        NodeCollection cells = document.getChildNodes(NodeType.CELL, true);
+        for (int i = 0; i < cells.getCount(); i++) {
+            Cell label = (Cell) cells.get(i);
+            if (!"鎵瑰噯".equals(label.getText().replace("\r", "").replace("\u0007", "").trim())) {
+                continue;
+            }
+            Node sibling = label.getNextSibling();
+            if (!(sibling instanceof Cell)) {
+                continue;
+            }
+            Cell signatureCell = (Cell) sibling;
+            for (Shape shape : imageShapesIn(signatureCell)) {
+                shape.remove();
+            }
+            Paragraph paragraph = signatureCell.getFirstParagraph();
+            if (paragraph == null) {
+                throw new IllegalStateException("鎵瑰噯绛惧悕鍗曞厓鏍肩己灏戞钀�");
+            }
+            if (!signatureCell.getText().contains("{{@ratifyUrl}}")) {
+                paragraph.appendChild(new Run(document, "{{@ratifyUrl}}"));
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private static List<Shape> imageShapesIn(Cell cell) throws Exception {
+        List<Shape> result = new ArrayList<>();
+        NodeCollection nodes = cell.getChildNodes(NodeType.SHAPE, true);
+        for (int i = 0; i < nodes.getCount(); i++) {
+            Shape shape = (Shape) nodes.get(i);
+            if (shape.hasImage()) {
+                result.add(shape);
+            }
+        }
+        return result;
+    }
+
+    private static boolean hasPlaceholderMarker(Shape shape, String marker) throws Exception {
+        String alternativeText = shape.getAlternativeText();
+        return alternativeText != null && alternativeText.contains(marker);
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private static void replaceWithPlaceholder(Document document, Shape shape, String placeholder) {
+        CompositeNode parent = (CompositeNode) shape.getParentNode();
+        parent.insertBefore(new Run(document, placeholder), shape);
+        shape.remove();
+    }
+
     private static void applyAsposeLicense() throws Exception {
         try (InputStream inputStream = ReportSealLayoutUtil.class.getResourceAsStream("/lib/license.xml")) {
             if (inputStream == null) {
@@ -93,7 +188,7 @@
             return null;
         }
         Path path = java.nio.file.Paths.get(imagePath);
-        return Files.isRegularFile(path) ? Files.readAllBytes(path) : null;
+        return Files.readAllBytes(path);
     }
 
     private static void removeMatchingImages(Document document, byte[] imageBytes) throws Exception {
@@ -115,12 +210,18 @@
 
     private static void addCoverReportSeal(Document document, String imagePath) throws Exception {
         Paragraph anchor = firstBodyParagraph(document);
+        LayoutCollector collector = new LayoutCollector(document);
+        Paragraph company = findPageCompanyParagraph(document, collector, 1);
+        Rectangle2D rectangle = company == null ? null : getLayoutRectangle(document, collector, company);
         PageSetup pageSetup = document.getFirstSection().getPageSetup();
         double[] size = fitImage(imagePath, REPORT_MAX_WIDTH, REPORT_MAX_HEIGHT);
         Shape shape = createFloatingImage(document, imagePath, size[0], size[1]);
-        shape.setLeft(pageSetup.getLeftMargin() + 20D);
-        shape.setTop(pageSetup.getPageHeight() - pageSetup.getBottomMargin()
-                - size[1] - POSITION_GAP);
+        shape.setLeft((pageSetup.getPageWidth() - size[0]) / 2D);
+        // 灏侀潰绔犱笌搴曢儴鏈烘瀯鍚嶇О閲嶅彔锛岄殢鍚嶇О鐨勫疄闄呮帓鐗堜綅缃Щ鍔ㄣ��
+        double preferredTop = rectangle == null
+                ? pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1] - POSITION_GAP
+                : rectangle.getCenterY() - size[1] / 2D;
+        shape.setTop(clamp(preferredTop, 0D, pageSetup.getPageHeight() - size[1]));
         shape.setBehindText(true);
         anchor.appendChild(shape);
     }
@@ -157,7 +258,9 @@
     private static void addLastPageReportSeal(Document document, String imagePath) throws Exception {
         int lastPage = document.getPageCount();
         LayoutCollector collector = new LayoutCollector(document);
-        Paragraph anchor = findLastPageAnchor(document, collector, lastPage);
+        Paragraph title = findResultTitle(document);
+        // 缁撴灉琛ㄥ彲鑳借法椤碉紝绔犲簲璺熼殢缁撴灉鏍囬锛屼笉鑳借窡闅忔渶鍚庝竴椤电殑鈥滄姤鍛婄粨鏉熲�濄��
+        Paragraph anchor = title == null ? findLastPageAnchor(document, collector, lastPage) : title;
         if (anchor == null) {
             return;
         }
@@ -167,11 +270,23 @@
         Shape shape = createFloatingImage(document, imagePath, size[0], size[1]);
         shape.setLeft((pageSetup.getPageWidth() - size[0]) / 2D);
         Rectangle2D rectangle = getLayoutRectangle(document, collector, anchor);
-        double preferredTop = rectangle == null
-                ? pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1] - POSITION_GAP
-                : rectangle.getY() + rectangle.getHeight() + POSITION_GAP;
+        double preferredTop;
+        if (title != null) {
+            Paragraph company = findPageCompanyParagraph(document, collector, collector.getStartPageIndex(title));
+            Rectangle2D companyRectangle = company == null ? null : getLayoutRectangle(document, collector, company);
+            // 绔犵殑涓績瀵瑰噯鏈烘瀯鍚嶇О涓庣粨鏋滄爣棰樼粍鎴愮殑鍖哄煙锛屽厑璁镐几鍏ヤ笂椤佃竟璺濄��
+            double headingTop = companyRectangle == null || rectangle == null
+                    ? (rectangle == null ? pageSetup.getTopMargin() : rectangle.getY())
+                    : Math.min(companyRectangle.getY(), rectangle.getY());
+            double headingBottom = rectangle == null ? headingTop : rectangle.getMaxY();
+            preferredTop = (headingTop + headingBottom - size[1]) / 2D;
+        } else {
+            preferredTop = rectangle == null
+                    ? pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1] - POSITION_GAP
+                    : rectangle.getY() + rectangle.getHeight() + POSITION_GAP;
+        }
         shape.setTop(clamp(preferredTop,
-                pageSetup.getTopMargin(),
+                title == null ? pageSetup.getTopMargin() : 0D,
                 pageSetup.getPageHeight() - pageSetup.getBottomMargin() - size[1]));
         shape.setBehindText(true);
         anchor.appendChild(shape);
@@ -180,7 +295,14 @@
     private static Shape createFloatingImage(Document document, String imagePath,
                                               double width, double height) throws Exception {
         Shape shape = new Shape(document, ShapeType.IMAGE);
-        shape.getImageData().setImage(imagePath);
+        // Aspose 15 浼氭妸 Linux 鐨� / 寮�澶磋矾寰勫綋浣滅浉瀵硅矾寰勶紝鍙兘鐢熸垚绌哄浘鐗囦笖涓嶆姤閿欍��
+        // 閫氳繃娴佸祵鍏ョ湡瀹炲浘鐗囧唴瀹癸紝閬垮厤璺緞琚寜鏂囨。鐨勮祫婧愬湴鍧�鍐嶆瑙f瀽銆�
+        try (InputStream imageStream = Files.newInputStream(java.nio.file.Paths.get(imagePath))) {
+            shape.getImageData().setImage(imageStream);
+        }
+        if (!shape.hasImage()) {
+            throw new IllegalStateException("鎶ュ憡鍗扮珷鍥剧墖宓屽叆澶辫触");
+        }
         shape.setWidth(width);
         shape.setHeight(height);
         shape.setWrapType(WrapType.NONE);
@@ -220,6 +342,42 @@
         return fallback;
     }
 
+    private static Paragraph findPageCompanyParagraph(Document document, LayoutCollector collector, int page)
+            throws Exception {
+        Paragraph company = null;
+        NodeCollection paragraphs = document.getChildNodes(NodeType.PARAGRAPH, true);
+        for (int i = 0; i < paragraphs.getCount(); i++) {
+            Paragraph paragraph = (Paragraph) paragraphs.get(i);
+            if (paragraph.getAncestor(NodeType.BODY) != null
+                    && paragraph.getAncestor(NodeType.CELL) == null
+                    && collector.getStartPageIndex(paragraph) == page
+                    && normalizedText(paragraph).endsWith("鍏徃")) {
+                company = paragraph;
+            }
+        }
+        return company;
+    }
+
+    private static Paragraph findResultTitle(Document document) throws Exception {
+        NodeCollection paragraphs = document.getChildNodes(NodeType.PARAGRAPH, true);
+        for (int i = 0; i < paragraphs.getCount(); i++) {
+            Paragraph paragraph = (Paragraph) paragraphs.get(i);
+            if (paragraph.getAncestor(NodeType.BODY) == null
+                    || paragraph.getAncestor(NodeType.CELL) != null) {
+                continue;
+            }
+            String text = normalizedText(paragraph);
+            if ("妫�楠岀粨鏋�".equals(text) || "妫�娴嬬粨鏋�".equals(text)) {
+                return paragraph;
+            }
+        }
+        return null;
+    }
+
+    private static String normalizedText(Paragraph paragraph) {
+        return paragraph.getText().replaceAll("[\\s\\u3000]+", "");
+    }
+
     private static Paragraph findBodyParagraphContaining(Document document, String text,
                                                           LayoutCollector collector, Integer page)
             throws Exception {

--
Gitblit v1.9.3