|
|
@@ -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,25 +36,23 @@ 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.ExcelTabServiceImpl;
|
|
|
import org.springblade.manager.service.impl.NodeBaseInfoServiceImpl;
|
|
|
import org.springblade.manager.service.impl.WbsTreeContractServiceImpl;
|
|
|
+import org.springblade.manager.util.DataStructureFormatUtils;
|
|
|
+import org.springblade.manager.utils.DuplicateSheetRecognizer;
|
|
|
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;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
@@ -85,7 +72,21 @@ 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;
|
|
|
+import static java.util.stream.Collectors.toMap;
|
|
|
+
|
|
|
|
|
|
@RestController
|
|
|
@AllArgsConstructor
|
|
|
@@ -431,8 +432,308 @@ public class WbsTreeContractController extends BladeController {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ // 1. 工具方法:统一主表名匹配格式(保留原逻辑,确保无字符差异)
|
|
|
+ private String getMatchableMainName(String name) {
|
|
|
+ if (name == null) return "";
|
|
|
+ String cleanName = name.replaceAll("\\s+", ""); // 去空格
|
|
|
+ cleanName = cleanName.replaceAll("[\\\\/:*?\"<>|_-]", ""); // 去特殊字符
|
|
|
+ return cleanName.toLowerCase(); // 转小写
|
|
|
+ }
|
|
|
+ @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("该节点下没有找到对应的表单数据");
|
|
|
+ }
|
|
|
+ // 2. 第一步:批量收集所有主表,创建分组(按主表在formList中的原始顺序)
|
|
|
+ Map<String, List<WbsTreeContract>> mainCopyGroupMap = new LinkedHashMap<>(); // 保持主表原始顺序
|
|
|
+ List<WbsTreeContract> copyTableList = new ArrayList<>(); // 临时存储所有复制表
|
|
|
+
|
|
|
+ // 2.1 第一次遍历:分离主表和复制表
|
|
|
+ for (WbsTreeContract contract : formList) {
|
|
|
+ String nodeName = contract.getNodeName();
|
|
|
+ boolean isCopy = nodeName.contains("__");
|
|
|
+
|
|
|
+ if (isCopy) {
|
|
|
+ // 复制表:先暂存,不立即匹配
|
|
|
+ copyTableList.add(contract);
|
|
|
+ } else {
|
|
|
+ // 主表:创建分组(按原始顺序插入)
|
|
|
+ String matchableMainName = getMatchableMainName(nodeName);
|
|
|
+ if (!mainCopyGroupMap.containsKey(matchableMainName)) {
|
|
|
+ List<WbsTreeContract> groupList = new ArrayList<>();
|
|
|
+ groupList.add(contract); // 主表放在分组第一位
|
|
|
+ mainCopyGroupMap.put(matchableMainName, groupList);
|
|
|
+ } else {
|
|
|
+ // 重复主表:仍放在分组第一位,保证主表优先
|
|
|
+ mainCopyGroupMap.get(matchableMainName).add(0, contract);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
+ // 3. 第二步:批量处理所有复制表,匹配主表分组
|
|
|
+ List<WbsTreeContract> orphanCopyList = new ArrayList<>();
|
|
|
+ for (WbsTreeContract copyContract : copyTableList) {
|
|
|
+ String nodeName = copyContract.getNodeName();
|
|
|
+ String[] parts = nodeName.split("__", 2);
|
|
|
+ if (parts.length < 2) {
|
|
|
+ orphanCopyList.add(copyContract);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 复制表拆分+统一格式,匹配主表分组
|
|
|
+ String rawMainName = parts[0];
|
|
|
+ String matchableMainName = getMatchableMainName(rawMainName);
|
|
|
+ boolean foundMatch = false;
|
|
|
+
|
|
|
+ // 遍历主表分组,找匹配的分组
|
|
|
+ for (Map.Entry<String, List<WbsTreeContract>> groupEntry : mainCopyGroupMap.entrySet()) {
|
|
|
+ if (groupEntry.getKey().equals(matchableMainName)) {
|
|
|
+ groupEntry.getValue().add(copyContract); // 加入对应主表分组
|
|
|
+ foundMatch = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
+ if (!foundMatch) {
|
|
|
+ orphanCopyList.add(copyContract);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 第三步:处理每个主表分组,对复制表排序(保持主表原始顺序)
|
|
|
+ List<WbsTreeContract> sortedList = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, List<WbsTreeContract>> entry : mainCopyGroupMap.entrySet()) {
|
|
|
+ List<WbsTreeContract> groupList = entry.getValue();
|
|
|
+ if (groupList.size() <= 1) {
|
|
|
+ // 只有主表,直接加入
|
|
|
+ sortedList.addAll(groupList);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 拆分主表(第一位)和复制表(剩余)
|
|
|
+ WbsTreeContract mainContract = groupList.get(0);
|
|
|
+ List<WbsTreeContract> copyList = groupList.subList(1, groupList.size());
|
|
|
+
|
|
|
+ // 复制表按数字排序
|
|
|
+ copyList.sort((c1, c2) -> {
|
|
|
+ String numStr1 = c1.getNodeName().split("__", 2)[1];
|
|
|
+ String numStr2 = c2.getNodeName().split("__", 2)[1];
|
|
|
+ try {
|
|
|
+ return Integer.compare(Integer.parseInt(numStr1), Integer.parseInt(numStr2));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return numStr1.compareTo(numStr2);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 主表 + 排序后的复制表,加入最终列表
|
|
|
+ sortedList.add(mainContract);
|
|
|
+ sortedList.addAll(copyList);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 第四步:加入无对应主表的复制表(排在最后)
|
|
|
+ sortedList.addAll(orphanCopyList);
|
|
|
+
|
|
|
+ // 6. 替换原列表
|
|
|
+ formList = sortedList;
|
|
|
+
|
|
|
+ // 获取节点及祖先节点信息用于构建文件名
|
|
|
+ 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 originalFileName = excelName + ".xlsx";
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 先编码所有字符
|
|
|
+ String fullyEncoded = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name());
|
|
|
+
|
|
|
+ // 2. 解码我们想要保留的特殊字符
|
|
|
+ String partiallyDecoded = fullyEncoded
|
|
|
+ .replaceAll("\\+", "%20") // 空格保持编码为%20
|
|
|
+ .replaceAll("%2B", "+") // 解码+号
|
|
|
+ .replaceAll("%2F", "/") // 解码/号
|
|
|
+ .replaceAll("%23", "#") // 解码#号
|
|
|
+ .replaceAll("%7E", "~") // 解码~号
|
|
|
+ // - 号不需要处理,URL编码不会编码-
|
|
|
+ ;
|
|
|
+
|
|
|
+ // 3. 设置响应头
|
|
|
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
|
+ response.setHeader("Content-Disposition",
|
|
|
+ "attachment; filename=\"" + partiallyDecoded + "\"; ");
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 备用方案:简单清理
|
|
|
+ String safeFileName = originalFileName
|
|
|
+ .replaceAll("[\\\\:*?\"<>|]", "_")
|
|
|
+ .trim();
|
|
|
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
|
+ response.setHeader("Content-Disposition",
|
|
|
+ "attachment; filename=\"" + safeFileName + "\"");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 写入输出流
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
|
|
|
// 计算最大列数以对齐所有行
|
|
|
@@ -510,7 +811,385 @@ public class WbsTreeContractController extends BladeController {
|
|
|
cellStyle.setBorderRight(BorderStyle.THIN);
|
|
|
cellStyle.setRightBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
}
|
|
|
+ @PostMapping("/import-node-excel")
|
|
|
+ @ApiOperationSupport(order = 13)
|
|
|
+ @ApiOperation(value = "客户端-导入多sheet excel到对应节点下的表单", notes = "传入节点ID、分类和多sheet excel文件")
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public R importNodeExcel(
|
|
|
+ @RequestPart MultipartFile file,
|
|
|
+ @RequestParam Long nodeId,
|
|
|
+ @RequestParam Integer classify) throws Exception {
|
|
|
+
|
|
|
+ // 1. 获取节点下所有表单,并建立 sheet名 -> WbsTreeContract 的映射(处理特殊字符)
|
|
|
+ List<WbsTreeContract> wbsTreeContracts = wbsTreeContractServiceImpl.selectAllPkeyIdByNodeId(nodeId, classify);
|
|
|
+ if (wbsTreeContracts.isEmpty()) {
|
|
|
+ return R.fail("该节点下没有找到对应的表单数据");
|
|
|
+ }
|
|
|
+ // 2. 加载上传的多sheet Excel文件
|
|
|
+ com.spire.xls.Workbook mainWorkbook = new com.spire.xls.Workbook();
|
|
|
+ try {
|
|
|
+ mainWorkbook.loadFromStream(file.getInputStream());
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("加载Excel文件失败", e);
|
|
|
+ return R.fail("Excel文件解析失败:" + e.getMessage());
|
|
|
+ }
|
|
|
|
|
|
+ // 3. 遍历所有sheet,逐个处理
|
|
|
+ int sheetCount = mainWorkbook.getWorksheets().getCount();
|
|
|
+ // 处理表单名称(与下载时的sheet名处理逻辑一致,确保匹配)
|
|
|
+ Map<String, WbsTreeContract> nodeNameToContractMap = new HashMap<>();
|
|
|
+ for (WbsTreeContract contract : wbsTreeContracts) {
|
|
|
+ String processedNodeName = contract.getNodeName().replaceAll("[\\\\/:*?\"<>|]", "_").trim();
|
|
|
+ nodeNameToContractMap.put(processedNodeName, contract);
|
|
|
+ }
|
|
|
+ if(wbsTreeContracts.size()<sheetCount){
|
|
|
+ for (int i = 0; i < sheetCount; i++) {
|
|
|
+ com.spire.xls.Worksheet sheet = mainWorkbook.getWorksheets().get(i);
|
|
|
+ String sheetName = sheet.getName();
|
|
|
+ String processedSheetName = sheetName.replaceAll("[\\\\/:*?\"<>|]", "_").trim(); // 处理sheet名特殊字符
|
|
|
+ // 1. 识别当前sheet是否为复制表
|
|
|
+ DuplicateSheetRecognizer.DuplicateSheetResult result =
|
|
|
+ DuplicateSheetRecognizer.recognize(processedSheetName);
|
|
|
+ if (result.isDuplicate()) {
|
|
|
+ String originalName = result.getOriginalName();
|
|
|
+ if(nodeNameToContractMap.containsKey(originalName)){
|
|
|
+ WbsTreeContract contract = nodeNameToContractMap.get(originalName);
|
|
|
+ R r = excelTabController.copeBussTab(contract.getPKeyId());
|
|
|
+ if(r.isSuccess()){
|
|
|
+ WbsTreeContract data = (WbsTreeContract) r.getData();
|
|
|
+ nodeNameToContractMap.put(data.getNodeName(), data);
|
|
|
+ sheet.setName(data.getNodeName());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (int i = 0; i < sheetCount; i++) {
|
|
|
+ com.spire.xls.Worksheet sheet = mainWorkbook.getWorksheets().get(i);
|
|
|
+ String sheetName = sheet.getName();
|
|
|
+ String processedSheetName = sheetName.replaceAll("[\\\\/:*?\"<>|]", "_").trim(); // 处理sheet名特殊字符
|
|
|
+
|
|
|
+ // 匹配对应的表单
|
|
|
+ WbsTreeContract matchedContract = nodeNameToContractMap.get(processedSheetName);
|
|
|
+ if (matchedContract == null) {
|
|
|
+ logger.warn("sheet名[{}]未匹配到任何表单,已跳过", sheetName);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取当前表单的pkeyId
|
|
|
+ String pkeyId = matchedContract.getPKeyId() + "";
|
|
|
+ logger.info("开始处理sheet[{}],对应表单pkeyId[{}]", sheetName, pkeyId);
|
|
|
+
|
|
|
+ // 4. 将当前sheet保存为临时Excel文件(模拟单个文件上传)
|
|
|
+ File tempFile = null;
|
|
|
+ InputStream tempInputStream = null;
|
|
|
+ try {
|
|
|
+ // 创建临时文件
|
|
|
+ tempFile = File.createTempFile("sheet_", ".xlsx");
|
|
|
+ // 创建仅包含当前sheet的新工作簿
|
|
|
+ com.spire.xls.Workbook singleSheetWorkbook = new com.spire.xls.Workbook();
|
|
|
+ singleSheetWorkbook.getWorksheets().clear();
|
|
|
+ singleSheetWorkbook.getWorksheets().addCopy(sheet); // 复制当前sheet到新工作簿
|
|
|
+ singleSheetWorkbook.saveToFile(tempFile.getAbsolutePath(), com.spire.xls.FileFormat.Version2016);
|
|
|
+ singleSheetWorkbook.dispose();
|
|
|
+
|
|
|
+ // 读取临时文件作为输入流,调用单表单导入逻辑
|
|
|
+ tempInputStream = new FileInputStream(tempFile);
|
|
|
+ Map<String, Object> sheetResult = processSingleSheetImport(pkeyId, tempInputStream);
|
|
|
+ if(sheetResult.isEmpty()){
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Map<String, String> dataMap = getDataMap(sheetResult);
|
|
|
+ for (Map.Entry<String, String> entry : dataMap.entrySet()) {
|
|
|
+ String value = entry.getValue();
|
|
|
+ if (value != null) {
|
|
|
+ value = value.replace("'", "''");
|
|
|
+ entry.setValue(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ String delSql = "delete from " + matchedContract.getInitTableName() + " where p_key_id=" + matchedContract.getPKeyId();
|
|
|
+ dataMap.put("p_key_id", matchedContract.getPKeyId()+"");
|
|
|
+ String sqlInfo = buildMTableInsertSql(matchedContract.getInitTableName(), dataMap, SnowFlakeUtil.getId(), null, null).toString();
|
|
|
+ jdbcTemplate.execute(delSql);
|
|
|
+ jdbcTemplate.execute(sqlInfo);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ throw new ServiceException("处理sheet[" + sheetName + "]失败:" + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ // 关闭流并删除临时文件
|
|
|
+ if (tempInputStream != null) {
|
|
|
+ tempInputStream.close();
|
|
|
+ }
|
|
|
+ if (tempFile != null && !tempFile.delete()) {
|
|
|
+ logger.warn("临时文件[{}]删除失败", tempFile.getAbsolutePath());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ mainWorkbook.dispose();
|
|
|
+ return R.success("导入成功");
|
|
|
+ }
|
|
|
+
|
|
|
+ public StringBuilder buildMTableInsertSql(String tabName, Map<String, String> dataMap2, Object id, Object groupId, Object pKeyId) {
|
|
|
+ if (dataMap2 == null || dataMap2.isEmpty() || tabName == null || tabName.isEmpty()) {
|
|
|
+ return new StringBuilder();
|
|
|
+ }
|
|
|
+ //拼接SQL
|
|
|
+ StringBuilder sql = new StringBuilder("INSERT INTO " + tabName),
|
|
|
+ keySql = new StringBuilder(),
|
|
|
+ valSql = new StringBuilder();
|
|
|
+ if (id == null) {
|
|
|
+ if (dataMap2.containsKey("id")) {
|
|
|
+ id = dataMap2.get("id");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ keySql.append("id");
|
|
|
+ valSql.append(id == null ? SnowFlakeUtil.getId() : id);
|
|
|
+ if (groupId == null) {
|
|
|
+ groupId = dataMap2.get("group_id");
|
|
|
+ }
|
|
|
+ if (groupId != null) {
|
|
|
+ keySql.append(", group_id");
|
|
|
+ valSql.append(", ").append(groupId);
|
|
|
+ }
|
|
|
+ if (pKeyId == null) {
|
|
|
+ pKeyId = dataMap2.get("p_key_id");
|
|
|
+ }
|
|
|
+ if (pKeyId != null) {
|
|
|
+ keySql.append(", p_key_id");
|
|
|
+ valSql.append(", ").append(pKeyId);
|
|
|
+ }
|
|
|
+ //参数
|
|
|
+ Map<String, String> opsParamMap = new HashMap<>();
|
|
|
+ dataMap2.remove("id");
|
|
|
+ dataMap2.remove("group_id");
|
|
|
+ dataMap2.remove("p_key_id");
|
|
|
+ String key201 = dataMap2.remove("key_201");
|
|
|
+ String fields = dataMap2.keySet().stream().map(key -> "'" + key + "'").collect(Collectors.joining(","));
|
|
|
+ Map<String, Integer> map = new HashMap<>();
|
|
|
+ if (!fields.isEmpty()) {
|
|
|
+ try {
|
|
|
+ fields = fields + ", 'key_201'";
|
|
|
+ List<Map<String, Object>> fieldMap = jdbcTemplate.queryForList("select distinct COLUMN_NAME as fieldName, CHARACTER_MAXIMUM_LENGTH as fieldLength from information_schema.COLUMNS where TABLE_NAME = '" + tabName +
|
|
|
+ "' and COLUMN_NAME in (" + fields + ")");
|
|
|
+ map = fieldMap.stream().collect(toMap(k -> k.get("fieldName") + "", v -> {
|
|
|
+ try {
|
|
|
+ return Integer.parseInt(v.get("fieldLength") + "");
|
|
|
+ } catch (Exception e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }, Math::min));
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (key201 != null) {
|
|
|
+ Map<String, String> map1 = DataStructureFormatUtils.parseDataByKey(key201);
|
|
|
+ if (!map1.isEmpty()) {
|
|
|
+ opsParamMap.putAll(map1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (String key : dataMap2.keySet()) {
|
|
|
+ String[] split = key.split("_");
|
|
|
+ if (split.length > 1 && Integer.parseInt(split[1]) > 80) {
|
|
|
+ // 大于80则保留在扩展字段中
|
|
|
+ opsParamMap.put(key, dataMap2.get(key));
|
|
|
+ } else {
|
|
|
+ String value = dataMap2.get(key);
|
|
|
+ if (value != null) {
|
|
|
+ Integer i = map.get(key);
|
|
|
+ // 长度超过数据库长度也保留在扩展字段中
|
|
|
+ if (i != null && value.length() > i) {
|
|
|
+ opsParamMap.put(key, dataMap2.get(key));
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ keySql.append(", ").append(key);
|
|
|
+ valSql.append(", '").append(value).append("'");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!opsParamMap.isEmpty()) {
|
|
|
+ keySql.append(", key_201");
|
|
|
+ String data = DataStructureFormatUtils.buildData(opsParamMap);
|
|
|
+ try {
|
|
|
+ if (!map.containsKey( "key_201")) {
|
|
|
+ jdbcTemplate.execute("alter table " + tabName + " add column key_201 text");
|
|
|
+ } else {
|
|
|
+ Integer i = map.get("key_201");
|
|
|
+ if (data.length() > i) {
|
|
|
+ if (i < 10000) {
|
|
|
+ // 65535 byte
|
|
|
+ jdbcTemplate.execute("alter table " + tabName + " modify column key_201 text");
|
|
|
+ }else {
|
|
|
+ // 16777215 byte
|
|
|
+ jdbcTemplate.execute("alter table " + tabName + " modify column key_201 mediumtext");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ valSql.append(", '").append(data).append("'");
|
|
|
+ }
|
|
|
+ sql.append("(").append(keySql).append(")").append(" values(").append(valSql).append(")");
|
|
|
+ return sql;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String,String> getDataMap(Map<String, Object> originalMap){
|
|
|
+ // 用于存储合并后的结果
|
|
|
+ Map<String, String> mergedMap = new HashMap<>();
|
|
|
+
|
|
|
+ // 正则表达式:匹配"key_前缀__剩余坐标"的格式
|
|
|
+ Pattern pattern = Pattern.compile("key_(\\w+)__([\\w_]+)");
|
|
|
+
|
|
|
+ for (Map.Entry<String, Object> entry : originalMap.entrySet()) {
|
|
|
+ String key = entry.getKey();
|
|
|
+ String value = entry.getValue()+"";
|
|
|
+ Matcher matcher = pattern.matcher(key);
|
|
|
+
|
|
|
+ if (matcher.matches()) {
|
|
|
+ String prefix = matcher.group(1); // 提取__前面的前缀(如7、17)
|
|
|
+ String suffix = matcher.group(2); // 提取__后面的剩余坐标(如11_9、12_5)
|
|
|
+
|
|
|
+ // 构建合并后的键(简化为prefix,如7、17)
|
|
|
+ String mergedKey = "key_" + prefix;
|
|
|
+ // 构建合并后的值(值_^_剩余坐标)
|
|
|
+ String mergedValue = value + "_^_" + suffix;
|
|
|
+
|
|
|
+ // 若该键已存在,用☆拼接新值;否则直接存入
|
|
|
+ if (mergedMap.containsKey(mergedKey)) {
|
|
|
+ mergedMap.put(mergedKey, mergedMap.get(mergedKey) + "☆" + mergedValue);
|
|
|
+ } else {
|
|
|
+ mergedMap.put(mergedKey, mergedValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return mergedMap;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 复用importExcel的核心逻辑,处理单个sheet的导入
|
|
|
+ * 抽取自原importExcel方法,参数改为pkeyId和输入流
|
|
|
+ */
|
|
|
+ private Map<String, Object> processSingleSheetImport(String pkeyId, InputStream inputStream) throws Exception {
|
|
|
+ // 获取当前表htmlString(模板)
|
|
|
+ String htmlString_1 = wbsTreeContractServiceImpl.getHtmlString(pkeyId);
|
|
|
+ if (StringUtils.isEmpty(htmlString_1)) {
|
|
|
+ throw new ServiceException("获取表单[" + pkeyId + "]的html模板失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 结果集
|
|
|
+ Map<String, Object> resultMap = new HashMap<>();
|
|
|
+
|
|
|
+ // 日期格式正则(复用原逻辑)
|
|
|
+ String doubleSlashRegex_XG = ".*\\/[^\\/]*\\/.*";
|
|
|
+ String dateFormatRegex_yyyyMdd = "\\d{4}/\\d{1,2}/\\d{1,2}";
|
|
|
+ String dateFormatRegex_yyyyMMdd = "\\d{4}/\\d{2}/\\d{2}";
|
|
|
+ String dateFormatRegex_chinese = "(\\d{4}年\\d{1,2}月\\d{1,2}日|\\d{4}年\\d{2}月\\d{2}日)";
|
|
|
+ SimpleDateFormat inputDateFormat = new SimpleDateFormat("yyyy/M/dd");
|
|
|
+ SimpleDateFormat outputDateFormat = new SimpleDateFormat("yyyy年MM月dd日");
|
|
|
+
|
|
|
+ // 临时文件路径(复用原逻辑)
|
|
|
+ Long id = SnowFlakeUtil.getId();
|
|
|
+ String importExcelFilePath = FileUtils.getSysLocalFileUrl();
|
|
|
+ String importExcelTOHtmlPath = importExcelFilePath + "/pdf//" + id + ".html";
|
|
|
+
|
|
|
+ com.spire.xls.Workbook workbook = null;
|
|
|
+ try {
|
|
|
+ // 导入的excel转换为html(复用原逻辑)
|
|
|
+ workbook = new com.spire.xls.Workbook();
|
|
|
+ workbook.loadFromHtml(inputStream); // 加载单个sheet的输入流
|
|
|
+ workbook.saveToFile(importExcelTOHtmlPath, com.spire.xls.FileFormat.HTML);
|
|
|
+ com.spire.xls.Worksheet sheet = workbook.getWorksheets().get(0);
|
|
|
+
|
|
|
+ // 获取转换后的html路径
|
|
|
+ String url_1 = importExcelTOHtmlPath.split("pdf//")[0];
|
|
|
+ String excelToHtmlFileUrl = url_1 + "/pdf/" + id + "_files/" + sheet.getName() + ".html";
|
|
|
+ String htmlString_2 = IoUtil.readToString(new FileInputStream(ResourceUtil.getFile(excelToHtmlFileUrl)));
|
|
|
+
|
|
|
+ // 解析两张html的tr、td(复用原逻辑)
|
|
|
+ Document doc_1 = Jsoup.parse(htmlString_1); // 模板html
|
|
|
+ Document doc_2 = Jsoup.parse(htmlString_2); // 导入的excel转换的html
|
|
|
+ Elements trElements1 = doc_1.select("table tbody tr");
|
|
|
+ Elements trElements2 = doc_2.select("table tbody tr");
|
|
|
+
|
|
|
+ for (int i = 0; i < trElements1.size(); i++) {
|
|
|
+ Element tr1 = trElements1.get(i);
|
|
|
+ Element tr2 = trElements2.size() > i ? trElements2.get(i) : null;
|
|
|
+ if (tr2 == null) break;
|
|
|
+
|
|
|
+ Elements tdElements1 = tr1.select("td");
|
|
|
+ Elements tdElements2 = tr2.select("td");
|
|
|
+
|
|
|
+ for (int j = 0; j < tdElements1.size(); j++) {
|
|
|
+ Element td1 = tdElements1.get(j);
|
|
|
+ if (td1.attr("dqid").length() > 0) { // 跳过包含dqid的td
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 跳过包含hc-table-form-upload子元素的td
|
|
|
+ if (!td1.select("hc-table-form-upload").isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Element td2 = tdElements2.size() > j ? tdElements2.get(j) : null;
|
|
|
+ if (td2 == null) break;
|
|
|
+
|
|
|
+ String keyName = getKeyNameFromChildElement(td1); // 复用原方法获取key
|
|
|
+ if (StringUtils.isNotEmpty(keyName)) {
|
|
|
+ String divValue = td2.text();
|
|
|
+ if (StringUtils.isNotEmpty(divValue)) {
|
|
|
+ // 日期范围处理
|
|
|
+ if (parseDateRange(divValue).size() == 2) {
|
|
|
+ resultMap.put(keyName, parseDateRange(divValue));
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 日期格式转换(复用原逻辑)
|
|
|
+ Pattern pattern_XG = Pattern.compile(doubleSlashRegex_XG);
|
|
|
+ Matcher matcher_XG = pattern_XG.matcher(divValue);
|
|
|
+ if (matcher_XG.matches()) {
|
|
|
+ Pattern pattern_yyyyMdd = Pattern.compile(dateFormatRegex_yyyyMdd);
|
|
|
+ Pattern pattern_yyyyMMdd = Pattern.compile(dateFormatRegex_yyyyMMdd);
|
|
|
+ Matcher matcher_yyyyMdd = pattern_yyyyMdd.matcher(divValue);
|
|
|
+ Matcher matcher_yyyyMMdd = pattern_yyyyMMdd.matcher(divValue);
|
|
|
+
|
|
|
+ if (matcher_yyyyMdd.matches() || matcher_yyyyMMdd.matches()) {
|
|
|
+ Date date = inputDateFormat.parse(divValue);
|
|
|
+ divValue = outputDateFormat.format(date);
|
|
|
+ }
|
|
|
+ } else if (divValue.contains("年") && divValue.contains("月") && divValue.contains("日")) {
|
|
|
+ Pattern pattern_chinese = Pattern.compile(dateFormatRegex_chinese);
|
|
|
+ Matcher matcher_chinese = pattern_chinese.matcher(divValue);
|
|
|
+ if (matcher_chinese.matches()) {
|
|
|
+ Date date = outputDateFormat.parse(divValue);
|
|
|
+ divValue = outputDateFormat.format(date);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ resultMap.put(keyName, divValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (workbook != null) {
|
|
|
+ workbook.dispose();
|
|
|
+ }
|
|
|
+ // 删除临时文件(复用原逻辑)
|
|
|
+ if (deleteFolder(Paths.get(importExcelTOHtmlPath))) {
|
|
|
+ logger.info("表单[{}]临时文件删除成功", pkeyId);
|
|
|
+ } else {
|
|
|
+ logger.warn("表单[{}]临时文件删除失败", pkeyId);
|
|
|
+ }
|
|
|
+ String url_1 = importExcelTOHtmlPath.split("pdf//")[0];
|
|
|
+ if (deleteFolderAndContents(Paths.get(url_1 + "/pdf/" + id + "_files"))) {
|
|
|
+ logger.info("表单[{}]临时文件夹删除成功", pkeyId);
|
|
|
+ } else {
|
|
|
+ logger.warn("表单[{}]临时文件夹删除失败", pkeyId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return resultMap;
|
|
|
+ }
|
|
|
/**
|
|
|
* 客户端-导入excel数据到对应元素表中
|
|
|
*
|
|
|
@@ -981,6 +1660,8 @@ public class WbsTreeContractController extends BladeController {
|
|
|
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 判断日期范围格式数据,以下12种格式
|
|
|
* 2023-01-01-2023-01-30 或 2023-01-01~2023-01-30
|