From 74416dabafaee756c8b6393a3ff2a945714a47e7 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 27 Jul 2026 14:53:28 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E8=B0=83=E6=95=B4=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=EF=BC=8C=E5=AF=BC=E5=87=BAexcel=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=202=E3=80=81=E8=B0=83=E6=95=B4=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 3 +- .../common/excel/ImportFailureExcelUtil.java | 100 ++++++++++++++++-- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 757716b..2d36484 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,7 +215,8 @@ Entity 基类的选择**直接决定** Service 和 ServiceImpl 的继承方式 - 所有 Excel 批量导入功能如果存在部分数据无法导入、校验失败或处理异常,后端必须将导入失败的数据导出为 Excel 文件返回给前端下载。 - 导入失败 Excel 的字段顺序必须与原导入模板保持一致,并在最后一列追加“导入失败原因”;失败原因应包含明确行号和可读错误信息。 -- 导入失败 Excel 中的失败数据行必须使用红色文字展示,最后一列“导入失败原因”的错误信息也必须使用红色文字展示;表头可保持默认样式。 +- 导入失败 Excel 中仅出错字段/列对应的单元格使用红色文字展示,最后一列“导入失败原因”的错误信息也必须使用红色文字展示;不得将整行失败数据全部标红,表头可保持默认样式。 +- 失败原因应尽量包含原导入模板中的列名,便于通用导出工具准确定位并标红对应错误单元格;无法定位具体字段的业务错误,仅标红“导入失败原因”列。 - 导入模板本身不得包含失败原因列;如复用导入模型,应使用 `@ExcelIgnore` 忽略内部错误字段,或单独定义 `XxxImportFailureExcel` 模型。 - 导入逻辑应逐行处理:可成功导入的数据正常保存,失败行收集到失败明细;除非业务明确要求全量事务回滚,不得因部分失败回滚已成功行。 - Controller 在失败明细非空时应直接通过导入失败明细通用导出工具写入 `HttpServletResponse`,文件名统一包含业务名称、`导入失败明细` 和时间戳;全部成功时返回标准 `R.success`。 diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java index 83c3a3e..f95885d 100644 --- a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java @@ -28,11 +28,17 @@ package org.springblade.common.excel; import cn.idev.excel.FastExcel; import cn.idev.excel.annotation.ExcelIgnore; import cn.idev.excel.annotation.ExcelProperty; -import cn.idev.excel.write.metadata.style.WriteCellStyle; -import cn.idev.excel.write.metadata.style.WriteFont; -import cn.idev.excel.write.style.HorizontalCellStyleStrategy; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.write.handler.CellWriteHandler; +import cn.idev.excel.write.handler.context.CellWriteHandlerContext; +import cn.idev.excel.write.metadata.holder.WriteSheetHolder; +import cn.idev.excel.write.metadata.holder.WriteTableHolder; import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Workbook; import java.io.IOException; import java.lang.reflect.Field; @@ -40,7 +46,9 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -76,7 +84,7 @@ public class ImportFailureExcelUtil { List> rows = buildRows(data, excelFields); try { FastExcel.write(response.getOutputStream()) - .registerWriteHandler(failureRowStyle()) + .registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows)) .head(head) .sheet(sheetName) .doWrite(rows); @@ -154,13 +162,85 @@ public class ImportFailureExcelUtil { throw new NoSuchFieldException(String.join(",", fieldNames)); } - private static HorizontalCellStyleStrategy failureRowStyle() { - WriteFont contentFont = new WriteFont(); - contentFont.setColor(IndexedColors.RED.getIndex()); + private static String columnName(Field field) { + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + String[] value = excelProperty.value(); + return value.length == 0 ? field.getName() : value[0]; + } - WriteCellStyle contentStyle = new WriteCellStyle(); - contentStyle.setWriteFont(contentFont); - return new HorizontalCellStyleStrategy(null, contentStyle); + private static String normalize(String value) { + return value == null ? "" : value.replaceAll("[\\s*_::,,。;;()()\\[\\]【】<>《》-]", "").toLowerCase(); + } + + private static List columnKeywords(Field field) { + String columnName = columnName(field); + List keywords = new ArrayList<>(); + keywords.add(columnName); + keywords.add(field.getName()); + keywords.addAll(Arrays.asList(columnName.replace("*", "").split("[//、()()\\s]+"))); + return keywords.stream() + .map(ImportFailureExcelUtil::normalize) + .filter(keyword -> keyword.length() >= 2) + .distinct() + .toList(); + } + + private static class ImportFailureCellStyleHandler implements CellWriteHandler { + + private final List> columnKeywords; + private final List> rows; + private final int failureReasonColumnIndex; + private final Map redStyleCache = new HashMap<>(); + + private ImportFailureCellStyleHandler(List excelFields, List> rows) { + this.columnKeywords = excelFields.stream() + .map(ImportFailureExcelUtil::columnKeywords) + .toList(); + this.rows = rows; + this.failureReasonColumnIndex = excelFields.size(); + } + + @Override + public void afterCellDispose( + WriteSheetHolder writeSheetHolder, + WriteTableHolder writeTableHolder, + List> cellDataList, + Cell cell, + cn.idev.excel.metadata.Head head, + Integer relativeRowIndex, + Boolean isHead) { + if (Boolean.TRUE.equals(isHead) || relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) { + return; + } + if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) { + markRed(cell); + } + } + + private boolean shouldMarkRed(int rowIndex, int columnIndex) { + if (columnIndex == failureReasonColumnIndex) { + return true; + } + if (columnIndex < 0 || columnIndex >= columnKeywords.size()) { + return false; + } + String failureReason = normalize(String.valueOf(rows.get(rowIndex).get(failureReasonColumnIndex))); + return columnKeywords.get(columnIndex).stream().anyMatch(failureReason::contains); + } + + private void markRed(Cell cell) { + CellStyle currentStyle = cell.getCellStyle(); + CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { + Workbook workbook = cell.getSheet().getWorkbook(); + CellStyle newStyle = workbook.createCellStyle(); + newStyle.cloneStyleFrom(currentStyle); + Font font = workbook.createFont(); + font.setColor(IndexedColors.RED.getIndex()); + newStyle.setFont(font); + return newStyle; + }); + cell.setCellStyle(redStyle); + } } }