Browse Source

Merge branch 'dev' of http://219.151.181.73:3000/zhuwei/bladex into dev

lvy 6 days ago
parent
commit
e26b404bc2
23 changed files with 1221 additions and 321 deletions
  1. 3 0
      blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/entity/ScanFile.java
  2. 1 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ScanFileServiceImpl.java
  3. 3 3
      blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/Archive2Controller.java
  4. 3 3
      blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/ArchiveController.java
  5. 1 1
      blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/ChekSignData.java
  6. 1 1
      blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/EVController.java
  7. 228 0
      blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/ScanFileController.java
  8. 0 181
      blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/testTaskInfo.java
  9. 15 0
      blade-service/blade-manager/pom.xml
  10. 7 2
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/ExcelTabController.java
  11. 101 99
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/HtmlTableToExcelConverter.java
  12. 2 2
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsDivideController.java
  13. 707 22
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeContractController.java
  14. 8 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsDivideMapper.java
  15. 8 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsDivideMapper.xml
  16. 2 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeContractMapper.java
  17. 8 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeContractMapper.xml
  18. 9 1
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/IWbsDivideService.java
  19. 6 1
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ContractInfoServiceImpl.java
  20. 26 4
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsDivideServiceImpl.java
  21. 18 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeContractServiceImpl.java
  22. 59 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/DuplicateSheetRecognizer.java
  23. 5 1
      pom.xml

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

@@ -49,4 +49,7 @@ public class ScanFile {
     private Integer isDeleted;
     @ApiModelProperty(value = "是否移动")
     private Integer isMove;
+    @ApiModelProperty(value = "识别状态 0:未扫描 1:识别中 2:识别成功 99:识别失败")
+    private Integer scanStatus;
+
 }

+ 1 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ScanFileServiceImpl.java

@@ -417,6 +417,7 @@ public class ScanFileServiceImpl  extends ServiceImpl<ScanFileMapper, ScanFile>
                     bladeFile.getLink(),//OSS路径
                     null,//负责人
                     0,
+                    0,
                     0
             );
             // 入库

+ 3 - 3
blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/Archive2Controller.java

@@ -59,12 +59,12 @@ public class Archive2Controller {
     @Resource(name = "archivePoolExecutor")
     private ThreadPoolExecutor archExecutor;
 
-    //@Scheduled(cron = "0/30 * * * * ?")
+    @Scheduled(cron = "0/30 * * * * ?")
     public void SignTaskBatchPng() {
         //执行代码
         log.info("分解pdf专图片");
        // String sql = "SELECT distinct b.id,b.archive_id as archiveId ,REPLACE(b.file_url,'https://xinan1.zos.ctyun.cn','http://100.86.2.1:80') as fileUrl from u_archives_auto a ,u_archive_file b  where a.id=b.archive_id  and a.is_deleted=0 and b.is_deleted=0 and a.split_status=10 LIMIT 20";
-        String sql = "SELECT distinct b.id,b.archive_id as archiveId ,b.file_url as fileUrl from u_archives_auto a ,u_archive_file b  where a.id=b.archive_id  and a.is_deleted=0 and b.is_deleted=0 and a.split_status=10 and a.id=1945020959844990977 LIMIT 20";
+        String sql = "SELECT distinct b.id,b.archive_id as archiveId ,b.file_url as fileUrl from u_archives_auto a ,u_archive_file b  where a.id=b.archive_id  and a.is_deleted=0 and b.is_deleted=0 and a.split_status=10 LIMIT 20";
         List<TaskArchiveSplitVO> query = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(TaskArchiveSplitVO.class));
 
         if (query != null && query.size() >= 1) {
@@ -332,7 +332,7 @@ public class Archive2Controller {
             }
         } catch (Exception e) {
             e.printStackTrace();
-            return "1";
+            return "0";
         }
     }
 

+ 3 - 3
blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/ArchiveController.java

@@ -69,8 +69,8 @@ public class ArchiveController {
     public void SignTaskBatchPng() {
         //执行代码
         log.info("分解pdf专图片");
-        String sql = "SELECT distinct b.id,b.archive_id as archiveId ,REPLACE(b.file_url,'https://xinan1.zos.ctyun.cn','http://100.86.2.1:80') as fileUrl,c.id as taskId from u_archives_auto a , u_archive_file b ,u_task_split c  where a.id=b.archive_id and ((FIND_IN_SET(a.id,c.ids) and c.type=3) or (a.contract_id=c.contract_id and c.type=2)) and a.is_deleted=0 and b.is_deleted=0 and a.split_status not in(1,2) LIMIT 20";
-      //  String sql = "SELECT distinct b.id,b.archive_id as archiveId ,b.file_url as fileUrl,c.id as taskId from u_archives_auto a , u_archive_file b ,u_task_split c  where a.id=b.archive_id and ((FIND_IN_SET(a.id,c.ids) and c.type=3) or (a.contract_id=c.contract_id and c.type=2)) and a.is_deleted=0 and b.is_deleted=0 and a.split_status not in(1,2) and a.id=1947207326716919808 LIMIT 20";
+       // String sql = "SELECT distinct b.id,b.archive_id as archiveId ,REPLACE(b.file_url,'https://xinan1.zos.ctyun.cn','http://100.86.2.1:80') as fileUrl,c.id as taskId from u_archives_auto a , u_archive_file b ,u_task_split c  where a.id=b.archive_id and ((FIND_IN_SET(a.id,c.ids) and c.type=3) or (a.contract_id=c.contract_id and c.type=2)) and a.is_deleted=0 and b.is_deleted=0 and a.split_status not in(1,2) LIMIT 20";
+        String sql = "SELECT distinct b.id,b.archive_id as archiveId ,b.file_url as fileUrl,c.id as taskId from u_archives_auto a , u_archive_file b ,u_task_split c  where a.id=b.archive_id and ((FIND_IN_SET(a.id,c.ids) and c.type=3) or (a.contract_id=c.contract_id and c.type=2)) and a.is_deleted=0 and b.is_deleted=0 and a.split_status not in(1,2) LIMIT 20";
         List<TaskArchiveSplitVO> query = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(TaskArchiveSplitVO.class));
 
         if (query != null && query.size() >= 1) {
@@ -170,7 +170,7 @@ public class ArchiveController {
             e.printStackTrace();
         }
     }
-   // @Scheduled(cron = "0/30 * * * * ?")
+  //  @Scheduled(cron = "0/30 * * * * ?")
     public void SplitPdfInfo() {
         //执行代码
         log.info("分解html开始");

+ 1 - 1
blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/ChekSignData.java

@@ -47,7 +47,7 @@ public class ChekSignData {
     @Resource(name = "taskExecutor1")
     private ThreadPoolExecutor executor;
 
-    @Scheduled(cron = "0/10 * * * * ?")
+//    @Scheduled(cron = "0/10 * * * * ?")
     public void SignInfo() {
         // 质检SQL
         String sql = "SELECT a.id ,a.e_visa_pdf_url,b.process_instance_id,a.contract_id,a.project_id,c.remark_type from u_information_query a ,u_task b ,m_project_info c where  c.id=a.project_id  and a.`status` = 2 and a.is_deleted=0 and a.e_visa_pdf_url is not null  and b.form_data_id = a.id and b.`status` = 2 and a.chek_status=1 LIMIT 30";

+ 1 - 1
blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/EVController.java

@@ -58,7 +58,7 @@ public class EVController {
     @Resource(name = "taskExecutor1")
     private ThreadPoolExecutor executor;
 
-    @Scheduled(cron = "0/10 * * * * ?")
+   // @Scheduled(cron = "0/10 * * * * ?")
     public void SignInfo() {
         //执行代码
 

+ 228 - 0
blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/ScanFileController.java

@@ -0,0 +1,228 @@
+package org.springblade.evisa.controller;
+
+import io.swagger.annotations.Api;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.rendering.PDFRenderer;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.springblade.common.utils.CommonUtil;
+import org.springblade.common.utils.SnowFlakeUtil;
+import org.springblade.core.oss.model.BladeFile;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.core.tool.utils.IoUtil;
+import org.springblade.evisa.utils.FileUtils;
+import org.springblade.evisa.vo.ArchivesSplitInfoVO;
+import org.springblade.evisa.vo.TaskArchiveSplitVO;
+import org.springblade.resource.feign.NewIOSSClient;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * 扫描文件的分解识别
+ *
+ * @author BladeX
+ * @since 2022-05-18
+ */
+@RestController
+@AllArgsConstructor
+@Api(value = "扫描文件的分解识别", tags = "扫描文件的分解识别接口")
+@Slf4j
+public class ScanFileController {
+
+    private final StringRedisTemplate RedisTemplate;
+
+    private final JdbcTemplate jdbcTemplate;
+
+    private final NewIOSSClient newIOSSClient;
+
+    // 线程池
+    @Resource(name = "archivePoolExecutor")
+    private ThreadPoolExecutor archExecutor;
+
+ //   @Scheduled(cron = "0/30 * * * * ?")
+    public void SignTaskBatchPng() {
+        //执行代码
+        log.info("扫描文件的分解识别");
+       // String sql = "SELECT distinct b.id,b.archive_id as archiveId ,REPLACE(b.file_url,'https://xinan1.zos.ctyun.cn','http://100.86.2.1:80') as fileUrl,c.id as taskId from u_archives_auto a , u_archive_file b ,u_task_split c  where a.id=b.archive_id and ((FIND_IN_SET(a.id,c.ids) and c.type=3) or (a.contract_id=c.contract_id and c.type=2)) and a.is_deleted=0 and b.is_deleted=0 and a.split_status not in(1,2) LIMIT 20";
+        String sql = "SELECT a.id,a.oss_url as fileUrl, b.id as taskId  from scan_file a,u_task_split b where ((FIND_IN_SET(a.id,b.ids) and b.type=3) or (a.contract_id=b.contract_id and b.type=2)) and b.data_type=2 and a.is_deleted=0 and b.is_deleted=0 ";
+        List<TaskArchiveSplitVO> query = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(TaskArchiveSplitVO.class));
+
+        if (query != null && query.size() >= 1) {
+            for (TaskArchiveSplitVO dataInfo : query) {
+                if (archExecutor.getQueue().size() <= 20) {
+                    Boolean aBoolean = RedisTemplate.hasKey("scanpdf-" + dataInfo.getArchiveId());
+                    if (!aBoolean) {
+                        RedisTemplate.opsForValue().setIfAbsent("scanpdf-" + dataInfo.getArchiveId(), "1", 600, TimeUnit.SECONDS);
+                        CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
+                            try {
+                                signTaskBatchpngToHtml(dataInfo);
+                            } catch (Exception e) {
+                                e.printStackTrace();
+                            }
+                        }, archExecutor);
+                    }
+                }
+            }
+        }
+        System.out.println("队列_扫描分解_img" + archExecutor.getQueue().size());
+        System.out.println("活跃_扫描分解__img" + archExecutor.getActiveCount());
+        System.out.println("总共_扫描分解__img" + archExecutor.getTaskCount());
+        System.out.println("完成_扫描分解__img" + archExecutor.getCompletedTaskCount());
+    }
+
+    // 分解第一页的任务
+
+    public void signTaskBatchpngToHtml(TaskArchiveSplitVO taskSign) {
+        try {
+            String fileUrl = taskSign.getFileUrl();
+            String archiveId = taskSign.getArchiveId();
+            String id = taskSign.getId();
+            String taskId = taskSign.getTaskId();
+            List<String> listPdf = new ArrayList<>();
+
+            String firstUrl = FileUtils.getSysLocalFileUrl() + "archiveScan/" + archiveId + "first__" + 1 + "__.pdf";
+            File file = new File(firstUrl);
+            if (!file.exists()) {
+                int pdfByPage = getPdfByPage(1, 1, fileUrl, firstUrl);
+            }
+
+            // 保存第一页为300DPI图片
+            String imagePath = FileUtils.getSysLocalFileUrl() + "archiveScan/" + archiveId + "first__" + 1 + "__.png";
+            File imgfile = new File(imagePath);
+            if (!imgfile.exists()) {
+                int dataNum = savePdfAsImage(1, firstUrl, imagePath);
+            }
+
+            file.delete();
+            String state = getImgDataInfo(imagePath, "1");
+
+
+        } catch (Exception e) {
+            System.out.println("12321312");
+            e.printStackTrace();
+        }
+    }
+
+    public static String getImgDataInfo(String fileUrl, String type) {
+        String lasHhtmlUrl = "";
+        try {
+            // 定义Python解释器路径和脚本路径
+            String pythonScript = "/Users/hongchuangyanfa/PycharmProjects/PythonProject/ScanPngInfo.py";
+            // 构建命令
+            ProcessBuilder pb = new ProcessBuilder("python3", pythonScript, fileUrl, type);
+            Process process = pb.start();
+
+            // 读取Python脚本输出
+            BufferedReader reader = new BufferedReader(
+                    new InputStreamReader(process.getInputStream()));
+            String htmlUrl;
+            while ((htmlUrl = reader.readLine()) != null) {
+                System.out.println("222" + htmlUrl);
+                if (htmlUrl.indexOf("标题123x") >= 0) {
+                    System.out.println(htmlUrl);
+                }
+
+                if (htmlUrl.indexOf("单位") >= 0) {
+                    System.out.println(htmlUrl);
+                }
+                if (htmlUrl.indexOf("时间") >= 0) {
+                    System.out.println(htmlUrl);
+                }
+            }
+            // 等待进程结束
+            int exitCode = process.waitFor();
+            if (exitCode == 0) {
+                return lasHhtmlUrl;
+            } else {
+                return "1";
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "1";
+        }
+    }
+
+
+    public static int getPdfByPage(int startPage, int endPage, String filePath, String savePath) {
+        try {
+            InputStream inputStreamByUrl = CommonUtil.getOSSInputStream3(filePath);
+            // 加载PDF文件
+            PDDocument document = PDDocument.load(inputStreamByUrl);
+            // 创建新文档
+            PDDocument newDocument = new PDDocument();
+
+            // 注意:PDFBox中的页面索引从0开始
+            int actualStart = Math.max(0, startPage - 1); // 将用户输入的1转换为0
+            int actualEnd = Math.min(document.getNumberOfPages() - 1, endPage - 1); // 将用户输入的10转换为9
+
+            // 添加指定范围的页面
+            for (int i = actualStart; i <= actualEnd; i++) {
+                PDPage page = document.getPage(i);
+                newDocument.addPage(page);
+            }
+
+            // 保存为新文件
+            newDocument.save(savePath);
+            newDocument.close();
+            document.close();
+            return 0;
+        } catch (Exception e) {
+            return 1;
+        }
+    }
+
+    public static int savePdfAsImage(int pageNum, String filePath, String outputPath) {
+        try (InputStream inputStream = FileUtils.getInputStreamByUrl(filePath);
+             PDDocument document = PDDocument.load(inputStream)) {
+
+            // 验证页码范围
+            if (pageNum < 1 || pageNum > document.getNumberOfPages()) {
+                return 1;
+            }
+
+            PDFRenderer renderer = new PDFRenderer(document);
+
+            // 设置DPI为300
+            final int DPI = 300;
+
+            // 渲染指定页面(注意PDFBox使用0-based索引)
+            //BufferedImage image = renderer.renderImage(pageNum - 1, DPI / 72f);
+            BufferedImage image = renderer.renderImageWithDPI(0, DPI); // 0 表示第一页
+            // 确保输出目录存在
+            File outputFile = new File(outputPath);
+            outputFile.getParentFile().mkdirs();
+
+            // 保存为PNG格式(可改为JPG等)
+            ImageIO.write(image, "PNG", outputFile);
+
+            log.info("PDF页面已成功保存为图片: {}", outputPath);
+            inputStream.close();
+            document.close();
+            return 0;
+        } catch (Exception e) {
+            log.error("PDF转图片失败", e);
+            return 1;
+        }
+    }
+
+}

+ 0 - 181
blade-service/blade-e-visa/src/main/java/org/springblade/evisa/controller/testTaskInfo.java

@@ -1,181 +0,0 @@
-package org.springblade.evisa.controller;
-
-import io.swagger.annotations.Api;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.pdmodel.PDPage;
-import org.apache.pdfbox.rendering.PDFRenderer;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-import org.springblade.common.utils.CommonUtil;
-import org.springblade.common.utils.SnowFlakeUtil;
-import org.springblade.core.launch.StartEventListener;
-import org.springblade.core.oss.model.BladeFile;
-import org.springblade.core.tool.utils.Func;
-import org.springblade.core.tool.utils.IoUtil;
-import org.springblade.evisa.utils.FileUtils;
-import org.springblade.evisa.vo.ArchivesSplitInfoVO;
-import org.springblade.evisa.vo.TaskArchiveSplitVO;
-import org.springblade.resource.feign.NewIOSSClient;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.jdbc.core.BeanPropertyRowMapper;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.web.bind.annotation.RestController;
-
-import javax.annotation.Resource;
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
-import java.io.*;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-
-/**
- * 清表基础数据表 控制器
- *
- * @author BladeX
- * @since 2022-05-18
- */
-@RestController
-@AllArgsConstructor
-@Api(value = "电签类", tags = "电签类接口")
-@Slf4j
-public class testTaskInfo {
-
-    private final StringRedisTemplate RedisTemplate;
-
-    private final JdbcTemplate jdbcTemplate;
-
-    private final NewIOSSClient newIOSSClient;
-
-    // 线程池
-    @Resource(name = "archivePoolExecutor")
-    private ThreadPoolExecutor archExecutor;
-    @Autowired
-    private StartEventListener startEventListener;
-
-    //@Scheduled(cron = "0/20 * * * * ?")
-    public void SignTaskBatchPng() {
-        //执行代码
-        log.info("分解pdf专图片");
-     
-        String sql = "select * from u_information_query_zhu where is_deleted=0 and wbs_id=1932616157764780032 ";
-        List<Map<String, Object>> mapList = jdbcTemplate.queryForList(sql);
-        if (mapList != null && mapList.size() >= 1) {
-            for (Map<String, Object> dataInfo : mapList) {
-
-                String ndid=dataInfo.get("id")+"";
-                String contractId=dataInfo.get("contract_id")+"";
-                String projectId=dataInfo.get("project_id")+"";
-                String classify=dataInfo.get("classify")+"";
-                String wbs_id=dataInfo.get("wbs_id")+"";
-
-
-                if (archExecutor.getQueue().size() <= 30) {
-                    Boolean aBoolean = RedisTemplate.hasKey("taskIdxx-" + ndid);
-                    if (!aBoolean) {
-                        RedisTemplate.opsForValue().setIfAbsent("taskIdxx-" + ndid, "1", 600, TimeUnit.SECONDS);
-                        CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
-                            try {
-                                signTaskBatchpngToHtml(ndid,contractId,projectId,classify,wbs_id);
-                            } catch (Exception e) {
-                                e.printStackTrace();
-                            }
-                        }, archExecutor);
-                    }
-                }
-            }
-        }
-        System.out.println("队列数量_img" + archExecutor.getQueue().size());
-        System.out.println("活跃数量_img" + archExecutor.getActiveCount());
-        System.out.println("总共数量_img" + archExecutor.getTaskCount());
-        System.out.println("完成数量_img" + archExecutor.getCompletedTaskCount());
-    }
-
-    // 分解第一页的任务
-
-    public void signTaskBatchpngToHtml(String ndid,String contractId,String projectId,String classify,String wbs_id) {
-        //
-
-        String taownInfo = "";
-        if(classify.equals("1")){
-            taownInfo="1,2,3";
-        }
-        if(classify.equals("2")){
-            taownInfo="4,5,6";
-        }
-        String  node_info = "select * from m_wbs_tree_contract where is_deleted=0 and table_owner in("+taownInfo+") and p_id = '" + wbs_id + "'";
-        List<Map<String, Object>> nodeInfoList = jdbcTemplate.queryForList(node_info);
-        if(nodeInfoList!=null && nodeInfoList.size()>=1){
-          String priIds = nodeInfoList.stream().map(m -> m.get("is_type_private_pid")).map(Object::toString).collect(Collectors.joining(","));
-            String qweq = nodeInfoList.get(0).get("ancestors_p_id")+"";
-            System.out.println("qweq"+qweq);
-            List<String> str = Func.toStrList(qweq);
-
-            for(int i=1;i<=5;i++){
-                String anId = str.get(str.size() - i-1);
-                String sqldat ="";
-                if(i==5){
-                    sqldat= " SELECT c.p_id, c.aaCount,(SELECT b.id from u_information_query a ,u_task b where a.id=b.form_data_id and a.wbs_id=c.p_id and b.contract_id="+contractId+"  and  a.is_deleted=0 and b.is_deleted=0 ORDER BY a.`status` desc LIMIT 1) as taskId from ("+
-                            "select p_id,count(1) as aaCount from m_wbs_tree_contract where is_deleted=0 and contract_id = '" + contractId + "' and table_owner in ( " + taownInfo + " ) and is_type_private_pid in("+priIds+") and p_id<>"+wbs_id+" group by p_id "+
-                            " ) c ";
-                }else{
-                    sqldat= " SELECT c.p_id, c.aaCount,(SELECT b.id from u_information_query a ,u_task b where a.id=b.form_data_id and a.wbs_id=c.p_id and a.is_deleted=0 and b.contract_id="+contractId+"  and b.is_deleted=0 ORDER BY a.`status` desc LIMIT 1) as taskId from ("+
-                            "select p_id,count(1) as aaCount from m_wbs_tree_contract where is_deleted=0 and contract_id = '" + contractId + "' and table_owner in ( " + taownInfo + " ) and is_type_private_pid in("+priIds+") and ancestors_p_id like '%"+anId+"%' and p_id<>"+wbs_id+" group by p_id "+
-                            " ) c ";
-                }
-
-                List<Map<String, Object>> noInfoList = jdbcTemplate.queryForList(sqldat);
-                if(noInfoList!=null && noInfoList.size()>=1){
-                    for(Map<String, Object> noInfo : noInfoList) {
-                        Integer exceptxx = Integer.parseInt(noInfo.get("aaCount")+"");
-                        String taskId = noInfo.get("taskId")+"";
-                        if(exceptxx >= nodeInfoList.size() && taskId.length()>4){
-                            long newPkId = SnowFlakeUtil.getId();
-                            long projInstand = SnowFlakeUtil.getId();
-                            String dataInfo = " insert into u_task(id,process_definition_id,process_instance_id,project_id,contract_id,start_time,end_time,report_user,report_user_name,task_name,task_content,task_user,fixed_flow_id,form_data_id,batch,type,approval_type,create_user,create_dept,create_time,update_user,update_time,status,is_deleted,trial_self_inspection_record_id,archive_ids,attachment_pdf_url,task_create_timestamp,meter_task_type,meter_task_repeal_desc,task_common_money,is_build_audit) " +
-                                    "SELECT "+newPkId+",process_definition_id,"+projInstand+",project_id,contract_id,start_time,end_time,report_user,report_user_name,task_name,task_content,task_user,fixed_flow_id,"+ndid+",batch,type,approval_type,create_user,create_dept,create_time,update_user,update_time,status,is_deleted,trial_self_inspection_record_id,archive_ids,attachment_pdf_url,task_create_timestamp,meter_task_type,meter_task_repeal_desc,task_common_money,is_build_audit from u_task where id="+taskId;
-
-                            // 添加任务
-
-                            String peall = "SELECT a.* from u_task_parallel a ,u_task b  where a.process_instance_id=b.process_instance_id and b.id='"+taskId+"' and a.is_deleted=0 and b.is_deleted=0 ";
-                            List<Map<String, Object>> mapList = jdbcTemplate.queryForList(peall);
-                            if(mapList!=null && mapList.size()>=1){
-                                for (Map<String, Object> mapData : mapList){
-                                    String pallId= mapData.get("id")+"";
-                                    long projInstaxnd = SnowFlakeUtil.getId();
-                                    String datadx = " insert into u_task_parallel(id,process_instance_id,parallel_process_instance_id,e_visa_status,e_visa_content,task_user,task_user_name,initiative,create_user,create_dept,create_time,update_user,update_time,status,is_deleted,sort,exe_count) " +
-                                             " SELECT "+projInstaxnd+","+projInstand+","+projInstaxnd+",e_visa_status,e_visa_content,task_user,task_user_name,initiative,create_user,create_dept,create_time,update_user,update_time,status,is_deleted,sort,exe_count from u_task_parallel where id='"+pallId+"'";
-
-                                    jdbcTemplate.execute(datadx);
-                                }
-                            }
-
-                            jdbcTemplate.execute(dataInfo);
-                            String updateSql = "update u_information_query_zhu set is_deleted=1 where id="+ndid;
-                            jdbcTemplate.execute(updateSql);
-                            // 修改完成情况
-                            RedisTemplate.delete("taskIdxx-" + ndid);
-                            return;
-                        }
-                    }
-                }
-            }
-        }
-
-        String updatel = "update u_information_query_zhu set is_deleted=5 where id="+ndid;
-        jdbcTemplate.execute(updatel);
-        RedisTemplate.delete("taskIdxx-" + ndid);
-    }
-
-}

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

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

@@ -670,7 +670,12 @@ public class ExcelTabController extends BladeController {
                 }
             }
         }
-        saveOldHtmlConfig(aPrivate, doc);
+
+        //第一次关联 或者 原来数据上没有html时,不需要 去复制默认值超做
+        if(aPrivate!=null && (aPrivate.getHtmlUrl()+"").length()>=10){
+            saveOldHtmlConfig(aPrivate, doc);
+        }
+
         File writefile = new File(thmlUrl);
         FileUtil.writeToFile(writefile, doc.html(), Boolean.parseBoolean("UTF-8"));
 
@@ -1904,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);

+ 2 - 2
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsDivideController.java

@@ -78,11 +78,11 @@ public class WbsDivideController extends BladeController {
     }
 
     /**
-     * 批量绑定节点
+     * 批量绑定节点(先删除该pKeyId的所有已有绑定,再重新绑定)
      */
     @PostMapping("/batchBind")
     @ApiOperationSupport(order = 2)
-    @ApiOperation(value = "批量绑定节点", notes = "传入pKeyId、projectId、contractId和节点列表")
+    @ApiOperation(value = "批量绑定节点", notes = "传入pKeyId、projectId、contractId和节点列表。注意:会先删除该pKeyId的所有已有绑定,再重新绑定传入的节点列表")
     public R<Integer> batchBind(@RequestParam Long pKeyId,
                                  @RequestParam String projectId,
                                  @RequestParam String contractId,

+ 707 - 22
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数据到对应元素表中
      *
@@ -923,11 +1602,14 @@ public class WbsTreeContractController extends BladeController {
                             for (String s : oldSp) {
                                 //单个单元格的数据
                                 String[] split1 = s.split("_\\^_");
-                                //根据旧数据的坐标去新数据中查询
-                                String s1 = newCoordinate.get(split1[1]);
-                                //如果没有查询到就需要当前坐标数据
-                                if (s1 == null) {
-                                    oldRetainData.add(s);
+                                //错误数据
+                                if(split1.length == 2){
+                                    //根据旧数据的坐标去新数据中查询
+                                    String s1 = newCoordinate.get(split1[1]);
+                                    //如果没有查询到就需要当前坐标数据
+                                    if (s1 == null) {
+                                        oldRetainData.add(s);
+                                    }
                                 }
                             }
                             if (CollectionUtil.isNotEmpty(oldRetainData)) {
@@ -966,6 +1648,7 @@ public class WbsTreeContractController extends BladeController {
             }
             return R.success("ok");
         } catch (IndexOutOfBoundsException e) {
+            e.printStackTrace();
             return  R.fail("导入失败,请检查导入表格:是否有修改导入模板(如:拆分、合并、增加或减少行列)、本次导入模版是否统一。");
         } catch (Exception e) {
             throw new RuntimeException(e);
@@ -977,6 +1660,8 @@ public class WbsTreeContractController extends BladeController {
 
     }
 
+
+
     /**
      * 判断日期范围格式数据,以下12种格式
      * 2023-01-01-2023-01-30 或 2023-01-01~2023-01-30

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

@@ -45,6 +45,14 @@ public interface WbsDivideMapper extends BaseMapper<WbsDivide> {
      */
     Integer unbind(@Param("divideId") String divideId, @Param("pKeyId") Long pKeyId);
 
+    /**
+     * 批量解绑,根据pKeyId删除所有相关记录(设置isDeleted为1)
+     *
+     * @param pKeyId 关联键ID
+     * @return 影响行数
+     */
+    Integer unbindAllByPKeyId(@Param("pKeyId") Long pKeyId);
+
 }
 
 

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

@@ -54,6 +54,14 @@
           AND p_key_id = #{pKeyId}
     </update>
 
+    <!-- 批量解绑,根据pKeyId删除所有相关记录(设置isDeleted为1) -->
+    <update id="unbindAllByPKeyId">
+        UPDATE m_wbs_divide
+        SET is_deleted = 1
+        WHERE p_key_id = #{pKeyId}
+          AND is_deleted = 0
+    </update>
+
 </mapper>
 
 

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

+ 9 - 1
blade-service/blade-manager/src/main/java/org/springblade/manager/service/IWbsDivideService.java

@@ -61,7 +61,7 @@ public interface IWbsDivideService extends BaseService<WbsDivide> {
     DivideClientReq getChildNodes(String contractId, String parentId);
 
     /**
-     * 批量绑定节点
+     * 批量绑定节点(先删除该pKeyId的所有已有绑定,再重新绑定)
      *
      * @param pKeyId          关联键ID
      * @param divideClientVos 划分客户端VO列表
@@ -71,6 +71,14 @@ public interface IWbsDivideService extends BaseService<WbsDivide> {
      */
     int batchBind(Long pKeyId, List<DivideClientVo> divideClientVos, String projectId, String contractId);
 
+    /**
+     * 批量解绑,根据pKeyId删除所有相关记录
+     *
+     * @param pKeyId 关联键ID
+     * @return 是否成功
+     */
+    boolean unbindAllByPKeyId(Long pKeyId);
+
     /**
      * 查询指定pKeyId已关联的所有divideId列表
      *

+ 6 - 1
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ContractInfoServiceImpl.java

@@ -1160,7 +1160,12 @@ public class ContractInfoServiceImpl extends BaseServiceImpl<ContractInfoMapper,
                         for (SaveUserInfoByProjectDTO sg : userRoleInfoSG) {
                             if (sg.getUserId().equals(jl.getUserId())) {
                                 //如果监理合同段与施工合同段中有相同的用户信息,就移除,一个用户只能存在一个合同段中
-                                iterator.remove();
+                                System.out.println(iterator.hasNext());
+                                try {
+                                    iterator.remove();
+                                }catch (Exception e){
+                                    continue;
+                                }
                             }
                         }
                     }

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

@@ -137,18 +137,40 @@ public class WbsDivideServiceImpl extends BaseServiceImpl<WbsDivideMapper, WbsDi
             return 0;
         }
 
+        // 先删除该pKeyId的所有已有绑定
+        log.info("批量绑定前,先删除pKeyId={}的所有已有绑定", pKeyId);
+        unbindAllByPKeyId(pKeyId);
+
+        // 重新绑定
         int successCount = 0;
         for (DivideClientVo divideClientVo : divideClientVos) {
-            boolean result = bind(pKeyId, divideClientVo, projectId, contractId);
-            if (result) {
+            // 创建新记录(不再需要检查是否存在,因为已经全部删除了)
+            WbsDivide wbsDivide = new WbsDivide();
+            wbsDivide.setPKeyId(pKeyId);
+            wbsDivide.setDivideId(divideClientVo.getId());
+            wbsDivide.setName(divideClientVo.getName());
+            wbsDivide.setParentId(divideClientVo.getParentId());
+            wbsDivide.setContractId(contractId);
+            wbsDivide.setDivideNum(divideClientVo.getDivideNum());
+            wbsDivide.setProjectId(projectId);
+            
+            if (save(wbsDivide)) {
                 successCount++;
             }
         }
         
-        log.info("批量绑定节点,pKeyId={}, 总数={}, 成功={}", pKeyId, divideClientVos.size(), successCount);
+        log.info("批量绑定节点完成,pKeyId={}, 总数={}, 成功={}", pKeyId, divideClientVos.size(), successCount);
         return successCount;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean unbindAllByPKeyId(Long pKeyId) {
+        Integer rows = baseMapper.unbindAllByPKeyId(pKeyId);
+        log.info("批量解绑pKeyId={}的记录,影响行数={}", pKeyId, rows);
+        return rows != null && rows > 0;
+    }
+
     @Override
     public List<String> getBindedDivideIds(Long pKeyId) {
         List<WbsDivide> list = baseMapper.selectByPKeyId(pKeyId);
@@ -202,7 +224,7 @@ public class WbsDivideServiceImpl extends BaseServiceImpl<WbsDivideMapper, WbsDi
                     // 3. 使用informationQueryClient.getInfoByWbsId获取InformationQuery对象
                     // 第二个参数classify传0
                     org.springblade.business.entity.InformationQuery informationQuery = 
-                            informationQueryClient.getInfoByWbsId(pKeyId, 0);
+                            informationQueryClient.getInfoByWbsId(pKeyId, 1);
                     
                     if (informationQuery != null && informationQuery.getPdfUrl() != null 
                             && !informationQuery.getPdfUrl().trim().isEmpty()) {

+ 18 - 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> {
 

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