Browse Source

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

LHB 4 days ago
parent
commit
2140091fb2
23 changed files with 1739 additions and 18 deletions
  1. 8 1
      blade-service-api/blade-business-api/src/main/java/org/springblade/business/entity/ArchiveFile.java
  2. 7 0
      blade-service-api/blade-business-api/src/main/java/org/springblade/business/entity/TrialSelfInspectionRecord.java
  3. 300 0
      blade-service-api/blade-business-api/src/main/java/org/springblade/business/utils/DigestUtil.java
  4. 69 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/InformationImportRecord.java
  5. 10 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/feign/ExcelTabClient.java
  6. 8 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/feign/ExcelTabClientFallBack.java
  7. 35 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/InformationImportRecordVO.java
  8. 15 0
      blade-service/blade-business/src/main/java/org/springblade/business/controller/TrialDetectionController.java
  9. 32 0
      blade-service/blade-business/src/main/java/org/springblade/business/feignClient/ArchiveFileClientImpl.java
  10. 2 2
      blade-service/blade-business/src/main/java/org/springblade/business/mapper/InformationQueryMapper.xml
  11. 3 0
      blade-service/blade-business/src/main/java/org/springblade/business/service/ITrialSelfInspectionRecordService.java
  12. 15 0
      blade-service/blade-business/src/main/java/org/springblade/business/service/impl/ArchiveFileServiceImpl.java
  13. 138 4
      blade-service/blade-business/src/main/java/org/springblade/business/service/impl/TrialSelfInspectionRecordServiceImpl.java
  14. 194 3
      blade-service/blade-business/src/main/java/org/springblade/business/utils/FileUtils.java
  15. 12 3
      blade-service/blade-e-visa/src/main/java/org/springblade/evisa/service/impl/EVDataServiceImpl.java
  16. 2 1
      blade-service/blade-e-visa/src/main/java/org/springblade/evisa/utils/PDFUtils.java
  17. 773 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/InformationImportRecordController.java
  18. 2 2
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeContractController.java
  19. 27 2
      blade-service/blade-manager/src/main/java/org/springblade/manager/feign/ExcelTabClientImpl.java
  20. 26 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/InformationImportRecordMapper.java
  21. 4 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/IExcelTabService.java
  22. 30 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/InformationImportRecordService.java
  23. 27 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/InformationImportRecordServiceImpl.java

+ 8 - 1
blade-service-api/blade-business-api/src/main/java/org/springblade/business/entity/ArchiveFile.java

@@ -342,8 +342,15 @@ public class ArchiveFile extends BaseEntity {
     @ApiModelProperty("是否锁定 1已锁定")
     private Integer isLock;
 
-
+    /**
+     * fileUrl md5值
+     */
+    @ApiModelProperty("fileUrl md5值")
     private String fileMd5;
+    /**
+     * pdfFileUrl md5值
+     */
+    @ApiModelProperty("pdfFileUrl md5值")
     private String pdfMd5;
 
     @ApiModelProperty("旧案卷ID")

+ 7 - 0
blade-service-api/blade-business-api/src/main/java/org/springblade/business/entity/TrialSelfInspectionRecord.java

@@ -92,4 +92,11 @@ public class TrialSelfInspectionRecord extends BaseEntity {
     @ApiModelProperty("基础信息")
     private String baseInfo;
 
+    @ApiModelProperty("记录表pdf")
+    private String recordPdfUrl;
+
+    @ApiModelProperty("报告表pdf")
+    private String reportPdfUrl;
+
+
 }

+ 300 - 0
blade-service-api/blade-business-api/src/main/java/org/springblade/business/utils/DigestUtil.java

@@ -0,0 +1,300 @@
+package org.springblade.business.utils;
+
+import java.io.*;
+import java.math.BigInteger;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 扩展的摘要算法工具类
+ * 新增对文件流、网络资源等的摘要计算支持
+ */
+public class DigestUtil {
+
+    private static final int BUFFER_SIZE = 8192;
+
+
+    /**
+     * 计算本地文件的MD5摘要
+     * @param filePath 文件路径
+     * @return 文件的MD5摘要
+     */
+    public static String md5OfFile(String filePath) throws IOException {
+        return digestOfFile(filePath, "MD5");
+    }
+
+    /**
+     * 计算本地文件的SHA-256摘要
+     * @param filePath 文件路径
+     * @return 文件的SHA-256摘要
+     */
+    public static String sha256OfFile(String filePath) throws IOException {
+        return digestOfFile(filePath, "SHA-256");
+    }
+
+    /**
+     * 计算本地文件的指定算法摘要
+     * @param filePath 文件路径
+     * @param algorithm 摘要算法
+     * @return 文件的摘要
+     */
+    public static String digestOfFile(String filePath, String algorithm) throws IOException {
+        Path path = Paths.get(filePath);
+        try (InputStream fis = Files.newInputStream(path)) {
+            return digestOfStream(fis, algorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("不支持的摘要算法: " + algorithm, e);
+        }
+    }
+
+    /**
+     * 计算MultipartFile的MD5摘要
+     * @param multipartFile MultipartFile对象
+     * @return 文件的MD5摘要
+     */
+    public static String md5OfMultipartFile(org.springframework.web.multipart.MultipartFile multipartFile) throws IOException {
+        return digestOfMultipartFile(multipartFile, "MD5");
+    }
+
+    /**
+     * 计算MultipartFile的SHA-256摘要
+     * @param multipartFile MultipartFile对象
+     * @return 文件的SHA-256摘要
+     */
+    public static String sha256OfMultipartFile(org.springframework.web.multipart.MultipartFile multipartFile) throws IOException {
+        return digestOfMultipartFile(multipartFile, "SHA-256");
+    }
+
+    /**
+     * 计算MultipartFile的指定算法摘要
+     * @param multipartFile MultipartFile对象
+     * @param algorithm 摘要算法
+     * @return 文件的摘要
+     */
+    public static String digestOfMultipartFile(org.springframework.web.multipart.MultipartFile multipartFile, String algorithm) throws IOException {
+        try (InputStream inputStream = multipartFile.getInputStream()) {
+            return digestOfStream(inputStream, algorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("不支持的摘要算法: " + algorithm, e);
+        }
+    }
+
+    /**
+     * 计算网络文件的MD5摘要
+     * @param fileUrl 网络文件URL
+     * @return 文件的MD5摘要
+     */
+    public static String md5OfUrl(String fileUrl) throws IOException {
+        return digestOfUrl(fileUrl, "MD5");
+    }
+
+    /**
+     * 计算网络文件的SHA-256摘要
+     * @param fileUrl 网络文件URL
+     * @return 文件的SHA-256摘要
+     */
+    public static String sha256OfUrl(String fileUrl) throws IOException {
+        return digestOfUrl(fileUrl, "SHA-256");
+    }
+
+    /**
+     * 计算网络文件的指定算法摘要
+     * @param fileUrl 网络文件URL
+     * @param algorithm 摘要算法
+     * @return 文件的摘要
+     */
+    public static String digestOfUrl(String fileUrl, String algorithm) throws IOException {
+        URL url = new URL(fileUrl);
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        connection.setRequestMethod("GET");
+        connection.connect();
+
+        try (InputStream inputStream = connection.getInputStream()) {
+            return digestOfStream(inputStream, algorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("不支持的摘要算法: " + algorithm, e);
+        } finally {
+            connection.disconnect();
+        }
+    }
+
+    /**
+     * 计算输入流的指定算法摘要
+     * @param inputStream 输入流
+     * @param algorithm 摘要算法
+     * @return 流的摘要
+     */
+    public static String digestOfStream(InputStream inputStream, String algorithm) throws IOException, NoSuchAlgorithmException {
+        MessageDigest md = MessageDigest.getInstance(algorithm);
+        try (DigestInputStream dis = new DigestInputStream(inputStream, md)) {
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (dis.read(buffer) != -1) {
+                // 读取数据的同时自动更新摘要
+            }
+        }
+        return bytesToHex(md.digest());
+    }
+
+
+    /**
+     * 计算大文件的摘要(适用于内存受限情况)
+     * @param filePath 文件路径
+     * @param algorithm 摘要算法
+     * @return 文件的摘要
+     */
+    public static String digestOfLargeFile(String filePath, String algorithm) throws IOException {
+        Path path = Paths.get(filePath);
+        try (FileChannel channel = FileChannel.open(path);
+             InputStream inputStream = Channels.newInputStream(channel)) {
+            return digestOfStream(inputStream, algorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("不支持的摘要算法: " + algorithm, e);
+        }
+    }
+
+    /**
+     * MD5摘要算法
+     * @param input 输入字符串
+     * @return 32位MD5摘要
+     */
+    public static String md5(String input) {
+        return digest(input, "MD5");
+    }
+
+    /**
+     * SHA-1摘要算法
+     * @param input 输入字符串
+     * @return SHA-1摘要
+     */
+    public static String sha1(String input) {
+        return digest(input, "SHA-1");
+    }
+
+    /**
+     * SHA-256摘要算法
+     * @param input 输入字符串
+     * @return SHA-256摘要
+     */
+    public static String sha256(String input) {
+        return digest(input, "SHA-256");
+    }
+
+    /**
+     * SHA-512摘要算法
+     * @param input 输入字符串
+     * @return SHA-512摘要
+     */
+    public static String sha512(String input) {
+        return digest(input, "SHA-512");
+    }
+
+    /**
+     * 通用摘要算法
+     * @param input 输入字符串
+     * @param algorithm 算法名称 MD5, SHA-1, SHA-256, SHA-512 ....
+     * @return 指定算法的摘要
+     */
+    private static String digest(String input, String algorithm) {
+        try {
+            MessageDigest md = MessageDigest.getInstance(algorithm);
+            byte[] hashBytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
+            return bytesToHex(hashBytes);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("不支持的摘要算法: " + algorithm, e);
+        }
+    }
+
+    /**
+     * 通用摘要算法
+     * @param input 输入字符串
+     * @param algorithm 算法名称
+     * @return 指定算法的摘要
+     */
+    public static String digest(byte[] input, String algorithm) {
+        try {
+            MessageDigest md = MessageDigest.getInstance(algorithm);
+            byte[] hashBytes = md.digest(input);
+            return bytesToHex(hashBytes);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("不支持的摘要算法: " + algorithm, e);
+        }
+    }
+
+    /**
+     * 字节数组转十六进制字符串
+     * @param bytes 字节数组
+     * @return 十六进制字符串
+     */
+    public static String bytesToHex(byte[] bytes) {
+        StringBuilder result = new StringBuilder();
+        for (byte b : bytes) {
+            result.append(String.format("%02x", b));
+        }
+        return result.toString();
+    }
+
+    /**
+     * 十六进制字符串转字节数组
+     * @param hex 十六进制字符串
+     * @return 字节数组
+     */
+    public static byte[] hexToBytes(String hex) {
+        int len = hex.length();
+        byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+                    + Character.digit(hex.charAt(i+1), 16));
+        }
+        return data;
+    }
+
+    /**
+     * 字节数组转Base64字符串
+     * @param bytes 字节数组
+     * @return Base64编码字符串
+     */
+    public static String bytesToBase64(byte[] bytes) {
+        return java.util.Base64.getEncoder().encodeToString(bytes);
+    }
+
+    /**
+     * Base64字符串转字节数组
+     * @param base64 Base64编码字符串
+     * @return 字节数组
+     */
+    public static byte[] base64ToBytes(String base64) {
+        return java.util.Base64.getDecoder().decode(base64);
+    }
+
+    /**
+     * 字节数组转十进制字符串
+     * @param bytes 字节数组
+     * @return 十进制字符串表示
+     */
+    public static String bytesToDecimal(byte[] bytes) {
+        BigInteger bi = new BigInteger(1, bytes);
+        return bi.toString(10);
+    }
+
+    /**
+     * 获取指定长度的摘要(截取前length位)
+     * @param input 输入字符串
+     * @param algorithm 算法名称
+     * @param length 截取长度
+     * @return 截取后的摘要
+     */
+    public static String digestWithLength(String input, String algorithm, int length) {
+        String digest = digest(input, algorithm);
+        return digest.substring(0, Math.min(digest.length(), length));
+    }
+}
+

+ 69 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/InformationImportRecord.java

@@ -0,0 +1,69 @@
+package org.springblade.manager.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("u_information_import_record")
+@ApiModel(value = "Dict对象", description = "Dict对象")
+public class InformationImportRecord implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("主键id")
+    @TableId(
+            value = "id",
+            type = IdType.ASSIGN_ID
+    )
+    private Long id;
+
+    @ApiModelProperty("导入文件名称")
+    private String fileName;
+
+    @ApiModelProperty("导入文件路径")
+    private String filePath;
+
+    @ApiModelProperty("合同段id")
+    private Long contractId;
+
+    @ApiModelProperty("节点id")
+    private Long nodeId;
+
+    @ApiModelProperty("目标节点id")
+    private Long targetId;
+
+    @ApiModelProperty("所属方")
+    private Integer classify;
+
+    @ApiModelProperty("导入时间")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("更新时间")
+    private LocalDateTime updateTime;
+
+    @ApiModelProperty("导入人")
+    private Long createUser;
+
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    @ApiModelProperty("进度")
+    private Integer process;
+
+    @ApiModelProperty("备注")
+    private String remark;
+
+    @ApiModelProperty("是否删除")
+    private Integer isDeleted;
+
+    @ApiModelProperty("用户名")
+    @TableField(exist = false)
+    private String createUserName;
+}

+ 10 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/feign/ExcelTabClient.java

@@ -99,4 +99,14 @@ public interface ExcelTabClient {
 
     @PostMapping(API_PREFIX + "/getTheLogPdInfo")
     void getTheLogPdInfo(@RequestParam String logPkeyId,  @RequestParam String nodePrimaryKeyId, @RequestParam String recordTime, @RequestParam String contractId,@RequestParam Long createUser) throws Exception;
+
+    /**
+     * 在线excel 保存
+     * @param excelEditCallback
+     */
+    @PostMapping(API_PREFIX + "/saveExcelTableLink")
+    boolean saveExcelTableLink(@RequestBody ExcelEditCallback excelEditCallback);
+
+    @PostMapping(API_PREFIX + "/feign/copeBussTab")
+    R copeBussTab(@RequestParam Long pKeyId, @RequestParam String header) throws Exception;
 }

+ 8 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/feign/ExcelTabClientFallBack.java

@@ -106,5 +106,13 @@ public class ExcelTabClientFallBack implements ExcelTabClient {
 
     }
 
+    @Override
+    public boolean saveExcelTableLink(ExcelEditCallback excelEditCallback) {
+        return false;
+    }
 
+    @Override
+    public R copeBussTab(Long pKeyId, String header) throws Exception {
+        return null;
+    }
 }

+ 35 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/InformationImportRecordVO.java

@@ -0,0 +1,35 @@
+package org.springblade.manager.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+public class InformationImportRecordVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("文件名")
+    private String fileName;
+
+    @ApiModelProperty("开始时间")
+    private String startTime;
+
+    @ApiModelProperty("结束时间")
+    private String endTime;
+
+    @ApiModelProperty("状态,0:未开始, 1:进行中, 2:成功, 3:失败")
+    private Integer status;
+
+    @ApiModelProperty("操作人ID")
+    private Long userId;
+
+    @ApiModelProperty("所属方,1:施工, 2:监理")
+    private Integer classify = 1;
+
+    @ApiModelProperty("合同段ID, 必填")
+    private Long contractId;
+
+}

+ 15 - 0
blade-service/blade-business/src/main/java/org/springblade/business/controller/TrialDetectionController.java

@@ -45,6 +45,7 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -848,4 +849,18 @@ public class TrialDetectionController extends BladeController {
         iTrialSelfInspectionRecordService.updateSort(dto);
         return R.success("成功");
     }
+
+    /**
+     * pdf文件下载
+     * @param ids 试验记录ids
+     * @param classify 1 施工、2 监理、3业主
+     * @param type 1 整份下载,2 报告表下载,3 记录表下载,4 委托单下载
+     * @param response 响应
+     */
+    @PostMapping("/self/download")
+    @ApiOperationSupport(order = 31)
+    @ApiOperation(value = "pdf文件下载", notes = "传入记录ids,classify:1 施工、2 监理、3业主, type:1 整份下载、2 报告表下载、3 记录表下载、4 委托单下载")
+    public void download(@RequestParam String ids, @RequestParam Integer classify, @RequestParam Integer type, HttpServletResponse response) {
+        iTrialSelfInspectionRecordService.download(ids, classify, type, response);
+    }
 }

+ 32 - 0
blade-service/blade-business/src/main/java/org/springblade/business/feignClient/ArchiveFileClientImpl.java

@@ -17,9 +17,11 @@ import org.springblade.business.feign.ArchiveFileClient;
 import org.springblade.business.mapper.ArchiveFileMapper;
 import org.springblade.business.service.IArchiveFileService;
 import org.springblade.business.service.ITaskService;
+import org.springblade.business.utils.DigestUtil;
 import org.springblade.business.vo.ArchiveFileVO;
 import org.springblade.common.utils.FileUtils;
 import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.tool.utils.StringUtil;
 import org.springblade.manager.entity.ContractInfo;
 import org.springblade.manager.enums.StorageTypeEnum;
 import org.springblade.manager.feign.ContractClient;
@@ -49,6 +51,9 @@ public class ArchiveFileClientImpl implements ArchiveFileClient {
 
     @Override
     public void saveArchiveFile(ArchiveFileVO vo) {
+        if (vo.getList() != null) {
+            vo.getList().forEach(ArchiveFileClientImpl::digestMd5);
+        }
         this.iArchiveFileService.saveArchiveFile(vo.getList());
     }
 
@@ -281,6 +286,9 @@ public class ArchiveFileClientImpl implements ArchiveFileClient {
 
     @Override
     public void addArchiveFile(List<ArchiveFile> files) {
+        for (ArchiveFile file : files) {
+            digestMd5(file);
+        }
         iArchiveFileService.saveBatch(files);
     }
 
@@ -310,6 +318,9 @@ public class ArchiveFileClientImpl implements ArchiveFileClient {
 
     @Override
     public void addArchiveFileEx(List<ArchiveFile> files) {
+        for (ArchiveFile file : files) {
+            digestMd5(file);
+        }
         iArchiveFileService.saveBatch(files);
     }
 
@@ -364,9 +375,27 @@ public class ArchiveFileClientImpl implements ArchiveFileClient {
 
     @Override
     public void saveArchiveFileByBIM(ArchiveFile archiveFile) {
+        digestMd5(archiveFile);
         iArchiveFileService.save(archiveFile);
     }
 
+    private static void digestMd5(ArchiveFile archiveFile) {
+        try {
+            if (StringUtil.isNotBlank(archiveFile.getFileUrl())) {
+                archiveFile.setFileMd5(DigestUtil.md5OfUrl(archiveFile.getFileUrl()));
+            }
+            if (StringUtil.isNotBlank(archiveFile.getPdfFileUrl())) {
+                if (Objects.equals(archiveFile.getPdfFileUrl(), archiveFile.getFileUrl())) {
+                    archiveFile.setPdfMd5(archiveFile.getFileMd5());
+                } else {
+                    archiveFile.setPdfMd5(DigestUtil.md5OfUrl(archiveFile.getPdfFileUrl()));
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
     @Override
     public List<ArchiveFile> getArchiveFileByArchiveIds(String archiveIds) {
         return iArchiveFileService.getArchiveFileByArchivesId(archiveIds,null);
@@ -471,6 +500,9 @@ public class ArchiveFileClientImpl implements ArchiveFileClient {
 
     @Override
     public void saveBatchArchiveFile(List<ArchiveFile> list) {
+        for (ArchiveFile file : list) {
+            digestMd5(file);
+        }
         iArchiveFileService.saveBatch(list);
     }
 

+ 2 - 2
blade-service/blade-business/src/main/java/org/springblade/business/mapper/InformationQueryMapper.xml

@@ -1022,13 +1022,13 @@
     </select>
 
     <update id="addCheckPdfInfoByNodeId" >
-        update u_information_query a set a.chek_status=1
+        update u_information_query a set a.chek_status=1, check_desc = ''
         where a.is_deleted = 0 and a.classify=#{classify} and a.wbs_id
         in( select b.p_key_id from m_wbs_tree_contract b where b.is_deleted = 0 and b.ancestors_p_id like CONCAT(CONCAT('%', #{ids}), '%') and b.is_deleted = 0) and a.status = 2
     </update>
 
     <update id="addCheckPdfInfoByIds">
-        update u_information_query set chek_status=1
+        update u_information_query set chek_status=1, check_desc = ''
         where status = 2 and is_deleted = 0 and classify=#{classify} and id in
         <foreach collection="ids" item="id" open="(" separator="," close=")">
             #{id}

+ 3 - 0
blade-service/blade-business/src/main/java/org/springblade/business/service/ITrialSelfInspectionRecordService.java

@@ -8,6 +8,7 @@ import org.springblade.core.mp.base.BaseService;
 import org.springblade.core.tool.api.R;
 import org.springframework.web.multipart.MultipartFile;
 
+import javax.servlet.http.HttpServletResponse;
 import java.io.FileNotFoundException;
 import java.util.List;
 
@@ -49,4 +50,6 @@ public interface ITrialSelfInspectionRecordService extends BaseService<TrialSelf
     Long saveBaseInfo(TrialSeleInspectionRecordInfoDTO vo);
 
     void updateSort(TrialSelfInspectionRecordPageDTO dto);
+
+    void download(String ids, Integer classify, Integer type, HttpServletResponse response);
 }

+ 15 - 0
blade-service/blade-business/src/main/java/org/springblade/business/service/impl/ArchiveFileServiceImpl.java

@@ -17,6 +17,7 @@ import org.springblade.business.dto.VolumeDto4;
 import org.springblade.business.entity.ArchiveFile;
 import org.springblade.business.entity.Task;
 import org.springblade.business.entity.TaskParallel;
+import org.springblade.business.utils.DigestUtil;
 import org.springblade.business.vo.ArchiveFileVO;
 import org.springblade.business.mapper.ArchiveFileMapper;
 import org.springblade.business.service.IArchiveFileService;
@@ -164,6 +165,20 @@ public class ArchiveFileServiceImpl extends BaseServiceImpl<ArchiveFileMapper, A
                 if (list.get(i).getRectification() != null && list.get(i).getRectification() == 1) {
                     list.get(i).setRectification(2);
                 }
+                try {
+                    if (list.get(i).getFileUrl() != null) {
+                        list.get(i).setFileMd5(DigestUtil.md5OfUrl(list.get(i).getFileUrl()));
+                    }
+                    if (list.get(i).getPdfFileUrl() != null) {
+                        if (Objects.equals(list.get(i).getPdfFileUrl(), list.get(i).getFileUrl())) {
+                            list.get(i).setPdfMd5(list.get(i).getFileMd5());
+                        } else {
+                            list.get(i).setPdfMd5(DigestUtil.md5OfUrl(list.get(i).getPdfFileUrl()));
+                        }
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
             }
         }
         // 删除oss文件

+ 138 - 4
blade-service/blade-business/src/main/java/org/springblade/business/service/impl/TrialSelfInspectionRecordServiceImpl.java

@@ -6,7 +6,6 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
@@ -31,6 +30,7 @@ import org.springblade.business.mapper.TrialSelfInspectionRecordMapper;
 import org.springblade.business.service.*;
 import org.springblade.business.utils.FileUtils;
 import org.springblade.business.utils.FileUtils2;
+import org.springblade.business.utils.PDFUtil;
 import org.springblade.business.utils.StringSPUtils;
 import org.springblade.business.vo.*;
 import org.springblade.business.wrapper.TrialSelfInspectionRecordWarpper;
@@ -41,8 +41,6 @@ import org.springblade.core.log.exception.ServiceException;
 import org.springblade.core.mp.base.BaseServiceImpl;
 import org.springblade.core.mp.support.Condition;
 import org.springblade.core.oss.model.BladeFile;
-import org.springblade.core.redis.cache.BladeRedis;
-import org.springblade.core.secure.BladeUser;
 import org.springblade.core.secure.utils.AuthUtil;
 import org.springblade.core.tool.api.R;
 import org.springblade.core.tool.utils.*;
@@ -71,6 +69,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
+import javax.servlet.http.HttpServletResponse;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.InputStream;
@@ -312,7 +311,7 @@ public class TrialSelfInspectionRecordServiceImpl extends BaseServiceImpl<TrialS
      * @throws FileNotFoundException
      */
     private String getMergePdfToTrialNew(Long contractId, Long nodeId, Integer type) throws FileNotFoundException {
-        String sql = "select pdf_url,e_visa_pdf_url from u_information_query where wbs_id='" + nodeId + "' and status in(0,1,2) and contract_id ='" + contractId + "' and classify = '" + type + "'";
+        String sql = "select pdf_url,e_visa_pdf_url from u_information_query where wbs_id='" + nodeId + "' and status in(0,1,2) and contract_id ='" + contractId + "' and classify = '" + type + "' order by status desc";
         List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
         if (maps.size() >= 1) {
             Map<String, Object> stringObjectMap = maps.get(0);
@@ -2540,4 +2539,139 @@ public class TrialSelfInspectionRecordServiceImpl extends BaseServiceImpl<TrialS
         }
     }
 
+    @Override
+    public void download(String ids, Integer classify, Integer type, HttpServletResponse response) {
+        List<TrialSelfInspectionRecord> recordList = baseMapper.selectList(Wrappers.<TrialSelfInspectionRecord>lambdaQuery().in(TrialSelfInspectionRecord::getId, Func.toLongList(ids)));
+//                .eq(TrialSelfInspectionRecord::getTaskStatus, "已审批"));
+        if (recordList.isEmpty()) {
+            throw new ServiceException("请选择已审批的记录");
+        }
+        List<String> pdfList = null;
+        List<String> deletedFileUrls = new ArrayList<>();
+        //打包下载
+        try {
+            if (type == 1) {
+                // 整份下载
+                List<String> tempUrls = new ArrayList<>();
+                recordList.forEach(record -> {
+                    //合并的pdfUrl
+                    try {
+                        String pdf = this.getMergePdfToTrialNew(record.getContractId(), record.getId(), classify);
+                        if (StringUtil.hasText(pdf)) {
+
+                            String name = "";
+                            if(StringUtils.isNotEmpty(record.getReportNo())){
+                                name = "[" + record.getReportNo() + "]";
+                            }
+                            if(StringUtils.isNotEmpty(record.getTrialProjectName())){
+                                name = name + record.getTrialProjectName();
+                            }
+                            name = name + "试验检测报告及附件";
+                            tempUrls.add(pdf + "@@@" + name.replaceAll("\\\\", "_").replaceAll("/", "_"));
+                            deletedFileUrls.add(pdf);
+                        }
+                    } catch (FileNotFoundException e) {
+                        e.printStackTrace();
+                    }
+                });
+                pdfList = tempUrls;
+            } else if (type == 2) {
+                // 报告表下载
+                pdfList = recordList.stream().map(item -> {
+                    boolean flag = !StringUtil.hasText(item.getReportPdfUrl()) && StringUtil.hasText(item.getPdfUrl()) && StringUtil.hasText(item.getReportNo());
+                    String[] strArr = new String[] {"报告编号", item.getReportNo()};
+                    String url = item.getPdfUrl();
+                    if ("已审批".equals(item.getTaskStatus())) {
+                        String sql = "select e_visa_pdf_url from u_information_query where wbs_id='" + item.getId() + "' and status in(0,1,2) and contract_id ='" + item.getContractId() + "' and classify = '" + classify + "' order by status desc limit  1";
+                        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
+                        if (!maps.isEmpty()) {
+                            Map<String, Object> stringObjectMap = maps.get(0);
+                            if (stringObjectMap.get("e_visa_pdf_url") != null) {
+                                url = (String)stringObjectMap.get("e_visa_pdf_url");
+                                if ( flag) {
+                                    return FileUtils.extractPdfPages(url, strArr) + "@@@" + item.getReportNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                                } else {
+                                    return FileUtils.extractPdfPages(url, item.getReportPdfUrl()) + "@@@" + item.getReportNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                                }
+                            }
+                        }
+                    }
+                    if (StringUtil.hasText(item.getReportPdfUrl())) {
+                        return item.getReportPdfUrl() + "@@@" + item.getReportNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                    } else if (flag) {
+                        return FileUtils.extractPdfPages(url, strArr) + "@@@" + item.getReportNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                    }
+                    return "";
+                }).filter(StringUtil::hasText).collect(Collectors.toList());
+            } else if (type == 3) {
+                // 记录表下载
+                pdfList = recordList.stream().map(item -> {
+                    boolean flag = !StringUtil.hasText(item.getRecordPdfUrl()) && StringUtil.hasText(item.getPdfUrl()) && StringUtil.hasText(item.getRecordNo());
+                    String[] strArr = new String[] {"记录编号", item.getRecordNo()};
+                    String url = item.getPdfUrl();
+                    if ("已审批".equals(item.getTaskStatus())) {
+                        String sql = "select e_visa_pdf_url from u_information_query where wbs_id='" + item.getId() + "' and status in(0,1,2) and contract_id ='" + item.getContractId() + "' and classify = '" + classify + "' order by status desc limit  1";
+                        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
+                        if (!maps.isEmpty()) {
+                            Map<String, Object> stringObjectMap = maps.get(0);
+                            if (stringObjectMap.get("e_visa_pdf_url") != null) {
+                                url = (String)stringObjectMap.get("e_visa_pdf_url");
+                                if ( flag) {
+                                    return FileUtils.extractPdfPages(url, strArr) + "@@@" + item.getRecordNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                                } else {
+                                    return FileUtils.extractPdfPages(url, item.getRecordPdfUrl()) + "@@@" + item.getRecordNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                                }
+                            }
+                        }
+                    }
+                    if (StringUtil.hasText(item.getRecordPdfUrl())) {
+                        return item.getRecordPdfUrl() + "@@@" + item.getRecordNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                    } else if (flag) {
+                        return FileUtils.extractPdfPages(url, strArr) + "@@@" + item.getRecordNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                    }
+                    return "";
+                }).filter(StringUtil::hasText).collect(Collectors.toList());
+            } else if (type == 4) {
+                // 委托单下载
+                Set<Long> entrustIds = recordList.stream().filter(record -> record.getEntrustId() != null && record.getEntrustId() > 0).map(TrialSelfInspectionRecord::getEntrustId).collect(Collectors.toSet());
+                if (entrustIds.isEmpty()) {
+                    throw new ServiceException("未找到委托单记录");
+                }
+                List<EntrustInfo> entrustInfoList = entrustInfoService.listByIds(entrustIds);
+                pdfList = entrustInfoList.stream()
+                        .map(item -> {
+                            String url = "";
+                            if (item.getEntrustEPdf() != null && StringUtil.hasText(item.getEntrustEPdf())) {
+                                url = item.getEntrustEPdf();
+                            } else if (item.getEntrustPdf() != null && StringUtil.hasText(item.getEntrustPdf())) {
+                                url = item.getEntrustPdf();
+                            } else {
+                                return  "";
+                            }
+                            return url + "@@@" + item.getEntrustNo().replaceAll("\\\\", "_").replaceAll("/", "_");
+                        }).filter(StringUtil::hasText).collect(Collectors.toList());
+            }
+            if (pdfList == null || pdfList.isEmpty()) {
+                if (type == 1) {
+                    throw new ServiceException("未找到试验自检记录的pdf");
+                } else if (type == 2) {
+                    throw new ServiceException("未找到报告表pdf");
+                } else if (type == 3) {
+                    throw new ServiceException("未找到记录表pdf");
+                } else if (type == 4) {
+                    throw new ServiceException("未找到委托单pdf");
+                } else {
+                    throw new ServiceException("未找到pdf");
+                }
+            }
+            FileUtils.batchDownloadFileToZip(pdfList, response);
+        } catch (Exception e)  {
+            e.printStackTrace();
+        } finally {
+            if (!deletedFileUrls.isEmpty()) {
+                newIOSSClient.removeFiles(deletedFileUrls);
+            }
+        }
+    }
+
 }

+ 194 - 3
blade-service/blade-business/src/main/java/org/springblade/business/utils/FileUtils.java

@@ -10,6 +10,8 @@ import com.itextpdf.text.pdf.PdfCopy;
 import com.itextpdf.text.pdf.PdfReader;
 import net.coobird.thumbnailator.Thumbnails;
 import org.apache.commons.lang.StringUtils;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
 import org.apache.poi.ss.usermodel.ClientAnchor;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.util.CellRangeAddress;
@@ -91,9 +93,12 @@ public class FileUtils {
                             fileName = array[1];
                             symbol = url.substring(url.lastIndexOf("."));
                         }
-
-                        //获取文件流
-                        inputStream = CommonUtil.getOSSInputStream(url);
+                        if (!url.contains("http://") && !url.contains("https://") && Files.exists(Paths.get(url))) {
+                            inputStream = Files.newInputStream(Paths.get(url));
+                        } else {
+                            //获取文件流
+                            inputStream = CommonUtil.getOSSInputStream(url);
+                        }
                         //转换
                         byte[] bytes = CommonUtil.InputStreamToBytes(inputStream);
 
@@ -740,4 +745,190 @@ public class FileUtils {
             throw new ServiceException("IO错误");
         }
     }
+
+
+    /**
+     * 从电签pdf中提取指定未电签pdf的页面,并生成新的pdf
+     */
+    public static String extractPdfPages(String eVisaPdfUrl, String pdfUrl){
+        if (eVisaPdfUrl == null || eVisaPdfUrl.trim().isEmpty() || eVisaPdfUrl.equals(pdfUrl)) {
+            return pdfUrl;
+        }
+        try (
+                InputStream eVisaIs = CommonUtil.getOSSInputStream(eVisaPdfUrl);
+                InputStream pdfIs = CommonUtil.getOSSInputStream(pdfUrl);
+                // 使用pdfbox读取电签pdf
+                PDDocument eVisaDocument = PDDocument.load(eVisaIs);
+                PDDocument pdfDocument = PDDocument.load(pdfIs);
+                PDDocument newDocument = new PDDocument();
+        ) {
+            int page = eVisaDocument.getNumberOfPages();
+            int page1 = pdfDocument.getNumberOfPages();
+            int j = 0;
+            for (int i = 0; i < page; i++) {
+                String eVisaText = getPdfContent(eVisaDocument, i + 1, i + 1);
+                if (j >= page1) {
+                    break;
+                }
+                String pdfText = getPdfContent(pdfDocument, j + 1, j + 1);
+                if (!StringUtils.isBlank(eVisaText) && calculateDiffRatio(eVisaText,pdfText) < 0.10) {
+                    newDocument.addPage(eVisaDocument.getPage(i));
+                    j++;
+                }
+            }
+            // 判断新的pdf是否为空,不为空则保存新的pdf
+            if (newDocument.getNumberOfPages() > 0) {
+                // 获取临时文件目录
+                String tempDir = System.getProperty("java.io.tmpdir");
+                String tempFile = tempDir + File.separator + "new_" + System.currentTimeMillis() + ".pdf";
+                File file = new File(tempFile);
+                try {
+                    newDocument.save(file);
+                    return tempFile;
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return pdfUrl;
+    }
+
+    public static String extractPdfPages(String eVisaPdfUrl, String[] strList){
+        if (eVisaPdfUrl == null || eVisaPdfUrl.trim().isEmpty()) {
+            return eVisaPdfUrl;
+        }
+        try (
+                InputStream eVisaIs = CommonUtil.getOSSInputStream(eVisaPdfUrl);
+                // 使用pdfbox读取电签pdf
+                PDDocument eVisaDocument = PDDocument.load(eVisaIs);
+                PDDocument newDocument = new PDDocument();
+        ) {
+            int page = eVisaDocument.getNumberOfPages();
+            for (int i = 0; i < page; i++) {
+                String eVisaText = getPdfContent(eVisaDocument, i + 1, i + 1);
+                if (!StringUtils.isBlank(eVisaText)) {
+                    boolean flag = true;
+                    for (String s : strList) {
+                        flag = eVisaText.contains(s);
+                        if (!flag) {
+                            break;
+                        }
+                    }
+                    if (flag) {
+                        newDocument.addPage(eVisaDocument.getPage(i));
+                    }
+                }
+            }
+            // 判断新的pdf是否为空,不为空则保存新的pdf
+            if (newDocument.getNumberOfPages() > 0) {
+                // 获取临时文件目录
+                String tempDir = System.getProperty("java.io.tmpdir");
+                String tempFile = tempDir + File.separator + "new_" + System.currentTimeMillis() + ".pdf";
+                File file = new File(tempFile);
+                try {
+                    newDocument.save(file);
+                    return tempFile;
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return eVisaPdfUrl;
+    }
+
+    /**
+     * 获取pdf 指定页面的内容
+     */
+    private static String getPdfContent(PDDocument doc,  int startPage, int endPage) {
+        try {
+            PDFTextStripper stripper = new PDFTextStripper();
+            stripper.setStartPage(startPage);
+            stripper.setEndPage(endPage);
+            return stripper.getText(doc);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "";
+        }
+    }
+    /**
+     * 计算两个文本之间的差异比例
+     * @param original 原始文本
+     * @param modified 修改后的文本
+     * @return 差异比例 (0.0 - 1.0),0表示完全相同,1表示完全不同
+     */
+    public static double calculateDiffRatio(String original, String modified) {
+        if (original == null && modified == null) {
+            return 0.0;
+        }
+
+        if (original == null || modified == null) {
+            return 1.0;
+        }
+
+        if (original.equals(modified)) {
+            return 0.0;
+        }
+
+        // 按行分割文本
+        String[] originalLines = original.split("\n", -1);
+        String[] modifiedLines = modified.split("\n", -1);
+
+        // 使用动态规划计算编辑距离
+        int editDistance = computeEditDistance(originalLines, modifiedLines);
+
+        // 计算最大可能的编辑距离
+        int maxLength = Math.max(originalLines.length, modifiedLines.length);
+
+        if (maxLength == 0) {
+            return 0.0;
+        }
+
+        // 返回差异比例
+        return (double) editDistance / maxLength;
+    }
+    /**
+     * 使用动态规划算法计算两个字符串数组之间的编辑距离(Levenshtein距离)
+     * @param original 原始字符串数组
+     * @param modified 修改后的字符串数组
+     * @return 编辑距离
+     */
+    private static int computeEditDistance(String[] original, String[] modified) {
+        int m = original.length;
+        int n = modified.length;
+
+        // 创建DP表
+        int[][] dp = new int[m + 1][n + 1];
+
+        // 初始化边界条件
+        for (int i = 0; i <= m; i++) {
+            dp[i][0] = i;
+        }
+
+        for (int j = 0; j <= n; j++) {
+            dp[0][j] = j;
+        }
+
+        // 填充DP表
+        for (int i = 1; i <= m; i++) {
+            for (int j = 1; j <= n; j++) {
+                if (original[i - 1].equals(modified[j - 1])) {
+                    dp[i][j] = dp[i - 1][j - 1]; // 字符相同,无需操作
+                } else {
+                    // 取三种操作的最小值+1
+                    dp[i][j] = 1 + Math.min(
+                            Math.min(dp[i - 1][j],     // 删除
+                                    dp[i][j - 1]),    // 插入
+                            dp[i - 1][j - 1]           // 替换
+                    );
+                }
+            }
+        }
+
+        return dp[m][n];
+    }
+
 }

+ 12 - 3
blade-service/blade-e-visa/src/main/java/org/springblade/evisa/service/impl/EVDataServiceImpl.java

@@ -96,8 +96,12 @@ public class EVDataServiceImpl implements EVDataService {
             List<String> eVisaConfigList = PDFUtils.getPdfSignIds(fileUrl, taskApp);
             if (eVisaConfigList == null || eVisaConfigList.size() == 0) {
                 //没有电签配置,默认当前任务为不签字审批,返回成功
-                taskApp.setSigState(2);
-                taskApp.setSignSmg("pdf未获取到关键字Id");
+                if(taskApp.getSigState() == 10) {
+                    taskApp.setSigState(2);
+                } else {
+                    taskApp.setSigState(2);
+                    taskApp.setSignSmg("pdf未获取到关键字Id");
+                }
                 SignBackPdfInfo(taskApp);
                 return;
             }
@@ -407,7 +411,12 @@ public class EVDataServiceImpl implements EVDataService {
                     }
                     this.jdbcTemplate.execute("delete from u_task_batch where id in(" + taskApp.getId()+")");
                 }else{
-                    this.jdbcTemplate.execute("update u_task_parallel set exe_count=(exe_count+1), e_visa_status=99,e_visa_content='" + taskApp.getSignSmg() + "' ,update_time=SYSDATE() where parallel_process_instance_id in (" + taskApp.getParallelProcessInstanceId() + ")");
+                    if (taskApp.getSigType() == 1) {
+                        this.jdbcTemplate.execute("update u_task_parallel set exe_count=(exe_count+1), e_visa_status=99,e_visa_content='" + taskApp.getSignSmg() + "' ,update_time=SYSDATE() where parallel_process_instance_id in (" + taskApp.getParallelProcessInstanceId() + ")");
+                    } else {
+                        // 签章失败
+                        this.jdbcTemplate.execute("update u_task_parallel set exe_count=(exe_count+1), meter_task_repeal_desc='" + taskApp.getSignSmg() + "' ,update_time=SYSDATE() where parallel_process_instance_id in (" + taskApp.getParallelProcessInstanceId() + ")");
+                    }
                     this.jdbcTemplate.execute("update u_task set status=1 ,update_time=SYSDATE() where id='" + taskApp.getTaskId() + "'");
                     if (totalCount >= 3) {
                         this.jdbcTemplate.execute("delete from u_task_batch where id in(" + taskApp.getId()+")");

+ 2 - 1
blade-service/blade-e-visa/src/main/java/org/springblade/evisa/utils/PDFUtils.java

@@ -116,7 +116,8 @@ public class PDFUtils {
             return unique;
         }catch (Exception e){
             e.printStackTrace();
-            System.out.println("pdf大小为0");
+            taskApp.setSigState(10);
+            taskApp.setSignSmg("解析pdf异常");
             return eVisaConfigList;
         }
     }

+ 773 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/InformationImportRecordController.java

@@ -0,0 +1,773 @@
+package org.springblade.manager.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import com.spire.xls.CellRange;
+import io.swagger.annotations.*;
+import lombok.AllArgsConstructor;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.poi.ss.usermodel.Comment;
+import org.apache.poi.ss.usermodel.Drawing;
+import org.apache.poi.ss.usermodel.Shape;
+import org.apache.poi.ss.util.CellAddress;
+import org.apache.poi.xssf.usermodel.XSSFComment;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.jetbrains.annotations.NotNull;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springblade.business.entity.InformationQuery;
+import org.springblade.common.constant.CommonConstant;
+import org.springblade.common.utils.SnowFlakeUtil;
+import org.springblade.common.utils.SystemUtils;
+import org.springblade.core.boot.ctrl.BladeController;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.IoUtil;
+import org.springblade.core.tool.utils.ResourceUtil;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.manager.dto.ServiceUserDto;
+import org.springblade.manager.entity.*;
+import org.springblade.manager.feign.ExcelTabClient;
+import org.springblade.manager.feign.ExcelTabClientImpl;
+import org.springblade.manager.service.IExcelTabService;
+import org.springblade.manager.service.IWbsTreeContractService;
+import org.springblade.manager.service.InformationImportRecordService;
+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.vo.InformationImportRecordVO;
+import org.springblade.system.cache.ParamCache;
+import org.springblade.system.user.feign.IUserClient;
+import org.springframework.beans.BeanUtils;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static java.util.stream.Collectors.toMap;
+
+/**
+ * 资料导入及记录查询控制器
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/informationImportRecord")
+@Api(tags = "资料导入及记录查询接口")
+public class InformationImportRecordController extends BladeController {
+
+    private final IExcelTabService excelTabService;
+
+    private final IWbsTreeContractService wbsTreeContractService;
+    private final WbsTreeContractServiceImpl wbsTreeContractServiceImpl;
+    private final ExcelTabClient excelTabClient;
+    private final InformationImportRecordService informationImportRecordService;
+    private final IUserClient userClient;
+    private final JdbcTemplate jdbcTemplate;
+
+    private static final Logger logger = LoggerFactory.getLogger(InformationImportRecordController.class);
+    private static final java.util.concurrent.atomic.AtomicBoolean RUNNING = new java.util.concurrent.atomic.AtomicBoolean(false);
+    private static final java.util.concurrent.ExecutorService IMPORT_EXECUTOR = new java.util.concurrent.ThreadPoolExecutor(
+            Math.max(2, Runtime.getRuntime().availableProcessors()),
+            Math.max(4, Runtime.getRuntime().availableProcessors() * 2),
+            60L,
+            java.util.concurrent.TimeUnit.SECONDS,
+            new java.util.concurrent.LinkedBlockingQueue<>(200),
+            new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()
+    );
+
+
+    @GetMapping("/page")
+    @ApiOperationSupport(order = 1)
+    @ApiOperation(value = "获取资料导入记录")
+    public R<IPage<InformationImportRecord>> page(InformationImportRecordVO record, Query query) {
+        LambdaQueryWrapper<InformationImportRecord> wrapper = Wrappers.lambdaQuery();
+        wrapper.eq(InformationImportRecord::getContractId, record.getContractId());
+        if (StringUtil.hasText(record.getFileName())) {
+            wrapper.like(InformationImportRecord::getFileName, record.getFileName());
+        }
+        if (record.getStatus() != null && (record.getStatus() >= 0 && record.getStatus() <= 3)) {
+            wrapper.eq(InformationImportRecord::getStatus, record.getStatus());
+        }
+        if (record.getUserId() != null && record.getUserId() > 0) {
+            wrapper.eq(InformationImportRecord::getCreateUser, record.getUserId());
+        }
+        if (record.getClassify() != null && (record.getClassify() == 1 || record.getClassify() == 2)) {
+            wrapper.eq(InformationImportRecord::getClassify, record.getClassify());
+        }
+        if (record.getStartTime() != null && record.getEndTime() != null) {
+            if (record.getStartTime().trim().matches("^\\d{4}-\\d{2}-\\d{2}$") && record.getEndTime().trim().matches("^\\d{4}-\\d{2}-\\d{2}$")) {
+                wrapper.between(InformationImportRecord::getCreateTime, record.getStartTime() + " 00:00:00", record.getEndTime() + " 23:59:59");
+            }
+        }
+        wrapper.orderByDesc(InformationImportRecord::getCreateTime);
+        IPage<InformationImportRecord> page = informationImportRecordService.page(Condition.getPage(query), wrapper);
+        List<InformationImportRecord> records = page.getRecords();
+        if (records != null && !records.isEmpty()) {
+            String userIds = records.stream().filter(item -> item.getCreateUser() != null && item.getCreateUser() > 0).map(item -> item.getCreateUser() + "").collect(Collectors.joining(","));
+            List<ServiceUserDto> userList = jdbcTemplate.query("select id as userId, name as userName from blade_user where id in (" + userIds + ")", new BeanPropertyRowMapper<>(ServiceUserDto.class));
+            Map<String, String> map = userList.stream().collect(Collectors.toMap(ServiceUserDto::getUserId, ServiceUserDto::getUserName, (k1, k2) -> k1));
+            records.forEach(item -> {
+                String username = map.get(item.getCreateUser() + "");
+                item.setCreateUserName(username);
+                item.setRemark(item.getRemark() == null ? "" : item.getRemark().split("::")[0]);
+            });
+        }
+        return R.data(page);
+    }
+
+    @GetMapping("/users")
+    @ApiOperationSupport(order = 1)
+    @ApiOperation(value = "获取资料导入操作人列表", notes = "传入合同段id,返回 id, name")
+    public R<List<ServiceUserDto>> page(@RequestParam Long contractId) {
+        List<ServiceUserDto> query = jdbcTemplate.query("select distinct b.id as userId, b.name as userName from u_information_import_record a left join blade_user b on a.create_user = b.id where contract_id = " + contractId,
+                new BeanPropertyRowMapper<>(ServiceUserDto.class));
+        return R.data(query);
+    }
+
+    @PostMapping("/import")
+    @ApiOperationSupport(order = 2)
+    @ApiOperation(value = "客户端-导入多sheet excel到对应节点下的表单", notes = "传入节点ID、分类和多sheet excel文件")
+    public R<Object> importNodeExcel(@RequestPart List<MultipartFile> files, @RequestParam Long nodeId, @RequestParam Integer classify) throws Exception {
+        if (files.isEmpty()) {
+            return R.fail("请选择文件");
+        }
+        if (nodeId == null) {
+            return R.fail("请选择节点");
+        }
+        List<WbsTreeContract> query = jdbcTemplate.query("select * from m_wbs_tree_contract where is_deleted = 0 and p_key_id = " + nodeId, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+        if (query.isEmpty()) {
+            return R.fail("请选择节点");
+        }
+        WbsTreeContract wbsTreeContract = query.get(0);
+        long contractId;
+        try {
+            contractId = Long.parseLong(wbsTreeContract.getContractId());
+        } catch (NumberFormatException e) {
+            return R.fail("未找到对应的合同段id");
+        }
+        if (classify == null || classify < 0 || classify > 2) {
+            return R.fail("请选择所属方");
+        }
+        String localPath = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+        String base = localPath + File.separator + "import" + File.separator;
+        {
+            // 如果文件夹不存在则创建
+            File file1 = new File(base);
+            if (!file1.exists()) {
+                boolean mkdirs = file1.mkdirs();
+                if (!mkdirs) {
+                    return R.fail("创建文件夹失败");
+                }
+            }
+        }
+        LocalDateTime now = LocalDateTime.now();
+        List<InformationImportRecord> records = new ArrayList<>();
+        for (MultipartFile file : files) {
+            // 将file保存到本地
+            Long id = SnowFlakeUtil.getId();
+            String filename = file.getOriginalFilename();
+            if (filename == null) {
+                continue;
+            }
+            InputStream is = file.getInputStream();
+            // 避免重名以及文件名过长的问题
+            String path = base + id + filename.substring(filename.lastIndexOf("."));
+
+            Files.write(Paths.get(path), IOUtils.toByteArray(is));
+            String name = getFileNameBySheet(path);
+
+            // 创建记录
+            InformationImportRecord record = new InformationImportRecord();
+            record.setId(id);
+            record.setNodeId(nodeId);
+            record.setContractId(contractId);
+            record.setClassify(classify);
+            record.setFileName(name == null ? filename.substring(0, filename.lastIndexOf(".")) : name.replace(".xlsx", "").replace(".xls", ""));
+            record.setFilePath(path);
+            record.setCreateTime(now);
+            record.setCreateUser(AuthUtil.getUserId());
+            record.setRemark("::" + filename);
+            records.add(record);
+        }
+        informationImportRecordService.saveBatch(records);
+        importInformationData();
+        return R.data(true);
+    }
+
+    private static String getFileNameBySheet(String excelPath) {
+        // 解析excel, 读取excel中的批注
+        try (InputStream is = Files.newInputStream(Paths.get(excelPath));XSSFWorkbook workbook = new XSSFWorkbook(is);) {
+            for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
+                XSSFSheet sheet = workbook.getSheetAt(i);
+                Map<CellAddress, XSSFComment> map = sheet.getCellComments();
+                for (Map.Entry<CellAddress, XSSFComment> entry : map.entrySet()) {
+                    XSSFComment value = entry.getValue();
+                    if (value != null && value.getString() != null) {
+                        String string = value.getString().getString();
+                        if (string != null && string.contains("》")) {
+                            return string.replaceAll("\r\n", "");
+                        }
+                    }
+                }
+            }
+            return null;
+        } catch (Exception e) {
+            logger.error("获取文件名失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 初始化: 匹配节点,创建子节点
+     * @param id 记录ID
+     * @param node 节点
+     */
+    public void init(@NotNull Long id, @NotNull WbsTreeContract node) {
+        InformationImportRecord record = informationImportRecordService.getById(id);
+        if (record == null || record.getStatus() != 0) {
+            logger.info("记录[{}]已经初始化,已跳过", id);
+            return;
+        }
+        String name = record.getFileName();
+        String[] split = name.split("》");
+        String lastName = split[split.length - 1];
+        String newName = "";
+        String[] split1 = lastName.split("【");
+        if (split1.length  > 1) {
+            lastName = split1[0];
+            newName = split1[1].replace("】", "");
+        }
+        WbsTreeContract target = null;
+        if (split.length > 1) {
+            // 获取节点下的所有子节点
+            Long pId = node.getPKeyId();
+            int i = 0;
+            for (; i < split.length - 1; i++) {
+                // 使用 like 是因为Windows 和 excel 不支持 ‘/’ 等特殊字符,下载模板时将特殊字符转为了 '_', 而 '_' 在 MySQL like中表示一个任意字符, 所以这里使用 like
+                List<WbsTreeContract> query = jdbcTemplate.query("select * from m_wbs_tree_contract where is_deleted = 0 and p_id = " + pId + " and full_name like '" + split[i] + "'",
+                        new BeanPropertyRowMapper<>(WbsTreeContract.class));
+                if ((query.isEmpty() || query.get(0) == null)) {
+                    if (target != null) {
+                        break;
+                    }
+                    if (i == split.length - 2 ) {
+                        if (node.getFullName() != null && node.getFullName().replaceAll("[\\\\/:*?\"<>|]", "_").equals(split[i]) ||
+                            node.getNodeName() != null && node.getNodeName().replaceAll("[\\\\/:*?\"<>|]", "_").equals(split[i])) {
+                            target = node;
+                        }
+                    }
+                    continue;
+                }
+                target = query.get(0);
+                pId = target.getPKeyId();
+                if (pId == null ) {
+                    break;
+                }
+            }
+            if (target == null || i < split.length - 2) {
+                informationImportRecordService.updateProcess(record.getId(), null, 3, "节点未找到-客户端::" + split[i - 1], record.getUpdateTime());
+                return;
+            }
+        } else {
+            target = node;
+        }
+
+        {
+            // todo 判断target节点下是否有该节点,如果有是否有资料,没有的 监理和施工
+            List<WbsTreeContract> query = jdbcTemplate.query("select * from m_wbs_tree_contract where is_deleted = 0 and p_id = " + target.getPKeyId() + " and full_name = '" + (StringUtil.hasText(newName) ? newName : lastName) + "'",
+                    new BeanPropertyRowMapper<>(WbsTreeContract.class));
+            if (!query.isEmpty()) {
+                for (WbsTreeContract wbsTreeContract : query) {
+                    List<InformationQuery> informationQueries = jdbcTemplate.query("select * from u_information_query where is_deleted = 0 and contract_id = " + record.getContractId() + " and wbs_id = " + wbsTreeContract.getPKeyId()
+                                    + " and classify = " + record.getClassify(), new BeanPropertyRowMapper<>(InformationQuery.class));
+                    if (informationQueries.isEmpty()) {
+                        record.setTargetId(wbsTreeContract.getPKeyId());
+                    }
+                }
+            }
+            if (record.getTargetId() != null && record.getTargetId() > 0) {
+                informationImportRecordService.update(Wrappers.<InformationImportRecord>lambdaUpdate().eq(InformationImportRecord::getId, record.getId()).set(InformationImportRecord::getStatus, 1)
+                        .set(InformationImportRecord::getTargetId, record.getTargetId()).set(InformationImportRecord::getProcess, 20));
+                return;
+            }
+        }
+
+        List<WbsTreePrivate> query = jdbcTemplate.query("select * from m_wbs_tree_private where is_deleted = 0 and project_id = " + target.getProjectId() + " and node_name like '" + lastName + "'",
+                new BeanPropertyRowMapper<>(WbsTreePrivate.class));
+        if (query.isEmpty()) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, "节点未找到-后管::" + target.getFullName(), record.getUpdateTime());
+            return;
+        }
+        WbsTreePrivate wbsTreePrivate = null;
+        List<WbsTreePrivate> tables = null;
+        for (WbsTreePrivate treePrivate : query) {
+            tables = jdbcTemplate.query("select * from m_wbs_tree_private where is_deleted = 0 and type in (2,10) and p_id = " + treePrivate.getPKeyId(), new BeanPropertyRowMapper<>(WbsTreePrivate.class));
+            if (tables.isEmpty()) {
+                tables = jdbcTemplate.query("select * from m_wbs_tree_private where is_deleted = 0 and type in (2,10) and parent_id = " + treePrivate.getId() + " and project_id = " + target.getProjectId(), new BeanPropertyRowMapper<>(WbsTreePrivate.class));
+            }
+            if (!tables.isEmpty()) {
+                wbsTreePrivate = treePrivate;
+                break;
+            }
+            if (treePrivate.getNodeType() != null && treePrivate.getNodeType() == 6) {
+                wbsTreePrivate = treePrivate;
+            }
+        }
+        if (wbsTreePrivate == null) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, "节点未找到-后管::" + target.getFullName(), record.getUpdateTime());
+            return;
+        }
+        if (tables.isEmpty()) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, "表单未找到-后管::" + wbsTreePrivate.getNodeName(), record.getUpdateTime());
+            return;
+        }
+        informationImportRecordService.updateProcess(record.getId(), 7, 1, null, record.getUpdateTime());
+
+        // 在target节点下创建子节点及表单
+        Long parentPKeyId = SnowFlakeUtil.getId();
+        Long parentId = SnowFlakeUtil.getId();
+        String ancestors = target.getAncestors() == null ? target.getId() + "" : target.getAncestors() + "," + target.getId();
+        String ancestorsPId = target.getAncestorsPId() == null ? target.getPKeyId() + "" : target.getAncestorsPId() + "," + target.getPKeyId();
+
+        List<WbsTreeContract> saveList = new ArrayList<>();
+        {
+            WbsTreeContract newData = new WbsTreeContract();
+            BeanUtils.copyProperties(wbsTreePrivate, newData);
+
+            //重塑pKeyId、id和parentId
+            newData.setPKeyId(parentPKeyId);
+            newData.setId(parentId);
+            newData.setParentId(target.getId());
+            newData.setPId(target.getPKeyId());
+            newData.setAncestors(ancestors);
+            newData.setAncestorsPId(ancestorsPId);
+
+            //设置合同段等信息
+            newData.setWbsType(target.getWbsType());
+            newData.setContractId(target.getContractId());
+            newData.setContractIdRelation(target.getContractIdRelation());
+            newData.setContractType(target.getContractType());
+            newData.setCreateTime(new Date());
+            newData.setIsTypePrivatePid(wbsTreePrivate.getPKeyId());
+            if (wbsTreePrivate.getType() != null && new Integer("2").equals(wbsTreePrivate.getType())) {
+                newData.setIsBussShow(1);
+            }
+            //获取当前所有复制的节点的最大sort
+            newData.setSort(ObjectUtils.isNotEmpty(wbsTreePrivate.getSort()) ? wbsTreePrivate.getSort() : 0);
+
+            //记录旧ID
+            newData.setOldId(wbsTreePrivate.getId().toString());
+
+            newData.setFullName(StringUtil.hasText(newName) ? newName : lastName);
+
+            saveList.add(newData);
+        }
+
+        //处理数据
+        for (WbsTreePrivate half : tables) {
+            //处理合同段数据
+            WbsTreeContract newData = new WbsTreeContract();
+            BeanUtils.copyProperties(half, newData);
+
+            //重塑pKeyId、id和parentId
+            newData.setPKeyId(SnowFlakeUtil.getId());
+            newData.setId(SnowFlakeUtil.getId());
+            newData.setParentId(parentId);
+            newData.setPId(parentPKeyId);
+            newData.setAncestors(ancestors + "," + parentId);
+            newData.setAncestorsPId(ancestorsPId + "," + parentPKeyId);
+
+            //记录旧ID
+            newData.setOldId(half.getId().toString());
+            //设置合同段等信息
+            newData.setWbsType(target.getWbsType());
+            newData.setContractId(target.getContractId());
+            newData.setContractIdRelation(target.getContractIdRelation());
+            newData.setContractType(target.getContractType());
+            newData.setCreateTime(new Date());
+            newData.setIsTypePrivatePid(half.getPKeyId());
+            if (half.getType() != null && new Integer("2").equals(half.getType())) {
+                if (half.getDefaultConceal() != null &&  half.getDefaultConceal() == 1) {
+                    // 后续如果此表格中有数据再修改成 1
+                    newData.setIsBussShow(2);
+                } else {
+                    //2023年8月1日14:41:03更改需求,isBussShow默认=1
+                    newData.setIsBussShow(1);
+                }
+            }
+            //获取当前所有复制的节点的最大sort
+            newData.setSort(ObjectUtils.isNotEmpty(half.getSort()) ? half.getSort() : 0);
+
+            //设置名称, 后续根据sheet 名称进行设置
+            newData.setFullName(half.getFullName());
+
+            //设置到保存集合中
+            saveList.add(newData);
+        }
+        wbsTreeContractService.saveBatch(saveList);
+        informationImportRecordService.update(Wrappers.<InformationImportRecord>lambdaUpdate().eq(InformationImportRecord::getId, record.getId())
+                .set(InformationImportRecord::getTargetId, parentPKeyId).set(InformationImportRecord::getProcess, 20));
+    }
+
+    public void importData(@NotNull Long id) {
+        InformationImportRecord record = informationImportRecordService.getById(id);
+        if (record == null || record.getStatus() != 1) {
+            logger.info("记录[{}]已经不存在或者状态错误,已跳过", id);
+            return;
+        }
+        if (record.getTargetId() == null || record.getTargetId() <= 0) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::找不到目标节点", record.getUpdateTime());
+            return;
+        }
+
+        Path path1 = Paths.get(record.getFilePath());
+        if (!Files.exists(path1)) {
+            logger.info("文件[{}]不存在,已跳过", record.getFilePath());
+            informationImportRecordService.updateProcess(record.getId(), null, 3,  "文件不存在", record.getUpdateTime());
+            return;
+        }
+        try (InputStream is = Files.newInputStream(path1)) {
+            importNodeExcel(is, record);
+            informationImportRecordService.updateProcess(record.getId(), 65, null, null, record.getUpdateTime());
+        } catch (Exception e) {
+            logger.error("文件[{}]读取异常,已跳过", record.getFilePath(), e);
+            informationImportRecordService.updateProcess(record.getId(), null, 3, "文件读取异常", record.getUpdateTime());
+        }
+    }
+
+    public void saveAgain(Long  id, String projectId) {
+        InformationImportRecord record = informationImportRecordService.getById(id);
+        if (record == null || record.getStatus() != 1) {
+            logger.info("记录[{}]已经不存在或者状态错误,已跳过", id);
+            return;
+        }
+        if (record.getTargetId() == null || record.getTargetId() <= 0) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::找不到目标节点", record.getUpdateTime());
+            return;
+        }
+        try {
+            excelTabClient.synPDFInfo(record.getContractId() + "", record.getTargetId() + "", record.getClassify() + "", projectId, userClient.getTokenByUser("admin"));
+        } catch (Exception e) {
+            logger.error("生成pdf失败", e);
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::生成pdf失败", record.getUpdateTime());
+            return;
+        }
+        // 检查是否成功
+        List<InformationQuery> query = jdbcTemplate.query("select * from u_information_query where contract_id = " + record.getContractId() + " and wbs_id = " + record.getTargetId() + " and classify = " + record.getClassify(),
+                new BeanPropertyRowMapper<>(InformationQuery.class));
+        if (!query.isEmpty()) {
+            informationImportRecordService.updateProcess(record.getId(), 100, 2, null, record.getUpdateTime());
+        }
+    }
+
+    public void importNodeExcel(InputStream is, InformationImportRecord record) throws Exception {
+
+        // 1. 获取节点下所有表单,并建立 sheet名 -> WbsTreeContract 的映射(处理特殊字符)
+        Object[] params;
+        if (record.getClassify() == 1) {
+            params = new Object[]{record.getTargetId(), 1, 2, 3};
+        } else {
+            params = new Object[]{record.getTargetId(), 4, 5, 6};
+        }
+        List<WbsTreeContract> wbsTreeContracts = 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);
+
+        if (wbsTreeContracts.isEmpty()) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::没有找到对应的表单数据", record.getUpdateTime());
+            return;
+        }
+        // 2. 加载上传的多sheet Excel文件
+        com.spire.xls.Workbook mainWorkbook = new com.spire.xls.Workbook();
+        try {
+            mainWorkbook.loadFromStream(is);
+        } catch (Exception e) {
+            logger.error("加载Excel文件失败", e);
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::Excel文件解析失败", record.getUpdateTime());
+            return ;
+        }
+
+        // 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 = excelTabClient.copeBussTab(contract.getPKeyId(), userClient.getTokenByUser("admin"));
+                        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 = WbsTreeContractController.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 = excelTabService.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();
+    }
+
+    /**
+     * 复用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 = WbsTreeContractController.getKeyNameFromChildElement(td1); // 复用原方法获取key
+                    if (StringUtils.isNotEmpty(keyName)) {
+                        String divValue = td2.text();
+                        if (StringUtils.isNotEmpty(divValue)) {
+                            // 日期范围处理
+                            if (WbsTreeContractController.parseDateRange(divValue).size() == 2) {
+                                resultMap.put(keyName, WbsTreeContractController.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();
+            }
+        }
+        return resultMap;
+    }
+
+
+    @Scheduled(fixedDelay = 1000L * 60 * 10)
+    @Async
+    public void importInformationData() {
+        if (!SystemUtils.isLinux() || !RUNNING.compareAndSet(false, true)) {
+            return;
+        }
+        try {
+            List<InformationImportRecord> records = informationImportRecordService.list(Wrappers.<InformationImportRecord>lambdaQuery()
+                            .select(InformationImportRecord::getId, InformationImportRecord::getNodeId, InformationImportRecord::getUpdateTime, InformationImportRecord::getProcess)
+                    .in(InformationImportRecord::getStatus, 0, 1).last(" order by create_time  limit 100"));
+            if (!records.isEmpty()) {
+                for (InformationImportRecord record : records) {
+                    IMPORT_EXECUTOR.submit(() -> {
+                        try {
+                            WbsTreeContract node = wbsTreeContractService.getOne(Wrappers.<WbsTreeContract>lambdaQuery().eq(WbsTreeContract::getPKeyId, record.getNodeId()));
+                            if (node == null) {
+                                informationImportRecordService.updateProcess(record.getId(), null, 3, "节点未找到-客户端", record.getUpdateTime());
+                                return;
+                            }
+                            if (record.getProcess() <= 15) {
+                                init(record.getId(), node);
+                            }
+                            if (record.getProcess() < 60) {
+                                importData(record.getId());
+                            }
+                            if (record.getProcess() < 100) {
+                                saveAgain(record.getId(), node.getProjectId());
+                            }
+                        } catch (Exception e) {
+                            logger.error("导入数据失败", e);
+                        }
+                    });
+                }
+            }
+            List<InformationImportRecord> list = informationImportRecordService.list(Wrappers.<InformationImportRecord>lambdaQuery().select(InformationImportRecord::getId, InformationImportRecord::getFilePath)
+                    .eq(InformationImportRecord::getStatus, 2).apply("update_time < DATE_SUB(NOW(), INTERVAL 3 DAY)"));
+            for (InformationImportRecord record : list) {
+                try {
+                    Files.deleteIfExists(Paths.get(record.getFilePath()));
+                } catch (Exception e) {
+                    logger.error("删除文件失败", e);
+                }
+            }
+        } finally {
+            RUNNING.set(false);
+        }
+    }
+}

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

@@ -1035,7 +1035,7 @@ public class WbsTreeContractController extends BladeController {
         return sql;
     }
 
-    public Map<String,String> getDataMap(Map<String, Object> originalMap){
+    public static Map<String,String> getDataMap(Map<String, Object> originalMap){
         // 用于存储合并后的结果
         Map<String, String> mergedMap = new HashMap<>();
 
@@ -1736,7 +1736,7 @@ public class WbsTreeContractController extends BladeController {
         return false;
     }
 
-    private static String getKeyNameFromChildElement(Element element) {
+    public static String getKeyNameFromChildElement(Element element) {
         //TODO Element UI的时间标签待补全
         String[] tagNames = {"el-input", "el-date-picker", "el-time-picker", "hc-form-select-search", "hc-table-form-upload", "hc-form-checkbox-group", "el-radio-group", "el-select"};
         for (String tagName : tagNames) {

+ 27 - 2
blade-service/blade-manager/src/main/java/org/springblade/manager/feign/ExcelTabClientImpl.java

@@ -20,6 +20,7 @@ import org.springblade.business.feign.ContractLogClient;
 import org.springblade.business.feign.EntrustInfoServiceClient;
 import org.springblade.business.feign.InformationQueryClient;
 import org.springblade.common.utils.CommonUtil;
+import org.springblade.core.log.exception.ServiceException;
 import org.springblade.manager.util.DataStructureFormatUtils;
 import org.springblade.common.utils.SnowFlakeUtil;
 import org.springblade.core.oss.model.BladeFile;
@@ -470,18 +471,37 @@ public class ExcelTabClientImpl implements ExcelTabClient {
     @Override
     public R saveReEntrustTabData(ReSigningEntrustDto dto, String header) {
         WbsTreePrivate wbsTreePrivate = jdbcTemplate.queryForObject("select * from m_wbs_tree_private where p_key_id=" + dto.getNodeId(), new BeanPropertyRowMapper<>(WbsTreePrivate.class));
+        if (wbsTreePrivate == null) {
+            throw new ServiceException("找不到节点信息");
+        }
+        // 合同段信息
+        ContractInfo contractInfo = contractInfoService.getById(dto.getContractId());
+        if (contractInfo == null) {
+            throw new ServiceException("合同段信息为null");
+        }
+        Long pkeyId = 0L;
+        if (contractInfo.getContractType() == 2) { //3 监理
+            pkeyId = wbsTreePrivate.getJlerTreeId() ;
+        } else if (contractInfo.getContractType() == 3 || contractInfo.getContractType() == 8) { //业主
+            pkeyId = wbsTreePrivate.getYzerTreeId();
+        } else {
+            return R.fail("该合同段没有委托单权限业务");
+        }
         Boolean isRemove=true;
-        List<Map<String, Object>> list = excelTabService.getBussDataInfoTrialentrust(Long.parseLong(dto.getEntrustId()), wbsTreePrivate.getJlerTreeId(), Long.parseLong(dto.getContractId()), null, null,isRemove);
+        List<Map<String, Object>> list = excelTabService.getBussDataInfoTrialentrust(Long.parseLong(dto.getEntrustId()), pkeyId, Long.parseLong(dto.getContractId()), null, null,isRemove);
         Map<String, Object> map = list.get(0);
         if(!map.containsKey("contractId")){
             map.put("contractId",dto.getContractId());
         }
         if(!map.containsKey("nodeErTreeId")){
-            map.put("nodeErTreeId",wbsTreePrivate.getJlerTreeId());
+            map.put("nodeErTreeId",pkeyId);
         }
         if(!map.containsKey("nodeId")){
             map.put("nodeId",dto.getNodeId());
         }
+        if(!map.containsKey("id")){
+            map.put("id",dto.getEntrustId());
+        }
         JSONObject jsonObject = new JSONObject(map);
         return  entrustInfoServiceClient.saventrustData(jsonObject);
     }
@@ -893,4 +913,9 @@ public class ExcelTabClientImpl implements ExcelTabClient {
         return R.success("成功");
     }
 
+    @Override
+    public R copeBussTab(Long pKeyId, String header) throws Exception {
+        return excelTabController.copeBussTab(pKeyId);
+    }
+
 }

+ 26 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/InformationImportRecordMapper.java

@@ -0,0 +1,26 @@
+/*
+ *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.manager.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springblade.manager.entity.InformationImportRecord;
+
+public interface InformationImportRecordMapper extends BaseMapper<InformationImportRecord> {
+
+
+}

+ 4 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/IExcelTabService.java

@@ -228,4 +228,8 @@ public interface IExcelTabService extends BaseService<ExcelTab> {
     ExcelTabVO templateDetail(Long id);
 
     void setAutomatic(Long pkeyId, String string, Document doc);
+
+    Map<String, String> getFormulaData(JSONArray dataArray, WbsTreeContract wbsTreeTable);
+
+    StringBuilder buildMTableInsertSql(String tabName, Map<String, String> dataMap2, Object id, Object groupId, Object pKeyId);
 }

+ 30 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/InformationImportRecordService.java

@@ -0,0 +1,30 @@
+/*
+ *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.manager.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.manager.entity.InformationImportRecord;
+
+import java.time.LocalDateTime;
+
+
+public interface InformationImportRecordService extends IService<InformationImportRecord> {
+
+    Boolean updateProcess(Long id, Integer process, Integer status, String remake, LocalDateTime updateTime);
+
+
+}

+ 27 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/InformationImportRecordServiceImpl.java

@@ -0,0 +1,27 @@
+package org.springblade.manager.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springblade.manager.entity.InformationImportRecord;
+import org.springblade.manager.mapper.InformationImportRecordMapper;
+import org.springblade.manager.service.InformationImportRecordService;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Service
+public class InformationImportRecordServiceImpl extends ServiceImpl<InformationImportRecordMapper, InformationImportRecord> implements InformationImportRecordService {
+    @Override
+    public Boolean updateProcess(Long id, Integer process, Integer status, String remake, LocalDateTime updateTime) {
+        if (id == null || (process == null && status == null)) {
+            return false;
+        }
+        if (process != null && process < 100) {
+            process = (int) (Math.random() * 9 + process - 5);
+        }
+        return this.update(Wrappers.<InformationImportRecord>lambdaUpdate().eq(InformationImportRecord::getId, id).eq(InformationImportRecord::getUpdateTime, updateTime)
+                .set(process != null, InformationImportRecord::getProcess, process)
+                .set(status != null, InformationImportRecord::getStatus, status)
+                .set(remake != null, InformationImportRecord::getRemark, remake));
+    }
+}