Jelajahi Sumber

build 新版报告

mamingxu 5 hari lalu
induk
melakukan
69e80160e9

+ 769 - 0
easier-report-biz/src/main/java/com/yaoyicloud/render/AbstractNewRender.java

@@ -0,0 +1,769 @@
+package com.yaoyicloud.render;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import javax.imageio.ImageIO;
+
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.rendering.PDFRenderer;
+import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFHyperlinkRun;
+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.CTTc;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalJc;
+
+import com.deepoove.poi.XWPFTemplate;
+import com.deepoove.poi.config.Configure;
+import com.deepoove.poi.config.ConfigureBuilder;
+import com.deepoove.poi.data.ByteArrayPictureRenderData;
+import com.deepoove.poi.data.HyperlinkTextRenderData;
+import com.deepoove.poi.data.PictureType;
+import com.deepoove.poi.data.style.PictureStyle;
+import com.deepoove.poi.exception.RenderException;
+import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
+import com.deepoove.poi.policy.ParagraphRenderPolicy;
+import com.deepoove.poi.policy.PictureRenderPolicy;
+import com.deepoove.poi.policy.RenderPolicy;
+import com.deepoove.poi.policy.TextRenderPolicy;
+import com.deepoove.poi.template.ElementTemplate;
+import com.deepoove.poi.template.run.RunTemplate;
+import com.deepoove.poi.util.TableTools;
+import com.deepoove.poi.xwpf.BodyContainer;
+import com.deepoove.poi.xwpf.BodyContainerFactory;
+import com.yaoyicloud.tools.Util;
+
+import cn.hutool.core.lang.Pair;
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 抽象渲染器
+ *
+ */
+public abstract class AbstractNewRender {
+
+    /*
+    * 导出文件位置
+    */
+    protected final String cwd;
+
+    /*
+     * Docx结果文件路径
+     */
+    protected String docxResultPath;
+
+    /*
+     * html结果文件路径
+     */
+    protected String htmlResultPath;
+
+    /*
+     * pdf结果文件路径
+     */
+    protected String pdfResultPath;
+
+    public AbstractNewRender(String cwd) {
+        this.cwd = cwd;
+    }
+
+    /**
+     * Docx 渲染
+     *
+     * 
+     * @param templateFileContent 模板内容
+     * @return 本地文件目录
+     * @throws IOException
+     */
+    @SuppressWarnings("checkstyle:ParameterNumber")
+    public final String renderDocx(Map<String, Object> dataMap, byte[] templateFileContent,
+        ConfigureBuilder builder, String relationId, String moduleType, Map<String, Object> specialMap)
+        throws IOException {
+
+        // 注: 报告模板的模板变量按照json序列化的结果命名
+        String basicPath = this.getBasicPath();
+        String reportImagePath = this.getReportImagePath();
+        String label = relationId + "_" + moduleType;
+        // word导出位置
+        String reportTempWordFile = basicPath + "/" + cwd + "/" + label + ".docx";
+        // 新增:创建文件夹
+        File parentDir = new File(basicPath + "/" + cwd);
+        if (!parentDir.exists()) {
+            boolean created = parentDir.mkdirs();
+            if (!created) {
+                throw new IOException("无法创建文件保存目录");
+            }
+        }
+
+        Configure config = builder.build();
+        XWPFTemplate template =
+            XWPFTemplate.compile(new ByteArrayInputStream(templateFileContent), config).render(dataMap);
+        ArrayList<String> templateDele = new ArrayList<>();
+        for (Map.Entry<String, Object> entry : specialMap.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            if (value.equals("")) {
+                templateDele.add(key);
+            }
+        }
+        Map<String, String> keywordMap = new HashMap<>();
+        keywordMap.put("suggestion", "建议");
+        keywordMap.put("score", "分");
+        XWPFDocument doc = template.getXWPFDocument();
+        List<XWPFTable> tables = doc.getTables();
+        XWPFTable xwpfTable = tables.get(tables.size() - 1);
+        for (int i = xwpfTable.getNumberOfRows() - 1; i >= 0; i--) {
+            XWPFTableRow row = xwpfTable.getRow(i);
+            StringBuilder rowText = new StringBuilder();
+            for (XWPFTableCell cell : row.getTableCells()) {
+                rowText.append(cell.getText());
+            }
+            for (String codeKeyword : templateDele) {
+                String templateKeyword = keywordMap.get(codeKeyword);
+                if (rowText.toString().contains(templateKeyword)) {
+                    xwpfTable.removeRow(i);
+                    break;
+                }
+            }
+        }
+
+        template.writeToFile(reportTempWordFile);
+        template.close();
+
+        this.docxResultPath = reportTempWordFile;
+        return this.docxResultPath;
+    }
+
+    /**
+     * docx转换为pdf
+     *
+     * @return 本地文件目录
+     * @throws IOException
+     */
+    public final String fromDocxToPdf() throws IOException {
+        // 转换 docxResultPath 为 html
+        // 加工html
+        // 把 html 文件转成 pdf
+        fromDocxToHtml();
+        this.pdfResultPath = fromHtmlToPdf();
+        return this.pdfResultPath;
+    }
+
+    /**
+     * docx转换为Html
+     *
+     * @return 本地文件目录
+     * @throws IOException
+     */
+    public final String fromDocxToHtml() throws IOException {
+        // 转换 docxResultPath 为 html
+        // 加工html
+        this.htmlResultPath = cwd + "/1.html";
+        return this.htmlResultPath;
+    }
+
+    /**
+     * html转换为pdf
+     *
+     * @return 本地文件目录
+     * @throws IOException
+     */
+    public final String fromHtmlToPdf() throws IOException {
+        // 转换 htmlResultPath 为 pdf
+        this.pdfResultPath = cwd + "/1.pdf";
+        return this.pdfResultPath;
+    }
+
+    protected abstract String getBasicPath() throws IOException;
+
+    protected abstract String getReportImagePath();
+
+    /**
+     * 针对于protobuf传递来的数据处理
+     * 
+     * @return
+     */
+    protected RenderPolicy indicatorsRenderPolicyToProtobuf() {
+        return new LoopRowTableRenderPolicy() {
+            @Override
+            public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+                // 获取模板中的变量如:[name]
+                ArrayList<String> strings = processElement(eleTemplate, data);
+                List<Map<String, Object>> processedData = null;
+                if (null != data && data instanceof Iterable) {
+                    processedData = (List<Map<String, Object>>) data;
+                    processedData.forEach(map -> {
+                        for (String string : strings) {
+                            Object o = map.get(string);
+                            if (null == o || "".equals(o)) {
+                                map.put(string, "-");
+                            }
+                        }
+                    });
+                }
+                // 调用父类渲染处理后的数据
+                super.render(eleTemplate, processedData, template);
+            }
+
+        };
+    }
+
+    @SuppressWarnings("checkstyle:NestedForDepth")
+    private ArrayList<String> processElement(ElementTemplate eleTemplate, Object data) {
+        RunTemplate runTemplate = (RunTemplate) eleTemplate;
+        XWPFRun run = runTemplate.getRun();
+        XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
+        XWPFTable table = tagCell.getTableRow().getTable();
+        StringBuilder textBuilder = new StringBuilder();
+
+        for (XWPFTableRow row : table.getRows()) {
+            // 遍历行中的每一个单元格
+            for (XWPFTableCell cell : row.getTableCells()) {
+                // 提取单元格内的所有文本(包含段落、Run 等)
+                for (XWPFParagraph para : cell.getParagraphs()) {
+                    for (XWPFRun r : para.getRuns()) {
+                        textBuilder.append(r.getText(0)); // 获取 Run 的文本
+                    }
+                }
+                textBuilder.append(" ");
+            }
+            textBuilder.append(" ");
+        }
+        ArrayList<String> strings = new ArrayList<>();
+        String string = textBuilder.toString();
+        String[] split = string.split(" ");
+        for (String s : split) {
+            Pattern pattern = Pattern.compile("\\[(.*?)\\]");
+            Matcher matcher = pattern.matcher(s);
+            while (matcher.find()) {
+                String contentInBracket = matcher.group(1);
+                strings.add(contentInBracket);
+            }
+        }
+        return strings;
+    }
+
+    /**
+     * 针对于json格式全量传输数据的列表处理 将默认值null转为“-”
+     * 
+     * @return
+     */
+    protected RenderPolicy indicatorsRenderPolicy() {
+        return new LoopRowTableRenderPolicy() {
+            @Override
+            public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+                // 处理数据中的null值
+                Object processedData = processData(data);
+                // 调用父类渲染处理后的数据
+                super.render(eleTemplate, processedData, template);
+            }
+
+            private Object processData(Object data) {
+                if (data == null) {
+                    return null;
+                }
+
+                if (data instanceof List) {
+                    // 处理List类型数据
+                    return ((List<?>) data).stream()
+                        .map(this::processItem)
+                        .collect(Collectors.toList());
+                }
+
+                return data;
+            }
+
+            private Object processItem(Object item) {
+                if (item == null) {
+                    return "-";
+                }
+
+                // 如果元素是Map,处理Map中的null值
+                if (item instanceof Map) {
+                    Map<?, ?> map = (Map<?, ?>) item;
+                    return map.entrySet().stream()
+                        .collect(Collectors.toMap(
+                            Map.Entry::getKey,
+                            e -> e.getValue() == null ? "-" : e.getValue()));
+                }
+
+                return item;
+            }
+        };
+    }
+
+    /**
+     * 这些render policy类都应当是共享的 重要设计假设: data的类型cast都可以建立在json通用反序列化后的基本类型基础上。
+     */
+    protected RenderPolicy pictureRenderPolicy() {
+        return new PictureRenderPolicy() {
+            @Override
+            public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> mpData = (Map<String, Object>) data;
+                String filename = mpData.getOrDefault("fileName", "").toString().trim();
+                String url = mpData.getOrDefault("fileUri", "").toString();
+                float targetWidth = 456.5f;
+
+                if (StrUtil.isBlank(filename)) {
+                    // uri render when no filename
+                    TextRenderPolicy.Helper.renderTextRun(((RunTemplate) eleTemplate).getRun(),
+                        new HyperlinkTextRenderData(url, url));
+                } else if (filename.endsWith(".pdf")) {
+                    // pdf render, replace data with bytestream
+                    PDDocument document = null;
+                    try {
+                        document = Loader.loadPDF(new URL(url).openStream().readAllBytes());
+                        PDFRenderer renderer = new PDFRenderer(document);
+
+                        XWPFRun run = ((RunTemplate) eleTemplate).getRun();
+                        BodyContainer bodyContainer = BodyContainerFactory.getBodyContainer(run);
+                        // 每页一张图片
+                        for (int i = 0; i < document.getNumberOfPages(); i++) {
+                            // pdf 转 jpeg
+                            BufferedImage image = renderer.renderImageWithDPI(i, 150);
+                            ByteArrayOutputStream stream = new ByteArrayOutputStream();
+                            ImageIO.write(image, "jpg", stream);
+
+                            // 准备docx元素
+                            if (i == 0) {
+                                run.setText("", 0);
+                            } else {
+                                run = bodyContainer.insertNewParagraph(run).createRun();
+                            }
+
+                            // 准备POI-TL数据
+                            ByteArrayPictureRenderData picData =
+                                new ByteArrayPictureRenderData(stream.toByteArray(), PictureType.JPEG);
+                            Pair<Integer, Integer> targetSize =
+                                calculateTargetSize(image.getWidth(), image.getHeight(), targetWidth);
+                            PictureStyle style = new PictureStyle();
+                            style.setWidth(targetSize.getKey());
+                            style.setHeight(targetSize.getValue());
+                            picData.setPictureStyle(style);
+                            Helper.renderPicture(run, picData);
+                        }
+                    } catch (Exception e) {
+                        throw new RenderException(
+                            "AttachmentRenderPolicy for " + eleTemplate + " error: " + e.getMessage(), e);
+                    } finally {
+                        if (document != null) {
+                            try {
+                                document.close();
+                            } catch (IOException e) {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                } else {
+                    BufferedImage image = null;
+                    ByteArrayOutputStream stream = null;
+                    try {
+                        image = ImageIO.read(new URL(url));
+                        stream = new ByteArrayOutputStream();
+                        ImageIO.write(image, getFileExtension(filename), stream);
+                    } catch (IOException e) {
+                        throw new RenderException(
+                            "AttachmentRenderPolicy for " + eleTemplate + " error: " + e.getMessage(), e);
+                    }
+
+                    ByteArrayPictureRenderData picData =
+                        new ByteArrayPictureRenderData(stream.toByteArray(), PictureType.suggestFileType(filename));
+                    Pair<Integer, Integer> targetSize =
+                        calculateTargetSize(image.getWidth(), image.getHeight(), targetWidth);
+                    PictureStyle style = new PictureStyle();
+                    style.setWidth(targetSize.getKey());
+                    style.setHeight(targetSize.getValue());
+                    picData.setPictureStyle(style);
+                    super.render(eleTemplate, picData, template);
+                }
+            }
+
+            /**
+             * 计算目标尺寸(提取为内部方法)
+             */
+            private Pair<Integer, Integer> calculateTargetSize(int originWidth, int originHeight, float targetWidth) {
+                float targetHeight = (originHeight * targetWidth) / originWidth;
+                if (targetHeight > 645) {
+                    targetHeight = 645;
+                    targetWidth = (originWidth * targetHeight) / originHeight;
+                }
+                return Pair.of(Math.round(targetWidth), Math.round(targetHeight));
+            }
+
+            /**
+             * 获取文件扩展名
+             */
+            private String getFileExtension(String filename) {
+                int dotIndex = filename.lastIndexOf('.');
+                return dotIndex > 0 ? filename.substring(dotIndex + 1).toLowerCase() : "jpg";
+            }
+        };
+    }
+
+    /**
+     * 超链接列表策略
+     * 
+     * @return
+     */
+    protected RenderPolicy hyperlinkRenderPolicy() {
+        return new ParagraphRenderPolicy() {
+            @Override
+            public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+                // 获取当前运行的
+                XWPFParagraph paragraph = ((RunTemplate) eleTemplate).getRun().getParagraph();
+                int pos = paragraph.getRuns().indexOf(((RunTemplate) eleTemplate).getRun());
+                paragraph.removeRun(pos);
+                paragraph.removeRun(pos);
+                if (data instanceof String) {
+                    XWPFHyperlinkRun hyperlinkRun = paragraph.createHyperlinkRun(data.toString());
+                    hyperlinkRun.setText(data.toString());
+                    hyperlinkRun.setColor("0000FF");
+                    hyperlinkRun.setUnderline(UnderlinePatterns.SINGLE);
+                    hyperlinkRun.setFontFamily("思源黑体 Medium");
+                    hyperlinkRun.setFontSize(10.5);
+
+                }
+            }
+        };
+    }
+
+    protected RenderPolicy getScoreRenderPolicy() {
+        RenderPolicy policy = new LoopRowTableRenderPolicy() {
+            @SuppressWarnings("checkstyle:NestedForDepth")
+            @Override
+            public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+                // 检查数据是否为空
+                if (data == null || (data instanceof Collection && ((Collection<?>) data).isEmpty())) {
+                    // 数据为空时,删除整个表格
+                    // removeTemplateTable(eleTemplate, template);
+                    return;
+                }
+
+                super.render(eleTemplate, data, template);
+                XWPFDocument doc = template.getXWPFDocument();
+                for (XWPFTable table : doc.getTables()) {
+                    boolean isTargetTable = false;
+                    // 判断是否为目标表格:通过检查表格第一行是否包含货物明细相关表头关键词
+                    List<String> headerKeywords = List.of("删除");
+                    XWPFTableRow firstRow = table.getRow(0);
+                    if (firstRow != null) {
+                        for (XWPFTableCell cell : firstRow.getTableCells()) {
+                            for (XWPFParagraph para : cell.getParagraphs()) {
+                                for (XWPFRun run : para.getRuns()) {
+                                    String text = run.text().trim();
+                                    if (headerKeywords.contains(text)) {
+                                        isTargetTable = true;
+                                        break;
+                                    }
+                                }
+                                if (isTargetTable) {
+                                    break;
+                                }
+                            }
+                            if (isTargetTable) {
+                                break;
+                            }
+                        }
+                    }
+                    if (isTargetTable) {
+                        // 删除表头(假设表头是第一行)
+                        if (table.getRows().size() > 0) {
+                            table.removeRow(0);
+                        }
+                        // 合并第一列相同内容单元格并居中
+                        mergeAndCenterFirstColumn(table);
+                    }
+                }
+            }
+
+            private void mergeAndCenterFirstColumn(XWPFTable table) {
+                if (table.getNumberOfRows() < 2) {
+                    return;
+                }
+
+                int startRow = 0;
+                String currentValue = getCellText(table.getRow(0)).get(0);
+
+                for (int i = 1; i < table.getNumberOfRows(); i++) {
+                    List<String> rowValues = getCellText(table.getRow(i));
+                    if (rowValues.isEmpty()) {
+                        continue;
+                    }
+
+                    String firstCellValue = rowValues.get(0);
+                    if (firstCellValue.equals(currentValue)) {
+                        // 继续查找相同值的行
+                        continue;
+                    } else {
+                        // 合并从startRow到i-1行的第一列单元格
+                        if (i - 1 > startRow) {
+                            TableTools.mergeCellsVertically(table, 0, startRow, i - 1);
+                            // 设置合并后单元格内容居中
+                            setCellCenterAlignment(table.getRow(startRow).getCell(0));
+                        }
+                        startRow = i;
+                        currentValue = firstCellValue;
+                    }
+                }
+
+                // 处理最后一组相同值
+                if (table.getNumberOfRows() - 1 > startRow) {
+                    TableTools.mergeCellsVertically(table, 0, startRow, table.getNumberOfRows() - 1);
+                    setCellCenterAlignment(table.getRow(startRow).getCell(0));
+                }
+            }
+        };
+        return policy;
+    }
+
+    private List<String> getCellText(XWPFTableRow row) {
+        List<String> texts = new ArrayList<>();
+        if (row == null) {
+            return texts;
+        }
+        for (XWPFTableCell cell : row.getTableCells()) {
+            StringBuilder sb = new StringBuilder();
+            for (XWPFParagraph para : cell.getParagraphs()) {
+                for (XWPFRun run : para.getRuns()) {
+                    sb.append(run.text());
+                }
+            }
+            texts.add(sb.toString().trim());
+        }
+        return texts;
+    }
+
+    private void setCellCenterAlignment(XWPFTableCell cell) {
+        CTTc cttc = cell.getCTTc();
+        CTTcPr ctPr = cttc.addNewTcPr();
+        ctPr.addNewVAlign().setVal(STVerticalJc.CENTER);
+    }
+
+    /**
+     * 这些render policy类都应当是共享的 重要设计假设: data的类型cast都可以建立在json通用反序列化后的基本类型基础上。
+     */
+    public class LoopColumnStaticTableRenderPolicy implements RenderPolicy {
+
+        private String prefix;
+        private String suffix;
+        private boolean onSameLine;
+        private boolean reverse;
+        private int valColIndex;
+
+        public LoopColumnStaticTableRenderPolicy() {
+            this(false);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(boolean onSameLine) {
+            this("[", "]", onSameLine);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(String prefix, String suffix) {
+            this(prefix, suffix, false);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine) {
+            this(prefix, suffix, onSameLine, false);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine, boolean reverse) {
+            this(prefix, suffix, onSameLine, false, 1);
+        }
+
+        public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine, boolean reverse,
+            int valRowIndex) {
+            this.prefix = prefix;
+            this.suffix = suffix;
+            this.onSameLine = onSameLine;
+            this.reverse = reverse;
+            this.valColIndex = valRowIndex;
+        }
+
+        @Override
+        public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+            RunTemplate runTemplate = (RunTemplate) eleTemplate;
+            XWPFRun run = runTemplate.getRun();
+            try {
+                if (!TableTools.isInsideTable(run)) {
+                    throw new IllegalStateException(
+                        "The template tag " + runTemplate.getSource() + " must be inside a table");
+                }
+                XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
+                XWPFTable table = tagCell.getTableRow().getTable();
+                run.setText("", 0);
+
+                int templateColIndex = getTemplateColIndex(tagCell);
+                // 模版变量列总是写在左边
+                int minIndex = templateColIndex;
+                int maxIndex = table.getRows().get(valColIndex).getTableCells().size() - 1;
+                int currIndex = reverse ? maxIndex : minIndex;
+                int indexDelta = reverse ? -1 : 1;
+
+                // 目前expression当作数据Map的key来对待,将来可以当作POI-TL变量一致化的处理
+                Map<Integer, String> idx2Expression = new HashMap<>();
+                for (int i = 0; i < table.getRows().size(); i++) {
+                    XWPFTableCell cell = table.getRows().get(i).getCell(templateColIndex);
+                    String text = cell.getText().trim();
+                    if (text.startsWith(prefix) && text.endsWith(suffix)) {
+                        idx2Expression.put(i, text.substring(1, text.length() - 1));
+                        cell.setText("");
+                    }
+                }
+
+                int rowSize = table.getRows().size();
+                @SuppressWarnings("unchecked")
+                List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
+
+                for (Map<String, Object> realData : mpData) {
+                    for (int i = 0; i < rowSize; i++) {
+                        if (!idx2Expression.containsKey(i)) {
+                            continue;
+                        }
+                        XWPFTableRow row = table.getRow(i);
+                        XWPFTableCell valueCell = row.getCell(currIndex);
+                        String valStr = realData.getOrDefault(idx2Expression.get(i), "-").toString();
+                        valueCell.setText(valStr);
+                    }
+                    currIndex += indexDelta;
+                }
+
+            } catch (Exception e) {
+                throw new RenderException("HackLoopTable for " + eleTemplate + "error: " + e.getMessage(), e);
+            }
+        }
+
+        private int getTemplateColIndex(XWPFTableCell tagCell) {
+            return onSameLine ? Util.getColIndexOfFirstRow(tagCell) : (Util.getColIndexOfFirstRow(tagCell) + 1);
+        }
+    }
+
+    public class LoopRowCutAndMergeFirstColTableRenderPolicy extends LoopRowTableRenderPolicy {
+
+        @Override
+        public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+            RunTemplate runTemplate = (RunTemplate) eleTemplate;
+            XWPFRun run = runTemplate.getRun();
+            XWPFTable table = null;
+            try {
+                if (!TableTools.isInsideTable(run)) {
+                    throw new IllegalStateException(
+                        "The template tag " + runTemplate.getSource() + " must be inside a table");
+                }
+                // Reserve the first two rows
+                XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
+                table = tagCell.getTableRow().getTable();
+                for (int i = table.getNumberOfRows() - 1; i > 1; i--) {
+                    table.removeRow(i);
+                }
+            } catch (Exception e) {
+                throw new RenderException(
+                    "LoopRowCutAndMergeFirstColTable for " + eleTemplate + " error: " + e.getMessage(), e);
+            }
+
+            // in case data not sorted by rank
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
+            mpData.sort((a, b) -> Integer.valueOf(a.getOrDefault("rank", "0").toString())
+                - Integer.valueOf(b.getOrDefault("rank", "0").toString()));
+
+            super.render(eleTemplate, data, template);
+
+            try {
+                // merge the first column
+                Util.mergeFirstNColSimple(table, 1, 0);
+            } catch (Exception e) {
+                throw new RenderException(
+                    "LoopRowCutAndMergeFirstColTable for " + eleTemplate + " error: " + e.getMessage(), e);
+            }
+        }
+    }
+
+    public class LoopRowIncludeStatisticsTableRenderPolicy extends LoopRowTableRenderPolicy {
+
+        private String valueTag;
+        // 存储计算得到的平均值
+        private static final Map<String, String> AVERAGE_VALUES = new HashMap<>();
+
+        public LoopRowIncludeStatisticsTableRenderPolicy(String valueTag) {
+            this.valueTag = valueTag;
+        }
+
+        @Override
+        public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
+
+            for (Map<String, Object> row : mpData) {
+                @SuppressWarnings("unchecked")
+                List<String> values = (List<String>) row.get(valueTag);
+
+                // 检查是否有百分号结尾的值
+                boolean hasPercentage = values.stream().anyMatch(v -> v != null && v.endsWith("%"));
+
+                // 处理百分号并转换为数值
+                List<Double> processedValues = values.stream()
+                    .filter(Objects::nonNull)
+                    .map(v -> {
+                        if (v.endsWith("%")) {
+                            return v.substring(0, v.length() - 1);
+                        }
+                        return v;
+                    })
+                    .map(v -> {
+                        try {
+                            return Double.valueOf(v);
+                        } catch (NumberFormatException e) {
+                            return Double.NaN;
+                        }
+                    })
+                    .filter(v -> !Double.isNaN(v))
+                    .collect(Collectors.toList());
+
+                // 计算平均值并保留两位小数
+                double avg = processedValues.stream()
+                    .mapToDouble(Double::doubleValue)
+                    .average()
+                    .orElse(Double.NaN);
+
+                // 格式化结果
+                String formattedAvg;
+                if (Double.isNaN(avg)) {
+                    formattedAvg = "-"; // 处理无有效数据的情况
+                } else {
+                    DecimalFormat df = new DecimalFormat("#0.00"); // 保留两位小数
+                    formattedAvg = df.format(avg);
+                    if (hasPercentage) {
+                        formattedAvg += "%"; // 添加百分号
+                    }
+
+                }
+
+                row.put("avg", formattedAvg);
+                // AVERAGE_VALUES.put(row.get(""), formattedAvg);
+            }
+
+            super.render(eleTemplate, data, template);
+        }
+    }
+}

+ 5 - 1
easier-report-biz/src/main/java/com/yaoyicloud/render/AbstractRender.java

@@ -95,7 +95,8 @@ public abstract class AbstractRender {
      * @throws IOException
      */
     public final String renderDocx(Map<String, Object> dataMap, byte[] templateFileContent,
-        ConfigureBuilder builder, String relationId, String moduleType) throws IOException {
+        ConfigureBuilder builder, String relationId, String moduleType)
+        throws IOException {
 
         // 注: 报告模板的模板变量按照json序列化的结果命名
         String basicPath = this.getBasicPath();
@@ -670,6 +671,8 @@ public abstract class AbstractRender {
     public class LoopRowIncludeStatisticsTableRenderPolicy extends LoopRowTableRenderPolicy {
 
         private String valueTag;
+        // 存储计算得到的平均值
+        private static final Map<String, String> AVERAGE_VALUES = new HashMap<>();
 
         public LoopRowIncludeStatisticsTableRenderPolicy(String valueTag) {
             this.valueTag = valueTag;
@@ -726,6 +729,7 @@ public abstract class AbstractRender {
                 }
 
                 row.put("avg", formattedAvg);
+                // AVERAGE_VALUES.put(row.get(""), formattedAvg);
             }
 
             super.render(eleTemplate, data, template);

+ 107 - 0
easier-report-biz/src/main/java/com/yaoyicloud/render/platform/update/AntiBriberyNewRender.java

@@ -0,0 +1,107 @@
+package com.yaoyicloud.render.platform.update;
+
+import java.io.IOException;
+import java.util.Map;
+
+import com.deepoove.poi.config.Configure;
+import com.deepoove.poi.config.ConfigureBuilder;
+import com.deepoove.poi.policy.RenderPolicy;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.protobuf.util.JsonFormat;
+import com.yaoyicloud.config.FilerepoProperties;
+import com.yaoyicloud.message.FxyProtos;
+import com.yaoyicloud.render.AbstractNewRender;
+import com.yaoyicloud.render.AbstractRender;
+
+import lombok.extern.slf4j.Slf4j;
+
+
+/**
+ * AntiBribery渲染器
+ *
+ */
+@Slf4j
+public final class AntiBriberyNewRender extends AbstractNewRender {
+    private final FilerepoProperties filerepoProperties;
+
+    // 注入父类所需的 cwd 参数
+    public AntiBriberyNewRender(String cwd, FilerepoProperties filerepoProperties) {
+        super(cwd);
+        this.filerepoProperties = filerepoProperties;
+    }
+
+    @Override
+    protected String getBasicPath() throws IOException {
+        return filerepoProperties.getBasePath();
+    }
+
+    @Override
+    protected String getReportImagePath() {
+        return filerepoProperties.getReportImagePath();
+    }
+
+    /**
+     * Docx 渲染
+     *
+     * @param info 数据
+     * @param addtionalMap 额外数据
+     * @param templateFileContent 模板内容
+     * @param relationId 关联ID
+     * @return 本地文件目录
+     * @throws IOException
+     */
+    public String renderDocx(String info, Map<String, Object> addtionalMap, byte[] templateFileContent, String relationId) throws IOException {
+        log.info("开始渲染其他风险报告模块,relationId: {}", relationId);
+
+        // 配置POI-TL渲染器
+        ConfigureBuilder builder = Configure.builder();
+        RenderPolicy indicatorsRenderPolicy = this.indicatorsRenderPolicyToProtobuf();
+        builder.bind("questionnaireItems", indicatorsRenderPolicy);
+        builder.bind("otherRiskChecks", indicatorsRenderPolicy);
+
+        builder.useSpringEL();
+        // 解析数据
+
+        //通过默认protobuf实例来填充不存在的key
+        FxyProtos.AntiBribery.Builder basicInfoBuilder = FxyProtos.AntiBribery.newBuilder();
+        JsonFormat.parser().merge(info, basicInfoBuilder);
+
+        FxyProtos.AntiBribery defaultInstance = FxyProtos.AntiBribery.getDefaultInstance();
+        FxyProtos.AntiBribery mergedProto = defaultInstance.toBuilder()
+                .mergeFrom(basicInfoBuilder.build())
+                .build();
+
+        String completeJson = JsonFormat.printer()
+                .includingDefaultValueFields()
+                .print(mergedProto);
+        ObjectMapper objectMapper = new ObjectMapper();
+        Map<String, Object> data = objectMapper.readValue(completeJson, new TypeReference<Map<String, Object>>() {});
+        if (addtionalMap != null) {
+            data.putAll(addtionalMap);
+        }
+        fillDefaultValues(data);
+        Map<String, Object> antiBriberySummary = (Map<String, Object>) data.get("antiBriberySummary");
+        String riskSummary = (String) antiBriberySummary.get("riskSummary");
+        antiBriberySummary.put("riskSummary", riskSummary == null || riskSummary.isEmpty() ? "-" : riskSummary);
+
+        try {
+            // 渲染文档
+            String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "antiBribery", antiBriberySummary);
+            log.info("其他风险报告模块渲染成功,文件路径: {}", resultPath);
+            return resultPath;
+        } catch (Exception e) {
+            log.error("其他风险报告模块渲染失败,relationId: {}", relationId, e);
+            throw new IOException("文档渲染失败", e);
+        }
+    }
+
+    /**
+     * 填充默认值,确保所有必要字段都存在
+     */
+    private void fillDefaultValues(Map<String, Object> data) {
+
+
+    }
+
+}

+ 6 - 6
easier-report-biz/src/main/java/com/yaoyicloud/render/platform/update/BasicInfoNewRender.java

@@ -11,7 +11,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.protobuf.util.JsonFormat;
 import com.yaoyicloud.config.FilerepoProperties;
 import com.yaoyicloud.message.FxyProtos;
-import com.yaoyicloud.render.AbstractRender;
+import com.yaoyicloud.render.AbstractNewRender;
+
 
 import lombok.extern.slf4j.Slf4j;
 
@@ -20,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
  *
  */
 @Slf4j
-public final class BasicInfoNewRender extends AbstractRender {
+public final class BasicInfoNewRender extends AbstractNewRender {
     private final FilerepoProperties filerepoProperties;
     public BasicInfoNewRender(String cwd, FilerepoProperties filerepoProperties) {
         super(cwd);
@@ -74,10 +75,10 @@ public final class BasicInfoNewRender extends AbstractRender {
             data.putAll(addtionalMap);
         }
         fillBasicDefaultValues(data);
-
+        Map<String, Object> basicInfoSummary = (Map<String, Object>) data.get("basicInfoSummary");
         try {
             // 渲染文档
-            String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "basicInfo");
+            String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "basicInfo", basicInfoSummary);
             log.info("渲染工商信息报告模块成功,文件路径: {}", resultPath);
             return resultPath;
         } catch (Exception e) {
@@ -90,8 +91,7 @@ public final class BasicInfoNewRender extends AbstractRender {
      * 填充默认值,确保所有必要字段都存在
      */
     private void fillBasicDefaultValues(Map<String, Object> data) {
-        Map<String, Object> basicInfoSummary = (Map<String, Object>) data.get("basicInfoSummary");
-        basicInfoSummary.replaceAll((k, v) -> v.equals("") ? "-" : v);
+
         Map<String, Object> platformExt = (Map<String, Object>) data.get("platformExt");
         platformExt.replaceAll((k, v) -> v.equals("") ? "-" : v);
     }

+ 6 - 6
easier-report-biz/src/main/java/com/yaoyicloud/render/platform/update/FinancialInfoNewRender.java

@@ -10,7 +10,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.protobuf.util.JsonFormat;
 import com.yaoyicloud.config.FilerepoProperties;
 import com.yaoyicloud.message.FxyProtos;
-import com.yaoyicloud.render.AbstractRender;
+import com.yaoyicloud.render.AbstractNewRender;
+
 
 import lombok.extern.slf4j.Slf4j;
 
@@ -20,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
  *
  */
 @Slf4j
-public final class FinancialInfoNewRender extends AbstractRender {
+public final class FinancialInfoNewRender extends AbstractNewRender {
     private final FilerepoProperties filerepoProperties;
     public FinancialInfoNewRender(String cwd, FilerepoProperties filerepoProperties) {
         super(cwd);
@@ -74,10 +75,10 @@ public final class FinancialInfoNewRender extends AbstractRender {
             data.putAll(addtionalMap);
         }
         fillBasicDefaultValues(data);
-
+        Map<String, Object> financialSummary = (Map<String, Object>) data.get("financialSummary");
         try {
             // 渲染文档
-            String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "financialInfo");
+            String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "financialInfo", financialSummary);
             log.info("渲染财务模块成功,文件路径: {}", resultPath);
             return resultPath;
         } catch (Exception e) {
@@ -89,8 +90,7 @@ public final class FinancialInfoNewRender extends AbstractRender {
      * 填充默认值,确保所有必要字段都存在
      */
     private void fillBasicDefaultValues(Map<String, Object> data) {
-        Map<String, Object> basicInfoSummary = (Map<String, Object>) data.get("financialSummary");
-        basicInfoSummary.replaceAll((k, v) -> v.equals("") ? "-" : v);
+
     }
 
 

+ 6 - 8
easier-report-biz/src/main/java/com/yaoyicloud/render/platform/update/PublicRecordNewRender.java

@@ -22,7 +22,8 @@ import com.google.protobuf.util.JsonFormat;
 import com.yaoyicloud.config.FilerepoProperties;
 import com.yaoyicloud.message.FxyProtos.PublicRecord;
 
-import com.yaoyicloud.render.AbstractRender;
+import com.yaoyicloud.render.AbstractNewRender;
+
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 
@@ -31,7 +32,7 @@ import org.apache.commons.collections4.CollectionUtils;
  *
  */
 @Slf4j
-public final class PublicRecordNewRender extends AbstractRender {
+public final class PublicRecordNewRender extends AbstractNewRender {
     private final FilerepoProperties filerepoProperties;
 
     public PublicRecordNewRender(String cwd, FilerepoProperties filerepoProperties) {
@@ -79,10 +80,11 @@ public final class PublicRecordNewRender extends AbstractRender {
             data.putAll(addtionalMap);
         }
         fillBasicDefaultValues(data);
-
+        Map<String, Object> publicRecordSummary = (Map<String, Object>) data.get("publicRecordSummary");
         try {
             // 渲染文档
-            String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "publicRecord");
+            String resultPath =
+                this.renderDocx(data, templateFileContent, builder, relationId, "publicRecord", publicRecordSummary);
             log.info("渲染司法风险模块成功,文件路径: {}", resultPath);
             return resultPath;
         } catch (Exception e) {
@@ -95,10 +97,6 @@ public final class PublicRecordNewRender extends AbstractRender {
      * 填充默认值,确保所有必要字段都存在
      */
     private void fillBasicDefaultValues(Map<String, Object> data) {
-        Map<String, Object> basicInfoSummary = (Map<String, Object>) data.get("publicRecordSummary");
-        basicInfoSummary.putIfAbsent("riskSummary", "-");
-        basicInfoSummary.putIfAbsent("suggestion", "-");
-        basicInfoSummary.replaceAll((k, v) -> v.equals("") ? "-" : v);
 
         List<Map<String, Object>> dishonestPersons = (List<Map<String, Object>>) data.get("dishonestPersons");
         if (CollectionUtils.isEmpty(dishonestPersons)) {

+ 6 - 0
easier-report-biz/src/main/java/com/yaoyicloud/service/impl/ReportServiceImpl.java

@@ -24,6 +24,7 @@ import com.yaoyicloud.render.platform.FinancialInfoRender;
 import com.yaoyicloud.render.platform.ServiceProviderInfoRender;
 import com.yaoyicloud.render.association.AssociationBasicInfoRender;
 import com.yaoyicloud.render.foundation.FoundationBasicInfoRender;
+import com.yaoyicloud.render.platform.update.AntiBriberyNewRender;
 import com.yaoyicloud.render.platform.update.BasicInfoNewRender;
 import com.yaoyicloud.render.platform.update.PublicRecordNewRender;
 import com.yaoyicloud.service.ReportService;
@@ -82,6 +83,11 @@ public class ReportServiceImpl implements ReportService {
                     new AntiBriberyRender(sessionId, filerepoProperties).renderDocx(data, processedData, templateBytes,
                         String.valueOf(relationId));
                 return reportPath;
+            case ANTIBRIBERY_NEW:
+                reportPath =
+                    new AntiBriberyNewRender(sessionId, filerepoProperties).renderDocx(data, processedData, templateBytes,
+                        String.valueOf(relationId));
+                return reportPath;
             case PLATFORM_COMPANY_BASICINFO:
                 reportPath =
                     new BasicInfoRender(sessionId, filerepoProperties).renderDocx(data, processedData, templateBytes,

+ 23 - 1
easier-report-biz/src/main/proto/fxy.proto

@@ -28,9 +28,11 @@ message CheckItemScore {
 }
 
 message CheckSummary {
-  optional int32 score = 1; // ${基本信息评分及建议:基本信息总分}
+ // optional int32 score = 1; // ${基本信息评分及建议:基本信息总分}
   optional string riskSummary = 2; // ${基本信息评分及建议:风险综述}
   optional string suggestion = 3 ; // ${基本信息评分及建议:建议}
+
+  optional string score = 4; // ${基本信息评分及建议:基本信息总分}
 }
 
 message AuditResult {
@@ -290,6 +292,7 @@ message QuestionnaireItem {
 message AntiBribery {
   repeated QuestionnaireItem questionnaireItems = 1; // 反贿赂反腐败诚信保证问卷
   optional CheckSummary antiBriberySummary = 2; // 反贿赂反腐败诚信保证评分及建议
+  repeated CheckItemDetail otherRiskChecks = 3; // 审查内容
 }
 
 message ProjectInfo {
@@ -374,3 +377,22 @@ message AssociationattachmentSection {
   repeated Attachment organizationalStructureScan = 33; //组织结构-扫描件并加盖公章
 
 }
+message SameAddress {
+  // 企业名称
+  optional string name = 1;
+
+  // 成立日期
+  optional string startDate = 2;
+
+  // 企业法人
+  optional string operName = 3;
+
+  // 税号
+  optional string creditNo = 4;
+
+  // 注册资本
+  optional string regCapiDesc = 5;
+
+  // 企业状态
+  optional string status = 6;
+}

+ 2 - 2
easier-report-biz/src/test/java/com/yaoyicloud/render/test/TestAntiBriberyRender.java

@@ -14,7 +14,7 @@
 //import com.yaoyicloud.message.FxyProtos.AntiBribery;
 //import com.yaoyicloud.message.FxyProtos.CheckSummary;
 //import com.yaoyicloud.message.FxyProtos.QuestionnaireItem;
-//import com.yaoyicloud.render.platform.AntiBriberyRender;
+//import com.yaoyicloud.render.platform.AntiBriberyNewRender;
 //
 //public class
 //
@@ -30,7 +30,7 @@
 //
 //        byte[] content = Files
 //            .readAllBytes(Paths.get(getClass().getClassLoader().getResource("docx/antiBribery.docx").getFile()));
-//        AntiBriberyRender render = new AntiBriberyRender("../temp/");
+//        AntiBriberyNewRender render = new AntiBriberyNewRender("../temp/");
 //        String retPath = render.renderDocx(
 //            AntiBribery.newBuilder()
 //                .setAntiBriberySummary(CheckSummary.newBuilder().setRiskSummary("high risk").setSuggestion("accept it"))