From 4b6908d3bf543a874b05ac98b19c827bb5d758c6 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 27 Jul 2026 14:36:45 +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 | 52 +++++++++++++++---- .../controller/CargoTypeController.java | 3 +- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 118bba3..757716b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,9 +215,10 @@ Entity 基类的选择**直接决定** Service 和 ServiceImpl 的继承方式 - 所有 Excel 批量导入功能如果存在部分数据无法导入、校验失败或处理异常,后端必须将导入失败的数据导出为 Excel 文件返回给前端下载。 - 导入失败 Excel 的字段顺序必须与原导入模板保持一致,并在最后一列追加“导入失败原因”;失败原因应包含明确行号和可读错误信息。 +- 导入失败 Excel 中的失败数据行必须使用红色文字展示,最后一列“导入失败原因”的错误信息也必须使用红色文字展示;表头可保持默认样式。 - 导入模板本身不得包含失败原因列;如复用导入模型,应使用 `@ExcelIgnore` 忽略内部错误字段,或单独定义 `XxxImportFailureExcel` 模型。 - 导入逻辑应逐行处理:可成功导入的数据正常保存,失败行收集到失败明细;除非业务明确要求全量事务回滚,不得因部分失败回滚已成功行。 -- Controller 在失败明细非空时应直接通过 `ExcelUtil.export` 写入 `HttpServletResponse`,文件名统一包含业务名称、`导入失败明细` 和时间戳;全部成功时返回标准 `R.success`。 +- Controller 在失败明细非空时应直接通过导入失败明细通用导出工具写入 `HttpServletResponse`,文件名统一包含业务名称、`导入失败明细` 和时间戳;全部成功时返回标准 `R.success`。 - Service 层应返回失败明细列表或等效结构,禁止只抛出拼接后的错误字符串导致前端无法下载失败数据。 ### 6.12 日志 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 db2c0e4..83c3a3e 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,7 +28,11 @@ 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 jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.ss.usermodel.IndexedColors; import java.io.IOException; import java.lang.reflect.Field; @@ -48,6 +52,7 @@ public class ImportFailureExcelUtil { private static final String FAILURE_REASON = "导入失败原因"; private static final String ERROR_MESSAGE_FIELD = "errorMessage"; + private static final String FAILURE_REASON_FIELD = "failureReason"; private ImportFailureExcelUtil() { } @@ -60,9 +65,8 @@ public class ImportFailureExcelUtil { * @param sheetName 工作表名 * @param data 失败数据 * @param excelClass 原导入 Excel 类型 - * @param 泛型 */ - public static void export(HttpServletResponse response, String fileName, String sheetName, List data, Class excelClass) { + public static void export(HttpServletResponse response, String fileName, String sheetName, List data, Class excelClass) { response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8); @@ -71,7 +75,11 @@ public class ImportFailureExcelUtil { List> head = buildHead(excelFields); List> rows = buildRows(data, excelFields); try { - FastExcel.write(response.getOutputStream()).head(head).sheet(sheetName).doWrite(rows); + FastExcel.write(response.getOutputStream()) + .registerWriteHandler(failureRowStyle()) + .head(head) + .sheet(sheetName) + .doWrite(rows); } catch (IOException exception) { throw new IllegalStateException("导出导入失败明细失败", exception); } @@ -82,6 +90,7 @@ public class ImportFailureExcelUtil { .filter(field -> field.getAnnotation(ExcelProperty.class) != null) .filter(field -> field.getAnnotation(ExcelIgnore.class) == null) .filter(field -> !Objects.equals(field.getName(), ERROR_MESSAGE_FIELD)) + .filter(field -> !Objects.equals(field.getName(), FAILURE_REASON_FIELD)) .toList(); } @@ -96,9 +105,9 @@ public class ImportFailureExcelUtil { return head; } - private static List> buildRows(List data, List excelFields) { + private static List> buildRows(List data, List excelFields) { List> rows = new ArrayList<>(); - for (T item : data) { + for (Object item : data) { List row = new ArrayList<>(); for (Field field : excelFields) { row.add(fieldValue(item, field)); @@ -111,16 +120,17 @@ public class ImportFailureExcelUtil { private static Object fieldValue(Object item, Field field) { try { - field.setAccessible(true); - return field.get(item); - } catch (IllegalAccessException exception) { + Field targetField = targetField(item.getClass(), field.getName()); + targetField.setAccessible(true); + return targetField.get(item); + } catch (NoSuchFieldException | IllegalAccessException exception) { return null; } } private static Object errorMessage(Object item) { try { - Field field = item.getClass().getDeclaredField(ERROR_MESSAGE_FIELD); + Field field = targetField(item.getClass(), ERROR_MESSAGE_FIELD, FAILURE_REASON_FIELD); field.setAccessible(true); Object value = field.get(item); return value == null ? "" : String.valueOf(value); @@ -129,4 +139,28 @@ public class ImportFailureExcelUtil { } } + private static Field targetField(Class itemClass, String... fieldNames) throws NoSuchFieldException { + Class currentClass = itemClass; + while (currentClass != null) { + for (String fieldName : fieldNames) { + try { + return currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException ignored) { + // 继续查找其他字段名或父类字段。 + } + } + currentClass = currentClass.getSuperclass(); + } + throw new NoSuchFieldException(String.join(",", fieldNames)); + } + + private static HorizontalCellStyleStrategy failureRowStyle() { + WriteFont contentFont = new WriteFont(); + contentFont.setColor(IndexedColors.RED.getIndex()); + + WriteCellStyle contentStyle = new WriteCellStyle(); + contentStyle.setWriteFont(contentFont); + return new HorizontalCellStyleStrategy(null, contentStyle); + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java index 2266757..745968b 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java @@ -45,6 +45,7 @@ import org.springblade.core.tenant.annotation.NonDS; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.common.excel.ImportFailureExcelUtil; import org.springblade.system.excel.CargoTypeExcel; import org.springblade.system.excel.CargoTypeExportExcel; import org.springblade.system.excel.CargoTypeImportFailureExcel; @@ -158,7 +159,7 @@ public class CargoTypeController extends BladeController { List data = ExcelUtil.read(file, CargoTypeExcel.class); List failureList = cargoTypeService.importCargoType(data); if (Func.isNotEmpty(failureList)) { - ExcelUtil.export(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeImportFailureExcel.class); + ImportFailureExcelUtil.export(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeExcel.class); return null; } return R.success("导入数据成功");