一. 实战代码
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.策略使用(这里我是由于业务需求需要模板化标题使用)
- 继承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);
}
- 标题生成以及获取光标 光标这里其实可以获取最后光标,如果最后无数据
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);
}
}
}
- 设置大纲级别和插入位置,以及导航跳转
这里表格样式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();
}
- 关于表格适配
表格的话,注意点其实就一个,因为我的需求是表格和页宽一致,这里的话,需要设置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);
}