Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/dev' into dev

LHB 5 dní pred
rodič
commit
340c286253
27 zmenil súbory, kde vykonal 1170 pridanie a 208 odobranie
  1. 15 0
      blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/dto/AddScanFileDto.java
  2. 2 0
      blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/entity/ScanFile.java
  3. 2 0
      blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/vo/ScanFolderVO.java
  4. 31 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/controller/ScanFileController.java
  5. 1 1
      blade-service/blade-archive/src/main/java/org/springblade/archive/mapper/ScanFileMapper.java
  6. 4 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/mapper/ScanFolderMapper.java
  7. 3 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/mapper/ScanFolderMapper.xml
  8. 7 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/service/ScanFileService.java
  9. 3 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ArchivesAutoServiceImpl.java
  10. 100 7
      blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ScanFileServiceImpl.java
  11. 6 0
      blade-service/blade-business/src/main/java/org/springblade/business/service/impl/TaskServiceImpl.java
  12. 15 0
      blade-service/blade-manager/pom.xml
  13. 1 1
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/ExcelTabController.java
  14. 101 99
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/HtmlTableToExcelConverter.java
  15. 37 4
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/TextdictInfoController.java
  16. 698 17
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeContractController.java
  17. 6 6
      blade-service/blade-manager/src/main/java/org/springblade/manager/formula/ITurnPointCalculator.java
  18. 2 1
      blade-service/blade-manager/src/main/java/org/springblade/manager/formula/impl/FormulaTurnPoint.java
  19. 2 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeContractMapper.java
  20. 8 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeContractMapper.xml
  21. 1 1
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreePrivateMapper.xml
  22. 3 70
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/FormulaServiceImpl.java
  23. 26 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsDivideServiceImpl.java
  24. 25 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeContractServiceImpl.java
  25. 7 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreePrivateServiceImpl.java
  26. 59 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/DuplicateSheetRecognizer.java
  27. 5 1
      pom.xml

+ 15 - 0
blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/dto/AddScanFileDto.java

@@ -0,0 +1,15 @@
+package org.springblade.archive.dto;
+
+import lombok.Data;
+
+@Data
+public class AddScanFileDto {
+    private Long projectId;
+    private Long contractId;
+    private String fileName;
+    private Long forderId;
+    private Integer fileSize;
+    private String ossUrl;
+    private String responsible;
+    private String fileDate;
+}

+ 2 - 0
blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/entity/ScanFile.java

@@ -33,6 +33,8 @@ public class ScanFile {
     private String fileNum;
     @ApiModelProperty(value = "文件题名")
     private String fileName;
+    @ApiModelProperty(value = "文件名")
+    private String fileNameSuffix;
     @ApiModelProperty(value = "文件页数")
     private String fileSize;
     @ApiModelProperty(value = "文件日期")

+ 2 - 0
blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/vo/ScanFolderVO.java

@@ -15,4 +15,6 @@ public class ScanFolderVO extends ScanFolder {
     private Boolean hasChildren;
     @ApiModelProperty(value = "子级节点")
     private List<ScanFolderVO> childs;
+    @ApiModelProperty(value = "是否可以删除")
+    private Boolean isRemove;
 }

+ 31 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/controller/ScanFileController.java

@@ -1,17 +1,25 @@
 package org.springblade.archive.controller;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.itextpdf.text.pdf.PdfDictionary;
+import com.itextpdf.text.pdf.PdfReader;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
+import org.apache.pdfbox.io.MemoryUsageSetting;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.springblade.archive.dto.AddScanFileDto;
 import org.springblade.archive.dto.ScanFileMoveDTO;
 import org.springblade.archive.entity.ScanFile;
 import org.springblade.archive.entity.ScanFolder;
 import org.springblade.archive.service.ScanFileService;
 import org.springblade.archive.service.ScanFolderService;
 import org.springblade.archive.vo.ScanFolderVO;
+import org.springblade.business.entity.ArchiveFile;
+import org.springblade.common.utils.CommonUtil;
 import org.springblade.core.mp.support.Query;
 import org.springblade.core.tool.api.R;
 import org.springblade.manager.entity.ContractInfo;
@@ -23,6 +31,11 @@ import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.io.InputStream;
+import java.net.URLEncoder;
+import java.time.LocalTime;
+import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -32,6 +45,8 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 @RestController
 @AllArgsConstructor
@@ -121,6 +136,22 @@ public class ScanFileController {
         IPage<ScanFile> page=scanFileService.getScanFile(contractId,projectId,folderId,query,move);
         return R.data(page);
     }
+    @PostMapping("/addScanFolder")
+    @ApiOperation("新增节点扫描文件夹")
+    public R addScanFolder(Long projectId,Long contractId,Long parentId,String forderName){
+        return R.status(scanFileService.addScanFolder(projectId,contractId,parentId,forderName));
+    }
+    @PostMapping("/addScanFile")
+    @ApiOperation("新增节点扫描文件")
+    public R addScanFile(@RequestBody List<AddScanFileDto>list){
+        return R.status(scanFileService.addScanFile(list));
+    }
+    @GetMapping("/deleteScanFolder")
+    @ApiOperation("删除扫描文件夹")
+    @ApiImplicitParam(name = "id", value = "文件夹ID")
+    public R deleteScanFolder(@RequestParam Long id){
+        return R.status(scanFileService.deleteScanFolder(id));
+    }
     @GetMapping("/getDetil")
     @ApiOperation("获取扫描文件详情")
     @ApiImplicitParam(name = "id", value = "文件ID")

+ 1 - 1
blade-service/blade-archive/src/main/java/org/springblade/archive/mapper/ScanFileMapper.java

@@ -22,5 +22,5 @@ public interface ScanFileMapper extends BaseMapper<ScanFile> {
 
     IPage<ScanFile> getScanFile(IPage<ScanFile> page, @Param("contractId") Long contractId, @Param("projectId") Long projectId, @Param("folderId") Long folderId,@Param("move")Integer  move);
 
-    void removeScan(@Param("longList") List<Long> longList);
+    void removeScan(@Param("longList") List<Long> longList, @Param("projectId")Long projectId, @Param("contractId")Long contractId);
 }

+ 4 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/mapper/ScanFolderMapper.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Param;
 import org.springblade.archive.entity.ScanFolder;
 
+import java.util.List;
+
 public interface ScanFolderMapper extends BaseMapper<ScanFolder> {
 
     int exists(@Param("projectId") Long projectId, @Param("contractId") Long contractId, @Param("folderName") String folderName);
@@ -12,4 +14,6 @@ public interface ScanFolderMapper extends BaseMapper<ScanFolder> {
 
 
     Long getId(@Param("folderName") String folderName, @Param("contractId") Long contractId, @Param("projectId") Long projectId);
+
+    List<ScanFolder> selectAllChildren(@Param("id") Long id);
 }

+ 3 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/mapper/ScanFolderMapper.xml

@@ -9,4 +9,7 @@
     <select id="getId" resultType="java.lang.Long">
         select id from scan_folder where project_id = #{projectId} AND contract_id = #{contractId} AND folder_name = #{folderName}
     </select>
+    <select id="selectAllChildren" resultType="org.springblade.archive.entity.ScanFolder">
+        CALL GetScanFolderChildren(#{id})
+    </select>
 </mapper>

+ 7 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/service/ScanFileService.java

@@ -2,6 +2,7 @@ package org.springblade.archive.service;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.archive.dto.AddScanFileDto;
 import org.springblade.archive.entity.ScanFile;
 import org.springblade.archive.vo.ScanFolderVO;
 import org.springblade.core.mp.support.Query;
@@ -28,4 +29,10 @@ public interface ScanFileService extends IService<ScanFile> {
     Boolean autoRecognize(String ids);
 
     boolean moveScanFile(List<Long> ids, Long nodeId);
+
+    boolean addScanFolder(Long projectId, Long contractId ,Long parentId, String forderName);
+
+    boolean addScanFile(List<AddScanFileDto>list);
+
+    boolean deleteScanFolder(Long id);
 }

+ 3 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ArchivesAutoServiceImpl.java

@@ -5368,6 +5368,7 @@ public class ArchivesAutoServiceImpl extends BaseServiceImpl<ArchivesAutoMapper,
 		//String url="D:\\AutoPdf\\";
 		//List<Long> idsList=Func.toLongList(ids);
 		List<ArchivesAuto> archivesAutoList = this.list(new LambdaQueryWrapper<ArchivesAuto>().in(ArchivesAuto::getId, idsList));
+		this.update(Wrappers.<ArchivesAuto>lambdaUpdate().set(ArchivesAuto::getColourStatus, 2).in(ArchivesAuto::getId, idsList));
 		for (ArchivesAuto auto : archivesAutoList) {
 			String sql=" select * from u_archive_file where is_deleted = 0 and archive_id="+auto.getId();
 			List<ArchiveFile> archiveFiles = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(ArchiveFile.class));
@@ -5439,6 +5440,8 @@ public class ArchivesAutoServiceImpl extends BaseServiceImpl<ArchivesAutoMapper,
 
 			}finally {
 				FileUtils.removeFile(filePath);
+				String updateSql="update u_archives_auto set colour_status=1 where id="+auto.getId();
+				jdbcTemplate.execute(updateSql);
 			}
 		}
 		this.updateBatchById(archivesAutoList);

+ 100 - 7
blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ScanFileServiceImpl.java

@@ -6,17 +6,20 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
+import org.springblade.archive.dto.AddScanFileDto;
 import org.springblade.archive.entity.ArchivesAuto;
 import org.springblade.archive.entity.ScanFile;
 import org.springblade.archive.entity.ScanFolder;
 import org.springblade.archive.mapper.ScanFileMapper;
 import org.springblade.archive.mapper.ScanFolderMapper;
 import org.springblade.archive.service.ScanFileService;
+import org.springblade.archive.service.ScanFolderService;
 import org.springblade.archive.utils.FileUtils;
 import org.springblade.archive.vo.ScanFolderVO;
 import org.springblade.business.entity.ArchiveFile;
 import org.springblade.business.feign.ArchiveFileClient;
 import org.springblade.common.utils.SnowFlakeUtil;
+import org.springblade.core.log.exception.ServiceException;
 import org.springblade.core.mp.support.Query;
 import org.springblade.core.oss.model.BladeFile;
 import org.springblade.core.tool.api.R;
@@ -61,8 +64,7 @@ public class ScanFileServiceImpl  extends ServiceImpl<ScanFileMapper, ScanFile>
     private final ArchiveTreeContractClient archiveTreeContractClient;
     private final JdbcTemplate jdbcTemplate;
     private final ArchiveFileClient archiveFileClient;
-
-
+    private final ScanFolderService scanFolderService;
 
 
     @Override
@@ -97,6 +99,8 @@ public class ScanFileServiceImpl  extends ServiceImpl<ScanFileMapper, ScanFile>
         List<ScanFolderVO> result = new ArrayList<>();
 
         for (ScanFolderVO vo : voList) {
+            List<ScanFolder> scanFolders = scanFolderMapper.selectAllChildren(vo.getId());
+            vo.setIsRemove(scanFolders.isEmpty());
             Long parentId = vo.getParentId();
             if (parentId == null || parentId == 0) {
                 // 没有父节点的作为根节点
@@ -110,7 +114,6 @@ public class ScanFileServiceImpl  extends ServiceImpl<ScanFileMapper, ScanFile>
                 }
             }
         }
-
         return result;
 
     }
@@ -126,10 +129,10 @@ public class ScanFileServiceImpl  extends ServiceImpl<ScanFileMapper, ScanFile>
     public String deleteScanFile(String ids) {
         List<Long> longList = Func.toLongList(ids);
         List<ScanFile> scanFiles = baseMapper.selectList(new LambdaQueryWrapper<>(ScanFile.class).in(ScanFile::getId, longList));
-        List<String> fileNames = scanFiles.stream().filter(o-> !StringUtil.isBlank(o.getFileName())).map(o -> o.getFileName()).collect(Collectors.toList());
-        baseMapper.removeScan(longList);
+        List<String> fileNames = scanFiles.stream().filter(o-> !StringUtil.isBlank(o.getOssUrl())).map(o -> (FileUtils.getAliYunSubUrl(o.getOssUrl()))).collect(Collectors.toList());
+        baseMapper.removeScan(longList,scanFiles.get(0).getProjectId(),scanFiles.get(0).getContractId());
         newIOSSClient.removeFiles(fileNames);
-        return "";
+        return "删除成功";
     }
 
     @Override
@@ -185,6 +188,95 @@ public class ScanFileServiceImpl  extends ServiceImpl<ScanFileMapper, ScanFile>
         }
     }
 
+    @Override
+    public boolean addScanFolder(Long projectId, Long contractId, Long parentId, String forderName) {
+            ScanFolder scanFolder = new ScanFolder();
+            scanFolder.setId(SnowFlakeUtil.getId());
+            scanFolder.setProjectId(projectId);
+            scanFolder.setContractId(contractId);
+            scanFolder.setFolderName(forderName);
+            scanFolder.setParentId(parentId);
+            scanFolder.setIsDeleted(0);
+//            String folderPath="";
+//            if(parentId!=0){
+//                ScanFolder fatherFolder = scanFolderMapper.selectById(parentId);
+//                if (fatherFolder!=null){
+//                    String fatherPath = fatherFolder.getFolderPath();
+//                    folderPath=fatherPath+"/"+forderName;
+//                }
+//            }else {
+//                 folderPath=ROOT_PREFIX+"/"+contractId+"/"+forderName;
+//            }
+//            File folder = new File(folderPath);
+//           if (!folder.exists()) {
+//             boolean created = folder.mkdirs();
+//             if (!created) {
+//                throw new ServiceException("创建文件夹失败");
+//             }
+//           }
+//          scanFolder.setFolderPath(folderPath);
+          int insert = scanFolderMapper.insert(scanFolder);
+          return insert == 1;
+    }
+
+    @Override
+    public boolean addScanFile(List<AddScanFileDto>list) {
+        List<ScanFile>fileList=new ArrayList<>();
+        Integer digitalNum = baseMapper.selectMaxDigitalNum(list.get(0).getContractId(), list.get(0).getProjectId());
+        if(digitalNum==null||digitalNum<1){
+            digitalNum=0;
+        }
+        Integer sort = baseMapper.selectMaxSort(list.get(0).getContractId(), list.get(0).getProjectId(), list.get(0).getForderId());
+        if(sort==null||sort<1){
+            sort=0;
+        }
+        for (AddScanFileDto dto : list) {
+            ScanFile file = new ScanFile();
+            BeanUtils.copyProperties(dto,file);
+            if(dto.getFileName().indexOf(".")>0&&dto.getFileName().indexOf("pdf")>0){
+                file.setFileName(dto.getFileName().substring(0,dto.getFileName().lastIndexOf(".")));
+            }else {
+                file.setFileName(dto.getFileName());
+            }
+            file.setFileNameSuffix(dto.getFileName());
+            file.setId(SnowFlakeUtil.getId());
+            file.setDigitalNum(++digitalNum);
+            file.setSort(++sort);
+            file.setIsDeleted(0);
+            file.setIsMove(0);
+            fileList.add(file);
+        }
+        return this.saveBatch(fileList);
+    }
+
+    @Override
+    public boolean deleteScanFolder(Long id) {
+        List<ScanFolder> scanFolders = scanFolderMapper.selectAllChildren(id);
+        List<Long> longList = scanFolders.stream().map(ScanFolder::getId).collect(Collectors.toList());
+        List<ScanFile> scanFiles = baseMapper.selectList(new LambdaQueryWrapper<>(ScanFile.class).in(ScanFile::getFolderId, longList));
+        if(!scanFiles.isEmpty()){
+            throw new ServiceException("当前节点或子节点存在文件,无法删除");
+        }
+        for (ScanFolder folder : scanFolders) {
+            scanFolderMapper.deleteById(folder.getId());
+            //this.deleteScanFolderLinux(folder.getId());
+        }
+        return true;
+    }
+
+    private void deleteScanFolderLinux(Long id) {
+        ScanFolder scanFolder = scanFolderMapper.selectById(id);
+        if (scanFolder != null&&scanFolder.getFolderPath()!=null){
+            File folder = new File(scanFolder.getFolderPath());
+            if (folder.exists()) {
+                boolean deleted = folder.delete();
+                if (!deleted) {
+                    throw new ServiceException("删除文件夹失败");
+                }
+            }
+        }
+    }
+
     /**
      * 入口方法:扫描并入库指定contractId的所有文件夹
      * @param contractId 传入的合同ID(对应D:\PDF下的文件夹名)
@@ -409,7 +501,8 @@ public class ScanFileServiceImpl  extends ServiceImpl<ScanFileMapper, ScanFile>
                     null,//序号
                     null,//数字编号
                     null,//文件编号
-                    fileName,
+                    fileName.substring(0,fileName.lastIndexOf(".")),//文件题名
+                    fileName,//文件名
                     pdfNum,//文件页数
                     null,//文件日期
                     createTime,

+ 6 - 0
blade-service/blade-business/src/main/java/org/springblade/business/service/impl/TaskServiceImpl.java

@@ -2371,6 +2371,12 @@ public class TaskServiceImpl extends BaseServiceImpl<TaskMapper, Task> implement
             recordResignLog("一键重签", ids, queryList, requestMap, null, null, null);
         }
         taskProgressService.addTaskProgress(queryList.get(0).getProjectId(), queryList.get(0).getContractId(), 4,queryList.size(),ids);
+        try {
+            List<WbsTreeContractStatisticsDTO> collect = queryList.stream().map(WbsTreeContractStatisticsDTO::new).collect(Collectors.toList());
+            wbsTreeContractStatisticsClient.updateInformationQueryStatus(collect);
+        } catch (Exception e) {
+            log.error("更新统计异常", e);
+        }
         return R.success("操作成功");
     }
 

+ 15 - 0
blade-service/blade-manager/pom.xml

@@ -214,6 +214,21 @@
             <version>2.10.3</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>xalan</groupId>
+            <artifactId>xalan</artifactId>
+            <version>2.7.2</version>
+        </dependency>
+        <dependency>
+            <groupId>xerces</groupId>
+            <artifactId>xercesImpl</artifactId>
+            <version>2.12.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+            <version>1.16.1</version>
+        </dependency>
     </dependencies>
     <build>
         <plugins>

+ 1 - 1
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/ExcelTabController.java

@@ -1909,7 +1909,7 @@ public class ExcelTabController extends BladeController {
             updateWrapper.set("tab_group_id", tabGroupId);
             wbsTreeContractService.update(updateWrapper);
         }
-        return R.data("成功");
+        return R.data(wbsTreeContract);
     }
 
 

+ 101 - 99
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/HtmlTableToExcelConverter.java

@@ -61,131 +61,133 @@ public class HtmlTableToExcelConverter {
         Map<Integer, Set<Integer>> occupiedCells = new HashMap<>();
 
         int excelRowNum = 0;
-        for (Element tr : table.select("tr")) {
-            // 跳过空行
-            if (tr.children().isEmpty()) {
-                excelRowNum++;
-                continue;
-            }
+        if (table != null) {
+            for (Element tr : table.select("tr")) {
+                // 跳过空行
+                if (tr.children().isEmpty()) {
+                    excelRowNum++;
+                    continue;
+                }
 
-            // 创建行并设置默认行高(比默认值小)
-            Row excelRow = sheet.createRow(excelRowNum);
+                // 创建行并设置默认行高(比默认值小)
+                Row excelRow = sheet.createRow(excelRowNum);
 
 
-            // 初始化当前行占用集合
-            occupiedCells.putIfAbsent(excelRowNum, new HashSet<>());
+                // 初始化当前行占用集合
+                occupiedCells.putIfAbsent(excelRowNum, new HashSet<>());
 
-            int excelColNum = 0;
+                int excelColNum = 0;
 
 
-            //是否设置行高
-            Boolean isHeight = false;
+                //是否设置行高
+                Boolean isHeight = false;
 
-            for (Element td : tr.select("td")) {
+                for (Element td : tr.select("td")) {
 
-                //设置行高
-                String style1 = td.attr("style");
-                if (!isHeight && StringUtil.isNotBlank(style1)) {
-                    List<String> collect = Arrays.stream(style1.split(";")).collect(Collectors.toList());
-                    HashMap<String, String> map = new HashMap<>();
-                    collect.forEach(s -> {
-                        map.put(s.split(":")[0], s.split(":")[1]);
-                    });
+                    //设置行高
+                    String style1 = td.attr("style");
+                    if (!isHeight && StringUtil.isNotBlank(style1)) {
+                        List<String> collect = Arrays.stream(style1.split(";")).collect(Collectors.toList());
+                        HashMap<String, String> map = new HashMap<>();
+                        collect.forEach(s -> {
+                            map.put(s.split(":")[0], s.split(":")[1]);
+                        });
 
-                    String height = map.get("height");
-                    Float rowHeight = 16f; // 默认16点
-                    if (StringUtil.isNotBlank(height)) {
-                        isHeight = true;
-                        height = height.replace("px", "");
-                        try {
-                            Float px = Float.valueOf(height);
-                            rowHeight = px * 0.75f; // 像素转点 (1px ≈ 0.75pt)
-                        } catch (Exception ignored) {
-                            ignored.printStackTrace();
-                        }
+                        String height = map.get("height");
+                        Float rowHeight = 16f; // 默认16点
+                        if (StringUtil.isNotBlank(height)) {
+                            isHeight = true;
+                            height = height.replace("px", "");
+                            try {
+                                Float px = Float.valueOf(height);
+                                rowHeight = px * 0.75f; // 像素转点 (1px ≈ 0.75pt)
+                            } catch (Exception ignored) {
+                                ignored.printStackTrace();
+                            }
 
-                        excelRow.setHeightInPoints(rowHeight);
+                            excelRow.setHeightInPoints(rowHeight);
+                        }
                     }
-                }
 
 
-                // 跳过已被占用的单元格
-                while (isCellOccupied(occupiedCells, excelRowNum, excelColNum)) {
-                    excelColNum++;
-                }
+                    // 跳过已被占用的单元格
+                    while (isCellOccupied(occupiedCells, excelRowNum, excelColNum)) {
+                        excelColNum++;
+                    }
 
-                // 处理列跨度和行跨度
-                int colspan = getSpan(td, "colspan");
-                int rowspan = getSpan(td, "rowspan");
+                    // 处理列跨度和行跨度
+                    int colspan = getSpan(td, "colspan");
+                    int rowspan = getSpan(td, "rowspan");
 
-                // 获取单元格内容
-                String cellText = extractCellText(td);
+                    // 获取单元格内容
+                    String cellText = extractCellText(td);
 
-                // 创建单元格
-                Cell cell = excelRow.createCell(excelColNum);
-                cell.setCellValue(cellText);
+                    // 创建单元格
+                    Cell cell = excelRow.createCell(excelColNum);
+                    cell.setCellValue(cellText);
 
-                // 应用样式
-                String styleKey = getCellStyleKey(td, cssRules);
-                if (!styleCache.containsKey(styleKey)) {
-                    CellStyle style = createCellStyle(workbook, td, cssRules, fontCache);
-                    styleCache.put(styleKey, style);
+                    // 应用样式
+                    String styleKey = getCellStyleKey(td, cssRules);
+                    if (!styleCache.containsKey(styleKey)) {
+                        CellStyle style = createCellStyle(workbook, td, cssRules, fontCache);
+                        styleCache.put(styleKey, style);
 
-                    // 检查是否需要自动换行
-                    if (shouldWrapText(td, cssRules, cellText)) {
-                        style.setWrapText(true);
+                        // 检查是否需要自动换行
+                        if (shouldWrapText(td, cssRules, cellText)) {
+                            style.setWrapText(true);
+                        }
                     }
-                }
-                cell.setCellStyle(styleCache.get(styleKey));
-
-                // 记录列宽(优先使用HTML中的宽度)
-                if (td.hasAttr("width")) {
-                    String widthStr = td.attr("width").replace("px", "");
-                    try {
-                        float px = Float.parseFloat(widthStr);
-                        float charWidth = px / 7f; // 像素转字符宽度 (1字符≈7px)
-
-                        // 考虑跨列情况:总宽度分配到各列
-                        for (int i = 0; i < colspan; i++) {
-                            int colIdx = excelColNum + i;
-                            float perColWidth = charWidth / colspan;
-
-                            // 取最大宽度作为列宽
-                            columnWidths.putIfAbsent(colIdx, 0f);
-                            if (perColWidth > columnWidths.get(colIdx)) {
-                                columnWidths.put(colIdx, perColWidth);
+                    cell.setCellStyle(styleCache.get(styleKey));
+
+                    // 记录列宽(优先使用HTML中的宽度)
+                    if (td.hasAttr("width")) {
+                        String widthStr = td.attr("width").replace("px", "");
+                        try {
+                            float px = Float.parseFloat(widthStr);
+                            float charWidth = px / 7f; // 像素转字符宽度 (1字符≈7px)
+
+                            // 考虑跨列情况:总宽度分配到各列
+                            for (int i = 0; i < colspan; i++) {
+                                int colIdx = excelColNum + i;
+                                float perColWidth = charWidth / colspan;
+
+                                // 取最大宽度作为列宽
+                                columnWidths.putIfAbsent(colIdx, 0f);
+                                if (perColWidth > columnWidths.get(colIdx)) {
+                                    columnWidths.put(colIdx, perColWidth);
+                                }
                             }
+                        } catch (NumberFormatException ignored) {
                         }
-                    } catch (NumberFormatException ignored) {
                     }
-                }
 
-                // 标记当前单元格占据的所有位置
-                markCellsAsOccupied(occupiedCells, excelRowNum, excelColNum, rowspan, colspan);
-
-                // 创建合并区域
-                if (colspan > 1 || rowspan > 1) {
-                    CellRangeAddress region = new CellRangeAddress(
-                            excelRowNum,
-                            excelRowNum + rowspan - 1,
-                            excelColNum,
-                            excelColNum + colspan - 1
-                    );
-
-                    // 检查是否与现有区域重叠
-                    if (!isOverlapping(mergedRegions, region)) {
-                        sheet.addMergedRegion(region);
-                        mergedRegions.add(region);
-                        CellStyle cellStyle = styleCache.get(styleKey);
-                        mergedFrame.put(region, cellStyle);
-                        // 为合并区域设置边框(使用左上角单元格的样式)
-//                        setMergedRegionBorders(workbook, sheet, region, styleCache.get(styleKey));
+                    // 标记当前单元格占据的所有位置
+                    markCellsAsOccupied(occupiedCells, excelRowNum, excelColNum, rowspan, colspan);
+
+                    // 创建合并区域
+                    if (colspan > 1 || rowspan > 1) {
+                        CellRangeAddress region = new CellRangeAddress(
+                                excelRowNum,
+                                excelRowNum + rowspan - 1,
+                                excelColNum,
+                                excelColNum + colspan - 1
+                        );
+
+                        // 检查是否与现有区域重叠
+                        if (!isOverlapping(mergedRegions, region)) {
+                            sheet.addMergedRegion(region);
+                            mergedRegions.add(region);
+                            CellStyle cellStyle = styleCache.get(styleKey);
+                            mergedFrame.put(region, cellStyle);
+                            // 为合并区域设置边框(使用左上角单元格的样式)
+    //                        setMergedRegionBorders(workbook, sheet, region, styleCache.get(styleKey));
+                        }
                     }
-                }
 
-                excelColNum += colspan;
+                    excelColNum += colspan;
+                }
+                excelRowNum++;
             }
-            excelRowNum++;
         }
         // 修复合并单元格边框问题
         fixMergedRegionBorders(workbook, sheet, mergedRegions, mergedFrame);

+ 37 - 4
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/TextdictInfoController.java

@@ -56,6 +56,7 @@ import org.springblade.manager.vo.*;
 import org.springframework.dao.DataAccessException;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.SingleColumnRowMapper;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
@@ -410,13 +411,45 @@ public class TextdictInfoController extends BladeController {
                 String isExitSql = " select * from information_schema.TABLES where TABLE_NAME='" + tabName + "'";
                 List<Map<String, Object>> tablist = jdbcTemplate.queryForList(isExitSql);
                 if (tablist != null && tablist.size() > 0 && wbsTreePrivate.getType() != 10) {
+                    String textType = textdictInfo.getTextId();
+                    int type = 0;
+                    if (textType.equals("daterange") || textType.equals("selectBox")) {
+                        type = 1;
+                    }
                     String[] split = keyname.split("__");
-                    String clarSql = "update  " + tabName + " set " + split[0] + "=null where p_key_id in(SELECT p_key_id FROM m_wbs_tree_contract WHERE is_type_private_pid ='" + wbsTreePrivate.getPKeyId() + "' and project_id='" + wbsTreePrivate.getProjectId() + "'UNION ALL SELECT 1 )";
-                    if (split.length > 1 && StringUtil.isNumeric(split[1]) && Integer.parseInt(split[1]) > 80) {
-                        clarSql = "update  " + tabName + " set key_201 = REPLACE(key_201, '" + split[0] + ":' , ':') where p_key_id in(SELECT p_key_id FROM m_wbs_tree_contract WHERE is_type_private_pid ='" + wbsTreePrivate.getPKeyId() + "' and project_id='" + wbsTreePrivate.getProjectId() + "'UNION ALL SELECT 1 )";
+                    List<String> query = jdbcTemplate.query("SELECT column_name from information_schema.COLUMNS where table_name = '" + tabName + "' and column_name in ( '" + split[0] + "', 'key_201')", new SingleColumnRowMapper<>(String.class));
+                    String sql = "";
+                    if (!query.isEmpty()) {
+                        String value;
+                        if (type == 1) {
+                            // 转数组格式
+                            value = String.format(" %s = if(substr(%s,1,1) = '[', %s, concat('[', @v := substr(%s,1, if(instr(%s, '_^_') - 1 < 0, char_length(%s), instr(%s, '_^_') - 1)), if(instr(@v,',') > 0, '', concat(',',@v)) ,']', substr(%s, instr(%s, '_^_'))))", split[0], split[0], split[0], split[0], split[0], split[0], split[0], split[0], split[0]);
+                        } else {
+                            // 转普通格式
+                            value = String.format(" %s = if(substr(%s,1,1) = '[', concat(substr(%s, instr(%s, '[') + 1, (CHAR_LENGTH(%s) - instr(REVERSE(%s) , ']') - instr(%s, '['))), substr(%s, instr(%s, '_^_'))), %s)",
+                                    split[0], split[0], split[0], split[0], split[0], split[0], split[0], split[0], split[0], split[0]);
+                        }
+                        if (query.contains("key_201")) {
+                            String value1;
+                            if (type == 1) {
+                                // 转数组格式
+                                value1 = String.format(" key_201 = concat(SUBSTRING_INDEX(key_201,'%s:',1), '%s:', if(substr(@v := if(INSTR(SUBSTRING_INDEX(key_201, '%s:',-1),'$$') - 1 < 0, SUBSTRING_INDEX(key_201, '%s:',-1), SUBSTR(SUBSTRING_INDEX(key_201, '%s:',-1), 1 , INSTR(SUBSTRING_INDEX(key_201, '%s:',-1),'$$') - 1)) , 1, 1) = '[', @v, concat('[', @vv := substr(@v,1, if(instr(@v, '_^_') - 1 < 0, char_length(@v), instr(@v, '_^_') - 1), if(instr(@vv,',') > 0, '', concat(',',@vv)) ,']', substr(@v, instr(@v, '_^_'))))  , if (INSTR(SUBSTRING_INDEX(key_201, 'key_16:',-1),'$$') - 1 < 0, '', concat('$$', SUBSTRING_INDEX(SUBSTRING_INDEX(key_201,'key_16:',-1),'$$',-1))) ) ",
+                                        split[0],split[0], split[0], split[0], split[0], split[0]);
+                            } else {
+                                // 转普通格式
+                                value1 = String.format(" key_201 = concat(SUBSTRING_INDEX(key_201,'%s:',1), '%s:', if(substr(@v := if(INSTR(SUBSTRING_INDEX(key_201, '%s:',-1),'$$') - 1 < 0, SUBSTRING_INDEX(key_201, '%s:',-1), SUBSTR(SUBSTRING_INDEX(key_201, '%s:',-1), 1 , INSTR(SUBSTRING_INDEX(key_201, '%s:',-1),'$$') - 1)) , 1, 1) != '[', @v, concat(substr(@v, instr(@v, '[') + 1, (CHAR_LENGTH(@v) - instr(REVERSE(@v) , ']') - instr(@v, '['))), substr(@v, instr(@v, '_^_')))) , if (INSTR(SUBSTRING_INDEX(key_201, 'key_16:',-1),'$$') - 1 < 0, '', concat('$$', SUBSTRING_INDEX(SUBSTRING_INDEX(key_201,'key_16:',-1),'$$',-1))) ) ",
+                                        split[0],split[0],split[0],split[0],split[0], split[0]);
+                            }
+                            value = value + " , " + value1;
+                            sql = String.format("update %s set %s where p_key_id in ( select p_key_id from m_wbs_tree_contract WHERE is_type_private_pid = %d and project_id = %s )",
+                                    tabName, value ,wbsTreePrivate.getPKeyId(), wbsTreePrivate.getProjectId());
+                        } else {
+                            sql = String.format("update %s set %s where p_key_id in ( select p_key_id from m_wbs_tree_contract WHERE is_type_private_pid = %d and project_id = %s ) and %s is not null and %s != ''",
+                                    tabName, value ,wbsTreePrivate.getPKeyId(), wbsTreePrivate.getProjectId(), split[0], split[0]);
+                        }
                     }
                     try {
-                        jdbcTemplate.execute(clarSql);
+                        jdbcTemplate.execute(sql);
                     } catch (DataAccessException e) {
                         e.printStackTrace();
                     }

+ 698 - 17
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeContractController.java

@@ -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

+ 6 - 6
blade-service/blade-manager/src/main/java/org/springblade/manager/formula/ITurnPointCalculator.java

@@ -131,11 +131,11 @@ public interface ITurnPointCalculator {
         return true;
     }
 
-
+    // 数据校验
     static boolean identifying(LevelInfo levelInfo, List<TurnPoint> tmp,TurnPoint tp,boolean isHead,boolean isTail ){
         if (isHead) {
-            if (tp.checkBmd()) {
-                tp.setType(TurnPoint.BMD);
+            if (tp.checkBmd()) { ///*合法水准点,设计标高、后视必须为数字*/
+                tp.setType(TurnPoint.BMD); //起始点,水准点
                 tp.setBmd(tp.getSj0L() + tp.getH0L());
                 levelInfo.setBmdName(tp.getName());
                 levelInfo.setBmdSj(tp.getSj0L());
@@ -144,7 +144,7 @@ public interface ITurnPointCalculator {
                 return false;
             }
         } else if (tp.getName().matches(ZD_REG)) {
-            tp.setType(TurnPoint.ZD);
+            tp.setType(TurnPoint.ZD); //转点
         } else if (isTail) {
             if (StringUtils.isEquals(tp.getName(), levelInfo.getBmdName())) {
                 if (tp.getSj() == null) {
@@ -155,9 +155,9 @@ public interface ITurnPointCalculator {
                     tp.setSc(tp.getSj0L() + ldx);
                     tp.setDx(ldx);
                 }
-                tp.setType(TurnPoint.CLOSE);
+                tp.setType(TurnPoint.CLOSE);//闭合点
             } else {
-                tp.setType(TurnPoint.CE);
+                tp.setType(TurnPoint.CE); //普通测点
                 tmp.add(tp);
                 TurnPoint close = new TurnPoint(levelInfo, new HashMap<>());
                 close.setName(levelInfo.getBmdName());

+ 2 - 1
blade-service/blade-manager/src/main/java/org/springblade/manager/formula/impl/FormulaTurnPoint.java

@@ -82,6 +82,7 @@ public class FormulaTurnPoint implements FormulaStrategy {
             List<List<Map<String, Object>>> total = group(tableData);
             /*项目配置*/
             LevelInfo info = new LevelInfo();
+            //数获取G8偏差范围
             String dev=Expression.parse(F_DEV).calculate(tec.getConstantMap());
             if(Func.isNotBlank(dev)){
                 info.setDx(dev);
@@ -93,7 +94,7 @@ public class FormulaTurnPoint implements FormulaStrategy {
             }
             /*获取水准点里程*/
             milestone(tec.getContractId(),info);
-           /* 分组计算*/
+           /* 分组计算 ITurnPointCalculator.create 并组装值*/
             List<List<TurnPoint>> result = total.stream().map(e->ITurnPointCalculator.create(e,configMap,info)).collect(Collectors.toList());
             /*附加属性如:顶面和底面高程判断*/
             forG8(cur,result, (Map<String, Object>)tec.getConstantMap().computeIfAbsent("G8", k -> new HashMap<>()),tec);

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

@@ -187,4 +187,6 @@ public interface WbsTreeContractMapper extends EasyBaseMapper<WbsTreeContract> {
     void updateAncestorsPid(@Param("ancestorsPId") String ancestorsPId, @Param("ancestors") String ancestors,@Param("pKeyId")Long pKeyId);
 
     void updateWbsTreeAncestors(@Param("contract")WbsTreeContract contract);
+
+    List<WbsTreeContract> getAncestorsList(@Param("pKeyIds") List<Long> longList);
 }

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

@@ -1082,5 +1082,13 @@
     <select id="getChildWbsTreeContracts" resultType="org.springblade.manager.entity.WbsTreeContract">
         select p_key_id,ancestors,ancestors_p_id from m_wbs_tree_contract where FIND_IN_SET(#{pKeyId}, ancestors_p_id)   and is_deleted=0
     </select>
+    <select id="getAncestorsList" resultType="org.springblade.manager.entity.WbsTreeContract">
+        select * from m_wbs_tree_contract where p_key_id in (
+        <foreach collection="pKeyIds" item="pkeyId" separator=",">
+            #{pkeyId}
+        </foreach>
+        ) and is_deleted=0
+        order by node_type
+    </select>
 
 </mapper>

+ 1 - 1
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreePrivateMapper.xml

@@ -250,7 +250,7 @@
     <update id="updateSortById3">
         UPDATE m_wbs_tree_private
         SET sort = #{sort}
-        WHERE id = #{id}
+        WHERE tree_p_id = #{id}
           AND status = 1
           AND is_deleted = 0
     </update>

+ 3 - 70
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/FormulaServiceImpl.java

@@ -1325,7 +1325,7 @@ public class FormulaServiceImpl extends BaseServiceImpl<FormulaMapper, Formula>
             checkTable = op.get().getInitTableName();
         }
         for (FormData fd : tec.formDataList) {
-            if(fd.getCode().equals("_20240528110420_1795289980302524416:key_8")){
+            if(fd.getCode().equals("m_20220929100217_1575304930258845696:key_10")){
                 System.out.println("111");
             }
             if (fd.verify()) {
@@ -1456,75 +1456,8 @@ public class FormulaServiceImpl extends BaseServiceImpl<FormulaMapper, Formula>
                             /*错位计算偏移量重置*/
                             ele.stream().filter(s -> s.getOffset() > 0).forEach(FormData::restore);
                         } else {
-                            // 做特殊监抽检76 现浇墩、台帽或盖梁抽检记录 特殊处理
-                            Object data = null;
-//                            Boolean flage=true;
-//                            List<TableInfo> tableInfos = tec.getTableInfoList();
-//                            if(!tableInfos.isEmpty()){
-//                                //只有监理才特殊处理
-//                                flage=tableInfos.get(0).getClassify().equals("2");
-//                            }
-//                            if(flage){
-//                                if(f.contains("G8") && f.contains("dx")){
-//                                    String data2 ="";
-//                                    String map = formula.getMap();
-//                                    JSONObject jsonObject = JSON.parseObject(map);
-//                                    String tabKey =jsonObject.keySet().stream().toArray()[0]+"";
-//                                    String[] split = tabKey.split(":");
-//
-//                                    List<NodeTable> tableList = tec.getTableAll();
-//                                    List<Map<String, String>> dataMap = new ArrayList<>();
-//                                    List<TableInfo> tableAll = tec.getTableInfoList();
-//
-//                                    for(NodeTable appwbsTree : tableList){
-//                                        if(appwbsTree.getInitTableName().equals(split[0])){
-//                                            String p_key= appwbsTree.getPKeyId()+"";
-//                                            for(TableInfo nodeTable:tableAll){
-//                                                if(p_key.contains(nodeTable.getPkeyId())){
-//                                                    System.out.println(appwbsTree.getNodeName());
-//                                                    dataMap.add(nodeTable.getDataMap());
-//                                                }
-//                                            }
-//                                        }
-//                                    }
-//                                    List<KeyMapper> keyMappers = tec.getKeyMappers();
-//                                    // 高程偏差 key
-//                                    String dataKeyVal = "key_3";
-//                                /*for(KeyMapper datakey:keyMappers){
-//                                    if(datakey.getEName().indexOf("高程偏差")>=0 && split[0].equals(datakey.getTableName()) && p_key.contains(datakey.getPkId()+"")){
-//                                        dataKeyVal = datakey.getField();
-//                                    }
-//                                }*/
-//                                    //
-//                                    for(Map<String, String> dataMa:dataMap){
-//                                        String dataVal = dataMa.get(split[1]);
-//                                        String[] split1 = dataVal.split("☆");
-//                                        Arrays.sort(split1, Comparator.comparingInt(valu -> Integer.parseInt(((valu+"").split("_\\^_")[1]).split("_")[0])));
-//                                        String dataVal2 = dataMa.get(dataKeyVal);
-//                                        String[] split2 = dataVal2.split("☆");
-//                                        Arrays.sort(split2, Comparator.comparingInt(valu -> Integer.parseInt(((valu+"").split("_\\^_")[1]).split("_")[0])));
-//                                        for(String s:split1){
-//                                            if(s.indexOf("K")>=0){
-//                                                String s1 = "_^_"+s.split("_\\^_")[1].split("_")[0];
-//                                                for(String s2:split2){
-//                                                    if(s2.indexOf(s1)>=0){
-//                                                        data2 = data2 + s2.split("_\\^_")[0] + ",";
-//                                                    }
-//                                                }
-//                                            }
-//                                        }
-//                                    }
-//                                    if(!data2.isEmpty()){
-//                                        data = data2.substring(0,data2.length()-1);
-//                                    }
-//                                    else {
-//                                        data =Expression.parse(formula.getFormula()).calculate(currentMap);
-//                                    }
-//                                }
-//                            }else{
-//
-//                            }
-                            data = Expression.parse(formula.getFormula()).calculate(currentMap);
+                            // 特殊处理 获取值
+                            Object data = Expression.parse(formula.getFormula()).calculate(currentMap);
                             write(tec, fd, data);
                         }
                     } catch (Exception e) {

+ 26 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsDivideServiceImpl.java

@@ -1,6 +1,7 @@
 package org.springblade.manager.service.impl;
 
 import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.HttpEntity;
@@ -12,8 +13,10 @@ import org.apache.http.util.EntityUtils;
 import org.springblade.business.feign.InformationQueryClient;
 import org.springblade.core.mp.base.BaseServiceImpl;
 import org.springblade.manager.entity.WbsDivide;
+import org.springblade.manager.entity.WbsTreeContract;
 import org.springblade.manager.mapper.WbsDivideMapper;
 import org.springblade.manager.service.IWbsDivideService;
+import org.springblade.manager.service.IWbsTreeContractService;
 import org.springblade.manager.vo.DivideClientReq;
 import org.springblade.manager.vo.DivideClientVo;
 import org.springblade.manager.vo.DivideFileVo;
@@ -36,6 +39,7 @@ import java.util.Map;
 public class WbsDivideServiceImpl extends BaseServiceImpl<WbsDivideMapper, WbsDivide> implements IWbsDivideService {
 
     private final InformationQueryClient informationQueryClient;
+    private final IWbsTreeContractService wbsTreeContractService;
 
     @Override
     public List<WbsDivide> getByPKeyId(Long pKeyId) {
@@ -160,6 +164,28 @@ public class WbsDivideServiceImpl extends BaseServiceImpl<WbsDivideMapper, WbsDi
         }
         
         log.info("批量绑定节点完成,pKeyId={}, 总数={}, 成功={}", pKeyId, divideClientVos.size(), successCount);
+        
+        // 更新WbsTreeContract的partitionCode属性
+        try {
+            String divideNum = divideClientVos.get(0).getDivideNum();
+            log.info("开始更新WbsTreeContract的partitionCode,pKeyId={}, divideNum={}", pKeyId, divideNum);
+            
+            WbsTreeContract wbsNode = wbsTreeContractService.getOne(
+                    Wrappers.<WbsTreeContract>lambdaQuery().eq(WbsTreeContract::getPKeyId, pKeyId)
+            );
+            
+            if (wbsNode != null) {
+                wbsNode.setPartitionCode(divideNum);
+                boolean updateResult = wbsTreeContractService.updateById(wbsNode);
+                log.info("更新WbsTreeContract的partitionCode完成,pKeyId={}, divideNum={}, 结果={}", 
+                        pKeyId, divideNum, updateResult);
+            } else {
+                log.warn("未找到pKeyId={}对应的WbsTreeContract记录", pKeyId);
+            }
+        } catch (Exception e) {
+            log.error("更新WbsTreeContract的partitionCode失败,pKeyId={}, error={}", pKeyId, e.getMessage(), e);
+        }
+        
         return successCount;
     }
 

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

@@ -723,6 +723,24 @@ public class WbsTreeContractServiceImpl extends BaseServiceImpl<WbsTreeContractM
         }
     }
 
+    public List<WbsTreeContract> selectAllPkeyIdByNodeId(Long nodeId,Integer classify) {
+        Object[] params;
+        if (classify == 1) {
+            params = new Object[]{nodeId, 1, 2, 3};
+        } else {
+            params = new Object[]{nodeId, 4, 5, 6};
+        }
+
+        return jdbcTemplate.query(
+                "select * from m_wbs_tree_contract where p_id = ? and is_deleted = 0 and status!=0 and type = 2 and table_owner in (?,?,?) order by sort",
+                new BeanPropertyRowMapper<>(WbsTreeContract.class),
+                params);
+    }
+
+    public List<WbsTreeContract> getAncestorsList(String ancestorsPId) {
+        return baseMapper.getAncestorsList(Func.toLongList(ancestorsPId));
+    }
+
 
     public static class WbsTreeContractComparator implements Comparator<AppWbsTreeContractVO> {
 
@@ -3397,6 +3415,13 @@ public class WbsTreeContractServiceImpl extends BaseServiceImpl<WbsTreeContractM
                 bladeRedis.del("import:projectId:"+wbsTreeContractRoot.getProjectId()+"contractId:"+wbsTreeContractRoot.getContractId());
             }finally {
                 bladeRedis.del("import:projectId:"+wbsTreeContractRoot.getProjectId()+"contractId:"+wbsTreeContractRoot.getContractId());
+                try {
+                    if (!insertList.isEmpty()) {
+                        wbsTreeContractStatisticsClient.updateWbsTreeContractNodes(insertList.stream().map(item -> item.getPKeyId() + "").collect(Collectors.joining(",")));
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
             }
             bladeRedis.setEx("import:projectId:"+wbsTreeContractRoot.getProjectId()+"contractId:"+wbsTreeContractRoot.getContractId(), "100",7L);
             return R.success("新增了" + insertList.size() + "个节点" + "," + String.join(",", updateList) + "节点编号已被修改");

+ 7 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreePrivateServiceImpl.java

@@ -428,6 +428,13 @@ public class WbsTreePrivateServiceImpl extends BaseServiceImpl<WbsTreePrivateMap
                         .eq(WbsTreeContract::getProjectId, projectId)
                         .eq(WbsTreeContract::getOldId, id)
                 );
+                //修改项目中对应合同段节点
+                wbsTreeContractService.update(Wrappers.<WbsTreeContract>lambdaUpdate()
+                        .set(WbsTreeContract::getSort, number)
+                        .eq(WbsTreeContract::getWbsId, wbsId)
+                        .eq(WbsTreeContract::getProjectId, projectId)
+                        .eq(WbsTreeContract::getIsTypePrivatePid, objPrivate.getPKeyId())
+                );
             }
             number++;
         }

+ 59 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/DuplicateSheetRecognizer.java

@@ -0,0 +1,59 @@
+package org.springblade.manager.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 识别复制的sheet(格式:原表名(数字),数字≥2)
+ */
+public class DuplicateSheetRecognizer {
+
+    // 正则表达式:匹配"原表名(数字)",数字从2开始(支持多位数如2、10、100等)
+    // 分组1:原表名(去除末尾空格);分组2:复制序号(数字)
+    private static final Pattern DUPLICATE_SHEET_PATTERN = Pattern.compile(
+            "^(.*?)\\(\\s*([2-9]\\d*)\\s*\\)$"
+    );
+
+    /**
+     * 识别sheet是否为复制表
+     * @param sheetName 待识别的sheet名称
+     * @return 识别结果(包含是否为复制表、原表名、复制序号)
+     */
+    public static DuplicateSheetResult recognize(String sheetName) {
+        if (sheetName == null || sheetName.trim().isEmpty()) {
+            return new DuplicateSheetResult(false, null, null);
+        }
+
+        Matcher matcher = DUPLICATE_SHEET_PATTERN.matcher(sheetName.trim());
+        if (matcher.matches()) {
+            // 提取原表名(去除可能的末尾空格)
+            String originalName = matcher.group(1).trim();
+            // 提取复制序号(转换为数字)
+            Integer sequence = Integer.parseInt(matcher.group(2).trim());
+            return new DuplicateSheetResult(true, originalName, sequence);
+        } else {
+            // 非复制表
+            return new DuplicateSheetResult(false, null, null);
+        }
+    }
+
+    /**
+     * 识别结果封装类
+     */
+    public static class DuplicateSheetResult {
+        private boolean isDuplicate; // 是否为复制表
+        private String originalName; // 原表名(仅当isDuplicate为true时有效)
+        private Integer sequence;    // 复制序号(仅当isDuplicate为true时有效,≥2)
+
+        public DuplicateSheetResult(boolean isDuplicate, String originalName, Integer sequence) {
+            this.isDuplicate = isDuplicate;
+            this.originalName = originalName;
+            this.sequence = sequence;
+        }
+
+        // getter方法
+        public boolean isDuplicate() { return isDuplicate; }
+        public String getOriginalName() { return originalName; }
+        public Integer getSequence() { return sequence; }
+    }
+}

+ 5 - 1
pom.xml

@@ -226,7 +226,11 @@
             <name>Release Repository</name>
             <url>http://nexus.bladex.vip/repository/maven-releases/</url>
         </repository>
-
+        <repository>
+            <id>com.e-iceblue</id>
+            <name>e-iceblue</name>
+            <url>https://repo.e-iceblue.cn/repository/maven-public/</url>
+        </repository>
     </repositories>
 
     <pluginRepositories>