cr 1 ماه پیش
والد
کامیت
e36f391a9a

+ 9 - 651
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeContractController.java

@@ -160,6 +160,13 @@ public class WbsTreeContractController extends BladeController {
         return iWbsTreeContractService.importTree(file,pkeyId);
     }
 
+    @PostMapping("/importPartitionCode")
+    @ApiOperation("导入划分编码")
+    @ApiOperationSupport(order = 31)
+    private R importPartitionCode(@RequestParam("file") MultipartFile file){
+        return iWbsTreeContractService.importPartitionCode(file);
+    }
+
 
     @GetMapping("/getNameRuleByPkeyId")
     @ApiOperationSupport(order = 31)
@@ -841,661 +848,12 @@ public class WbsTreeContractController extends BladeController {
     @PostMapping("/exportTree")
     @ApiOperationSupport(order = 31)
     @ApiOperation(value = "工程划分-导出划分")
-    public ResponseEntity<Resource> exportTree(Long contractId, HttpServletResponse response) {
-        String templatePath = "C:\\upload\\excel\\gcdc.xlsx";
-        String outputPath = "C:\\upload\\excel\\111.xlsx";
-
-        // 查询数据
-        String sql = "select * from m_wbs_tree_contract where contract_id = " + contractId +
-                " and is_deleted = 0 and type = 1 and node_type != 6 and p_id != 0";
-        List<WbsTreeContract> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(WbsTreeContract.class));
-
-        InputStream templateStream = new FileInputStream(new File(templatePath));
-        org.apache.poi.ss.usermodel.Workbook workbook = WorkbookFactory.create(templateStream);
-        Sheet templateSheet = workbook.getSheetAt(0);
-
-        // 移除默认的Sheet1
-        workbook.removeSheetAt(0);
-
-        // 创建居中的单元格样式
-        CellStyle centerStyle = workbook.createCellStyle();
-        centerStyle.setAlignment(HorizontalAlignment.CENTER);
-        centerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
-
-        // 按单位工程分组
-        Map<Long, List<WbsTreeContract>> unitProjectMap = list.stream()
-                .filter(item -> item.getNodeType() == 18) // 单位工程
-                .collect(Collectors.toMap(
-                        WbsTreeContract::getPKeyId,
-                        unit -> list.stream()
-                                .filter(item -> item.getAncestors() != null &&
-                                        item.getAncestors().contains(unit.getPKeyId().toString()))
-                                .collect(Collectors.toList())
-                ));
-
-        // 为每个单位工程创建sheet
-        for (Map.Entry<Long, List<WbsTreeContract>> entry : unitProjectMap.entrySet()) {
-            Long unitId = entry.getKey();
-            List<WbsTreeContract> unitProjects = entry.getValue();
-
-            // 获取单位工程名称
-            String unitName = unitProjects.stream()
-                    .filter(item -> item.getPKeyId().equals(unitId))
-                    .findFirst()
-                    .map(WbsTreeContract::getNodeName)
-                    .orElse("未知单位工程");
-            // 创建安全的sheet名称
-            String safeUnitName = unitName.replaceAll("[\\\\/*\\[\\]:?]", "_");
-            if (safeUnitName.length() > 31) {
-                safeUnitName = safeUnitName.substring(0, 31);
-            }
-            String validSheetName = WorkbookUtil.createSafeSheetName(safeUnitName);
-
-            // 创建sheet并设置名称
-            Sheet sheet = workbook.createSheet(validSheetName);
-
-            // 创建表头(两行)- 使用模板sheet
-            createHeaderRows(sheet, templateSheet, centerStyle);
-
-            // 处理数据行
-            processDataRows(sheet, unitProjects, unitId, centerStyle);
-
-            // 设置列宽自动调整
-            autoSizeColumns(sheet);
-
-            // 只隐藏pkeyId列(不保护,允许用户取消隐藏)
-            hidePkeyIdColumnsOnly(sheet);
-        }
-
-        // 保存到本地文件
-        saveWorkbookToFile(workbook, outputPath);
-
-        // 同时返回给浏览器下载
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        workbook.write(outputStream);
-        workbook.close();
-
-        ByteArrayResource resource = new ByteArrayResource(outputStream.toByteArray());
-        return ResponseEntity.ok()
-                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=engineering_division.xlsx")
-                .contentType(MediaType.APPLICATION_OCTET_STREAM)
-                .contentLength(resource.contentLength())
-                .body(resource);
-    }
-
-    // 只隐藏pkeyId列(不设置保护,允许用户取消隐藏)
-    private void hidePkeyIdColumnsOnly(Sheet sheet) {
-        int[] pkeyIdColumns = {2, 5, 8, 11, 14};
-
-        // 只隐藏列,不设置任何保护
-        for (int col : pkeyIdColumns) {
-            sheet.setColumnHidden(col, true);
-        }
-
-        // 特别注意:不调用 sheet.protectSheet() 方法
-        // 这样用户就可以自由调整列宽和取消隐藏列
+    public ResponseEntity<Resource> exportTree(Long contractId, HttpServletResponse response){
+        return iWbsTreeContractService.exportTree(contractId,response);
     }
 
-    // 创建表头行(两行)- 添加居中样式
-    private void createHeaderRows(Sheet sheet, Sheet templateSheet, CellStyle centerStyle) {
-        // 复制第一行
-        Row templateHeaderRow = templateSheet.getRow(0);
-        Row headerRow = sheet.createRow(0);
-        for (int i = 0; i < 15; i++) {
-            Cell templateCell = templateHeaderRow.getCell(i);
-            Cell newCell = headerRow.createCell(i);
-            if (templateCell != null) {
-                newCell.setCellValue(templateCell.getStringCellValue());
-                newCell.setCellStyle(centerStyle);
-            }
-        }
-
-        // 复制第二行
-        Row templateSubHeaderRow = templateSheet.getRow(1);
-        Row subHeaderRow = sheet.createRow(1);
-        for (int i = 0; i < 15; i++) {
-            Cell templateCell = templateSubHeaderRow.getCell(i);
-            Cell newCell = subHeaderRow.createCell(i);
-            if (templateCell != null) {
-                newCell.setCellValue(templateCell.getStringCellValue());
-                newCell.setCellStyle(centerStyle);
-            }
-        }
-
-        // 从模板复制合并区域
-        copyMergedRegionsFromTemplate(sheet, templateSheet);
-    }
-
-    // 处理数据行 - 添加居中样式参数
-    private void processDataRows(Sheet sheet, List<WbsTreeContract> unitProjects, Long unitId, CellStyle centerStyle) {
-        int rowNum = 2; // 从第3行开始(0-based,所以是第3行)
-        Map<Integer, List<CellRangeAddress>> mergeMap = new HashMap<>();
-
-        // 按照层级顺序查找叶子节点:子分项 -> 分项 -> 子分部 -> 分部 -> 单位工程
-        List<WbsTreeContract> leafNodes = findLeafNodesByPriority(unitProjects);
-
-        for (WbsTreeContract leafNode : leafNodes) {
-            Row row = sheet.createRow(rowNum);
-
-            // 解析祖级节点
-            String[] ancestors = leafNode.getAncestors().split(",");
-
-            // 转换为 List<Long>
-            List<Long> ancestorIds = Arrays.stream(ancestors)
-                    .map(Long::parseLong)
-                    .collect(Collectors.toList());
-
-            // 根据叶子节点类型填充各级工程数据
-            fillEngineeringDataByLeafType(row, unitProjects, ancestorIds, mergeMap, rowNum, centerStyle, leafNode.getNodeType());
-
-            rowNum++;
-        }
-
-        // 应用合并单元格
-        applyMergedRegions(sheet, mergeMap, centerStyle);
-    }
-
-    // 按照优先级查找叶子节点
-    private List<WbsTreeContract> findLeafNodesByPriority(List<WbsTreeContract> projects) {
-        // 按照节点类型优先级:子分项(5) -> 分项(4) -> 子分部(3) -> 分部(2) -> 单位工程(18)
-        List<WbsTreeContract> leafNodes = new ArrayList<>();
-
-        // 1. 先找子分项工程 (节点类型5)
-        leafNodes = projects.stream()
-                .filter(item -> item.getNodeType() == 5)
-                .collect(Collectors.toList());
-
-        if (!leafNodes.isEmpty()) {
-            return leafNodes;
-        }
-
-        // 2. 如果没有子分项,找分项工程 (节点类型4)
-        leafNodes = projects.stream()
-                .filter(item -> item.getNodeType() == 4)
-                .collect(Collectors.toList());
-
-        if (!leafNodes.isEmpty()) {
-            return leafNodes;
-        }
-
-        // 3. 如果没有分项,找子分部工程 (节点类型3)
-        leafNodes = projects.stream()
-                .filter(item -> item.getNodeType() == 3)
-                .collect(Collectors.toList());
-
-        if (!leafNodes.isEmpty()) {
-            return leafNodes;
-        }
 
-        // 4. 如果没有子分部,找分部工程 (节点类型2)
-        leafNodes = projects.stream()
-                .filter(item -> item.getNodeType() == 2)
-                .collect(Collectors.toList());
 
-        if (!leafNodes.isEmpty()) {
-            return leafNodes;
-        }
-
-        // 5. 最后找单位工程 (节点类型18)
-        return projects.stream()
-                .filter(item -> item.getNodeType() == 18)
-                .collect(Collectors.toList());
-    }
-
-    // 根据叶子节点类型填充工程数据
-    private void fillEngineeringDataByLeafType(Row row, List<WbsTreeContract> projects, List<Long> ancestorIds,
-                                               Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum,
-                                               CellStyle centerStyle, int leafNodeType) {
-
-        switch (leafNodeType) {
-            case 5: // 子分项工程 - 显示所有层级
-                fillLevelData(row, projects, ancestorIds, 0, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
-                fillLevelData(row, projects, ancestorIds, 3, 5, mergeMap, rowNum, 2, centerStyle);  // 分部工程
-                fillLevelData(row, projects, ancestorIds, 6, 8, mergeMap, rowNum, 3, centerStyle);  // 子分部工程
-                fillLevelData(row, projects, ancestorIds, 9, 11, mergeMap, rowNum, 4, centerStyle); // 分项工程
-                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 12, 13, 14, mergeMap, rowNum, 5, centerStyle); // 子分项工程
-                break;
-
-            case 4: // 分项工程 - 显示到分项
-                fillLevelData(row, projects, ancestorIds, 0, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
-                fillLevelData(row, projects, ancestorIds, 3, 5, mergeMap, rowNum, 2, centerStyle);  // 分部工程
-                fillLevelData(row, projects, ancestorIds, 6, 8, mergeMap, rowNum, 3, centerStyle);  // 子分部工程
-                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 9, 10, 11, mergeMap, rowNum, 4, centerStyle); // 分项工程
-                break;
-
-            case 3: // 子分部工程 - 显示到子分部
-                fillLevelData(row, projects, ancestorIds, 0, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
-                fillLevelData(row, projects, ancestorIds, 3, 5, mergeMap, rowNum, 2, centerStyle);  // 分部工程
-                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 6, 7, 8, mergeMap, rowNum, 3, centerStyle); // 子分部工程
-                break;
-
-            case 2: // 分部工程 - 显示到分部
-                fillLevelData(row, projects, ancestorIds, 0, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
-                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 3, 4, 5, mergeMap, rowNum, 2, centerStyle); // 分部工程
-                break;
-
-            case 18: // 单位工程 - 只显示单位工程
-                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 0, 1, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
-                break;
-
-            default:
-                // 其他情况,尝试填充所有能找到的层级
-                fillAllAvailableLevels(row, projects, ancestorIds, mergeMap, rowNum, centerStyle);
-        }
-    }
-
-    // 填充叶子节点数据
-    private void fillLeafNodeData(Row row, List<WbsTreeContract> nodes, Long nodeId,
-                                  int codeCol, int nameCol, int idCol,
-                                  Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum, int expectedType, CellStyle centerStyle) {
-        WbsTreeContract node = findNodeById(nodes, nodeId);
-        if (node != null && node.getNodeType() == expectedType) {
-            // 划分编码(如果有)
-            if (node.getPartitionCode() != null && !node.getPartitionCode().isEmpty()) {
-                setCellValue(row, codeCol, node.getPartitionCode(), mergeMap, rowNum, centerStyle);
-            }
-            // 名称(一定有)
-            setCellValue(row, nameCol, node.getNodeName(), mergeMap, rowNum, centerStyle);
-            // pkeyId
-            setCellValue(row, idCol, node.getPKeyId().toString(), mergeMap, rowNum, centerStyle);
-        }
-    }
-
-    // 填充所有能找到的层级
-    private void fillAllAvailableLevels(Row row, List<WbsTreeContract> nodes, List<Long> ancestorIds,
-                                        Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum, CellStyle centerStyle) {
-        // 尝试填充单位工程
-        if (ancestorIds.size() > 0) {
-            WbsTreeContract unit = findNodeByIdAndType(nodes, ancestorIds, 18);
-            if (unit != null) {
-                fillLeafNodeData(row, nodes, unit.getPKeyId(), 0, 1, 2, mergeMap, rowNum, 18, centerStyle);
-            }
-        }
-
-        // 尝试填充分部工程
-        if (ancestorIds.size() > 1) {
-            WbsTreeContract division = findNodeByIdAndType(nodes, ancestorIds, 2);
-            if (division != null) {
-                fillLeafNodeData(row, nodes, division.getPKeyId(), 3, 4, 5, mergeMap, rowNum, 2, centerStyle);
-            }
-        }
-
-        // 尝试填充子分部工程
-        if (ancestorIds.size() > 2) {
-            WbsTreeContract subDivision = findNodeByIdAndType(nodes, ancestorIds, 3);
-            if (subDivision != null) {
-                fillLeafNodeData(row, nodes, subDivision.getPKeyId(), 6, 7, 8, mergeMap, rowNum, 3, centerStyle);
-            }
-        }
-
-        // 尝试填充分项工程
-        if (ancestorIds.size() > 3) {
-            WbsTreeContract item = findNodeByIdAndType(nodes, ancestorIds, 4);
-            if (item != null) {
-                fillLeafNodeData(row, nodes, item.getPKeyId(), 9, 10, 11, mergeMap, rowNum, 4, centerStyle);
-            }
-        }
-
-        // 尝试填充子分项工程
-        if (ancestorIds.size() > 4) {
-            WbsTreeContract subItem = findNodeByIdAndType(nodes, ancestorIds, 5);
-            if (subItem != null) {
-                fillLeafNodeData(row, nodes, subItem.getPKeyId(), 12, 13, 14, mergeMap, rowNum, 5, centerStyle);
-            }
-        }
-    }
-
-    // 根据ID查找节点
-    private WbsTreeContract findNodeById(List<WbsTreeContract> list, Long id) {
-        return list.stream()
-                .filter(item -> item.getPKeyId().equals(id))
-                .findFirst()
-                .orElse(null);
-    }
-
-    // 填充各级工程数据 - 添加居中样式参数
-    private void fillLevelData(Row row, List<WbsTreeContract> projects, List<Long> ancestors,
-                               int startCol, int endCol,
-                               Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum, int nodeType, CellStyle centerStyle) {
-        if (!ancestors.isEmpty()) {
-            WbsTreeContract node = findNodeByIdAndType(projects, ancestors, nodeType);
-            if (node != null) {
-                // 划分编码(如果有)
-                if (node.getPartitionCode() != null && !node.getPartitionCode().isEmpty()) {
-                    setCellValue(row, startCol, node.getPartitionCode(), mergeMap, rowNum, centerStyle);
-                }
-                // 名称(一定有)
-                setCellValue(row, startCol + 1, node.getNodeName(), mergeMap, rowNum, centerStyle);
-                // pkeyId
-                setCellValue(row, endCol, node.getPKeyId().toString(), mergeMap, rowNum, centerStyle);
-            }
-        }
-    }
-
-
-    // 设置单元格值并记录合并信息 - 添加居中样式参数
-    private void setCellValue(Row row, int col, String value, Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum, CellStyle centerStyle) {
-        Cell cell = row.getCell(col);
-        if (cell == null) {
-            cell = row.createCell(col);
-        }
-        cell.setCellValue(value);
-        cell.setCellStyle(centerStyle); // 设置居中样式和边框
-
-        if (!mergeMap.containsKey(col)) {
-            mergeMap.put(col, new ArrayList<>());
-        }
-        mergeMap.get(col).add(new CellRangeAddress(rowNum, rowNum, col, col));
-    }
-
-    // 应用合并单元格 - 添加居中样式参数
-    private void applyMergedRegions(Sheet sheet, Map<Integer, List<CellRangeAddress>> mergeMap, CellStyle centerStyle) {
-        // 使用Set来记录已经合并的区域,避免重复合并
-        Set<String> mergedRegions = new HashSet<>();
-
-        // 首先处理名称列的合并
-        Map<Integer, List<CellRangeAddress>> nameColumnMerges = new HashMap<>();
-
-        // 名称列的索引:1, 4, 7, 10, 13
-        int[] nameColumns = {1, 4, 7, 10, 13};
-
-        // 收集名称列的合并信息
-        for (int nameCol : nameColumns) {
-            if (mergeMap.containsKey(nameCol)) {
-                nameColumnMerges.put(nameCol, new ArrayList<>(mergeMap.get(nameCol)));
-            }
-        }
-
-        // 合并名称列和对应的划分编码列
-        for (Map.Entry<Integer, List<CellRangeAddress>> entry : nameColumnMerges.entrySet()) {
-            int nameCol = entry.getKey();
-            List<CellRangeAddress> regions = entry.getValue();
-
-            if (regions.size() > 1) {
-                // 按行号排序
-                regions.sort(Comparator.comparingInt(CellRangeAddress::getFirstRow));
-
-                int mergeStart = -1;
-                int mergeEnd = -1;
-                String lastValue = null;
-
-                for (CellRangeAddress region : regions) {
-                    int rowNum = region.getFirstRow();
-                    Row row = sheet.getRow(rowNum);
-                    if (row == null) continue;
-
-                    Cell cell = row.getCell(nameCol);
-                    if (cell == null) continue;
-
-                    String currentValue = getCellStringValue(cell);
-
-                    if (lastValue == null) {
-                        // 第一个单元格
-                        mergeStart = rowNum;
-                        mergeEnd = rowNum;
-                        lastValue = currentValue;
-                    } else if (currentValue.equals(lastValue)) {
-                        // 值相同,扩展合并范围
-                        mergeEnd = rowNum;
-                    } else {
-                        // 值不同,合并之前的区域
-                        if (mergeStart < mergeEnd) {
-                            mergeNameAndCodeColumns(sheet, nameCol, mergeStart, mergeEnd, centerStyle, mergedRegions);
-                        }
-                        // 开始新的合并区域
-                        mergeStart = rowNum;
-                        mergeEnd = rowNum;
-                        lastValue = currentValue;
-                    }
-                }
-
-                // 合并最后一段区域
-                if (mergeStart < mergeEnd) {
-                    mergeNameAndCodeColumns(sheet, nameCol, mergeStart, mergeEnd, centerStyle, mergedRegions);
-                }
-            }
-        }
-
-        // 处理其他列的合并(pkeyId列等)
-        for (Map.Entry<Integer, List<CellRangeAddress>> entry : mergeMap.entrySet()) {
-            int col = entry.getKey();
-            List<CellRangeAddress> regions = entry.getValue();
-
-            // 跳过已经处理过的名称列
-            if (isNameColumn(col)) {
-                continue;
-            }
-
-            if (regions.size() > 1) {
-                // 按行号排序
-                regions.sort(Comparator.comparingInt(CellRangeAddress::getFirstRow));
-
-                int mergeStart = -1;
-                int mergeEnd = -1;
-                String lastValue = null;
-
-                for (CellRangeAddress region : regions) {
-                    int rowNum = region.getFirstRow();
-                    Row row = sheet.getRow(rowNum);
-                    if (row == null) continue;
-
-                    Cell cell = row.getCell(col);
-                    if (cell == null) continue;
-
-                    String currentValue = getCellStringValue(cell);
-
-                    if (lastValue == null) {
-                        // 第一个单元格
-                        mergeStart = rowNum;
-                        mergeEnd = rowNum;
-                        lastValue = currentValue;
-                    } else if (currentValue.equals(lastValue)) {
-                        // 值相同,扩展合并范围
-                        mergeEnd = rowNum;
-                    } else {
-                        // 值不同,合并之前的区域
-                        if (mergeStart < mergeEnd) {
-                            mergeSingleColumnIfNotExists(sheet, col, mergeStart, mergeEnd, centerStyle, mergedRegions);
-                        }
-                        // 开始新的合并区域
-                        mergeStart = rowNum;
-                        mergeEnd = rowNum;
-                        lastValue = currentValue;
-                    }
-                }
-
-                // 合并最后一段区域
-                if (mergeStart < mergeEnd) {
-                    mergeSingleColumnIfNotExists(sheet, col, mergeStart, mergeEnd, centerStyle, mergedRegions);
-                }
-            }
-        }
-    }
-
-    // 合并名称列和对应的划分编码列(检查是否已存在)
-    private void mergeNameAndCodeColumns(Sheet sheet, int nameCol, int startRow, int endRow, CellStyle centerStyle, Set<String> mergedRegions) {
-        // 合并名称列
-        mergeSingleColumnIfNotExists(sheet, nameCol, startRow, endRow, centerStyle, mergedRegions);
-
-        // 合并对应的划分编码列
-        int codeCol = getCorrespondingCodeColumn(nameCol);
-        if (codeCol != -1) {
-            mergeSingleColumnIfNotExists(sheet, codeCol, startRow, endRow, centerStyle, mergedRegions);
-        }
-    }
-
-    // 合并单列并设置样式(检查是否已存在)
-    private void mergeSingleColumnIfNotExists(Sheet sheet, int col, int startRow, int endRow, CellStyle style, Set<String> mergedRegions) {
-        String regionKey = col + ":" + startRow + ":" + endRow;
-
-        if (!mergedRegions.contains(regionKey)) {
-            CellRangeAddress mergedRegion = new CellRangeAddress(startRow, endRow, col, col);
-
-            // 检查是否已经存在相同的合并区域
-            boolean alreadyExists = false;
-            for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
-                CellRangeAddress existingRegion = sheet.getMergedRegion(i);
-                if (existingRegion.getFirstColumn() == col &&
-                        existingRegion.getFirstRow() == startRow &&
-                        existingRegion.getLastRow() == endRow) {
-                    alreadyExists = true;
-                    break;
-                }
-            }
-
-            if (!alreadyExists) {
-                sheet.addMergedRegion(mergedRegion);
-                setMergedRegionStyle(sheet, mergedRegion, style);
-                mergedRegions.add(regionKey);
-            }
-        }
-    }
-
-    // 判断是否是名称列
-    private boolean isNameColumn(int columnIndex) {
-        int[] nameColumns = {1, 4, 7, 10, 13};
-        for (int nameCol : nameColumns) {
-            if (columnIndex == nameCol) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    // 获取对应的划分编码列
-    private int getCorrespondingCodeColumn(int nameColumn) {
-        switch (nameColumn) {
-            case 1: return 0;  // 单位工程名称(1) -> 单位工程划分编码(0)
-            case 4: return 3;  // 分部工程名称(4) -> 分部工程划分编码(3)
-            case 7: return 6;  // 子分部工程名称(7) -> 子分部工程划分编码(6)
-            case 10: return 9; // 分项工程名称(10) -> 分项工程划分编码(9)
-            case 13: return 12; // 子分项工程名称(13) -> 子分项工程划分编码(12)
-            default: return -1;
-        }
-    }
-
-    // 设置合并区域样式为居中
-    private void setMergedRegionStyle(Sheet sheet, CellRangeAddress mergedRegion, CellStyle style) {
-        for (int rowNum = mergedRegion.getFirstRow(); rowNum <= mergedRegion.getLastRow(); rowNum++) {
-            Row row = sheet.getRow(rowNum);
-            if (row != null) {
-                for (int colNum = mergedRegion.getFirstColumn(); colNum <= mergedRegion.getLastColumn(); colNum++) {
-                    Cell cell = row.getCell(colNum);
-                    if (cell != null) {
-                        cell.setCellStyle(style);
-                    }
-                }
-            }
-        }
-    }
-
-    // 获取单元格的字符串值
-    private String getCellStringValue(Cell cell) {
-        if (cell == null) {
-            return "";
-        }
-
-        try {
-            return cell.getStringCellValue();
-        } catch (Exception e) {
-            return "";
-        }
-    }
-
-    // 自动调整列宽
-    private void autoSizeColumns(Sheet sheet) {
-        for (int i = 0; i < 15; i++) {
-            // 先手动计算列宽
-            int maxWidth = calculateColumnWidth(sheet, i);
-
-            // 设置列宽,至少3000(约30个字符宽度)
-            int columnWidth = Math.max(maxWidth + 1000, 3000); // 加一些边距
-            sheet.setColumnWidth(i, columnWidth);
-        }
-    }
-
-    // 手动计算列宽
-    private int calculateColumnWidth(Sheet sheet, int columnIndex) {
-        int maxWidth = 0;
-
-        for (int rowNum = 0; rowNum <= sheet.getLastRowNum(); rowNum++) {
-            Row row = sheet.getRow(rowNum);
-            if (row != null) {
-                Cell cell = row.getCell(columnIndex);
-                if (cell != null) {
-                    String cellValue = getCellStringValue(cell);
-                    if (cellValue != null && !cellValue.isEmpty()) {
-                        // 估算字符串宽度(中文字符算2个英文字符宽度)
-                        int width = estimateStringWidth(cellValue);
-                        maxWidth = Math.max(maxWidth, width);
-                    }
-                }
-            }
-        }
-
-        return maxWidth * 256; // POI中列宽的单位是1/256个字符宽度
-    }
-
-    // 估算字符串宽度
-    private int estimateStringWidth(String text) {
-        if (text == null || text.isEmpty()) {
-            return 0;
-        }
-
-        int width = 0;
-        for (char c : text.toCharArray()) {
-            // 中文字符或全角字符宽度为2,英文字符宽度为1
-            if (c >= 0x4E00 && c <= 0x9FA5) {
-                width += 2; // 中文字符
-            } else if (c > 0xFF00 && c < 0xFF5F) {
-                width += 2; // 全角字符
-            } else {
-                width += 1; // 英文字符
-            }
-        }
-
-        return width;
-    }
-
-    // 保存Workbook到本地文件
-    private void saveWorkbookToFile(org.apache.poi.ss.usermodel.Workbook workbook, String filePath) {
-        try {
-            File file = new File(filePath);
-            File parentDir = file.getParentFile();
-            if (parentDir != null && !parentDir.exists()) {
-                parentDir.mkdirs();
-            }
-
-            try (FileOutputStream fileOut = new FileOutputStream(file)) {
-                workbook.write(fileOut);
-                System.out.println("文件已成功保存到: " + filePath);
-            }
-        } catch (IOException e) {
-            System.err.println("保存文件失败: " + e.getMessage());
-            e.printStackTrace();
-            throw new RuntimeException("保存文件失败", e);
-        }
-    }
-
-    // 根据ID和类型查找节点
-    private WbsTreeContract findNodeByIdAndType(List<WbsTreeContract> list, List<Long> ids, int nodeType) {
-        return list.stream()
-                .filter(item -> ids.contains(item.getPKeyId()) && item.getNodeType() == nodeType)
-                .findFirst()
-                .orElse(null);
-    }
-
-
-    // 从模板复制合并区域
-    private void copyMergedRegionsFromTemplate(Sheet targetSheet, Sheet templateSheet) {
-        for (int i = 0; i < templateSheet.getNumMergedRegions(); i++) {
-            CellRangeAddress mergedRegion = templateSheet.getMergedRegion(i);
-            // 只复制前两行的合并区域
-            if (mergedRegion.getFirstRow() <= 1) {
-                targetSheet.addMergedRegion(mergedRegion);
-            }
-        }
-    }
 
 
 }

+ 2 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeContractMapper.java

@@ -173,4 +173,6 @@ public interface WbsTreeContractMapper extends EasyBaseMapper<WbsTreeContract> {
      */
     List<APIWbsContractSubdivisionVo> getWbsContractSubdivisionParentNode(@Param("contractId") String contractId,
                                                                           @Param("ids") Set<Long> ids);
+
+    void updatePartitionCodeByPKyId(@Param("wbsTreeContract") WbsTreeContract wbsTreeContract);
 }

+ 7 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeContractMapper.xml

@@ -836,6 +836,13 @@
             where p_key_id = #{item.pKeyId}
         </foreach>
     </update>
+    <update id="updatePartitionCodeByPKyId">
+        <if test="wbsTreeContract.partitionCode != null and wbsTreeContract.pKeyId != null and wbsTreeContract.partitionCode != '' and wbsTreeContract.pKeyId != ''">
+            UPDATE m_wbs_tree_contract
+            SET partition_code = #{wbsTreeContract.partitionCode}
+            WHERE p_key_id = #{wbsTreeContract.pKeyId}
+        </if>
+    </update>
 
     <select id="selectQueryValueLikeNodeName" resultMap="ResultMap">
         select *

+ 4 - 1
blade-service/blade-manager/src/main/java/org/springblade/manager/service/IWbsTreeContractService.java

@@ -1,5 +1,6 @@
 package org.springblade.manager.service;
 
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.springblade.business.dto.EKeyDto;
 import org.springblade.core.mp.base.BaseService;
 import org.springblade.core.tool.api.R;
@@ -10,9 +11,12 @@ import org.springblade.manager.entity.ContractRelationJlyz;
 import org.springblade.manager.entity.WbsTreeContract;
 import org.springblade.manager.entity.WbsTreePrivate;
 import org.springblade.manager.vo.*;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.List;
@@ -97,5 +101,4 @@ public interface IWbsTreeContractService extends BaseService<WbsTreeContract> {
     ResponseEntity<Resource> exportTree(Long contractId, HttpServletResponse response) throws IOException, InvalidFormatException;
 
     R importPartitionCode(MultipartFile file);
-
 }

+ 776 - 25
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeContractServiceImpl.java

@@ -16,10 +16,16 @@ import org.apache.commons.lang.StringUtils;
 import org.apache.ibatis.session.ExecutorType;
 import org.apache.ibatis.session.SqlSession;
 import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.apache.poi.xssf.usermodel.XSSFCell;
+import org.apache.poi.xssf.usermodel.XSSFRow;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
@@ -45,7 +51,6 @@ import org.springblade.core.log.exception.ServiceException;
 import org.springblade.core.mp.base.BaseServiceImpl;
 import org.springblade.core.redis.cache.BladeRedis;
 import org.springblade.core.secure.utils.AuthUtil;
-import org.springblade.core.secure.utils.SecureUtil;
 import org.springblade.core.tool.api.R;
 import org.springblade.core.tool.node.ForestNodeMerger;
 import org.springblade.core.tool.utils.*;
@@ -56,15 +61,19 @@ import org.springblade.manager.excel.WbsExcelBatchUtil;
 import org.springblade.manager.excel.WbsExcelUtil;
 import org.springblade.manager.feign.ContractClient;
 import org.springblade.manager.mapper.*;
+import org.springblade.manager.service.INodeBaseInfoService;
 import org.springblade.manager.service.ITableFileService;
 import org.springblade.manager.service.IWbsTreeContractService;
 import org.springblade.manager.utils.CompositeKey;
 import org.springblade.manager.vo.*;
 import org.springblade.system.cache.ParamCache;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.dao.DataAccessException;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.Resource;
 import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.jdbc.BadSqlGrammarException;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
@@ -77,10 +86,8 @@ import org.springframework.util.LinkedCaseInsensitiveMap;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.MathContext;
@@ -134,6 +141,8 @@ public class WbsTreeContractServiceImpl extends BaseServiceImpl<WbsTreeContractM
     private final Map<String, List<WbsTreeContractLazyVO>> localCacheParentCountNodes = new ConcurrentHashMap<>();
     @Autowired
     private WbsTreeMapper wbsTreeMapper;
+    @Autowired
+    private WbsTreeContractMapper wbsTreeContractMapper;
 
     @Override
     public List<WbsTreeContract> selectQueryCurrentNodeByAncestors(List<String> ids, String contractId) {
@@ -614,23 +623,6 @@ public class WbsTreeContractServiceImpl extends BaseServiceImpl<WbsTreeContractM
 //        if(info!=null){
 //            resultTabs.forEach(tab->tab.setFileName(info.getName()));
 //        }
-        //同步标识
-        if (CollectionUtil.isNotEmpty(resultTabs) && SecureUtil.isAdministrator()) {
-            List<Long> collect1 = resultTabs.stream().map(WbsTreeContract::getPKeyId).collect(Collectors.toList());
-            List<Long> longs = null;
-            try {
-                longs = jdbcTemplate.queryForList("select p_key_id from m_wbs_tree_contract_extend where is_sync = 1 and is_deleted = 0 and p_key_id in (" + StringUtils.join(collect1, ",") + ")", Long.class);
-            } catch (DataAccessException e) {
-                //TODO 暂时忽略异常,避免表不存在报错
-                e.printStackTrace();
-            }
-            if (CollectionUtil.isNotEmpty(longs)) {
-                List<Long> finalLongs = longs;
-                resultTabs.forEach(f -> {
-                    f.setIsSync(finalLongs.contains(f.getPKeyId()) && f.getIsBussShow() != 2 ? 1 : 0);
-                });
-            }
-        }
 
         if (Optional.ofNullable(wbsTreeContract.getIsUseSort()).orElse(0) == 0) {
             //表单排序
@@ -3243,7 +3235,6 @@ public class WbsTreeContractServiceImpl extends BaseServiceImpl<WbsTreeContractM
         return true;
     }
 
-
     /**
      * 展开所有合并单元格并将值填充到每个单元格
      *
@@ -4468,6 +4459,766 @@ public class WbsTreeContractServiceImpl extends BaseServiceImpl<WbsTreeContractM
         return list;
     }
 
+    @Override
+    public ResponseEntity<Resource> exportTree(Long contractId, HttpServletResponse response) throws IOException, InvalidFormatException {
+
+        String templatePath = "/mnt/sdc/Users/hongchuangyanfa/Desktop/excel/gcdcTemplate.xlsx";
+        //String templatePath = "C:\\upload\\excel\\gcdc.xlsx";
+        // 查询数据
+        String sql = "select * from m_wbs_tree_contract where contract_id = " + contractId +
+                " and is_deleted = 0 and type = 1 and node_type != 6 and p_id != 0";
+        List<WbsTreeContract> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+
+        InputStream templateStream = new FileInputStream(new File(templatePath));
+        org.apache.poi.ss.usermodel.Workbook workbook = WorkbookFactory.create(templateStream);
+        Sheet templateSheet = workbook.getSheetAt(0);
+
+        // 移除默认的Sheet1
+        workbook.removeSheetAt(0);
+
+        // 创建居中的单元格样式
+        CellStyle centerStyle = workbook.createCellStyle();
+        centerStyle.setAlignment(HorizontalAlignment.CENTER);
+        centerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+
+        // 按单位工程分组
+        Map<Long, List<WbsTreeContract>> unitProjectMap = list.stream()
+                .filter(item -> item.getNodeType() == 18) // 单位工程
+                .collect(Collectors.toMap(
+                        WbsTreeContract::getPKeyId,
+                        unit -> list.stream()
+                                .filter(item -> item.getAncestors() != null &&
+                                        item.getAncestors().contains(unit.getPKeyId().toString()))
+                                .collect(Collectors.toList())
+                ));
+
+        // 为每个单位工程创建sheet
+        for (Map.Entry<Long, List<WbsTreeContract>> entry : unitProjectMap.entrySet()) {
+            Long unitId = entry.getKey();
+            List<WbsTreeContract> unitProjects = entry.getValue();
+
+            // 获取单位工程名称
+            String unitName = unitProjects.stream()
+                    .filter(item -> item.getPKeyId().equals(unitId))
+                    .findFirst()
+                    .map(WbsTreeContract::getNodeName)
+                    .orElse("未知单位工程");
+            // 创建安全的sheet名称
+            String safeUnitName = unitName.replaceAll("[\\\\/*\\[\\]:?]", "_");
+            if (safeUnitName.length() > 31) {
+                safeUnitName = safeUnitName.substring(0, 31);
+            }
+            String validSheetName = WorkbookUtil.createSafeSheetName(safeUnitName);
+
+            // 创建sheet并设置名称
+            Sheet sheet = workbook.createSheet(validSheetName);
+
+            // 创建表头(两行)- 使用模板sheet
+            createHeaderRows(sheet, templateSheet, centerStyle);
+
+            // 处理数据行
+            processDataRows(sheet, unitProjects, unitId, centerStyle);
+
+            // 设置列宽自动调整
+            autoSizeColumns(sheet);
+
+            // 只隐藏pkeyId列(不保护,允许用户取消隐藏)
+            hidePkeyIdColumnsOnly(sheet);
+        }
+
+        // 保存文件到本地(本地测试放开,正式环境不需要)
+        // String outputPath = "C:\\upload\\excel\\111.xlsx";
+        //saveWorkbookToFile(workbook, outputPath);
+
+        // 同时返回给浏览器下载
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        workbook.write(outputStream);
+        workbook.close();
+
+        ByteArrayResource resource = new ByteArrayResource(outputStream.toByteArray());
+        return ResponseEntity.ok()
+                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=划分.xlsx")
+                .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                .contentLength(resource.contentLength())
+                .body(resource);
+    }
+
+    // 列映射关系:pkeyId列 -> 划分编码列
+    private static final Map<Integer, Integer> COLUMN_MAPPING = new HashMap<>();
+
+    static {
+        COLUMN_MAPPING.put(2, 0);   // C列(2) -> A列(0) 单位工程
+        COLUMN_MAPPING.put(5, 3);   // F列(5) -> D列(3) 分部工程
+        COLUMN_MAPPING.put(8, 6);   // I列(8) -> G列(6) 子分部工程
+        COLUMN_MAPPING.put(11, 9);  // L列(11) -> J列(9) 分项工程
+        COLUMN_MAPPING.put(14, 12); // O列(14) -> M列(12) 子分项工程
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R importPartitionCode(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            return R.fail("文件不能为空");
+        }
+        String originalFilename = file.getOriginalFilename();
+        if (originalFilename == null || !originalFilename.endsWith(".xlsx")) {
+            return R.fail("只支持.xlsx格式的文件");
+        }
+        List<WbsTreeContract> wbsTreeContractList = new ArrayList<>();
+        int totalCount = 0;
+        int successCount = 0;
+        try (InputStream inputStream = file.getInputStream();
+             XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) {
+
+            // 遍历所有sheet
+            for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) {
+                XSSFSheet sheet = workbook.getSheetAt(sheetIndex);
+                // 跳过表头(前两行)
+                for (int rowIndex = 2; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
+                    XSSFRow row = sheet.getRow(rowIndex);
+                    if (row == null) continue;
+
+                    // 处理每一列的映射关系
+                    for (Map.Entry<Integer, Integer> entry : COLUMN_MAPPING.entrySet()) {
+                        int pkeyColIndex = entry.getKey();
+                        int codeColIndex = entry.getValue();
+
+                        XSSFCell pkeyCell = row.getCell(pkeyColIndex);
+                        XSSFCell codeCell = row.getCell(codeColIndex);
+
+                        // 检查pkeyId和划分编码是否有效
+                        if (isValidCell(pkeyCell) && isValidCell(codeCell)) {
+                            String pkeyId = getCellValue(pkeyCell);
+                            String partitionCode = getCellValue(codeCell);
+
+                            if (StringUtils.isNotBlank(pkeyId) && StringUtils.isNotBlank(partitionCode)) {
+                                totalCount++;
+                                WbsTreeContract wbsTreeContract = new WbsTreeContract();
+                                wbsTreeContract.setPKeyId(Long.parseLong(pkeyId.trim()));
+                                wbsTreeContract.setPartitionCode(partitionCode.trim());
+                                wbsTreeContractList.add(wbsTreeContract);
+                                successCount++;
+                            }
+                        }
+                    }
+                }
+            }
+
+            // 批量保存到数据库
+            if (!wbsTreeContractList.isEmpty()) {
+                for (WbsTreeContract wbsTreeContract : wbsTreeContractList) {
+                    wbsTreeContractMapper.updatePartitionCodeByPKyId(wbsTreeContract);
+                }
+                return R.status(true);
+            }
+        } catch (IOException e) {
+            log.error("读取Excel文件失败", e);
+            return R.fail("读取文件失败: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("导入划分编码数据失败", e);
+            return R.fail("导入失败: " + e.getMessage());
+        }
+        return R.status(false);
+    }
+
+    /**
+     * 检查单元格是否有效
+     */
+    private boolean isValidCell(XSSFCell cell) {
+        return cell != null && cell.getCellType() != CellType.BLANK.getCode();
+    }
+
+    /**
+     * 获取单元格的值
+     */
+    private String getCellValue(XSSFCell cell) {
+        if (cell == null) {
+            return "";
+        }
+
+        try {
+            return cell.getStringCellValue();
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+
+    // 只隐藏pkeyId列(不设置保护,允许用户取消隐藏)
+    private void hidePkeyIdColumnsOnly(Sheet sheet) {
+        int[] pkeyIdColumns = {2, 5, 8, 11, 14};
+
+        // 只隐藏列,不设置任何保护
+        for (int col : pkeyIdColumns) {
+            sheet.setColumnHidden(col, true);
+        }
+
+        // 特别注意:不调用 sheet.protectSheet() 方法
+        // 这样用户就可以自由调整列宽和取消隐藏列
+    }
+
+    // 创建表头行(两行)- 添加居中样式
+    private void createHeaderRows(Sheet sheet, Sheet templateSheet, CellStyle centerStyle) {
+        // 复制第一行
+        Row templateHeaderRow = templateSheet.getRow(0);
+        Row headerRow = sheet.createRow(0);
+        for (int i = 0; i < 15; i++) {
+            Cell templateCell = templateHeaderRow.getCell(i);
+            Cell newCell = headerRow.createCell(i);
+            if (templateCell != null) {
+                newCell.setCellValue(templateCell.getStringCellValue());
+                newCell.setCellStyle(centerStyle);
+            }
+        }
+
+        // 复制第二行
+        Row templateSubHeaderRow = templateSheet.getRow(1);
+        Row subHeaderRow = sheet.createRow(1);
+        for (int i = 0; i < 15; i++) {
+            Cell templateCell = templateSubHeaderRow.getCell(i);
+            Cell newCell = subHeaderRow.createCell(i);
+            if (templateCell != null) {
+                newCell.setCellValue(templateCell.getStringCellValue());
+                newCell.setCellStyle(centerStyle);
+            }
+        }
+
+        // 从模板复制合并区域
+        copyMergedRegionsFromTemplate(sheet, templateSheet);
+    }
+
+    // 处理数据行 - 添加居中样式参数
+    private void processDataRows(Sheet sheet, List<WbsTreeContract> unitProjects, Long unitId, CellStyle centerStyle) {
+        int rowNum = 2; // 从第3行开始(0-based,所以是第3行)
+        Map<Integer, List<CellRangeAddress>> mergeMap = new HashMap<>();
+
+        // 按照层级顺序查找叶子节点:子分项 -> 分项 -> 子分部 -> 分部 -> 单位工程
+        List<WbsTreeContract> leafNodes = findLeafNodesByPriority(unitProjects);
+
+        for (WbsTreeContract leafNode : leafNodes) {
+            Row row = sheet.createRow(rowNum);
+
+            // 解析祖级节点
+            String[] ancestors = leafNode.getAncestors().split(",");
+
+            // 转换为 List<Long>
+            List<Long> ancestorIds = Arrays.stream(ancestors)
+                    .map(Long::parseLong)
+                    .collect(Collectors.toList());
+
+            // 根据叶子节点类型填充各级工程数据
+            fillEngineeringDataByLeafType(row, unitProjects, ancestorIds, mergeMap, rowNum, centerStyle, leafNode.getNodeType());
+
+            rowNum++;
+        }
+
+        // 应用合并单元格
+        applyMergedRegions(sheet, mergeMap, centerStyle);
+    }
+
+    // 按照优先级查找叶子节点
+    private List<WbsTreeContract> findLeafNodesByPriority(List<WbsTreeContract> projects) {
+        // 按照节点类型优先级:子分项(5) -> 分项(4) -> 子分部(3) -> 分部(2) -> 单位工程(18)
+        List<WbsTreeContract> leafNodes = new ArrayList<>();
+
+        // 1. 先找子分项工程 (节点类型5)
+        leafNodes = projects.stream()
+                .filter(item -> item.getNodeType() == 5)
+                .collect(Collectors.toList());
+
+        if (!leafNodes.isEmpty()) {
+            return leafNodes;
+        }
+
+        // 2. 如果没有子分项,找分项工程 (节点类型4)
+        leafNodes = projects.stream()
+                .filter(item -> item.getNodeType() == 4)
+                .collect(Collectors.toList());
+
+        if (!leafNodes.isEmpty()) {
+            return leafNodes;
+        }
+
+        // 3. 如果没有分项,找子分部工程 (节点类型3)
+        leafNodes = projects.stream()
+                .filter(item -> item.getNodeType() == 3)
+                .collect(Collectors.toList());
+
+        if (!leafNodes.isEmpty()) {
+            return leafNodes;
+        }
+
+        // 4. 如果没有子分部,找分部工程 (节点类型2)
+        leafNodes = projects.stream()
+                .filter(item -> item.getNodeType() == 2)
+                .collect(Collectors.toList());
+
+        if (!leafNodes.isEmpty()) {
+            return leafNodes;
+        }
+
+        // 5. 最后找单位工程 (节点类型18)
+        return projects.stream()
+                .filter(item -> item.getNodeType() == 18)
+                .collect(Collectors.toList());
+    }
+
+    // 根据叶子节点类型填充工程数据
+    private void fillEngineeringDataByLeafType(Row row, List<WbsTreeContract> projects, List<Long> ancestorIds,
+                                               Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum,
+                                               CellStyle centerStyle, int leafNodeType) {
+
+        switch (leafNodeType) {
+            case 5: // 子分项工程 - 显示所有层级
+                fillLevelData(row, projects, ancestorIds, 0, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
+                fillLevelData(row, projects, ancestorIds, 3, 5, mergeMap, rowNum, 2, centerStyle);  // 分部工程
+                fillLevelData(row, projects, ancestorIds, 6, 8, mergeMap, rowNum, 3, centerStyle);  // 子分部工程
+                fillLevelData(row, projects, ancestorIds, 9, 11, mergeMap, rowNum, 4, centerStyle); // 分项工程
+                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 12, 13, 14, mergeMap, rowNum, 5, centerStyle); // 子分项工程
+                break;
+
+            case 4: // 分项工程 - 显示到分项
+                fillLevelData(row, projects, ancestorIds, 0, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
+                fillLevelData(row, projects, ancestorIds, 3, 5, mergeMap, rowNum, 2, centerStyle);  // 分部工程
+                fillLevelData(row, projects, ancestorIds, 6, 8, mergeMap, rowNum, 3, centerStyle);  // 子分部工程
+                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 9, 10, 11, mergeMap, rowNum, 4, centerStyle); // 分项工程
+                break;
+
+            case 3: // 子分部工程 - 显示到子分部
+                fillLevelData(row, projects, ancestorIds, 0, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
+                fillLevelData(row, projects, ancestorIds, 3, 5, mergeMap, rowNum, 2, centerStyle);  // 分部工程
+                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 6, 7, 8, mergeMap, rowNum, 3, centerStyle); // 子分部工程
+                break;
+
+            case 2: // 分部工程 - 显示到分部
+                fillLevelData(row, projects, ancestorIds, 0, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
+                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 3, 4, 5, mergeMap, rowNum, 2, centerStyle); // 分部工程
+                break;
+
+            case 18: // 单位工程 - 只显示单位工程
+                fillLeafNodeData(row, projects, ancestorIds.get(ancestorIds.size() - 1), 0, 1, 2, mergeMap, rowNum, 18, centerStyle); // 单位工程
+                break;
+
+            default:
+                // 其他情况,尝试填充所有能找到的层级
+                fillAllAvailableLevels(row, projects, ancestorIds, mergeMap, rowNum, centerStyle);
+        }
+    }
+
+    // 填充叶子节点数据
+    private void fillLeafNodeData(Row row, List<WbsTreeContract> nodes, Long nodeId,
+                                  int codeCol, int nameCol, int idCol,
+                                  Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum, int expectedType, CellStyle centerStyle) {
+        WbsTreeContract node = findNodeById(nodes, nodeId);
+        if (node != null && node.getNodeType() == expectedType) {
+            // 划分编码(如果有)
+            if (node.getPartitionCode() != null && !node.getPartitionCode().isEmpty()) {
+                setCellValue(row, codeCol, node.getPartitionCode(), mergeMap, rowNum, centerStyle);
+            }
+            // 名称(一定有)
+            setCellValue(row, nameCol, node.getNodeName(), mergeMap, rowNum, centerStyle);
+            // pkeyId
+            setCellValue(row, idCol, node.getPKeyId().toString(), mergeMap, rowNum, centerStyle);
+        }
+    }
+
+    // 填充所有能找到的层级
+    private void fillAllAvailableLevels(Row row, List<WbsTreeContract> nodes, List<Long> ancestorIds,
+                                        Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum, CellStyle centerStyle) {
+        // 尝试填充单位工程
+        if (ancestorIds.size() > 0) {
+            WbsTreeContract unit = findNodeByIdAndType(nodes, ancestorIds, 18);
+            if (unit != null) {
+                fillLeafNodeData(row, nodes, unit.getPKeyId(), 0, 1, 2, mergeMap, rowNum, 18, centerStyle);
+            }
+        }
+
+        // 尝试填充分部工程
+        if (ancestorIds.size() > 1) {
+            WbsTreeContract division = findNodeByIdAndType(nodes, ancestorIds, 2);
+            if (division != null) {
+                fillLeafNodeData(row, nodes, division.getPKeyId(), 3, 4, 5, mergeMap, rowNum, 2, centerStyle);
+            }
+        }
+
+        // 尝试填充子分部工程
+        if (ancestorIds.size() > 2) {
+            WbsTreeContract subDivision = findNodeByIdAndType(nodes, ancestorIds, 3);
+            if (subDivision != null) {
+                fillLeafNodeData(row, nodes, subDivision.getPKeyId(), 6, 7, 8, mergeMap, rowNum, 3, centerStyle);
+            }
+        }
+
+        // 尝试填充分项工程
+        if (ancestorIds.size() > 3) {
+            WbsTreeContract item = findNodeByIdAndType(nodes, ancestorIds, 4);
+            if (item != null) {
+                fillLeafNodeData(row, nodes, item.getPKeyId(), 9, 10, 11, mergeMap, rowNum, 4, centerStyle);
+            }
+        }
+
+        // 尝试填充子分项工程
+        if (ancestorIds.size() > 4) {
+            WbsTreeContract subItem = findNodeByIdAndType(nodes, ancestorIds, 5);
+            if (subItem != null) {
+                fillLeafNodeData(row, nodes, subItem.getPKeyId(), 12, 13, 14, mergeMap, rowNum, 5, centerStyle);
+            }
+        }
+    }
+
+    // 根据ID查找节点
+    private WbsTreeContract findNodeById(List<WbsTreeContract> list, Long id) {
+        return list.stream()
+                .filter(item -> item.getPKeyId().equals(id))
+                .findFirst()
+                .orElse(null);
+    }
+
+    // 填充各级工程数据 - 添加居中样式参数
+    private void fillLevelData(Row row, List<WbsTreeContract> projects, List<Long> ancestors,
+                               int startCol, int endCol,
+                               Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum, int nodeType, CellStyle centerStyle) {
+        if (!ancestors.isEmpty()) {
+            WbsTreeContract node = findNodeByIdAndType(projects, ancestors, nodeType);
+            if (node != null) {
+                // 划分编码(可能为null)
+                if (node.getPartitionCode() != null && !node.getPartitionCode().isEmpty()) {
+                    setCellValue(row, startCol, node.getPartitionCode(), mergeMap, rowNum, centerStyle);
+                }
+                // 名称
+                setCellValue(row, startCol + 1, node.getNodeName(), mergeMap, rowNum, centerStyle);
+                // pkeyId
+                setCellValue(row, endCol, node.getPKeyId().toString(), mergeMap, rowNum, centerStyle);
+            }
+        }
+    }
+
+
+    // 设置单元格值并记录合并信息 - 添加居中样式参数
+    private void setCellValue(Row row, int col, String value, Map<Integer, List<CellRangeAddress>> mergeMap, int rowNum, CellStyle centerStyle) {
+        Cell cell = row.getCell(col);
+        if (cell == null) {
+            cell = row.createCell(col);
+        }
+        cell.setCellValue(value);
+        cell.setCellStyle(centerStyle); // 设置居中样式和边框
+
+        if (!mergeMap.containsKey(col)) {
+            mergeMap.put(col, new ArrayList<>());
+        }
+        mergeMap.get(col).add(new CellRangeAddress(rowNum, rowNum, col, col));
+    }
+
+    // 应用合并单元格 - 添加居中样式参数
+    private void applyMergedRegions(Sheet sheet, Map<Integer, List<CellRangeAddress>> mergeMap, CellStyle centerStyle) {
+        // 使用Set来记录已经合并的区域,避免重复合并
+        Set<String> mergedRegions = new HashSet<>();
+
+        // 首先处理名称列的合并
+        Map<Integer, List<CellRangeAddress>> nameColumnMerges = new HashMap<>();
+
+        // 名称列的索引:1, 4, 7, 10, 13
+        int[] nameColumns = {1, 4, 7, 10, 13};
+
+        // 收集名称列的合并信息
+        for (int nameCol : nameColumns) {
+            if (mergeMap.containsKey(nameCol)) {
+                nameColumnMerges.put(nameCol, new ArrayList<>(mergeMap.get(nameCol)));
+            }
+        }
+
+        // 合并名称列和对应的划分编码列
+        for (Map.Entry<Integer, List<CellRangeAddress>> entry : nameColumnMerges.entrySet()) {
+            int nameCol = entry.getKey();
+            List<CellRangeAddress> regions = entry.getValue();
+
+            if (regions.size() > 1) {
+                // 按行号排序
+                regions.sort(Comparator.comparingInt(CellRangeAddress::getFirstRow));
+
+                int mergeStart = -1;
+                int mergeEnd = -1;
+                String lastValue = null;
+
+                for (CellRangeAddress region : regions) {
+                    int rowNum = region.getFirstRow();
+                    Row row = sheet.getRow(rowNum);
+                    if (row == null) continue;
+
+                    Cell cell = row.getCell(nameCol);
+                    if (cell == null) continue;
+
+                    String currentValue = getCellStringValue(cell);
+
+                    if (lastValue == null) {
+                        // 第一个单元格
+                        mergeStart = rowNum;
+                        mergeEnd = rowNum;
+                        lastValue = currentValue;
+                    } else if (currentValue.equals(lastValue)) {
+                        // 值相同,扩展合并范围
+                        mergeEnd = rowNum;
+                    } else {
+                        // 值不同,合并之前的区域
+                        if (mergeStart < mergeEnd) {
+                            mergeNameAndCodeColumns(sheet, nameCol, mergeStart, mergeEnd, centerStyle, mergedRegions);
+                        }
+                        // 开始新的合并区域
+                        mergeStart = rowNum;
+                        mergeEnd = rowNum;
+                        lastValue = currentValue;
+                    }
+                }
+
+                // 合并最后一段区域
+                if (mergeStart < mergeEnd) {
+                    mergeNameAndCodeColumns(sheet, nameCol, mergeStart, mergeEnd, centerStyle, mergedRegions);
+                }
+            }
+        }
+
+        // 处理其他列的合并(pkeyId列等)
+        for (Map.Entry<Integer, List<CellRangeAddress>> entry : mergeMap.entrySet()) {
+            int col = entry.getKey();
+            List<CellRangeAddress> regions = entry.getValue();
+
+            // 跳过已经处理过的名称列
+            if (isNameColumn(col)) {
+                continue;
+            }
+
+            if (regions.size() > 1) {
+                // 按行号排序
+                regions.sort(Comparator.comparingInt(CellRangeAddress::getFirstRow));
+
+                int mergeStart = -1;
+                int mergeEnd = -1;
+                String lastValue = null;
+
+                for (CellRangeAddress region : regions) {
+                    int rowNum = region.getFirstRow();
+                    Row row = sheet.getRow(rowNum);
+                    if (row == null) continue;
+
+                    Cell cell = row.getCell(col);
+                    if (cell == null) continue;
+
+                    String currentValue = getCellStringValue(cell);
+
+                    if (lastValue == null) {
+                        // 第一个单元格
+                        mergeStart = rowNum;
+                        mergeEnd = rowNum;
+                        lastValue = currentValue;
+                    } else if (currentValue.equals(lastValue)) {
+                        // 值相同,扩展合并范围
+                        mergeEnd = rowNum;
+                    } else {
+                        // 值不同,合并之前的区域
+                        if (mergeStart < mergeEnd) {
+                            mergeSingleColumnIfNotExists(sheet, col, mergeStart, mergeEnd, centerStyle, mergedRegions);
+                        }
+                        // 开始新的合并区域
+                        mergeStart = rowNum;
+                        mergeEnd = rowNum;
+                        lastValue = currentValue;
+                    }
+                }
+
+                // 合并最后一段区域
+                if (mergeStart < mergeEnd) {
+                    mergeSingleColumnIfNotExists(sheet, col, mergeStart, mergeEnd, centerStyle, mergedRegions);
+                }
+            }
+        }
+    }
+
+    // 合并名称列和对应的划分编码列(检查是否已存在)
+    private void mergeNameAndCodeColumns(Sheet sheet, int nameCol, int startRow, int endRow, CellStyle centerStyle, Set<String> mergedRegions) {
+        // 合并名称列
+        mergeSingleColumnIfNotExists(sheet, nameCol, startRow, endRow, centerStyle, mergedRegions);
+
+        // 合并对应的划分编码列
+        int codeCol = getCorrespondingCodeColumn(nameCol);
+        if (codeCol != -1) {
+            mergeSingleColumnIfNotExists(sheet, codeCol, startRow, endRow, centerStyle, mergedRegions);
+        }
+    }
+
+    // 合并单列并设置样式(检查是否已存在)
+    private void mergeSingleColumnIfNotExists(Sheet sheet, int col, int startRow, int endRow, CellStyle style, Set<String> mergedRegions) {
+        String regionKey = col + ":" + startRow + ":" + endRow;
+
+        if (!mergedRegions.contains(regionKey)) {
+            CellRangeAddress mergedRegion = new CellRangeAddress(startRow, endRow, col, col);
+
+            // 检查是否已经存在相同的合并区域
+            boolean alreadyExists = false;
+            for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
+                CellRangeAddress existingRegion = sheet.getMergedRegion(i);
+                if (existingRegion.getFirstColumn() == col &&
+                        existingRegion.getFirstRow() == startRow &&
+                        existingRegion.getLastRow() == endRow) {
+                    alreadyExists = true;
+                    break;
+                }
+            }
+
+            if (!alreadyExists) {
+                sheet.addMergedRegion(mergedRegion);
+                setMergedRegionStyle(sheet, mergedRegion, style);
+                mergedRegions.add(regionKey);
+            }
+        }
+    }
+
+    // 判断是否是名称列
+    private boolean isNameColumn(int columnIndex) {
+        int[] nameColumns = {1, 4, 7, 10, 13};
+        for (int nameCol : nameColumns) {
+            if (columnIndex == nameCol) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // 获取对应的划分编码列
+    private int getCorrespondingCodeColumn(int nameColumn) {
+        switch (nameColumn) {
+            case 1: return 0;  // 单位工程名称(1) -> 单位工程划分编码(0)
+            case 4: return 3;  // 分部工程名称(4) -> 分部工程划分编码(3)
+            case 7: return 6;  // 子分部工程名称(7) -> 子分部工程划分编码(6)
+            case 10: return 9; // 分项工程名称(10) -> 分项工程划分编码(9)
+            case 13: return 12; // 子分项工程名称(13) -> 子分项工程划分编码(12)
+            default: return -1;
+        }
+    }
+
+    // 设置合并区域样式为居中
+    private void setMergedRegionStyle(Sheet sheet, CellRangeAddress mergedRegion, CellStyle style) {
+        for (int rowNum = mergedRegion.getFirstRow(); rowNum <= mergedRegion.getLastRow(); rowNum++) {
+            Row row = sheet.getRow(rowNum);
+            if (row != null) {
+                for (int colNum = mergedRegion.getFirstColumn(); colNum <= mergedRegion.getLastColumn(); colNum++) {
+                    Cell cell = row.getCell(colNum);
+                    if (cell != null) {
+                        cell.setCellStyle(style);
+                    }
+                }
+            }
+        }
+    }
+
+    // 获取单元格的字符串值
+    private String getCellStringValue(Cell cell) {
+        if (cell == null) {
+            return "";
+        }
+
+        try {
+            return cell.getStringCellValue();
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+    // 自动调整列宽
+    private void autoSizeColumns(Sheet sheet) {
+        for (int i = 0; i < 15; i++) {
+            // 先手动计算列宽
+            int maxWidth = calculateColumnWidth(sheet, i);
+
+            // 设置列宽,至少3000(约30个字符宽度)
+            int columnWidth = Math.max(maxWidth + 1000, 3000); // 加一些边距
+            sheet.setColumnWidth(i, columnWidth);
+        }
+    }
+
+    // 手动计算列宽
+    private int calculateColumnWidth(Sheet sheet, int columnIndex) {
+        int maxWidth = 0;
+
+        for (int rowNum = 0; rowNum <= sheet.getLastRowNum(); rowNum++) {
+            Row row = sheet.getRow(rowNum);
+            if (row != null) {
+                Cell cell = row.getCell(columnIndex);
+                if (cell != null) {
+                    String cellValue = getCellStringValue(cell);
+                    if (cellValue != null && !cellValue.isEmpty()) {
+                        // 估算字符串宽度(中文字符算2个英文字符宽度)
+                        int width = estimateStringWidth(cellValue);
+                        maxWidth = Math.max(maxWidth, width);
+                    }
+                }
+            }
+        }
+
+        return maxWidth * 256; // POI中列宽的单位是1/256个字符宽度
+    }
+
+    // 估算字符串宽度
+    private int estimateStringWidth(String text) {
+        if (text == null || text.isEmpty()) {
+            return 0;
+        }
+
+        int width = 0;
+        for (char c : text.toCharArray()) {
+            // 中文字符或全角字符宽度为2,英文字符宽度为1
+            if (c >= 0x4E00 && c <= 0x9FA5) {
+                width += 2; // 中文字符
+            } else if (c > 0xFF00 && c < 0xFF5F) {
+                width += 2; // 全角字符
+            } else {
+                width += 1; // 英文字符
+            }
+        }
+
+        return width;
+    }
+
+     //保存Workbook到本地文件(本地测试放开)
+    private void saveWorkbookToFile(org.apache.poi.ss.usermodel.Workbook workbook, String filePath) {
+        try {
+            File file = new File(filePath);
+            File parentDir = file.getParentFile();
+            if (parentDir != null && !parentDir.exists()) {
+                parentDir.mkdirs();
+            }
+
+            try (FileOutputStream fileOut = new FileOutputStream(file)) {
+                workbook.write(fileOut);
+                System.out.println("文件已成功保存到: " + filePath);
+            }
+        } catch (IOException e) {
+            System.err.println("保存文件失败: " + e.getMessage());
+            e.printStackTrace();
+            throw new RuntimeException("保存文件失败", e);
+        }
+    }
+
+    // 根据ID和类型查找节点
+    private WbsTreeContract findNodeByIdAndType(List<WbsTreeContract> list, List<Long> ids, int nodeType) {
+        return list.stream()
+                .filter(item -> ids.contains(item.getPKeyId()) && item.getNodeType() == nodeType)
+                .findFirst()
+                .orElse(null);
+    }
+
+
+    // 从模板复制合并区域
+    private void copyMergedRegionsFromTemplate(Sheet targetSheet, Sheet templateSheet) {
+        for (int i = 0; i < templateSheet.getNumMergedRegions(); i++) {
+            CellRangeAddress mergedRegion = templateSheet.getMergedRegion(i);
+            // 只复制前两行的合并区域
+            if (mergedRegion.getFirstRow() <= 1) {
+                targetSheet.addMergedRegion(mergedRegion);
+            }
+        }
+    }
+
+
     // 抽取公共处理逻辑
     private void processApprovalData(APIWbsContractSubdivisionVo vo,
                                      List<WbsTreeContract> children,