🐛 修复运单计划-1占位,并对账导入失败明细按原表全量列导出
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+160
-23
@@ -45,7 +45,9 @@ import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -174,7 +176,10 @@ public class ImportFailureExcelUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 以「原始单元格文本」方式重读导入文件,得到的行用于构造读取阶段的失败明细。
|
||||
* 以「原始单元格文本」方式重读导入文件,得到的行用于构造失败明细。
|
||||
* <p>
|
||||
* 优先使用 Excel 文件真实表头(含动态费用列等模型类未声明的列),保证失败明细
|
||||
* 与原导入模板列一致,用户修正后可直接用该文件重新导入。
|
||||
* <p>
|
||||
* 用途:当 {@code ExcelUtil.read(file, XxxExcel.class)} 因单元格无法转换而失败时,
|
||||
* 已解析的实体列表是不完整的(异常中断了整批读取)。此时用本方法重读一次:
|
||||
@@ -182,18 +187,36 @@ public class ImportFailureExcelUtil {
|
||||
* 供导出「原表全量 + 仅出错行带原因」的失败明细。
|
||||
*
|
||||
* @param file 上传的 Excel 文件
|
||||
* @param excelClass 导入模型类(按列序号反查表头名,用于把列序号映射成列名)
|
||||
* @param excelClass 导入模型类(表头缺失时按列序号反查表头名)
|
||||
* @return 按行组织的明细数据:每行是「表头名 → 单元格文本」的有序 Map;
|
||||
* 读不出内容时返回空列表(调用方按空明细处理)
|
||||
*/
|
||||
public static List<Map<String, String>> readRawRows(java.io.InputStream file, Class<?> excelClass) {
|
||||
List<Map<Integer, String>> rawRows = new ArrayList<>();
|
||||
Map<Integer, String> headerByIndex = new LinkedHashMap<>();
|
||||
try {
|
||||
cn.idev.excel.read.builder.ExcelReaderBuilder builder = cn.idev.excel.FastExcel
|
||||
.read(file, new cn.idev.excel.event.AnalysisEventListener<Map<Integer, String>>() {
|
||||
@Override
|
||||
public void invokeHeadMap(Map<Integer, String> headMap, cn.idev.excel.context.AnalysisContext context) {
|
||||
if (headMap == null) {
|
||||
return;
|
||||
}
|
||||
headMap.forEach((index, header) -> {
|
||||
if (index == null) {
|
||||
return;
|
||||
}
|
||||
String name = header == null ? "" : header.trim();
|
||||
// 历史失败明细再导入时忽略旧的失败原因列,导出时重新追加
|
||||
if (!name.isEmpty() && !FAILURE_REASON.equals(name)) {
|
||||
headerByIndex.put(index, name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(Map<Integer, String> data, cn.idev.excel.context.AnalysisContext context) {
|
||||
rawRows.add(new java.util.LinkedHashMap<>(data));
|
||||
rawRows.add(data == null ? new LinkedHashMap<>() : new LinkedHashMap<>(data));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -206,25 +229,80 @@ public class ImportFailureExcelUtil {
|
||||
// 重读本身失败时退回空明细,由调用方按「无法读取」提示
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Field> fields = excelClass == null ? List.of() : excelFields(excelClass);
|
||||
List<Map<String, String>> rows = new ArrayList<>();
|
||||
for (Map<Integer, String> rawRow : rawRows) {
|
||||
Map<String, String> row = new java.util.LinkedHashMap<>();
|
||||
for (Map.Entry<Integer, String> entry : rawRow.entrySet()) {
|
||||
Integer columnIndex = entry.getKey();
|
||||
String columnName = columnIndex != null && columnIndex < fields.size()
|
||||
? columnName(excelClass, columnIndex)
|
||||
: null;
|
||||
if (columnName == null) {
|
||||
columnName = "第" + (columnIndex == null ? "?" : columnIndex + 1) + "列";
|
||||
Map<String, String> row = new LinkedHashMap<>();
|
||||
Set<Integer> columnIndexes = new LinkedHashSet<>(headerByIndex.keySet());
|
||||
if (rawRow != null) {
|
||||
columnIndexes.addAll(rawRow.keySet());
|
||||
}
|
||||
List<Integer> sortedIndexes = new ArrayList<>(columnIndexes);
|
||||
Collections.sort(sortedIndexes);
|
||||
for (Integer columnIndex : sortedIndexes) {
|
||||
if (columnIndex == null) {
|
||||
continue;
|
||||
}
|
||||
row.put(columnName, entry.getValue());
|
||||
String columnName = headerByIndex.get(columnIndex);
|
||||
if (columnName == null || columnName.isBlank()) {
|
||||
columnName = columnName(excelClass, columnIndex);
|
||||
}
|
||||
if (columnName == null || columnName.isBlank()) {
|
||||
columnName = "第" + (columnIndex + 1) + "列";
|
||||
}
|
||||
if (FAILURE_REASON.equals(columnName)) {
|
||||
continue;
|
||||
}
|
||||
row.put(columnName, rawRow == null ? null : rawRow.get(columnIndex));
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按原上传文件真实表头导出失败明细(含动态列),并追加「导入失败原因」。
|
||||
* <p>
|
||||
* 适用于运输对账等「模板列 = 固定列 + 动态费用列」场景:按实体字段反射导出
|
||||
* 会丢掉动态列,用户无法用失败文件修正后重新导入。本方法重读原文件全量列,
|
||||
* 再按 failureList 行序挂上失败原因。
|
||||
*
|
||||
* @param response 响应
|
||||
* @param fileName 文件名
|
||||
* @param sheetName 工作表名
|
||||
* @param file 原导入文件
|
||||
* @param failureList 校验失败列表(与原表行序一致,成功行 errorMessage 可为空)
|
||||
* @param excelClass 导入模型类(原文件重读失败时回退到按实体导出)
|
||||
*/
|
||||
public static void exportOriginalWithFailures(HttpServletResponse response, String fileName, String sheetName,
|
||||
org.springframework.web.multipart.MultipartFile file,
|
||||
List<?> failureList, Class<?> excelClass) {
|
||||
List<Map<String, String>> rawRows = new ArrayList<>();
|
||||
if (file != null && !file.isEmpty()) {
|
||||
try (java.io.InputStream inputStream = file.getInputStream()) {
|
||||
rawRows = readRawRows(inputStream, excelClass);
|
||||
} catch (IOException ignored) {
|
||||
rawRows = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
if (rawRows.isEmpty()) {
|
||||
// 原文件读不出时回退实体导出,至少保证失败原因可下载
|
||||
if (failureList != null && !failureList.isEmpty()) {
|
||||
export(response, fileName, sheetName, failureList, excelClass);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Map<Integer, String> failureMessageByRow = new HashMap<>();
|
||||
if (failureList != null) {
|
||||
for (int index = 0; index < failureList.size(); index++) {
|
||||
Object message = errorMessage(failureList.get(index));
|
||||
if (message != null && !String.valueOf(message).isBlank()) {
|
||||
failureMessageByRow.put(index, String.valueOf(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出「读取阶段失败」明细:原表全量行 + 末尾「导入失败原因」列。
|
||||
* <p>
|
||||
@@ -259,24 +337,20 @@ public class ImportFailureExcelUtil {
|
||||
}
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
Map<Integer, Set<Integer>> redColumnsByRow = new HashMap<>();
|
||||
List<List<String>> headerKeywords = new ArrayList<>();
|
||||
for (int columnIndex = 0; columnIndex < headers.size() - 1; columnIndex++) {
|
||||
headerKeywords.add(headerKeywords(headers.get(columnIndex)));
|
||||
}
|
||||
for (int rowIndex = 0; rowIndex < rawRows.size(); rowIndex++) {
|
||||
Map<String, String> rawRow = rawRows.get(rowIndex);
|
||||
List<Object> row = new ArrayList<>();
|
||||
Set<Integer> redColumns = new LinkedHashSet<>();
|
||||
// 失败原因列恒标红
|
||||
redColumns.add(headers.size() - 1);
|
||||
String failureMessage = failureMessageByRow == null ? null : failureMessageByRow.get(rowIndex);
|
||||
for (int columnIndex = 0; columnIndex < headers.size() - 1; columnIndex++) {
|
||||
String header = headers.get(columnIndex);
|
||||
row.add(rawRow.get(header));
|
||||
// 失败原因里出现该列名时,标红该列单元格
|
||||
if (failureMessage != null && !failureMessage.isBlank() && failureMessage.contains("「" + header + "」")) {
|
||||
redColumns.add(columnIndex);
|
||||
}
|
||||
row.add(rawRow.get(headers.get(columnIndex)));
|
||||
}
|
||||
row.add(failureMessage == null ? "" : failureMessage);
|
||||
rows.add(row);
|
||||
redColumnsByRow.put(rowIndex, redColumns);
|
||||
redColumnsByRow.put(rowIndex, resolveRawRedColumns(failureMessage, headerKeywords, headers.size() - 1));
|
||||
}
|
||||
try {
|
||||
FastExcel.write(response.getOutputStream())
|
||||
@@ -612,6 +686,69 @@ public class ImportFailureExcelUtil {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按真实表头生成定位关键词(用于原表失败明细标红)
|
||||
*/
|
||||
private static List<String> headerKeywords(String header) {
|
||||
if (header == null || header.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> keywords = new ArrayList<>();
|
||||
keywords.add(header);
|
||||
keywords.addAll(Arrays.asList(header.replace("*", "").split("[//、()()\\s]+")));
|
||||
return keywords.stream()
|
||||
.map(ImportFailureExcelUtil::normalize)
|
||||
.filter(keyword -> keyword.length() >= 2)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按失败原因命中表头关键词,计算需要标红的列(含失败原因列)
|
||||
*/
|
||||
private static Set<Integer> resolveRawRedColumns(String failureMessage, List<List<String>> headerKeywords,
|
||||
int failureReasonColumnIndex) {
|
||||
Set<Integer> redColumns = new LinkedHashSet<>();
|
||||
redColumns.add(failureReasonColumnIndex);
|
||||
String normalizedReason = normalize(failureMessage);
|
||||
if (normalizedReason.isEmpty()) {
|
||||
return redColumns;
|
||||
}
|
||||
List<int[]> matches = new ArrayList<>();
|
||||
for (int column = 0; column < headerKeywords.size(); column++) {
|
||||
for (String keyword : headerKeywords.get(column)) {
|
||||
int fromIndex = 0;
|
||||
while (true) {
|
||||
int index = normalizedReason.indexOf(keyword, fromIndex);
|
||||
if (index < 0) {
|
||||
break;
|
||||
}
|
||||
matches.add(new int[]{index, index + keyword.length(), column});
|
||||
fromIndex = index + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int[] match : matches) {
|
||||
if (!isRawMatchSubsumed(match, matches)) {
|
||||
redColumns.add(match[2]);
|
||||
}
|
||||
}
|
||||
return redColumns;
|
||||
}
|
||||
|
||||
private static boolean isRawMatchSubsumed(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 static class ImportFailureCellStyleHandler implements CellWriteHandler {
|
||||
|
||||
private final int failureReasonColumnIndex;
|
||||
|
||||
Reference in New Issue
Block a user