1、调整导入失败,导出excel功能
2、调整规范
This commit is contained in:
@@ -215,7 +215,8 @@ Entity 基类的选择**直接决定** Service 和 ServiceImpl 的继承方式
|
|||||||
|
|
||||||
- 所有 Excel 批量导入功能如果存在部分数据无法导入、校验失败或处理异常,后端必须将导入失败的数据导出为 Excel 文件返回给前端下载。
|
- 所有 Excel 批量导入功能如果存在部分数据无法导入、校验失败或处理异常,后端必须将导入失败的数据导出为 Excel 文件返回给前端下载。
|
||||||
- 导入失败 Excel 的字段顺序必须与原导入模板保持一致,并在最后一列追加“导入失败原因”;失败原因应包含明确行号和可读错误信息。
|
- 导入失败 Excel 的字段顺序必须与原导入模板保持一致,并在最后一列追加“导入失败原因”;失败原因应包含明确行号和可读错误信息。
|
||||||
- 导入失败 Excel 中的失败数据行必须使用红色文字展示,最后一列“导入失败原因”的错误信息也必须使用红色文字展示;表头可保持默认样式。
|
- 导入失败 Excel 中仅出错字段/列对应的单元格使用红色文字展示,最后一列“导入失败原因”的错误信息也必须使用红色文字展示;不得将整行失败数据全部标红,表头可保持默认样式。
|
||||||
|
- 失败原因应尽量包含原导入模板中的列名,便于通用导出工具准确定位并标红对应错误单元格;无法定位具体字段的业务错误,仅标红“导入失败原因”列。
|
||||||
- 导入模板本身不得包含失败原因列;如复用导入模型,应使用 `@ExcelIgnore` 忽略内部错误字段,或单独定义 `XxxImportFailureExcel` 模型。
|
- 导入模板本身不得包含失败原因列;如复用导入模型,应使用 `@ExcelIgnore` 忽略内部错误字段,或单独定义 `XxxImportFailureExcel` 模型。
|
||||||
- 导入逻辑应逐行处理:可成功导入的数据正常保存,失败行收集到失败明细;除非业务明确要求全量事务回滚,不得因部分失败回滚已成功行。
|
- 导入逻辑应逐行处理:可成功导入的数据正常保存,失败行收集到失败明细;除非业务明确要求全量事务回滚,不得因部分失败回滚已成功行。
|
||||||
- Controller 在失败明细非空时应直接通过导入失败明细通用导出工具写入 `HttpServletResponse`,文件名统一包含业务名称、`导入失败明细` 和时间戳;全部成功时返回标准 `R.success`。
|
- Controller 在失败明细非空时应直接通过导入失败明细通用导出工具写入 `HttpServletResponse`,文件名统一包含业务名称、`导入失败明细` 和时间戳;全部成功时返回标准 `R.success`。
|
||||||
|
|||||||
@@ -28,11 +28,17 @@ package org.springblade.common.excel;
|
|||||||
import cn.idev.excel.FastExcel;
|
import cn.idev.excel.FastExcel;
|
||||||
import cn.idev.excel.annotation.ExcelIgnore;
|
import cn.idev.excel.annotation.ExcelIgnore;
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
import cn.idev.excel.write.metadata.style.WriteCellStyle;
|
import cn.idev.excel.metadata.data.WriteCellData;
|
||||||
import cn.idev.excel.write.metadata.style.WriteFont;
|
import cn.idev.excel.write.handler.CellWriteHandler;
|
||||||
import cn.idev.excel.write.style.HorizontalCellStyleStrategy;
|
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 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.IndexedColors;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
@@ -40,7 +46,9 @@ import java.net.URLEncoder;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +84,7 @@ public class ImportFailureExcelUtil {
|
|||||||
List<List<Object>> rows = buildRows(data, excelFields);
|
List<List<Object>> rows = buildRows(data, excelFields);
|
||||||
try {
|
try {
|
||||||
FastExcel.write(response.getOutputStream())
|
FastExcel.write(response.getOutputStream())
|
||||||
.registerWriteHandler(failureRowStyle())
|
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows))
|
||||||
.head(head)
|
.head(head)
|
||||||
.sheet(sheetName)
|
.sheet(sheetName)
|
||||||
.doWrite(rows);
|
.doWrite(rows);
|
||||||
@@ -154,13 +162,85 @@ public class ImportFailureExcelUtil {
|
|||||||
throw new NoSuchFieldException(String.join(",", fieldNames));
|
throw new NoSuchFieldException(String.join(",", fieldNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HorizontalCellStyleStrategy failureRowStyle() {
|
private static String columnName(Field field) {
|
||||||
WriteFont contentFont = new WriteFont();
|
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||||
contentFont.setColor(IndexedColors.RED.getIndex());
|
String[] value = excelProperty.value();
|
||||||
|
return value.length == 0 ? field.getName() : value[0];
|
||||||
|
}
|
||||||
|
|
||||||
WriteCellStyle contentStyle = new WriteCellStyle();
|
private static String normalize(String value) {
|
||||||
contentStyle.setWriteFont(contentFont);
|
return value == null ? "" : value.replaceAll("[\\s*_::,,。;;()()\\[\\]【】<>《》-]", "").toLowerCase();
|
||||||
return new HorizontalCellStyleStrategy(null, contentStyle);
|
}
|
||||||
|
|
||||||
|
private static List<String> columnKeywords(Field field) {
|
||||||
|
String columnName = columnName(field);
|
||||||
|
List<String> 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<List<String>> columnKeywords;
|
||||||
|
private final List<List<Object>> rows;
|
||||||
|
private final int failureReasonColumnIndex;
|
||||||
|
private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
|
||||||
|
|
||||||
|
private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> 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<WriteCellData<?>> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user