实现excel的树形导出

这个是我实现的原型图,可以先看一下是否是你想要的格式。其实我一直是不想做这个树形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树形结构的导入、导出时,一定要和我联系(直接私信我,或者在评论区下评论),非常感谢。

相关推荐
郭涤生8 小时前
Excel知识体系
信息可视化·excel
葡萄城技术团队8 小时前
从 “纸笔清单” 到全栈引擎:数据填报与类 Excel 控件如何重塑企业效率曲线
excel
用户98402276679183 天前
【Node.js】基于 Koa 将 Xlsx 文件转化为数据
node.js·excel·koa
葡萄城技术团队7 天前
从100秒到10秒的性能优化,你真的掌握 Excel 的使用技巧了吗?
excel
QQ3596773459 天前
ArcGIS Pro实现基于 Excel 表格批量创建标准地理数据库(GDB)——高效数据库建库解决方案
数据库·arcgis·excel
星空的资源小屋10 天前
Digital Clock 4,一款免费的个性化桌面数字时钟
stm32·单片机·嵌入式硬件·电脑·excel
揭老师高效办公10 天前
在Excel和WPS表格中批量删除数据区域的批注
excel·wps表格
我是zxb10 天前
EasyExcel:快速读写Excel的工具类
数据库·oracle·excel
辣香牛肉面10 天前
[Windows] 搜索文本2.6.2(从word、wps、excel、pdf和txt文件中查找文本的工具)
word·excel·wps·搜索文本