feat:导入excel返回错误最多1000条

This commit is contained in:
dzc committed 2026-09-15 15:16:54 +08:00
1 parent aa8e823741
commit bc2c7e2b06
3 files changed
+65 -7

No files matched your search

@@ -55,6 +55,9 @@ public class WalletActiveImportServiceImpl implements WalletActiveImportService
/** 批量用户定位单次远程请求携带的最大键数量,防止 SQL 过长。 */
private static final int LOOKUP_BATCH_SIZE = 1000;
/** 单次导入同步返回的最大错误条数,超出部分以汇总行提示,避免大文件全错时响应体过大。 */
private static final int MAX_RETURNED_ERRORS = 1000;
/** 用户远程服务,用于按默认模板或学生身份条件精确定位系统用户。 */
private final RemoteUserService remoteUserService;
@@ -84,7 +87,7 @@ public class WalletActiveImportServiceImpl implements WalletActiveImportService
.filter(row -> !ObjectUtil.isAllEmpty(row.getUsername(), row.getUserMobile(), row.getCardNumber()))
.toList();
if (effectiveRows.isEmpty()) {
return errors.isEmpty() ? R.failed("Excel内容为空") : R.failed(errors);
return errors.isEmpty() ? R.failed("Excel内容为空") : R.failed(capErrors(errors));
}
effectiveRows.forEach(row -> {
@@ -140,7 +143,7 @@ public class WalletActiveImportServiceImpl implements WalletActiveImportService
.filter(row -> !ObjectUtil.isAllEmpty(row.getStudentName(), row.getSchoolName(), row.getStudentNo(), row.getCardNumber()))
.toList();
if (effectiveRows.isEmpty()) {
return errors.isEmpty() ? R.failed("Excel内容为空") : R.failed(errors);
return errors.isEmpty() ? R.failed("Excel内容为空") : R.failed(capErrors(errors));
}
effectiveRows.forEach(row -> {
@@ -235,7 +238,7 @@ public class WalletActiveImportServiceImpl implements WalletActiveImportService
});
validateCardOccupancy(rows, errors);
if (!errors.isEmpty()) {
return R.failed(errors);
return R.failed(capErrors(errors));
}
AsyncTaskProgressVO progress = asyncTaskExecutorService.submit(redisKey, taskName, rows.size(), reporter -> {
for (ValidatedImportRow row : rows) {
@@ -406,6 +409,22 @@ public class WalletActiveImportServiceImpl implements WalletActiveImportService
return studentByKey;
}
/**
* 截断超量的逐行错误,仅保留前 {@value #MAX_RETURNED_ERRORS} 条并追加一条汇总说明。
*
* @param errors 完整校验错误集合
* @return 不超过上限的错误列表;未超量时原样返回
*/
private List<ErrorMessage> capErrors(List<ErrorMessage> errors) {
if (errors.size() <= MAX_RETURNED_ERRORS) {
return errors;
}
List<ErrorMessage> capped = new ArrayList<>(errors.subList(0, MAX_RETURNED_ERRORS));
capped.add(new ErrorMessage(null, Set.of(
"错误共 " + errors.size() + " 条,仅展示前 " + MAX_RETURNED_ERRORS + " 条,请修正后重新导入")));
return capped;
}
/**
* 统计非空文本在 Excel 中的出现次数。
*
@@ -68,6 +68,9 @@ public class WalletRechargeApplyImportServiceImpl implements WalletRechargeApply
/** 异步落库阶段单批保存的明细行数,配合进度报告器逐批推进。 */
private static final int SAVE_BATCH_SIZE = 500;
/** 单次导入同步返回的最大错误条数,超出部分以汇总行提示,避免大文件全错时响应体过大。 */
private static final int MAX_RETURNED_ERRORS = 1000;
/** 充值/扣款申请 Mapper,用于创建不绑定发票的申请主记录。 */
private final WalletRechargeApplyMapper walletRechargeApplyMapper;
@@ -352,7 +355,7 @@ public class WalletRechargeApplyImportServiceImpl implements WalletRechargeApply
.toList();
if (effectiveRows.isEmpty()) {
// 无有效行时优先回显解析期逐行错误(如字典未配置),确实为空表时给出明确提示
return ImmutablePair.of(errors.isEmpty() ? List.of(new ErrorMessage(1L, Set.of("Excel内容为空"))) : errors, null);
return ImmutablePair.of(errors.isEmpty() ? List.of(new ErrorMessage(1L, Set.of("Excel内容为空"))) : capErrors(errors), null);
}
resolveTransactionTagValues(effectiveRows, errors);
// 预统计手机号出现次数,替代逐行全表扫描的重复校验
@@ -371,7 +374,7 @@ public class WalletRechargeApplyImportServiceImpl implements WalletRechargeApply
errors.add(new ErrorMessage(row.getLineNum(), rowErrors));
}
}
return errors.isEmpty() ? ImmutablePair.of(null, effectiveRows) : ImmutablePair.of(errors, null);
return errors.isEmpty() ? ImmutablePair.of(null, effectiveRows) : ImmutablePair.of(capErrors(errors), null);
}
/**
@@ -390,7 +393,7 @@ public class WalletRechargeApplyImportServiceImpl implements WalletRechargeApply
.filter(row -> !ObjectUtil.isAllEmpty(row.getStudentName(), row.getSchoolName(), row.getStudentNo(), row.getPrice()))
.toList();
if (effectiveRows.isEmpty()) {
return ImmutablePair.of(errors.isEmpty() ? List.of(new ErrorMessage(1L, Set.of("Excel内容为空"))) : errors, null);
return ImmutablePair.of(errors.isEmpty() ? List.of(new ErrorMessage(1L, Set.of("Excel内容为空"))) : capErrors(errors), null);
}
resolveStudentTransactionTagValues(effectiveRows, errors);
Map<String, Long> studentCounts = countValues(effectiveRows.stream()
@@ -409,7 +412,7 @@ public class WalletRechargeApplyImportServiceImpl implements WalletRechargeApply
errors.add(new ErrorMessage(row.getLineNum(), rowErrors));
}
}
return errors.isEmpty() ? ImmutablePair.of(null, effectiveRows) : ImmutablePair.of(errors, null);
return errors.isEmpty() ? ImmutablePair.of(null, effectiveRows) : ImmutablePair.of(capErrors(errors), null);
}
/**
@@ -595,6 +598,22 @@ public class WalletRechargeApplyImportServiceImpl implements WalletRechargeApply
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}
/**
* 截断超量的逐行错误,仅保留前 {@value #MAX_RETURNED_ERRORS} 条并追加一条汇总说明。
*
* @param errors 完整校验错误集合
* @return 不超过上限的错误列表;未超量时原样返回
*/
private List<ErrorMessage> capErrors(List<ErrorMessage> errors) {
if (errors.size() <= MAX_RETURNED_ERRORS) {
return errors;
}
List<ErrorMessage> capped = new ArrayList<>(errors.subList(0, MAX_RETURNED_ERRORS));
capped.add(new ErrorMessage(null, Set.of(
"错误共 " + errors.size() + " 条,仅展示前 " + MAX_RETURNED_ERRORS + " 条,请修正后重新导入")));
return capped;
}
/**
* 获取公共 Excel 解析器写入 BindingResult 的逐行错误集合。
*
@@ -43,6 +43,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -279,6 +280,25 @@ class WalletRechargeApplyImportServiceImplTest {
assertTrue(errors.get(0).getErrors().contains("Excel内容为空"));
}
/**
* 全部行都有错误时应只返回前 1000 条错误,并追加一条汇总说明行。
*/
@Test
void importDefaultCapsReturnedErrorsAtThousand() {
List<WalletRechargeExcelDto> rows = new ArrayList<>();
for (int i = 0; i < 1005; i++) {
rows.add(buildDefaultRow(2L + i, "张三", String.format("1380000%04d", i)));
}
when(remoteUserService.getUsersByMobiles(anyList())).thenReturn(R.ok(List.of()));
R<?> response = service.importDefault(rows, emptyBindingResult(), "1");
assertFalse(response.isOk());
List<ErrorMessage> errors = assertInstanceOf(List.class, response.getData());
assertEquals(1001, errors.size());
assertTrue(errors.get(1000).getErrors().toString().contains("仅展示前 1000 条"));
}
/**
* 全部行被解析监听器拒绝时应回显监听器错误,而不是覆盖为通用提示。
*/