package cn.iocoder.yudao.module.qcreport.service.aiimport.document; import cn.iocoder.yudao.module.qcreport.config.QcReportAiImportProperties; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xwpf.usermodel.IBodyElement; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFHeaderFooter; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_EMPTY; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_LEGACY_OFFICE; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_OFFICE_CORRUPT; /** * Word / Excel / 纯文本抽取器。 *
* 用 POI 而不是 Tika,两个原因,缺一不可: * 检验报告的核心信息在表格里,行列结构一旦被 Tika 拍平成一段文字就再也找不回来; * 而 {@code tika-parsers-standard-package} 会拖进几百 MB 的解析器依赖, * 一个只处理四种格式的模块没必要背这个包袱。 * *
* 正文按 {@code getBodyElements()} 迭代而不是「先所有段落、再所有表格」:报告里表格夹在段落之间, * 分开收集会把版面顺序彻底打乱,模型看到的是一堆标题堆在开头、表格堆在结尾。 *
* 表格用 Tab 分隔单元格、换行分隔行,并在前后加标记——标记的作用是让模型知道 * 「这几行属于同一个表格」,没有它,一段用 Tab 拼起来的文本和普通段落无从区分。 * 表格内部的合并单元格由 {@link #appendTable} 归一化成对齐的网格,合并信息因此不会丢失。 *
* 页眉/页脚必须读:报告的抬头(公司名、报告名)和落款(厂址、电话)通常就住在这里,
* 而 {@code getBodyElements()} 只看正文,不读的话模型压根没见过这些文字,
* 也就不可能把它们认成 {@code ReportHeader} / {@code ReportFooter}。读到的内容用
* {@code [页眉]} / {@code [页脚]} 包住,顺序与文档视觉顺序一致(页眉在前、页脚在后)。
*/
private DocxRead readDocx(byte[] content, String originalFilename) {
try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(content))) {
StringBuilder sb = new StringBuilder();
String header = collectHeaderFooter(document.getHeaderList());
String footer = collectHeaderFooter(document.getFooterList());
if (!header.isBlank()) {
sb.append(HEADER_MARK).append('\n').append(header);
}
appendBodyElements(sb, document.getBodyElements());
if (!footer.isBlank()) {
sb.append(FOOTER_MARK).append('\n').append(footer);
}
List
* 两件事必须做,少一件都会把噪声当信号:
*
* 直接按 {@code row.getTableCells()} 拼 Tab 是不够的:合并单元格会让每行的列数各不相同,
* 模型收到的是一张行列错位、参差不齐的「表」。
*
* 所以这里把合并信息翻译成模型看得见的字符:
*
* {@code
* 用 {@link DataFormatter} 取显示值而不是原始值:原始值里 {@code 1} 会变成 {@code 1.0}、
* 日期会变成一串序列号,模型看到这些基本只能瞎猜。用户眼里看到的是什么,就该给模型什么。
*
* 行数按 {@code maxXlsxRows} 封顶,空白行整行丢弃——超大表里大量空白行会把提示词灌满噪声。
*/
private String readXlsx(byte[] content, String originalFilename) {
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
StringBuilder sb = new StringBuilder();
DataFormatter formatter = new DataFormatter();
for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) {
Sheet sheet = workbook.getSheetAt(sheetIndex);
sb.append("[工作表: ").append(sheet.getSheetName()).append("]\n");
int lastRow = sheet.getLastRowNum();
int written = 0;
for (int rowIndex = 0; rowIndex <= lastRow && written < properties.getMaxXlsxRows(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
continue;
}
List
*
*/
private static String collectHeaderFooter(List extends XWPFHeaderFooter> parts) {
Set
*
* 后果不只是排版难看:模型看不到两层结构,就不会去挑「分组检验项表」组件,
* 只能退而求其次挑平铺的检验表,把分组信息整个丢掉。
*
*
* 续格自身带文字时以文字为准:续格在 Word 里本不该有内容,真有就说明制表不规范,
* 此时丢文字比丢结构更可惜。整表网格宽度取所有行的最大值,而不是第一行的列数——
* 表头常常带横向合并,按表头宽度切会把下面几行截断。
*/
private static void appendTable(StringBuilder sb, XWPFTable table) {
List> gridRows = new ArrayList<>();
int width = 0;
for (XWPFTableRow row : table.getRows()) {
List