diff --git a/jeecg-boot-base-core/pom.xml b/jeecg-boot-base-core/pom.xml index 3fbe879..94bba34 100644 --- a/jeecg-boot-base-core/pom.xml +++ b/jeecg-boot-base-core/pom.xml @@ -327,5 +327,14 @@ org.jeecgframework.boot jeecg-boot-starter-chatgpt + + org.springframework.boot + spring-boot-starter-validation + + + com.alibaba + easyexcel + compile + diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java index f3ff51e..0e42e8a 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java @@ -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> { - /**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> { String selections = request.getParameter("selections"); if (oConvertUtils.isNotEmpty(selections)) { List selectionList = Arrays.asList(selections.split(",")); - queryWrapper.in("id",selectionList); + queryWrapper.in("id", selectionList); } // Step.2 获取导出数据 List exportList = service.list(queryWrapper); @@ -68,53 +75,54 @@ public class JeecgController> { 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 clazz, String title,String exportFields,Integer pageNum) { + protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class clazz, String title, String exportFields, Integer pageNum) { // Step.1 组装查询条件 QueryWrapper 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 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> listMap = new ArrayList>(); - for (int i = 1; i <=count ; i++) { + for (int i = 1; i <= count; i++) { Page page = new Page(i, pageNum); IPage pageList = service.page(page, queryWrapper); List exportList = pageList.getRecords(); Map 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> { * * @param request */ - protected ModelAndView exportXls(HttpServletRequest request, T object, Class 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 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 clazz, String title) throws IOException { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap()); + + String selections = request.getParameter("selections"); + if (oConvertUtils.isNotEmpty(selections)) { + List selectionList = Arrays.asList(selections.split(",")); + queryWrapper.in("id", selectionList); + } + + List exportList = service.list(queryWrapper); + writeExcel(response, clazz, title, exportList); + } + + /** + * 仅导出Excel表头(不含数据),基于 @ExcelProperty 注解读取列配置 + */ + protected void exportXlsHeadersByEasyExcel(HttpServletResponse response, + Class clazz, String title) throws IOException { + writeExcel(response, clazz, title, Collections.emptyList()); + } + + private void writeExcel(HttpServletResponse response, Class clazz, + String title, List data) throws IOException { + ExcelAnnotationUtils.write(response, clazz, title, data); + } + /** * 获取对象ID * @@ -153,6 +191,42 @@ public class JeecgController> { } } + /** + * 通过EasyExcel导入数据,基于 @Excel 注解读取列配置 + */ + protected Result importExcelByEasyExcel(HttpServletRequest request, Class clazz) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map 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 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> { //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: 导入数据重复增加提示 diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ColumnMeta.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ColumnMeta.java new file mode 100644 index 0000000..1163f26 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ColumnMeta.java @@ -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; } +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ColumnWidthHandler.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ColumnWidthHandler.java new file mode 100644 index 0000000..1f92d7b --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ColumnWidthHandler.java @@ -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 columns; + + public ColumnWidthHandler(List columns) { + this.columns = columns; + } + + @Override + protected void setColumnWidth(WriteSheetHolder writeSheetHolder, + List> 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)); + } + } +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelAnnotationUtils.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelAnnotationUtils.java new file mode 100644 index 0000000..14568d8 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelAnnotationUtils.java @@ -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 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 parseColumns(Class clazz) { + List 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> buildHead(List columns) { + boolean hasGroup = columns.stream().anyMatch(c -> StringUtils.isNotEmpty(c.getGroupName())); + List> 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> convertData(List columns, List dataList) { + List> rows = new ArrayList<>(dataList.size()); + for (Object row : dataList) { + List 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 columns = parseColumns(clazz); + List> 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 int read(InputStream inputStream, Class clazz, + Consumer> batchHandler, int batchSize, + Consumer> errorHandler) { + + // 1. 职责一:解析注解元数据,准备基础映射表 + List columns = parseColumns(clazz); + + Map nameToCol = new LinkedHashMap<>(); + Map displayToCode = new LinkedHashMap<>(); + + for (ColumnMeta col : columns) { + nameToCol.put(col.getName(), col); + buildReverseReplace(col.getReplace(), displayToCode); + } + + // 2. 职责二:实例化专属的流式解析校验监听器 + ExcelImportListener 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 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 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 getAllFields(Class clazz) { + List fields = new ArrayList<>(); + Class current = clazz; + while (current != null && current != Object.class) { + Collections.addAll(fields, current.getDeclaredFields()); + current = current.getSuperclass(); + } + return fields; + } +} \ No newline at end of file diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelDataListener.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelDataListener.java new file mode 100644 index 0000000..c584f93 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelDataListener.java @@ -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 extends AnalysisEventListener { + private static final int BATCH_COUNT = 1000; + private final List cachedDataList = new ArrayList<>(BATCH_COUNT); + private final IService baseService; + private int totalCount = 0; + + public ExcelDataListener(IService 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; + } +} \ No newline at end of file diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelImportListener.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelImportListener.java new file mode 100644 index 0000000..0af665e --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/excel/ExcelImportListener.java @@ -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 implements ReadListener> { + + private final Class clazz; + private final int batchSize; + private final Consumer> batchHandler; + private final Consumer> errorHandler; + + // 外部传入的数据支撑 + private final Map nameToCol; + private final Map displayToCode; + + // 内部状态机 + private final List batch; + private final List errorBatch; + private final Map columnOrder = new LinkedHashMap<>(); + private final Map 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 clazz, int batchSize, + Map nameToCol, Map displayToCode, + Consumer> batchHandler, Consumer> 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> headMap, AnalysisContext context) { + if (headRowIndex[0] >= 0) return; + + for (Map.Entry> 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 data, AnalysisContext context) { + if (context.readRowHolder().getRowIndex() == headRowIndex[0]) return; + int rowIndex = context.readRowHolder().getRowIndex(); + + try { + T obj = clazz.getDeclaredConstructor().newInstance(); + List rowErrors = new ArrayList<>(); + + // 1. 字典校验 + 安全转换赋值 + for (Map.Entry 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> violations = currentValidator.validate(obj); + if (!violations.isEmpty()) { + for (ConstraintViolation 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 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]; + } +} \ No newline at end of file diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java index fecc1df..0e6fce4 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java @@ -373,6 +373,26 @@ public class BgXiSpeakController extends JeecgController 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 节点下的反馈统计数量 * diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/BgXiSpeak.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/BgXiSpeak.java index a672fe2..962940d 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/BgXiSpeak.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/BgXiSpeak.java @@ -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; diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/dj/inspectimprove/controller/DjInspectImproveController.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/dj/inspectimprove/controller/DjInspectImproveController.java index 4d13b14..c85eba2 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/dj/inspectimprove/controller/DjInspectImproveController.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/dj/inspectimprove/controller/DjInspectImproveController.java @@ -282,6 +282,15 @@ public class DjInspectImproveController extends JeecgController7.4.0 1.11.3 + 4.0.3 @@ -346,6 +347,13 @@ ${dom4j.version} + + com.alibaba + easyexcel + ${esayexcel.version} + compile + +