!53 refactor(excel): 提取Excel工具包为独立模块jeecg-module-tools,支持跨项目复用
* refactor(excel): 提取Excel工具包为独立模块jeecg-module-tools,支持跨项目复用 * fix(excel): 校验错误详情返回前端,ColumnMeta重构为Record,避免字典错后重复报NotBlank * Merge remote-tracking branch 'origin/master' * chore: 添加easyexcel依赖
This commit is contained in:
@@ -44,6 +44,12 @@
|
|||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-common</artifactId>
|
<artifactId>jeecg-boot-common</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- excel 独立工具包 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
|
<artifactId>jeecg-module-tools</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
<!--集成springmvc框架并实现自动配置 -->
|
<!--集成springmvc框架并实现自动配置 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
+13
-3
@@ -18,7 +18,9 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
|
|||||||
import org.jeecgframework.poi.excel.entity.ImportParams;
|
import org.jeecgframework.poi.excel.entity.ImportParams;
|
||||||
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||||
|
import org.jeecg.common.util.excel.DictQuery;
|
||||||
import org.jeecg.common.util.excel.ExcelAnnotationUtils;
|
import org.jeecg.common.util.excel.ExcelAnnotationUtils;
|
||||||
|
import org.jeecgframework.dict.service.AutoPoiDictServiceI;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -47,6 +49,8 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
protected S service;
|
protected S service;
|
||||||
@Resource
|
@Resource
|
||||||
private JeecgBaseConfig jeecgBaseConfig;
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
@Autowired(required = false)
|
||||||
|
private AutoPoiDictServiceI dictService;
|
||||||
|
|
||||||
private final int BATCH_NUM = 500;
|
private final int BATCH_NUM = 500;
|
||||||
|
|
||||||
@@ -174,7 +178,12 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
|
|
||||||
private void writeExcel(HttpServletResponse response, Class<T> clazz,
|
private void writeExcel(HttpServletResponse response, Class<T> clazz,
|
||||||
String title, List<T> data) throws IOException {
|
String title, List<T> data) throws IOException {
|
||||||
ExcelAnnotationUtils.write(response, clazz, title, data);
|
ExcelAnnotationUtils.write(response, clazz, title, data, dictQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DictQuery dictQuery() {
|
||||||
|
return dictService == null ? null
|
||||||
|
: (dictTable, dicCode, dicText) -> dictService.queryDict(dictTable, dicCode, dicText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -209,13 +218,14 @@ public class JeecgController<T, S extends IService<T>> {
|
|||||||
clazz,
|
clazz,
|
||||||
batch -> service.saveBatch(batch),
|
batch -> service.saveBatch(batch),
|
||||||
BATCH_NUM,
|
BATCH_NUM,
|
||||||
errors::addAll);
|
errors::addAll,
|
||||||
|
dictQuery());
|
||||||
log.info("【EasyExcel导入】导入完成, 数据行数: {}, 耗时: {}毫秒", total, System.currentTimeMillis() - start);
|
log.info("【EasyExcel导入】导入完成, 数据行数: {}, 耗时: {}毫秒", total, System.currentTimeMillis() - start);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
for (ExcelAnnotationUtils.ValidationError err : errors) {
|
for (ExcelAnnotationUtils.ValidationError err : errors) {
|
||||||
log.error("【EasyExcel导入】校验失败, 行号: {}, 错误: {}", err.row(), err.messages());
|
log.error("【EasyExcel导入】校验失败, 行号: {}, 错误: {}", err.row(), err.messages());
|
||||||
}
|
}
|
||||||
return Result.error("文件导入失败: 存在" + errors.size() + "行数据校验不通过,请修改后重新导入");
|
return Result.error("文件导入失败: 存在" + errors.size() + "行数据校验不通过,请修改后重新导入", errors);
|
||||||
}
|
}
|
||||||
return Result.ok("文件导入成功!数据行数:" + total);
|
return Result.ok("文件导入成功!数据行数:" + total);
|
||||||
} catch (DuplicateKeyException e) {
|
} catch (DuplicateKeyException e) {
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
|
<artifactId>jeecg-module-tools</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>jeecg-module-tools</name>
|
||||||
|
<description>独立 Excel 导入导出工具包 — @ExcelColumn + DictQuery + EasyExcel,零框架耦合</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<easyexcel.version>4.0.3</easyexcel.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- EasyExcel 核心 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>easyexcel</artifactId>
|
||||||
|
<version>${easyexcel.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 字符串工具 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
<version>3.12.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Bean Validation API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.validation</groupId>
|
||||||
|
<artifactId>validation-api</artifactId>
|
||||||
|
<version>2.0.1.Final</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Hibernate Validator(运行时的 JSR-303 实现) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate.validator</groupId>
|
||||||
|
<artifactId>hibernate-validator</artifactId>
|
||||||
|
<version>6.2.5.Final</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Servlet API(仅导出 write() 方法需要) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
<version>4.0.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.30</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package org.jeecg.common.util.excel;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Excel 注解解析后的列元数据
|
||||||
|
*/
|
||||||
|
public record 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) {
|
||||||
|
}
|
||||||
+2
-2
@@ -34,10 +34,10 @@ public class ColumnWidthHandler extends AbstractColumnWidthStyleStrategy {
|
|||||||
}
|
}
|
||||||
ColumnMeta meta = columns.get(columnIndex);
|
ColumnMeta meta = columns.get(columnIndex);
|
||||||
Sheet sheet = cell.getSheet();
|
Sheet sheet = cell.getSheet();
|
||||||
if (meta.isColumnHidden()) {
|
if (meta.columnHidden()) {
|
||||||
sheet.setColumnHidden(columnIndex, true);
|
sheet.setColumnHidden(columnIndex, true);
|
||||||
} else {
|
} else {
|
||||||
sheet.setColumnWidth(columnIndex, (int) ((meta.getWidth() + 2) * 256));
|
sheet.setColumnWidth(columnIndex, (int) ((meta.width() + 2) * 256));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package org.jeecg.common.util.excel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典查询接口,由调用方实现并注入,
|
||||||
|
* 用于 Excel 导入导出时的字典翻译(如 sex=0 → 男)。
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface DictQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询字典键值对映射
|
||||||
|
*
|
||||||
|
* @param dictTable 字典表名(来自 @ExcelColumn.dictTable())
|
||||||
|
* @param dicCode 字典编码字段(来自 @ExcelColumn.dicCode())
|
||||||
|
* @param dicText 字典文本字段(来自 @ExcelColumn.dicText())
|
||||||
|
* @return 格式为 {"text_code", ...} 的数组,如 {"男_0", "女_1"};无结果返回 null 或空数组
|
||||||
|
*/
|
||||||
|
String[] query(String dictTable, String dicCode, String dicText);
|
||||||
|
}
|
||||||
+174
-179
@@ -1,20 +1,11 @@
|
|||||||
package org.jeecg.common.util.excel;
|
package org.jeecg.common.util.excel;
|
||||||
|
|
||||||
import com.alibaba.excel.EasyExcel;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
import org.jeecg.common.util.excel.annotation.ExcelColumn;
|
||||||
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.servlet.http.HttpServletResponse;
|
||||||
import javax.validation.ConstraintViolation;
|
|
||||||
import javax.validation.Validation;
|
import javax.validation.Validation;
|
||||||
import javax.validation.Validator;
|
import javax.validation.Validator;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -32,65 +23,70 @@ import java.util.*;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Excel 注解工具类,用于 EasyExcel 导入导出。
|
* @ExcelColumn 注解工具类,用于 EasyExcel 导入导出。
|
||||||
* 从实体类字段上读取 @Excel 注解,构建 EasyExcel 表头和数据,无需 @ExcelProperty。
|
* 从实体类字段上读取 @ExcelColumn 注解,构建 EasyExcel 表头和数据,无需 @ExcelProperty。
|
||||||
|
* <p>
|
||||||
|
* 已解耦 jeecgframework,可独立打包复用。调用方通过 {@link DictQuery} 注入字典服务。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
|
||||||
public final class ExcelAnnotationUtils {
|
public final class ExcelAnnotationUtils {
|
||||||
|
|
||||||
private static volatile ExcelAnnotationUtils instance;
|
private static volatile Validator cachedValidator;
|
||||||
private final Validator validator;
|
|
||||||
|
private ExcelAnnotationUtils() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验错误信息载体:行号 + 错误消息列表(现代 Java Record,1行搞定)
|
* 校验错误信息载体:行号 + 错误消息列表
|
||||||
*/
|
*/
|
||||||
public record ValidationError(int row, List<String> messages) {}
|
public record ValidationError(int row, List<String> messages) {}
|
||||||
|
|
||||||
@Autowired
|
// ==================== Validator ====================
|
||||||
public ExcelAnnotationUtils(Validator validator) {
|
|
||||||
this.validator = validator;
|
|
||||||
ExcelAnnotationUtils.instance = this;
|
|
||||||
log.info("【Excel导入】Spring Validator 成功通过构造器桥接到静态上下文");
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExcelAnnotationUtils(Validator validator, boolean isBackup) {
|
|
||||||
this.validator = validator;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Validator 实例(懒加载缓存)。
|
||||||
|
* 若调用方未通过 {@link #setValidator(Validator)} 注入,则自动创建纯 Java Validator。
|
||||||
|
*/
|
||||||
public static Validator getValidator() {
|
public static Validator getValidator() {
|
||||||
if (instance != null && instance.validator != null) {
|
if (cachedValidator != null) {
|
||||||
return instance.validator;
|
return cachedValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (ExcelAnnotationUtils.class) {
|
synchronized (ExcelAnnotationUtils.class) {
|
||||||
if (instance == null) {
|
if (cachedValidator == null) {
|
||||||
log.warn("【Excel导入】未检测到已注入的 Spring 实例,正在启用原生纯 Java Validator 作为安全保底!");
|
cachedValidator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||||
Validator tempValidator = Validation.buildDefaultValidatorFactory().getValidator();
|
log.info("【Excel工具】已创建默认 Validator(非 Spring 模式)");
|
||||||
instance = new ExcelAnnotationUtils(tempValidator, true);
|
|
||||||
return tempValidator;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return instance.validator;
|
return cachedValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 Class 上带 @Excel 注解的字段,按 orderNum 排序返回列元数据
|
* 注入自定义 Validator(如 Spring 管理的 Validator),需在首次调用前设置。
|
||||||
*/
|
*/
|
||||||
public static List<ColumnMeta> parseColumns(Class<?> clazz) {
|
public static void setValidator(Validator validator) {
|
||||||
|
cachedValidator = validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 注解解析 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Class 上带 @ExcelColumn 注解的字段,按 orderNum 排序返回列元数据
|
||||||
|
*
|
||||||
|
* @param clazz 实体类
|
||||||
|
* @param dictQuery 字典查询接口,可为 null(不启用字典翻译)
|
||||||
|
*/
|
||||||
|
public static List<ColumnMeta> parseColumns(Class<?> clazz, DictQuery dictQuery) {
|
||||||
List<ColumnMeta> columns = new ArrayList<>();
|
List<ColumnMeta> columns = new ArrayList<>();
|
||||||
AutoPoiDictServiceI dictService = getDictService();
|
|
||||||
|
|
||||||
for (Field field : getAllFields(clazz)) {
|
for (Field field : getAllFields(clazz)) {
|
||||||
Excel excel = field.getAnnotation(Excel.class);
|
ExcelColumn col = field.getAnnotation(ExcelColumn.class);
|
||||||
if (excel == null) {
|
if (col == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
|
|
||||||
String[] replace = excel.replace();
|
String[] replace = col.replace();
|
||||||
if (dictService != null && (StringUtils.isNotEmpty(excel.dictTable()) || StringUtils.isNotEmpty(excel.dicCode()))) {
|
if (dictQuery != null && (StringUtils.isNotEmpty(col.dictTable()) || StringUtils.isNotEmpty(col.dicCode()))) {
|
||||||
String[] dictReplace = dictService.queryDict(excel.dictTable(), excel.dicCode(), excel.dicText());
|
String[] dictReplace = dictQuery.query(col.dictTable(), col.dicCode(), col.dicText());
|
||||||
if (dictReplace != null && dictReplace.length > 0) {
|
if (dictReplace != null && dictReplace.length > 0) {
|
||||||
replace = mergeReplace(replace, dictReplace);
|
replace = mergeReplace(replace, dictReplace);
|
||||||
}
|
}
|
||||||
@@ -98,41 +94,43 @@ public final class ExcelAnnotationUtils {
|
|||||||
|
|
||||||
columns.add(new ColumnMeta(
|
columns.add(new ColumnMeta(
|
||||||
field,
|
field,
|
||||||
excel.name(),
|
col.name(),
|
||||||
excel.width(),
|
col.width(),
|
||||||
StringUtils.isNotEmpty(excel.exportFormat()) ? excel.exportFormat() : excel.format(),
|
StringUtils.isNotEmpty(col.exportFormat()) ? col.exportFormat() : col.format(),
|
||||||
replace,
|
replace,
|
||||||
excel.multiReplace(),
|
col.multiReplace(),
|
||||||
excel.numFormat(),
|
col.numFormat(),
|
||||||
excel.groupName(),
|
col.groupName(),
|
||||||
parseOrderNum(excel.orderNum()),
|
parseOrderNum(col.orderNum()),
|
||||||
excel.suffix(),
|
col.suffix(),
|
||||||
excel.databaseFormat(),
|
col.databaseFormat(),
|
||||||
excel.isColumnHidden()
|
col.hidden()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
columns.sort(Comparator.comparingInt(ColumnMeta::getOrderNum));
|
columns.sort(Comparator.comparingInt(ColumnMeta::orderNum));
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 导出 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建 EasyExcel 多级表头,支持 @Excel.groupName()
|
* 构建 EasyExcel 多级表头,支持 @ExcelColumn.groupName()
|
||||||
*/
|
*/
|
||||||
public static List<List<String>> buildHead(List<ColumnMeta> columns) {
|
public static List<List<String>> buildHead(List<ColumnMeta> columns) {
|
||||||
boolean hasGroup = columns.stream().anyMatch(c -> StringUtils.isNotEmpty(c.getGroupName()));
|
boolean hasGroup = columns.stream().anyMatch(c -> StringUtils.isNotEmpty(c.groupName()));
|
||||||
List<List<String>> head = new ArrayList<>();
|
List<List<String>> head = new ArrayList<>();
|
||||||
for (ColumnMeta col : columns) {
|
for (ColumnMeta col : columns) {
|
||||||
if (hasGroup && StringUtils.isNotEmpty(col.getGroupName())) {
|
if (hasGroup && StringUtils.isNotEmpty(col.groupName())) {
|
||||||
head.add(Arrays.asList(col.getGroupName(), col.getName()));
|
head.add(Arrays.asList(col.groupName(), col.name()));
|
||||||
} else {
|
} else {
|
||||||
head.add(Collections.singletonList(col.getName()));
|
head.add(Collections.singletonList(col.name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return head;
|
return head;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将数据列表按 ColumnMeta 转换为 EasyExcel 的行数据,应用字典翻译、日期格式化、值替换等
|
* 将数据列表按 ColumnMeta 转换为 EasyExcel 的行数据
|
||||||
*/
|
*/
|
||||||
public static List<List<Object>> convertData(List<ColumnMeta> columns, List<?> dataList) {
|
public static List<List<Object>> convertData(List<ColumnMeta> columns, List<?> dataList) {
|
||||||
List<List<Object>> rows = new ArrayList<>(dataList.size());
|
List<List<Object>> rows = new ArrayList<>(dataList.size());
|
||||||
@@ -148,10 +146,12 @@ public final class ExcelAnnotationUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 写出 Excel 的公共入口:设置响应头 + 构建表头 + 写数据
|
* 写出 Excel 的公共入口:设置响应头 + 构建表头 + 写数据
|
||||||
|
*
|
||||||
|
* @param dictQuery 字典查询接口,可为 null
|
||||||
*/
|
*/
|
||||||
public static void write(HttpServletResponse response, Class<?> clazz,
|
public static void write(HttpServletResponse response, Class<?> clazz,
|
||||||
String title, List<?> data) throws IOException {
|
String title, List<?> data, DictQuery dictQuery) throws IOException {
|
||||||
List<ColumnMeta> columns = parseColumns(clazz);
|
List<ColumnMeta> columns = parseColumns(clazz, dictQuery);
|
||||||
List<List<String>> head = buildHead(columns);
|
List<List<String>> head = buildHead(columns);
|
||||||
|
|
||||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
@@ -161,60 +161,152 @@ public final class ExcelAnnotationUtils {
|
|||||||
|
|
||||||
EasyExcel.write(response.getOutputStream())
|
EasyExcel.write(response.getOutputStream())
|
||||||
.head(head)
|
.head(head)
|
||||||
.registerWriteHandler(new ColumnWidthHandler(columns)) // 这里保持自适应宽度
|
.registerWriteHandler(new ColumnWidthHandler(columns))
|
||||||
.sheet(title)
|
.sheet(title)
|
||||||
.doWrite(convertData(columns, data));
|
.doWrite(convertData(columns, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 私有辅助方法 ----
|
// ==================== 导入 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Excel 输入流分批导入数据,基于 @ExcelColumn 注解读取列配置。
|
||||||
|
*
|
||||||
|
* @param dictQuery 字典查询接口,可为 null(不启用字典翻译)
|
||||||
|
*/
|
||||||
|
public static <T> int read(InputStream inputStream, Class<T> clazz,
|
||||||
|
Consumer<List<T>> batchHandler, int batchSize,
|
||||||
|
Consumer<List<ValidationError>> errorHandler,
|
||||||
|
DictQuery dictQuery) {
|
||||||
|
|
||||||
|
List<ColumnMeta> columns = parseColumns(clazz, dictQuery);
|
||||||
|
|
||||||
|
Map<String, ColumnMeta> nameToCol = new LinkedHashMap<>();
|
||||||
|
Map<String, String> displayToCode = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (ColumnMeta col : columns) {
|
||||||
|
nameToCol.put(col.name(), col);
|
||||||
|
buildReverseReplace(col.replace(), displayToCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExcelImportListener<T> listener = new ExcelImportListener<>(
|
||||||
|
clazz, batchSize, nameToCol, displayToCode, batchHandler, errorHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
EasyExcel.read(inputStream, listener).sheet().doRead();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setFieldValue(ColumnMeta col, Object obj, String value,
|
||||||
|
Map<String, String> displayToCode) throws Exception {
|
||||||
|
Field field = col.field();
|
||||||
|
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.databaseFormat()) ? col.databaseFormat() : col.format();
|
||||||
|
if (StringUtils.isNotEmpty(format)) {
|
||||||
|
field.set(obj, new SimpleDateFormat(format).parse(resolved));
|
||||||
|
}
|
||||||
|
} else if (type == LocalDateTime.class) {
|
||||||
|
String format = col.format();
|
||||||
|
if (StringUtils.isNotEmpty(format)) {
|
||||||
|
field.set(obj, LocalDateTime.parse(resolved, DateTimeFormatter.ofPattern(format)));
|
||||||
|
}
|
||||||
|
} else if (type == LocalDate.class) {
|
||||||
|
String format = col.format();
|
||||||
|
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 Object getCellValue(ColumnMeta col, Object obj) {
|
private static Object getCellValue(ColumnMeta col, Object obj) {
|
||||||
try {
|
try {
|
||||||
Object value = col.getField().get(obj);
|
Object value = col.field().get(obj);
|
||||||
if (value == null || "".equals(value)) {
|
if (value == null || "".equals(value)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotEmpty(col.getFormat())) {
|
if (StringUtils.isNotEmpty(col.format())) {
|
||||||
value = formatDate(value, col);
|
value = formatDate(value, col);
|
||||||
}
|
}
|
||||||
|
|
||||||
String strValue = value.toString();
|
String strValue = value.toString();
|
||||||
|
|
||||||
if (col.getReplace() != null && col.getReplace().length > 0) {
|
if (col.replace() != null && col.replace().length > 0) {
|
||||||
strValue = col.isMultiReplace()
|
strValue = col.multiReplace()
|
||||||
? multiReplaceValue(col.getReplace(), strValue)
|
? multiReplaceValue(col.replace(), strValue)
|
||||||
: replaceValue(col.getReplace(), strValue);
|
: replaceValue(col.replace(), strValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotEmpty(col.getNumFormat()) && value instanceof Number) {
|
if (StringUtils.isNotEmpty(col.numFormat()) && value instanceof Number) {
|
||||||
strValue = new DecimalFormat(col.getNumFormat()).format(value);
|
strValue = new DecimalFormat(col.numFormat()).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotEmpty(col.getSuffix())) {
|
if (StringUtils.isNotEmpty(col.suffix())) {
|
||||||
strValue = strValue + col.getSuffix();
|
strValue = strValue + col.suffix();
|
||||||
}
|
}
|
||||||
|
|
||||||
return strValue;
|
return strValue;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取单元格值失败, field: {}, row: {}", col.getField().getName(), obj, e);
|
log.error("获取单元格值失败, field: {}, row: {}", col.field().getName(), obj, e);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Object formatDate(Object value, ColumnMeta col) throws Exception {
|
private static Object formatDate(Object value, ColumnMeta col) throws Exception {
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return new SimpleDateFormat(col.getFormat()).format((Date) value);
|
return new SimpleDateFormat(col.format()).format((Date) value);
|
||||||
}
|
}
|
||||||
if (value instanceof LocalDateTime) {
|
if (value instanceof LocalDateTime) {
|
||||||
return ((LocalDateTime) value).format(DateTimeFormatter.ofPattern(col.getFormat()));
|
return ((LocalDateTime) value).format(DateTimeFormatter.ofPattern(col.format()));
|
||||||
}
|
}
|
||||||
if (value instanceof LocalDate) {
|
if (value instanceof LocalDate) {
|
||||||
return ((LocalDate) value).format(DateTimeFormatter.ofPattern(col.getFormat()));
|
return ((LocalDate) value).format(DateTimeFormatter.ofPattern(col.format()));
|
||||||
}
|
}
|
||||||
if (value instanceof String && StringUtils.isNotEmpty(col.getDatabaseFormat())) {
|
if (value instanceof String && StringUtils.isNotEmpty(col.databaseFormat())) {
|
||||||
Date parsed = new SimpleDateFormat(col.getDatabaseFormat()).parse((String) value);
|
Date parsed = new SimpleDateFormat(col.databaseFormat()).parse((String) value);
|
||||||
return new SimpleDateFormat(col.getFormat()).format(parsed);
|
return new SimpleDateFormat(col.format()).format(parsed);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -255,54 +347,6 @@ public final class ExcelAnnotationUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
private static void buildReverseReplace(String[] replace, Map<String, String> displayToCode) {
|
||||||
if (replace == null || replace.length == 0) {
|
if (replace == null || replace.length == 0) {
|
||||||
return;
|
return;
|
||||||
@@ -315,55 +359,6 @@ public final class ExcelAnnotationUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
private static List<Field> getAllFields(Class<?> clazz) {
|
||||||
List<Field> fields = new ArrayList<>();
|
List<Field> fields = new ArrayList<>();
|
||||||
Class<?> current = clazz;
|
Class<?> current = clazz;
|
||||||
@@ -373,4 +368,4 @@ public final class ExcelAnnotationUtils {
|
|||||||
}
|
}
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+20
-7
@@ -68,8 +68,8 @@ public class ExcelImportListener<T> implements ReadListener<Map<Integer, String>
|
|||||||
|
|
||||||
// 缓存 Java 属性名 -> Excel 表头列名的转换映射
|
// 缓存 Java 属性名 -> Excel 表头列名的转换映射
|
||||||
for (ColumnMeta cm : columnOrder.values()) {
|
for (ColumnMeta cm : columnOrder.values()) {
|
||||||
if (cm.getField() != null) {
|
if (cm.field() != null) {
|
||||||
fieldToColName.put(cm.getField().getName(), cm.getName());
|
fieldToColName.put(cm.field().getName(), cm.name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,22 +82,29 @@ public class ExcelImportListener<T> implements ReadListener<Map<Integer, String>
|
|||||||
try {
|
try {
|
||||||
T obj = clazz.getDeclaredConstructor().newInstance();
|
T obj = clazz.getDeclaredConstructor().newInstance();
|
||||||
List<String> rowErrors = new ArrayList<>();
|
List<String> rowErrors = new ArrayList<>();
|
||||||
|
// 🆕 新增:用来记录哪些 Java 属性名已经发生了字典错误
|
||||||
|
Set<String> dictFailedFields = new HashSet<>();
|
||||||
|
|
||||||
// 1. 字典校验 + 安全转换赋值
|
// 1. 赋值与字典校验
|
||||||
for (Map.Entry<Integer, String> entry : data.entrySet()) {
|
for (Map.Entry<Integer, String> entry : data.entrySet()) {
|
||||||
ColumnMeta col = columnOrder.get(entry.getKey());
|
ColumnMeta col = columnOrder.get(entry.getKey());
|
||||||
String cellValue = entry.getValue();
|
String cellValue = entry.getValue();
|
||||||
|
|
||||||
if (col == null || StringUtils.isEmpty(cellValue)) continue;
|
if (col == null || StringUtils.isEmpty(cellValue)) continue;
|
||||||
|
|
||||||
String[] replace = col.getReplace();
|
// 获取该列对应的 Java 属性名(字段名),例如 "gender"
|
||||||
|
String propertyName = col.field().getName();
|
||||||
|
|
||||||
|
String[] replace = col.replace();
|
||||||
if (replace != null && replace.length > 0) {
|
if (replace != null && replace.length > 0) {
|
||||||
if (!ExcelAnnotationUtils.isValidDictDisplay(replace, cellValue)) {
|
if (!ExcelAnnotationUtils.isValidDictDisplay(replace, cellValue)) {
|
||||||
rowErrors.add("【" + col.getName() + "】的值'" + cellValue + "'不在字典范围内");
|
rowErrors.add("【" + col.name() + "】的值'" + cellValue + "'不在字典范围内");
|
||||||
continue;
|
dictFailedFields.add(propertyName); // 🆕 标记这个字段字典错漏
|
||||||
|
continue; // 允许 continue,因为字典错的值无法安全转换
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 字典通过,或者不需要字典校验的,正常赋值
|
||||||
ExcelAnnotationUtils.setFieldValue(col, obj, cellValue, displayToCode);
|
ExcelAnnotationUtils.setFieldValue(col, obj, cellValue, displayToCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +115,12 @@ public class ExcelImportListener<T> implements ReadListener<Map<Integer, String>
|
|||||||
if (!violations.isEmpty()) {
|
if (!violations.isEmpty()) {
|
||||||
for (ConstraintViolation<T> v : violations) {
|
for (ConstraintViolation<T> v : violations) {
|
||||||
String propertyName = v.getPropertyPath().toString();
|
String propertyName = v.getPropertyPath().toString();
|
||||||
|
|
||||||
|
// 🆕 核心拦截:如果这个字段刚才已经报了“字典错误”,就别再报“不能为空”了
|
||||||
|
if (dictFailedFields.contains(propertyName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
String label = fieldToColName.getOrDefault(propertyName, propertyName);
|
String label = fieldToColName.getOrDefault(propertyName, propertyName);
|
||||||
rowErrors.add("【" + label + "】" + v.getMessage());
|
rowErrors.add("【" + label + "】" + v.getMessage());
|
||||||
}
|
}
|
||||||
@@ -130,7 +143,7 @@ public class ExcelImportListener<T> implements ReadListener<Map<Integer, String>
|
|||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
skipCount[0]++;
|
skipCount[0]++;
|
||||||
log.error("【Excel导入】行数据转换发生系统异常, row: {}", rowIndex, e);
|
log.error("【Excel导入】行数据转换发生 system 异常, row: {}", rowIndex, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package org.jeecg.common.util.excel.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excel 导入导出列注解,替代 jeecgframework 的 @Excel,
|
||||||
|
* 使 excel 工具包可独立打包复用。
|
||||||
|
*/
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ExcelColumn {
|
||||||
|
|
||||||
|
/** 列名(表头),必填 */
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/** 列宽 */
|
||||||
|
double width() default 15;
|
||||||
|
|
||||||
|
/** 日期显示格式(如 yyyy-MM-dd),导出时格式化 Date/LocalDate/LocalDateTime */
|
||||||
|
String format() default "";
|
||||||
|
|
||||||
|
/** 导出专用日期格式,优先于 format */
|
||||||
|
String exportFormat() default "";
|
||||||
|
|
||||||
|
/** 值替换映射(格式:text_code),如 {"男_1", "女_0"} */
|
||||||
|
String[] replace() default {};
|
||||||
|
|
||||||
|
/** 是否多值替换(逗号分隔的多个值分别替换) */
|
||||||
|
boolean multiReplace() default false;
|
||||||
|
|
||||||
|
/** 数字格式(DecimalFormat,如 #.##) */
|
||||||
|
String numFormat() default "";
|
||||||
|
|
||||||
|
/** 分组表头(支持多级表头) */
|
||||||
|
String groupName() default "";
|
||||||
|
|
||||||
|
/** 排序号(越小越靠前),字符串形式便于配置 */
|
||||||
|
String orderNum() default "";
|
||||||
|
|
||||||
|
/** 单元格后缀 */
|
||||||
|
String suffix() default "";
|
||||||
|
|
||||||
|
/** 数据库日期格式(导入时将字符串按此格式解析为 Date) */
|
||||||
|
String databaseFormat() default "";
|
||||||
|
|
||||||
|
/** 是否隐藏该列 */
|
||||||
|
boolean hidden() default false;
|
||||||
|
|
||||||
|
/** 字典表名(需搭配 DictQuery 使用) */
|
||||||
|
String dictTable() default "";
|
||||||
|
|
||||||
|
/** 字典编码字段 */
|
||||||
|
String dicCode() default "";
|
||||||
|
|
||||||
|
/** 字典文本字段 */
|
||||||
|
String dicText() default "";
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<module>jeecg-boot-module-wps</module>-->
|
<module>jeecg-boot-module-wps</module>-->
|
||||||
<module>jeecg-boot-module-airag</module>
|
<module>jeecg-boot-module-airag</module>
|
||||||
<module>jeecg-module-flow</module>
|
<module>jeecg-module-flow</module>
|
||||||
|
<module>jeecg-module-tools</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+55
-30
@@ -9,8 +9,9 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
|||||||
import org.jeecg.common.constant.ProvinceCityArea;
|
import org.jeecg.common.constant.ProvinceCityArea;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
|
||||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.jeecg.common.util.excel.annotation.ExcelColumn;
|
||||||
import org.jeecg.common.aspect.annotation.Dict;
|
import org.jeecg.common.aspect.annotation.Dict;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -57,128 +58,152 @@ public class BgXiSpeak implements Serializable {
|
|||||||
@Schema(description = "所属部门")
|
@Schema(description = "所属部门")
|
||||||
private java.lang.String sysOrgCode;
|
private java.lang.String sysOrgCode;
|
||||||
/**父级节点*/
|
/**父级节点*/
|
||||||
// @Excel(name = "父级节点", width = 15)
|
// @ExcelColumn(name = "父级节点", width = 15)
|
||||||
@Schema(description = "父级节点")
|
@Schema(description = "父级节点")
|
||||||
private java.lang.String pid;
|
private java.lang.String pid;
|
||||||
/**是否有子节点*/
|
/**是否有子节点*/
|
||||||
// @Excel(name = "是否有子节点", width = 15, dicCode = "yn")
|
// @ExcelColumn(name = "是否有子节点", width = 15, dicCode = "yn")
|
||||||
@Dict(dicCode = "yn")
|
@Dict(dicCode = "yn")
|
||||||
@Schema(description = "是否有子节点")
|
@Schema(description = "是否有子节点")
|
||||||
private java.lang.String hasChild;
|
private java.lang.String hasChild;
|
||||||
/**习近平总书记重要讲话指示批示情况*/
|
/**习近平总书记重要讲话指示批示情况*/
|
||||||
@Excel(name = "习近平总书记重要讲话指示批示情况", width = 15)
|
@Excel(name = "习近平总书记重要讲话指示批示情况", width = 15)
|
||||||
|
@ExcelColumn(name = "习近平总书记重要讲话指示批示情况", width = 15)
|
||||||
@Schema(description = "习近平总书记重要讲话指示批示情况")
|
@Schema(description = "习近平总书记重要讲话指示批示情况")
|
||||||
private java.lang.String speakStatus;
|
private java.lang.String speakStatus;
|
||||||
/**时间*/
|
/**时间*/
|
||||||
@Excel(name = "时间", width = 15, format = "yyyy-MM-dd")
|
@Excel(name = "时间", width = 15, format = "yyyy-MM-dd")
|
||||||
|
@ExcelColumn(name = "时间", width = 15, format = "yyyy-MM-dd")
|
||||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
|
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
|
||||||
@DateTimeFormat(pattern="yyyy-MM-dd")
|
@DateTimeFormat(pattern="yyyy-MM-dd")
|
||||||
@Schema(description = "时间")
|
@Schema(description = "时间")
|
||||||
private java.util.Date time;
|
private java.util.Date time;
|
||||||
/**企业负责同志批示情况*/
|
/**企业负责同志批示情况*/
|
||||||
@Excel(name = "企业负责同志批示情况", width = 15)
|
@Excel(name = "企业负责同志批示情况", width = 15)
|
||||||
|
@ExcelColumn(name = "企业负责同志批示情况", width = 15)
|
||||||
@Schema(description = "企业负责同志批示情况")
|
@Schema(description = "企业负责同志批示情况")
|
||||||
private java.lang.String elmComment;
|
private java.lang.String elmComment;
|
||||||
/**学习传达研究部署情况*/
|
/**学习传达研究部署情况*/
|
||||||
@Excel(name = "学习传达研究部署情况", width = 15)
|
@Excel(name = "学习传达研究部署情况", width = 15)
|
||||||
|
@ExcelColumn(name = "学习传达研究部署情况", width = 15)
|
||||||
@Schema(description = "学习传达研究部署情况")
|
@Schema(description = "学习传达研究部署情况")
|
||||||
private java.lang.String status;
|
private java.lang.String status;
|
||||||
/**密级*/
|
/**密级*/
|
||||||
@Excel(name = "密级(请填入非秘/秘密/机密)-必填", width = 15, dicCode = "secret_level")
|
@Excel(name = "密级(请填入非秘/秘密/机密)-必填", width = 15, dicCode = "secret_level")
|
||||||
|
@ExcelColumn(name = "密级(请填入非秘/秘密/机密)-必填", width = 15, dicCode = "secret_level")
|
||||||
@Dict(dicCode = "secret_level")
|
@Dict(dicCode = "secret_level")
|
||||||
@Schema(description = "密级")
|
@Schema(description = "密级")
|
||||||
@NotBlank
|
@NotBlank(message = "不能为空")
|
||||||
private java.lang.String secretLevel;
|
private java.lang.String secretLevel;
|
||||||
/**责任人*/
|
/**责任人*/
|
||||||
@Excel(name = "责任人", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
@Excel(name = "责任人", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
||||||
|
@ExcelColumn(name = "责任人", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
||||||
@Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
@Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
||||||
@Schema(description = "责任人")
|
@Schema(description = "责任人")
|
||||||
private java.lang.String responsiblePerson;
|
private java.lang.String responsiblePerson;
|
||||||
/**落实方案*/
|
/**落实方案*/
|
||||||
@Excel(name = "落实方案", width = 15)
|
@Excel(name = "落实方案", width = 15)
|
||||||
|
@ExcelColumn(name = "落实方案", width = 15)
|
||||||
@Schema(description = "落实方案")
|
@Schema(description = "落实方案")
|
||||||
private java.lang.String implPlan;
|
private java.lang.String implPlan;
|
||||||
/**牵头部门*/
|
/**牵头部门*/
|
||||||
@Excel(name = "牵头部门", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
@Excel(name = "牵头部门", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
||||||
|
@ExcelColumn(name = "牵头部门", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
||||||
@Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
@Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
||||||
@Schema(description = "牵头部门")
|
@Schema(description = "牵头部门")
|
||||||
private java.lang.String leadDepartment;
|
private java.lang.String leadDepartment;
|
||||||
/**贯彻落实措施*/
|
/**贯彻落实措施*/
|
||||||
@Excel(name = "贯彻落实措施-必填", width = 15)
|
@Excel(name = "贯彻落实措施-必填", width = 15)
|
||||||
|
@ExcelColumn(name = "贯彻落实措施-必填", width = 15)
|
||||||
@Schema(description = "贯彻落实措施")
|
@Schema(description = "贯彻落实措施")
|
||||||
@NotBlank
|
@NotBlank(message = "不能为空")
|
||||||
private java.lang.String implMeasures;
|
private java.lang.String implMeasures;
|
||||||
/**会议决策数*/
|
/**会议决策数*/
|
||||||
@Excel(name = "会议决策数", width = 15)
|
@Excel(name = "会议决策数", width = 15)
|
||||||
|
@ExcelColumn(name = "会议决策数", width = 15)
|
||||||
@Schema(description = "会议决策数")
|
@Schema(description = "会议决策数")
|
||||||
private java.lang.Integer numberOfResolutions;
|
private java.lang.Integer numberOfResolutions;
|
||||||
/**落实情况*/
|
/**落实情况*/
|
||||||
@Excel(name = "落实情况", width = 15)
|
@Excel(name = "落实情况", width = 15)
|
||||||
|
@ExcelColumn(name = "落实情况", width = 15)
|
||||||
@Schema(description = "落实情况")
|
@Schema(description = "落实情况")
|
||||||
private java.lang.String implStatus;
|
private java.lang.String implStatus;
|
||||||
/**落实条数*/
|
/**落实条数*/
|
||||||
@Excel(name = "落实条数", width = 15)
|
@Excel(name = "落实条数", width = 15)
|
||||||
|
@ExcelColumn(name = "落实条数", width = 15)
|
||||||
@Schema(description = "落实条数")
|
@Schema(description = "落实条数")
|
||||||
private java.lang.Integer implCount;
|
private java.lang.Integer implCount;
|
||||||
/**已闭环数量*/
|
/**已闭环数量*/
|
||||||
@Excel(name = "已闭环数量", width = 15)
|
@Excel(name = "已闭环数量", width = 15)
|
||||||
|
@ExcelColumn(name = "已闭环数量", width = 15)
|
||||||
@Schema(description = "已闭环数量")
|
@Schema(description = "已闭环数量")
|
||||||
private java.lang.Integer closedCount;
|
private java.lang.Integer closedCount;
|
||||||
/**报告反馈情况*/
|
/**报告反馈情况*/
|
||||||
@Excel(name = "报告反馈情况", width = 15)
|
@Excel(name = "报告反馈情况", width = 15)
|
||||||
|
@ExcelColumn(name = "报告反馈情况", width = 15)
|
||||||
@Schema(description = "报告反馈情况")
|
@Schema(description = "报告反馈情况")
|
||||||
private java.lang.String reportFeedbackStatus;
|
private java.lang.String reportFeedbackStatus;
|
||||||
/**是否存在完成风险(0不存在,1存在)*/
|
/**是否存在完成风险(0不存在,1存在)*/
|
||||||
@Excel(name = "是否存在完成风险(请填入是/否)", width = 15, dicCode = "true_or_flase")
|
@Excel(name = "是否存在完成风险(请填入是/否)", width = 15, dicCode = "true_or_flase")
|
||||||
|
@ExcelColumn(name = "是否存在完成风险(请填入是/否)", width = 15, dicCode = "true_or_flase")
|
||||||
@Schema(description = "是否存在完成风险(0不存在,1存在)")
|
@Schema(description = "是否存在完成风险(0不存在,1存在)")
|
||||||
private java.lang.Integer isCompletionRisk;
|
private java.lang.Integer isCompletionRisk;
|
||||||
/**拖期风险应对措施*/
|
/**拖期风险应对措施*/
|
||||||
@Excel(name = "拖期风险应对措施", width = 15)
|
@Excel(name = "拖期风险应对措施", width = 15)
|
||||||
|
@ExcelColumn(name = "拖期风险应对措施", width = 15)
|
||||||
@Schema(description = "拖期风险应对措施")
|
@Schema(description = "拖期风险应对措施")
|
||||||
private java.lang.String delayRiskMitigation;
|
private java.lang.String delayRiskMitigation;
|
||||||
/**完成状态(0推进中,1已完成)*/
|
/**完成状态(0推进中,1已完成)*/
|
||||||
@Excel(name = "完成状态(请填入推进中/已完成)-必填", width = 15, dicCode = "db_status")
|
@Excel(name = "完成状态(请填入推进中/已完成)-必填", width = 15, dicCode = "db_status")
|
||||||
|
@ExcelColumn(name = "完成状态(请填入推进中/已完成)-必填", width = 15, dicCode = "db_status")
|
||||||
@Schema(description = "完成状态(0推进中,1已完成)")
|
@Schema(description = "完成状态(0推进中,1已完成)")
|
||||||
@Dict( dicCode = "db_status")
|
@Dict( dicCode = "db_status")
|
||||||
private java.lang.Integer completionStatus;
|
private java.lang.Integer completionStatus;
|
||||||
/**落实部门id*/
|
/**落实部门id*/
|
||||||
@Excel(name = "落实部门", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
@Excel(name = "落实部门", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
||||||
|
@ExcelColumn(name = "落实部门", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
||||||
@Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
@Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
|
||||||
@Schema(description = "落实部门id")
|
@Schema(description = "落实部门id")
|
||||||
private java.lang.String implDept;
|
private java.lang.String implDept;
|
||||||
/**层深(最顶层节点层深为0)*/
|
/**层深(最顶层节点层深为0)*/
|
||||||
//@Excel(name = "层深(最顶层节点层深为0)", width = 15)
|
//@ExcelColumn(name = "层深(最顶层节点层深为0)", width = 15)
|
||||||
@Schema(description = "层深(最顶层节点层深为0)")
|
@Schema(description = "层深(最顶层节点层深为0)")
|
||||||
private java.lang.Integer treeDepth;
|
private java.lang.Integer treeDepth;
|
||||||
/**同级数据排序号*/
|
/**同级数据排序号*/
|
||||||
@Excel(name = "同级数据排序号", width = 15)
|
@Excel(name = "同级数据排序号", width = 15)
|
||||||
|
@ExcelColumn(name = "同级数据排序号", width = 15)
|
||||||
@Schema(description = "同级数据排序号")
|
@Schema(description = "同级数据排序号")
|
||||||
private java.lang.Integer sortOrder;
|
private java.lang.Integer sortOrder;
|
||||||
/**逻辑删除flag(0保留,1删除)*/
|
/**逻辑删除flag(0保留,1删除)*/
|
||||||
@Excel(name = "逻辑删除flag(0保留,1删除)", width = 15)
|
//@Excel(name = "逻辑删除flag(0保留,1删除)", width = 15)
|
||||||
|
//@ExcelColumn(name = "逻辑删除flag(0保留,1删除)", width = 15)
|
||||||
@Schema(description = "逻辑删除flag(0保留,1删除)")
|
@Schema(description = "逻辑删除flag(0保留,1删除)")
|
||||||
@TableLogic
|
@TableLogic
|
||||||
private java.lang.Integer delFlag;
|
private java.lang.Integer delFlag;
|
||||||
/**流程引擎状态字段*/
|
/**流程引擎状态字段*/
|
||||||
// @Excel(name = "流程引擎状态字段", width = 15)
|
// @ExcelColumn(name = "流程引擎状态字段", width = 15)
|
||||||
@Schema(description = "流程引擎状态字段")
|
@Schema(description = "流程引擎状态字段")
|
||||||
private java.lang.Integer bpmStatus;
|
private java.lang.Integer bpmStatus;
|
||||||
/**督办次数*/
|
/**督办次数*/
|
||||||
@Excel(name = "督办次数", width = 15)
|
@Excel(name = "督办次数", width = 15)
|
||||||
|
@ExcelColumn(name = "督办次数", width = 15)
|
||||||
@Schema(description = "督办次数")
|
@Schema(description = "督办次数")
|
||||||
private java.lang.Integer supervisionCount;
|
private java.lang.Integer supervisionCount;
|
||||||
/**dw领导userId字段*/
|
/**dw领导userId字段*/
|
||||||
@Excel(name = "领导", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
@Excel(name = "领导", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
||||||
|
@ExcelColumn(name = "领导", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
||||||
@Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
@Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username")
|
||||||
@Schema(description = "领导userId字段")
|
@Schema(description = "领导userId字段")
|
||||||
private java.lang.String sdwLeaderList;
|
private java.lang.String sdwLeaderList;
|
||||||
/**dw领导userId字段*/
|
/**dw领导userId字段*/
|
||||||
@Excel(name = "是否需要sdw审批(请填入是/否)", width = 15,dicCode = "true_or_flase")
|
@Excel(name = "是否需要sdw审批(请填入是/否)", width = 15,dicCode = "true_or_flase")
|
||||||
|
@ExcelColumn(name = "是否需要sdw审批(请填入是/否)", width = 15,dicCode = "true_or_flase")
|
||||||
@Schema(description = "是否需要sdw审批")
|
@Schema(description = "是否需要sdw审批")
|
||||||
private java.lang.Integer needSdwApproval;
|
private java.lang.Integer needSdwApproval;
|
||||||
|
|
||||||
/**dw领导userId字段*/
|
/**dw领导userId字段*/
|
||||||
@Excel(name = "截止日期", width = 15)
|
@Excel(name = "截止日期", width = 15)
|
||||||
|
@ExcelColumn(name = "截止日期", width = 15)
|
||||||
@Schema(description = "截止日期")
|
@Schema(description = "截止日期")
|
||||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") // 核心:告诉Jackson怎么把String转成Date
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") // 核心:告诉Jackson怎么把String转成Date
|
||||||
@DateTimeFormat(pattern="yyyy-MM-dd")
|
@DateTimeFormat(pattern="yyyy-MM-dd")
|
||||||
@@ -188,12 +213,12 @@ public class BgXiSpeak implements Serializable {
|
|||||||
private DeptApproveDetailMap approveInfo;
|
private DeptApproveDetailMap approveInfo;
|
||||||
|
|
||||||
/**督办次数*/
|
/**督办次数*/
|
||||||
// @Excel(name = "发起类型", width = 15)
|
// @ExcelColumn(name = "发起类型", width = 15)
|
||||||
@Schema(description = "0收集后直接发起,1收集后按照反馈时间周期性发起")
|
@Schema(description = "0收集后直接发起,1收集后按照反馈时间周期性发起")
|
||||||
private java.lang.Integer launchType;
|
private java.lang.Integer launchType;
|
||||||
|
|
||||||
/**周期类型 1日 2周 3两周 4月 5季*/
|
/**周期类型 1日 2周 3两周 4月 5季*/
|
||||||
// @Excel(name = "周期类型", width = 15)
|
// @ExcelColumn(name = "周期类型", width = 15)
|
||||||
@Schema(description = "周期类型 1日 2周 3两周 4月 5季")
|
@Schema(description = "周期类型 1日 2周 3两周 4月 5季")
|
||||||
private java.lang.Integer intervalType;
|
private java.lang.Integer intervalType;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user