关于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);
    }
相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛6 天前
计算机系统概论——校验码
学习
babe小鑫6 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms6 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下6 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。6 天前
2026.2.25监控学习
学习
im_AMBER6 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J6 天前
从“Hello World“ 开始 C++
c语言·c++·学习