feat(batch-import): 扩展批量导入支持多列企业名称匹配

- 新增 PARTY_SPLIT_PATTERN 正则表达式用于分割当事人名称
- 实现 refreshCompanyIdByCompanyNames 方法支持多列名称匹配
- 添加 splitPartyNames 工具方法处理当事人名称分割
- 优化公司ID刷新逻辑支持原告/被告等多个当事人字段
- 更新信用公示登记控制器使用多列名称
This commit is contained in:
2026-01-31 01:13:16 +08:00
parent ae2eac39a0
commit 7c0df4fd08
5 changed files with 184 additions and 24 deletions

View File

@@ -22,6 +22,7 @@ import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Pattern;
/** /**
* credit 模块 Excel 导入批处理支持: * credit 模块 Excel 导入批处理支持:
@@ -32,6 +33,7 @@ import java.util.function.Supplier;
public class BatchImportSupport { public class BatchImportSupport {
private final TransactionTemplate requiresNewTx; private final TransactionTemplate requiresNewTx;
private static final Pattern PARTY_SPLIT_PATTERN = Pattern.compile("[,;;、\\n\\r\\t/|]+");
public BatchImportSupport(PlatformTransactionManager transactionManager) { public BatchImportSupport(PlatformTransactionManager transactionManager) {
TransactionTemplate template = new TransactionTemplate(transactionManager); TransactionTemplate template = new TransactionTemplate(transactionManager);
@@ -71,7 +73,7 @@ public class BatchImportSupport {
/** /**
* 按企业名称匹配 CreditCompany(name / matchName) 并回填 companyId。 * 按企业名称匹配 CreditCompany(name / matchName) 并回填 companyId。
* *
* <p>默认仅更新 companyId=0 的记录onlyNull=trueonlyNull=false 时会覆盖更新(仅当 companyId 不同)。</p> * <p>默认仅更新 companyId 为空/0 的记录onlyNull=trueonlyNull=false 时会覆盖更新(仅当 companyId 不同)。</p>
* *
* <p>注意:为避免跨租户误更新,当 currentTenantId 为空时会按记录自身 tenantId 维度匹配, * <p>注意:为避免跨租户误更新,当 currentTenantId 为空时会按记录自身 tenantId 维度匹配,
* tenantId 为空的记录将被跳过并计入 notFound。</p> * tenantId 为空的记录将被跳过并计入 notFound。</p>
@@ -90,15 +92,80 @@ public class BatchImportSupport {
BiConsumer<T, Boolean> hasDataSetter, BiConsumer<T, Boolean> hasDataSetter,
SFunction<T, Integer> tenantIdGetter, SFunction<T, Integer> tenantIdGetter,
Supplier<T> patchFactory) { Supplier<T> patchFactory) {
// Keep existing API; delegate to the multi-column implementation.
return refreshCompanyIdByCompanyNames(service,
creditCompanyService,
currentTenantId,
onlyNull,
limit,
idGetter,
idSetter,
companyIdGetter,
companyIdSetter,
hasDataGetter,
hasDataSetter,
tenantIdGetter,
patchFactory,
nameGetter);
}
/**
* 按多列“当事人/企业名称”匹配 CreditCompany(name / matchName) 并回填 companyId。
*
* <p>按传入列顺序优先匹配:原告/上诉人 &gt; 被告/被上诉人 &gt; 其他当事人/第三人等。</p>
*
* <p>同一列若匹配到多个不同企业则视为歧义;若最终无法得到唯一 companyId则跳过并计入 ambiguous/notFound。</p>
*/
@SafeVarargs
public final <T> CompanyIdRefreshStats refreshCompanyIdByCompanyNames(IService<T> service,
CreditCompanyService creditCompanyService,
Integer currentTenantId,
Boolean onlyNull,
Integer limit,
SFunction<T, Integer> idGetter,
BiConsumer<T, Integer> idSetter,
SFunction<T, Integer> companyIdGetter,
BiConsumer<T, Integer> companyIdSetter,
SFunction<T, Boolean> hasDataGetter,
BiConsumer<T, Boolean> hasDataSetter,
SFunction<T, Integer> tenantIdGetter,
Supplier<T> patchFactory,
SFunction<T, String>... nameGetters) {
boolean onlyNullFlag = (onlyNull == null) || Boolean.TRUE.equals(onlyNull); boolean onlyNullFlag = (onlyNull == null) || Boolean.TRUE.equals(onlyNull);
if (nameGetters == null || nameGetters.length == 0) {
return new CompanyIdRefreshStats(false, 0, 0, 0, 0);
}
// 1) 读取待处理数据(仅取必要字段,避免一次性拉全表字段) // 1) 读取待处理数据(仅取必要字段,避免一次性拉全表字段)
@SuppressWarnings({"rawtypes", "unchecked"})
SFunction<T, ?>[] selectColumns = (SFunction<T, ?>[]) new SFunction[4 + nameGetters.length];
int colIdx = 0;
selectColumns[colIdx++] = idGetter;
selectColumns[colIdx++] = companyIdGetter;
selectColumns[colIdx++] = hasDataGetter;
selectColumns[colIdx++] = tenantIdGetter;
for (SFunction<T, String> ng : nameGetters) {
selectColumns[colIdx++] = ng;
}
var query = service.lambdaQuery() var query = service.lambdaQuery()
.select(idGetter, nameGetter, companyIdGetter, hasDataGetter, tenantIdGetter) .select(selectColumns)
.eq(currentTenantId != null, tenantIdGetter, currentTenantId) .eq(currentTenantId != null, tenantIdGetter, currentTenantId)
.isNotNull(nameGetter); .and(w -> {
// Only process rows that have at least one name column populated.
for (int i = 0; i < nameGetters.length; i++) {
if (i == 0) {
w.isNotNull(nameGetters[i]);
} else {
w.or().isNotNull(nameGetters[i]);
}
}
});
if (onlyNullFlag) { if (onlyNullFlag) {
query.eq(companyIdGetter, 0); // Historically some tables used 0 as the "unset" companyId, while others left it NULL.
// Treat both as "unset" so refresh won't silently do nothing.
query.and(w -> w.isNull(companyIdGetter).or().eq(companyIdGetter, 0));
} }
if (limit != null && limit > 0) { if (limit != null && limit > 0) {
query.last("limit " + Math.min(limit, 200000)); query.last("limit " + Math.min(limit, 200000));
@@ -146,9 +213,15 @@ public class BatchImportSupport {
LinkedHashMap<String, Integer> ambiguousByName = new LinkedHashMap<>(); LinkedHashMap<String, Integer> ambiguousByName = new LinkedHashMap<>();
LinkedHashSet<String> nameSet = new LinkedHashSet<>(); LinkedHashSet<String> nameSet = new LinkedHashSet<>();
for (T row : tenantRows) { for (T row : tenantRows) {
String name = normalizeCompanyName(row != null ? nameGetter.apply(row) : null); if (row == null) {
if (name != null) { continue;
nameSet.add(name); }
for (SFunction<T, String> ng : nameGetters) {
for (String name : splitPartyNames(ng.apply(row))) {
if (name != null) {
nameSet.add(name);
}
}
} }
} }
List<String> allNames = new ArrayList<>(nameSet); List<String> allNames = new ArrayList<>(nameSet);
@@ -174,20 +247,44 @@ public class BatchImportSupport {
// 3.2) 更新当前租户下的数据 companyId // 3.2) 更新当前租户下的数据 companyId
for (T row : tenantRows) { for (T row : tenantRows) {
String key = normalizeCompanyName(row != null ? nameGetter.apply(row) : null); if (row == null) {
if (key == null) {
continue; continue;
} }
Integer amb = ambiguousByName.get(key); Integer companyId = null;
if (amb != null && amb > 0) { boolean hasAmbiguousName = false;
ambiguous++; for (SFunction<T, String> ng : nameGetters) {
continue; LinkedHashSet<Integer> idsForColumn = new LinkedHashSet<>();
for (String key : splitPartyNames(ng.apply(row))) {
if (key == null) {
continue;
}
Integer amb = ambiguousByName.get(key);
if (amb != null && amb > 0) {
hasAmbiguousName = true;
continue;
}
Integer cid = companyIdByName.get(key);
if (cid != null) {
idsForColumn.add(cid);
}
}
if (idsForColumn.size() == 1) {
companyId = idsForColumn.iterator().next();
break;
}
if (idsForColumn.size() > 1) {
// Multiple companies matched within one column (e.g. multiple plaintiffs) -> ambiguous.
hasAmbiguousName = true;
}
} }
Integer companyId = companyIdByName.get(key);
if (companyId == null) { if (companyId == null) {
notFound++; if (hasAmbiguousName) {
ambiguous++;
} else {
notFound++;
}
continue; continue;
} }
matched++; matched++;
@@ -196,7 +293,7 @@ public class BatchImportSupport {
Boolean oldHasData = row != null ? hasDataGetter.apply(row) : null; Boolean oldHasData = row != null ? hasDataGetter.apply(row) : null;
boolean needUpdate; boolean needUpdate;
if (onlyNullFlag) { if (onlyNullFlag) {
needUpdate = oldCompanyId != null && oldCompanyId == 0; needUpdate = (oldCompanyId == null) || oldCompanyId == 0;
} else { } else {
needUpdate = oldCompanyId == null || !companyId.equals(oldCompanyId); needUpdate = oldCompanyId == null || !companyId.equals(oldCompanyId);
} }
@@ -710,6 +807,30 @@ public class BatchImportSupport {
return v.isEmpty() ? null : v; return v.isEmpty() ? null : v;
} }
/**
* Split a "party names" cell into normalized company name candidates.
* Supports common separators used in Excel/web copy (comma/semicolon/Chinese list delimiter/newlines).
*/
private static List<String> splitPartyNames(String raw) {
List<String> result = new ArrayList<>();
String v = normalizeCompanyName(raw);
if (v == null) {
return result;
}
String[] parts = PARTY_SPLIT_PATTERN.split(v);
if (parts == null || parts.length == 0) {
result.add(v);
return result;
}
for (String p : parts) {
String item = normalizeCompanyName(p);
if (item != null) {
result.add(item);
}
}
return result;
}
private static void addCompanyNameMapping(Map<String, Integer> idByName, private static void addCompanyNameMapping(Map<String, Integer> idByName,
Map<String, Integer> ambiguousByName, Map<String, Integer> ambiguousByName,
String key, String key,

View File

@@ -145,9 +145,9 @@ public class CreditGqdjController extends BaseController {
} }
/** /**
* 根据企业名称匹配企业并更新 companyId匹配 CreditCompany.name / CreditCompany.matchName * 根据当事人/企业名称匹配企业并更新 companyId匹配 CreditCompany.name / CreditCompany.matchName
* *
* <p>默认仅更新 companyId=0 的记录;如需覆盖更新,传 onlyNull=false。</p> * <p>默认仅更新 companyId 为空/0 的记录;如需覆盖更新,传 onlyNull=false。</p>
*/ */
@PreAuthorize("hasAuthority('credit:creditGqdj:update')") @PreAuthorize("hasAuthority('credit:creditGqdj:update')")
@OperationLog @OperationLog
@@ -160,7 +160,8 @@ public class CreditGqdjController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Match companyId by any party/company-name column (e.g. plaintiff/appellant, defendant/appellee).
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNames(
creditGqdjService, creditGqdjService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -168,13 +169,14 @@ public class CreditGqdjController extends BaseController {
limit, limit,
CreditGqdj::getId, CreditGqdj::getId,
CreditGqdj::setId, CreditGqdj::setId,
CreditGqdj::getAppellee,
CreditGqdj::getCompanyId, CreditGqdj::getCompanyId,
CreditGqdj::setCompanyId, CreditGqdj::setCompanyId,
CreditGqdj::getHasData, CreditGqdj::getHasData,
CreditGqdj::setHasData, CreditGqdj::setHasData,
CreditGqdj::getTenantId, CreditGqdj::getTenantId,
CreditGqdj::new CreditGqdj::new,
CreditGqdj::getPlaintiffAppellant,
CreditGqdj::getAppellee
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {

View File

@@ -603,11 +603,24 @@ public class CreditXgxfController extends BaseController {
entity.setCaseNumber(param.getCaseNumber()); entity.setCaseNumber(param.getCaseNumber());
entity.setType(param.getType()); entity.setType(param.getType());
entity.setDataType(param.getDataType()); entity.setDataType(param.getDataType());
entity.setPlaintiffUser(param.getPlaintiffUser());
entity.setDefendantUser(param.getDefendantUser());
entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty());
entity.setPlaintiffAppellant(param.getPlaintiffAppellant()); entity.setPlaintiffAppellant(param.getPlaintiffAppellant());
entity.setDataStatus(param.getDataStatus());
entity.setAppellee(param.getAppellee()); entity.setAppellee(param.getAppellee());
entity.setInvolvedAmount(param.getInvolvedAmount());
entity.setOccurrenceTime(param.getOccurrenceTime()); // 兼容不同模板字段:如果 *2 有值则以 *2 为准写入主字段
entity.setCourtName(param.getCourtName()); entity.setInvolvedAmount(!ImportHelper.isBlank(param.getInvolvedAmount2())
? param.getInvolvedAmount2()
: param.getInvolvedAmount());
entity.setOccurrenceTime(!ImportHelper.isBlank(param.getOccurrenceTime2())
? param.getOccurrenceTime2()
: param.getOccurrenceTime());
entity.setCourtName(!ImportHelper.isBlank(param.getCourtName2())
? param.getCourtName2()
: param.getCourtName());
entity.setReleaseDate(param.getReleaseDate()); entity.setReleaseDate(param.getReleaseDate());
entity.setComments(param.getComments()); entity.setComments(param.getComments());

View File

@@ -39,4 +39,7 @@ public class CreditBreachOfTrustImportParam implements Serializable {
@Excel(name = "备注") @Excel(name = "备注")
private String comments; private String comments;
// @Excel(name = "原告/上诉人")
// private String plaintiffAppellant2;
} }

View File

@@ -31,9 +31,15 @@ public class CreditXgxfImportParam implements Serializable {
@Excel(name = "涉案金额(元)") @Excel(name = "涉案金额(元)")
private String involvedAmount; private String involvedAmount;
@Excel(name = "涉案金额")
private String involvedAmount2;
@Excel(name = "立案日期") @Excel(name = "立案日期")
private String occurrenceTime; private String occurrenceTime;
@Excel(name = "发生时间")
private String occurrenceTime2;
@Excel(name = "执行法院") @Excel(name = "执行法院")
private String courtName; private String courtName;
@@ -43,4 +49,19 @@ public class CreditXgxfImportParam implements Serializable {
@Excel(name = "备注") @Excel(name = "备注")
private String comments; private String comments;
@Excel(name = "原告/上诉人")
private String plaintiffUser;
@Excel(name = "被告/被上诉人")
private String defendantUser;
@Excel(name = "其他当事人/第三人")
private String otherPartiesThirdParty;
@Excel(name = "数据状态")
private String dataStatus;
@Excel(name = "法院")
private String courtName2;
} }