|  | @@ -1,12 +1,5 @@
 | 
	
		
			
				|  |  |  package org.springblade.manager.controller;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -import com.alibaba.fastjson.JSONObject;
 | 
	
		
			
				|  |  | -import com.aspose.cells.LoadFormat;
 | 
	
		
			
				|  |  | -import com.aspose.cells.LoadOptions;
 | 
	
		
			
				|  |  | -import com.aspose.cells.SaveFormat;
 | 
	
		
			
				|  |  |  import com.aspose.cells.Workbook;
 | 
	
		
			
				|  |  | -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 | 
	
		
			
				|  |  | -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 | 
	
		
			
				|  |  |  import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 | 
	
		
			
				|  |  |  import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
 | 
	
		
			
				|  |  |  import com.google.common.collect.Lists;
 | 
	
	
		
			
				|  | @@ -20,10 +13,8 @@ import io.swagger.annotations.ApiOperation;
 | 
	
		
			
				|  |  |  import lombok.AllArgsConstructor;
 | 
	
		
			
				|  |  |  import lombok.SneakyThrows;
 | 
	
		
			
				|  |  |  import org.apache.commons.lang.StringUtils;
 | 
	
		
			
				|  |  | -import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 | 
	
		
			
				|  |  |  import org.apache.poi.ss.usermodel.*;
 | 
	
		
			
				|  |  |  import org.apache.poi.ss.util.CellRangeAddress;
 | 
	
		
			
				|  |  | -import org.apache.poi.ss.util.WorkbookUtil;
 | 
	
		
			
				|  |  |  import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 | 
	
		
			
				|  |  |  import org.jsoup.Jsoup;
 | 
	
		
			
				|  |  |  import org.jsoup.nodes.Document;
 | 
	
	
		
			
				|  | @@ -31,8 +22,6 @@ import org.jsoup.nodes.Element;
 | 
	
		
			
				|  |  |  import org.jsoup.select.Elements;
 | 
	
		
			
				|  |  |  import org.slf4j.Logger;
 | 
	
		
			
				|  |  |  import org.slf4j.LoggerFactory;
 | 
	
		
			
				|  |  | -import org.springblade.business.vo.NeiYeLedgerVO1;
 | 
	
		
			
				|  |  | -import org.springblade.common.utils.CommonUtil;
 | 
	
		
			
				|  |  |  import org.springblade.common.utils.SnowFlakeUtil;
 | 
	
		
			
				|  |  |  import org.springblade.core.boot.ctrl.BladeController;
 | 
	
		
			
				|  |  |  import org.springblade.core.log.exception.ServiceException;
 | 
	
	
		
			
				|  | @@ -47,21 +36,14 @@ import org.springblade.manager.dto.TableSortDTO;
 | 
	
		
			
				|  |  |  import org.springblade.manager.dto.WbsTreeContractDTO2;
 | 
	
		
			
				|  |  |  import org.springblade.manager.entity.*;
 | 
	
		
			
				|  |  |  import org.springblade.manager.feign.ContractClient;
 | 
	
		
			
				|  |  | -import org.springblade.manager.service.INodeBaseInfoService;
 | 
	
		
			
				|  |  |  import org.springblade.manager.service.IWbsParamService;
 | 
	
		
			
				|  |  |  import org.springblade.manager.service.IWbsTreeContractService;
 | 
	
		
			
				|  |  | -import org.springblade.manager.service.IWbsTreePrivateService;
 | 
	
		
			
				|  |  | -import org.springblade.manager.service.impl.NodeBaseInfoServiceImpl;
 | 
	
		
			
				|  |  |  import org.springblade.manager.service.impl.WbsTreeContractServiceImpl;
 | 
	
		
			
				|  |  |  import org.springblade.manager.utils.FileUtils;
 | 
	
		
			
				|  |  |  import org.springblade.manager.utils.RandomNumberHolder;
 | 
	
		
			
				|  |  |  import org.springblade.manager.vo.*;
 | 
	
		
			
				|  |  | -import org.springframework.beans.factory.annotation.Autowired;
 | 
	
		
			
				|  |  | -import org.springframework.core.io.ByteArrayResource;
 | 
	
		
			
				|  |  |  import org.springframework.core.io.Resource;
 | 
	
		
			
				|  |  |  import org.springframework.dao.DataAccessException;
 | 
	
		
			
				|  |  | -import org.springframework.http.HttpHeaders;
 | 
	
		
			
				|  |  | -import org.springframework.http.MediaType;
 | 
	
		
			
				|  |  |  import org.springframework.http.ResponseEntity;
 | 
	
		
			
				|  |  |  import org.springframework.jdbc.core.BeanPropertyRowMapper;
 | 
	
		
			
				|  |  |  import org.springframework.jdbc.core.JdbcTemplate;
 | 
	
	
		
			
				|  | @@ -84,7 +66,20 @@ import java.util.*;
 | 
	
		
			
				|  |  |  import java.util.regex.Matcher;
 | 
	
		
			
				|  |  |  import java.util.regex.Pattern;
 | 
	
		
			
				|  |  |  import java.util.stream.Collectors;
 | 
	
		
			
				|  |  | -import org.springframework.http.ContentDisposition;
 | 
	
		
			
				|  |  | +import org.apache.poi.xssf.streaming.SXSSFWorkbook;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import java.io.ByteArrayOutputStream;
 | 
	
		
			
				|  |  | +import java.io.InputStream;
 | 
	
		
			
				|  |  | +import java.util.List;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import static com.aspose.cells.HtmlLinkTargetType.BLANK;
 | 
	
		
			
				|  |  | +import static com.aspose.cells.LoadDataFilterOptions.FORMULA;
 | 
	
		
			
				|  |  | +import static com.aspose.cells.PropertyType.BOOLEAN;
 | 
	
		
			
				|  |  | +import static com.aspose.cells.PropertyType.STRING;
 | 
	
		
			
				|  |  | +import static java.sql.Types.NUMERIC;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  @RestController
 | 
	
		
			
				|  |  |  @AllArgsConstructor
 | 
	
	
		
			
				|  | @@ -429,8 +424,187 @@ public class WbsTreeContractController extends BladeController {
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +    @SneakyThrows
 | 
	
		
			
				|  |  | +    @GetMapping("/download-node-excel")
 | 
	
		
			
				|  |  | +    @ApiOperationSupport(order = 14)
 | 
	
		
			
				|  |  | +    @ApiOperation(value = "客户端-下载节点下所有表单为多sheet excel", notes = "传入节点ID和分类")
 | 
	
		
			
				|  |  | +    public void downloadNodeExcel(@RequestParam Long nodeId, @RequestParam Integer classify, HttpServletResponse response) {
 | 
	
		
			
				|  |  | +        // 构建Excel文件名
 | 
	
		
			
				|  |  | +        StringBuilder excelName = new StringBuilder();
 | 
	
		
			
				|  |  | +        // 获取节点下所有表单
 | 
	
		
			
				|  |  | +        List<WbsTreeContract> formList = wbsTreeContractServiceImpl.selectAllPkeyIdByNodeId(nodeId, classify);
 | 
	
		
			
				|  |  | +        if (ObjectUtil.isEmpty(formList)) {
 | 
	
		
			
				|  |  | +            throw new ServiceException("该节点下没有找到对应的表单数据");
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 获取节点及祖先节点信息用于构建文件名
 | 
	
		
			
				|  |  | +        WbsTreeContract node = wbsTreeContractServiceImpl.getById(nodeId);
 | 
	
		
			
				|  |  | +        List<WbsTreeContract> ancestorsList = wbsTreeContractServiceImpl.getAncestorsList(node.getAncestorsPId());
 | 
	
		
			
				|  |  | +        for (WbsTreeContract ancestor : ancestorsList) {
 | 
	
		
			
				|  |  | +            if (2 == ancestor.getNodeType()) {
 | 
	
		
			
				|  |  | +                excelName.append(ancestor.getNodeName());
 | 
	
		
			
				|  |  | +            } else if (4 == ancestor.getNodeType()) {
 | 
	
		
			
				|  |  | +                excelName.append("-" + ancestor.getNodeName());
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        excelName.append("-" + node.getNodeName());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 创建主工作簿(用于合并多sheet)
 | 
	
		
			
				|  |  | +        XSSFWorkbook mainWorkbook = new XSSFWorkbook();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +            // 遍历所有表单,生成对应的sheet
 | 
	
		
			
				|  |  | +            for (WbsTreeContract form : formList) {
 | 
	
		
			
				|  |  | +                String pKeyId = form.getPKeyId()+"";
 | 
	
		
			
				|  |  | +                String sheetName = form.getNodeName();
 | 
	
		
			
				|  |  | +                // 处理sheet名称中的特殊字符(Excel不允许的字符)
 | 
	
		
			
				|  |  | +                sheetName = sheetName.replaceAll("[\\\\/:*?\"<>|]", "_");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                // 1. 获取当前表单的htmlUrl(复用downloadExcel的逻辑)
 | 
	
		
			
				|  |  | +                String htmlUrl = "";
 | 
	
		
			
				|  |  | +                WbsTreeContract contractTab = iWbsTreeContractService.getBaseMapper()
 | 
	
		
			
				|  |  | +                        .selectOne(Wrappers.<WbsTreeContract>lambdaQuery().eq(WbsTreeContract::getPKeyId, pKeyId));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                if (ObjectUtil.isEmpty(contractTab)) {
 | 
	
		
			
				|  |  | +                    // 尝试从试验表获取
 | 
	
		
			
				|  |  | +                    WbsTreePrivate privateTab = jdbcTemplate.query(
 | 
	
		
			
				|  |  | +                                    "select * from m_wbs_tree_private where p_key_id = ?",
 | 
	
		
			
				|  |  | +                                    new BeanPropertyRowMapper<>(WbsTreePrivate.class),
 | 
	
		
			
				|  |  | +                                    pKeyId)
 | 
	
		
			
				|  |  | +                            .stream().findAny().orElse(null);
 | 
	
		
			
				|  |  | +                    if (privateTab != null && privateTab.getHtmlUrl() != null) {
 | 
	
		
			
				|  |  | +                        htmlUrl = privateTab.getHtmlUrl();
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                } else {
 | 
	
		
			
				|  |  | +                    htmlUrl = contractTab.getHtmlUrl();
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                // 跳过无html信息的表单
 | 
	
		
			
				|  |  | +                if (ObjectUtil.isEmpty(htmlUrl)) {
 | 
	
		
			
				|  |  | +                    logger.warn("表单pKeyId:{} 未获取到html信息,已跳过", pKeyId);
 | 
	
		
			
				|  |  | +                    continue;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                // 2. 转换html为单个sheet的工作簿
 | 
	
		
			
				|  |  | +                InputStream htmlStream = FileUtils.getInputStreamByUrl(htmlUrl);
 | 
	
		
			
				|  |  | +                String htmlContent = IoUtil.readToString(htmlStream);
 | 
	
		
			
				|  |  | +                org.apache.poi.ss.usermodel.Workbook singleSheetWorkbook = HtmlTableToExcelConverter.convertHtmlTableToExcel(htmlContent);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +                // 3. 将单个sheet复制到主工作簿
 | 
	
		
			
				|  |  | +                if (singleSheetWorkbook.getNumberOfSheets() > 0) {
 | 
	
		
			
				|  |  | +                    Sheet sourceSheet = singleSheetWorkbook.getSheetAt(0);
 | 
	
		
			
				|  |  | +                    Sheet targetSheet = mainWorkbook.createSheet(sheetName);
 | 
	
		
			
				|  |  | +                    copySheetContent(mainWorkbook, sourceSheet, targetSheet);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +                // 关闭临时工作簿释放资源
 | 
	
		
			
				|  |  | +                singleSheetWorkbook.close();
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            // 4. 输出主工作簿到响应
 | 
	
		
			
				|  |  | +            if (mainWorkbook.getNumberOfSheets() == 0) {
 | 
	
		
			
				|  |  | +                throw new ServiceException("所有表单均无法生成有效Excel内容");
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            // 设置响应头
 | 
	
		
			
				|  |  | +            String encodedFileName = URLEncoder.encode(excelName + ".xlsx", "UTF-8")
 | 
	
		
			
				|  |  | +                    .replaceAll("\\+", " ")
 | 
	
		
			
				|  |  | +                    .replaceAll("%2B", "+");
 | 
	
		
			
				|  |  | +            response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName);
 | 
	
		
			
				|  |  | +            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            // 写入输出流
 | 
	
		
			
				|  |  | +            try (ServletOutputStream outputStream = response.getOutputStream()) {
 | 
	
		
			
				|  |  | +                mainWorkbook.write(outputStream);
 | 
	
		
			
				|  |  | +                outputStream.flush(); // 强制刷出所有数据
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        } catch (Exception e) {
 | 
	
		
			
				|  |  | +            logger.error("下载节点多sheet Excel异常", e);
 | 
	
		
			
				|  |  | +            throw new ServiceException("下载失败:" + e.getMessage());
 | 
	
		
			
				|  |  | +        } finally {
 | 
	
		
			
				|  |  | +            if (mainWorkbook != null) {
 | 
	
		
			
				|  |  | +                mainWorkbook.close();
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 复制sheet内容(包括样式、合并区域、行高列宽)
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private void copySheetContent(XSSFWorkbook targetWorkbook, Sheet sourceSheet, Sheet targetSheet) {
 | 
	
		
			
				|  |  | +        // 复制合并区域
 | 
	
		
			
				|  |  | +        for (int i = 0; i < sourceSheet.getNumMergedRegions(); i++) {
 | 
	
		
			
				|  |  | +            CellRangeAddress mergedRegion = sourceSheet.getMergedRegion(i);
 | 
	
		
			
				|  |  | +            targetSheet.addMergedRegion(mergedRegion);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 复制列宽
 | 
	
		
			
				|  |  | +        for (int col = 0; col <= sourceSheet.getLastRowNum(); col++) {
 | 
	
		
			
				|  |  | +            targetSheet.setColumnWidth(col, sourceSheet.getColumnWidth(col));
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 复制行数据及样式
 | 
	
		
			
				|  |  | +        for (int rowIdx = 0; rowIdx <= sourceSheet.getLastRowNum(); rowIdx++) {
 | 
	
		
			
				|  |  | +            Row sourceRow = sourceSheet.getRow(rowIdx);
 | 
	
		
			
				|  |  | +            if (sourceRow == null) continue;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            Row targetRow = targetSheet.createRow(rowIdx);
 | 
	
		
			
				|  |  | +            targetRow.setHeight(sourceRow.getHeight());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            // 复制单元格
 | 
	
		
			
				|  |  | +            for (int cellIdx = 0; cellIdx < sourceRow.getLastCellNum(); cellIdx++) {
 | 
	
		
			
				|  |  | +                Cell sourceCell = sourceRow.getCell(cellIdx);
 | 
	
		
			
				|  |  | +                if (sourceCell == null) continue;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                Cell targetCell = targetRow.createCell(cellIdx);
 | 
	
		
			
				|  |  | +                copyCellContent(targetWorkbook, sourceCell, targetCell);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private void copyCellContent(XSSFWorkbook targetWorkbook, Cell sourceCell, Cell targetCell) {
 | 
	
		
			
				|  |  | +        // 复制样式
 | 
	
		
			
				|  |  | +        CellStyle targetStyle = targetWorkbook.createCellStyle();
 | 
	
		
			
				|  |  | +        targetStyle.cloneStyleFrom(sourceCell.getCellStyle());
 | 
	
		
			
				|  |  | +        targetCell.setCellStyle(targetStyle);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 复制单元格值(完善类型处理)
 | 
	
		
			
				|  |  | +        CellType cellType = CellType.forInt(sourceCell.getCellType());
 | 
	
		
			
				|  |  | +        // 公式单元格需要先获取计算后的值
 | 
	
		
			
				|  |  | +        if (cellType == CellType.FORMULA) {
 | 
	
		
			
				|  |  | +            cellType = CellType.forInt(sourceCell.getCachedFormulaResultType());
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        switch (cellType) {
 | 
	
		
			
				|  |  | +            case STRING:
 | 
	
		
			
				|  |  | +                targetCell.setCellValue(sourceCell.getStringCellValue());
 | 
	
		
			
				|  |  | +                break;
 | 
	
		
			
				|  |  | +            case NUMERIC:
 | 
	
		
			
				|  |  | +                if (DateUtil.isCellDateFormatted(sourceCell)) {
 | 
	
		
			
				|  |  | +                    targetCell.setCellValue(sourceCell.getDateCellValue());
 | 
	
		
			
				|  |  | +                } else {
 | 
	
		
			
				|  |  | +                    targetCell.setCellValue(sourceCell.getNumericCellValue());
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                break;
 | 
	
		
			
				|  |  | +            case BOOLEAN:
 | 
	
		
			
				|  |  | +                targetCell.setCellValue(sourceCell.getBooleanCellValue());
 | 
	
		
			
				|  |  | +                break;
 | 
	
		
			
				|  |  | +            case FORMULA:
 | 
	
		
			
				|  |  | +                // 公式单元格同时复制公式和缓存结果
 | 
	
		
			
				|  |  | +                targetCell.setCellFormula(sourceCell.getCellFormula());
 | 
	
		
			
				|  |  | +                targetCell.setCellValue(sourceCell.getNumericCellValue()); // 补充缓存值
 | 
	
		
			
				|  |  | +                break;
 | 
	
		
			
				|  |  | +            case BLANK:
 | 
	
		
			
				|  |  | +                break;
 | 
	
		
			
				|  |  | +            case ERROR:
 | 
	
		
			
				|  |  | +                targetCell.setCellErrorValue(sourceCell.getErrorCellValue());
 | 
	
		
			
				|  |  | +                break;
 | 
	
		
			
				|  |  | +            default:
 | 
	
		
			
				|  |  | +                // 兜底处理,避免遗漏
 | 
	
		
			
				|  |  | +                targetCell.setCellValue(sourceCell.toString());
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      // 计算最大列数以对齐所有行
 |