refactor(credit): 重构客户导入功能的批处理逻辑

- 导入java.sql.SQLException依赖用于数据库异常处理
- 在处理导入参数时对客户名称进行预修剪操作
- 将原有的复杂批处理逻辑提取到persistImportChunk方法中
- 简化了批量导入的核心处理流程,提高代码可读性
- 优化了重复键检测逻辑,支持多种数据库错误码识别
- 移除了冗长的内联批处理实现,改用统一的方法调用
This commit is contained in:
2026-02-11 18:02:31 +08:00
parent d778036daf
commit 95109bc031

View File

@@ -24,6 +24,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -219,7 +220,8 @@ public class CreditCustomerController extends BaseController {
try { try {
CreditCustomer item = convertImportParamToEntity(param); CreditCustomer item = convertImportParamToEntity(param);
if (!ImportHelper.isBlank(item.getName())) { if (!ImportHelper.isBlank(item.getName())) {
String link = urlByName.get(item.getName().trim()); item.setName(item.getName().trim());
String link = urlByName.get(item.getName());
if (!ImportHelper.isBlank(link)) { if (!ImportHelper.isBlank(link)) {
item.setUrl(link.trim()); item.setUrl(link.trim());
} }
@@ -256,116 +258,7 @@ public class CreditCustomerController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += persistImportChunk(chunkItems, chunkRowNumbers, mpBatchSize, errorMessages);
chunkItems,
chunkRowNumbers,
() -> {
// 批内一次查库,避免逐行查/写导致数据库压力过大
List<String> names = new ArrayList<>(chunkItems.size());
for (CreditCustomer it : chunkItems) {
if (it != null && !ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
}
List<CreditCustomer> existingList = names.isEmpty()
? new ArrayList<>()
: creditCustomerService.lambdaQuery()
.in(CreditCustomer::getName, names)
.list();
java.util.Map<String, CreditCustomer> existingByName = new java.util.HashMap<>();
for (CreditCustomer existing : existingList) {
if (existing != null && !ImportHelper.isBlank(existing.getName())) {
existingByName.putIfAbsent(existing.getName().trim(), existing);
}
}
java.util.Map<String, CreditCustomer> latestByName = new java.util.HashMap<>();
int acceptedRows = 0;
for (int idx = 0; idx < chunkItems.size(); idx++) {
CreditCustomer it = chunkItems.get(idx);
int rowNo = (idx < chunkRowNumbers.size()) ? chunkRowNumbers.get(idx) : -1;
if (it == null || ImportHelper.isBlank(it.getName())) {
continue;
}
String name = it.getName().trim();
CreditCustomer existing = existingByName.get(name);
if (existing != null) {
Integer existingTenantId = existing.getTenantId();
if (it.getTenantId() != null
&& existingTenantId != null
&& !it.getTenantId().equals(existingTenantId)) {
errorMessages.add("" + rowNo + "行:客户名称已存在且归属其他租户,无法导入");
continue;
}
it.setId(existing.getId());
if (existingTenantId != null) {
it.setTenantId(existingTenantId);
}
}
// 同名多行:保留最后一行的值(等价于“先插入/更新,再被后续行更新”)
latestByName.put(name, it);
acceptedRows++;
}
List<CreditCustomer> updates = new ArrayList<>();
List<CreditCustomer> inserts = new ArrayList<>();
for (CreditCustomer it : latestByName.values()) {
if (it.getId() != null) {
updates.add(it);
} else {
inserts.add(it);
}
}
if (!updates.isEmpty()) {
creditCustomerService.updateBatchById(updates, mpBatchSize);
}
if (!inserts.isEmpty()) {
creditCustomerService.saveBatch(inserts, mpBatchSize);
}
return acceptedRows;
},
(rowItem, rowNumber) -> {
CreditCustomer existing = creditCustomerService.lambdaQuery()
.eq(CreditCustomer::getName, rowItem.getName())
.one();
if (existing != null) {
Integer existingTenantId = existing.getTenantId();
if (rowItem.getTenantId() != null
&& existingTenantId != null
&& !rowItem.getTenantId().equals(existingTenantId)) {
errorMessages.add("" + rowNumber + "行:客户名称已存在且归属其他租户,无法导入");
return false;
}
rowItem.setId(existing.getId());
if (existingTenantId != null) {
rowItem.setTenantId(existingTenantId);
}
return creditCustomerService.updateById(rowItem);
}
try {
return creditCustomerService.save(rowItem);
} catch (DataIntegrityViolationException e) {
if (!isDuplicateCustomerName(e)) {
throw e;
}
CreditCustomer dbExisting = creditCustomerService.lambdaQuery()
.eq(CreditCustomer::getName, rowItem.getName())
.one();
if (dbExisting != null) {
Integer existingTenantId = dbExisting.getTenantId();
rowItem.setId(dbExisting.getId());
if (existingTenantId != null) {
rowItem.setTenantId(existingTenantId);
}
return creditCustomerService.updateById(rowItem);
}
}
errorMessages.add("" + rowNumber + "行:保存失败");
return false;
},
errorMessages
);
chunkItems.clear(); chunkItems.clear();
chunkRowNumbers.clear(); chunkRowNumbers.clear();
} }
@@ -376,114 +269,7 @@ public class CreditCustomerController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += persistImportChunk(chunkItems, chunkRowNumbers, mpBatchSize, errorMessages);
chunkItems,
chunkRowNumbers,
() -> {
List<String> names = new ArrayList<>(chunkItems.size());
for (CreditCustomer it : chunkItems) {
if (it != null && !ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
}
List<CreditCustomer> existingList = names.isEmpty()
? new ArrayList<>()
: creditCustomerService.lambdaQuery()
.in(CreditCustomer::getName, names)
.list();
java.util.Map<String, CreditCustomer> existingByName = new java.util.HashMap<>();
for (CreditCustomer existing : existingList) {
if (existing != null && !ImportHelper.isBlank(existing.getName())) {
existingByName.putIfAbsent(existing.getName().trim(), existing);
}
}
java.util.Map<String, CreditCustomer> latestByName = new java.util.HashMap<>();
int acceptedRows = 0;
for (int idx = 0; idx < chunkItems.size(); idx++) {
CreditCustomer it = chunkItems.get(idx);
int rowNo = (idx < chunkRowNumbers.size()) ? chunkRowNumbers.get(idx) : -1;
if (it == null || ImportHelper.isBlank(it.getName())) {
continue;
}
String name = it.getName().trim();
CreditCustomer existing = existingByName.get(name);
if (existing != null) {
Integer existingTenantId = existing.getTenantId();
if (it.getTenantId() != null
&& existingTenantId != null
&& !it.getTenantId().equals(existingTenantId)) {
errorMessages.add("" + rowNo + "行:客户名称已存在且归属其他租户,无法导入");
continue;
}
it.setId(existing.getId());
if (existingTenantId != null) {
it.setTenantId(existingTenantId);
}
}
latestByName.put(name, it);
acceptedRows++;
}
List<CreditCustomer> updates = new ArrayList<>();
List<CreditCustomer> inserts = new ArrayList<>();
for (CreditCustomer it : latestByName.values()) {
if (it.getId() != null) {
updates.add(it);
} else {
inserts.add(it);
}
}
if (!updates.isEmpty()) {
creditCustomerService.updateBatchById(updates, mpBatchSize);
}
if (!inserts.isEmpty()) {
creditCustomerService.saveBatch(inserts, mpBatchSize);
}
return acceptedRows;
},
(rowItem, rowNumber) -> {
CreditCustomer existing = creditCustomerService.lambdaQuery()
.eq(CreditCustomer::getName, rowItem.getName())
.one();
if (existing != null) {
Integer existingTenantId = existing.getTenantId();
if (rowItem.getTenantId() != null
&& existingTenantId != null
&& !rowItem.getTenantId().equals(existingTenantId)) {
errorMessages.add("" + rowNumber + "行:客户名称已存在且归属其他租户,无法导入");
return false;
}
rowItem.setId(existing.getId());
if (existingTenantId != null) {
rowItem.setTenantId(existingTenantId);
}
return creditCustomerService.updateById(rowItem);
}
try {
return creditCustomerService.save(rowItem);
} catch (DataIntegrityViolationException e) {
if (!isDuplicateCustomerName(e)) {
throw e;
}
CreditCustomer dbExisting = creditCustomerService.lambdaQuery()
.eq(CreditCustomer::getName, rowItem.getName())
.one();
if (dbExisting != null) {
Integer existingTenantId = dbExisting.getTenantId();
rowItem.setId(dbExisting.getId());
if (existingTenantId != null) {
rowItem.setTenantId(existingTenantId);
}
return creditCustomerService.updateById(rowItem);
}
}
errorMessages.add("" + rowNumber + "行:保存失败");
return false;
},
errorMessages
);
} }
creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.CUSTOMER, touchedCompanyIds); creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.CUSTOMER, touchedCompanyIds);
@@ -554,19 +340,71 @@ public class CreditCustomerController extends BaseController {
return value.trim(); return value.trim();
} }
private boolean isDuplicateCustomerName(DataIntegrityViolationException e) { private int persistImportChunk(List<CreditCustomer> items,
List<Integer> excelRowNumbers,
int mpBatchSize,
List<String> errorMessages) {
return batchImportSupport.persistChunkWithFallback(
items,
excelRowNumbers,
() -> {
boolean ok = creditCustomerService.saveBatch(items, mpBatchSize);
if (!ok) {
throw new RuntimeException("批量保存失败");
}
return items.size();
},
(rowItem, rowNumber) -> {
try {
boolean saved = creditCustomerService.save(rowItem);
if (saved) {
return true;
}
if (rowNumber != null && rowNumber > 0) {
errorMessages.add("" + rowNumber + "行:保存失败");
} else {
errorMessages.add("保存失败");
}
return false;
} catch (DataIntegrityViolationException e) {
if (isDuplicateKey(e)) {
String name = rowItem != null ? rowItem.getName() : null;
String label = ImportHelper.isBlank(name) ? "数据" : ("客户【" + name.trim() + "");
if (rowNumber != null && rowNumber > 0) {
errorMessages.add("" + rowNumber + "行:" + label + "重复(唯一索引冲突)");
} else {
errorMessages.add(label + "重复(唯一索引冲突)");
}
return false;
}
throw e;
}
},
errorMessages
);
}
private static boolean isDuplicateKey(DataIntegrityViolationException e) {
// Prefer structured detection (SQLState / vendor error code), fall back to message contains.
for (Throwable t = e; t != null; t = t.getCause()) {
if (t instanceof SQLException) {
SQLException se = (SQLException) t;
// MySQL: 1062 Duplicate entry; PostgreSQL/H2: SQLState 23505 unique_violation
if (se.getErrorCode() == 1062) {
return true;
}
if ("23505".equals(se.getSQLState())) {
return true;
}
}
}
Throwable mostSpecificCause = e.getMostSpecificCause(); Throwable mostSpecificCause = e.getMostSpecificCause();
String message = mostSpecificCause != null ? mostSpecificCause.getMessage() : e.getMessage(); String message = mostSpecificCause != null ? mostSpecificCause.getMessage() : e.getMessage();
if (message == null) { if (message == null) {
return false; return false;
} }
String lower = message.toLowerCase(); String lower = message.toLowerCase();
if (!lower.contains("duplicate")) { return lower.contains("duplicate") && lower.contains("key");
return false;
}
return lower.contains("credit_customer.name")
|| lower.contains("for key 'name'")
|| lower.contains("for key `name`");
} }
} }