🐛 修复导入失败明细无法按列标红的问题
4e1a162 移除了按列名匹配逻辑后只剩失败原因列标红。
现恢复并修正误标红:命中片段被更长片段覆盖时只保留更长的列
(避免原因含「车船类型」时把子串「类型」列一起标红)。
This commit is contained in:
+151
-8
@@ -35,6 +35,7 @@ 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.Cell;
|
||||||
import org.apache.poi.ss.usermodel.CellStyle;
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||||
import org.apache.poi.ss.usermodel.Font;
|
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 org.apache.poi.ss.usermodel.Workbook;
|
||||||
@@ -46,9 +47,11 @@ 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.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入失败明细 Excel 导出工具类
|
* 导入失败明细 Excel 导出工具类
|
||||||
@@ -95,7 +98,7 @@ public class ImportFailureExcelUtil {
|
|||||||
* @param excelClass 原导入 Excel 类型
|
* @param excelClass 原导入 Excel 类型
|
||||||
*/
|
*/
|
||||||
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) {
|
||||||
exportFailureReasonOnly(response, fileName, sheetName, data, excelClass);
|
export(response, fileName, sheetName, data, excelClass, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +112,16 @@ public class ImportFailureExcelUtil {
|
|||||||
*/
|
*/
|
||||||
public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
|
public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
|
||||||
List<?> data, Class<?> excelClass) {
|
List<?> data, Class<?> excelClass) {
|
||||||
|
export(response, fileName, sheetName, data, excelClass, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出导入失败明细
|
||||||
|
*
|
||||||
|
* @param markErrorColumns 是否按失败原因里出现的列名,标红对应字段单元格
|
||||||
|
*/
|
||||||
|
private static void export(HttpServletResponse response, String fileName, String sheetName,
|
||||||
|
List<?> data, Class<?> excelClass, boolean markErrorColumns) {
|
||||||
response.setContentType("application/vnd.ms-excel");
|
response.setContentType("application/vnd.ms-excel");
|
||||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
|
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
|
||||||
@@ -118,7 +131,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(new ImportFailureCellStyleHandler(excelFields.size()))
|
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows, markErrorColumns))
|
||||||
.head(head)
|
.head(head)
|
||||||
.sheet(sheetName)
|
.sheet(sheetName)
|
||||||
.doWrite(rows);
|
.doWrite(rows);
|
||||||
@@ -204,15 +217,60 @@ public class ImportFailureExcelUtil {
|
|||||||
throw new NoSuchFieldException(String.join(",", fieldNames));
|
throw new NoSuchFieldException(String.join(",", fieldNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本归一化:去掉空白、星号与中英文标点并统一小写
|
||||||
|
* <p>
|
||||||
|
* 目的是让「失败原因里写的列名」与「表头列名」能直接做包含匹配,
|
||||||
|
* 不受「*」「/」「:」等写法差异影响。
|
||||||
|
*/
|
||||||
|
private static String normalize(String value) {
|
||||||
|
return value == null ? "" : value.replaceAll("[\\s*_::,,。;;()()\\[\\]【】<>《》//、-]", "").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取某一列用于定位的候选关键词
|
||||||
|
* <p>
|
||||||
|
* 包含表头列名、字段名,以及列名按分隔符拆出的片段
|
||||||
|
* (如「车牌号/船号」拆出「车牌号」「船号」,失败原因只写其中一段时也能定位)。
|
||||||
|
*/
|
||||||
|
private static List<String> columnKeywords(Field field) {
|
||||||
|
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||||
|
String[] value = excelProperty.value();
|
||||||
|
String columnName = value.length == 0 ? field.getName() : value[0];
|
||||||
|
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 static class ImportFailureCellStyleHandler implements CellWriteHandler {
|
||||||
|
|
||||||
private final int failureReasonColumnIndex;
|
private final int failureReasonColumnIndex;
|
||||||
|
private final boolean markErrorColumns;
|
||||||
|
private final List<List<Object>> rows;
|
||||||
|
private final Map<Integer, Set<Integer>> redColumnsByRow = new HashMap<>();
|
||||||
private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
|
private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
|
||||||
|
private final Map<Short, CellStyle> redFillStyleCache = new HashMap<>();
|
||||||
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
|
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
|
||||||
private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
|
private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
|
||||||
|
|
||||||
private ImportFailureCellStyleHandler(int failureReasonColumnIndex) {
|
private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> rows, boolean markErrorColumns) {
|
||||||
this.failureReasonColumnIndex = failureReasonColumnIndex;
|
this.failureReasonColumnIndex = excelFields.size();
|
||||||
|
this.markErrorColumns = markErrorColumns;
|
||||||
|
this.rows = rows;
|
||||||
|
if (markErrorColumns) {
|
||||||
|
List<List<String>> columnKeywords = excelFields.stream()
|
||||||
|
.map(ImportFailureExcelUtil::columnKeywords)
|
||||||
|
.toList();
|
||||||
|
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
|
||||||
|
redColumnsByRow.put(rowIndex, resolveRedColumns(rows.get(rowIndex), columnKeywords));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -230,14 +288,95 @@ public class ImportFailureExcelUtil {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
adjustColumnWidth(cell);
|
adjustColumnWidth(cell);
|
||||||
if (cell.getColumnIndex() == failureReasonColumnIndex) {
|
if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) {
|
||||||
markRed(cell);
|
return;
|
||||||
|
}
|
||||||
|
if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) {
|
||||||
|
markRed(cell, isBlankValue(rows.get(relativeRowIndex), cell.getColumnIndex()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markRed(Cell cell) {
|
/**
|
||||||
|
* 计算某一行需要标红的列
|
||||||
|
* <p>
|
||||||
|
* 失败原因里出现的列名即视为出错列;但当某个命中片段被另一列更长的命中
|
||||||
|
* 片段完全覆盖时(如原因「车船类型不能为空」同时命中「车船类型」和它的
|
||||||
|
* 子串「类型」),只保留更长的那一列,避免把无关列一起标红。
|
||||||
|
*/
|
||||||
|
private Set<Integer> resolveRedColumns(List<Object> row, List<List<String>> columnKeywords) {
|
||||||
|
Set<Integer> redColumns = new LinkedHashSet<>();
|
||||||
|
redColumns.add(failureReasonColumnIndex);
|
||||||
|
String failureReason = normalize(String.valueOf(row.get(failureReasonColumnIndex)));
|
||||||
|
if (failureReason.isEmpty()) {
|
||||||
|
return redColumns;
|
||||||
|
}
|
||||||
|
List<int[]> matches = new ArrayList<>();
|
||||||
|
for (int column = 0; column < columnKeywords.size(); column++) {
|
||||||
|
for (String keyword : columnKeywords.get(column)) {
|
||||||
|
int fromIndex = 0;
|
||||||
|
while (true) {
|
||||||
|
int index = failureReason.indexOf(keyword, fromIndex);
|
||||||
|
if (index < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
matches.add(new int[]{index, index + keyword.length(), column});
|
||||||
|
fromIndex = index + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int[] match : matches) {
|
||||||
|
if (!isSubsumed(match, matches)) {
|
||||||
|
redColumns.add(match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return redColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断该命中片段是否被另一列更长的命中片段完全覆盖
|
||||||
|
*/
|
||||||
|
private boolean isSubsumed(int[] match, List<int[]> matches) {
|
||||||
|
int matchLength = match[1] - match[0];
|
||||||
|
for (int[] other : matches) {
|
||||||
|
if (other[2] == match[2]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((other[1] - other[0]) > matchLength && other[0] <= match[0] && other[1] >= match[1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldMarkRed(int rowIndex, int columnIndex) {
|
||||||
|
if (columnIndex == failureReasonColumnIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!markErrorColumns) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Set<Integer> redColumns = redColumnsByRow.get(rowIndex);
|
||||||
|
return redColumns != null && redColumns.contains(columnIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBlankValue(List<Object> row, int columnIndex) {
|
||||||
|
if (columnIndex < 0 || columnIndex >= row.size()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Object value = row.get(columnIndex);
|
||||||
|
return value == null || String.valueOf(value).isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标红单元格
|
||||||
|
* <p>
|
||||||
|
* 单元格为空时只改字体颜色屏幕上什么也看不到(「必填项为空」正是这种情况),
|
||||||
|
* 因此对空单元格额外加浅红底纹,保证用户能定位到是哪一列出错。
|
||||||
|
*/
|
||||||
|
private void markRed(Cell cell, boolean blankValue) {
|
||||||
CellStyle currentStyle = cell.getCellStyle();
|
CellStyle currentStyle = cell.getCellStyle();
|
||||||
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
|
Map<Short, CellStyle> styleCache = blankValue ? redFillStyleCache : redStyleCache;
|
||||||
|
CellStyle redStyle = styleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
|
||||||
Workbook workbook = cell.getSheet().getWorkbook();
|
Workbook workbook = cell.getSheet().getWorkbook();
|
||||||
CellStyle newStyle = workbook.createCellStyle();
|
CellStyle newStyle = workbook.createCellStyle();
|
||||||
newStyle.cloneStyleFrom(currentStyle);
|
newStyle.cloneStyleFrom(currentStyle);
|
||||||
@@ -245,6 +384,10 @@ public class ImportFailureExcelUtil {
|
|||||||
font.setColor(IndexedColors.RED.getIndex());
|
font.setColor(IndexedColors.RED.getIndex());
|
||||||
newStyle.setFont(font);
|
newStyle.setFont(font);
|
||||||
newStyle.setWrapText(true);
|
newStyle.setWrapText(true);
|
||||||
|
if (blankValue) {
|
||||||
|
newStyle.setFillForegroundColor(IndexedColors.ROSE.getIndex());
|
||||||
|
newStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
}
|
||||||
return newStyle;
|
return newStyle;
|
||||||
});
|
});
|
||||||
cell.setCellStyle(redStyle);
|
cell.setCellStyle(redStyle);
|
||||||
|
|||||||
Reference in New Issue
Block a user