From 4bc68ec7f7db4c21f8ced466799bf4a030e36d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Wed, 2 Sep 2026 19:15:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8B=A6=E6=88=AA=E9=9D=9E=E6=A0=87?= =?UTF-8?q?=E5=87=86=20XLSX=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 POI 解析前识别旧版 XLS 与异常容器 - 返回可操作提示并补充回归测试 --- .../MineruXlsxDocumentParseService.java | 52 +++++++++++++++++++ .../MineruXlsxDocumentParseServiceTest.java | 35 +++++++++++++ 2 files changed, 87 insertions(+) diff --git a/easy-agents-document/easy-agents-document-xlsx/src/main/java/com/easyagents/document/xlsx/mineru/MineruXlsxDocumentParseService.java b/easy-agents-document/easy-agents-document-xlsx/src/main/java/com/easyagents/document/xlsx/mineru/MineruXlsxDocumentParseService.java index 5062f58..8245130 100644 --- a/easy-agents-document/easy-agents-document-xlsx/src/main/java/com/easyagents/document/xlsx/mineru/MineruXlsxDocumentParseService.java +++ b/easy-agents-document/easy-agents-document-xlsx/src/main/java/com/easyagents/document/xlsx/mineru/MineruXlsxDocumentParseService.java @@ -14,6 +14,7 @@ import com.easyagents.document.core.entity.ParseRequest; import com.easyagents.document.core.entity.ParseResponse; import com.easyagents.document.core.entity.ParseResult; import com.easyagents.document.core.entity.XlsxParseRequest; +import com.easyagents.document.core.exception.DocumentParseException; import com.easyagents.document.core.support.AbstractAsyncDocumentParseService; import com.easyagents.document.xlsx.XlsxDocumentProvider; import com.easyagents.document.xlsx.model.XlsxCellArtifact; @@ -52,6 +53,10 @@ import java.util.concurrent.Executors; public class MineruXlsxDocumentParseService extends AbstractAsyncDocumentParseService implements XlsxDocumentProvider { public static final String PROVIDER_NAME = "mineru"; + private static final byte[] OLE2_SIGNATURE = new byte[] { + (byte) 0xd0, (byte) 0xcf, 0x11, (byte) 0xe0, + (byte) 0xa1, (byte) 0xb1, 0x1a, (byte) 0xe1 + }; private final MineruProperties properties; private final MineruClient client; @@ -145,6 +150,9 @@ public class MineruXlsxDocumentParseService extends AbstractAsyncDocumentParseSe @Override protected ParseResponse doParse(XlsxParseRequest request, DocumentAsyncTaskUpdater updater) { + for (ParseFile file : request.getFiles()) { + validateXlsxContent(file); + } ParseResponse response = new ParseResponse(); List results = new ArrayList(); String backend = null; @@ -211,6 +219,50 @@ public class MineruXlsxDocumentParseService extends AbstractAsyncDocumentParseSe return aggregate; } + /** + * 在 POI 打开工作簿前校验 XLSX 容器签名,避免向用户暴露底层格式异常。 + * + * @param file 待解析文件 + */ + private void validateXlsxContent(ParseFile file) { + byte[] content = file == null ? null : file.getContent(); + if (hasZipSignature(content)) { + return; + } + String fileName = file == null || !StringUtil.hasText(file.getFileName()) + ? "当前文件" + : "文件“" + file.getFileName() + "”"; + String reason = startsWith(content, OLE2_SIGNATURE) + ? "可能是旧版 XLS 或已加密文件" + : "文件内容与 .xlsx 扩展名不一致或文件已损坏"; + throw new DocumentParseException( + fileName + "不是标准 XLSX," + reason + + "。请解除保护后用 Excel/WPS 另存为 XLSX(修改文件后缀无效)" + ); + } + + private boolean hasZipSignature(byte[] content) { + return content != null + && content.length >= 4 + && content[0] == 'P' + && content[1] == 'K' + && ((content[2] == 3 && content[3] == 4) + || (content[2] == 5 && content[3] == 6) + || (content[2] == 7 && content[3] == 8)); + } + + private boolean startsWith(byte[] content, byte[] signature) { + if (content == null || content.length < signature.length) { + return false; + } + for (int index = 0; index < signature.length; index++) { + if (content[index] != signature[index]) { + return false; + } + } + return true; + } + private SheetExtraction extractSheet(XSSFSheet sheet, int sheetIndex, DataFormatter formatter, diff --git a/easy-agents-document/easy-agents-document-xlsx/src/test/java/com/easyagents/document/xlsx/mineru/MineruXlsxDocumentParseServiceTest.java b/easy-agents-document/easy-agents-document-xlsx/src/test/java/com/easyagents/document/xlsx/mineru/MineruXlsxDocumentParseServiceTest.java index 6d3a9df..c9ebfdf 100644 --- a/easy-agents-document/easy-agents-document-xlsx/src/test/java/com/easyagents/document/xlsx/mineru/MineruXlsxDocumentParseServiceTest.java +++ b/easy-agents-document/easy-agents-document-xlsx/src/test/java/com/easyagents/document/xlsx/mineru/MineruXlsxDocumentParseServiceTest.java @@ -16,6 +16,7 @@ import com.easyagents.document.core.entity.ParseTaskStatus; import com.easyagents.document.core.entity.XlsxParseRequest; import com.easyagents.document.core.exception.DocumentParseException; import com.easyagents.document.xlsx.model.XlsxParseArtifact; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.ClientAnchor; import org.apache.poi.xssf.usermodel.XSSFDrawing; import org.apache.poi.xssf.usermodel.XSSFSheet; @@ -119,6 +120,31 @@ public class MineruXlsxDocumentParseServiceTest { Assert.assertEquals("image/jpeg", result.getImages().get(0).getMimeType()); } + @Test + public void shouldRejectLegacyXlsContentWithActionableMessage() throws Exception { + RecordingClient client = new RecordingClient(defaultProperties()); + MineruMapper mapper = new MineruMapper(defaultProperties()); + MineruXlsxDocumentParseService service = new MineruXlsxDocumentParseService( + defaultProperties(), + client, + mapper, + new DocumentAsyncTaskManager(new InMemoryDocumentAsyncTaskRepository(), directExecutor()) + ); + XlsxParseRequest request = new XlsxParseRequest(); + request.addFile(ParseFile.of("legacy.xlsx", buildLegacyWorkbookBytes())); + + DocumentParseException error = Assert.assertThrows( + DocumentParseException.class, + () -> service.parse(request) + ); + + Assert.assertEquals( + "文件“legacy.xlsx”不是标准 XLSX,可能是旧版 XLS 或已加密文件。" + + "请解除保护后用 Excel/WPS 另存为 XLSX(修改文件后缀无效)", + error.getMessage() + ); + } + @Test public void shouldAppendImageReferenceForImageOnlySheet() throws Exception { RecordingClient client = new RecordingClient(defaultProperties()); @@ -255,6 +281,15 @@ public class MineruXlsxDocumentParseServiceTest { return writeWorkbook(workbook); } + private byte[] buildLegacyWorkbookBytes() throws Exception { + try (HSSFWorkbook workbook = new HSSFWorkbook(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + workbook.createSheet("Sheet1").createRow(0).createCell(0).setCellValue("旧版表格"); + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + private void addPicture(XSSFWorkbook workbook, XSSFSheet sheet, int rowIndex,