Add import dropdown validation

This commit is contained in:
wkc
2026-05-06 14:04:21 +08:00
parent 0b64532959
commit c00d5475e6
8 changed files with 386 additions and 10 deletions

View File

@@ -2,19 +2,36 @@ package com.ruoyi.info.collection.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.info.collection.handler.DictDropdownWriteHandler;
import com.ruoyi.info.collection.handler.RequiredFieldWriteHandler;
import com.ruoyi.info.collection.handler.TextFormatWriteHandler;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
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.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.util.CellRangeAddress;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
@@ -77,8 +94,10 @@ public class EasyExcelUtil {
* @return 数据列表
*/
public static <T> List<T> importExcel(String fileName, Class<T> clazz) {
try {
return EasyExcel.read(fileName).head(clazz).sheet().doReadSync();
try (InputStream inputStream = java.nio.file.Files.newInputStream(java.nio.file.Path.of(fileName))) {
return importExcel(inputStream, clazz);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
@@ -94,7 +113,11 @@ public class EasyExcelUtil {
*/
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz) {
try {
return EasyExcel.read(inputStream).head(clazz).sheet().doReadSync();
byte[] bytes = inputStream.readAllBytes();
validateDictDropdownTemplate(bytes, clazz, null);
return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet().doReadSync();
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
@@ -111,7 +134,11 @@ public class EasyExcelUtil {
*/
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz, String sheetName) {
try {
return EasyExcel.read(inputStream).head(clazz).sheet(sheetName).doReadSync();
byte[] bytes = inputStream.readAllBytes();
validateDictDropdownTemplate(bytes, clazz, sheetName);
return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet(sheetName).doReadSync();
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
@@ -128,7 +155,7 @@ public class EasyExcelUtil {
public static <T> void importTemplateExcel(HttpServletResponse response, Class<T> clazz, String sheetName) {
try {
setResponseHeader(response, sheetName + "模板");
EasyExcel.write(response.getOutputStream(), clazz)
templateWriter(response, clazz)
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.doWrite(List.of());
@@ -151,7 +178,7 @@ public class EasyExcelUtil {
WriteHandler... handlers) {
try {
setResponseHeader(response, sheetName + "模板");
var writerBuilder = EasyExcel.write(response.getOutputStream(), clazz)
var writerBuilder = templateWriter(response, clazz)
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());
// 注册所有自定义处理器
@@ -190,7 +217,7 @@ public class EasyExcelUtil {
public static <T> void importTemplateWithDictDropdown(HttpServletResponse response, Class<T> clazz, String sheetName) {
try {
setResponseHeader(response, sheetName + "模板");
EasyExcel.write(response.getOutputStream(), clazz)
templateWriter(response, clazz)
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
@@ -217,7 +244,7 @@ public class EasyExcelUtil {
String sheetName, String fileName) {
try {
setResponseHeader(response, fileName);
EasyExcel.write(response.getOutputStream(), clazz)
templateWriter(response, clazz)
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
@@ -250,7 +277,7 @@ public class EasyExcelUtil {
String fileName
) {
setResponseHeader(response, fileName);
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream()).build()) {
try (ExcelWriter writer = templateWriter(response).build()) {
writer.write(List.of(), buildTemplateSheet(0, firstClazz, firstSheetName));
writer.write(List.of(), buildTemplateSheet(1, secondClazz, secondSheetName));
} catch (IOException e) {
@@ -322,4 +349,137 @@ public class EasyExcelUtil {
throw new RuntimeException("导出带字典下拉框的Excel失败", e);
}
}
private static void validateDictDropdownTemplate(byte[] bytes, Class<?> clazz, String sheetName) {
List<DropdownColumn> dropdownColumns = resolveDropdownColumns(clazz);
if (dropdownColumns.isEmpty()) {
return;
}
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes))) {
Sheet sheet = sheetName == null ? workbook.getSheetAt(0) : workbook.getSheet(sheetName);
if (sheet == null) {
return;
}
int lastDataRowIndex = findLastDataRowIndex(sheet);
if (lastDataRowIndex < 1) {
return;
}
List<String> missingColumnTitles = new ArrayList<>();
for (DropdownColumn column : dropdownColumns) {
if (!isListValidationCovered(sheet, column.index(), lastDataRowIndex)) {
missingColumnTitles.add(column.title());
}
}
if (!missingColumnTitles.isEmpty()) {
throw new ServiceException(sheet.getSheetName() + " Sheet 的 "
+ String.join("", missingColumnTitles)
+ " 列缺少下拉框,请下载最新导入模板填写后重新导入");
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
}
private static List<DropdownColumn> resolveDropdownColumns(Class<?> clazz) {
List<DropdownColumn> columns = new ArrayList<>();
Class<?> current = clazz;
while (current != null && current != Object.class) {
for (Field field : current.getDeclaredFields()) {
if (field.getAnnotation(DictDropdown.class) == null) {
continue;
}
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty == null || excelProperty.index() < 0) {
continue;
}
columns.add(new DropdownColumn(excelProperty.index(), resolveColumnTitle(field, excelProperty)));
}
current = current.getSuperclass();
}
columns.sort(Comparator.comparingInt(DropdownColumn::index));
return columns;
}
private static String resolveColumnTitle(Field field, ExcelProperty excelProperty) {
if (excelProperty.value().length > 0 && excelProperty.value()[0] != null
&& !excelProperty.value()[0].isBlank()) {
return excelProperty.value()[0].replace("*", "");
}
return field.getName();
}
private static int findLastDataRowIndex(Sheet sheet) {
int lastDataRowIndex = -1;
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (hasData(row)) {
lastDataRowIndex = rowIndex;
}
}
return lastDataRowIndex;
}
private static boolean hasData(Row row) {
if (row == null || row.getLastCellNum() < 0) {
return false;
}
for (int cellIndex = row.getFirstCellNum(); cellIndex < row.getLastCellNum(); cellIndex++) {
if (cellIndex < 0) {
continue;
}
Cell cell = row.getCell(cellIndex);
if (cell != null && cell.toString() != null && !cell.toString().isBlank()) {
return true;
}
}
return false;
}
private static boolean isListValidationCovered(Sheet sheet, int columnIndex, int lastDataRowIndex) {
boolean[] coveredRows = new boolean[lastDataRowIndex + 1];
for (DataValidation validation : sheet.getDataValidations()) {
DataValidationConstraint constraint = validation.getValidationConstraint();
if (constraint == null || constraint.getValidationType() != DataValidationConstraint.ValidationType.LIST) {
continue;
}
for (CellRangeAddress address : validation.getRegions().getCellRangeAddresses()) {
if (address.getFirstColumn() > columnIndex || address.getLastColumn() < columnIndex) {
continue;
}
int firstRow = Math.max(1, address.getFirstRow());
int lastRow = Math.min(lastDataRowIndex, address.getLastRow());
for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
coveredRows[rowIndex] = true;
}
}
}
for (int rowIndex = 1; rowIndex <= lastDataRowIndex; rowIndex++) {
if (hasData(sheet.getRow(rowIndex)) && !coveredRows[rowIndex]) {
return false;
}
}
return true;
}
private record DropdownColumn(int index, String title) {}
private static <T> ExcelWriterBuilder templateWriter(HttpServletResponse response, Class<T> clazz)
throws IOException {
// 模板为空且体量小,使用内存工作簿避免 SXSSF 在无字体环境初始化 Fontconfig。
return EasyExcel.write(response.getOutputStream(), clazz).inMemory(Boolean.TRUE);
}
private static ExcelWriterBuilder templateWriter(HttpServletResponse response) throws IOException {
return EasyExcel.write(response.getOutputStream()).inMemory(Boolean.TRUE);
}
}

View File

@@ -42,7 +42,7 @@
AND e.status = #{query.status}
</if>
</where>
ORDER BY e.create_time DESC
ORDER BY e.create_time DESC, e.staff_id DESC
</select>
<!-- 批量插入或更新员工信息只更新非null字段 -->