
这个是我实现的原型图,可以先看一下是否是你想要的格式。其实我一直是不想做这个树形excel的导出的,我认为excel结构不能很清晰的展示出数据的层级关系,并且就算是展示出来了,也是很脆弱的层级关系。我们在进行excel导入的时候很难去清洗数据,并还原出树形结构。但是,公司一定要求实现这个功能,只能勉强的做了。
我查询了大量的资料,发现树形结构的构建貌似只能使用这种合并单元格的形式来实现。(还有一种是通过空格缩进的形式来构建树形结构,但是,我认为这种形式的树形结构更加的脆弱,所以。不实现这种方式)由于我们是使用合并单元格的形式,并且直接使用<层级n名称>来构建整合树形结构,所以那种传统的excel肯定是不行的,我们这边的层级是不固定的,所以,就需要动态的设置excel的表头,并且,还要合并相应的单元格。还要保证excel导入时能正确的清洗出数据。
我使用了谷歌的poi进行数据库的操作。相应的maven依赖如下:
java
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
<scope>compile</scope>
</dependency>
我这边是使用了 《名称》这个字段来构建层级关系,你也可以使用其他的字段(但是,一般都要保证这个字段在数据库中是唯一的)由于我们在导出excel时通常是不导出id的,所以,唯一字段就显得格外的重要。我们再导入时根据这个唯一字段来进行区分excel中的数据是新增或者修改。我的业务代码啊中,要保证《名称》和《编码》这两个字段的组合唯一。
还要注意的是,我的树形结构构建时还要有一个属性:leveType,这个字段是用来展示数据的层级关系的(一般来说,只要是树形结构都会有这个字段的。如果,你的树形结构没有的话,那么在进行excel的导出时就要自己再构建这个属性)。我的方法需要得到一个以及构建好层级关系的树形列表:List<T> 这样,再导出时就可以构建树形结构了。
并且,再导入方法时,最成功的构建了树形关系。这时,可以进行数据库的操作了。(可以在数据清洗时,就直接判断出那些数据是新增/修改的)
相应的代码如下:
主逻辑,excel的导入、导出
java
package com.scmpt.templates.infrastructure.domains;
import com.scmpt.framework.core.httpException.ResultException;
import com.scmpt.templates.client.domains.dto.data.DomainsVo;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Stack;
/**
* @Author 张乔
* @Date 2025/9/8 14:51
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DomainsExcelService {
private final TreeConvertService treeConvertService;
// 常量定义
private static final int MIN_COLUMN_WIDTH = 10 * 256; // 最小列宽(10个字符)
private static final int MAX_COLUMN_WIDTH = 50 * 256; // 最大列宽(50个字符)
private static final int DEFAULT_COLUMN_WIDTH = 15 * 256; // 默认列宽(15个字符)
private static final int FIRST_LEVEL_WIDTH = 25 * 256; // 第一级列宽
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final String FONT_NAME = "微软雅黑"; // 字体名称
public void exportExcel(HttpServletResponse response, List<DomainsVo> treeData) {
if (treeData == null || treeData.isEmpty()) {
return;
}
// 1. 将树形结构转换为平面列表
treeConvertService.convertTreeToFlatList(treeData);
int maxLevel = treeConvertService.getMaxLevel();
// 2. 创建工作簿和工作表
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("模板领域");
// 3. 创建样式
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle dataStyle = createDataStyle(workbook);
CellStyle dateStyle = createDateStyle(workbook); // 创建时间样式
// 4. 创建表头
createHeaderRow(sheet, headerStyle, maxLevel);
// 5. 填充数据并处理合并
int rowNum = 1; // 从第1行开始(第0行是表头)
List<CellRangeAddress> mergeRegions = new ArrayList<>();
for (DomainsVo node : treeData) {
rowNum = processNode(sheet, dataStyle, dateStyle,node,
rowNum, 0, new ArrayList<>(), mergeRegions, maxLevel);
}
// 6. 应用合并区域
for (CellRangeAddress region : mergeRegions) {
sheet.addMergedRegion(region);
}
// 7. 动态调整列宽
adjustColumnWidths(sheet, maxLevel);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("数据导出", StandardCharsets.UTF_8);
response.setHeader("Content-disposition", "attachment;filename="+fileName+".xlsx");
try {
// 8. 写入响应流
workbook.write(response.getOutputStream());
workbook.close();
} catch (IOException e) {
log.error("导出数据出错", e);
}
}
/**
* 动态调整列宽
*/
private void adjustColumnWidths(Sheet sheet, int maxLevel) {
// 设置层级列初始宽度
for (int i = 0; i < maxLevel; i++) {
if (i == 0) {
sheet.setColumnWidth(i, FIRST_LEVEL_WIDTH);
} else {
sheet.setColumnWidth(i, DEFAULT_COLUMN_WIDTH);
}
}
// 设置固定列初始宽度
int[] fixedColumnWidths = {15, 20, 18}; // 编码、备注、创建时间的初始宽度(字符数)
for (int i = 0; i < fixedColumnWidths.length; i++) {
sheet.setColumnWidth(maxLevel + i, fixedColumnWidths[i] * 256);
}
// 基于内容自动调整列宽,但限制在最小和最大宽度之间
for (int i = 0; i < maxLevel + 3; i++) {
sheet.autoSizeColumn(i);
int currentWidth = sheet.getColumnWidth(i);
// 确保列宽在合理范围内
if (currentWidth < MIN_COLUMN_WIDTH) {
sheet.setColumnWidth(i, MIN_COLUMN_WIDTH);
} else if (currentWidth > MAX_COLUMN_WIDTH) {
sheet.setColumnWidth(i, MAX_COLUMN_WIDTH);
}
}
}
/**
* 创建表头样式
*/
private CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
// 设置字体
Font font = workbook.createFont();
font.setBold(true);
font.setFontHeightInPoints((short) 12);
font.setFontName(FONT_NAME);
font.setColor(IndexedColors.WHITE.getIndex());
style.setFont(font);
// 设置对齐方式
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置边框
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 设置背景色
style.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
return style;
}
/**
* 创建数据样式
*/
private CellStyle createDataStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
// 设置字体
Font font = workbook.createFont();
font.setFontHeightInPoints((short) 11);
font.setFontName(FONT_NAME);
style.setFont(font);
// 设置对齐方式
style.setAlignment(HorizontalAlignment.LEFT);
style.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置边框
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setWrapText(true);
return style;
}
/**
* 创建时间样式
*/
private CellStyle createDateStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
// 设置字体
Font font = workbook.createFont();
font.setFontHeightInPoints((short) 11);
font.setFontName(FONT_NAME);
style.setFont(font);
// 设置对齐方式
style.setAlignment(HorizontalAlignment.LEFT);
style.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置边框
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 设置时间格式
CreationHelper createHelper = workbook.getCreationHelper();
style.setDataFormat(createHelper.createDataFormat().
getFormat(EXCEL_DATE_FORMAT));
return style;
}
/**
* 创建表头行
*/
private void createHeaderRow(Sheet sheet, CellStyle style, int maxLevel) {
Row headerRow = sheet.createRow(0);
headerRow.setHeightInPoints(25); // 设置表头行高
// 创建层级列(从第0列开始)
for (int i = 0; i < maxLevel; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue("层级" + (i + 1) + "名称");
cell.setCellStyle(style);
}
// 创建固定列
String[] fixedHeaders = {"编码", "备注", "创建时间"};
for (int i = 0; i < fixedHeaders.length; i++) {
Cell cell = headerRow.createCell(maxLevel + i);
cell.setCellValue(fixedHeaders[i]);
cell.setCellStyle(style);
}
}
/**
* 处理节点数据
*/
private int processNode(Sheet sheet, CellStyle style,CellStyle dateStyle,
DomainsVo node, int rowNum,
Integer level, List<DomainsVo> parentNodes,
List<CellRangeAddress> mergeRegions, int maxLevel) {
if (node == null) return rowNum;
// 创建当前行
Row row = sheet.createRow(rowNum);
// 设置父级节点数据(从第0列开始)
for (int i = 0; i < parentNodes.size(); i++) {
Cell cell = row.createCell(i);
cell.setCellValue(parentNodes.get(i).getName());
cell.setCellStyle(style);
}
// 设置当前节点数据
int currentCol = parentNodes.size();
Cell cell = row.createCell(currentCol);
cell.setCellValue(node.getName());
cell.setCellStyle(style);
// 记录当前节点位置,用于后续合并
// 递归处理子节点
List<DomainsVo> newParentNodes = new ArrayList<>(parentNodes);
newParentNodes.add(node);
int childRowNum = rowNum + 1;
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
for (DomainsVo child : node.getChildren()) {
childRowNum = processNode(sheet, style,dateStyle, child, childRowNum, level + 1,
newParentNodes, mergeRegions, maxLevel);
}
}
// 如果需要合并,添加合并区域
if (childRowNum - rowNum > 1) {
mergeRegions.add(new CellRangeAddress(rowNum, childRowNum - 1, currentCol, currentCol));
}
// 设置其他固定列数据
Cell codeCell = row.createCell(maxLevel);
codeCell.setCellValue(node.getSoleCode() != null ? node.getSoleCode() : "");
codeCell.setCellStyle(style);
Cell remarkCell = row.createCell(maxLevel + 1);
remarkCell.setCellValue(node.getRemark() != null ? node.getRemark() : "");
remarkCell.setCellStyle(style);
Cell timeCell = row.createCell(maxLevel + 2);
timeCell.setCellValue(node.getCreatedTime() != null ?
node.getCreatedTime() : new Date());
timeCell.setCellStyle(node.getCreatedTime() != null ? dateStyle :style);
return childRowNum;
}
/**
* 导入Excel文件并转换为树形结构
*
* @param file 上传的Excel文件
*/
public Object importExcel(MultipartFile file) {
try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {
Sheet sheet = workbook.getSheetAt(0);
// 1. 读取表头,确定最大层级
int maxLevel = determineMaxLevel(sheet);
// 2. 解析数据行
List<ExcelRowData> rowDataList = parseRowData(sheet, maxLevel);
// 3. 重建树形结构
List<DomainsVo> list = rebuildTreeStructure(rowDataList, maxLevel);
log.info("导入数据成功,树形结构已重建");
for (DomainsVo node : list){
log.info("节点数据:{}", node);
}
return list;
} catch (IOException e) {
log.error("读取Excel文件失败", e);
throw new ResultException(501,"读取Excel文件失败");
}
}
/**
* 根据表头确定最大层级数
*/
private int determineMaxLevel(Sheet sheet) {
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
return 0;
}
int level = 0;
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
Cell cell = headerRow.getCell(i);
if (cell != null && cell.getStringCellValue().startsWith("层级")) {
level++;
} else {
break;
}
}
return level;
}
/**
* 解析Excel中的数据行
*/
private List<ExcelRowData> parseRowData(Sheet sheet, int maxLevel) {
List<ExcelRowData> rowDataList = new ArrayList<>();
// 从第1行开始(跳过表头)
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) {
continue;
}
ExcelRowData rowData = new ExcelRowData();
// 解析层级数据
List<String> levelData = new ArrayList<>();
for (int j = 0; j < maxLevel; j++) {
Cell cell = row.getCell(j);
String value = (cell != null) ? getCellValueAsString(cell) : "";
levelData.add(value);
}
rowData.setLevelData(levelData);
// 解析固定列数据
if (maxLevel < row.getLastCellNum()) {
Cell codeCell = row.getCell(maxLevel);
rowData.setSoleCode(getCellValueAsString(codeCell));
Cell remarkCell = row.getCell(maxLevel + 1);
rowData.setRemark(getCellValueAsString(remarkCell));
Cell timeCell = row.getCell(maxLevel + 2);
String timeStr = getCellValueAsString(timeCell);
try {
rowData.setCreatedTime(timeStr.isEmpty() ? null : DATE_FORMAT.parse(timeStr));
} catch (ParseException e) {
log.warn("日期格式解析失败: {}", timeStr);
rowData.setCreatedTime(null);
}
}
rowDataList.add(rowData);
}
return rowDataList;
}
/**
* 获取单元格的字符串值
*/
private String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return DATE_FORMAT.format(cell.getDateCellValue());
} else {
// 数字格式,避免科学计数法和浮点数问题
double numericValue = cell.getNumericCellValue();
if (numericValue == (long) numericValue) {
return String.valueOf((long) numericValue);
} else {
return String.valueOf(numericValue);
}
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
return cell.getCellFormula();
default:
return "";
}
}
/**
* 重建树形结构
*/
private List<DomainsVo> rebuildTreeStructure(List<ExcelRowData> rowDataList, int maxLevel) {
List<DomainsVo> rootNodes = new ArrayList<>();
Stack<DomainsVo> nodeStack = new Stack<>();
Stack<Integer> levelStack = new Stack<>();
for (ExcelRowData rowData : rowDataList) {
// 确定当前节点的层级
int currentLevel = 0;
for (int i = 0; i < maxLevel; i++) {
if (!rowData.getLevelData().get(i).isEmpty()) {
currentLevel = i;
}
}
// 创建当前节点
DomainsVo currentNode = new DomainsVo();
currentNode.setName(rowData.getLevelData().get(currentLevel));
currentNode.setSoleCode(rowData.getSoleCode());
currentNode.setRemark(rowData.getRemark());
currentNode.setCreatedTime(rowData.getCreatedTime());
currentNode.setChildren(new ArrayList<>());
// 处理层级关系
if (nodeStack.isEmpty()) {
// 根节点
rootNodes.add(currentNode);
nodeStack.push(currentNode);
levelStack.push(currentLevel);
} else {
// 寻找父节点
while (!levelStack.isEmpty() && currentLevel <= levelStack.peek()) {
nodeStack.pop();
levelStack.pop();
}
if (!nodeStack.isEmpty()) {
// 添加到父节点的子节点列表
nodeStack.peek().getChildren().add(currentNode);
} else {
// 应该不会发生,但为了安全
rootNodes.add(currentNode);
}
nodeStack.push(currentNode);
levelStack.push(currentLevel);
}
}
return rootNodes;
}
}
java
package com.scmpt.templates.infrastructure.domains;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class ExcelRowData {
/**
* 层级数据
*/
private List<String> levelData;
/**
* 唯一编码
*/
private String soleCode;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private Date createdTime;
}
相应的辅助类
java
package com.scmpt.templates.infrastructure.domains;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 动态数据VO
*/
@Data
public class DynamicDataVO {
// 使用Map存储动态层级数据,key为列索引,value为单元格值
private Map<Integer, String> levelData = new LinkedHashMap<>();
// 其他固定字段
private String soleCode;
private String remark;
// 用于合并单元格的辅助字段
private transient int levelType;
private transient String parentPath;
/**
* 设置层级数据
*/
public void setLevelData(int columnIndex, String value) {
levelData.put(columnIndex, value);
}
/**
* 获取层级数据
*/
public String getLevelData(int columnIndex) {
return levelData.getOrDefault(columnIndex, "");
}
}
树形结构的转换,并且,算出,最大层级数。
java
package com.scmpt.templates.infrastructure.domains;
import com.scmpt.templates.client.domains.dto.data.DomainsVo;
import lombok.Getter;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 树形结构转换服务
*/
@Getter
@Service
public class TreeConvertService {
/**
* -- GETTER --
* 获取最大层级
*/
private int maxLevel = 0;
/**
* 将树形结构转换为平面列表
*/
public void convertTreeToFlatList(List<DomainsVo> treeData) {
List<DynamicDataVO> result = new ArrayList<>();
maxLevel = 0;
// 首先计算最大层级
calculateMaxLevel(treeData, 1);
// 转换数据
int index = 1;
for (DomainsVo node : treeData) {
index = traverseNode(node, result,
index, new String[maxLevel], "", 0);
}
}
/**
* 计算最大层级
*/
private void calculateMaxLevel(List<DomainsVo> nodes, int currentLevel) {
if (nodes == null || nodes.isEmpty()) {
return;
}
for (DomainsVo node : nodes) {
int nodeLevel = node.getLevelType() != null ? node.getLevelType() : currentLevel;
if (nodeLevel > maxLevel) {
maxLevel = nodeLevel;
}
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
calculateMaxLevel(node.getChildren(), nodeLevel + 1);
}
}
}
/**
* 递归遍历节点
*/
private int traverseNode(DomainsVo node, List<DynamicDataVO> result, int index,
String[] parentNames, String parentPath, int depth) {
if (node == null) return index;
// 创建数据VO
DynamicDataVO dataVO = new DynamicDataVO();
int currentLevel = node.getLevelType() != null ? node.getLevelType() : (depth + 1);
dataVO.setLevelType(currentLevel);
// 设置当前路径
String currentPath = parentPath.isEmpty() ?
node.getName() : parentPath + "|" + node.getName();
dataVO.setParentPath(currentPath);
// 设置层级数据
for (int i = 1; i < maxLevel; i++) {
if (i < currentLevel - 1) {
// 父级节点
dataVO.setLevelData(i + 1, parentNames[i]);
} else if (i == currentLevel - 1) {
// 当前节点
dataVO.setLevelData(i + 1, node.getName());
} else {
// 子级节点(空)
dataVO.setLevelData(i + 1, "");
}
}
// 设置其他字段
dataVO.setSoleCode(node.getSoleCode());
dataVO.setRemark(node.getRemark());
// 添加到结果列表
result.add(dataVO);
// 递归处理子节点
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
String[] newParentNames = new String[maxLevel];
System.arraycopy(parentNames, 0, newParentNames, 0, parentNames.length);
if (currentLevel - 1 >= 0 && currentLevel - 1 < maxLevel) {
newParentNames[currentLevel - 1] = node.getName();
}
for (DomainsVo child : node.getChildren()) {
index = traverseNode(child, result, index, newParentNames, currentPath, depth + 1);
}
}
return index;
}
}
至此,我的excel树形结构的导入、导出就完成了。
可以,参考一下相应的代码。如果,那位同学有更好的excel树形结构的导入、导出时,一定要和我联系(直接私信我,或者在评论区下评论),非常感谢。