diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveImportServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveImportServiceImpl.java index 1333999ea..2d2b62b07 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveImportServiceImpl.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveImportServiceImpl.java @@ -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 capErrors(List errors) { + if (errors.size() <= MAX_RETURNED_ERRORS) { + return errors; + } + List capped = new ArrayList<>(errors.subList(0, MAX_RETURNED_ERRORS)); + capped.add(new ErrorMessage(null, Set.of( + "错误共 " + errors.size() + " 条,仅展示前 " + MAX_RETURNED_ERRORS + " 条,请修正后重新导入"))); + return capped; + } + /** * 统计非空文本在 Excel 中的出现次数。 * diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeApplyImportServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeApplyImportServiceImpl.java index c87c54685..e3fc5e1ee 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeApplyImportServiceImpl.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeApplyImportServiceImpl.java @@ -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 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 capErrors(List errors) { + if (errors.size() <= MAX_RETURNED_ERRORS) { + return errors; + } + List 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 的逐行错误集合。 * diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeApplyImportServiceImplTest.java b/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeApplyImportServiceImplTest.java index cde4e9bb6..6eb337a8c 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeApplyImportServiceImplTest.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeApplyImportServiceImplTest.java @@ -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 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 errors = assertInstanceOf(List.class, response.getData()); + assertEquals(1001, errors.size()); + assertTrue(errors.get(1000).getErrors().toString().contains("仅展示前 1000 条")); + } + /** * 全部行被解析监听器拒绝时应回显监听器错误,而不是覆盖为通用提示。 */