!52 feat(excel): 新增EasyExcel分批导入,支持字典校验和Hibernate Validator

* feat(excel): 新增EasyExcel分批导入,支持字典校验和Hibernate Validator
* feat(excel): 新增EasyExcel导出工具类,基于@Excel注解解析并支持模板表头下载
* chore: 添加easyexcel依赖
This commit is contained in:
new_new_new
2026-06-23 10:36:56 +00:00
parent 8b3dd7fb8f
commit 9aec8dbbbe
11 changed files with 828 additions and 27 deletions
+9
View File
@@ -327,5 +327,14 @@
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -18,7 +18,9 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.jeecg.common.util.excel.ExcelAnnotationUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
@@ -27,6 +29,7 @@ import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
/**
@@ -37,12 +40,16 @@ import java.util.*;
*/
@Slf4j
public class JeecgController<T, S extends IService<T>> {
/**issues/2933 JeecgController注入service时改用protected修饰,能避免重复引用service*/
/**
* issues/2933 JeecgController注入service时改用protected修饰,能避免重复引用service
*/
@Autowired
protected S service;
@Resource
private JeecgBaseConfig jeecgBaseConfig;
private final int BATCH_NUM = 500;
/**
* 导出excel
*
@@ -57,7 +64,7 @@ public class JeecgController<T, S extends IService<T>> {
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id",selectionList);
queryWrapper.in("id", selectionList);
}
// Step.2 获取导出数据
List<T> exportList = service.list(queryWrapper);
@@ -68,53 +75,54 @@ public class JeecgController<T, S extends IService<T>> {
mv.addObject(NormalExcelConstants.FILE_NAME, title);
mv.addObject(NormalExcelConstants.CLASS, clazz);
//update-begin--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置--------------------
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
//update-end--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置----------------------
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
return mv;
}
/**
* 根据每页sheet数量导出多sheet
*
* @param request
* @param object 实体类
* @param clazz 实体类class
* @param title 标题
* @param object 实体类
* @param clazz 实体类class
* @param title 标题
* @param exportFields 导出字段自定义
* @param pageNum 每个sheet的数据条数
* @param pageNum 每个sheet的数据条数
* @param request
*/
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title, String exportFields, Integer pageNum) {
// Step.1 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
// Step.2 计算分页sheet数据
double total = service.count();
int count = (int)Math.ceil(total/pageNum);
int count = (int) Math.ceil(total / pageNum);
//update-begin-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
// Step.3 过滤选中数据
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id",selectionList);
queryWrapper.in("id", selectionList);
}
//update-end-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
// Step.4 多sheet处理
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
for (int i = 1; i <=count ; i++) {
for (int i = 1; i <= count; i++) {
Page<T> page = new Page<T>(i, pageNum);
IPage<T> pageList = service.page(page, queryWrapper);
List<T> exportList = pageList.getRecords();
Map<String, Object> map = new HashMap<>(5);
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title+i,jeecgBaseConfig.getPath().getUpload());
ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title + i, jeecgBaseConfig.getPath().getUpload());
exportParams.setType(ExcelType.XSSF);
//map.put("title",exportParams);
//表格Title
map.put(NormalExcelConstants.PARAMS,exportParams);
map.put(NormalExcelConstants.PARAMS, exportParams);
//表格对应实体
map.put(NormalExcelConstants.CLASS,clazz);
map.put(NormalExcelConstants.CLASS, clazz);
//数据集合
map.put(NormalExcelConstants.DATA_LIST, exportList);
listMap.add(map);
@@ -133,12 +141,42 @@ public class JeecgController<T, S extends IService<T>> {
*
* @param request
*/
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields) {
ModelAndView mv = this.exportXls(request,object,clazz,title);
mv.addObject(NormalExcelConstants.EXPORT_FIELDS,exportFields);
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title, String exportFields) {
ModelAndView mv = this.exportXls(request, object, clazz, title);
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
return mv;
}
/**
* 通过EasyExcel导出excel,基于 @ExcelProperty 注解读取列配置
*/
protected void exportXlsByEasyExcel(HttpServletRequest request, HttpServletResponse response,
T object, Class<T> clazz, String title) throws IOException {
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id", selectionList);
}
List<T> exportList = service.list(queryWrapper);
writeExcel(response, clazz, title, exportList);
}
/**
* 仅导出Excel表头(不含数据),基于 @ExcelProperty 注解读取列配置
*/
protected void exportXlsHeadersByEasyExcel(HttpServletResponse response,
Class<T> clazz, String title) throws IOException {
writeExcel(response, clazz, title, Collections.emptyList());
}
private void writeExcel(HttpServletResponse response, Class<T> clazz,
String title, List<T> data) throws IOException {
ExcelAnnotationUtils.write(response, clazz, title, data);
}
/**
* 获取对象ID
*
@@ -153,6 +191,42 @@ public class JeecgController<T, S extends IService<T>> {
}
}
/**
* 通过EasyExcel导入数据,基于 @Excel 注解读取列配置
*/
protected Result<?> importExcelByEasyExcel(HttpServletRequest request, Class<T> clazz) {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
MultipartFile file = fileMap.values().stream().findFirst().orElse(null);
if (file == null) {
return Result.error("未找到上传文件!");
}
try (InputStream inputStream = file.getInputStream()) {
long start = System.currentTimeMillis();
List<ExcelAnnotationUtils.ValidationError> errors = new ArrayList<>();
int total = ExcelAnnotationUtils.read(
inputStream,
clazz,
batch -> service.saveBatch(batch),
BATCH_NUM,
errors::addAll);
log.info("【EasyExcel导入】导入完成, 数据行数: {}, 耗时: {}毫秒", total, System.currentTimeMillis() - start);
if (!errors.isEmpty()) {
for (ExcelAnnotationUtils.ValidationError err : errors) {
log.error("【EasyExcel导入】校验失败, 行号: {}, 错误: {}", err.row(), err.messages());
}
return Result.error("文件导入失败: 存在" + errors.size() + "行数据校验不通过,请修改后重新导入");
}
return Result.ok("文件导入成功!数据行数:" + total);
} catch (DuplicateKeyException e) {
log.error("【EasyExcel导入】存在重复数据", e);
return Result.error("文件导入失败:有重复数据!");
} catch (IOException e) {
log.error("【EasyExcel导入】文件读取失败", e);
return Result.error("文件导入失败:" + e.getMessage());
}
}
/**
* 通过excel导入数据
*
@@ -184,9 +258,9 @@ public class JeecgController<T, S extends IService<T>> {
//update-begin-author:taoyan date:20211124 for: 导入数据重复增加提示
String msg = e.getMessage();
log.error(msg, e);
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
if (msg != null && msg.indexOf("Duplicate entry") >= 0) {
return Result.error("文件导入失败:有重复数据!");
}else{
} else {
return Result.error("文件导入失败:" + e.getMessage());
}
//update-end-author:taoyan date:20211124 for: 导入数据重复增加提示
@@ -0,0 +1,51 @@
package org.jeecg.common.util.excel;
import java.lang.reflect.Field;
/**
* @Excel 注解解析后的列元数据
*/
public class ColumnMeta {
private final Field field;
private final String name;
private final double width;
private final String format;
private final String[] replace;
private final boolean multiReplace;
private final String numFormat;
private final String groupName;
private final int orderNum;
private final String suffix;
private final String databaseFormat;
private final boolean columnHidden;
public ColumnMeta(Field field, String name, double width, String format, String[] replace,
boolean multiReplace, String numFormat, String groupName, int orderNum,
String suffix, String databaseFormat, boolean columnHidden) {
this.field = field;
this.name = name;
this.width = width;
this.format = format;
this.replace = replace;
this.multiReplace = multiReplace;
this.numFormat = numFormat;
this.groupName = groupName;
this.orderNum = orderNum;
this.suffix = suffix;
this.databaseFormat = databaseFormat;
this.columnHidden = columnHidden;
}
public Field getField() { return field; }
public String getName() { return name; }
public double getWidth() { return width; }
public String getFormat() { return format; }
public String[] getReplace() { return replace; }
public boolean isMultiReplace() { return multiReplace; }
public String getNumFormat() { return numFormat; }
public String getGroupName() { return groupName; }
public int getOrderNum() { return orderNum; }
public String getSuffix() { return suffix; }
public String getDatabaseFormat() { return databaseFormat; }
public boolean isColumnHidden() { return columnHidden; }
}
@@ -0,0 +1,43 @@
package org.jeecg.common.util.excel;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import java.util.List;
/**
* EasyExcel 列宽处理器,读取 ColumnMeta 中的 width 设置列宽
*/
public class ColumnWidthHandler extends AbstractColumnWidthStyleStrategy {
private final List<ColumnMeta> columns;
public ColumnWidthHandler(List<ColumnMeta> columns) {
this.columns = columns;
}
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder,
List<WriteCellData<?>> cellDataList,
Cell cell, Head head,
Integer relativeRowIndex, Boolean isHead) {
if (isHead == null || !isHead || columns == null || columns.isEmpty()) {
return;
}
int columnIndex = cell.getColumnIndex();
if (columnIndex >= columns.size()) {
return;
}
ColumnMeta meta = columns.get(columnIndex);
Sheet sheet = cell.getSheet();
if (meta.isColumnHidden()) {
sheet.setColumnHidden(columnIndex, true);
} else {
sheet.setColumnWidth(columnIndex, (int) ((meta.getWidth() + 2) * 256));
}
}
}
@@ -0,0 +1,376 @@
package org.jeecg.common.util.excel;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.write.handler.WriteHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecgframework.dict.service.AutoPoiDictServiceI;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
/**
* @Excel 注解工具类,用于 EasyExcel 导入导出。
* 从实体类字段上读取 @Excel 注解,构建 EasyExcel 表头和数据,无需 @ExcelProperty。
*/
@Slf4j
@Component
public final class ExcelAnnotationUtils {
private static volatile ExcelAnnotationUtils instance;
private final Validator validator;
/**
* 校验错误信息载体:行号 + 错误消息列表(现代 Java Record,1行搞定)
*/
public record ValidationError(int row, List<String> messages) {}
@Autowired
public ExcelAnnotationUtils(Validator validator) {
this.validator = validator;
ExcelAnnotationUtils.instance = this;
log.info("【Excel导入】Spring Validator 成功通过构造器桥接到静态上下文");
}
private ExcelAnnotationUtils(Validator validator, boolean isBackup) {
this.validator = validator;
}
public static Validator getValidator() {
if (instance != null && instance.validator != null) {
return instance.validator;
}
synchronized (ExcelAnnotationUtils.class) {
if (instance == null) {
log.warn("【Excel导入】未检测到已注入的 Spring 实例,正在启用原生纯 Java Validator 作为安全保底!");
Validator tempValidator = Validation.buildDefaultValidatorFactory().getValidator();
instance = new ExcelAnnotationUtils(tempValidator, true);
return tempValidator;
}
}
return instance.validator;
}
/**
* 解析 Class 上带 @Excel 注解的字段,按 orderNum 排序返回列元数据
*/
public static List<ColumnMeta> parseColumns(Class<?> clazz) {
List<ColumnMeta> columns = new ArrayList<>();
AutoPoiDictServiceI dictService = getDictService();
for (Field field : getAllFields(clazz)) {
Excel excel = field.getAnnotation(Excel.class);
if (excel == null) {
continue;
}
field.setAccessible(true);
String[] replace = excel.replace();
if (dictService != null && (StringUtils.isNotEmpty(excel.dictTable()) || StringUtils.isNotEmpty(excel.dicCode()))) {
String[] dictReplace = dictService.queryDict(excel.dictTable(), excel.dicCode(), excel.dicText());
if (dictReplace != null && dictReplace.length > 0) {
replace = mergeReplace(replace, dictReplace);
}
}
columns.add(new ColumnMeta(
field,
excel.name(),
excel.width(),
StringUtils.isNotEmpty(excel.exportFormat()) ? excel.exportFormat() : excel.format(),
replace,
excel.multiReplace(),
excel.numFormat(),
excel.groupName(),
parseOrderNum(excel.orderNum()),
excel.suffix(),
excel.databaseFormat(),
excel.isColumnHidden()
));
}
columns.sort(Comparator.comparingInt(ColumnMeta::getOrderNum));
return columns;
}
/**
* 构建 EasyExcel 多级表头,支持 @Excel.groupName()
*/
public static List<List<String>> buildHead(List<ColumnMeta> columns) {
boolean hasGroup = columns.stream().anyMatch(c -> StringUtils.isNotEmpty(c.getGroupName()));
List<List<String>> head = new ArrayList<>();
for (ColumnMeta col : columns) {
if (hasGroup && StringUtils.isNotEmpty(col.getGroupName())) {
head.add(Arrays.asList(col.getGroupName(), col.getName()));
} else {
head.add(Collections.singletonList(col.getName()));
}
}
return head;
}
/**
* 将数据列表按 ColumnMeta 转换为 EasyExcel 的行数据,应用字典翻译、日期格式化、值替换等
*/
public static List<List<Object>> convertData(List<ColumnMeta> columns, List<?> dataList) {
List<List<Object>> rows = new ArrayList<>(dataList.size());
for (Object row : dataList) {
List<Object> rowData = new ArrayList<>(columns.size());
for (ColumnMeta col : columns) {
rowData.add(getCellValue(col, row));
}
rows.add(rowData);
}
return rows;
}
/**
* 写出 Excel 的公共入口:设置响应头 + 构建表头 + 写数据
*/
public static void write(HttpServletResponse response, Class<?> clazz,
String title, List<?> data) throws IOException {
List<ColumnMeta> columns = parseColumns(clazz);
List<List<String>> head = buildHead(columns);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String fileName = URLEncoder.encode(title, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream())
.head(head)
.registerWriteHandler(new ColumnWidthHandler(columns)) // 这里保持自适应宽度
.sheet(title)
.doWrite(convertData(columns, data));
}
// ---- 私有辅助方法 ----
private static Object getCellValue(ColumnMeta col, Object obj) {
try {
Object value = col.getField().get(obj);
if (value == null || "".equals(value)) {
return "";
}
if (StringUtils.isNotEmpty(col.getFormat())) {
value = formatDate(value, col);
}
String strValue = value.toString();
if (col.getReplace() != null && col.getReplace().length > 0) {
strValue = col.isMultiReplace()
? multiReplaceValue(col.getReplace(), strValue)
: replaceValue(col.getReplace(), strValue);
}
if (StringUtils.isNotEmpty(col.getNumFormat()) && value instanceof Number) {
strValue = new DecimalFormat(col.getNumFormat()).format(value);
}
if (StringUtils.isNotEmpty(col.getSuffix())) {
strValue = strValue + col.getSuffix();
}
return strValue;
} catch (Exception e) {
log.error("获取单元格值失败, field: {}, row: {}", col.getField().getName(), obj, e);
return "";
}
}
private static Object formatDate(Object value, ColumnMeta col) throws Exception {
if (value instanceof Date) {
return new SimpleDateFormat(col.getFormat()).format((Date) value);
}
if (value instanceof LocalDateTime) {
return ((LocalDateTime) value).format(DateTimeFormatter.ofPattern(col.getFormat()));
}
if (value instanceof LocalDate) {
return ((LocalDate) value).format(DateTimeFormatter.ofPattern(col.getFormat()));
}
if (value instanceof String && StringUtils.isNotEmpty(col.getDatabaseFormat())) {
Date parsed = new SimpleDateFormat(col.getDatabaseFormat()).parse((String) value);
return new SimpleDateFormat(col.getFormat()).format(parsed);
}
return value;
}
private static String replaceValue(String[] replace, String value) {
for (String s : replace) {
String[] arr = s.split("_");
if (arr.length == 2 && value.equals(arr[1])) {
return arr[0];
}
}
return value;
}
private static String multiReplaceValue(String[] replace, String value) {
StringBuilder sb = new StringBuilder();
for (String v : value.split(",")) {
String replaced = replaceValue(replace, v.trim());
if (sb.length() > 0) sb.append(",");
sb.append(replaced);
}
return sb.toString();
}
private static String[] mergeReplace(String[] original, String[] dictReplace) {
if (original == null || original.length == 0) return dictReplace;
if (dictReplace == null || dictReplace.length == 0) return original;
String[] merged = Arrays.copyOf(original, original.length + dictReplace.length);
System.arraycopy(dictReplace, 0, merged, original.length, dictReplace.length);
return merged;
}
private static int parseOrderNum(String orderNum) {
try {
return Integer.parseInt(orderNum);
} catch (NumberFormatException e) {
return 0;
}
}
private static AutoPoiDictServiceI getDictService() {
try {
return SpringContextUtils.getBean(AutoPoiDictServiceI.class);
} catch (Exception e) {
return null;
}
}
/**
* 从 Excel 输入流分批导入数据,基于 @Excel 注解读取列配置。
*/
public static <T> int read(InputStream inputStream, Class<T> clazz,
Consumer<List<T>> batchHandler, int batchSize,
Consumer<List<ValidationError>> errorHandler) {
// 1. 职责一:解析注解元数据,准备基础映射表
List<ColumnMeta> columns = parseColumns(clazz);
Map<String, ColumnMeta> nameToCol = new LinkedHashMap<>();
Map<String, String> displayToCode = new LinkedHashMap<>();
for (ColumnMeta col : columns) {
nameToCol.put(col.getName(), col);
buildReverseReplace(col.getReplace(), displayToCode);
}
// 2. 职责二:实例化专属的流式解析校验监听器
ExcelImportListener<T> listener = new ExcelImportListener<>(
clazz, batchSize, nameToCol, displayToCode, batchHandler, errorHandler
);
// 3. 职责三:交由 EasyExcel 引擎启动
EasyExcel.read(inputStream, listener).sheet().doRead();
// 4. 返回结果
return listener.getSuccessCount();
}
static boolean isValidDictDisplay(String[] replace, String value) {
for (String s : replace) {
String[] arr = s.split("_");
if (arr.length >= 2 && arr[0].equals(value)) {
return true;
}
}
return false;
}
private static void buildReverseReplace(String[] replace, Map<String, String> displayToCode) {
if (replace == null || replace.length == 0) {
return;
}
for (String s : replace) {
String[] arr = s.split("_");
if (arr.length >= 2) {
displayToCode.put(arr[0], arr[1]);
}
}
}
static void setFieldValue(ColumnMeta col, Object obj, String value,
Map<String, String> displayToCode) throws Exception {
Field field = col.getField();
Class<?> type = field.getType();
String resolved = displayToCode.getOrDefault(value, value);
if (type == String.class) {
field.set(obj, resolved);
return;
}
try {
if (type == Integer.class || type == int.class) {
field.set(obj, Integer.valueOf(resolved));
} else if (type == Long.class || type == long.class) {
field.set(obj, Long.valueOf(resolved));
} else if (type == Double.class || type == double.class) {
field.set(obj, Double.valueOf(resolved));
} else if (type == BigDecimal.class) {
field.set(obj, new BigDecimal(resolved));
} else if (type == Boolean.class || type == boolean.class) {
field.set(obj, Boolean.valueOf(resolved));
} else if (type == Date.class) {
String format = StringUtils.isNotEmpty(col.getDatabaseFormat()) ? col.getDatabaseFormat() : col.getFormat();
if (StringUtils.isNotEmpty(format)) {
field.set(obj, new SimpleDateFormat(format).parse(resolved));
}
} else if (type == LocalDateTime.class) {
String format = col.getFormat();
if (StringUtils.isNotEmpty(format)) {
field.set(obj, LocalDateTime.parse(resolved, DateTimeFormatter.ofPattern(format)));
}
} else if (type == LocalDate.class) {
String format = col.getFormat();
if (StringUtils.isNotEmpty(format)) {
field.set(obj, LocalDate.parse(resolved, DateTimeFormatter.ofPattern(format)));
}
} else if (type == Float.class || type == float.class) {
field.set(obj, Float.valueOf(resolved));
} else if (type == Short.class || type == short.class) {
field.set(obj, Short.valueOf(resolved));
} else {
field.set(obj, resolved);
}
} catch (NumberFormatException e) {
log.warn("【Excel导入】字段值类型转换失败, field: {}, value: {}", field.getName(), resolved);
}
}
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> current = clazz;
while (current != null && current != Object.class) {
Collections.addAll(fields, current.getDeclaredFields());
current = current.getSuperclass();
}
return fields;
}
}
@@ -0,0 +1,43 @@
package org.jeecg.common.util.excel; // 换成你的包名
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.ArrayList;
import java.util.List;
public class ExcelDataListener<T> extends AnalysisEventListener<T> {
private static final int BATCH_COUNT = 1000;
private final List<T> cachedDataList = new ArrayList<>(BATCH_COUNT);
private final IService<T> baseService;
private int totalCount = 0;
public ExcelDataListener(IService<T> baseService) {
this.baseService = baseService;
}
@Override
public void invoke(T data, AnalysisContext context) {
cachedDataList.add(data);
totalCount++;
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
cachedDataList.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (!cachedDataList.isEmpty()) {
saveData();
}
}
private void saveData() {
baseService.saveBatch(cachedDataList);
}
public int getTotalCount() {
return totalCount;
}
}
@@ -0,0 +1,165 @@
package org.jeecg.common.util.excel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.util.excel.ExcelAnnotationUtils.ValidationError;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.*;
import java.util.function.Consumer;
/**
* 工业级 Excel 导入流式解析与全防御校验监听器
*/
@Slf4j
public class ExcelImportListener<T> implements ReadListener<Map<Integer, String>> {
private final Class<T> clazz;
private final int batchSize;
private final Consumer<List<T>> batchHandler;
private final Consumer<List<ValidationError>> errorHandler;
// 外部传入的数据支撑
private final Map<String, ColumnMeta> nameToCol;
private final Map<String, String> displayToCode;
// 内部状态机
private final List<T> batch;
private final List<ValidationError> errorBatch;
private final Map<Integer, ColumnMeta> columnOrder = new LinkedHashMap<>();
private final Map<String, String> fieldToColName = new HashMap<>();
private final int[] headRowIndex = {-1};
private final int[] successCount = {0};
private final int[] skipCount = {0};
private final int[] validationFailCount = {0};
public ExcelImportListener(Class<T> clazz, int batchSize,
Map<String, ColumnMeta> nameToCol, Map<String, String> displayToCode,
Consumer<List<T>> batchHandler, Consumer<List<ValidationError>> errorHandler) {
this.clazz = clazz;
this.batchSize = batchSize;
this.nameToCol = nameToCol;
this.displayToCode = displayToCode;
this.batchHandler = batchHandler;
this.errorHandler = errorHandler;
this.batch = new ArrayList<>(batchSize);
this.errorBatch = new ArrayList<>(batchSize);
}
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
if (headRowIndex[0] >= 0) return;
for (Map.Entry<Integer, ReadCellData<?>> entry : headMap.entrySet()) {
String headName = entry.getValue() != null ? entry.getValue().getStringValue() : null;
if (headName != null) {
ColumnMeta col = nameToCol.get(headName);
if (col != null) {
columnOrder.put(entry.getKey(), col);
}
}
}
headRowIndex[0] = context.readRowHolder().getRowIndex();
// 缓存 Java 属性名 -> Excel 表头列名的转换映射
for (ColumnMeta cm : columnOrder.values()) {
if (cm.getField() != null) {
fieldToColName.put(cm.getField().getName(), cm.getName());
}
}
}
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
if (context.readRowHolder().getRowIndex() == headRowIndex[0]) return;
int rowIndex = context.readRowHolder().getRowIndex();
try {
T obj = clazz.getDeclaredConstructor().newInstance();
List<String> rowErrors = new ArrayList<>();
// 1. 字典校验 + 安全转换赋值
for (Map.Entry<Integer, String> entry : data.entrySet()) {
ColumnMeta col = columnOrder.get(entry.getKey());
String cellValue = entry.getValue();
if (col == null || StringUtils.isEmpty(cellValue)) continue;
String[] replace = col.getReplace();
if (replace != null && replace.length > 0) {
if (!ExcelAnnotationUtils.isValidDictDisplay(replace, cellValue)) {
rowErrors.add("" + col.getName() + "】的值'" + cellValue + "'不在字典范围内");
continue;
}
}
ExcelAnnotationUtils.setFieldValue(col, obj, cellValue, displayToCode);
}
// 2. Hibernate JSR-303 进阶校验
Validator currentValidator = ExcelAnnotationUtils.getValidator();
if (currentValidator != null && errorHandler != null) {
Set<ConstraintViolation<T>> violations = currentValidator.validate(obj);
if (!violations.isEmpty()) {
for (ConstraintViolation<T> v : violations) {
String propertyName = v.getPropertyPath().toString();
String label = fieldToColName.getOrDefault(propertyName, propertyName);
rowErrors.add("" + label + "" + v.getMessage());
}
}
}
// 3. 错误统一拦截清算
if (!rowErrors.isEmpty()) {
handleError(rowIndex + 1, rowErrors);
return;
}
// 4. 成功加入批次
batch.add(obj);
successCount[0]++;
if (batch.size() >= batchSize) {
batchHandler.accept(new ArrayList<>(batch));
batch.clear();
}
} catch (Exception e) {
skipCount[0]++;
log.error("【Excel导入】行数据转换发生系统异常, row: {}", rowIndex, e);
}
}
private void handleError(int rowNum, List<String> errors) {
validationFailCount[0]++;
errorBatch.add(new ValidationError(rowNum, errors));
if (errorBatch.size() >= batchSize) {
errorHandler.accept(new ArrayList<>(errorBatch));
errorBatch.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (!batch.isEmpty()) {
batchHandler.accept(new ArrayList<>(batch));
batch.clear();
}
if (!errorBatch.isEmpty()) {
errorHandler.accept(new ArrayList<>(errorBatch));
errorBatch.clear();
}
if (skipCount[0] > 0 || validationFailCount[0] > 0) {
log.warn("【Excel导入】导入完成, 成功: {} 行, 转换失败: {} 行, 校验失败: {} 行",
successCount[0], skipCount[0], validationFailCount[0]);
}
}
public int getSuccessCount() {
return successCount[0];
}
}
@@ -373,6 +373,26 @@ public class BgXiSpeakController extends JeecgController<BgXiSpeak, IBgXiSpeakSe
return super.importExcel(request, response, BgXiSpeak.class);
}
/**
* 通过EasyExcel导入数据
*/
@RequiresPermissions("bg.xispeak:bg_xi_speak:importExcel")
@RequestMapping(value = "/importExcelByEasyExcel", method = RequestMethod.POST)
public Result<?> importExcelByEasyExcel(HttpServletRequest request) {
return super.importExcelByEasyExcel(request, BgXiSpeak.class);
}
/**
* 仅导出Excel表头(EasyExcel
*/
@RequiresPermissions("bg.xispeak:bg_xi_speak:exportXls")
@GetMapping(value = "/exportXlsHeaders")
public void exportXlsHeaders(HttpServletResponse response) throws IOException {
super.exportXlsHeadersByEasyExcel(response, BgXiSpeak.class, "习总书记重要讲话指示批示情况");
}
/**
* 批量查询多个 xispeak 节点下的反馈统计数量
*
@@ -7,7 +7,6 @@ import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
@@ -15,6 +14,8 @@ import org.jeecgframework.poi.excel.annotation.Excel;
import org.jeecg.common.aspect.annotation.Dict;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotBlank;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
@@ -83,9 +84,10 @@ public class BgXiSpeak implements Serializable {
@Schema(description = "学习传达研究部署情况")
private java.lang.String status;
/**密级*/
@Excel(name = "密级", width = 15, dicCode = "secret_level")
@Excel(name = "密级(请填入非秘/秘密/机密)-必填", width = 15, dicCode = "secret_level")
@Dict(dicCode = "secret_level")
@Schema(description = "密级")
@NotBlank
private java.lang.String secretLevel;
/**责任人*/
@Excel(name = "责任人", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
@@ -102,8 +104,9 @@ public class BgXiSpeak implements Serializable {
@Schema(description = "牵头部门")
private java.lang.String leadDepartment;
/**贯彻落实措施*/
@Excel(name = "贯彻落实措施", width = 15)
@Excel(name = "贯彻落实措施-必填", width = 15)
@Schema(description = "贯彻落实措施")
@NotBlank
private java.lang.String implMeasures;
/**会议决策数*/
@Excel(name = "会议决策数", width = 15)
@@ -126,7 +129,7 @@ public class BgXiSpeak implements Serializable {
@Schema(description = "报告反馈情况")
private java.lang.String reportFeedbackStatus;
/**是否存在完成风险(0不存在,1存在)*/
@Excel(name = "是否存在完成风险(0不存在,1存在)", width = 15, dicCode = "true_or_flase")
@Excel(name = "是否存在完成风险(请填入是/否)", width = 15, dicCode = "true_or_flase")
@Schema(description = "是否存在完成风险(0不存在,1存在)")
private java.lang.Integer isCompletionRisk;
/**拖期风险应对措施*/
@@ -134,7 +137,7 @@ public class BgXiSpeak implements Serializable {
@Schema(description = "拖期风险应对措施")
private java.lang.String delayRiskMitigation;
/**完成状态(0推进中,1已完成)*/
@Excel(name = "完成状态(0推进中1已完成)", width = 15, dicCode = "db_status")
@Excel(name = "完成状态(请填入推进中/已完成)-必填", width = 15, dicCode = "db_status")
@Schema(description = "完成状态(0推进中,1已完成)")
@Dict( dicCode = "db_status")
private java.lang.Integer completionStatus;
@@ -170,7 +173,7 @@ public class BgXiSpeak implements Serializable {
@Schema(description = "领导userId字段")
private java.lang.String sdwLeaderList;
/**dw领导userId字段*/
@Excel(name = "是否需要sdw审批", width = 15)
@Excel(name = "是否需要sdw审批(请填入是/否)", width = 15,dicCode = "true_or_flase")
@Schema(description = "是否需要sdw审批")
private java.lang.Integer needSdwApproval;
@@ -282,6 +282,15 @@ public class DjInspectImproveController extends JeecgController<DjInspectImprove
return super.exportXls(request, djInspectImprove, DjInspectImprove.class, "巡视整改台账");
}
/**
* 仅导出Excel表头(EasyExcel
*/
@RequiresPermissions("dj.inspectimprove:dj_inspect_improve:exportXls")
@GetMapping(value = "/exportXlsHeaders")
public void exportXlsHeaders(HttpServletResponse response) throws IOException {
super.exportXlsHeadersByEasyExcel(response, DjInspectImprove.class, "巡视整改台账");
}
/**
* 通过excel导入数据
*
+8
View File
@@ -105,6 +105,7 @@
<qiniu-uapp-sdk.version>7.4.0</qiniu-uapp-sdk.version>
<jsoup.version>1.11.3</jsoup.version>
<esayexcel.version>4.0.3</esayexcel.version>
</properties>
@@ -346,6 +347,13 @@
<version>${dom4j.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${esayexcel.version}</version>
<scope>compile</scope>
</dependency>
<!-- hutool工具类
<dependency>
<groupId>cn.hutool</groupId>