feat(controller): 添加历史法院公告批量导入功能并优化Excel导入支持

- 新增批量导入历史法院公告接口,支持"历史法院公告"和"历史法庭公告"选项卡
- 实现数据库唯一索引约束防止重复数据导入
- 优化专利导入逻辑,优先读取"专利"选项卡并兼容多sheet格式
- 增强Excel导入头部匹配功能,支持括号标注的表头识别
- 添加全角半角括号统一处理和表头规范化映射
- 实现带括号后缀表头的智能匹配和剥离功能
- 新增专利导入相关单元测试验证括号表头处理
This commit is contained in:
2026-02-14 16:52:02 +08:00
parent 546027e7a4
commit d177555ef9
4 changed files with 355 additions and 26 deletions

View File

@@ -309,6 +309,137 @@ public class CreditCourtAnnouncementController extends BaseController {
} }
} }
/**
* 批量导入历史法院公告(仅解析“历史法院公告”选项卡;兼容“历史法庭公告”)
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditCourtAnnouncement:save')")
@Operation(summary = "批量导入历史法院公告司法大数据")
@PostMapping("/import/history")
public ApiResult<List<String>> importHistoryBatch(@RequestParam("file") MultipartFile file,
@RequestParam(value = "companyId", required = false) Integer companyId) {
List<String> errorMessages = new ArrayList<>();
int successCount = 0;
Set<Integer> touchedCompanyIds = new HashSet<>();
try {
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史法院公告");
if (sheetIndex < 0) {
// 兼容旧命名
sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史法庭公告");
}
if (sheetIndex < 0) {
return fail("未读取到数据,请确认文件中存在“历史法院公告”选项卡且表头与示例格式一致", null);
}
ExcelImportSupport.ImportResult<CreditCourtAnnouncementImportParam> importResult = ExcelImportSupport.read(
file, CreditCourtAnnouncementImportParam.class, this::isEmptyImportRow, sheetIndex);
List<CreditCourtAnnouncementImportParam> 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<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditCourtAnnouncement> chunkItems = new ArrayList<>(chunkSize);
List<Integer> 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);
}
}
/** /**
* 下载法院公告司法大数据导入模板 * 下载法院公告司法大数据导入模板
*/ */

View File

@@ -195,8 +195,13 @@ public class CreditPatentController extends BaseController {
Set<Integer> touchedCompanyIds = new HashSet<>(); Set<Integer> touchedCompanyIds = new HashSet<>();
try { try {
ExcelImportSupport.ImportResult<CreditPatentImportParam> importResult = ExcelImportSupport.readAnySheet( // 单企业表通常是多 sheet专利页签名一般为“专利”
file, CreditPatentImportParam.class, this::isEmptyImportRow); int preferredSheetIndex = ExcelImportSupport.findSheetIndex(file, "专利", 0);
ExcelImportSupport.ImportResult<CreditPatentImportParam> importResult = ExcelImportSupport.read(
file, CreditPatentImportParam.class, this::isEmptyImportRow, preferredSheetIndex);
if (CollectionUtils.isEmpty(importResult.getData())) {
importResult = ExcelImportSupport.readAnySheet(file, CreditPatentImportParam.class, this::isEmptyImportRow);
}
List<CreditPatentImportParam> list = importResult.getData(); List<CreditPatentImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows(); int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows(); int usedHeadRows = importResult.getHeadRows();
@@ -345,7 +350,16 @@ public class CreditPatentController extends BaseController {
if (isImportHeaderRow(param)) { if (isImportHeaderRow(param)) {
return true; 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) { private boolean isImportHeaderRow(CreditPatentImportParam param) {

View File

@@ -2,6 +2,7 @@ package com.gxwebsoft.credit.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil; 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.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.afterturn.easypoi.excel.entity.ImportParams;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Cell;
@@ -18,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -277,7 +279,7 @@ public class ExcelImportSupport {
if (workbook.getNumberOfSheets() > sheetIndex) { if (workbook.getNumberOfSheets() > sheetIndex) {
Sheet sheet = workbook.getSheetAt(sheetIndex); Sheet sheet = workbook.getSheetAt(sheetIndex);
if (sheet != null) { if (sheet != null) {
normalizeHeaderCells(sheet, titleRows, headRows); normalizeHeaderCells(sheet, titleRows, headRows, clazz);
} }
} }
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { 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) { if (sheet == null || headRows <= 0) {
return; return;
} }
Map<String, String> expectedHeadersByKey = buildExpectedHeaderKeyMap(clazz);
if (expectedHeadersByKey.isEmpty()) {
return;
}
int headerStart = Math.max(titleRows, 0); int headerStart = Math.max(titleRows, 0);
int headerEnd = headerStart + headRows - 1; int headerEnd = headerStart + headRows - 1;
for (int r = headerStart; r <= headerEnd; r++) { for (int r = headerStart; r <= headerEnd; r++) {
@@ -317,21 +323,85 @@ public class ExcelImportSupport {
continue; continue;
} }
String normalized = normalizeHeaderText(text); String canonical = findCanonicalHeader(text, expectedHeadersByKey);
if (normalized != null && !normalized.equals(text)) { if (canonical != null && !canonical.equals(text)) {
cell.setCellValue(normalized); cell.setCellValue(canonical);
} }
} }
} }
} }
private static String normalizeHeaderText(String text) { private static Map<String, String> buildExpectedHeaderKeyMap(Class<?> clazz) {
if (text == null) { if (clazz == null) {
return Collections.emptyMap();
}
Map<String, String> 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<String, String> expectedHeadersByKey) {
if (rawHeaderText == null) {
return 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.
*
* <p>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.</p>
*/
private static String normalizeHeaderKey(String text) {
if (text == null) {
return "";
}
String normalized = text String normalized = text
// unify common full-width punctuation
.replace("", "/") .replace("", "/")
.replace("", "[")
.replace("", "]")
// remove common invisible whitespace characters, including full-width space.
.replace(" ", "") .replace(" ", "")
.replace("\t", "") .replace("\t", "")
.replace("\r", "") .replace("\r", "")
@@ -339,10 +409,34 @@ public class ExcelImportSupport {
.replace("\u00A0", "") .replace("\u00A0", "")
.replace(" ", "") .replace(" ", "")
.trim(); .trim();
// Some upstream templates append explanations in brackets, e.g. "原告/上诉人(申请执行人)".
// Strip bracketed suffixes so Easypoi can match the canonical header name. // Make ( ) and comparable by normalizing both to ASCII.
normalized = normalized.replaceAll("[\\((【\\[].*?[\\))】\\]]", ""); normalized = normalized.replace('', '(').replace('', ')');
return normalized.trim(); 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 <T> List<T> filterEmptyRows(List<T> rawList, Predicate<T> emptyRowPredicate) { private static <T> List<T> filterEmptyRows(List<T> rawList, Predicate<T> emptyRowPredicate) {
@@ -557,6 +651,10 @@ public class ExcelImportSupport {
} }
private static int findColumnIndexByHeader(Sheet sheet, int titleRows, int headRows, String headerName) { 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 firstHeaderRow = Math.max(0, titleRows);
int lastHeaderRow = Math.max(0, titleRows + headRows - 1); int lastHeaderRow = Math.max(0, titleRows + headRows - 1);
for (int r = firstHeaderRow; r <= lastHeaderRow; r++) { for (int r = firstHeaderRow; r <= lastHeaderRow; r++) {
@@ -569,20 +667,13 @@ public class ExcelImportSupport {
if (cell == null) { if (cell == null) {
continue; 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(); DataFormatter formatter = new DataFormatter();
String value = formatter.formatCellValue(cell); String value = formatter.formatCellValue(cell);
if (headerName.equals(value != null ? value.trim() : null)) { if (targetKey.equals(normalizeHeaderKey(value))) {
return c; return c;
} }
} }
} }
}
return -1; return -1;
} }

View File

@@ -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<CreditPatentImportParam> 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<CreditPatentImportParam> 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()
);
}
}
}
}