From d177555ef9e97bacb6546efc461e748ee5ecbb50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 14 Feb 2026 16:52:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(controller):=20=E6=B7=BB=E5=8A=A0=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E6=B3=95=E9=99=A2=E5=85=AC=E5=91=8A=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?Excel=E5=AF=BC=E5=85=A5=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增批量导入历史法院公告接口,支持"历史法院公告"和"历史法庭公告"选项卡 - 实现数据库唯一索引约束防止重复数据导入 - 优化专利导入逻辑,优先读取"专利"选项卡并兼容多sheet格式 - 增强Excel导入头部匹配功能,支持括号标注的表头识别 - 添加全角半角括号统一处理和表头规范化映射 - 实现带括号后缀表头的智能匹配和剥离功能 - 新增专利导入相关单元测试验证括号表头处理 --- .../CreditCourtAnnouncementController.java | 131 +++++++++++++++++ .../controller/CreditPatentController.java | 20 ++- .../credit/controller/ExcelImportSupport.java | 137 +++++++++++++++--- .../ExcelImportSupportPatentImportTest.java | 93 ++++++++++++ 4 files changed, 355 insertions(+), 26 deletions(-) create mode 100644 src/test/java/com/gxwebsoft/credit/controller/ExcelImportSupportPatentImportTest.java diff --git a/src/main/java/com/gxwebsoft/credit/controller/CreditCourtAnnouncementController.java b/src/main/java/com/gxwebsoft/credit/controller/CreditCourtAnnouncementController.java index fffae60..a9b2f60 100644 --- a/src/main/java/com/gxwebsoft/credit/controller/CreditCourtAnnouncementController.java +++ b/src/main/java/com/gxwebsoft/credit/controller/CreditCourtAnnouncementController.java @@ -309,6 +309,137 @@ public class CreditCourtAnnouncementController extends BaseController { } } + /** + * 批量导入历史法院公告(仅解析“历史法院公告”选项卡;兼容“历史法庭公告”) + * 规则:使用数据库唯一索引约束,重复数据不导入。 + */ + @PreAuthorize("hasAuthority('credit:creditCourtAnnouncement:save')") + @Operation(summary = "批量导入历史法院公告司法大数据") + @PostMapping("/import/history") + public ApiResult> importHistoryBatch(@RequestParam("file") MultipartFile file, + @RequestParam(value = "companyId", required = false) Integer companyId) { + List errorMessages = new ArrayList<>(); + int successCount = 0; + Set touchedCompanyIds = new HashSet<>(); + + try { + int sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史法院公告"); + if (sheetIndex < 0) { + // 兼容旧命名 + sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史法庭公告"); + } + if (sheetIndex < 0) { + return fail("未读取到数据,请确认文件中存在“历史法院公告”选项卡且表头与示例格式一致", null); + } + + ExcelImportSupport.ImportResult importResult = ExcelImportSupport.read( + file, CreditCourtAnnouncementImportParam.class, this::isEmptyImportRow, sheetIndex); + List list = importResult.getData(); + int usedTitleRows = importResult.getTitleRows(); + int usedHeadRows = importResult.getHeadRows(); + int usedSheetIndex = importResult.getSheetIndex(); + + if (CollectionUtils.isEmpty(list)) { + return fail("未读取到数据,请确认模板表头与示例格式一致", null); + } + + User loginUser = getLoginUser(); + Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; + Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; + Map urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号"); + + final int chunkSize = 500; + final int mpBatchSize = 500; + List chunkItems = new ArrayList<>(chunkSize); + List chunkRowNumbers = new ArrayList<>(chunkSize); + + for (int i = 0; i < list.size(); i++) { + CreditCourtAnnouncementImportParam param = list.get(i); + int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows; + try { + CreditCourtAnnouncement item = convertImportParamToEntity(param); + if (item.getCaseNumber() != null) { + item.setCaseNumber(item.getCaseNumber().trim()); + } + if (ImportHelper.isBlank(item.getCaseNumber())) { + errorMessages.add("第" + excelRowNumber + "行:案号不能为空"); + continue; + } + + String link = urlByCaseNumber.get(item.getCaseNumber()); + if (!ImportHelper.isBlank(link)) { + item.setUrl(link.trim()); + } + + if (item.getCompanyId() == null && companyId != null) { + item.setCompanyId(companyId); + } + 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.getDeleted() == null) { + item.setDeleted(0); + } + // 历史导入的数据统一标记为“失效” + item.setDataStatus("失效"); + if (item.getRecommend() == null) { + item.setRecommend(0); + } + if (item.getCompanyId() != null && item.getCompanyId() > 0) { + touchedCompanyIds.add(item.getCompanyId()); + } + + chunkItems.add(item); + chunkRowNumbers.add(excelRowNumber); + if (chunkItems.size() >= chunkSize) { + successCount += batchImportSupport.persistInsertOnlyChunk( + creditCourtAnnouncementService, + chunkItems, + chunkRowNumbers, + mpBatchSize, + CreditCourtAnnouncement::getCaseNumber, + "", + errorMessages + ); + chunkItems.clear(); + chunkRowNumbers.clear(); + } + } catch (Exception e) { + errorMessages.add("第" + excelRowNumber + "行:" + e.getMessage()); + e.printStackTrace(); + } + } + + if (!chunkItems.isEmpty()) { + successCount += batchImportSupport.persistInsertOnlyChunk( + creditCourtAnnouncementService, + chunkItems, + chunkRowNumbers, + mpBatchSize, + CreditCourtAnnouncement::getCaseNumber, + "", + errorMessages + ); + } + + creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.COURT_ANNOUNCEMENT, touchedCompanyIds); + + if (errorMessages.isEmpty()) { + return success("成功导入" + successCount + "条数据", null); + } + return success("导入完成,成功" + successCount + "条,失败" + errorMessages.size() + "条", errorMessages); + } catch (Exception e) { + e.printStackTrace(); + return fail("导入失败:" + e.getMessage(), null); + } + } + /** * 下载法院公告司法大数据导入模板 */ diff --git a/src/main/java/com/gxwebsoft/credit/controller/CreditPatentController.java b/src/main/java/com/gxwebsoft/credit/controller/CreditPatentController.java index 7700964..c75de0d 100644 --- a/src/main/java/com/gxwebsoft/credit/controller/CreditPatentController.java +++ b/src/main/java/com/gxwebsoft/credit/controller/CreditPatentController.java @@ -195,8 +195,13 @@ public class CreditPatentController extends BaseController { Set touchedCompanyIds = new HashSet<>(); try { - ExcelImportSupport.ImportResult importResult = ExcelImportSupport.readAnySheet( - file, CreditPatentImportParam.class, this::isEmptyImportRow); + // 单企业表通常是多 sheet,专利页签名一般为“专利” + int preferredSheetIndex = ExcelImportSupport.findSheetIndex(file, "专利", 0); + ExcelImportSupport.ImportResult importResult = ExcelImportSupport.read( + file, CreditPatentImportParam.class, this::isEmptyImportRow, preferredSheetIndex); + if (CollectionUtils.isEmpty(importResult.getData())) { + importResult = ExcelImportSupport.readAnySheet(file, CreditPatentImportParam.class, this::isEmptyImportRow); + } List list = importResult.getData(); int usedTitleRows = importResult.getTitleRows(); int usedHeadRows = importResult.getHeadRows(); @@ -345,7 +350,16 @@ public class CreditPatentController extends BaseController { if (isImportHeaderRow(param)) { return true; } - return ImportHelper.isBlank(param.getPublicNo()); + return ImportHelper.isBlank(param.getName()) + && ImportHelper.isBlank(param.getType()) + && ImportHelper.isBlank(param.getStatusText()) + && ImportHelper.isBlank(param.getRegisterNo()) + && ImportHelper.isBlank(param.getRegisterDate()) + && ImportHelper.isBlank(param.getPublicNo()) + && ImportHelper.isBlank(param.getPublicDate()) + && ImportHelper.isBlank(param.getInventor()) + && ImportHelper.isBlank(param.getPatentApplicant()) + && ImportHelper.isBlank(param.getComments()); } private boolean isImportHeaderRow(CreditPatentImportParam param) { diff --git a/src/main/java/com/gxwebsoft/credit/controller/ExcelImportSupport.java b/src/main/java/com/gxwebsoft/credit/controller/ExcelImportSupport.java index 6948569..7d8c647 100644 --- a/src/main/java/com/gxwebsoft/credit/controller/ExcelImportSupport.java +++ b/src/main/java/com/gxwebsoft/credit/controller/ExcelImportSupport.java @@ -2,6 +2,7 @@ package com.gxwebsoft.credit.controller; import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.ExcelImportUtil; +import cn.afterturn.easypoi.excel.annotation.Excel; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.ImportParams; import org.apache.poi.ss.usermodel.Cell; @@ -18,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.lang.reflect.Field; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -277,7 +279,7 @@ public class ExcelImportSupport { if (workbook.getNumberOfSheets() > sheetIndex) { Sheet sheet = workbook.getSheetAt(sheetIndex); if (sheet != null) { - normalizeHeaderCells(sheet, titleRows, headRows); + normalizeHeaderCells(sheet, titleRows, headRows, clazz); } } try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { @@ -289,10 +291,14 @@ public class ExcelImportSupport { } } - private static void normalizeHeaderCells(Sheet sheet, int titleRows, int headRows) { + private static void normalizeHeaderCells(Sheet sheet, int titleRows, int headRows, Class clazz) { if (sheet == null || headRows <= 0) { return; } + Map expectedHeadersByKey = buildExpectedHeaderKeyMap(clazz); + if (expectedHeadersByKey.isEmpty()) { + return; + } int headerStart = Math.max(titleRows, 0); int headerEnd = headerStart + headRows - 1; for (int r = headerStart; r <= headerEnd; r++) { @@ -317,21 +323,85 @@ public class ExcelImportSupport { continue; } - String normalized = normalizeHeaderText(text); - if (normalized != null && !normalized.equals(text)) { - cell.setCellValue(normalized); + String canonical = findCanonicalHeader(text, expectedHeadersByKey); + if (canonical != null && !canonical.equals(text)) { + cell.setCellValue(canonical); } } } } - private static String normalizeHeaderText(String text) { - if (text == null) { + private static Map buildExpectedHeaderKeyMap(Class clazz) { + if (clazz == null) { + return Collections.emptyMap(); + } + Map map = new HashMap<>(); + Class current = clazz; + while (current != null && current != Object.class) { + Field[] fields = current.getDeclaredFields(); + for (Field field : fields) { + Excel excel = field.getAnnotation(Excel.class); + if (excel == null) { + continue; + } + String name = excel.name(); + if (name == null || name.trim().isEmpty()) { + continue; + } + String key = normalizeHeaderKey(name); + if (!key.isEmpty()) { + // key -> canonical annotation name + map.putIfAbsent(key, name); + } + } + current = current.getSuperclass(); + } + return map; + } + + private static String findCanonicalHeader(String rawHeaderText, Map expectedHeadersByKey) { + if (rawHeaderText == null) { return null; } - // Remove common invisible whitespace characters, including full-width space. + String key = normalizeHeaderKey(rawHeaderText); + if (key.isEmpty()) { + return null; + } + + String canonical = expectedHeadersByKey.get(key); + if (canonical != null) { + return canonical; + } + + // Some upstream templates append explanations in brackets, e.g. "原告/上诉人(申请执行人)". + // Only strip trailing bracket groups when the full header doesn't match any known expected header. + String strippedKey = stripTrailingBracketSuffix(key); + if (!strippedKey.equals(key)) { + canonical = expectedHeadersByKey.get(strippedKey); + if (canonical != null) { + return canonical; + } + } + + return null; + } + + /** + * Normalize header text for matching purposes only. + * + *

Do NOT drop bracket content (e.g. keep "(元)" or "(公告)" in the middle), because many templates use them as + * part of the canonical header name. We only remove whitespace and unify common full-width punctuation.

+ */ + private static String normalizeHeaderKey(String text) { + if (text == null) { + return ""; + } String normalized = text + // unify common full-width punctuation .replace("/", "/") + .replace("【", "[") + .replace("】", "]") + // remove common invisible whitespace characters, including full-width space. .replace(" ", "") .replace("\t", "") .replace("\r", "") @@ -339,10 +409,34 @@ public class ExcelImportSupport { .replace("\u00A0", "") .replace(" ", "") .trim(); - // Some upstream templates append explanations in brackets, e.g. "原告/上诉人(申请执行人)". - // Strip bracketed suffixes so Easypoi can match the canonical header name. - normalized = normalized.replaceAll("[\\((【\\[].*?[\\))】\\]]", ""); - return normalized.trim(); + + // Make ( ) and ( ) comparable by normalizing both to ASCII. + normalized = normalized.replace('(', '(').replace(')', ')'); + return normalized; + } + + private static String stripTrailingBracketSuffix(String text) { + if (text == null) { + return ""; + } + String s = text.trim(); + while (true) { + if (s.endsWith(")")) { + int idx = s.lastIndexOf('('); + if (idx >= 0) { + s = s.substring(0, idx).trim(); + continue; + } + } + if (s.endsWith("]")) { + int idx = s.lastIndexOf('['); + if (idx >= 0) { + s = s.substring(0, idx).trim(); + continue; + } + } + return s; + } } private static List filterEmptyRows(List rawList, Predicate emptyRowPredicate) { @@ -557,6 +651,10 @@ public class ExcelImportSupport { } private static int findColumnIndexByHeader(Sheet sheet, int titleRows, int headRows, String headerName) { + String targetKey = normalizeHeaderKey(headerName); + if (targetKey.isEmpty()) { + return -1; + } int firstHeaderRow = Math.max(0, titleRows); int lastHeaderRow = Math.max(0, titleRows + headRows - 1); for (int r = firstHeaderRow; r <= lastHeaderRow; r++) { @@ -569,17 +667,10 @@ public class ExcelImportSupport { if (cell == null) { continue; } - if (cell.getCellType() == CellType.STRING) { - String value = cell.getStringCellValue(); - if (headerName.equals(value != null ? value.trim() : null)) { - return c; - } - } else { - DataFormatter formatter = new DataFormatter(); - String value = formatter.formatCellValue(cell); - if (headerName.equals(value != null ? value.trim() : null)) { - return c; - } + DataFormatter formatter = new DataFormatter(); + String value = formatter.formatCellValue(cell); + if (targetKey.equals(normalizeHeaderKey(value))) { + return c; } } } diff --git a/src/test/java/com/gxwebsoft/credit/controller/ExcelImportSupportPatentImportTest.java b/src/test/java/com/gxwebsoft/credit/controller/ExcelImportSupportPatentImportTest.java new file mode 100644 index 0000000..6b6814d --- /dev/null +++ b/src/test/java/com/gxwebsoft/credit/controller/ExcelImportSupportPatentImportTest.java @@ -0,0 +1,93 @@ +package com.gxwebsoft.credit.controller; + +import com.gxwebsoft.credit.param.CreditPatentImportParam; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayOutputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class ExcelImportSupportPatentImportTest { + + @Test + void import_should_map_parentheses_headers() throws Exception { + MultipartFile file = buildPatentWorkbookFile("公开(公告)号", "申请(专利权)人"); + + ExcelImportSupport.ImportResult result = ExcelImportSupport.readAnySheet( + file, + CreditPatentImportParam.class, + p -> p == null || (ImportHelper.isBlank(p.getRegisterNo()) && ImportHelper.isBlank(p.getName())) + ); + + assertNotNull(result); + assertEquals(1, result.getData().size()); + CreditPatentImportParam row = result.getData().get(0); + assertEquals("CN2024XXXXXXXX.X", row.getRegisterNo()); + assertEquals("CN1XXXXXXXXX", row.getPublicNo()); + assertEquals("示例科技有限公司", row.getPatentApplicant()); + } + + @Test + void import_should_handle_fullwidth_parentheses_headers() throws Exception { + MultipartFile file = buildPatentWorkbookFile("公开(公告)号", "申请(专利权)人"); + + ExcelImportSupport.ImportResult result = ExcelImportSupport.readAnySheet( + file, + CreditPatentImportParam.class, + p -> p == null || (ImportHelper.isBlank(p.getRegisterNo()) && ImportHelper.isBlank(p.getName())) + ); + + assertNotNull(result); + assertEquals(1, result.getData().size()); + CreditPatentImportParam row = result.getData().get(0); + assertEquals("CN2024XXXXXXXX.X", row.getRegisterNo()); + assertEquals("CN1XXXXXXXXX", row.getPublicNo()); + assertEquals("示例科技有限公司", row.getPatentApplicant()); + } + + private static MultipartFile buildPatentWorkbookFile(String publicNoHeader, String applicantHeader) throws Exception { + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("专利"); + Row header = sheet.createRow(0); + header.createCell(0).setCellValue("发明名称"); + header.createCell(1).setCellValue("专利类型"); + header.createCell(2).setCellValue("法律状态"); + header.createCell(3).setCellValue("申请号"); + header.createCell(4).setCellValue("申请日"); + header.createCell(5).setCellValue(publicNoHeader); + header.createCell(6).setCellValue("公开(公告)日期"); + header.createCell(7).setCellValue("发明人"); + header.createCell(8).setCellValue(applicantHeader); + header.createCell(9).setCellValue("备注"); + + Row row = sheet.createRow(1); + row.createCell(0).setCellValue("一种示例装置及方法"); + row.createCell(1).setCellValue("发明专利"); + row.createCell(2).setCellValue("有效"); + row.createCell(3).setCellValue("CN2024XXXXXXXX.X"); + row.createCell(4).setCellValue("2024-01-01"); + row.createCell(5).setCellValue("CN1XXXXXXXXX"); + row.createCell(6).setCellValue("2024-06-01"); + row.createCell(7).setCellValue("张三;李四"); + row.createCell(8).setCellValue("示例科技有限公司"); + row.createCell(9).setCellValue("备注信息"); + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + workbook.write(bos); + return new MockMultipartFile( + "file", + "patent.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + bos.toByteArray() + ); + } + } + } +}