关于word生成报告的POI学习

一. 实战代码

java 复制代码
import com.deepoove.poi.policy.AbstractRenderPolicy;
import com.deepoove.poi.render.RenderContext;
import com.deepoove.poi.xwpf.XWPFParagraphWrapper;
import org.apache.poi.xwpf.usermodel.*;
import java.util.List;
import java.math.BigInteger;

import org.apache.xmlbeans.XmlCursor;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*; // 添加必要的OpenXML格式导入


public class VulnerabilityPolicy extends AbstractRenderPolicy<Object> {

    @Override
    public void doRender(RenderContext<Object> context) throws Exception {
        // 获取模板标签位置的XWPFParagraph
        XWPFParagraph startPara = (XWPFParagraph) context.getRun().getParent();
        XWPFDocument doc = startPara.getDocument();
        
        // 获取要渲染的漏洞列表数据
        List<RiskReportDTO.SecurityRiskReport> vulList = 
            (List<RiskReportDTO.SecurityRiskReport>) context.getData();

        // 清除模板标签内容,但保留段落的样式
        while (startPara.getRuns().size() > 0) {
            startPara.removeRun(0);
        }

        if (vulList != null && !vulList.isEmpty()) {
            // 动态处理漏洞列表,无论漏洞数量多少都能正确渲染
            // 使用索引来生成正确的顺序编号,确保章节按2.2.1、2.2.2...正序排列
            XWPFParagraph currentPara = startPara;
            for (int i = 0; i < vulList.size(); i++) {
                RiskReportDTO.SecurityRiskReport vul = vulList.get(i);
                // 忽略原有编号,使用索引生成顺序编号
                int actualNumber = i + 1;
                vul.setNumber("2.2." + actualNumber); // 生成正确的顺序编号
                
                // 为每个漏洞生成一个完整的章节
                // 使用currentPara作为插入位置,确保内容按顺序排列
                // 所有漏洞都使用相同的generateVulnerabilitySection方法,确保标题样式一致
                currentPara = generateVulnerabilitySection(currentPara, vul, i);
            }
        }
    }
    
    /**
     * 生成风险影响表格
     * @param doc 文档对象
     * @param afterPara 插入位置的段落
     * @param riskImpactDTOList 风险影响数据列表
     */
    public void generateRiskImpactTable(XWPFDocument doc, XWPFParagraph afterPara, List<RiskReportDTO.RiskImpactDTO> riskImpactDTOList) {
        // 创建表格并设置样式
        XWPFTable table = doc.insertNewTbl(afterPara.getCTP().newCursor());
        
        // 设置表格样式
        CTTblPr tblPr = table.getCTTbl().addNewTblPr();
        CTTblWidth tblWidth = tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.PCT); // 设置为百分比类型
        tblWidth.setW(BigInteger.valueOf(5000)); // 100% = 10000

        // 设置表格边框
        CTTblBorders borders = tblPr.addNewTblBorders();
        setTableBorders(borders, STBorder.SINGLE, BigInteger.valueOf(2), "000000");
        
        // 创建表格网格,定义4列的宽度 - 必须在创建任何行之前执行
        CTTblGrid tblGrid = table.getCTTbl().addNewTblGrid();
        int[] columnWidths = {1100, 1100, 1600, 1200}; // 调整列宽比例,总和5000,隐患类型列改为1200
        for (int i = 0; i < 4; i++) {
            CTTblGridCol gridCol = tblGrid.addNewGridCol();
            gridCol.setW(BigInteger.valueOf(columnWidths[i]));
        }
        
        // 检查并删除表格默认创建的行(如果有)
        while (table.getRows().size() > 0) {
            table.removeRow(0);
        }
        
        // 创建表头行 - 此时表格已有明确的4列定义,createRow()会创建4个单元格
        XWPFTableRow headerRow = table.createRow();
        
        // 确保表头行有4个单元格
        while (headerRow.getTableCells().size() < 4) {
            headerRow.addNewTableCell();
        }
        
        // 设置表头单元格内容和样式
        String[] headers = {"序号", "隐患等级", "隐患名称", "隐患类型"};
        
        for (int i = 0; i < headers.length; i++) {
            XWPFTableCell cell = headerRow.getCell(i);
            
            // 清除单元格内的现有段落
            while (cell.getParagraphs().size() > 0) {
                cell.removeParagraph(0);
            }
            
            XWPFParagraph para = cell.addParagraph();
            // 设置段落居中对齐
            para.setAlignment(ParagraphAlignment.CENTER);
            XWPFRun run = para.createRun();
            run.setText(headers[i]);
            run.setFontSize(14); // 小四字体
            // 不加粗
            run.setBold(false);
            
            // 设置表头背景为灰色
            CTTcPr cellPr = cell.getCTTc().addNewTcPr();
            CTShd shd = cellPr.addNewShd();
            shd.setFill("b8cce4"); // 灰色背景
            shd.setVal(STShd.CLEAR);
            
            // 设置单元格宽度
            CTTblWidth cellWidth = cellPr.addNewTcW();
            cellWidth.setType(STTblWidth.PCT);
            cellWidth.setW(BigInteger.valueOf(columnWidths[i]));
            
            // 设置单元格边框
            CTTcBorders cellBorders = cellPr.addNewTcBorders();
            createCellBorder(cellBorders.addNewTop(), STBorder.SINGLE, BigInteger.valueOf(2), "000000");
            createCellBorder(cellBorders.addNewBottom(), STBorder.SINGLE, BigInteger.valueOf(2), "000000");
            createCellBorder(cellBorders.addNewLeft(), STBorder.SINGLE, BigInteger.valueOf(2), "000000");
            createCellBorder(cellBorders.addNewRight(), STBorder.SINGLE, BigInteger.valueOf(2), "000000");
        }
        

        
        // 填充数据行
        if (riskImpactDTOList != null && !riskImpactDTOList.isEmpty()) {
            for (RiskReportDTO.RiskImpactDTO riskImpactDTO : riskImpactDTOList) {
                XWPFTableRow dataRow = table.createRow();
                
                // 确保数据行有4个单元格
                while (dataRow.getTableCells().size() < 4) {
                    dataRow.addNewTableCell();
                }
                
                // 设置序号
                XWPFTableCell orderCell = dataRow.getCell(0);
                if (orderCell == null) {
                    orderCell = dataRow.addNewTableCell();
                }
                setCellContent(orderCell, riskImpactDTO.getOrder() != null ? riskImpactDTO.getOrder().toString() : "");
                
                // 设置隐患等级
                XWPFTableCell levelCell = dataRow.getCell(1);
                if (levelCell == null) {
                    levelCell = dataRow.addNewTableCell();
                }
                setCellContent(levelCell, riskImpactDTO.getRiskLevel() != null ? riskImpactDTO.getRiskLevel() : "");
                
                // 设置隐患名称
                XWPFTableCell nameCell = dataRow.getCell(2);
                if (nameCell == null) {
                    nameCell = dataRow.addNewTableCell();
                }
                setCellContent(nameCell, riskImpactDTO.getRiskName() != null ? riskImpactDTO.getRiskName() : "");
                
                // 设置隐患类型
                XWPFTableCell typeCell = dataRow.getCell(3);
                if (typeCell == null) {
                    typeCell = dataRow.addNewTableCell();
                }
                setCellContent(typeCell, riskImpactDTO.getRiskType() != null ? riskImpactDTO.getRiskType() : "");
            }
            // 设置表头行高
        setTableRowHeight(table, 400, 2);
        }
    }
    
    /**
     * 设置单元格内容和基本样式
     * @param cell 单元格对象
     * @param content 单元格内容
     */
    private void setCellContent(XWPFTableCell cell, String content) {
        if (cell == null) {
            return; // 如果单元格为null,直接返回,避免空指针异常
        }
        
        // 清除单元格内的现有段落
        while (cell.getParagraphs().size() > 0) {
            cell.removeParagraph(0);
        }
        
        XWPFParagraph para = cell.addParagraph();
        // 设置段落居中对齐
        para.setAlignment(ParagraphAlignment.CENTER);
        XWPFRun run = para.createRun();
        run.setText(content);
        run.setFontSize(12);
        
        // 设置单元格边框
        CTTcPr cellPr = cell.getCTTc().addNewTcPr();
        CTTcBorders cellBorders = cellPr.addNewTcBorders();
        createCellBorder(cellBorders.addNewTop(), STBorder.SINGLE, BigInteger.valueOf(2), "000000");
        createCellBorder(cellBorders.addNewBottom(), STBorder.SINGLE, BigInteger.valueOf(2), "000000");
        createCellBorder(cellBorders.addNewLeft(), STBorder.SINGLE, BigInteger.valueOf(2), "000000");
        createCellBorder(cellBorders.addNewRight(), STBorder.SINGLE, BigInteger.valueOf(2), "000000");
    }
    
    /**
     * 创建单元格边框样式
     * @param border 边框对象
     * @param borderType 边框类型
     * @param size 边框粗细
     * @param color 边框颜色
     */
    private void createCellBorder(CTBorder border, STBorder.Enum borderType, BigInteger size, String color) {
        border.setVal(borderType);
        border.setSz(size);
        border.setColor(color);
    }
    
    /**
     * 设置表格边框样式
     * @param borders 表格边框对象
     * @param borderType 边框类型
     * @param size 边框粗细
     * @param color 边框颜色
     */
    private void setTableBorders(CTTblBorders borders, STBorder.Enum borderType, BigInteger size, String color) {
        createCellBorder(borders.addNewTop(), borderType, size, color);
        createCellBorder(borders.addNewBottom(), borderType, size, color);
        createCellBorder(borders.addNewLeft(), borderType, size, color);
        createCellBorder(borders.addNewRight(), borderType, size, color);
        createCellBorder(borders.addNewInsideH(), borderType, size, color);
        createCellBorder(borders.addNewInsideV(), borderType, size, color);
    }
    
    /**
     * 设置表格边框样式(支持内部边框使用不同大小)
     * @param borders 表格边框对象
     * @param borderType 边框类型
     * @param outerSize 外部边框粗细
     * @param innerSize 内部边框粗细
     * @param color 边框颜色
     */
    private void setTableBorders(CTTblBorders borders, STBorder.Enum borderType, BigInteger outerSize, BigInteger innerSize, String color) {
        // 设置外部边框
        createCellBorder(borders.addNewTop(), borderType, outerSize, color);
        createCellBorder(borders.addNewBottom(), borderType, outerSize, color);
        createCellBorder(borders.addNewLeft(), borderType, outerSize, color);
        createCellBorder(borders.addNewRight(), borderType, outerSize, color);
        // 设置内部边框
        createCellBorder(borders.addNewInsideH(), borderType, innerSize, color);
        createCellBorder(borders.addNewInsideV(), borderType, innerSize, color);
    }

    private XWPFParagraph generateVulnerabilitySection(XWPFParagraph startPara, RiskReportDTO.SecurityRiskReport vul, int index) {
        XWPFDocument doc = startPara.getDocument();
        
        // 最终顺序应该是:
        // 1. 主标题(2.2.1 SQL注入漏洞)
        // 2. 属性表格
        // 3. 子章节.1(域名主办单位信息截图)及其描述
        // 4. 子章节.2(隐患验证截图)及其描述
        
        XWPFParagraph titlePara;
        XmlCursor cursor;
        
        if (index == 0) {
            // 第一个漏洞:直接使用startPara作为标题段落,避免空行
            titlePara = startPara;
            // 设置为Word标题样式(标题3级别)
            titlePara.setStyle("Heading 3");
            
            // 直接设置段落的大纲级别为3,确保导航功能正常
            if (titlePara.getCTP().getPPr() == null) {
                titlePara.getCTP().addNewPPr();
            }
            CTDecimalNumber outlineLevel = titlePara.getCTP().getPPr().addNewOutlineLvl();
            outlineLevel.setVal(BigInteger.valueOf(3));
            
            // 使用startPara的光标作为起点,确保插入位置正确
            cursor = startPara.getCTP().newCursor();
            cursor.toNextToken();
        } else {
            // 后续漏洞:在startPara后面插入新的标题段落
            // 使用startPara的光标作为起点,确保插入位置正确
            cursor = startPara.getCTP().newCursor();
            cursor.toNextToken();
            
            // 创建主标题段落
            titlePara = doc.insertNewParagraph(cursor);
            if (titlePara == null) {
                titlePara = doc.createParagraph();
            }
            // 设置为Word标题样式(标题3级别)
            titlePara.setStyle("Heading 3");
            
            // 直接设置段落的大纲级别为3,确保导航功能正常
            CTDecimalNumber outlineLevel = titlePara.getCTP().addNewPPr().addNewOutlineLvl();
            outlineLevel.setVal(BigInteger.valueOf(3));
        }
        
        // 为标题添加书签,支持点击导航
        String bookmarkName = "vul_" + vul.getNumber().replace(".", "_");
        titlePara.getCTP().addNewBookmarkStart().setName(bookmarkName);
        
        XWPFRun titleRun = titlePara.createRun();
        titleRun.setText(vul.getNumber() + " " + vul.getIssueName());
        titleRun.setBold(true); // 加粗
        titleRun.setFontSize(16); // 三号字体
        
        titlePara.getCTP().addNewBookmarkEnd().setId(BigInteger.valueOf(index));

        // 2. 创建属性表格 - 在主标题后面插入
        cursor = titlePara.getCTP().newCursor();
        cursor.toNextToken();
        XWPFTable propertyTable = doc.insertNewTbl(cursor);
        if (propertyTable == null) {
            propertyTable = doc.createTable();
        }
        generatePropertyTable(doc, propertyTable, vul);

        // 3. 创建子章节1标题 - 在表格后面插入
        XmlCursor sub1Cursor = propertyTable.getCTTbl().newCursor();
        sub1Cursor.toNextToken();
        XWPFParagraph sub1TitlePara = doc.insertNewParagraph(sub1Cursor);
        if (sub1TitlePara == null) {
            sub1TitlePara = doc.createParagraph();
        }
        // 设置为Word标题样式(标题4级别)
        sub1TitlePara.setStyle("Heading 4");
        
        // 直接设置段落的大纲级别为4,确保导航功能正常
        if (sub1TitlePara.getCTP().getPPr() == null) {
            sub1TitlePara.getCTP().addNewPPr();
        }
        CTDecimalNumber sub1OutlineLevel = sub1TitlePara.getCTP().getPPr().addNewOutlineLvl();
        sub1OutlineLevel.setVal(BigInteger.valueOf(4));
        
        // 为子标题添加书签,支持点击导航
        String sub1BookmarkName = "vul_" + vul.getNumber().replace(".", "_") + "_sub1";
        sub1TitlePara.getCTP().addNewBookmarkStart().setName(sub1BookmarkName);
        
        XWPFRun sub1TitleRun = sub1TitlePara.createRun();
        sub1TitleRun.setText(vul.getNumber() + ".1. 域名主办单位信息截图");
        sub1TitleRun.setBold(true); // 加粗
        sub1TitleRun.setFontSize(14); // 四号字体
        
        sub1TitlePara.getCTP().addNewBookmarkEnd().setId(BigInteger.valueOf(index * 10 + 1));

        // 4. 创建子章节1描述 - 在子章节1标题后面插入
        XmlCursor sub1DescCursor = sub1TitlePara.getCTP().newCursor();
        sub1DescCursor.toNextToken();
        XWPFParagraph sub1DescPara = doc.insertNewParagraph(sub1DescCursor);
        if (sub1DescPara == null) {
            sub1DescPara = doc.createParagraph();
        }
        // 确保没有任何缩进
        sub1DescPara.setIndentationFirstLine(0);
        sub1DescPara.setIndentationLeft(0);
        sub1DescPara.setIndentationRight(0);
        XWPFRun sub1DescRun = sub1DescPara.createRun();
        sub1DescRun.setText("(提供域名主办单位信息截图,例:工信部备案官方网站 beian.miit.gov.cn 截图、域名含有主办单位相关信息截图等)");
        sub1DescRun.setFontSize(12);

        // 5. 创建子章节2标题 - 在子章节1描述后面插入
        XmlCursor sub2Cursor = sub1DescPara.getCTP().newCursor();
        sub2Cursor.toNextToken();
        XWPFParagraph sub2TitlePara = doc.insertNewParagraph(sub2Cursor);
        if (sub2TitlePara == null) {
            sub2TitlePara = doc.createParagraph();
        }
        // 设置为Word标题样式(标题4级别)
        sub2TitlePara.setStyle("Heading 4");
        
        // 直接设置段落的大纲级别为4,确保导航功能正常
        if (sub2TitlePara.getCTP().getPPr() == null) {
            sub2TitlePara.getCTP().addNewPPr();
        }
        CTDecimalNumber sub2OutlineLevel = sub2TitlePara.getCTP().getPPr().addNewOutlineLvl();
        sub2OutlineLevel.setVal(BigInteger.valueOf(4));
        
        // 为子标题添加书签,支持点击导航
        String sub2BookmarkName = "vul_" + vul.getNumber().replace(".", "_") + "_sub2";
        sub2TitlePara.getCTP().addNewBookmarkStart().setName(sub2BookmarkName);
        
        XWPFRun sub2TitleRun = sub2TitlePara.createRun();
        sub2TitleRun.setText(vul.getNumber() + ".2. 隐患验证截图");
        sub2TitleRun.setBold(true); // 加粗
        sub2TitleRun.setFontSize(14); // 四号字体
        
        sub2TitlePara.getCTP().addNewBookmarkEnd().setId(BigInteger.valueOf(index * 10 + 2));

        // 6. 创建子章节2描述 - 在子章节2标题后面插入
        XmlCursor sub2DescCursor = sub2TitlePara.getCTP().newCursor();
        sub2DescCursor.toNextToken();
        XWPFParagraph sub2DescPara = doc.insertNewParagraph(sub2DescCursor);
        if (sub2DescPara == null) {
            sub2DescPara = doc.createParagraph();
        }
        // 确保没有任何缩进
        sub2DescPara.setIndentationFirstLine(0);
        sub2DescPara.setIndentationLeft(0);
        sub2DescPara.setIndentationRight(0);
        XWPFRun sub2DescRun = sub2DescPara.createRun();
        sub2DescRun.setText("(提供清晰的验证截图并配上简单说明)");
        sub2DescRun.setFontSize(12);

        // 关闭所有光标
        cursor.dispose();
        sub1Cursor.dispose();
        sub1DescCursor.dispose();
        sub2Cursor.dispose();
        sub2DescCursor.dispose();
        
        // 返回最后创建的段落作为下一个漏洞章节的参考点
        return sub2DescPara;
    }
    
    /**
     * 生成漏洞章节内容,但不创建标题段落
     * 用于第一个漏洞,避免在标题前产生空行
     */
    private XWPFParagraph generateVulnerabilitySectionWithoutTitle(XWPFParagraph startPara, RiskReportDTO.SecurityRiskReport vul, int index) {
        XWPFDocument doc = startPara.getDocument();
        
        // 最终顺序应该是:
        // 1. 属性表格
        // 2. 子章节.1(域名主办单位信息截图)及其描述
        // 3. 子章节.2(隐患验证截图)及其描述
        
        // 使用startPara的光标作为起点,确保插入位置正确
        XmlCursor cursor = startPara.getCTP().newCursor();
        cursor.toNextToken();

        // 1. 创建属性表格 - 在startPara后面插入
        XWPFTable propertyTable = doc.insertNewTbl(cursor);
        if (propertyTable == null) {
            propertyTable = doc.createTable();
        }
        generatePropertyTable(doc, propertyTable, vul);

        // 2. 创建子章节1标题 - 在表格后面插入
        XmlCursor sub1Cursor = propertyTable.getCTTbl().newCursor();
        sub1Cursor.toNextToken();
        XWPFParagraph sub1TitlePara = doc.insertNewParagraph(sub1Cursor);
        if (sub1TitlePara == null) {
            sub1TitlePara = doc.createParagraph();
        }
        // 设置为Word标题样式(标题4级别)
        sub1TitlePara.setStyle("Heading 4");
        
        // 直接设置段落的大纲级别为4,确保导航功能正常
        if (sub1TitlePara.getCTP().getPPr() == null) {
            sub1TitlePara.getCTP().addNewPPr();
        }
        CTDecimalNumber sub1OutlineLevel = sub1TitlePara.getCTP().getPPr().addNewOutlineLvl();
        sub1OutlineLevel.setVal(BigInteger.valueOf(4));
        
        // 为子标题添加书签,支持点击导航
        String sub1BookmarkName = "vul_" + vul.getNumber().replace(".", "_") + "_sub1";
        sub1TitlePara.getCTP().addNewBookmarkStart().setName(sub1BookmarkName);
        
        XWPFRun sub1TitleRun = sub1TitlePara.createRun();
        sub1TitleRun.setText(vul.getNumber() + ".1. 域名主办单位信息截图");
        sub1TitleRun.setBold(true); // 加粗
        sub1TitleRun.setFontSize(14); // 四号字体
        
        sub1TitlePara.getCTP().addNewBookmarkEnd().setId(BigInteger.valueOf(index * 10 + 1));

        // 3. 创建子章节1描述 - 在子章节1标题后面插入
        XmlCursor sub1DescCursor = sub1TitlePara.getCTP().newCursor();
        sub1DescCursor.toNextToken();
        XWPFParagraph sub1DescPara = doc.insertNewParagraph(sub1DescCursor);
        if (sub1DescPara == null) {
            sub1DescPara = doc.createParagraph();
        }
        // 确保没有任何缩进
        sub1DescPara.setIndentationFirstLine(0);
        sub1DescPara.setIndentationLeft(0);
        sub1DescPara.setIndentationRight(0);
        XWPFRun sub1DescRun = sub1DescPara.createRun();
        sub1DescRun.setText("(提供域名主办单位信息截图,例:工信部备案官方网站 beian.miit.gov.cn 截图、域名含有主办单位相关信息截图等)");
        sub1DescRun.setFontSize(12);

        // 4. 创建子章节2标题 - 在子章节1描述后面插入
        XmlCursor sub2Cursor = sub1DescPara.getCTP().newCursor();
        sub2Cursor.toNextToken();
        XWPFParagraph sub2TitlePara = doc.insertNewParagraph(sub2Cursor);
        if (sub2TitlePara == null) {
            sub2TitlePara = doc.createParagraph();
        }
        // 设置为Word标题样式(标题4级别)
        sub2TitlePara.setStyle("Heading 4");
        
        // 直接设置段落的大纲级别为4,确保导航功能正常
        if (sub2TitlePara.getCTP().getPPr() == null) {
            sub2TitlePara.getCTP().addNewPPr();
        }
        CTDecimalNumber sub2OutlineLevel = sub2TitlePara.getCTP().getPPr().addNewOutlineLvl();
        sub2OutlineLevel.setVal(BigInteger.valueOf(4));
        
        // 为子标题添加书签,支持点击导航
        String sub2BookmarkName = "vul_" + vul.getNumber().replace(".", "_") + "_sub2";
        sub2TitlePara.getCTP().addNewBookmarkStart().setName(sub2BookmarkName);
        
        XWPFRun sub2TitleRun = sub2TitlePara.createRun();
        sub2TitleRun.setText(vul.getNumber() + ".2. 隐患验证截图");
        sub2TitleRun.setBold(true); // 加粗
        sub2TitleRun.setFontSize(14); // 四号字体
        
        sub2TitlePara.getCTP().addNewBookmarkEnd().setId(BigInteger.valueOf(index * 10 + 2));

        // 5. 创建子章节2描述 - 在子章节2标题后面插入
        XmlCursor sub2DescCursor = sub2TitlePara.getCTP().newCursor();
        sub2DescCursor.toNextToken();
        XWPFParagraph sub2DescPara = doc.insertNewParagraph(sub2DescCursor);
        if (sub2DescPara == null) {
            sub2DescPara = doc.createParagraph();
        }
        // 确保没有任何缩进
        sub2DescPara.setIndentationFirstLine(0);
        sub2DescPara.setIndentationLeft(0);
        sub2DescPara.setIndentationRight(0);
        XWPFRun sub2DescRun = sub2DescPara.createRun();
        sub2DescRun.setText("(提供清晰的验证截图并配上简单说明)");
        sub2DescRun.setFontSize(12);

        // 关闭所有光标
        cursor.dispose();
        sub1Cursor.dispose();
        sub1DescCursor.dispose();
        sub2Cursor.dispose();
        sub2DescCursor.dispose();
        
        // 返回最后创建的段落作为下一个漏洞章节的参考点
        return sub2DescPara;
    }

    private void generatePropertyTable(XWPFDocument doc, XWPFTable table, RiskReportDTO.SecurityRiskReport vul) {

        // 设置表格样式
        CTTblPr tblPr = table.getCTTbl().addNewTblPr();
        CTTblWidth tblWidth = tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.PCT); // 改为百分比类型
        tblWidth.setW(BigInteger.valueOf(5000)); // 100% = 10000 (因为单位是1/1000)
        
        // 设置表格边框(外部边框2pt,内部边框1pt)
        CTTblBorders borders = tblPr.addNewTblBorders();
        setTableBorders(borders, STBorder.SINGLE, BigInteger.valueOf(2), BigInteger.valueOf(1), "000000");

        String[][] data = {
            {"隐患风险等级", vul.getRiskLevel()},
            {"隐患URL", vul.getRiskUrl()},
            {"隐患是否验证", vul.getVerified()},
            {"隐患危害", vul.getRiskImpact()},
            {"修补建议", vul.getFixSuggestion()}
        };

        for (int i = 0; i < data.length; i++) {
            XWPFTableRow row = (i == 0) ? table.getRow(0) : table.createRow();

            // 确保每行都有两个单元格
            if (row.getTableCells().size() < 2) {
                row.addNewTableCell();
            }

            // 设置表头样式
            XWPFTableCell cell0 = row.getCell(0);
            // 清除单元格内的现有段落
            while (cell0.getParagraphs().size() > 0) {
                cell0.removeParagraph(0);
            }
            XWPFParagraph para0 = cell0.addParagraph();
            para0.setAlignment(ParagraphAlignment.CENTER); // 水平居中
            XWPFRun cell0Run = para0.createRun();
            cell0Run.setText(data[i][0]);
            cell0Run.setFontSize(12);
//            cell0Run.setBold(true); // 表头加粗
            
            // 设置垂直居中
            cell0.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);

            // 设置数据单元格
            XWPFTableCell cell1 = row.getCell(1);
            // 清除单元格内的现有段落
            while (cell1.getParagraphs().size() > 0) {
                cell1.removeParagraph(0);
            }
            XWPFParagraph para1 = cell1.addParagraph();
            para1.setAlignment(ParagraphAlignment.LEFT); // 水平左对齐
            XWPFRun cell1Run = para1.createRun();
            cell1Run.setFontSize(12);
            // 如果是URL单元格,设置为红色
//            if ("隐患URL".equals(data[i][0])) {
//                cell1Run.setColor("FF0000");
//            }
            
            // 设置垂直居中
            cell1.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);

            // 确保数据不为null
            if (data[i][1] != null) {
                cell1Run.setText(data[i][1]);
            } else {
                cell1Run.setText(""); // 空值处理
            }
        }
        
        // 1. 设置表格为100%页面宽度
        setTableFullWidth(table);

        // 2. 设置列宽比例(左侧40%,右侧60%)- 所有行创建完成后调用,确保所有行应用相同的列宽
        setColumnWidths(table, 1500, 3500);
        
        // 设置行高
        setTableRowHeight(table, 500, 0);
    }

    private XWPFParagraph generateSubSection(XWPFDocument doc, XWPFParagraph afterPara, String number, String title, String description, int index) {
        // 添加空值检查
        if (afterPara == null) {
            // 如果afterPara为null,创建一个新段落作为起始点
            afterPara = doc.createParagraph();
        }
        
        // 生成子章节标题,格式为 "编号. 标题名",并确保正确的编号格式
        XmlCursor cursor = afterPara.getCTP().newCursor();
        XWPFParagraph subTitlePara = doc.insertNewParagraph(cursor);
        // 添加null检查,确保subTitlePara有效
        if (subTitlePara == null) {
            subTitlePara = doc.createParagraph();
        }
        // 设置为Word标题样式(标题4级别)
        subTitlePara.setStyle("Heading 4");
        
        // 为子章节标题添加书签,支持点击导航
        String bookmarkName = "vul_" + number.replace(".", "_");
        subTitlePara.getCTP().addNewBookmarkStart().setName(bookmarkName);
        
        XWPFRun subTitleRun = subTitlePara.createRun();
        subTitleRun.setText(number + ". " + title); // 添加空格分隔编号和标题
        subTitleRun.setBold(true); // 加粗
        subTitleRun.setFontSize(14); // 四号字体
        
        subTitlePara.getCTP().addNewBookmarkEnd().setId(BigInteger.valueOf(index * 10 + 3)); // 使用不同的偏移量避免冲突

        // 生成描述文本段落
        cursor = subTitlePara.getCTP().newCursor();
        XWPFParagraph descPara = doc.insertNewParagraph(cursor);
        // 添加null检查,确保descPara有效
        if (descPara == null) {
            descPara = doc.createParagraph();
        }
        
        // 确保没有任何缩进
        descPara.setIndentationFirstLine(0); // 首行缩进0
        descPara.setIndentationLeft(0); // 左侧缩进0
        descPara.setIndentationRight(0); // 右侧缩进0
        
        XWPFRun descRun = descPara.createRun();
        descRun.setText(description); // 直接将description放到内容里
        descRun.setFontSize(11);
        
        return descPara; // 返回最后插入的段落,以便下一次调用使用正确的插入点
    }

    /**
     * 设置表格为100%页面宽度
     */
    private void setTableFullWidth(XWPFTable table) {
        CTTblPr tblPr = table.getCTTbl().addNewTblPr();
        CTTblWidth tblWidth = tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.PCT); // 百分比类型
        tblWidth.setW(BigInteger.valueOf(5000)); // 100% = 10000

        // 设置表格左对齐
        CTJc jc = tblPr.addNewJc();
        jc.setVal(STJc.LEFT);
    }

    /**
     * 设置列宽比例
     * @param leftPercent 左侧列宽度百分比(单位:1/1000)
     * @param rightPercent 右侧列宽度百分比(单位:1/1000)
     */
    private void setColumnWidths(XWPFTable table, int leftPercent, int rightPercent) {
        // 设置左侧列(0列)宽度
        for (XWPFTableRow row : table.getRows()) {
            if (row.getTableCells().size() > 0) {
                XWPFTableCell leftCell = row.getCell(0);
                CTTcPr leftTcPr = leftCell.getCTTc().addNewTcPr();
                CTTblWidth leftWidth = leftTcPr.addNewTcW();
                leftWidth.setType(STTblWidth.PCT);
                leftWidth.setW(BigInteger.valueOf(leftPercent));

//                // 设置左侧单元格背景色(浅灰色)
//                leftCell.setColor("F2F2F2");
            }

            // 设置右侧列(1列)宽度
            if (row.getTableCells().size() > 1) {
                XWPFTableCell rightCell = row.getCell(1);
                CTTcPr rightTcPr = rightCell.getCTTc().addNewTcPr();
                CTTblWidth rightWidth = rightTcPr.addNewTcW();
                rightWidth.setType(STTblWidth.PCT);
                rightWidth.setW(BigInteger.valueOf(rightPercent));
            }
        }
    }

    /**
     * 设置表格的固定行高
     * 单位:twips(1磅 = 20 twips)
     */
    private static void setTableRowHeight(XWPFTable table, int rowHeightTwips,double firstADD) {
        int idx=0;
        for (XWPFTableRow row : table.getRows()) {
            // 获取行的属性
            CTTrPr trPr = row.getCtRow().isSetTrPr() ?
                    row.getCtRow().getTrPr() : row.getCtRow().addNewTrPr();
            CTHeight ctHeight = CTHeight.Factory.newInstance();
            if (firstADD!=0&& idx==0){
                idx++;
                ctHeight.setVal(BigInteger.valueOf(rowHeightTwips* 2L)); // 行高值
            }else {
                ctHeight.setVal(BigInteger.valueOf(rowHeightTwips)); // 行高值
            }
            // 设置行高
            ctHeight.setHRule(STHeightRule.EXACT); // EXACT: 固定高度

            trPr.setTrHeightArray(new CTHeight[]{ctHeight});

            // 设置单元格垂直居中
            for (XWPFTableCell cell : row.getTableCells()) {
                cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
            }
        }
    }
}

二.相关API讲解

1.自定义策略类的创建(需要创建格式才需要创建,非必要)

1.策略使用(这里我是由于业务需求需要模板化标题使用)
  1. 继承AbstractRenderPolicy,并重写doRender方法
java 复制代码
public class VulnerabilityPolicy extends AbstractRenderPolicy<Object> {

    @Override
    public void doRender(RenderContext<Object> context) throws Exception {
        // 获取模板标签位置的XWPFParagraph
        XWPFParagraph startPara = (XWPFParagraph) context.getRun().getParent();
        XWPFDocument doc = startPara.getDocument();
        
        // 获取要渲染的漏洞列表数据
        List<RiskReportDTO.SecurityRiskReport> vulList = 
            (List<RiskReportDTO.SecurityRiskReport>) context.getData();

        // 清除模板标签内容,但保留段落的样式
        while (startPara.getRuns().size() > 0) {
            startPara.removeRun(0);
        }
         
  1. 标题生成以及获取光标 光标这里其实可以获取最后光标,如果最后无数据
java 复制代码
        if (vulList != null && !vulList.isEmpty()) {
            // 动态处理漏洞列表,无论漏洞数量多少都能正确渲染
            // 使用索引来生成正确的顺序编号,确保章节按2.2.1、2.2.2...正序排列
            XWPFParagraph currentPara = startPara;
            for (int i = 0; i < vulList.size(); i++) {
                RiskReportDTO.SecurityRiskReport vul = vulList.get(i);
                // 忽略原有编号,使用索引生成顺序编号
                int actualNumber = i + 1;
                vul.setNumber("2.2." + actualNumber); // 生成正确的顺序编号
                
                // 为每个漏洞生成一个完整的章节
                // 使用currentPara作为插入位置,确保内容按顺序排列
                // 所有漏洞都使用相同的generateVulnerabilitySection方法,确保标题样式一致
                currentPara = generateVulnerabilitySection(currentPara, vul, i);
            }
        }
    }
  1. 设置大纲级别和插入位置,以及导航跳转

这里表格样式setStyle设置失败了,我怀疑是标题适配性问题,由于DDL问题没有时间去测试这个了,所以兄弟们自己可以测一下,通过自定义标题格式来设置setStyle。

书签其实就是加个唯一值跳转

java 复制代码
   // 后续漏洞:在startPara后面插入新的标题段落
            // 使用startPara的光标作为起点,确保插入位置正确
            cursor = startPara.getCTP().newCursor();
            cursor.toNextToken();
            
            // 创建主标题段落
            titlePara = doc.insertNewParagraph(cursor);
            if (titlePara == null) {
                titlePara = doc.createParagraph();
            }
            // 设置为Word标题样式(标题3级别)
            titlePara.setStyle("Heading 3");
            
            // 直接设置段落的大纲级别为3,确保导航功能正常
            CTDecimalNumber outlineLevel = titlePara.getCTP().addNewPPr().addNewOutlineLvl();
            outlineLevel.setVal(BigInteger.valueOf(3));
        }
        
        // 为标题添加书签,支持点击导航
        String bookmarkName = "vul_" + vul.getNumber().replace(".", "_");
        titlePara.getCTP().addNewBookmarkStart().setName(bookmarkName);
        
        XWPFRun titleRun = titlePara.createRun();
        titleRun.setText(vul.getNumber() + " " + vul.getIssueName());
        titleRun.setBold(true); // 加粗
        titleRun.setFontSize(16); // 三号字体
        
        titlePara.getCTP().addNewBookmarkEnd().setId(BigInteger.valueOf(index));

        // 2. 创建属性表格 - 在主标题后面插入
        cursor = titlePara.getCTP().newCursor();
        // 标题移位到下一个位置
        cursor.toNextToken();
        XWPFTable propertyTable = doc.insertNewTbl(cursor);
        if (propertyTable == null) {
            propertyTable = doc.createTable();
        }
  1. 关于表格适配
    表格的话,注意点其实就一个,因为我的需求是表格和页宽一致,这里的话,需要设置setType和setW,注意这里setW一定是5000,1000的话会超过很多。
java 复制代码
 private void generatePropertyTable(XWPFDocument doc, XWPFTable table, RiskReportDTO.SecurityRiskReport vul) {

        // 设置表格样式
        CTTblPr tblPr = table.getCTTbl().addNewTblPr();
        CTTblWidth tblWidth = tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.PCT); // 改为百分比类型
        tblWidth.setW(BigInteger.valueOf(5000)); // 100% = 10000 (因为单位是1/1000)
        
        // 设置表格边框(外部边框2pt,内部边框1pt)
        CTTblBorders borders = tblPr.addNewTblBorders();
        setTableBorders(borders, STBorder.SINGLE, BigInteger.valueOf(2), BigInteger.valueOf(1), "000000");

        String[][] data = {
            {"隐患风险等级", vul.getRiskLevel()},
            {"隐患URL", vul.getRiskUrl()},
            {"隐患是否验证", vul.getVerified()},
            {"隐患危害", vul.getRiskImpact()},
            {"修补建议", vul.getFixSuggestion()}
        };

        for (int i = 0; i < data.length; i++) {
            XWPFTableRow row = (i == 0) ? table.getRow(0) : table.createRow();

            // 确保每行都有两个单元格
            if (row.getTableCells().size() < 2) {
                row.addNewTableCell();
            }

            // 设置表头样式
            XWPFTableCell cell0 = row.getCell(0);
            // 清除单元格内的现有段落
            while (cell0.getParagraphs().size() > 0) {
                cell0.removeParagraph(0);
            }
            XWPFParagraph para0 = cell0.addParagraph();
            para0.setAlignment(ParagraphAlignment.CENTER); // 水平居中
            XWPFRun cell0Run = para0.createRun();
            cell0Run.setText(data[i][0]);
            cell0Run.setFontSize(12);
//            cell0Run.setBold(true); // 表头加粗
            
            // 设置垂直居中
            cell0.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);

            // 设置数据单元格
            XWPFTableCell cell1 = row.getCell(1);
            // 清除单元格内的现有段落
            while (cell1.getParagraphs().size() > 0) {
                cell1.removeParagraph(0);
            }
            XWPFParagraph para1 = cell1.addParagraph();
            para1.setAlignment(ParagraphAlignment.LEFT); // 水平左对齐
            XWPFRun cell1Run = para1.createRun();
            cell1Run.setFontSize(12);
            // 如果是URL单元格,设置为红色
//            if ("隐患URL".equals(data[i][0])) {
//                cell1Run.setColor("FF0000");
//            }
            
            // 设置垂直居中
            cell1.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);

            // 确保数据不为null
            if (data[i][1] != null) {
                cell1Run.setText(data[i][1]);
            } else {
                cell1Run.setText(""); // 空值处理
            }
        }
        
        // 1. 设置表格为100%页面宽度
        setTableFullWidth(table);

        // 2. 设置列宽比例(左侧40%,右侧60%)- 所有行创建完成后调用,确保所有行应用相同的列宽
        setColumnWidths(table, 1500, 3500);
        
        // 设置行高
        setTableRowHeight(table, 500, 0);
    }
        private void setTableFullWidth(XWPFTable table) {
        CTTblPr tblPr = table.getCTTbl().addNewTblPr();
        CTTblWidth tblWidth = tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.PCT); // 百分比类型
        tblWidth.setW(BigInteger.valueOf(5000)); // 100% = 10000

        // 设置表格左对齐
        CTJc jc = tblPr.addNewJc();
        jc.setVal(STJc.LEFT);
    }
相关推荐
马剑威(威哥爱编程)2 小时前
【鸿蒙学习笔记】基于HarmonyOS实现申请Push Token的功能
笔记·学习·harmonyos
jjjxxxhhh1232 小时前
2025年底 -对工作做个跨年总结
学习
快点好好学习吧3 小时前
PHP程序员到底为什么要学习正则表达式?使用场景是什么?底层原理是什么?
学习·正则表达式·php
小胡想找到工作3 小时前
一起学习javascript-正则表达式
学习
阿赵3D3 小时前
JavaScript学习笔记——11、正则表达式
javascript·笔记·学习·正则表达式
代码or搬砖3 小时前
JVM学习笔记
jvm·笔记·学习
军军君013 小时前
Three.js基础功能学习二:场景图与材质
前端·javascript·学习·3d·材质·three·三维
南山星火3 小时前
人工智能“学习”范式大全(24种)
人工智能·学习
L***一3 小时前
数字经济背景下前端开发者的能力认证体系研究
学习