当前位置: 首页 > news >正文

多级数据结构导出Excel工具类,支持多级数据导入导出,支持自定义字体颜色和背景颜色,支持自定义转化器

Excel 工具类使用文档

📖 概述

本Excel工具类基于Apache POI封装,提供了简单易用的Excel导入导出功能。通过统一的@Excel注解配置,支持多级数据结构、样式配置、自定义转换器等高级功能。

✨ 核心特性

  • 🎯 统一注解:合并了原有的3个注解为一个@Excel注解
  • 📊 多级结构:支持对象嵌套、集合等复杂数据结构
  • 🎨 样式配置:支持字体颜色、背景颜色等样式设置
  • 🔄 自定义转换:支持数据转换器,实现双向转换
  • 🚀 高性能:样式缓存机制,优化内存使用
  • 🔒 安全可靠:解决Zip bomb安全问题

📋 @Excel 注解属性详解

🏷️ 基础属性

属性类型默认值必填说明示例
nameString""Excel列标题名称@Excel(name = "姓名")
indexint0Excel列索引(从0开始)@Excel(index = 3)
defaultValueString""单元格为空时的默认值@Excel(defaultValue = "未填写")

🔧 字段类型配置

属性类型默认值说明使用场景
fieldTypeFieldTypeCOLUMN字段处理类型控制字段的处理方式
FieldType 枚举值说明
枚举值说明适用场景示例
COLUMN普通列字段基本数据类型字段String name, Integer age
OBJECT对象字段嵌套对象ClassInfo classInfo
COLLECTION集合字段对象集合List<Student> students

🏷️ 分组和排序

属性类型默认值说明示例
isKeybooleanfalse是否为分组关键字段用于多级数据的分组依据

🎨 样式配置

属性类型默认值说明可选值示例
fontColorIndexedColorsAUTOMATIC字体颜色BLACK, WHITE, RED, BLUE
backgroundColorIndexedColorsAUTOMATIC背景颜色YELLOW, LIGHT_GREEN, PINK
常用颜色枚举值
颜色类别枚举值
基础色BLACK, WHITE, RED, GREEN, BLUE, YELLOW, ORANGE, PINK
浅色系LIGHT_BLUE, LIGHT_GREEN, LIGHT_YELLOW, LIGHT_ORANGE
深色系DARK_BLUE, DARK_GREEN, DARK_RED, DARK_YELLOW
灰色系GREY_25_PERCENT, GREY_40_PERCENT, GREY_50_PERCENT

🔄 转换器配置

属性类型默认值说明示例
converterClass<? extends ExcelConverter>DefaultConverter.class自定义转换器GenderConverter.class
paramsString[]{}转换器参数params = {"param1", "param2"}

📦 集合类型配置

属性类型默认值说明使用条件
typeClass<?>Object.class集合元素类型fieldType 不为 COLUMN 时使用

🚀 使用示例

1️⃣ 基础实体类配置

@Data
public class Student {@Excel(name = "学号", index = 0)private String studentId;@Excel(name = "姓名", index = 1, fontColor = IndexedColors.BLUE)private String name;@Excel(name = "年龄", index = 2)private Integer age;@Excel(name = "性别", index = 3, converter = GenderConverter.class)private String gender;@Excel(name = "成绩", index = 4, converter = ScoreConverter.class)private Double score;@Excel(name = "备注", index = 5, defaultValue = "无")private String remark;
}

2️⃣ 多级数据结构配置

@Data
public class School {@Excel(name = "学校名称", index = 0, isKey = true)private String schoolName;@Excel(name = "学校编码", index = 1)private String schoolCode;@Excel(name = "建校日期", index = 2)private Date establishDate;// 班级信息(对象类型)@Excel(name = "班级信息", fieldType = Excel.FieldType.OBJECT, type = ClassInfo.class)private ClassInfo classInfo;// 学生列表(集合类型)@Excel(name = "学生列表", fieldType = Excel.FieldType.COLLECTION, type = Student.class)private List<Student> students;
}

3️⃣ 样式配置示例

@Data
public class ClassInfo {@Excel(name = "班级编码", index = 5, isKey = true)private String classCode;// 黄色背景 + 黑色字体@Excel(name = "班级名称", index = 6, fontColor = IndexedColors.BLACK, backgroundColor = IndexedColors.YELLOW)private String className;// 浅绿色背景@Excel(name = "年级", index = 7, backgroundColor = IndexedColors.LIGHT_GREEN)private String grade;
}

🔄 自定义转换器

转换器接口定义

public interface ExcelConverter<T, E> {/*** 导出时:将Java对象转换为Excel显示值*/E convertToExcel(T value, String[] params);/*** 导入时:将Excel单元格值转换为Java对象*/T convertFromExcel(E value, String[] params);/*** 获取单元格样式(可选实现)*/default CellStyleInfo getCellStyle(T value, String[] params) {return null;}
}

转换器实现示例

1️⃣ 性别转换器(支持动态样式)
public class GenderConverter implements ExcelConverter<String, String> {@Overridepublic String convertToExcel(String value, String[] params) {return "1".equals(value) ? "男" : "0".equals(value) ? "女" : value;}@Overridepublic String convertFromExcel(String value, String[] params) {return "男".equals(value) ? "1" : "女".equals(value) ? "0" : value;}@Overridepublic CellStyleInfo getCellStyle(String value, String[] params) {if ("1".equals(value)) {// 男生:蓝色背景 + 白色字体return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.BLUE);} else if ("0".equals(value)) {// 女生:粉色背景 + 黑色字体return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.PINK);}return null;}
}
2️⃣ 分数转换器(支持动态样式)
public class ScoreConverter implements ExcelConverter<Double, String> {@Overridepublic String convertToExcel(Double value, String[] params) {return value == null ? "未评分" : String.format("%.1f", value);}@Overridepublic Double convertFromExcel(String value, String[] params) {if (value == null || "未评分".equals(value)) {return null;}try {return Double.parseDouble(value);} catch (NumberFormatException e) {return null;}}@Overridepublic CellStyleInfo getCellStyle(Double value, String[] params) {if (value == null) return null;if (value >= 90) {// 优秀:绿色背景 + 白色字体return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.GREEN);} else if (value >= 80) {// 良好:浅绿色背景 + 黑色字体return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.LIGHT_GREEN);} else if (value >= 70) {// 中等:黄色背景 + 黑色字体return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.YELLOW);} else if (value >= 60) {// 及格:橙色背景 + 黑色字体return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.ORANGE);} else {// 不及格:红色背景 + 白色字体return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.RED);}}
}

🎨 样式配置详解

静态样式(注解配置)

// 直接在注解中配置固定样式
@Excel(name = "重要字段", index = 1, fontColor = IndexedColors.WHITE, backgroundColor = IndexedColors.RED)
private String importantField;

动态样式(转换器配置)

// 通过转换器根据值动态设置样式
@Excel(name = "状态", index = 2, converter = StatusConverter.class)
private String status;public class StatusConverter implements ExcelConverter<String, String> {@Overridepublic CellStyleInfo getCellStyle(String value, String[] params) {switch (value) {case "success": return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.GREEN);case "warning": return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.YELLOW);case "error": return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.RED);default: return null;}}
}

样式优先级

  1. 动态样式(转换器返回)- 最高优先级
  2. 静态样式(注解配置)- 次优先级
  3. 默认样式(系统默认)- 最低优先级

🔧 工具类调用方法

导出Excel

// Controller示例
@GetMapping("/export")
public void exportData(HttpServletResponse response) {List<School> data = schoolService.getAllData();ExcelUtils.exportExcel(data, School.class, "学校数据.xlsx", response);
}

导入Excel

// Controller示例
@PostMapping("/import")
public String importData(@RequestParam("file") MultipartFile file) {try (InputStream inputStream = file.getInputStream()) {List<School> dataList = ExcelUtils.importExcel(inputStream, School.class);schoolService.batchSave(dataList);return "导入成功,共导入 " + dataList.size() + " 条记录";} catch (Exception e) {return "导入失败:" + e.getMessage();}
}

⚡ 性能优化特性

样式缓存机制

  • 自动缓存:相同颜色组合的样式自动复用
  • 内存优化:避免创建重复样式,减少内存占用
  • 缓存清理:每次导出开始时自动清理缓存

Zip Bomb 安全处理

  • 自动调整:导入时临时调整POI安全限制
  • 自动恢复:导入完成后自动恢复原始设置
  • 安全保障:不影响其他应用的安全性

📝 最佳实践

1️⃣ 注解配置建议

场景建议配置示例
基础字段只配置name和index@Excel(name = "姓名", index = 1)
关键字段添加颜色突出显示@Excel(name = "ID", index = 0, fontColor = IndexedColors.BLUE)
可空字段设置默认值@Excel(name = "备注", index = 5, defaultValue = "无")
分组字段标记为key@Excel(name = "部门", index = 0, isKey = true)

2️⃣ 转换器使用建议

场景建议原因
简单映射使用静态样式性能更好,配置简单
复杂逻辑使用转换器灵活性更强,支持动态样式
性能敏感缓存转换结果避免重复计算

3️⃣ 样式配置建议

原则说明示例
对比鲜明确保文字清晰可读深色背景配浅色文字
语义明确用颜色表达含义红色表示错误,绿色表示成功
数量适中避免过多颜色混乱建议不超过5种颜色组合

4️⃣ 错误处理建议

// 推荐的错误处理方式
@Override
public Double convertFromExcel(String value, String[] params) {try {return value == null ? null : Double.parseDouble(value);} catch (NumberFormatException e) {log.warn("数值转换失败: {}", value);return null; // 返回null而不是抛异常}
}

🚨 注意事项

⚠️ 配置约束

  1. 索引唯一性:同一层级的字段index不能重复
  2. 类型匹配fieldType与字段类型要匹配
  3. 转换器泛型:转换器泛型要与字段类型匹配

⚠️ 性能考虑

  1. 大数据量:建议分批处理,单次不超过10000条记录
  2. 复杂样式:过多动态样式可能影响性能
  3. 内存使用:注意大文件导入的内存占用

⚠️ 兼容性说明

  • POI版本:基于Apache POI 5.x开发
  • Java版本:需要Java 8+
  • 文件格式:支持.xlsx格式,建议不使用.xls

源码

package org.whh.excel.annotation;import org.apache.poi.ss.usermodel.IndexedColors;
import org.whh.excel.converter.ExcelConverter;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** Excel统一注解* 合并了原来的 ExcelColumn、ExcelConvert、ExcelObject 三个注解的功能* 用于标识实体类字段的Excel处理方式** @author whh*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Excel {// ========== ExcelColumn 相关属性 ==========/*** 列名称*/String name() default "";/*** 列索引(从0开始),如果不指定则按字段声明顺序*/int index() default -1;/*** 列宽度*/int width() default 20;/*** 是否必填*/boolean required() default false;/*** 日期格式(当字段为日期类型时使用)*/String dateFormat() default "yyyy-MM-dd HH:mm:ss";/*** 数字格式(当字段为数字类型时使用)*/String numberFormat() default "";/*** 默认值*/String defaultValue() default "";/*** 是否为关键字段,用于多级数据导入时的分组标识* 当多行数据的所有关键字段值都相同时,这些行会被归为同一个对象* <p>* 使用示例:* - 如果只需要按学校编码分组:在schoolCode字段上设置 isKey = true* - 如果需要按姓名+年龄+性别组合分组:在name、age、gender三个字段上都设置 isKey = true*/boolean isKey() default false;// ========== 样式相关属性 ==========/*** 字体颜色配置* 默认为 AUTOMATIC 表示使用系统默认颜色*/IndexedColors fontColor() default IndexedColors.AUTOMATIC;/*** 背景颜色配置* 默认为 AUTOMATIC 表示无背景色*/IndexedColors backgroundColor() default IndexedColors.AUTOMATIC;// ========== ExcelConvert 相关属性 ==========/*** 转换器类* 默认为 DefaultConverter.class 表示不使用转换器*/Class<? extends ExcelConverter<?, ?>> converter() default DefaultConverter.class;/*** 转换参数(可选)*/String[] params() default {};// ========== ExcelObject 相关属性 ==========/*** 对象类型或集合元素类型* 当 fieldType = OBJECT 时,表示对象类型* 当 fieldType = COLLECTION 时,表示集合元素类型* 当 fieldType = COLUMN 时,此属性无意义*/Class<?> type() default Object.class;/*** 层级顺序(数字越小层级越高)*/int order() default 0;// ========== 通用属性 ==========/*** 说明*/String description() default "";/*** 字段类型枚举*/FieldType fieldType() default FieldType.COLUMN;/*** 字段类型枚举定义*/enum FieldType {/*** 普通列字段*/COLUMN,/*** 对象字段*/OBJECT,/*** 集合字段*/COLLECTION}/*** 默认转换器(表示不使用转换器)*/class DefaultConverter implements ExcelConverter<Object, Object> {@Overridepublic Object convertToExcel(Object value, String[] params) {return value;}@Overridepublic Object convertFromExcel(Object value, String[] params) {return value;}@Overridepublic org.whh.excel.converter.CellStyleInfo getCellStyle(Object value, String[] params) {return null; // 默认转换器不提供样式}}
}
package org.whh.excel.converter;import lombok.Data;
import org.apache.poi.ss.usermodel.IndexedColors;/*** Excel单元格样式信息类* 用于在转换器中返回单元格的样式配置** @author whh*/
@Data
public class CellStyleInfo {/*** 字体颜色* 使用 IndexedColors 枚举,如 IndexedColors.BLACK, IndexedColors.WHITE 等* 默认为 null 表示使用注解或系统默认颜色*/private IndexedColors fontColor;/*** 背景颜色* 使用 IndexedColors 枚举,如 IndexedColors.YELLOW, IndexedColors.LIGHT_BLUE 等* 默认为 null 表示使用注解或系统默认颜色*/private IndexedColors backgroundColor;/*** 默认构造函数*/public CellStyleInfo() {}/*** 构造函数** @param fontColor       字体颜色* @param backgroundColor 背景颜色*/public CellStyleInfo(IndexedColors fontColor, IndexedColors backgroundColor) {this.fontColor = fontColor;this.backgroundColor = backgroundColor;}/*** 创建只有字体颜色的样式** @param fontColor 字体颜色* @return 样式信息*/public static CellStyleInfo withFontColor(IndexedColors fontColor) {return new CellStyleInfo(fontColor, null);}/*** 创建只有背景颜色的样式** @param backgroundColor 背景颜色* @return 样式信息*/public static CellStyleInfo withBackgroundColor(IndexedColors backgroundColor) {return new CellStyleInfo(null, backgroundColor);}/*** 创建同时设置字体和背景颜色的样式** @param fontColor       字体颜色* @param backgroundColor 背景颜色* @return 样式信息*/public static CellStyleInfo withColors(IndexedColors fontColor, IndexedColors backgroundColor) {return new CellStyleInfo(fontColor, backgroundColor);}/*** 检查是否有字体颜色设置*/public boolean hasFontColor() {return fontColor != null && fontColor != IndexedColors.AUTOMATIC;}/*** 检查是否有背景颜色设置*/public boolean hasBackgroundColor() {return backgroundColor != null && backgroundColor != IndexedColors.AUTOMATIC;}/*** 检查是否有任何样式设置*/public boolean hasAnyStyle() {return hasFontColor() || hasBackgroundColor();}
}
package org.whh.excel.converter;/*** Excel字段转换器接口* 用于实现字段值的双向转换** @param <T> 实体类字段类型* @param <E> Excel中显示的类型* @author whh*/
public interface ExcelConverter<T, E> {/*** 导出时的转换:将实体类字段值转换为Excel中显示的值** @param value  实体类字段值* @param params 转换参数* @return Excel中显示的值*/E convertToExcel(T value, String[] params);/*** 导入时的转换:将Excel中的值转换为实体类字段值** @param value  Excel中的值* @param params 转换参数* @return 实体类字段值*/T convertFromExcel(E value, String[] params);/*** 根据字段值返回单元格样式信息(可选实现)* 实现此方法可以根据不同的字段值设置不同的颜色* <p>* 使用示例:* - 性别字段:男生返回蓝色背景+白色字体,女生返回粉色背景+黑色字体* - 状态字段:成功返回绿色背景,失败返回红色背景* - 分数字段:高分返回绿色字体,低分返回红色字体** @param value  实体类字段值* @param params 转换参数* @return 样式信息,返回 null 表示使用默认样式*/default CellStyleInfo getCellStyle(T value, String[] params) {return null; // 默认实现返回 null,表示不设置特殊样式}
}
package org.whh.excel.converter;import org.apache.poi.ss.usermodel.IndexedColors;/*** 性别转换器* 将"男/女"转换为"1/0",反之亦然* 支持根据性别设置不同的单元格样式** @author whh*/
public class GenderConverter implements ExcelConverter<String, String> {@Overridepublic String convertToExcel(String value, String[] params) {if ("1".equals(value)) {return "男";} else if ("0".equals(value)) {return "女";}return value;}@Overridepublic String convertFromExcel(String value, String[] params) {if ("男".equals(value)) {return "1";} else if ("女".equals(value)) {return "0";}return value;}@Overridepublic CellStyleInfo getCellStyle(String value, String[] params) {if ("1".equals(value)) {// 男生:蓝色背景 + 白色字体return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.BLUE);} else if ("0".equals(value)) {// 女生:粉色背景 + 黑色字体return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.PINK);}// 其他值使用默认样式return null;}
}
package org.whh.excel.util;import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.whh.excel.annotation.Excel;
import org.whh.excel.converter.CellStyleInfo;
import org.whh.excel.converter.ExcelConverter;import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;/*** 通用Excel导入导出工具类* 基于注解自动处理多级数据结构的导入导出* 支持@ExcelColumn、@ExcelObject、@ExcelConvert注解** @author whh*/
@Slf4j
public class ExcelUtils {// ========== 表头样式全局配置 ==========// 修改这些配置即可改变所有Excel导出的表头样式/*** 表头背景颜色配置* 常用颜色选项:* - IndexedColors.DARK_BLUE (深蓝色,商务风格)* - IndexedColors.BLUE (蓝色)* - IndexedColors.LIGHT_BLUE (浅蓝色)* - IndexedColors.GREEN (绿色)* - IndexedColors.DARK_GREEN (深绿色)* - IndexedColors.GREY_50_PERCENT (灰色)* - IndexedColors.ORANGE (橙色)* - IndexedColors.RED (红色)*/private static final IndexedColors HEADER_BACKGROUND_COLOR = IndexedColors.AUTOMATIC;/*** 表头字体颜色配置* 常用颜色选项:* - IndexedColors.WHITE (白色,适合深色背景)* - IndexedColors.BLACK (黑色,适合浅色背景)* - IndexedColors.BLUE (蓝色)* - IndexedColors.RED (红色)* - IndexedColors.GREEN (绿色)*/private static final IndexedColors HEADER_FONT_COLOR = IndexedColors.BLACK;/*** 表头字体大小配置 (推荐范围: 10-16)*/private static final short HEADER_FONT_SIZE = 12;/*** 表头是否加粗配置* true: 加粗显示 (推荐)* false: 正常字体*/private static final boolean HEADER_FONT_BOLD = true;/*** 表头边框样式配置* - BorderStyle.THIN (细边框,推荐)* - BorderStyle.MEDIUM (中等边框)* - BorderStyle.THICK (粗边框)* - BorderStyle.NONE (无边框)*/private static final BorderStyle HEADER_BORDER_STYLE = BorderStyle.THIN;/*** 表头边框颜色配置* 常用:IndexedColors.BLACK (黑色边框)*/private static final IndexedColors HEADER_BORDER_COLOR = IndexedColors.BLACK;/*** 导出数据到Excel(通用方法)*/public static <T> void exportExcel(List<T> data, Class<T> clazz, String fileName, HttpServletResponse response) {// 清理样式缓存,避免不同工作簿间的样式冲突clearStyleCache();try (Workbook workbook = new XSSFWorkbook()) {Sheet sheet = workbook.createSheet();// 解析实体类结构List<FieldInfo> allFields = parseEntityStructure(clazz);// 创建表头createHeader(sheet, allFields);// 填充数据int currentRow = 1;for (T item : data) {currentRow = fillMultiLevelData(sheet, item, allFields, currentRow);}// 设置响应头response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName + ".xlsx", "UTF-8"));// 设置缓存控制,避免代理服务器问题response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");response.setHeader("Pragma", "no-cache");response.setDateHeader("Expires", 0);workbook.write(response.getOutputStream());log.info("Excel导出成功,文件名: {}, 数据量: {} 条", fileName, data.size());} catch (org.springframework.web.context.request.async.AsyncRequestNotUsableException e) {// 客户端取消下载或连接中断,这是正常情况,不需要报错log.info("客户端连接中断 (可能是多线程下载工具如迅雷等导致): {}", e.getMessage());} catch (java.io.IOException e) {if (e.getMessage() != null && (e.getMessage().contains("你的主机中的软件中止了一个已建立的连接") || e.getMessage().contains("Connection reset") || e.getMessage().contains("Broken pipe"))) {// 网络连接中断,通常是客户端问题(多线程下载工具常见)log.info("客户端网络连接中断 (通常是多线程下载工具如迅雷等的正常行为,文件仍会正确下载): {}", e.getMessage());} else {log.error("Excel导出时发生IO异常", e);throw new RuntimeException("Excel导出失败", e);}} catch (org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException e) {// 检查是否是因为客户端中断导致的POI异常Throwable cause = e.getCause();if (cause instanceof org.apache.poi.openxml4j.exceptions.OpenXML4JException) {// 进一步检查根本原因String message = e.getMessage();if (message != null && (message.contains("failed to be saved in the stream") || message.contains("Fail to save"))) {log.info("Excel文件保存过程中客户端连接中断 (通常是多线程下载工具导致,文件会正确下载): {}", message);return; // 不抛出异常,因为这是正常的多线程下载行为}}log.error("Excel导出失败", e);throw new RuntimeException("Excel导出失败", e);} catch (Exception e) {log.error("Excel导出失败", e);throw new RuntimeException("Excel导出失败", e);}}/*** 导入Excel数据(通用方法)*/public static <T> List<T> importExcel(InputStream inputStream, Class<T> clazz) {List<T> result;// 保存原始的压缩率限制double originalMinInflateRatio = ZipSecureFile.getMinInflateRatio();try {// 临时调整压缩率限制,允许处理包含大量样式的Excel文件// 从默认的 0.01 调整到 0.005,以支持样式丰富的Excel文件ZipSecureFile.setMinInflateRatio(0.005);try (Workbook workbook = WorkbookFactory.create(inputStream)) {Sheet sheet = workbook.getSheetAt(0);// 解析实体类结构List<FieldInfo> allFields = parseEntityStructure(clazz);// 读取数据并构建对象result = parseMultiLevelData(sheet, clazz, allFields);log.info("Excel导入成功,共导入 {} 条记录", result.size());}} catch (Exception e) {log.error("Excel导入失败", e);throw new RuntimeException("Excel导入失败", e);} finally {// 恢复原始的压缩率限制ZipSecureFile.setMinInflateRatio(originalMinInflateRatio);log.debug("已恢复POI安全设置,压缩率限制: {}", originalMinInflateRatio);}return result;}/*** 解析实体类结构,获取所有层级的字段信息*/private static <T> List<FieldInfo> parseEntityStructure(Class<T> clazz) {List<FieldInfo> allFields = new ArrayList<>();collectAllFields(clazz, allFields, 0);// 按层级和索引排序allFields.sort((a, b) -> {if (a.level != b.level) {return Integer.compare(a.level, b.level);}return Integer.compare(a.columnIndex, b.columnIndex);});// 重新分配列索引int currentIndex = 0;for (FieldInfo fieldInfo : allFields) {if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {fieldInfo.columnIndex = currentIndex++;}}return allFields;}/*** 递归收集所有字段信息*/private static void collectAllFields(Class<?> clazz, List<FieldInfo> allFields, int level) {Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {field.setAccessible(true);Excel excel = field.getAnnotation(Excel.class);if (excel != null) {FieldInfo fieldInfo = new FieldInfo();fieldInfo.field = field;fieldInfo.excel = excel;fieldInfo.level = level;fieldInfo.fieldType = excel.fieldType();if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {fieldInfo.columnIndex = excel.index() != -1 ? excel.index() : Integer.MAX_VALUE;}// 初始化转换器if (excel.converter() != Excel.DefaultConverter.class) {try {@SuppressWarnings("unchecked") ExcelConverter<Object, Object> converter = (ExcelConverter<Object, Object>) excel.converter().getDeclaredConstructor().newInstance();fieldInfo.converter = converter;} catch (Exception e) {log.warn("初始化转换器失败: {}", excel.converter().getName());}}allFields.add(fieldInfo);// 如果是对象或集合类型,递归收集子字段if (fieldInfo.fieldType == Excel.FieldType.OBJECT || fieldInfo.fieldType == Excel.FieldType.COLLECTION) {Class<?> actualType = getActualType(field);if (actualType != Object.class) {collectAllFields(actualType, allFields, level + 1);}}}}}/*** 获取字段的实际类型*/private static Class<?> getActualType(Field field) {Excel excel = field.getAnnotation(Excel.class);if (excel != null && excel.type() != Object.class) {return excel.type();}Type genericType = field.getGenericType();if (genericType instanceof ParameterizedType) {ParameterizedType paramType = (ParameterizedType) genericType;Type[] actualTypeArguments = paramType.getActualTypeArguments();if (actualTypeArguments.length > 0) {return (Class<?>) actualTypeArguments[0];}}return field.getType();}/*** 创建表头*/private static void createHeader(Sheet sheet, List<FieldInfo> allFields) {Row headerRow = sheet.createRow(0);// 创建表头样式CellStyle headerStyle = createHeaderStyle(sheet.getWorkbook());for (FieldInfo fieldInfo : allFields) {if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {Cell cell = headerRow.createCell(fieldInfo.columnIndex);String headerName = "";if (fieldInfo.excel != null && StringUtils.isNotBlank(fieldInfo.excel.name())) {headerName = fieldInfo.excel.name();} else {headerName = fieldInfo.field.getName();}cell.setCellValue(headerName);// 应用表头样式cell.setCellStyle(headerStyle);// 设置列宽if (fieldInfo.excel != null && fieldInfo.excel.width() > 0) {sheet.setColumnWidth(fieldInfo.columnIndex, fieldInfo.excel.width() * 256);} else {sheet.setColumnWidth(fieldInfo.columnIndex, 20 * 256);}}}}/*** 创建表头样式*/private static CellStyle createHeaderStyle(Workbook workbook) {CellStyle headerStyle = workbook.createCellStyle();// 创建字体(使用全局配置)Font headerFont = workbook.createFont();headerFont.setBold(HEADER_FONT_BOLD);  // 使用配置的加粗设置headerFont.setFontHeightInPoints(HEADER_FONT_SIZE);  // 使用配置的字体大小headerFont.setColor(HEADER_FONT_COLOR.getIndex());  // 使用配置的字体颜色// 将字体应用到样式headerStyle.setFont(headerFont);// 设置背景色(使用全局配置)headerStyle.setFillForegroundColor(HEADER_BACKGROUND_COLOR.getIndex());headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);// 设置对齐方式headerStyle.setAlignment(HorizontalAlignment.CENTER);  // 水平居中headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);  // 垂直居中// 设置边框(使用全局配置)headerStyle.setBorderTop(HEADER_BORDER_STYLE);headerStyle.setBorderBottom(HEADER_BORDER_STYLE);headerStyle.setBorderLeft(HEADER_BORDER_STYLE);headerStyle.setBorderRight(HEADER_BORDER_STYLE);// 设置边框颜色(使用全局配置)headerStyle.setTopBorderColor(HEADER_BORDER_COLOR.getIndex());headerStyle.setBottomBorderColor(HEADER_BORDER_COLOR.getIndex());headerStyle.setLeftBorderColor(HEADER_BORDER_COLOR.getIndex());headerStyle.setRightBorderColor(HEADER_BORDER_COLOR.getIndex());return headerStyle;}// 样式缓存,避免创建重复样式(线程安全)private static final Map<String, CellStyle> styleCache = new ConcurrentHashMap<>();/*** 清理样式缓存* 在每次导出开始时调用,避免不同工作簿间的样式冲突*/private static void clearStyleCache() {styleCache.clear();}/*** 创建或获取单元格样式(支持动态颜色和样式缓存)*/private static CellStyle createCellStyle(Workbook workbook, FieldInfo fieldInfo, Object value) {// 默认颜色(从注解获取)IndexedColors fontColor = fieldInfo.excel.fontColor();IndexedColors backgroundColor = fieldInfo.excel.backgroundColor();// 如果有转换器,尝试获取动态样式if (fieldInfo.converter != null) {try {CellStyleInfo styleInfo = fieldInfo.converter.getCellStyle(value, fieldInfo.excel.params());if (styleInfo != null) {// 动态样式优先级高于注解样式if (styleInfo.hasFontColor()) {fontColor = styleInfo.getFontColor();}if (styleInfo.hasBackgroundColor()) {backgroundColor = styleInfo.getBackgroundColor();}}} catch (Exception e) {log.warn("获取动态样式失败: {}", e.getMessage());}}// 检查是否需要创建样式boolean needStyle = fontColor != IndexedColors.AUTOMATIC || backgroundColor != IndexedColors.AUTOMATIC;// 只有在需要时才创建样式if (!needStyle) {return null;}// 创建样式缓存键String cacheKey = fontColor.name() + "_" + backgroundColor.name();// 先查缓存CellStyle cachedStyle = styleCache.get(cacheKey);if (cachedStyle != null) {return cachedStyle;}CellStyle cellStyle = workbook.createCellStyle();// 应用字体样式if (fontColor != IndexedColors.AUTOMATIC) {Font font = workbook.createFont();font.setColor(fontColor.getIndex());cellStyle.setFont(font);}// 应用背景样式if (backgroundColor != IndexedColors.AUTOMATIC) {cellStyle.setFillForegroundColor(backgroundColor.getIndex());cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);}// 缓存样式styleCache.put(cacheKey, cellStyle);return cellStyle;}/*** 填充多级数据*/private static int fillMultiLevelData(Sheet sheet, Object rootItem, List<FieldInfo> allFields, int startRow) {try {// 展开为平面数据行List<Map<String, Object>> dataRows = expandToFlatRows(rootItem, allFields);// 填充每行数据for (int i = 0; i < dataRows.size(); i++) {Row row = sheet.createRow(startRow + i);Map<String, Object> rowData = dataRows.get(i);for (FieldInfo fieldInfo : allFields) {if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {Cell cell = row.createCell(fieldInfo.columnIndex);// 使用层级特定的唯一键获取值String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();Object value = rowData.get(uniqueKey);String cellValue = convertToExcelValue(value, fieldInfo);cell.setCellValue(cellValue);// 应用单元格样式(支持注解样式和动态样式)CellStyle cellStyle = createCellStyle(sheet.getWorkbook(), fieldInfo, value);if (cellStyle != null) {cell.setCellStyle(cellStyle);}}}}// 创建合并单元格if (dataRows.size() > 1) {createMergeRegions(sheet, rootItem, allFields, startRow, dataRows.size());}return startRow + dataRows.size();} catch (Exception e) {log.error("填充多级数据失败", e);return startRow + 1;}}/*** 展开对象为平面数据行*/private static List<Map<String, Object>> expandToFlatRows(Object item, List<FieldInfo> allFields) {List<Map<String, Object>> result = new ArrayList<>();try {expandObjectToRows(item, new HashMap<>(), allFields, result);} catch (Exception e) {log.warn("展开对象失败", e);}return result.isEmpty() ? List.of(new HashMap<>()) : result;}/*** 递归展开对象到行数据*/private static void expandObjectToRows(Object item, Map<String, Object> parentData, List<FieldInfo> allFields, List<Map<String, Object>> result) throws Exception {Map<String, Object> currentData = new HashMap<>(parentData);List<Object> childItems = new ArrayList<>();// 收集当前层级的字段值for (FieldInfo fieldInfo : allFields) {if (fieldInfo.field.getDeclaringClass().isAssignableFrom(item.getClass())) {Object value = fieldInfo.field.get(item);if (fieldInfo.fieldType == Excel.FieldType.COLLECTION && value instanceof Collection) {childItems.addAll((Collection<?>) value);} else if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {// 使用层级+字段名作为唯一键,避免不同层级的同名字段相互覆盖String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();currentData.put(uniqueKey, value);  // 使用唯一键存储,避免覆盖}}}// 如果有子对象,递归处理if (!childItems.isEmpty()) {for (Object childItem : childItems) {expandObjectToRows(childItem, currentData, allFields, result);}} else {result.add(currentData);}}/*** 创建合并单元格(支持任意多级数据结构)*/private static void createMergeRegions(Sheet sheet, Object rootItem, List<FieldInfo> allFields, int startRow, int totalRows) {try {// 构建层级结构树HierarchyNode rootNode = buildHierarchyTree(rootItem, allFields, startRow);// 递归创建所有层级的合并区域createMergeRegionsRecursively(sheet, rootNode, allFields);} catch (Exception e) {log.warn("创建合并单元格失败", e);}}/*** 构建层级结构树(支持任意层级)*/private static HierarchyNode buildHierarchyTree(Object rootItem, List<FieldInfo> allFields, int startRow) {HierarchyNode rootNode = new HierarchyNode();rootNode.item = rootItem;rootNode.level = 0;rootNode.startRow = startRow;// 递归构建层级树buildHierarchyTreeRecursively(rootNode, allFields);return rootNode;}/*** 递归构建层级结构树*/private static void buildHierarchyTreeRecursively(HierarchyNode node, List<FieldInfo> allFields) {try {List<Object> childItems = new ArrayList<>();// 找到当前对象的子对象集合for (FieldInfo fieldInfo : allFields) {if (fieldInfo.field.getDeclaringClass().isAssignableFrom(node.item.getClass()) && fieldInfo.fieldType == Excel.FieldType.COLLECTION) {Object value = fieldInfo.field.get(node.item);if (value instanceof Collection) {childItems.addAll((Collection<?>) value);}break; // 每个对象只处理一个子集合}}// 为每个子对象创建节点int currentRow = node.startRow;for (Object childItem : childItems) {HierarchyNode childNode = new HierarchyNode();childNode.item = childItem;childNode.level = node.level + 1;childNode.startRow = currentRow;childNode.parent = node;node.children.add(childNode);// 递归构建子节点buildHierarchyTreeRecursively(childNode, allFields);// 计算当前子节点占用的行数childNode.rowCount = calculateNodeRowCount(childNode);currentRow += childNode.rowCount;}// 如果没有子节点,则为叶子节点,占用1行if (childItems.isEmpty()) {node.rowCount = 1;} else {// 有子节点时,行数为所有子节点行数之和node.rowCount = node.children.stream().mapToInt(child -> child.rowCount).sum();}} catch (Exception e) {log.warn("构建层级树失败", e);node.rowCount = 1;}}/*** 计算节点占用的行数*/private static int calculateNodeRowCount(HierarchyNode node) {if (node.children.isEmpty()) {return 1;}return node.children.stream().mapToInt(child -> child.rowCount).sum();}/*** 递归创建所有层级的合并区域*/private static void createMergeRegionsRecursively(Sheet sheet, HierarchyNode node, List<FieldInfo> allFields) {try {// 为当前层级的字段创建合并区域if (node.rowCount > 1) {List<FieldInfo> currentLevelFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.level == node.level).toList();for (FieldInfo fieldInfo : currentLevelFields) {CellRangeAddress mergeRegion = new CellRangeAddress(node.startRow, node.startRow + node.rowCount - 1, fieldInfo.columnIndex, fieldInfo.columnIndex);sheet.addMergedRegion(mergeRegion);// 设置居中样式setCellStyle(sheet, node.startRow, fieldInfo.columnIndex);}}// 递归处理子节点for (HierarchyNode child : node.children) {createMergeRegionsRecursively(sheet, child, allFields);}} catch (Exception e) {log.warn("递归创建合并区域失败", e);}}/*** 层级节点类(支持任意层级的数据结构)*/private static class HierarchyNode {Object item;              // 当前对象int level;               // 层级深度 (0=根层级)int startRow;            // 起始行号int rowCount;            // 占用行数HierarchyNode parent;    // 父节点List<HierarchyNode> children = new ArrayList<>();  // 子节点列表}/*** 设置单元格样式(保留已有样式,只添加居中对齐)*/private static void setCellStyle(Sheet sheet, int row, int col) {try {Row sheetRow = sheet.getRow(row);if (sheetRow != null) {Cell cell = sheetRow.getCell(col);if (cell != null) {CellStyle existingStyle = cell.getCellStyle();CellStyle newStyle = sheet.getWorkbook().createCellStyle();// 如果已有样式,复制其属性if (existingStyle != null && existingStyle.getIndex() != 0) {// 复制颜色和字体属性newStyle.cloneStyleFrom(existingStyle);}// 设置居中对齐newStyle.setAlignment(HorizontalAlignment.CENTER);newStyle.setVerticalAlignment(VerticalAlignment.CENTER);cell.setCellStyle(newStyle);}}} catch (Exception e) {log.warn("设置单元格样式失败", e);}}/*** 转换值为Excel显示格式*/private static String convertToExcelValue(Object value, FieldInfo fieldInfo) {if (value == null) {return fieldInfo.excel != null ? fieldInfo.excel.defaultValue() : "";}// 使用转换器if (fieldInfo.converter != null) {try {Object converted = fieldInfo.converter.convertToExcel(value, fieldInfo.excel.params());return String.valueOf(converted);} catch (Exception e) {log.warn("字段转换失败: {}", fieldInfo.field.getName());}}// 日期格式化if (value instanceof Date && fieldInfo.excel != null && StringUtils.isNotBlank(fieldInfo.excel.dateFormat())) {SimpleDateFormat sdf = new SimpleDateFormat(fieldInfo.excel.dateFormat());return sdf.format((Date) value);}return String.valueOf(value);}/*** 解析Excel数据为对象列表*/private static <T> List<T> parseMultiLevelData(Sheet sheet, Class<T> clazz, List<FieldInfo> allFields) {List<T> result = new ArrayList<>();try {if (sheet.getPhysicalNumberOfRows() <= 1) {log.warn("Excel文件没有数据行");return result;}// 读取表头Row headerRow = sheet.getRow(0);Map<String, Integer> headerMap = parseHeader(headerRow, allFields);// 读取所有数据行List<Map<String, Object>> rowDataList = readAllRows(sheet, headerMap, allFields);if (rowDataList.isEmpty()) {log.warn("没有读取到任何数据");return result;}// 构建多级对象结构result = buildMultiLevelObjects(rowDataList, clazz, allFields);} catch (Exception e) {log.error("解析Excel数据失败", e);throw new RuntimeException("解析Excel数据失败", e);}return result;}/*** 解析Excel表头*/private static Map<String, Integer> parseHeader(Row headerRow, List<FieldInfo> allFields) {Map<String, Integer> headerMap = new HashMap<>();if (headerRow != null) {// 按层级排序字段,确保低层级的字段优先映射List<FieldInfo> sortedFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.excel != null).sorted((a, b) -> Integer.compare(a.level, b.level)).toList();// 记录每个列名已经被哪些层级使用过Map<String, Set<Integer>> usedLevels = new HashMap<>();for (int i = 0; i < headerRow.getPhysicalNumberOfCells(); i++) {Cell cell = headerRow.getCell(i);if (cell != null) {String headerName = cell.getStringCellValue();// 按层级顺序找到第一个未映射的匹配字段for (FieldInfo fieldInfo : sortedFields) {if (headerName.equals(fieldInfo.excel.name())) {String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();// 检查这个字段是否已经被映射过if (!headerMap.containsKey(uniqueKey)) {// 记录此列名在此层级的使用usedLevels.computeIfAbsent(headerName, k -> new HashSet<>()).add(fieldInfo.level);headerMap.put(uniqueKey, i);break;}}}}}}log.debug("解析表头完成,映射关系: {}", headerMap);return headerMap;}/*** 读取所有数据行*/private static List<Map<String, Object>> readAllRows(Sheet sheet, Map<String, Integer> headerMap, List<FieldInfo> allFields) {List<Map<String, Object>> rowDataList = new ArrayList<>();for (int i = 1; i <= sheet.getLastRowNum(); i++) {Row row = sheet.getRow(i);if (row == null) continue;Map<String, Object> rowData = new HashMap<>();boolean hasData = false;// 读取每个字段的值for (FieldInfo fieldInfo : allFields) {if (fieldInfo.fieldType == Excel.FieldType.COLUMN && fieldInfo.excel != null) {String fieldName = fieldInfo.field.getName();// 使用层级特定的唯一键查找列索引String uniqueKey = "L" + fieldInfo.level + "_" + fieldName;Integer colIndex = headerMap.get(uniqueKey);if (colIndex != null) {Cell cell = row.getCell(colIndex);Object value = getCellValue(cell, fieldInfo);// 使用层级特定的唯一键存储,避免同名字段覆盖rowData.put(uniqueKey, value);if (value != null && !value.toString().trim().isEmpty()) {hasData = true;}}}}if (hasData) {rowData.put("_rowIndex", i);rowDataList.add(rowData);}}// 填补合并单元格的空值问题fillMergedCellValues(rowDataList, allFields);return rowDataList;}/*** 填补合并单元格的空值问题* 严格按层级和子对象组隔离,只在同层级同对象组内填补空值*/private static void fillMergedCellValues(List<Map<String, Object>> rowDataList, List<FieldInfo> allFields) {if (rowDataList.isEmpty()) return;// 按层级分组字段Map<Integer, List<String>> levelFields = new HashMap<>();for (FieldInfo fieldInfo : allFields) {if (fieldInfo.fieldType == Excel.FieldType.COLUMN && fieldInfo.excel != null) {String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();levelFields.computeIfAbsent(fieldInfo.level, k -> new ArrayList<>()).add(uniqueKey);}}// 找到最大层级(叶子节点层级)int maxLevel = levelFields.keySet().stream().mapToInt(Integer::intValue).max().orElse(0);// 按层级逐级处理,严格隔离for (int currentLevel = 0; currentLevel < maxLevel; currentLevel++) {List<String> currentLevelFieldKeys = levelFields.get(currentLevel);if (currentLevelFieldKeys == null || currentLevelFieldKeys.isEmpty()) continue;// 获取当前层级的关键字段List<String> currentLevelKeyFields = getKeyFieldNames(allFields, currentLevel);// 按当前层级的对象分组Map<String, List<Map<String, Object>>> currentLevelGroups = new LinkedHashMap<>();for (Map<String, Object> rowData : rowDataList) {String levelKey = buildObjectKey(rowData, currentLevelKeyFields, currentLevel);currentLevelGroups.computeIfAbsent(levelKey, k -> new ArrayList<>()).add(rowData);}// 为当前层级的每个对象组分别填补空值for (Map.Entry<String, List<Map<String, Object>>> levelGroup : currentLevelGroups.entrySet()) {String levelKey = levelGroup.getKey();List<Map<String, Object>> levelRowData = levelGroup.getValue();for (String fieldKey : currentLevelFieldKeys) {// 检查当前层级对象组内是否有空值需要填补boolean hasEmptyValue = false;Object masterValue = null;// 先找到第一个非空值作为主值for (Map<String, Object> rowData : levelRowData) {Object currentValue = rowData.get(fieldKey);if (currentValue != null && !currentValue.toString().trim().isEmpty()) {if (masterValue == null) {masterValue = currentValue;}} else {hasEmptyValue = true;}}// 只有同时满足"有主值"和"有空值"的条件才进行填充if (masterValue != null && hasEmptyValue) {int filledCount = 0;for (Map<String, Object> rowData : levelRowData) {Object currentValue = rowData.get(fieldKey);if (currentValue == null || currentValue.toString().trim().isEmpty()) {rowData.put(fieldKey, masterValue);filledCount++;}}}}}}}/*** 获取单元格值,支持合并单元格*/private static Object getCellValue(Cell cell, FieldInfo fieldInfo) {if (cell == null) return null;try {// 检查是否为合并单元格Sheet sheet = cell.getSheet();int rowIndex = cell.getRowIndex();int colIndex = cell.getColumnIndex();// 查找合并区域for (CellRangeAddress range : sheet.getMergedRegions()) {if (range.isInRange(rowIndex, colIndex)) {// 如果是合并单元格,获取合并区域的第一个单元格的值Row firstRow = sheet.getRow(range.getFirstRow());if (firstRow != null) {Cell firstCell = firstRow.getCell(range.getFirstColumn());if (firstCell != null && firstCell != cell) {return getCellValue(firstCell, fieldInfo);}}}}// 普通单元格处理switch (cell.getCellType()) {case STRING:String stringValue = cell.getStringCellValue();return convertCellValue(stringValue, fieldInfo);case NUMERIC:double numericValue = cell.getNumericCellValue();// 如果字段类型是Date,尝试转换为日期if (fieldInfo.field.getType() == Date.class) {return cell.getDateCellValue();} else if (fieldInfo.field.getType() == Integer.class || fieldInfo.field.getType() == int.class) {return (int) numericValue;}return numericValue;case BOOLEAN:return cell.getBooleanCellValue();default:return null;}} catch (Exception e) {log.warn("读取单元格值失败: {}", e.getMessage());return null;}}/*** 转换单元格值*/private static Object convertCellValue(String value, FieldInfo fieldInfo) {if (value == null || value.trim().isEmpty()) return null;Class<?> fieldType = fieldInfo.field.getType();try {// 如果有自定义转换器,先尝试转换if (fieldInfo.converter != null) {return fieldInfo.converter.convertFromExcel(value, new String[0]);}// 根据字段类型转换if (fieldType == String.class) {return value;} else if (fieldType == Integer.class || fieldType == int.class) {return Integer.parseInt(value);} else if (fieldType == Long.class || fieldType == long.class) {return Long.parseLong(value);} else if (fieldType == Double.class || fieldType == double.class) {return Double.parseDouble(value);} else if (fieldType == Boolean.class || fieldType == boolean.class) {return Boolean.parseBoolean(value);} else if (fieldType == Date.class) {// 尝试解析日期SimpleDateFormat[] formats = {new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), new SimpleDateFormat("yyyy-MM-dd"), new SimpleDateFormat("yyyy/MM/dd")};for (SimpleDateFormat format : formats) {try {return format.parse(value);} catch (Exception ignored) {}}}return value;} catch (Exception e) {log.warn("转换单元格值失败: {} -> {}", value, fieldType.getSimpleName());return value;}}/*** 构建多级对象结构*/private static <T> List<T> buildMultiLevelObjects(List<Map<String, Object>> rowDataList, Class<T> clazz, List<FieldInfo> allFields) {List<T> result = new ArrayList<>();try {// 找到最大层级深度int maxLevel = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN).mapToInt(f -> f.level).max().orElse(0);log.debug("最大层级深度: {}", maxLevel);// 按层级分组数据Map<String, List<Map<String, Object>>> groupedData = groupDataByHierarchy(rowDataList, allFields);// 构建根对象for (Map.Entry<String, List<Map<String, Object>>> entry : groupedData.entrySet()) {T rootObject = buildSingleObject(entry.getValue(), clazz, allFields, 0);if (rootObject != null) {result.add(rootObject);}}} catch (Exception e) {log.error("构建多级对象失败", e);}return result;}/*** 按层级分组数据(支持任意级别)*/private static Map<String, List<Map<String, Object>>> groupDataByHierarchy(List<Map<String, Object>> rowDataList, List<FieldInfo> allFields) {Map<String, List<Map<String, Object>>> groupedData = new LinkedHashMap<>();// 获取根层级的关键字段组合List<String> rootKeyFieldNames = getKeyFieldNames(allFields, 0);for (Map<String, Object> rowData : rowDataList) {// 构建根对象的唯一标识String key = buildObjectKey(rowData, rootKeyFieldNames, 0);groupedData.computeIfAbsent(key, k -> new ArrayList<>()).add(rowData);}return groupedData;}/*** 获取指定层级的关键字段名称列表*/private static List<String> getKeyFieldNames(List<FieldInfo> allFields, int level) {List<String> keyFieldNames = new ArrayList<>();// 获取当前层级的所有字段List<FieldInfo> currentLevelFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.level == level).toList();// 收集所有标记为isKey=true的字段for (FieldInfo field : currentLevelFields) {Excel excel = field.excel;if (excel != null && excel.isKey()) {keyFieldNames.add(field.field.getName());}}// 如果没有找到任何关键字段,使用第一个字段作为默认关键字段if (keyFieldNames.isEmpty() && !currentLevelFields.isEmpty()) {keyFieldNames.add(currentLevelFields.get(0).field.getName());}return keyFieldNames;}/*** 构建对象的唯一标识键*/private static String buildObjectKey(Map<String, Object> rowData, List<String> keyFieldNames, int level) {StringBuilder key = new StringBuilder();for (String fieldName : keyFieldNames) {// 使用层级特定的唯一键获取值String uniqueKey = "L" + level + "_" + fieldName;Object value = rowData.get(uniqueKey);key.append(value != null ? value.toString() : "null").append("|");}return key.toString();}/*** 构建单个对象*/private static <T> T buildSingleObject(List<Map<String, Object>> rowDataList, Class<T> clazz, List<FieldInfo> allFields, int level) {try {T object = clazz.getDeclaredConstructor().newInstance();// 设置当前层级的字段值Map<String, Object> representativeRow = findRepresentativeRow(rowDataList, allFields, level);List<FieldInfo> currentLevelFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.level == level).toList();for (FieldInfo fieldInfo : currentLevelFields) {// 使用层级特定的唯一键,避免同名字段覆盖问题String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();Object value = representativeRow.get(uniqueKey);if (value != null) {fieldInfo.field.set(object, value);}}List<FieldInfo> childCollectionFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLLECTION && f.level == level).toList();for (FieldInfo collectionField : childCollectionFields) {Class<?> elementType = getActualType(collectionField.field);// 按子对象分组Map<String, List<Map<String, Object>>> childGroups = groupChildData(rowDataList, allFields, level + 1);List<Object> childObjects = new ArrayList<>();for (List<Map<String, Object>> childRowData : childGroups.values()) {Object childObject = buildSingleObject(childRowData, elementType, allFields, level + 1);if (childObject != null) {childObjects.add(childObject);}}collectionField.field.set(object, childObjects);}return object;} catch (Exception e) {log.error("构建对象失败: {}", e.getMessage());return null;}}/*** 找到代表性的行数据(用于获取当前层级的字段值)* 对于每个字段,优先选择有值的行数据*/private static Map<String, Object> findRepresentativeRow(List<Map<String, Object>> rowDataList, List<FieldInfo> allFields, int level) {if (rowDataList.isEmpty()) {return new HashMap<>();}if (rowDataList.size() == 1) {return rowDataList.get(0);}// 获取当前层级的所有字段List<FieldInfo> currentLevelFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.level == level).toList();// 创建一个组合的代表性行数据Map<String, Object> representativeRow = new HashMap<>(rowDataList.get(0));// 对于每个字段,找到第一个非空值for (FieldInfo fieldInfo : currentLevelFields) {String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();Object currentValue = representativeRow.get(uniqueKey);// 如果当前值是空的,尝试从其他行中找到非空值if (currentValue == null || currentValue.toString().trim().isEmpty()) {for (Map<String, Object> rowData : rowDataList) {Object value = rowData.get(uniqueKey);if (value != null && !value.toString().trim().isEmpty()) {representativeRow.put(uniqueKey, value);break;}}}}return representativeRow;}/*** 分组子数据(支持任意级别)*/private static Map<String, List<Map<String, Object>>> groupChildData(List<Map<String, Object>> rowDataList, List<FieldInfo> allFields, int childLevel) {Map<String, List<Map<String, Object>>> childGroups = new LinkedHashMap<>();// 获取子层级的关键字段组合List<String> childKeyFieldNames = getKeyFieldNames(allFields, childLevel);for (Map<String, Object> rowData : rowDataList) {// 构建子对象的唯一标识String key = buildObjectKey(rowData, childKeyFieldNames, childLevel);childGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(rowData);}return childGroups;}/*** 字段信息类*/private static class FieldInfo {Field field;Excel excel;ExcelConverter<Object, Object> converter;int level;int columnIndex;Excel.FieldType fieldType;}
}

🔗 相关链接

  • Apache POI官方文档
  • IndexedColors颜色参考

最后更新:2025-08-25

http://www.dtcms.com/a/350679.html

相关文章:

  • Java 并发编程总结
  • SCSS上传图片占位区域样式
  • 基于多通道同步分析的智能听诊系统应用程序
  • 动态住宅代理:跨境电商数据抓取的稳定解决方案
  • vue-admin-template vue-cli 4升5(vue2版)
  • C语言中哪些常见的坑
  • Linux的奇妙冒险———进程信号
  • 滲透測試工具
  • Microsoft 365 中的 Rules-Based Classification 功能深度解析:企业数据治理与合规的智能基石
  • 25年8月通信基础知识补充2:星座的峭度(Kurtosis)、ISAC
  • 朴素贝叶斯分类器
  • A股市场高级日历效应详解与实战指南
  • 【P2P】P2P主要技术及RELAY服务1:python实现
  • 【Git】fatal: Unable to create ‘.git/index.lock’: File exists.
  • 迁移面试题
  • 亚远景- 从算法到刹车片:ISO/PAS 8800如何量化自动驾驶的“安全冗余”?
  • Life:Internship in OnSea Day 64
  • PyTorch损失函数全解析与实战指南
  • 高性能C++实践:原子操作与无锁队列实现
  • C++ #pragma
  • C++初阶(3)C++入门基础2
  • 现代C++工具链实战:CMake + Conan + vcpkg依赖管理
  • MYSQL的bin log是什么
  • JUC并发编程08 - 同步模式/异步模式
  • ROS2 python功能包launch,config文件编译后找不到
  • 链表OJ习题(2)
  • 搭建基于LangChain实现复杂RAG聊天机器人
  • AI在软件研发流程中的提效案例
  • 在vue3后台项目中使用热力图,并给热力图增加点击选中事件
  • Java中删除字符串首字符