diff --git a/src/main/java/com/gxwebsoft/credit/controller/CreditJudgmentDebtorController.java b/src/main/java/com/gxwebsoft/credit/controller/CreditJudgmentDebtorController.java index f97f0e4..7223a78 100644 --- a/src/main/java/com/gxwebsoft/credit/controller/CreditJudgmentDebtorController.java +++ b/src/main/java/com/gxwebsoft/credit/controller/CreditJudgmentDebtorController.java @@ -13,6 +13,7 @@ import com.gxwebsoft.credit.service.CreditJudgmentDebtorService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; @@ -20,9 +21,17 @@ import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Locale; import java.util.ArrayList; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** * 被执行人控制器 @@ -136,81 +145,25 @@ public class CreditJudgmentDebtorController extends BaseController { @Operation(summary = "批量导入被执行人") @PostMapping("/import") public ApiResult> importBatch(@RequestParam("file") MultipartFile file) { - List errorMessages = new ArrayList<>(); - int successCount = 0; - try { - ExcelImportSupport.ImportResult importResult = ExcelImportSupport.read( - file, CreditJudgmentDebtorImportParam.class, this::isEmptyImportRow); - List list = importResult.getData(); - System.out.println("list = " + list.size()); - int usedTitleRows = importResult.getTitleRows(); - int usedHeadRows = importResult.getHeadRows(); - - if (CollectionUtils.isEmpty(list)) { - return fail("未读取到数据,请确认模板表头与示例格式一致", null); - } - User loginUser = getLoginUser(); Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; - for (int i = 0; i < list.size(); i++) { - CreditJudgmentDebtorImportParam param = list.get(i); - try { - CreditJudgmentDebtor item = convertImportParamToEntity(param); - - if (item.getUserId() == null && currentUserId != null) { - item.setUserId(currentUserId); - } - if (item.getTenantId() == null && currentTenantId != null) { - item.setTenantId(currentTenantId); - } - if (item.getStatus() == null) { - item.setStatus(0); - } - if (item.getRecommend() == null) { - item.setRecommend(0); - } - if (item.getDeleted() == null) { - item.setDeleted(0); - } - - int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows; - if (ImportHelper.isBlank(item.getCaseNumber())) { - errorMessages.add("第" + excelRowNumber + "行:案号不能为空"); - continue; - } - - boolean saved = creditJudgmentDebtorService.save(item); - if (!saved) { - CreditJudgmentDebtor existing = creditJudgmentDebtorService.lambdaQuery() - .eq(CreditJudgmentDebtor::getCaseNumber, item.getCaseNumber()) - .one(); - if (existing != null) { - item.setId(existing.getId()); - if (creditJudgmentDebtorService.updateById(item)) { - successCount++; - continue; - } - } - } else { - successCount++; - continue; - } - errorMessages.add("第" + excelRowNumber + "行:保存失败"); - } catch (Exception e) { - int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows; - errorMessages.add("第" + excelRowNumber + "行:" + e.getMessage()); - e.printStackTrace(); - } - } - - if (errorMessages.isEmpty()) { - return success("成功导入" + successCount + "条数据", null); + ImportOutcome outcome; + if (isZip(file)) { + outcome = importFromZip(file, currentUserId, currentTenantId); } else { - return success("导入完成,成功" + successCount + "条,失败" + errorMessages.size() + "条", errorMessages); + outcome = importFromExcel(file, safeFileLabel(file.getOriginalFilename()), currentUserId, currentTenantId, false); } + + if (!outcome.anyDataRead) { + return fail("未读取到数据,请确认模板表头与示例格式一致", null); + } + if (outcome.errorMessages.isEmpty()) { + return success("成功导入" + outcome.successCount + "条数据", null); + } + return success("导入完成,成功" + outcome.successCount + "条,失败" + outcome.errorMessages.size() + "条", outcome.errorMessages); } catch (Exception e) { e.printStackTrace(); return fail("导入失败:" + e.getMessage(), null); @@ -245,14 +198,38 @@ public class CreditJudgmentDebtorController extends BaseController { } private boolean isEmptyImportRow(CreditJudgmentDebtorImportParam param) { - System.out.println("param2 = " + param); if (param == null) { return true; } + if (isImportHeaderRow(param)) { + return true; + } return ImportHelper.isBlank(param.getCaseNumber()) + && ImportHelper.isBlank(param.getName()) + && ImportHelper.isBlank(param.getName1()) && ImportHelper.isBlank(param.getCode()); } + private boolean isImportHeaderRow(CreditJudgmentDebtorImportParam param) { + return isHeaderValue(param.getName(), "序号") + || isHeaderValue(param.getName1(), "序号") + || isHeaderValue(param.getCaseNumber(), "案号") + || isHeaderValue(param.getName(), "被执行人名称") + || isHeaderValue(param.getName1(), "被执行人") + || isHeaderValue(param.getCode(), "证件号/组织机构代码") + || isHeaderValue(param.getOccurrenceTime(), "立案日期") + || isHeaderValue(param.getCourtName(), "法院") + || isHeaderValue(param.getAmount(), "执行标的(元)") + || isHeaderValue(param.getDataStatus(), "数据状态"); + } + + private static boolean isHeaderValue(String value, String headerText) { + if (value == null) { + return false; + } + return headerText.equals(value.trim()); + } + private CreditJudgmentDebtor convertImportParamToEntity(CreditJudgmentDebtorImportParam param) { CreditJudgmentDebtor entity = new CreditJudgmentDebtor(); @@ -263,10 +240,272 @@ public class CreditJudgmentDebtorController extends BaseController { entity.setCode(param.getCode()); entity.setOccurrenceTime(param.getOccurrenceTime()); entity.setAmount(param.getAmount()); + entity.setCourtName(param.getCourtName()); entity.setDataStatus(param.getDataStatus()); entity.setComments(param.getComments()); return entity; } + private static class ImportOutcome { + private final boolean anyDataRead; + private final int successCount; + private final List errorMessages; + + private ImportOutcome(boolean anyDataRead, int successCount, List errorMessages) { + this.anyDataRead = anyDataRead; + this.successCount = successCount; + this.errorMessages = errorMessages; + } + } + + private ImportOutcome importFromExcel(MultipartFile excelFile, String fileLabel, Integer currentUserId, Integer currentTenantId, boolean strictDebtorSheet) throws Exception { + List errorMessages = new ArrayList<>(); + int successCount = 0; + + ExcelImportSupport.ImportResult importResult = readDebtorImport(excelFile, strictDebtorSheet); + List list = importResult.getData(); + int usedTitleRows = importResult.getTitleRows(); + int usedHeadRows = importResult.getHeadRows(); + + if (CollectionUtils.isEmpty(list)) { + return new ImportOutcome(false, 0, errorMessages); + } + + String prefix = ImportHelper.isBlank(fileLabel) ? "" : "【" + fileLabel + "】"; + for (int i = 0; i < list.size(); i++) { + CreditJudgmentDebtorImportParam param = list.get(i); + try { + CreditJudgmentDebtor item = convertImportParamToEntity(param); + + if (item.getUserId() == null && currentUserId != null) { + item.setUserId(currentUserId); + } + if (item.getTenantId() == null && currentTenantId != null) { + item.setTenantId(currentTenantId); + } + if (item.getStatus() == null) { + item.setStatus(0); + } + if (item.getRecommend() == null) { + item.setRecommend(0); + } + if (item.getDeleted() == null) { + item.setDeleted(0); + } + System.out.println("item = " + item); + int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows; + if (ImportHelper.isBlank(item.getCaseNumber())) { + errorMessages.add(prefix + "第" + excelRowNumber + "行:案号不能为空"); + continue; + } + + boolean saved = creditJudgmentDebtorService.save(item); + if (!saved) { + CreditJudgmentDebtor existing = creditJudgmentDebtorService.lambdaQuery() + .eq(CreditJudgmentDebtor::getCaseNumber, item.getCaseNumber()) + .one(); + if (existing != null) { + item.setId(existing.getId()); + if (creditJudgmentDebtorService.updateById(item)) { + successCount++; + continue; + } + } + } else { + successCount++; + continue; + } + errorMessages.add(prefix + "第" + excelRowNumber + "行:保存失败"); + } catch (Exception e) { + int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows; + errorMessages.add(prefix + "第" + excelRowNumber + "行:" + e.getMessage()); + e.printStackTrace(); + } + } + return new ImportOutcome(true, successCount, errorMessages); + } + + private ImportOutcome importFromZip(MultipartFile zipFile, Integer currentUserId, Integer currentTenantId) throws Exception { + try { + return importFromZip(zipFile, currentUserId, currentTenantId, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return importFromZip(zipFile, currentUserId, currentTenantId, Charset.forName("GBK")); + } + } + + private ImportOutcome importFromZip(MultipartFile zipFile, Integer currentUserId, Integer currentTenantId, Charset charset) throws Exception { + List errorMessages = new ArrayList<>(); + int successCount = 0; + boolean anyDataRead = false; + + try (InputStream is = zipFile.getInputStream(); ZipInputStream zis = new ZipInputStream(is, charset)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + String entryName = entry.getName(); + if (!isExcelFileName(entryName)) { + continue; + } + + byte[] bytes = readAllBytes(zis); + String entryFileName = safeFileLabel(entryName); + MultipartFile excelFile = new InMemoryMultipartFile(entryFileName, bytes); + + try { + ImportOutcome outcome = importFromExcel(excelFile, entryFileName, currentUserId, currentTenantId, true); + if (outcome.anyDataRead) { + anyDataRead = true; + successCount += outcome.successCount; + errorMessages.addAll(outcome.errorMessages); + } + } catch (Exception e) { + errorMessages.add("【" + entryFileName + "】解析失败:" + e.getMessage()); + } + } + } + return new ImportOutcome(anyDataRead, successCount, errorMessages); + } + + private static boolean isZip(MultipartFile file) { + String filename = file != null ? file.getOriginalFilename() : null; + if (filename == null) { + return false; + } + return filename.toLowerCase(Locale.ROOT).endsWith(".zip"); + } + + private ExcelImportSupport.ImportResult readDebtorImport(MultipartFile excelFile, boolean strictDebtorSheet) throws Exception { + List debtorSheetIndices = findDebtorSheetIndices(excelFile); + for (Integer sheetIndex : debtorSheetIndices) { + ExcelImportSupport.ImportResult sheetResult = ExcelImportSupport.readBest( + excelFile, + CreditJudgmentDebtorImportParam.class, + this::isEmptyImportRow, + this::isScoreImportRow, + sheetIndex + ); + if (!CollectionUtils.isEmpty(sheetResult.getData())) { + return sheetResult; + } + } + if (strictDebtorSheet) { + return new ExcelImportSupport.ImportResult<>(new ArrayList<>(), 0, 0); + } + return ExcelImportSupport.readAnySheetBest(excelFile, CreditJudgmentDebtorImportParam.class, this::isEmptyImportRow, this::isScoreImportRow); + } + + private boolean isScoreImportRow(CreditJudgmentDebtorImportParam param) { + if (param == null) { + return false; + } + if (isImportHeaderRow(param)) { + return false; + } + return !ImportHelper.isBlank(param.getCaseNumber()); + } + + private List findDebtorSheetIndices(MultipartFile excelFile) throws Exception { + List indices = new ArrayList<>(); + try (InputStream is = excelFile.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) { + int sheetCount = workbook.getNumberOfSheets(); + for (int i = 0; i < sheetCount; i++) { + String sheetName = workbook.getSheetName(i); + if (isDebtorSheetName(sheetName)) { + indices.add(i); + } + } + } + return indices; + } + + private static boolean isDebtorSheetName(String sheetName) { + if (sheetName == null) { + return false; + } + String normalized = sheetName.replace(" ", "").trim(); + return normalized.contains("被执行人") && !normalized.contains("失信") && !normalized.contains("历史"); + } + + private static boolean isExcelFileName(String name) { + if (name == null) { + return false; + } + String lower = name.toLowerCase(Locale.ROOT); + return lower.endsWith(".xlsx") || lower.endsWith(".xls") || lower.endsWith(".xlsm"); + } + + private static String safeFileLabel(String name) { + if (ImportHelper.isBlank(name)) { + return ""; + } + int lastSlash = name.lastIndexOf('/'); + if (lastSlash >= 0 && lastSlash + 1 < name.length()) { + return name.substring(lastSlash + 1); + } + return name; + } + + private static byte[] readAllBytes(InputStream inputStream) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int read; + while ((read = inputStream.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + return out.toByteArray(); + } + + private static class InMemoryMultipartFile implements MultipartFile { + private final String originalFilename; + private final byte[] bytes; + + private InMemoryMultipartFile(String originalFilename, byte[] bytes) { + this.originalFilename = originalFilename; + this.bytes = bytes != null ? bytes : new byte[0]; + } + + @Override + public String getName() { + return originalFilename; + } + + @Override + public String getOriginalFilename() { + return originalFilename; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public boolean isEmpty() { + return bytes.length == 0; + } + + @Override + public long getSize() { + return bytes.length; + } + + @Override + public byte[] getBytes() { + return bytes; + } + + @Override + public InputStream getInputStream() { + return new java.io.ByteArrayInputStream(bytes); + } + + @Override + public void transferTo(java.io.File dest) throws IOException { + Files.write(dest.toPath(), bytes); + } + } + } diff --git a/src/main/java/com/gxwebsoft/credit/controller/ExcelImportSupport.java b/src/main/java/com/gxwebsoft/credit/controller/ExcelImportSupport.java index 7ff536b..5a274fe 100644 --- a/src/main/java/com/gxwebsoft/credit/controller/ExcelImportSupport.java +++ b/src/main/java/com/gxwebsoft/credit/controller/ExcelImportSupport.java @@ -5,9 +5,11 @@ import cn.afterturn.easypoi.excel.ExcelImportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.ImportParams; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; import org.springframework.util.CollectionUtils; import org.springframework.web.multipart.MultipartFile; +import java.io.InputStream; import java.util.List; import java.util.function.Predicate; @@ -44,6 +46,44 @@ public class ExcelImportSupport { return read(file, clazz, emptyRowPredicate, 0); } + /** + * 自动尝试所有 sheet(从第 0 个开始),直到读取到非空数据。 + */ + public static ImportResult readAnySheet(MultipartFile file, Class clazz, Predicate emptyRowPredicate) throws Exception { + ImportResult result = read(file, clazz, emptyRowPredicate, 0); + if (!CollectionUtils.isEmpty(result.getData())) { + return result; + } + + int sheetCount = getSheetCount(file); + for (int i = 1; i < sheetCount; i++) { + ImportResult sheetResult = read(file, clazz, emptyRowPredicate, i); + if (!CollectionUtils.isEmpty(sheetResult.getData())) { + return sheetResult; + } + } + return result; + } + + /** + * 自动尝试所有 sheet(从第 0 个开始),并在每个 sheet 内选择“得分”最高的表头配置。 + */ + public static ImportResult readAnySheetBest(MultipartFile file, Class clazz, Predicate emptyRowPredicate, Predicate scoreRowPredicate) throws Exception { + ImportResult result = readBest(file, clazz, emptyRowPredicate, scoreRowPredicate, 0); + if (!CollectionUtils.isEmpty(result.getData())) { + return result; + } + + int sheetCount = getSheetCount(file); + for (int i = 1; i < sheetCount; i++) { + ImportResult sheetResult = readBest(file, clazz, emptyRowPredicate, scoreRowPredicate, i); + if (!CollectionUtils.isEmpty(sheetResult.getData())) { + return sheetResult; + } + } + return result; + } + /** * 读取指定 sheet 的 Excel。 * @@ -53,7 +93,7 @@ public class ExcelImportSupport { List list = null; int usedTitleRows = 0; int usedHeadRows = 0; - int[][] tryConfigs = new int[][]{{1, 1}, {0, 1}, {0, 2}, {0, 3}}; + int[][] tryConfigs = new int[][]{{1, 1}, {0, 1}, {2, 1}, {3, 1}, {0, 2}, {0, 3}, {0, 4}}; for (int[] config : tryConfigs) { list = filterEmptyRows(importSheet(file, clazz, config[0], config[1], sheetIndex), emptyRowPredicate); @@ -66,6 +106,50 @@ public class ExcelImportSupport { return new ImportResult<>(list, usedTitleRows, usedHeadRows); } + /** + * 读取指定 sheet 的 Excel,并从多组表头配置中挑选“得分”最高的结果。 + */ + public static ImportResult readBest(MultipartFile file, Class clazz, Predicate emptyRowPredicate, Predicate scoreRowPredicate, int sheetIndex) throws Exception { + List bestList = null; + int bestTitleRows = 0; + int bestHeadRows = 0; + int bestScore = -1; + int bestSize = -1; + + 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 list = filterEmptyRows(importSheet(file, clazz, config[0], config[1], sheetIndex), emptyRowPredicate); + 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 = config[0]; + bestHeadRows = config[1]; + bestScore = score; + bestSize = size; + } + } + + if (bestList != null) { + return new ImportResult<>(bestList, bestTitleRows, bestHeadRows); + } + + return read(file, clazz, emptyRowPredicate, sheetIndex); + } + private static List importSheet(MultipartFile file, Class clazz, int titleRows, int headRows, int sheetIndex) throws Exception { ImportParams importParams = new ImportParams(); importParams.setTitleRows(titleRows); @@ -83,6 +167,12 @@ public class ExcelImportSupport { return rawList; } + private static int getSheetCount(MultipartFile file) throws Exception { + try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) { + return workbook.getNumberOfSheets(); + } + } + public static Workbook buildTemplate(String title, String sheetName, Class clazz, List examples) { ExportParams exportParams = new ExportParams(title, sheetName); return ExcelExportUtil.exportExcel(exportParams, clazz, examples);