fix(data-import): 修复股权冻结导入功能中的参数映射和模板兼容性问题

- 修复了多个信用相关模块中的appellee参数映射错误
- 为Excel导入功能添加了多模板兼容支持,包括案号、暗号等不同字段名
- 增强了Excel导入的容错能力,支持多种表头配置和异常处理
- 扩展了超链接提取功能,支持从多个可能的列名获取URL信息
- 添加了fallback机制以处理不同上游数据源的字段映射差异
- 改进了空行过滤逻辑,提高了数据导入准确性
This commit is contained in:
2026-01-30 14:16:33 +08:00
parent 5ac0eef8a6
commit 4da2a84421
14 changed files with 309 additions and 23 deletions

View File

@@ -197,13 +197,40 @@ public class CreditGqdjController extends BaseController {
try {
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "股权冻结", 0);
ExcelImportSupport.ImportResult<CreditGqdjImportParam> importResult = ExcelImportSupport.read(
file, CreditGqdjImportParam.class, this::isEmptyImportRow, sheetIndex);
// Prefer the "best" header configuration; many upstream files have extra title rows or multi-row headers.
ExcelImportSupport.ImportResult<CreditGqdjImportParam> importResult = ExcelImportSupport.readBest(
file,
CreditGqdjImportParam.class,
this::isEmptyImportRow,
// Score rows that look like real data (at least has a case number in either column).
p -> p != null
&& (!ImportHelper.isBlank(p.getCaseNumber())
|| !ImportHelper.isBlank(p.getCaseNumber2())
|| !ImportHelper.isBlank(p.getCaseNumber3())),
sheetIndex
);
List<CreditGqdjImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
int usedSheetIndex = importResult.getSheetIndex();
if (CollectionUtils.isEmpty(list)) {
// Fallback: try other sheets if the named/default sheet doesn't match.
importResult = ExcelImportSupport.readAnySheetBest(
file,
CreditGqdjImportParam.class,
this::isEmptyImportRow,
p -> p != null
&& (!ImportHelper.isBlank(p.getCaseNumber())
|| !ImportHelper.isBlank(p.getCaseNumber2())
|| !ImportHelper.isBlank(p.getCaseNumber3()))
);
list = importResult.getData();
usedTitleRows = importResult.getTitleRows();
usedHeadRows = importResult.getHeadRows();
usedSheetIndex = importResult.getSheetIndex();
}
if (CollectionUtils.isEmpty(list)) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
@@ -211,10 +238,18 @@ public class CreditGqdjController extends BaseController {
User loginUser = getLoginUser();
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
// easypoi 默认不会读取单元格超链接地址url 通常挂在“执行通知文书号”列的超链接中,需要额外读取回填。
// easypoi 默认不会读取单元格超链接地址url 通常挂在“案号/执行通知文书号”列的超链接中,需要额外读取回填。
String caseNumberHeader = "执行通知文书号";
Map<String, String> urlByCaseNumber = ExcelImportSupport.readHyperlinksByHeaderKey(
file, usedSheetIndex, usedTitleRows, usedHeadRows, caseNumberHeader);
// Some upstream sources use "案号" as the case number header.
Map<String, String> urlByCaseNumberFromAh = ExcelImportSupport.readHyperlinksByHeaderKey(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
urlByCaseNumberFromAh.forEach(urlByCaseNumber::putIfAbsent);
// Some upstream sources use "暗号" as the case number header.
Map<String, String> urlByCaseNumberFromAh2 = ExcelImportSupport.readHyperlinksByHeaderKey(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号");
urlByCaseNumberFromAh2.forEach(urlByCaseNumber::putIfAbsent);
// 有些源文件会单独提供“url/网址/链接”等列(可能是纯文本也可能是超链接)
Map<String, String> urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, caseNumberHeader, "url");
@@ -230,6 +265,40 @@ public class CreditGqdjController extends BaseController {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, caseNumberHeader, "链接");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
// Try again with "案号" as the key column name.
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号", "url");
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号", "URL");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号", "网址");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号", "链接");
}
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
// Try again with "暗号" as the key column name.
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号", "url");
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号", "URL");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号", "网址");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号", "链接");
}
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -399,8 +468,16 @@ public class CreditGqdjController extends BaseController {
return fail("未读取到数据,请确认文件中存在“历史股权冻结”选项卡且表头与示例格式一致", null);
}
ExcelImportSupport.ImportResult<CreditGqdjImportParam> importResult = ExcelImportSupport.read(
file, CreditGqdjImportParam.class, this::isEmptyImportRow, sheetIndex);
ExcelImportSupport.ImportResult<CreditGqdjImportParam> importResult = ExcelImportSupport.readBest(
file,
CreditGqdjImportParam.class,
this::isEmptyImportRow,
p -> p != null
&& (!ImportHelper.isBlank(p.getCaseNumber())
|| !ImportHelper.isBlank(p.getCaseNumber2())
|| !ImportHelper.isBlank(p.getCaseNumber3())),
sheetIndex
);
List<CreditGqdjImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
@@ -417,6 +494,12 @@ public class CreditGqdjController extends BaseController {
String caseNumberHeader = "执行通知文书号";
Map<String, String> urlByCaseNumber = ExcelImportSupport.readHyperlinksByHeaderKey(
file, usedSheetIndex, usedTitleRows, usedHeadRows, caseNumberHeader);
Map<String, String> urlByCaseNumberFromAh = ExcelImportSupport.readHyperlinksByHeaderKey(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
urlByCaseNumberFromAh.forEach(urlByCaseNumber::putIfAbsent);
Map<String, String> urlByCaseNumberFromAh2 = ExcelImportSupport.readHyperlinksByHeaderKey(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号");
urlByCaseNumberFromAh2.forEach(urlByCaseNumber::putIfAbsent);
Map<String, String> urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, caseNumberHeader, "url");
if (urlByCaseNumberFromUrlCol.isEmpty()) {
@@ -431,6 +514,38 @@ public class CreditGqdjController extends BaseController {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, caseNumberHeader, "链接");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号", "url");
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号", "URL");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号", "网址");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号", "链接");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号", "url");
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号", "URL");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号", "网址");
}
if (urlByCaseNumberFromUrlCol.isEmpty()) {
urlByCaseNumberFromUrlCol = ExcelImportSupport.readKeyValueByHeaders(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "暗号", "链接");
}
}
}
LinkedHashMap<String, CreditGqdj> latestByCaseNumber = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();
@@ -635,23 +750,52 @@ public class CreditGqdjController extends BaseController {
if (param == null) {
return true;
}
// Some upstream files use alternative headers that map to caseNumber2.
return ImportHelper.isBlank(param.getCaseNumber()) && ImportHelper.isBlank(param.getCaseNumber2());
// Don't over-filter here: if some columns are mapped but case number header differs,
// we still want to read the row and report "案号不能为空" instead of "未读取到数据".
return ImportHelper.isBlank(param.getCaseNumber())
&& ImportHelper.isBlank(param.getCaseNumber2())
&& ImportHelper.isBlank(param.getCaseNumber3())
&& ImportHelper.isBlank(param.getAppellee())
&& ImportHelper.isBlank(param.getAppellee2())
&& ImportHelper.isBlank(param.getPlaintiffAppellant())
&& ImportHelper.isBlank(param.getPlaintiffAppellant2())
&& ImportHelper.isBlank(param.getInvolvedAmount())
&& ImportHelper.isBlank(param.getCourtName())
&& ImportHelper.isBlank(param.getDataType())
&& ImportHelper.isBlank(param.getDataStatus())
&& ImportHelper.isBlank(param.getDataStatus2())
&& ImportHelper.isBlank(param.getFreezeDateStart())
&& ImportHelper.isBlank(param.getFreezeDateEnd())
&& ImportHelper.isBlank(param.getFreezeDateStart2())
&& ImportHelper.isBlank(param.getFreezeDateEnd2())
&& ImportHelper.isBlank(param.getPublicDate());
}
private CreditGqdj convertImportParamToEntity(CreditGqdjImportParam param) {
CreditGqdj entity = new CreditGqdj();
// Template compatibility: some sources use alternate headers for the same columns.
String appellee = !ImportHelper.isBlank(param.getAppellee()) ? param.getAppellee() : param.getAppellee2();
String plaintiffAppellant = !ImportHelper.isBlank(param.getPlaintiffAppellant())
? param.getPlaintiffAppellant()
: param.getPlaintiffAppellant2();
if (!ImportHelper.isBlank(param.getCaseNumber2())) {
entity.setCaseNumber(param.getCaseNumber2());
} else if (!ImportHelper.isBlank(param.getCaseNumber3())) {
entity.setCaseNumber(param.getCaseNumber3());
} else {
entity.setCaseNumber(param.getCaseNumber());
}
entity.setAppellee(param.getAppellee());
entity.setPlaintiffAppellant(param.getPlaintiffAppellant());
entity.setAppellee(appellee);
entity.setPlaintiffAppellant(plaintiffAppellant);
entity.setInvolvedAmount(param.getInvolvedAmount());
entity.setCourtName(param.getCourtName());
entity.setDataStatus(param.getDataStatus());
if (!ImportHelper.isBlank(param.getDataStatus2())) {
entity.setDataStatus(param.getDataStatus2());
} else {
entity.setDataStatus(param.getDataStatus());
}
entity.setDataType("股权冻结");
entity.setPublicDate(param.getPublicDate());
if (!ImportHelper.isBlank(param.getFreezeDateStart2())) {

View File

@@ -391,12 +391,24 @@ public class CreditMediationController extends BaseController {
private CreditMediation convertImportParamToEntity(CreditMediationImportParam param) {
CreditMediation entity = new CreditMediation();
entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty());
entity.setOccurrenceTime(param.getOccurrenceTime());
// Template compatibility: prefer new columns ("发生时间"/"其他当事人/第三人"), fallback to legacy ones ("立案日期"/"当事人").
String occurrenceTime = !ImportHelper.isBlank(param.getOccurrenceTime2())
? param.getOccurrenceTime2()
: param.getOccurrenceTime();
String otherPartiesThirdParty = !ImportHelper.isBlank(param.getOtherPartiesThirdParty2())
? param.getOtherPartiesThirdParty2()
: param.getOtherPartiesThirdParty();
entity.setOccurrenceTime(occurrenceTime);
entity.setOtherPartiesThirdParty(otherPartiesThirdParty);
entity.setCaseNumber(param.getCaseNumber());
entity.setCauseOfAction(param.getCauseOfAction());
entity.setCourtName(param.getCourtName());
entity.setComments(param.getComments());
entity.setPlaintiffAppellant(param.getPlaintiffAppellant());
entity.setAppellee(param.getAppellee());
entity.setInvolvedAmount(param.getInvolvedAmount());
entity.setDataStatus(param.getDataStatus());
return entity;
}

View File

@@ -114,15 +114,55 @@ public class ExcelImportSupport {
int usedTitleRows = 0;
int usedHeadRows = 0;
int[][] tryConfigs = new int[][]{{1, 1}, {0, 1}, {2, 1}, {3, 1}, {0, 2}, {0, 3}, {0, 4}};
Exception lastFailure = null;
boolean anyImported = false;
for (int[] config : tryConfigs) {
list = filterEmptyRows(importSheet(file, clazz, config[0], config[1], sheetIndex), emptyRowPredicate);
try {
list = filterEmptyRows(importSheet(file, clazz, config[0], config[1], sheetIndex), emptyRowPredicate);
anyImported = true;
} catch (Exception e) {
// Some source files can trigger easypoi/POI runtime exceptions for certain title/head row combinations.
// Keep trying other configurations instead of failing the whole import.
lastFailure = e;
continue;
}
if (!CollectionUtils.isEmpty(list)) {
usedTitleRows = config[0];
usedHeadRows = config[1];
break;
}
}
// Fallback for upstream files with extra banner/title rows or wider multi-row headers.
// Keep the default fast path above for common templates.
if (CollectionUtils.isEmpty(list)) {
final int maxTitleRows = 10;
final int maxHeadRows = 6;
outer:
for (int titleRows = 0; titleRows <= maxTitleRows; titleRows++) {
for (int headRows = 1; headRows <= maxHeadRows; headRows++) {
try {
list = filterEmptyRows(importSheet(file, clazz, titleRows, headRows, sheetIndex), emptyRowPredicate);
anyImported = true;
} catch (Exception e) {
lastFailure = e;
continue;
}
if (!CollectionUtils.isEmpty(list)) {
usedTitleRows = titleRows;
usedHeadRows = headRows;
break outer;
}
}
}
}
if (list == null) {
if (!anyImported && lastFailure != null) {
throw lastFailure;
}
list = Collections.emptyList();
}
return new ImportResult<>(list, usedTitleRows, usedHeadRows, sheetIndex);
}
@@ -135,11 +175,20 @@ public class ExcelImportSupport {
int bestHeadRows = 0;
int bestScore = -1;
int bestSize = -1;
Exception lastFailure = null;
boolean anyImported = false;
int[][] tryConfigs = new int[][]{{1, 1}, {0, 1}, {2, 1}, {3, 1}, {0, 2}, {0, 3}, {0, 4}, {1, 2}, {1, 3}, {1, 4}, {2, 2}, {2, 3}, {2, 4}, {3, 2}, {3, 3}, {3, 4}};
for (int[] config : tryConfigs) {
List<T> list = filterEmptyRows(importSheet(file, clazz, config[0], config[1], sheetIndex), emptyRowPredicate);
List<T> list;
try {
list = filterEmptyRows(importSheet(file, clazz, config[0], config[1], sheetIndex), emptyRowPredicate);
anyImported = true;
} catch (Exception e) {
lastFailure = e;
continue;
}
if (CollectionUtils.isEmpty(list)) {
continue;
}
@@ -163,10 +212,52 @@ public class ExcelImportSupport {
}
}
// Fallback scan: broaden the search space for messy source files (extra title rows, multi-row headers).
if (bestList == null) {
final int maxTitleRows = 10;
final int maxHeadRows = 6;
for (int titleRows = 0; titleRows <= maxTitleRows; titleRows++) {
for (int headRows = 1; headRows <= maxHeadRows; headRows++) {
List<T> list;
try {
list = filterEmptyRows(importSheet(file, clazz, titleRows, headRows, sheetIndex), emptyRowPredicate);
anyImported = true;
} catch (Exception e) {
lastFailure = e;
continue;
}
if (CollectionUtils.isEmpty(list)) {
continue;
}
int score = 0;
if (scoreRowPredicate != null) {
for (T row : list) {
if (scoreRowPredicate.test(row)) {
score++;
}
}
}
int size = list.size();
if (score > bestScore || (score == bestScore && size > bestSize)) {
bestList = list;
bestTitleRows = titleRows;
bestHeadRows = headRows;
bestScore = score;
bestSize = size;
}
}
}
}
if (bestList != null) {
return new ImportResult<>(bestList, bestTitleRows, bestHeadRows, sheetIndex);
}
if (!anyImported && lastFailure != null) {
throw lastFailure;
}
return read(file, clazz, emptyRowPredicate, sheetIndex);
}