Răsfoiți Sursa

集成FastExcel

Gaokun Wang 3 săptămâni în urmă
părinte
comite
85f1ae0063
25 a modificat fișierele cu 2111 adăugiri și 5 ștergeri
  1. 21 0
      eco-bom/pom.xml
  2. 74 0
      eco-common/com-core/src/main/java/org/eco/vip/core/api/IDict.java
  3. 56 0
      eco-common/com-core/src/main/java/org/eco/vip/core/utils/FileUtils.java
  4. 56 0
      eco-common/com-core/src/main/java/org/eco/vip/core/utils/ReflectUtils.java
  5. 1 0
      eco-common/com-core/src/main/java/org/eco/vip/core/utils/StrUtils.java
  6. 268 0
      eco-common/com-core/src/main/java/org/eco/vip/core/utils/StreamUtils.java
  7. 42 0
      eco-common/com-excel/pom.xml
  8. 29 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/annotation/CellMerge.java
  9. 42 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/annotation/ExcelDictFormat.java
  10. 39 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/annotation/ExcelEnumFormat.java
  11. 56 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/convert/ExcelBigNumberConvert.java
  12. 77 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/convert/ExcelDictConvert.java
  13. 18 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/convert/ExcelEnumConvert.java
  14. 119 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/core/CellMergeStrategy.java
  15. 119 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/core/DefaultExcelListener.java
  16. 71 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/core/DefaultExcelResult.java
  17. 154 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/core/DropDownOptions.java
  18. 380 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/core/ExcelDownHandler.java
  19. 19 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/core/ExcelListener.java
  20. 32 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/core/ExcelResult.java
  21. 82 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/strategy/DefaultCellStyleStrategy.java
  22. 87 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/strategy/DefaultColumnWidthStyleStrategy.java
  23. 268 0
      eco-common/com-excel/src/main/java/org/eco/vip/excel/utils/ExcelUtils.java
  24. 0 5
      eco-common/com-orm/pom.xml
  25. 1 0
      eco-common/pom.xml

+ 21 - 0
eco-bom/pom.xml

@@ -32,6 +32,8 @@
         <DmJdbcDriver18.version>8.1.3.140</DmJdbcDriver18.version>
         <sa-token.version>1.44.0</sa-token.version>
         <caffeine.version>3.2.1</caffeine.version>
+        <excel.version>1.2.0</excel.version>
+        <poi.version>5.4.1</poi.version>
     </properties>
 
     <!-- 全局的依赖配置-->
@@ -88,6 +90,13 @@
                 <version>${revision}</version>
             </dependency>
 
+            <!-- com-excel -->
+            <dependency>
+                <groupId>org.eco.vip</groupId>
+                <artifactId>com-excel</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
             <!-- lombok -->
             <dependency>
                 <groupId>org.projectlombok</groupId>
@@ -149,7 +158,19 @@
                 <artifactId>caffeine</artifactId>
                 <version>${caffeine.version}</version>
             </dependency>
+            <!-- https://mvnrepository.com/artifact/cn.idev.excel/fastexcel -->
+            <dependency>
+                <groupId>cn.idev.excel</groupId>
+                <artifactId>fastexcel</artifactId>
+                <version>${excel.version}</version>
+            </dependency>
 
+            <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi-ooxml</artifactId>
+                <version>${poi.version}</version>
+            </dependency>
 
         </dependencies>
     </dependencyManagement>

+ 74 - 0
eco-common/com-core/src/main/java/org/eco/vip/core/api/IDict.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.core.api;
+
+
+import java.util.Map;
+
+/**
+ * @description IDict
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 16:27
+ */
+public interface IDict {
+    /**
+     * 分隔符
+     */
+    String SEPARATOR = ",";
+
+    /**
+     * 根据字典类型和字典值获取字典标签
+     *
+     * @param dictType  字典类型
+     * @param dictValue 字典值
+     * @return 字典标签
+     */
+    default String getDictLabel(String dictType, String dictValue) {
+        return getDictLabel(dictType, dictValue, SEPARATOR);
+    }
+
+    /**
+     * 根据字典类型和字典标签获取字典值
+     *
+     * @param dictType  字典类型
+     * @param dictLabel 字典标签
+     * @return 字典值
+     */
+    default String getDictValue(String dictType, String dictLabel) {
+        return getDictValue(dictType, dictLabel, SEPARATOR);
+    }
+
+    /**
+     * 根据字典类型和字典值获取字典标签
+     *
+     * @param dictType  字典类型
+     * @param dictValue 字典值
+     * @param separator 分隔符
+     * @return 字典标签
+     */
+    String getDictLabel(String dictType, String dictValue, String separator);
+
+    /**
+     * 根据字典类型和字典标签获取字典值
+     *
+     * @param dictType  字典类型
+     * @param dictLabel 字典标签
+     * @param separator 分隔符
+     * @return 字典值
+     */
+    String getDictValue(String dictType, String dictLabel, String separator);
+
+    /**
+     * 获取字典下所有的字典值与标签
+     *
+     * @param dictType 字典类型
+     * @return dictValue为key,dictLabel为值组成的Map
+     */
+    Map<String, String> getAllDictByDictType(String dictType);
+
+
+}

+ 56 - 0
eco-common/com-core/src/main/java/org/eco/vip/core/utils/FileUtils.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.core.utils;
+
+
+import cn.hutool.core.io.FileUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @description FileUtils
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 17:50
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class FileUtils extends FileUtil {
+
+    /**
+     * 下载文件名重新编码
+     *
+     * @param response 响应对象
+     * @param realFileName 真实文件名
+     */
+    public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
+        String percentEncodedFileName = percentEncode(realFileName);
+
+        String contentDispositionValue = "attachment; filename=" +
+                percentEncodedFileName +
+                ";" +
+                "filename*=" +
+                "utf-8''" +
+                percentEncodedFileName;
+        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+        response.setHeader("Content-disposition", contentDispositionValue);
+        response.setHeader("download-filename", percentEncodedFileName);
+    }
+
+    /**
+     * 百分号编码工具方法
+     *
+     * @param s 需要百分号编码的字符串
+     * @return 百分号编码后的字符串
+     */
+    public static String percentEncode(String s) {
+        String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
+        return encode.replaceAll("\\+", "%20");
+    }
+}

+ 56 - 0
eco-common/com-core/src/main/java/org/eco/vip/core/utils/ReflectUtils.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.core.utils;
+
+
+import cn.hutool.core.util.ReflectUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * @description ReflectUtils
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 16:06
+ */
+@Slf4j
+public class ReflectUtils extends ReflectUtil {
+    private static final String SETTER_PREFIX = "set";
+
+    private static final String GETTER_PREFIX = "get";
+
+    private static final String CGLIB_CLASS_SEPARATOR = "$$";
+    private static final String UNKNOWN = "null";
+
+    /**
+     * 调用Getter方法.
+     * 支持多级,如:对象名.对象名.方法
+     */
+    public static Object invokeGetter(Object obj, String fieldName) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+        String getterName = GETTER_PREFIX + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
+        Method getter = obj.getClass().getMethod(getterName);
+        return getter.invoke(obj);
+    }
+
+    public static String getStringVal(Object obj) {
+        if (ObjUtils.isNotNull(obj) && !(UNKNOWN).equals(obj)) {
+            return obj.toString();
+        } else {
+            return "";
+        }
+    }
+
+    public static Long getLongVal(Object obj) {
+        return (ObjUtils.isNotNull(obj) && !(UNKNOWN).equals(obj)) ? (Long.parseLong(obj.toString())) : 0L;
+    }
+
+    public static Integer getIntVal(Object obj) {
+        return (ObjUtils.isNotNull(obj) && !(UNKNOWN).equals(obj)) ? (Integer.parseInt(obj.toString())) : 0;
+    }
+
+}

+ 1 - 0
eco-common/com-core/src/main/java/org/eco/vip/core/utils/StrUtils.java

@@ -16,4 +16,5 @@ import lombok.extern.slf4j.Slf4j;
  */
 @Slf4j
 public class StrUtils extends StrUtil {
+    public static final String SEPARATOR = ",";
 }

+ 268 - 0
eco-common/com-core/src/main/java/org/eco/vip/core/utils/StreamUtils.java

@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.core.utils;
+
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @description StreamUtils
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:52
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StreamUtils {
+
+    /**
+     * 将collection过滤
+     *
+     * @param collection 需要转化的集合
+     * @param function   过滤方法
+     * @return 过滤后的list
+     */
+    public static <E> List<E> filter(Collection<E> collection, Predicate<E> function) {
+        if (CollUtil.isEmpty(collection)) {
+            return CollUtil.newArrayList();
+        }
+        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+        return collection.stream().filter(function).collect(Collectors.toList());
+    }
+
+    /**
+     * 将collection拼接
+     *
+     * @param collection 需要转化的集合
+     * @param function   拼接方法
+     * @return 拼接后的list
+     */
+    public static <E> String join(Collection<E> collection, Function<E, String> function) {
+        return join(collection, function, StrUtils.SEPARATOR);
+    }
+
+    /**
+     * 将collection拼接
+     *
+     * @param collection 需要转化的集合
+     * @param function   拼接方法
+     * @param delimiter  拼接符
+     * @return 拼接后的list
+     */
+    public static <E> String join(Collection<E> collection, Function<E, String> function, CharSequence delimiter) {
+        if (CollUtil.isEmpty(collection)) {
+            return StrUtils.EMPTY;
+        }
+        return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
+    }
+
+    /**
+     * 将collection排序
+     *
+     * @param collection 需要转化的集合
+     * @param comparing  排序方法
+     * @return 排序后的list
+     */
+    public static <E> List<E> sorted(Collection<E> collection, Comparator<E> comparing) {
+        if (CollUtil.isEmpty(collection)) {
+            return CollUtil.newArrayList();
+        }
+        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+        return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
+    }
+
+    /**
+     * 将collection转化为类型不变的map<br>
+     * <B>{@code Collection<V>  ---->  Map<K,V>}</B>
+     *
+     * @param collection 需要转化的集合
+     * @param key        V类型转化为K类型的lambda方法
+     * @param <V>        collection中的泛型
+     * @param <K>        map中的key类型
+     * @return 转化后的map
+     */
+    public static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> key) {
+        if (CollUtil.isEmpty(collection)) {
+            return MapUtil.newHashMap();
+        }
+        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+    }
+
+    /**
+     * 将Collection转化为map(value类型与collection的泛型不同)<br>
+     * <B>{@code Collection<E> -----> Map<K,V>  }</B>
+     *
+     * @param collection 需要转化的集合
+     * @param key        E类型转化为K类型的lambda方法
+     * @param value      E类型转化为V类型的lambda方法
+     * @param <E>        collection中的泛型
+     * @param <K>        map中的key类型
+     * @param <V>        map中的value类型
+     * @return 转化后的map
+     */
+    public static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> value) {
+        if (CollUtil.isEmpty(collection)) {
+            return MapUtil.newHashMap();
+        }
+        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
+    }
+
+    /**
+     * 将collection按照规则(比如有相同的班级id)分类成map<br>
+     * <B>{@code Collection<E> -------> Map<K,List<E>> } </B>
+     *
+     * @param collection 需要分类的集合
+     * @param key        分类的规则
+     * @param <E>        collection中的泛型
+     * @param <K>        map中的key类型
+     * @return 分类后的map
+     */
+    public static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> key) {
+        if (CollUtil.isEmpty(collection)) {
+            return MapUtil.newHashMap();
+        }
+        return collection
+                .stream().filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
+    }
+
+    /**
+     * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map<br>
+     * <B>{@code Collection<E>  --->  Map<T,Map<U,List<E>>> } </B>
+     *
+     * @param collection 需要分类的集合
+     * @param key1       第一个分类的规则
+     * @param key2       第二个分类的规则
+     * @param <E>        集合元素类型
+     * @param <K>        第一个map中的key类型
+     * @param <U>        第二个map中的key类型
+     * @return 分类后的map
+     */
+    public static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1, Function<E, U> key2) {
+        if (CollUtil.isEmpty(collection)) {
+            return MapUtil.newHashMap();
+        }
+        return collection
+                .stream().filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
+    }
+
+    /**
+     * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map<br>
+     * <B>{@code Collection<E>  --->  Map<T,Map<U,E>> } </B>
+     *
+     * @param collection 需要分类的集合
+     * @param key1       第一个分类的规则
+     * @param key2       第二个分类的规则
+     * @param <T>        第一个map中的key类型
+     * @param <U>        第二个map中的key类型
+     * @param <E>        collection中的泛型
+     * @return 分类后的map
+     */
+    public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
+        if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
+            return MapUtil.newHashMap();
+        }
+        return collection
+                .stream().filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
+    }
+
+    /**
+     * 将collection转化为List集合,但是两者的泛型不同<br>
+     * <B>{@code Collection<E>  ------>  List<T> } </B>
+     *
+     * @param collection 需要转化的集合
+     * @param function   collection中的泛型转化为list泛型的lambda表达式
+     * @param <E>        collection中的泛型
+     * @param <T>        List中的泛型
+     * @return 转化后的list
+     */
+    public static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function) {
+        if (CollUtil.isEmpty(collection)) {
+            return CollUtil.newArrayList();
+        }
+        return collection
+                .stream()
+                .map(function)
+                .filter(Objects::nonNull)
+                // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 将collection转化为Set集合,但是两者的泛型不同<br>
+     * <B>{@code Collection<E>  ------>  Set<T> } </B>
+     *
+     * @param collection 需要转化的集合
+     * @param function   collection中的泛型转化为set泛型的lambda表达式
+     * @param <E>        collection中的泛型
+     * @param <T>        Set中的泛型
+     * @return 转化后的Set
+     */
+    public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
+        if (CollUtil.isEmpty(collection) || function == null) {
+            return CollUtil.newHashSet();
+        }
+        return collection
+                .stream()
+                .map(function)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+    }
+
+
+    /**
+     * 合并两个相同key类型的map
+     *
+     * @param map1  第一个需要合并的 map
+     * @param map2  第二个需要合并的 map
+     * @param merge 合并的lambda,将key  value1 value2合并成最终的类型,注意value可能为空的情况
+     * @param <K>   map中的key类型
+     * @param <X>   第一个 map的value类型
+     * @param <Y>   第二个 map的value类型
+     * @param <V>   最终map的value类型
+     * @return 合并后的map
+     */
+    public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
+        if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
+            return MapUtil.newHashMap();
+        } else if (MapUtil.isEmpty(map1)) {
+            map1 = MapUtil.newHashMap();
+        } else if (MapUtil.isEmpty(map2)) {
+            map2 = MapUtil.newHashMap();
+        }
+        Set<K> key = new HashSet<>();
+        key.addAll(map1.keySet());
+        key.addAll(map2.keySet());
+        Map<K, V> map = new HashMap<>();
+        for (K t : key) {
+            X x = map1.get(t);
+            Y y = map2.get(t);
+            V z = merge.apply(x, y);
+            if (z != null) {
+                map.put(t, z);
+            }
+        }
+        return map;
+    }
+}

+ 42 - 0
eco-common/com-excel/pom.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2025 GaoKunW
+  ~
+  -->
+
+<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>
+    <parent>
+        <groupId>org.eco.vip</groupId>
+        <artifactId>eco-common</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>com-excel</artifactId>
+    <name>${project.artifactId}</name>
+    <packaging>jar</packaging>
+    <dependencies>
+        <dependency>
+            <groupId>org.eco.vip</groupId>
+            <artifactId>com-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.idev.excel</groupId>
+            <artifactId>fastexcel</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.poi</groupId>
+                    <artifactId>poi-ooxml</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+        </dependency>
+    </dependencies>
+
+
+</project>

+ 29 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/annotation/CellMerge.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.annotation;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @description CellMerge
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:13
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface CellMerge {
+    /**
+     * col 索引
+     */
+    int index() default -1;
+}

+ 42 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/annotation/ExcelDictFormat.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.annotation;
+
+
+import org.eco.vip.core.utils.StrUtils;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @description ExcelDictFormat
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:13
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelDictFormat {
+
+    /**
+     * 如果是字典类型,请设置字典的type值 (如: COMMON_STATUS)
+     */
+    String dictType() default "";
+
+    /**
+     * 读取内容转表达式 value->label
+     */
+    String readConverterExp() default "";
+
+    /**
+     * 分隔符,读取字符串组内容
+     */
+    String separator() default StrUtils.SEPARATOR;
+}

+ 39 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/annotation/ExcelEnumFormat.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.annotation;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @description ExcelEnumFormat
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:17
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelEnumFormat {
+    /**
+     * 字典枚举类型
+     */
+    Class<? extends Enum<?>> enumClass();
+
+    /**
+     * 字典枚举类中对应的code属性名称,默认为code
+     */
+    String codeField() default "code";
+
+    /**
+     * 字典枚举类中对应的text属性名称,默认为text
+     */
+    String textField() default "text";
+}

+ 56 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/convert/ExcelBigNumberConvert.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.convert;
+
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.idev.excel.converters.Converter;
+import cn.idev.excel.enums.CellDataTypeEnum;
+import cn.idev.excel.metadata.GlobalConfiguration;
+import cn.idev.excel.metadata.data.ReadCellData;
+import cn.idev.excel.metadata.data.WriteCellData;
+import cn.idev.excel.metadata.property.ExcelContentProperty;
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigDecimal;
+
+/**
+ * @description ExcelBigNumberConvert
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 17:10
+ */
+@Slf4j
+public class ExcelBigNumberConvert implements Converter<Long> {
+    @Override
+    public Class<Long> supportJavaTypeKey() {
+        return Long.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+        return Convert.toLong(cellData.getData());
+    }
+
+    @Override
+    public WriteCellData<Object> convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+        if (ObjectUtil.isNotNull(object)) {
+            String str = Convert.toStr(object);
+            if (str.length() > 15) {
+                return new WriteCellData<>(str);
+            }
+        }
+        WriteCellData<Object> cellData = new WriteCellData<>(new BigDecimal(object));
+        cellData.setType(CellDataTypeEnum.NUMBER);
+        return cellData;
+    }
+}

+ 77 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/convert/ExcelDictConvert.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.convert;
+
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.idev.excel.converters.Converter;
+import cn.idev.excel.enums.CellDataTypeEnum;
+import cn.idev.excel.metadata.GlobalConfiguration;
+import cn.idev.excel.metadata.data.ReadCellData;
+import cn.idev.excel.metadata.data.WriteCellData;
+import cn.idev.excel.metadata.property.ExcelContentProperty;
+import org.eco.vip.core.api.IDict;
+import org.eco.vip.core.utils.SpringUtils;
+import org.eco.vip.core.utils.StrUtils;
+import org.eco.vip.excel.annotation.ExcelDictFormat;
+import org.eco.vip.excel.utils.ExcelUtils;
+
+import java.lang.reflect.Field;
+
+/**
+ * @description ExcelDictConvert
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 17:11
+ */
+public class ExcelDictConvert implements Converter<Object> {
+    @Override
+    public Class<Object> supportJavaTypeKey() {
+        return Object.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return null;
+    }
+
+    @Override
+    public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+        ExcelDictFormat anno = getAnnotation(contentProperty.getField());
+        String type = anno.dictType();
+        String label = cellData.getStringValue();
+        String value;
+        if (StrUtils.isBlank(type)) {
+            value = ExcelUtils.reverseByExp(label, anno.readConverterExp(), anno.separator());
+        } else {
+            value = SpringUtils.getBean(IDict.class).getDictValue(type, label, anno.separator());
+        }
+        return Convert.convert(contentProperty.getField().getType(), value);
+    }
+
+    @Override
+    public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+        if (ObjectUtil.isNull(object)) {
+            return new WriteCellData<>("");
+        }
+        ExcelDictFormat anno = getAnnotation(contentProperty.getField());
+        String type = anno.dictType();
+        String value = Convert.toStr(object);
+        String label;
+        if (StrUtils.isBlank(type)) {
+            label = ExcelUtils.convertByExp(value, anno.readConverterExp(), anno.separator());
+        } else {
+            label = SpringUtils.getBean(IDict.class).getDictLabel(type, value, anno.separator());
+        }
+        return new WriteCellData<>(label);
+    }
+
+    private ExcelDictFormat getAnnotation(Field field) {
+        return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class);
+    }
+}

+ 18 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/convert/ExcelEnumConvert.java

@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.convert;
+
+
+import cn.idev.excel.converters.Converter;
+
+/**
+ * @description ExcelEnumConvert
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 17:12
+ */
+public class ExcelEnumConvert implements Converter<Object> {
+}

+ 119 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/core/CellMergeStrategy.java

@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.core;
+
+
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.metadata.Head;
+import cn.idev.excel.write.merge.AbstractMergeStrategy;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.eco.vip.core.utils.ReflectUtils;
+import org.eco.vip.excel.annotation.CellMerge;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @description CellMergeStrategy 列值重复合并策略
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 16:02
+ */
+@Slf4j
+public class CellMergeStrategy extends AbstractMergeStrategy {
+    private final List<CellRangeAddress> cellList;
+
+    /**
+     * 是否有标题
+     */
+    private final boolean hasTitle;
+
+    /**
+     * 开始行号,从0开始(第一行),如果有要忽略的(标题行)需要设置非0
+     */
+    private final int rowIndex;
+
+    public CellMergeStrategy(List<?> list, boolean hasTitle) {
+        this.hasTitle = hasTitle;
+        // 行合并开始下标
+        this.rowIndex = hasTitle ? 1 : 0;
+        this.cellList = handle(list, hasTitle);
+    }
+
+    @Override
+    protected void merge(Sheet sheet, Cell cell, Head head, Integer integer) {
+        // 如果 cellList 不为空,并且当前单元格是第一列的第一行,则合并单元格
+        if (!cellList.isEmpty() && cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {
+            for (CellRangeAddress item : cellList) {
+                sheet.addMergedRegion(item);
+            }
+        }
+    }
+
+    @SneakyThrows
+    private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
+        List<CellRangeAddress> localCellList = new ArrayList<>();
+        if (list.isEmpty()) {
+            return localCellList;
+        }
+        // 获取字段信息并初始化合并字段
+        List<Field> mergeFields = new ArrayList<>();
+        List<Integer> mergeFieldsIndex = new ArrayList<>();
+        int startRowIndex = initializeMergeFields(list.getFirst().getClass(), mergeFields, mergeFieldsIndex, hasTitle);
+
+        // 处理合并逻辑
+        Map<Field, Object> prevRowValues = new HashMap<>();
+        for (int i = 1; i < list.size(); i++) {
+            boolean merged = false;
+            for (int j = 0; j < mergeFields.size(); j++) {
+                Field field = mergeFields.get(j);
+                Object currentValue = ReflectUtils.invokeGetter(list.get(i), field.getName());
+                Object prevValue = prevRowValues.get(field);
+
+                if (prevValue != null && prevValue.equals(currentValue)) {
+                    if (!merged) {
+                        int colNum = mergeFieldsIndex.get(j) - 1;
+                        localCellList.add(new CellRangeAddress(i - 1 + startRowIndex, i + startRowIndex, colNum, colNum));
+                        merged = true;
+                    }
+                } else {
+                    prevRowValues.put(field, currentValue);
+                }
+            }
+        }
+        return localCellList;
+    }
+
+    private int initializeMergeFields(Class<?> clazz, List<Field> mergeFields, List<Integer> mergeFieldsIndex, boolean hasTitle) {
+        Field[] fields = ReflectUtils.getFields(clazz);
+        int startRowIndex = 0;
+        for (int i = 0; i < fields.length; i++) {
+            Field field = fields[i];
+            if (field.isAnnotationPresent(CellMerge.class)) {
+                CellMerge cm = field.getAnnotation(CellMerge.class);
+                mergeFields.add(field);
+                mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
+
+                if (hasTitle) {
+                    ExcelProperty property = field.getAnnotation(ExcelProperty.class);
+                    startRowIndex = Math.max(startRowIndex, property.value().length);
+                }
+            }
+        }
+        return startRowIndex;
+    }
+
+    public boolean isHasTitle() {
+        return hasTitle;
+    }
+}

+ 119 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/core/DefaultExcelListener.java

@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.core;
+
+
+import cn.hutool.core.util.StrUtil;
+import cn.idev.excel.context.AnalysisContext;
+import cn.idev.excel.event.AnalysisEventListener;
+import cn.idev.excel.exception.ExcelAnalysisException;
+import cn.idev.excel.exception.ExcelDataConvertException;
+import cn.idev.excel.metadata.CellExtra;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eco.vip.core.utils.JsonUtils;
+import org.eco.vip.core.utils.StreamUtils;
+import org.eco.vip.core.utils.ValidatorUtils;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @description DefaultExcelListener
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:36
+ */
+@Slf4j
+@NoArgsConstructor
+public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements ExcelListener<T> {
+
+    /**
+     * 是否Validator检验,默认为是
+     */
+    private Boolean isValidate = Boolean.TRUE;
+
+    /**
+     * excel 表头数据
+     */
+    private Map<Integer, String> headMap;
+    /**
+     * 导入回执
+     */
+    private ExcelResult<T> excelResult;
+
+    public DefaultExcelListener(boolean isValidate) {
+        this.excelResult = new DefaultExcelResult<>();
+        this.isValidate = isValidate;
+    }
+
+    @Override
+    public void invoke(T t, AnalysisContext analysisContext) {
+        if (isValidate) {
+            ValidatorUtils.validate(t);
+        }
+        excelResult.getList().add(t);
+    }
+
+    /**
+     * 处理异常
+     *
+     * @param exception ExcelDataConvertException
+     * @param context   Excel 上下文
+     */
+    @Override
+    public void onException(Exception exception, AnalysisContext context) {
+        String errMsg = null;
+        if (exception instanceof ExcelDataConvertException excelDataConvertException) {
+            // 如果是某一个单元格的转换异常 能获取到具体行号
+            Integer rowIndex = excelDataConvertException.getRowIndex();
+            Integer columnIndex = excelDataConvertException.getColumnIndex();
+            errMsg = StrUtil.format("第{}行-第{}列-表头{}: 解析异常<br/>",
+                    rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));
+            if (log.isDebugEnabled()) {
+                log.error(errMsg);
+            }
+        }
+        if (exception instanceof ConstraintViolationException constraintViolationException) {
+            Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
+            String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");
+            errMsg = StrUtil.format("第{}行数据校验异常: {}", context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);
+            if (log.isDebugEnabled()) {
+                log.error(errMsg);
+            }
+        }
+        excelResult.getErrorList().add(errMsg);
+        throw new ExcelAnalysisException(errMsg);
+    }
+
+    @Override
+    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
+        this.headMap = headMap;
+        log.debug("解析到一条表头数据: {}", JsonUtils.toJsonString(headMap));
+    }
+
+    @Override
+    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
+        log.debug("所有数据解析完成!");
+    }
+
+    @Override
+    public void extra(CellExtra extra, AnalysisContext context) {
+        super.extra(extra, context);
+    }
+
+    @Override
+    public boolean hasNext(AnalysisContext context) {
+        return super.hasNext(context);
+    }
+
+    @Override
+    public ExcelResult<T> getExcelResult() {
+        return excelResult;
+    }
+}

+ 71 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/core/DefaultExcelResult.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.core;
+
+
+import lombok.Setter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @description DefaultExcelResult
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:39
+ */
+@Setter
+public class DefaultExcelResult <T> implements ExcelResult<T>{
+
+    /**
+     * 数据对象list
+     */
+    private List<T> list;
+
+    /**
+     * 错误信息列表
+     */
+    private List<String> errorList;
+    public DefaultExcelResult() {
+        this.list = new ArrayList<>();
+        this.errorList = new ArrayList<>();
+    }
+
+    public DefaultExcelResult(List<T> list, List<String> errorList) {
+        this.list = list;
+        this.errorList = errorList;
+    }
+
+    public DefaultExcelResult(ExcelResult<T> excelResult) {
+        this.list = excelResult.getList();
+        this.errorList = excelResult.getErrorList();
+    }
+
+    @Override
+    public List<T> getList() {
+        return list;
+    }
+
+    @Override
+    public List<String> getErrorList() {
+        return errorList;
+    }
+
+    @Override
+    public String getAnalysis() {
+        int successCount = list.size();
+        int errorCount = errorList.size();
+        if (successCount == 0) {
+            return "读取失败,未解析到数据";
+        } else {
+            if (errorCount == 0) {
+                return String.format("恭喜您,全部读取成功!共%d条", successCount);
+            } else {
+                return "";
+            }
+        }
+    }
+}

+ 154 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/core/DropDownOptions.java

@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.core;
+
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.eco.vip.core.exception.BusinessException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @description DropDownOptions
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 17:37
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DropDownOptions {
+    /**
+     * 一级下拉所在列index,从0开始算
+     */
+    private int index = 0;
+    /**
+     * 二级下拉所在的index,从0开始算,不能与一级相同
+     */
+    private int nextIndex = 0;
+    /**
+     * 一级下拉所包含的数据
+     */
+    private List<String> options = new ArrayList<>();
+    /**
+     * 二级下拉所包含的数据Map
+     * <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p>
+     */
+    private Map<String, List<String>> nextOptions = new HashMap<>();
+    /**
+     * 分隔符
+     */
+    private static final String DELIMITER = "_";
+
+    /**
+     * 创建只有一级的下拉选
+     */
+    public DropDownOptions(int index, List<String> options) {
+        this.index = index;
+        this.options = options;
+    }
+
+    /**
+     * <h2>创建每个选项可选值</h2>
+     * <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
+     *
+     * @param vars 可选值内包含的参数
+     * @return 合规的可选值
+     */
+    public static String createOptionValue(Object... vars) {
+        StringBuilder stringBuffer = new StringBuilder();
+        String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
+        for (int i = 0; i < vars.length; i++) {
+            String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
+            if (!var.matches(regex)) {
+                throw new BusinessException("选项数据不符合规则,仅允许使用中英文字符以及数字");
+            }
+            stringBuffer.append(var);
+            if (i < vars.length - 1) {
+                // 直至最后一个前,都以_作为切割线
+                stringBuffer.append(DELIMITER);
+            }
+        }
+        if (stringBuffer.toString().matches("^\\d_*$")) {
+            throw new BusinessException("禁止以数字开头");
+        }
+        return stringBuffer.toString();
+    }
+
+    /**
+     * 将处理后合理的可选值解析为原始的参数
+     *
+     * @param option 经过处理后的合理的可选项
+     * @return 原始的参数
+     */
+    public static List<String> analyzeOptionValue(String option) {
+        return StrUtil.split(option, DELIMITER, true, true);
+    }
+
+    /**
+     * 创建级联下拉选项
+     *
+     * @param parentList                  父实体可选项原始数据
+     * @param parentIndex                 父下拉选位置
+     * @param sonList                     子实体可选项原始数据
+     * @param sonIndex                    子下拉选位置
+     * @param parentHowToGetIdFunction    父类如何获取唯一标识
+     * @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
+     * @param howToBuildEveryOption       如何生成下拉选内容
+     * @return 级联下拉选项
+     */
+    public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
+                                                         int parentIndex,
+                                                         List<T> sonList,
+                                                         int sonIndex,
+                                                         Function<T, Number> parentHowToGetIdFunction,
+                                                         Function<T, Number> sonHowToGetParentIdFunction,
+                                                         Function<T, String> howToBuildEveryOption) {
+        DropDownOptions parentLinkSonOptions = new DropDownOptions();
+        // 先创建父类的下拉
+        parentLinkSonOptions.setIndex(parentIndex);
+        parentLinkSonOptions.setOptions(
+                parentList.stream()
+                        .map(howToBuildEveryOption)
+                        .collect(Collectors.toList())
+        );
+        // 提取父-子级联下拉
+        Map<String, List<String>> sonOptions = new HashMap<>();
+        // 父级依据自己的ID分组
+        Map<Number, List<T>> parentGroupByIdMap =
+                parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
+        // 遍历每个子集,提取到Map中
+        sonList.forEach(everySon -> {
+            if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
+                // 找到对应的上级
+                T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
+                // 提取名称和ID作为Key
+                String key = howToBuildEveryOption.apply(parentObj);
+                // Key对应的Value
+                List<String> thisParentSonOptionList;
+                if (sonOptions.containsKey(key)) {
+                    thisParentSonOptionList = sonOptions.get(key);
+                } else {
+                    thisParentSonOptionList = new ArrayList<>();
+                    sonOptions.put(key, thisParentSonOptionList);
+                }
+                // 往Value中添加当前子集选项
+                thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
+            }
+        });
+        parentLinkSonOptions.setNextIndex(sonIndex);
+        parentLinkSonOptions.setNextOptions(sonOptions);
+        return parentLinkSonOptions;
+    }
+}

+ 380 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/core/ExcelDownHandler.java

@@ -0,0 +1,380 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.core;
+
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.EnumUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.idev.excel.metadata.FieldCache;
+import cn.idev.excel.metadata.FieldWrapper;
+import cn.idev.excel.util.ClassUtils;
+import cn.idev.excel.write.handler.SheetWriteHandler;
+import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
+import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.DataValidation;
+import org.apache.poi.ss.usermodel.DataValidationConstraint;
+import org.apache.poi.ss.usermodel.DataValidationHelper;
+import org.apache.poi.ss.usermodel.Name;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+import org.eco.vip.core.api.IDict;
+import org.eco.vip.core.exception.BusinessException;
+import org.eco.vip.core.utils.CollUtils;
+import org.eco.vip.core.utils.SpringUtils;
+import org.eco.vip.core.utils.StrUtils;
+import org.eco.vip.core.utils.StreamUtils;
+import org.eco.vip.excel.annotation.ExcelDictFormat;
+import org.eco.vip.excel.annotation.ExcelEnumFormat;
+import org.eco.vip.excel.utils.ExcelUtils;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @description ExcelDownHandler
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 16:15
+ */
+@Slf4j
+public class ExcelDownHandler implements SheetWriteHandler {
+
+    /**
+     * Excel表格中的列名英文 仅为了解析列英文,禁止修改
+     */
+    private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+    /**
+     * 单选数据Sheet名
+     */
+    private static final String OPTIONS_SHEET_NAME = "options";
+
+    /**
+     * 联动选择数据Sheet名的头
+     */
+    private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
+    /**
+     * 下拉可选项
+     */
+    private final List<DropDownOptions> dropDownOptions;
+
+    /**
+     * 当前单选进度
+     */
+    private int currentOptionsColumnIndex;
+
+    /**
+     * 当前联动选择进度
+     */
+    private int currentLinkedOptionsSheetIndex;
+
+    public ExcelDownHandler(List<DropDownOptions> options) {
+        this.dropDownOptions = options;
+        this.currentOptionsColumnIndex = 0;
+        this.currentLinkedOptionsSheetIndex = 0;
+    }
+
+    @Override
+    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+        final Sheet sheet = writeSheetHolder.getSheet();
+        final DataValidationHelper helper = sheet.getDataValidationHelper();
+        final Workbook workbook = writeWorkbookHolder.getWorkbook();
+        final FieldCache fieldCache = ClassUtils.declaredFields(writeWorkbookHolder.getClazz(), writeWorkbookHolder);
+
+        for (Map.Entry<Integer, FieldWrapper> entry : fieldCache.getSortedFieldMap().entrySet()) {
+            final Integer index = entry.getKey();
+            final FieldWrapper wrapper = entry.getValue();
+            final Field field = wrapper.getField();
+            List<String> options = new ArrayList<>();
+            if (field.isAnnotationPresent(ExcelDictFormat.class)) {
+                final ExcelDictFormat dictFormat = field.getDeclaredAnnotation(ExcelDictFormat.class);
+                options = processDictFormat(dictFormat);
+            } else if (field.isAnnotationPresent(ExcelEnumFormat.class)) {
+                final ExcelEnumFormat enumFormat = field.getDeclaredAnnotation(ExcelEnumFormat.class);
+                List<Object> values = EnumUtil.getFieldValues(enumFormat.enumClass(), enumFormat.textField());
+                options = StreamUtils.toList(values, String::valueOf);
+            }
+            if (CollUtils.isNotEmpty(options)) {
+                // 仅当下拉可选项不为空时执行
+                if (options.size() > 20) {
+                    // 这里限制如果可选项大于20,则使用额外表形式
+                    dropDownWithSheet(helper, workbook, sheet, index, options);
+                } else {
+                    // 否则使用固定值形式
+                    dropDownWithSimple(helper, sheet, index, options);
+                }
+            }
+            if (CollUtil.isEmpty(dropDownOptions)) {
+                return;
+            }
+            dropDownOptions.forEach(everyOptions -> {
+                // 如果传递了下拉框选择器参数
+                if (!everyOptions.getNextOptions().isEmpty()) {
+                    // 当二级选项不为空时,使用额外关联表的形式
+                    dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
+                } else if (everyOptions.getOptions().size() > 10) {
+                    // 当一级选项参数个数大于10,使用额外表的形式
+                    dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+                } else if (!everyOptions.getOptions().isEmpty()) {
+                    // 当一级选项个数不为空,使用默认形式
+                    dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+                }
+            });
+
+        }
+    }
+
+    private List<String> processDictFormat(ExcelDictFormat dictFormat) {
+        return getOptions(dictFormat);
+    }
+
+    private List<String> getOptions(ExcelDictFormat dictFormat) {
+        List<String> options = new ArrayList<>();
+        final String dictType = dictFormat.dictType();
+        final String converterExp = dictFormat.readConverterExp();
+        final String separator = dictFormat.separator();
+        if (StrUtils.isNotBlank(dictType)) {
+            options = getDictTypeOptions(dictType);
+        } else if (StrUtils.isNotBlank(converterExp)) {
+            options = ExcelUtils.listByExp(converterExp, separator);
+        }
+        return options;
+    }
+
+    private List<String> getDictTypeOptions(String dictType) {
+        List<String> options = new ArrayList<>();
+        final IDict dictService = SpringUtils.getBean(IDict.class);
+        // 如果传递了字典名,则依据字典建立下拉
+        Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
+                .orElseThrow(() -> new BusinessException(String.format("字典 %s 不存在", dictType)))
+                .values();
+        if (!values.isEmpty()) {
+            options.addAll(values);
+        }
+        return options;
+    }
+
+    private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
+        // 创建下拉数据表
+        Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
+                .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
+        // 将下拉表隐藏
+        workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
+        // 完善纵向的一级选项数据表
+        for (int i = 0; i < value.size(); i++) {
+            int finalI = i;
+            // 获取每一选项行,如果没有则创建
+            Row row = Optional.ofNullable(simpleDataSheet.getRow(i)).orElseGet(() -> simpleDataSheet.createRow(finalI));
+            // 获取本级选项对应的选项列,如果没有则创建
+            Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex)).orElseGet(() -> row.createCell(currentOptionsColumnIndex));
+            // 设置值
+            cell.setCellValue(value.get(i));
+        }
+
+        // 创建名称管理器
+        Name name = workbook.createName();
+        // 设置名称管理器的别名
+        String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
+        name.setNameName(nameName);
+        // 以纵向第一列创建一级下拉拼接引用位置
+        String function = String.format("%s!$%s$1:$%s$%d", OPTIONS_SHEET_NAME, getExcelColumnName(currentOptionsColumnIndex),
+                getExcelColumnName(currentOptionsColumnIndex), value.size());
+        // 设置名称管理器的引用位置
+        name.setRefersToFormula(function);
+        // 设置数据校验为序列模式,引用的是名称管理器中的别名
+        this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
+        currentOptionsColumnIndex++;
+    }
+
+    /**
+     * 挂载下拉的列,仅限一级选项
+     */
+    private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex, DataValidationConstraint constraint) {
+        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+        CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
+        markDataValidationToSheet(helper, sheet, constraint, addressList);
+    }
+
+    /**
+     * 应用数据校验
+     */
+    private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet, DataValidationConstraint constraint, CellRangeAddressList addressList) {
+        // 数据有效性对象
+        DataValidation dataValidation = helper.createValidation(constraint, addressList);
+        // 处理Excel兼容性问题
+        if (dataValidation instanceof XSSFDataValidation) {
+            // 数据校验
+            dataValidation.setSuppressDropDownArrow(true);
+            // 错误提示
+            dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
+            dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
+            dataValidation.setShowErrorBox(true);
+            // 选定提示
+            dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
+            dataValidation.setShowPromptBox(true);
+            sheet.addValidationData(dataValidation);
+        } else {
+            dataValidation.setSuppressDropDownArrow(false);
+        }
+        sheet.addValidationData(dataValidation);
+    }
+
+    /**
+     * 依据列index获取列名英文 依据列index转换为Excel中的列名英文
+     * 例如第1列,index为0,解析出来为A列
+     * 第27列,index为26,解析为AA列
+     * 第28列,index为27,解析为AB列
+     *
+     * @param columnIndex 列index
+     * @return 列index所在得英文名
+     */
+    private String getExcelColumnName(int columnIndex) {
+        // 26一循环的次数
+        int columnCircleCount = columnIndex / 26;
+        // 26一循环内的位置
+        int thisCircleColumnIndex = columnIndex % 26;
+        // 26一循环的次数大于0,则视为栏名至少两位
+        String columnPrefix = columnCircleCount == 0 ? "" : StrUtils.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
+        // 从26一循环内取对应的栏位名
+        String columnNext = StrUtils.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
+        // 将二者拼接即为最终的栏位名
+        return columnPrefix + columnNext;
+    }
+
+    /**
+     * <h2>额外表格形式的级联下拉框</h2>
+     *
+     * @param options 额外表格形式存储的下拉可选项
+     */
+    private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
+        String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
+        // 创建联动下拉数据表
+        Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
+        // 将下拉表隐藏
+        workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
+        // 完善横向的一级选项数据表
+        List<String> firstOptions = options.getOptions();
+        Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
+
+        // 创建名称管理器
+        Name name = workbook.createName();
+        // 设置名称管理器的别名
+        name.setNameName(linkedOptionsSheetName);
+        // 以横向第一行创建一级下拉拼接引用位置
+        String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
+                linkedOptionsSheetName,
+                getExcelColumnName(0),
+                getExcelColumnName(firstOptions.size())
+        );
+        // 设置名称管理器的引用位置
+        name.setRefersToFormula(firstOptionsFunction);
+        // 设置数据校验为序列模式,引用的是名称管理器中的别名
+        this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
+
+        for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
+            // 先提取主表中一级下拉的列名
+            String firstOptionsColumnName = getExcelColumnName(columIndex);
+            // 一次循环是每一个一级选项
+            int finalI = columIndex;
+            // 本次循环的一级选项值
+            String thisFirstOptionsValue = firstOptions.get(columIndex);
+            // 创建第一行的数据
+            Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
+                    // 如果不存在则创建第一行
+                    .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
+                    // 第一行当前列
+                    .createCell(columIndex)
+                    // 设置值为当前一级选项值
+                    .setCellValue(thisFirstOptionsValue);
+
+            // 第二行开始,设置第二级别选项参数
+            List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
+            if (CollUtil.isEmpty(secondOptions)) {
+                // 必须保证至少有一个关联选项,否则将导致Excel解析错误
+                secondOptions = Collections.singletonList("暂无_0");
+            }
+
+            // 以该一级选项值创建子名称管理器
+            Name sonName = workbook.createName();
+            // 设置名称管理器的别名
+            sonName.setNameName(thisFirstOptionsValue);
+            // 以第二行该列数据拼接引用位置
+            String sonFunction = String.format("%s!$%s$2:$%s$%d",
+                    linkedOptionsSheetName,
+                    firstOptionsColumnName,
+                    firstOptionsColumnName,
+                    secondOptions.size() + 1
+            );
+            // 设置名称管理器的引用位置
+            sonName.setRefersToFormula(sonFunction);
+            // 数据验证为序列模式,引用到每一个主表中的二级选项位置
+            // 创建子项的名称管理器,只是为了使得Excel可以识别到数据
+            String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
+            for (int i = 0; i < 100; i++) {
+                // 以一级选项对应的主体所在位置创建二级下拉
+                String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
+                // 二级只能主表每一行的每一列添加二级校验
+                markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
+            }
+
+            for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
+                // 从第二行开始填充二级选项
+                int finalRowIndex = rowIndex + 1;
+                int finalColumIndex = columIndex;
+
+                Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
+                        // 没有则创建
+                        .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
+                Optional
+                        // 在本级一级选项所在的列
+                        .ofNullable(row.getCell(finalColumIndex))
+                        // 不存在则创建
+                        .orElseGet(() -> row.createCell(finalColumIndex))
+                        // 设置二级选项值
+                        .setCellValue(secondOptions.get(rowIndex));
+            }
+        }
+
+        currentLinkedOptionsSheetIndex++;
+    }
+
+    /**
+     * 挂载下拉的列,仅限二级选项
+     */
+    private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
+                                          Integer celIndex, DataValidationConstraint constraint) {
+        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+        CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
+        markDataValidationToSheet(helper, sheet, constraint, addressList);
+    }
+
+    /**
+     * <h2>简单下拉框</h2>
+     * 直接将可选项拼接为指定列的数据校验值
+     *
+     * @param celIndex 列index
+     * @param value    下拉选可选值
+     */
+    private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
+        if (ObjectUtil.isEmpty(value)) {
+            return;
+        }
+        this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
+    }
+}

+ 19 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/core/ExcelListener.java

@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.core;
+
+
+import cn.idev.excel.read.listener.ReadListener;
+
+/**
+ * @description ExcelListener
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:37
+ */
+public interface ExcelListener <T> extends ReadListener<T> {
+    ExcelResult<T> getExcelResult();
+}

+ 32 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/core/ExcelResult.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.core;
+
+
+import java.util.List;
+
+/**
+ * @description ExcelResult
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:37
+ */
+public interface ExcelResult<T> {
+    /**
+     * 对象列表
+     */
+    List<T> getList();
+
+    /**
+     * 错误列表
+     */
+    List<String> getErrorList();
+
+    /**
+     * 导入回执
+     */
+    String getAnalysis();
+}

+ 82 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/strategy/DefaultCellStyleStrategy.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.strategy;
+
+
+import cn.idev.excel.metadata.data.WriteCellData;
+import cn.idev.excel.write.handler.context.CellWriteHandlerContext;
+import cn.idev.excel.write.metadata.style.WriteCellStyle;
+import cn.idev.excel.write.metadata.style.WriteFont;
+import cn.idev.excel.write.style.HorizontalCellStyleStrategy;
+import lombok.Getter;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
+
+import java.util.List;
+
+/**
+ * @description DefaultCellStyleStrategy 设置表头和填充内容的样式
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:59
+ */
+public class DefaultCellStyleStrategy extends HorizontalCellStyleStrategy {
+
+    private final WriteCellStyle headWriteCellStyle;
+
+    private final WriteCellStyle contentWriteCellStyle;
+
+    @Getter
+    private final List<Integer> columnIndexes;
+
+    public DefaultCellStyleStrategy(List<Integer> columnIndexes, WriteCellStyle headWriteCellStyle, WriteCellStyle contentWriteCellStyle) {
+        this.columnIndexes = columnIndexes;
+        this.headWriteCellStyle = headWriteCellStyle;
+        this.contentWriteCellStyle = contentWriteCellStyle;
+    }
+
+    // 设置头样式
+    @Override
+    protected void setHeadCellStyle(CellWriteHandlerContext context) {
+        // 获取字体实例
+        WriteFont headWriteFont = new WriteFont();
+        headWriteFont.setFontName("宋体");
+
+        headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+        headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
+        headWriteFont.setFontHeightInPoints((short) 14);
+        headWriteFont.setBold(false);
+        headWriteFont.setFontName("宋体");
+
+        headWriteCellStyle.setWriteFont(headWriteFont);
+        if (stopProcessing(context)) {
+            return;
+        }
+        WriteCellData<?> cellData = context.getFirstCellData();
+        WriteCellStyle.merge(headWriteCellStyle, cellData.getOrCreateStyle());
+    }
+
+    // 设置填充数据样式
+    @Override
+    protected void setContentCellStyle(CellWriteHandlerContext context) {
+        WriteFont contentWriteFont = new WriteFont();
+        contentWriteFont.setFontName("宋体");
+        contentWriteFont.setFontHeightInPoints((short) 12);
+        // 设置数据填充后的实线边框
+        contentWriteCellStyle.setWriteFont(contentWriteFont);
+        contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);
+        contentWriteCellStyle.setBorderTop(BorderStyle.THIN);
+        contentWriteCellStyle.setBorderRight(BorderStyle.THIN);
+        contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);
+        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
+        // 垂直居中
+        contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+        WriteCellData<?> cellData = context.getFirstCellData();
+        WriteCellStyle.merge(contentWriteCellStyle, cellData.getOrCreateStyle());
+    }
+}

+ 87 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/strategy/DefaultColumnWidthStyleStrategy.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.strategy;
+
+
+import cn.idev.excel.enums.CellDataTypeEnum;
+import cn.idev.excel.metadata.Head;
+import cn.idev.excel.metadata.data.WriteCellData;
+import cn.idev.excel.util.MapUtils;
+import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
+import cn.idev.excel.write.style.column.AbstractColumnWidthStyleStrategy;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.poi.ss.usermodel.Cell;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @description DefaultColumnWidthStyleStrategy
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 16:00
+ */
+public class DefaultColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
+    private static final int MAX_COLUMN_WIDTH = 256;
+
+    private final Map<Integer, Map<Integer, Integer>> cache = MapUtils.newHashMapWithExpectedSize(8);
+
+    @Override
+    protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex,
+                                  Boolean isHead) {
+        boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
+        if (!needSetWidth) {
+            return;
+        }
+        Map<Integer, Integer> maxColumnWidthMap = cache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16, 0.75f));
+        Integer columnWidth = getColumnWidth(cellDataList, cell, isHead);
+        if (columnWidth < 0) {
+            return;
+        }
+        if (columnWidth > MAX_COLUMN_WIDTH) {
+            columnWidth = MAX_COLUMN_WIDTH;
+        }
+        Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
+        if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
+            maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
+            writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
+        }
+    }
+
+    private Integer getColumnWidth(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
+        if (isHead) {
+            return cell.getStringCellValue().getBytes().length;
+        }
+        WriteCellData<?> cellData = cellDataList.getFirst();
+        CellDataTypeEnum type = cellData.getType();
+        if (type == null) {
+            return -1;
+        }
+        return switch (type) {
+            case STRING -> getStringWidth(cellData.getStringValue());
+            case BOOLEAN -> cellData.getBooleanValue().toString().getBytes().length + 10;
+            case NUMBER -> cellData.getNumberValue().toString().getBytes().length + 10;
+            case DATE -> cellData.getDateValue().toString().getBytes().length + 10;
+            default -> -1;
+        };
+    }
+
+    private int getStringWidth(String str) {
+        if (str == null) {
+            return 0;
+        }
+        int width = str.length();
+        for (char ch : str.toCharArray()) {
+            // 中文字符
+            if (ch >= 0x4E00 && ch <= 0x9FA5) {
+                width++;
+            }
+        }
+        // 添加额外的填充以提高可读性
+        return width + 5;
+    }
+}

+ 268 - 0
eco-common/com-excel/src/main/java/org/eco/vip/excel/utils/ExcelUtils.java

@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2025 GaoKunW
+ *
+ */
+
+package org.eco.vip.excel.utils;
+
+
+import cn.hutool.core.util.IdUtil;
+import cn.idev.excel.FastExcel;
+import cn.idev.excel.FastExcelFactory;
+import cn.idev.excel.write.builder.ExcelWriterSheetBuilder;
+import cn.idev.excel.write.metadata.style.WriteCellStyle;
+import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eco.vip.core.utils.FileUtils;
+import org.eco.vip.core.utils.StrUtils;
+import org.eco.vip.excel.convert.ExcelBigNumberConvert;
+import org.eco.vip.excel.core.CellMergeStrategy;
+import org.eco.vip.excel.core.DefaultExcelListener;
+import org.eco.vip.excel.core.DropDownOptions;
+import org.eco.vip.excel.core.ExcelDownHandler;
+import org.eco.vip.excel.core.ExcelListener;
+import org.eco.vip.excel.core.ExcelResult;
+import org.eco.vip.excel.strategy.DefaultCellStyleStrategy;
+import org.eco.vip.excel.strategy.DefaultColumnWidthStyleStrategy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @description ExcelUtils
+ *
+ * @author GaoKunW
+ * @date 2025/7/21 15:34
+ */
+public class ExcelUtils {
+    private ExcelUtils() {
+        throw new IllegalStateException("ExcelUtils class Illegal");
+    }
+
+    /**
+     * 同步导入(适用于小数据量)
+     *
+     * @param is 输入流
+     * @return 转换后集合
+     */
+    public static <T> List<T> importExcel(InputStream is, Class<T> clazz) {
+        return FastExcel.read(is).head(clazz).autoCloseStream(false).sheet().doReadSync();
+    }
+
+    /**
+     * 使用校验监听器 异步导入 同步返回
+     *
+     * @param is         输入流
+     * @param clazz      对象类型
+     * @param isValidate 是否 Validator 检验 默认为是
+     * @return 转换后集合
+     */
+    public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) {
+        DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate);
+        FastExcel.read(is, clazz, listener).sheet().doRead();
+        return listener.getExcelResult();
+    }
+
+    /**
+     * 使用自定义监听器 异步导入 自定义返回
+     *
+     * @param is       输入流
+     * @param clazz    对象类型
+     * @param listener 自定义监听器
+     * @return 转换后集合
+     */
+    public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {
+        FastExcel.read(is, clazz, listener).sheet().doRead();
+        return listener.getExcelResult();
+    }
+
+    /**
+     * 编码文件名
+     */
+    public static String encodingFilename(String filename) {
+        return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx";
+    }
+
+    /**
+     * 重置响应体
+     */
+    private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
+        String filename = encodingFilename(sheetName);
+        FileUtils.setAttachmentResponseHeader(response, filename);
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param response  响应体
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response) {
+        try {
+            resetResponse(sheetName, response);
+            ServletOutputStream os = response.getOutputStream();
+            exportExcel(list, sheetName, clazz, false, os, null);
+        } catch (IOException e) {
+            throw new RuntimeException("导出Excel异常");
+        }
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param merge     是否合并单元格
+     * @param os        输出流
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
+                                       OutputStream os, List<DropDownOptions> options) {
+        ExcelWriterSheetBuilder builder = FastExcel.write(os, clazz)
+                .autoCloseStream(false)
+                // 自动适配
+                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+                // 大数值自动转换 防止失真
+                .registerConverter(new ExcelBigNumberConvert())
+                .sheet(sheetName);
+        if (merge) {
+            // 合并处理器
+            builder.registerWriteHandler(new CellMergeStrategy(list, true));
+        }
+        // 添加下拉框操作
+        builder.registerWriteHandler(new ExcelDownHandler(options));
+        builder.doWrite(list);
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param os        输出流
+     * @param options   下拉框选项
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
+        ExcelWriterSheetBuilder builder = FastExcelFactory.write(os, clazz).autoCloseStream(false)
+                // 列宽自动适配
+                .registerWriteHandler(new DefaultColumnWidthStyleStrategy())
+                // 表格样式
+                .registerWriteHandler(new DefaultCellStyleStrategy(Arrays.asList(0, 1), new WriteCellStyle(), new WriteCellStyle()))
+                .sheet(sheetName);
+        // 添加下拉框操作
+        builder.registerWriteHandler(new ExcelDownHandler(options));
+        builder.doWrite(list);
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param os        输出流
+     * @param options   下拉框选项
+     * @param merge     是否合并单元格
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options, boolean merge) {
+        ExcelWriterSheetBuilder builder = FastExcelFactory.write(os, clazz).autoCloseStream(false)
+                // 列宽自动适配
+                .registerWriteHandler(new DefaultColumnWidthStyleStrategy())
+                // 表格样式
+                .registerWriteHandler(new DefaultCellStyleStrategy(Arrays.asList(0, 1), new WriteCellStyle(), new WriteCellStyle()))
+                .sheet(sheetName);
+        if (merge) {
+            // 合并处理器
+            builder.registerWriteHandler(new CellMergeStrategy(list, true));
+        }
+        // 添加下拉框操作
+        builder.registerWriteHandler(new ExcelDownHandler(options));
+        builder.doWrite(list);
+    }
+
+    /**
+     * 将表达式转换为列表。
+     * 该方法根据指定的分隔符,将字符串表达式转换为字符串列表。
+     *
+     * @param converterExp 要转换的字符串表达式
+     * @param separator 用于分隔表达式的分隔符
+     * @return 转换后的字符串列表
+     */
+    public static List<String> listByExp(String converterExp, String separator) {
+        List<String> list = new ArrayList<>();
+        String[] convertSource = converterExp.split(separator);
+        for (String item : convertSource) {
+            String[] itemArray = item.split("=");
+            list.add(itemArray[0]);
+        }
+        return list;
+    }
+
+    /**
+     * 解析导出值 value -> label
+     *
+     * @param propertyValue 参数值
+     * @param converterExp  翻译注解
+     * @param separator     分隔符
+     * @return 解析后值
+     */
+    public static String convertByExp(String propertyValue, String converterExp, String separator) {
+        StringBuilder propertyString = new StringBuilder();
+        String[] convertSource = converterExp.split(StrUtils.SEPARATOR);
+        for (String item : convertSource) {
+            String[] itemArray = item.split("=");
+            if (StrUtils.containsAny(propertyValue, separator)) {
+                for (String value : propertyValue.split(separator)) {
+                    if (itemArray[0].equals(value)) {
+                        propertyString.append(itemArray[1]).append(separator);
+                        break;
+                    }
+                }
+            } else {
+                if (itemArray[0].equals(propertyValue)) {
+                    return itemArray[1];
+                }
+            }
+        }
+        return StrUtils.removeSuffix(propertyString.toString(), separator);
+    }
+
+    /**
+     * 反向解析值 label -> value
+     *
+     * @param propertyValue 参数值
+     * @param converterExp  翻译注解
+     * @param separator     分隔符
+     * @return 解析后值
+     */
+    public static String reverseByExp(String propertyValue, String converterExp, String separator) {
+        StringBuilder propertyString = new StringBuilder();
+        String[] convertSource = converterExp.split(StrUtils.SEPARATOR);
+        for (String item : convertSource) {
+            String[] itemArray = item.split("=");
+            if (StrUtils.containsAny(propertyValue, separator)) {
+                for (String value : propertyValue.split(separator)) {
+                    if (itemArray[1].equals(value)) {
+                        propertyString.append(itemArray[0]).append(separator);
+                        break;
+                    }
+                }
+            } else {
+                if (itemArray[1].equals(propertyValue)) {
+                    return itemArray[0];
+                }
+            }
+        }
+        return StrUtils.removeSuffix(propertyString.toString(), separator);
+    }
+}

+ 0 - 5
eco-common/com-orm/pom.xml

@@ -12,11 +12,6 @@
     <packaging>jar</packaging>
 
     <dependencies>
-        <!-- com-core -->
-<!--        <dependency>-->
-<!--            <groupId>org.eco.vip</groupId>-->
-<!--            <artifactId>com-core</artifactId>-->
-<!--        </dependency>-->
         <dependency>
             <groupId>org.eco.vip</groupId>
             <artifactId>com-security</artifactId>

+ 1 - 0
eco-common/pom.xml

@@ -17,5 +17,6 @@
         <module>com-core</module>
         <module>com-orm</module>
         <module>com-security</module>
+        <module>com-excel</module>
     </modules>
 </project>