Explorar el Código

Merge branch 'refs/heads/dev' into feature-lihb-bug

# Conflicts:
#	blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeContractMapper.java
LHB hace 1 mes
padre
commit
8a76ae7f64
Se han modificado 47 ficheros con 4844 adiciones y 222 borrados
  1. 47 0
      blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/trans/ArchiveReq.java
  2. 39 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/ServicePlanDTO.java
  3. 34 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/ServicePlanTaskDTO.java
  4. 13 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/ServiceUserDto.java
  5. 86 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/ServicePlan.java
  6. 50 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/ServicePlanTask.java
  7. 7 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/WbsTreeContract.java
  8. 7 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/WbsTreePrivate.java
  9. 142 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/WbsTreeSynchronousRecord.java
  10. 55 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/enums/WbsSyncTypeEnum.java
  11. 6 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/feign/ArchiveTreeContractClient.java
  12. 34 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/ServicePlanTaskVO.java
  13. 50 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/ServicePlanVO.java
  14. 2 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/WbsTreePrivateQueryValueVO.java
  15. 27 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/WbsTreeSynchronousRecordVo.java
  16. 37 1
      blade-service/blade-archive/src/main/java/org/springblade/archive/external/impl/ExternalDataArchiveBuildService.java
  17. 21 1
      blade-service/blade-archive/src/main/java/org/springblade/archive/external/impl/ExternalDataArchiveTreeService.java
  18. 7 1
      blade-service/blade-manager/pom.xml
  19. 442 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/ServicePlanController.java
  20. 129 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/ServicePlanTaskController.java
  21. 150 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeSynchronousRecordController.java
  22. 7 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/feign/ArchiveTreeContractImpl.java
  23. 45 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/ServicePlanMapper.java
  24. 53 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/ServicePlanMapper.xml
  25. 44 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/ServicePlanTaskMapper.java
  26. 22 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/ServicePlanTaskMapper.xml
  27. 14 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeContractMapper.xml
  28. 4 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreePrivateMapper.java
  29. 12 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreePrivateMapper.xml
  30. 21 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeSynchronousRecordMapper.java
  31. 30 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeSynchronousRecordMapper.xml
  32. 2 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/IArchiveTreeContractService.java
  33. 63 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/IServicePlanService.java
  34. 44 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/IServicePlanTaskService.java
  35. 0 1
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/IWbsTreePrivateService.java
  36. 25 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/WbsTreeSynchronousRecordService.java
  37. 32 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ArchiveTreeContractServiceImpl.java
  38. 5 5
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ExcelTabServiceImpl.java
  39. 914 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ServicePlanServiceImpl.java
  40. 50 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ServicePlanTaskServiceImpl.java
  41. 259 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousEViSaServiceImpl.java
  42. 1441 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousServiceImpl.java
  43. 0 213
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreePrivateServiceImpl.java
  44. 1 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeServiceImpl.java
  45. 273 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeSynchronousRecordServiceImpl.java
  46. 49 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/wrapper/ServicePlanTaskWrapper.java
  47. 49 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/wrapper/ServicePlanWrapper.java

+ 47 - 0
blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/trans/ArchiveReq.java

@@ -22,6 +22,21 @@ public class ArchiveReq {
 
     String remoteId;
 
+    /**
+     * 默认0,用nodeId去找
+     * 为1时,用contractName,nodeName找
+     */
+    Integer transType= 0;
+
+    //合同段名称
+    String contractName;
+
+    //节点名称
+    String nodeName;
+
+    //0 施工,1 监理
+    Integer nodeType;
+
     //归档树
     List<ArchiveTreeVo> trees=new ArrayList<>();
     //案卷
@@ -102,4 +117,36 @@ public class ArchiveReq {
     public void setProjectName(String projectName) {
         this.projectName = projectName;
     }
+
+    public Integer getTransType() {
+        return transType;
+    }
+
+    public void setTransType(Integer transType) {
+        this.transType = transType;
+    }
+
+    public String getContractName() {
+        return contractName;
+    }
+
+    public void setContractName(String contractName) {
+        this.contractName = contractName;
+    }
+
+    public String getNodeName() {
+        return nodeName;
+    }
+
+    public void setNodeName(String nodeName) {
+        this.nodeName = nodeName;
+    }
+
+    public Integer getNodeType() {
+        return nodeType;
+    }
+
+    public void setNodeType(Integer nodeType) {
+        this.nodeType = nodeType;
+    }
 }

+ 39 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/ServicePlanDTO.java

@@ -0,0 +1,39 @@
+/*
+ *      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.dto;
+
+import org.springblade.manager.entity.ServicePlan;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 数据传输对象实体类
+ *
+ * @author BladeX
+ * @since 2025-06-25
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ServicePlanDTO extends ServicePlan {
+	private static final long serialVersionUID = 1L;
+
+    private String userId;
+
+    private String planStartTime1;
+
+    private String planEndTime1;
+}

+ 34 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/ServicePlanTaskDTO.java

@@ -0,0 +1,34 @@
+/*
+ *      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.dto;
+
+import org.springblade.manager.entity.ServicePlanTask;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 数据传输对象实体类
+ *
+ * @author BladeX
+ * @since 2025-06-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ServicePlanTaskDTO extends ServicePlanTask {
+	private static final long serialVersionUID = 1L;
+
+}

+ 13 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/ServiceUserDto.java

@@ -0,0 +1,13 @@
+package org.springblade.manager.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ServiceUserDto {
+    private String userId;
+    private String userName;
+}

+ 86 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/ServicePlan.java

@@ -0,0 +1,86 @@
+/*
+ *      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.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.Serializable;
+import java.time.LocalDate;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.springblade.core.mp.base.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 实体类
+ *
+ * @author BladeX
+ * @since 2025-06-25
+ */
+@Data
+@TableName("m_service_plan")
+@EqualsAndHashCode(callSuper = true)
+public class ServicePlan extends BaseEntity {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	* 填写类型 1月度服务计划 2服务完成确认单
+	*/
+    @ApiModelProperty(value = "填写类型 1月度服务计划 2服务完成确认单")
+		private Integer fileInType;
+    /**
+     * 项目Id
+     */
+    @ApiModelProperty(value = "项目Id")
+        private Long projectId;
+    /**
+     * 合同段Id
+     */
+    @ApiModelProperty(value = "合同段Id")
+        private Long contractId;
+	/**
+	* 计划开始时间
+	*/
+    @ApiModelProperty(value = "计划开始时间")
+		private LocalDate planStartTime;
+	/**
+	* 计划结束时间
+	*/
+    @ApiModelProperty(value = "计划结束时间")
+		private LocalDate planEndTime;
+    /**
+     * 编写人
+     */
+    @ApiModelProperty(value = "编写人ID 逗号拼接")
+        private String writeUser;
+	/**
+	* 发送人员
+	*/
+    @ApiModelProperty(value = "发送人员ID 逗号拼接")
+		private String sendUser;
+    /**
+     * pdf路径
+      */
+    @ApiModelProperty(value = "pdf路径")
+        private String pdfUrl;
+
+
+
+
+
+}

+ 50 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/ServicePlanTask.java

@@ -0,0 +1,50 @@
+/*
+ *      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.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.Serializable;
+import org.springblade.core.mp.base.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 实体类
+ *
+ * @author BladeX
+ * @since 2025-06-27
+ */
+@Data
+@TableName("m_service_plan_task")
+@EqualsAndHashCode(callSuper = true)
+public class ServicePlanTask extends BaseEntity {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	* 服务计划主键ID
+	*/
+		private Long servicePlanId;
+	/**
+	* 发送人ID
+	*/
+		private Long userId;
+
+		private Integer isCancel;
+
+
+}

+ 7 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/WbsTreeContract.java

@@ -1,5 +1,8 @@
 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 com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.annotations.ApiModelProperty;
@@ -23,8 +26,12 @@ public class WbsTreeContract extends BaseEntity {
      * 主键id
      */
     @ApiModelProperty(value = "主键id")
+    @TableId(value = "p_key_id", type = IdType.INPUT)
     private Long pKeyId;
 
+    @TableField
+    private Long id;
+
 
     @ApiModelProperty(value = "新节点Id")
     private Long pId;

+ 7 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/WbsTreePrivate.java

@@ -1,5 +1,8 @@
 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 com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.annotations.ApiModelProperty;
@@ -20,9 +23,13 @@ public class WbsTreePrivate extends BaseEntity {
     /**
      * 主键
      */
+    @TableId(value = "p_key_id", type = IdType.INPUT)
     @JsonProperty(value = "pKeyId")
     private Long pKeyId;
 
+    @TableField
+    private Long id;
+
     @ApiModelProperty(value = "pid")
     private Long pId;
 

+ 142 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/WbsTreeSynchronousRecord.java

@@ -0,0 +1,142 @@
+package org.springblade.manager.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * WBS同步记录表
+ *
+ * @author LHB
+ * @TableName m_wbs_tree_synchronous_record
+ */
+@TableName(value = "m_wbs_tree_synchronous_record")
+@Data
+public class WbsTreeSynchronousRecord {
+    /**
+     * id111
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 项目id
+     */
+    private Long projectId;
+
+    /**
+     * 项目名称
+     */
+    private String projectName;
+
+
+    @TableField("`range`")
+    private Integer range;
+    /**
+     * 同步范围名称
+     */
+    private String rangeName;
+    /**
+     * 合同段范围 逗号拼接的编号 101.未填报 102.已填报-未上报 103.未上报 104.待审批 105.已审批
+     */
+    private String contractRange;
+    /**
+     * 合同段范围名称
+     */
+    private String contractRangeName;
+
+    /**
+     * 同步源Id
+     */
+    private Long templateId;
+
+    /**
+     * 同步源名称
+     */
+    private String templateName;
+
+    /**
+     * 同步类型 逗号拼接的编号 1.新增表单 2.清表配置 3.元素配置 4.电签配置 5.公式配置 6.默认值配置 7.表单排序
+     */
+    private String type;
+
+    /**
+     * 同步类型名称
+     */
+    private String typeName;
+
+    /**
+     * 同步节点id 多个节点
+     */
+    private String nodeId;
+
+    /**
+     * 同步节点名称
+     */
+    private String nodeName;
+    /**
+     * 表单Ids     range = 4 强制同步时  当前数据为同步源
+     */
+    private String formIds;
+    /**
+     * 同步节点数量
+     */
+    private Integer nodeNum;
+    /**
+     * 已同步数量
+     */
+    private Integer nodeNumEnd;
+
+    /**
+     * 是否删除(0-未删除,1-删除)
+     */
+    private Integer isDeleted;
+
+    /**
+     * 状态(0-未同步,1-正在同步,2-已同步,3-同步失败)
+     */
+    private Integer status;
+    /**
+     * 状态(0-未同步,1-正在同步,2-已同步,3-同步失败)
+     */
+    @TableField(exist = false)
+    private String statusName;
+    /**
+     * 错误信息
+     */
+    @TableField(updateStrategy = FieldStrategy.NOT_EMPTY)
+    private String errorMsg;
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 创建人
+     */
+    private String createUser;
+
+    /**
+     * 创建人id
+     */
+    private Long createUserId;
+
+    /**
+     * 修改时间
+     */
+    private Date updateTime;
+
+    /**
+     * 修改人
+     */
+    private String updateUser;
+
+    /**
+     * 修改人id
+     */
+    private Long updateUserId;
+}

+ 55 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/enums/WbsSyncTypeEnum.java

@@ -0,0 +1,55 @@
+package org.springblade.manager.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springblade.core.tool.utils.ObjectUtil;
+
+/**
+ * @author LHB
+ */
+@Getter
+@AllArgsConstructor
+public enum WbsSyncTypeEnum {
+    UNKNOWN(-1, ""),
+    INSERT_FORM(1, "新增表单"),
+    CLEAR_TABLE_CONFIG(2, "清表配置"),
+    ELEMENT_CONFIG(3, "元素配置"),
+    E_VISA_CONFIG(4, "电签配置"),
+    FORMULA_CONFIG(5, "公式配置"),
+    DEFAULT_VALUE_CONFIG(6, "默认值配置"),
+    FORM_SORT(7, "表单排序"),
+    /**
+     * 合同段同步范围
+     */
+    NOT_FILLED_IN(101, "未填报"),
+    ALREADY_FILLED_IN_NOT_REPORTED(102, "已填报-未上报"),
+    NOT_REPORTED(103, "未上报"),
+    PENDING_APPROVAL(104, "待审批"),
+    APPROVED(105, "已审批"),
+    ;
+
+    /**
+     * 编码
+     */
+    public Integer code;
+
+    /**
+     * 描述
+     */
+    private String desc;
+
+    /**
+     * 根据编码获取枚举描述
+     */
+    public static String getByCode(int code) {
+        // 判断code
+        if(ObjectUtil.isNotEmpty(code)){
+            for (StorageTypeEnum storageTypeEnum : StorageTypeEnum.values()) {
+                if (storageTypeEnum.getCode() == code) {
+                    return storageTypeEnum.getDesc();
+                }
+            }
+        }
+        return WbsSyncTypeEnum.UNKNOWN.getDesc();
+    }
+}

+ 6 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/feign/ArchiveTreeContractClient.java

@@ -82,4 +82,10 @@ public interface ArchiveTreeContractClient {
     @PostMapping(API_PREFIX+"/adsArchiveTreeContract")
     public void addArchiveTreeContract(@RequestBody List<ArchiveTreeContract> archiveTreeContracts,@RequestParam Long rootId);
 
+
+    @PostMapping(API_PREFIX + "/getNodeIdByName")
+    Long getNodeIdByName(@RequestParam("projectName") String projectName,
+                         @RequestParam("contractName") String contractName,
+                         @RequestParam("nodeName") String nodeName);
+
 }

+ 34 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/ServicePlanTaskVO.java

@@ -0,0 +1,34 @@
+/*
+ *      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.vo;
+
+import org.springblade.manager.entity.ServicePlanTask;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 视图实体类
+ *
+ * @author BladeX
+ * @since 2025-06-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ServicePlanTaskVO extends ServicePlanTask {
+	private static final long serialVersionUID = 1L;
+
+}

+ 50 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/ServicePlanVO.java

@@ -0,0 +1,50 @@
+/*
+ *      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.vo;
+
+import org.springblade.manager.entity.ServicePlan;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 视图实体类
+ *
+ * @author BladeX
+ * @since 2025-06-25
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ServicePlanVO extends ServicePlan {
+	private static final long serialVersionUID = 1L;
+   //1计划中  2协同中-甲方  3协同中-系统  4已计划
+    private String statusValue;
+    //是否可编辑
+   private Boolean isEdit;
+    //计划开始时间
+   private String startTime;
+   //计划结束时间
+   private String endTime;
+   //发送人
+   private String sendUserName;
+
+   private String writeUserName;
+
+   private String planTime;
+
+
+
+}

+ 2 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/WbsTreePrivateQueryValueVO.java

@@ -14,4 +14,6 @@ public class WbsTreePrivateQueryValueVO extends WbsTreePrivate {
 
     private Boolean hasChildren;
 
+    private Long primaryKeyId;
+
 }

+ 27 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/WbsTreeSynchronousRecordVo.java

@@ -0,0 +1,27 @@
+package org.springblade.manager.vo;
+
+import lombok.Data;
+
+/**
+ *
+ * @author LHB
+ */
+@Data
+public class WbsTreeSynchronousRecordVo {
+    /**
+     * id
+     */
+    private Long id;
+    /**
+     * 项目名称
+     */
+    private String name;
+    /**
+     * wbsId
+     */
+    private String wbsId;
+    /**
+     * type = 1 公共  type = 2 私有
+     */
+    private Integer type;
+}

+ 37 - 1
blade-service/blade-archive/src/main/java/org/springblade/archive/external/impl/ExternalDataArchiveBuildService.java

@@ -4,6 +4,7 @@ import lombok.AllArgsConstructor;
 import org.springblade.archive.external.bean.ExternalDataInfo;
 import org.springblade.archive.external.utils.TransUtil;
 import org.springblade.archive.trans.ArchiveReq;
+import org.springblade.manager.feign.ArchiveTreeContractClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -18,8 +19,11 @@ public class ExternalDataArchiveBuildService {
     @Autowired
     private  ExternalDataArchiveTreeService externalTreeService;
 
+
     private ExternalDataArchiveMetaService externalMetaService;
 
+    private final ArchiveTreeContractClient archiveTreeContractClient;
+
 
     /**
      *
@@ -31,7 +35,39 @@ public class ExternalDataArchiveBuildService {
             throw new IllegalArgumentException("请求参数不能为空");
         }
 
-        ExternalDataInfo externalDataInfo = externalTreeService.getDataByIds(req.getNodeId(),req.getRemoteId());
+        // 当 transType 为 1 时,使用新方式获取 remoteId, 并且在传输开始时设置
+        if (req.getTransType() != null && req.getTransType() == 1 && req.getType()== 0) {
+            // 获取节点ID
+            Long remoteIdLong = archiveTreeContractClient.getNodeIdByName(
+                    req.getProjectName(),
+                    req.getContractName(),
+                    req.getNodeName()
+            );
+
+            if (remoteIdLong == null) {
+                throw new RuntimeException("无法找到匹配的节点: " +
+                        req.getProjectName() + "/" +
+                        req.getContractName() + "/" +
+                        req.getNodeName());
+            }
+
+            // 将 Long 转换为 String
+            req.setRemoteId(String.valueOf(remoteIdLong));
+        }
+
+        // 调用获取数据的方法
+        ExternalDataInfo externalDataInfo = externalTreeService.getDataByIds(
+                req.getNodeId(),
+                req.getRemoteId(),
+                req.getProjectName(),
+                req.getContractName(),
+                req.getNodeName(),
+                req.getTransType()
+        );
+
+        if (externalDataInfo == null) {
+            return;
+        }
 
         switch (req.getType()) {
             case 0:

+ 21 - 1
blade-service/blade-archive/src/main/java/org/springblade/archive/external/impl/ExternalDataArchiveTreeService.java

@@ -26,7 +26,27 @@ public class ExternalDataArchiveTreeService {
     private ArchivesAutoMapper autoMapper;
     private final MetadataClassificationClient metadataClassificationClient;
 
-    public  ExternalDataInfo getDataByIds(String nodeId, String remoteId){
+    public ExternalDataInfo getDataByIds(String nodeId, String remoteId,
+                                         String projectName, String contractName,
+                                         String nodeName, Integer transType) {
+
+        // 当 transType 为 1 时,重新获取 remoteId
+        if (transType != null && transType == 1) {
+            // 获取节点ID
+            Long remoteIdLong = archiveTreeContractClient.getNodeIdByName(
+                    projectName,
+                    contractName,
+                    nodeName
+            );
+
+            if (remoteIdLong == null) {
+                throw new RuntimeException("无法找到匹配的节点: " +
+                        projectName + "/" + contractName + "/" + nodeName);
+            }
+
+            // 将 Long 转换为 String
+            remoteId = String.valueOf(remoteIdLong);
+        }
         ExternalDataInfo externalDataInfo = TransUtil.getDataByIds(nodeId, remoteId);
 
         if (externalDataInfo.getParent() == null) {

+ 7 - 1
blade-service/blade-manager/pom.xml

@@ -208,6 +208,12 @@
             <version>7.2.4</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+            <version>2.10.3</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
@@ -234,7 +240,7 @@
                     <target>${java.version}</target>
                     <encoding>${project.build.sourceEncoding}</encoding>
                     <compilerArguments>
-                        <bootclasspath>${java.home}/lib/rt.jar:${java.home}/lib/jce.jar:${java.home}/lib/jsse.jar
+                        <bootclasspath>${java.home}/lib/rt.jar;${java.home}/lib/jce.jar;${java.home}/lib/jsse.jar
                         </bootclasspath>
                     </compilerArguments>
                 </configuration>

+ 442 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/ServicePlanController.java

@@ -0,0 +1,442 @@
+/*
+ *      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.controller;
+
+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.toolkit.Wrappers;
+import io.swagger.annotations.*;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.models.auth.In;
+import lombok.AllArgsConstructor;
+import javax.validation.Valid;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.util.IOUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.springblade.business.entity.InformationQuery;
+import org.springblade.common.constant.CommonConstant;
+import org.springblade.common.utils.CommonUtil;
+import org.springblade.common.vo.DataVO;
+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.oss.model.BladeFile;
+import org.springblade.core.secure.BladeUser;
+import org.springblade.core.secure.utils.SecureUtil;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.*;
+import org.springblade.manager.bean.TableInfo;
+import org.springblade.manager.dto.ServicePlanDTO;
+import org.springblade.manager.dto.ServiceUserDto;
+import org.springblade.manager.entity.*;
+import org.springblade.manager.service.IServicePlanTaskService;
+import org.springblade.manager.utils.FileUtils;
+import org.springblade.system.cache.ParamCache;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestParam;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.springblade.manager.vo.ServicePlanVO;
+import org.springblade.manager.wrapper.ServicePlanWrapper;
+import org.springblade.manager.service.IServicePlanService;
+import org.springblade.core.boot.ctrl.BladeController;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ *  控制器
+ *
+ * @author BladeX
+ * @since 2025-06-25
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/serviceplan")
+@Api(value = "服务计划", tags = "服务计划接口")
+public class ServicePlanController extends BladeController {
+
+	private final IServicePlanService servicePlanService;
+
+    private final IServicePlanTaskService servicePlanTaskService;
+
+    private final JdbcTemplate jdbcTemplate;
+
+	/**
+	 * 详情
+	 */
+	@GetMapping("/detail")
+	@ApiOperationSupport(order = 1)
+	@ApiOperation(value = "详情", notes = "传入servicePlan")
+    @ApiImplicitParams(value = {
+        @ApiImplicitParam(name = "id", value = "id", required = true)
+    })
+	public R<ServicePlanVO> detail(Long id) {
+        ServicePlanVO detail = servicePlanService.getdetail(id);
+		return R.data(detail);
+	}
+
+	/**
+	 * 分页
+	 */
+//	@GetMapping("/list")
+//	@ApiOperationSupport(order = 2)
+//	@ApiOperation(value = "分页", notes = "传入servicePlan")
+//	public R<IPage<ServicePlanVO>> list(ServicePlan servicePlan, Query query) {
+//		IPage<ServicePlan> pages = servicePlanService.page(Condition.getPage(query), Condition.getQueryWrapper(servicePlan));
+//		return R.data(ServicePlanWrapper.build().pageVO(pages));
+//	}
+
+
+	/**
+	 * 自定义分页
+	 */
+	@GetMapping("/page")
+	@ApiOperationSupport(order = 3)
+	@ApiOperation(value = "分页", notes = "传入servicePlan")
+    @ApiImplicitParams(value = {
+        @ApiImplicitParam(name = "projectId", value = "项目Id", required = true),
+        @ApiImplicitParam(name = "contractId", value = "合同Id", required = true)
+    })
+	public R<IPage<ServicePlanVO>> page(ServicePlanDTO servicePlan, Query query) {
+		IPage<ServicePlanVO> pages = servicePlanService.selectServicePlanPage(Condition.getPage(query), servicePlan);
+		return R.data(pages);
+	}
+    @GetMapping("/getSendUserAndWriteUser")
+    @ApiOperationSupport(order = 35)
+    @ApiOperation(value = "获取发送人及填写人", notes = "获取发送人及填写人")
+    @ApiImplicitParams(value = {
+        @ApiImplicitParam(name = "projectId", value = "项目Id", required = true),
+        @ApiImplicitParam(name = "contractId", value = "合同Id", required = true)
+    })
+    public R<Map<String,List<ServiceUserDto>>> getSendUserAndWriteUser(Long projectId, Long contractId){
+        return R.data(servicePlanService.getSendUserAndWriteUser(projectId,contractId));
+    }
+
+	/**
+	 * 新增
+	 */
+//	@PostMapping("/save")
+//	@ApiOperationSupport(order = 4)
+//	@ApiOperation(value = "新增", notes = "传入servicePlan")
+//    @ApiImplicitParams(value = {
+//        @ApiImplicitParam(name = "projectId", value = "项目Id", required = true),
+//        @ApiImplicitParam(name = "contractId", value = "合同Id", required = true),
+//        @ApiImplicitParam(name = "type", value = "1月度服务计划 2服务完成确认单", required = true),
+//        @ApiImplicitParam(name = "planStartTime", value = "计划开始时间", required = true),
+//        @ApiImplicitParam(name = "planEndTime", value = "计划结束时间", required = true),
+//    })
+//	public R save(@Valid @RequestBody ServicePlan servicePlan) {
+//        servicePlan.setWriteUser(SecureUtil.getUserId()+"");
+//        return R.status(servicePlanService.save(servicePlan));
+//	}
+
+	/**
+	 * 修改
+	 */
+//	@PostMapping("/update")
+//	@ApiOperationSupport(order = 5)
+//	@ApiOperation(value = "修改", notes = "传入servicePlan")
+//	public R update(@Valid @RequestBody ServicePlan servicePlan) {
+//        if(servicePlan.getWriteUser()!=null){
+//            if(!servicePlan.getWriteUser().contains(SecureUtil.getUserId()+"")){
+//                servicePlan.setWriteUser(servicePlan.getWriteUser()+","+SecureUtil.getUserId()+"");
+//            }
+//        }
+//		return R.status(servicePlanService.updateById(servicePlan));
+//	}
+    @GetMapping("/sendServicePlan")
+    @ApiOperationSupport(order = 34)
+    @ApiOperation(value = "发送计划", notes = "修改服务计划流程")
+    @ApiImplicitParams(value = {
+        @ApiImplicitParam(name = "id", value = "计划id", required = true),
+        @ApiImplicitParam(name = "sendUser", value = "发送人ID 逗号拼接", required = true),
+    })
+    public R sendServicePlan(Long id,String sendUser){
+        ServicePlan plan = servicePlanService.getById(id);
+        plan.setStatus(2);
+        String[] split = sendUser.split(",");
+        for (String sid : split) {
+            ServicePlanTask task = servicePlanTaskService.getOne(new LambdaQueryWrapper<>(ServicePlanTask.class).eq(ServicePlanTask::getServicePlanId, id).eq(ServicePlanTask::getUserId, sid));
+            if(task==null){
+                task=new ServicePlanTask();
+            }
+            task.setServicePlanId(id);
+            task.setUserId(Long.valueOf(sid));
+            task.setStatus(2);
+            servicePlanTaskService.saveOrUpdate(task);
+        }
+        plan.setSendUser(sendUser);
+        servicePlanService.updateById(plan);
+        return R.status(true);
+    }
+
+    @GetMapping("/cancelServicePlan")
+    @ApiOperationSupport(order = 34)
+    @ApiOperation( value = "计划回退")
+    public R cancelServicePlan(Long id){
+        ServicePlan plan = servicePlanService.getById(id);
+        plan.setStatus(3);
+        String sql="update m_service_plan_task set status=3 where service_plan_id="+id+" and is_deleted=0";
+        jdbcTemplate.update(sql);
+        String sql2="update m_service_plan_task set is_cancel=1 where service_plan_id="+id+" and is_deleted=0 and user_id="+SecureUtil.getUserId();
+        jdbcTemplate.update(sql2);
+         return R.status(servicePlanService.updateById(plan));
+    }
+
+    @GetMapping("/confirmServicePlan")
+    @ApiOperationSupport(order = 34)
+    @ApiOperation( value = "确认计划")
+    public R confirmServicePlan(Long id){
+        ServicePlan plan = servicePlanService.getById(id);
+        String sql="update m_service_plan_task set status=4 where service_plan_id="+id+" and user_id="+SecureUtil.getUserId();
+        jdbcTemplate.update(sql);
+        List<ServicePlanTask> tasks = servicePlanTaskService.getTaskByServicePlanId(id);
+        if(!tasks.isEmpty()){
+            List<ServicePlanTask> collect = tasks.stream().filter(o -> o.getStatus() != 4).collect(Collectors.toList());
+            if(collect.isEmpty()){
+                plan.setStatus(4);
+             return R.status(servicePlanService.updateById(plan));
+            }
+        }
+        return R.status(true);
+    }
+
+	/**
+	 * 新增或修改
+	 */
+//	@PostMapping("/submit")
+//	@ApiOperationSupport(order = 6)
+//	@ApiOperation(value = "新增或修改", notes = "传入servicePlan")
+//	public R submit(@Valid @RequestBody ServicePlan servicePlan) {
+//		return R.status(servicePlanService.saveOrUpdate(servicePlan));
+//	}
+
+
+	/**
+	 * 删除
+	 */
+	@PostMapping("/remove")
+	@ApiOperationSupport(order = 7)
+	@ApiOperation(value = "逻辑删除", notes = "传入ids")
+	public R remove(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) {
+		return R.status(servicePlanService.deleteLogic(Func.toLongList(ids)));
+	}
+
+    @GetMapping("/getServiceHtml")
+    @ApiOperationSupport(order = 8)
+    @ApiOperation(value = "获取服务计划html", notes = "projectId,contractId,type 1月度服务计划 2服务完成确认单")
+    @ApiImplicitParams(value = {
+        @ApiImplicitParam(name = "projectId", value = "项目ID", required = true),
+        @ApiImplicitParam(name = "contractId", value = "合同段ID", required = false),
+        @ApiImplicitParam(name = "pkeyId", value = "1月度服务计划 2服务完成确认单", required = true),
+    })
+    public R<String> getServiceHtml(Long projectId, Long contractId,Long pkeyId){
+        return R.data(servicePlanService.getServiceHtml(projectId,contractId,pkeyId));
+    }
+
+    @GetMapping("/get-html-buss-cols")
+    @ApiOperationSupport(order = 36)
+    @ApiOperation(value = "获取坐标位置", notes = "获取坐标位置")
+    @ApiImplicitParams(value = {
+        @ApiImplicitParam(name = "pkeyId", value = "1月度服务计划 2服务完成确认单", required = true)
+    })
+    public R getHtmlBussCols(Long pkeyId) {
+        String sql="select * from m_wbs_tree_contract where p_key_id="+pkeyId;
+        WbsTreeContract wbsTreeContract = jdbcTemplate.queryForObject( sql, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+        if (wbsTreeContract == null) {
+            throw new ServiceException("暂无表单");
+        }
+        if (wbsTreeContract.getHtmlUrl() == null) {
+            throw new ServiceException("暂无表单");
+        }
+        String file_path = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+        String sys_file_net_url = ParamCache.getValue(CommonConstant.SYS_FILE_NET_URL);
+        try {
+            String fileUrl = wbsTreeContract.getHtmlUrl();
+            File file1 = ResourceUtil.getFile(fileUrl);
+            InputStream fileInputStream = null;
+            if (file1.exists()) {
+                fileInputStream = new FileInputStream(file1);
+            } else {
+                String path = sys_file_net_url + fileUrl.replaceAll("//", "/").replaceAll(file_path, "");
+                fileInputStream = CommonUtil.getOSSInputStream(path);
+            }
+
+
+            String htmlString = IoUtil.readToString(fileInputStream);
+            // 解析 style
+            Document doc = Jsoup.parse(htmlString);
+            Element table = doc.select("table").first();
+            Elements trs = table.select("tr");
+
+            List<List<String>> redata = new ArrayList<>();
+            for (int i = 0; i < trs.size(); i++) {
+                Element tr = trs.get(i);
+                Elements tds = tr.select("td");
+                List<String> tdList = new ArrayList<>();
+                for (int j = 0; j < tds.size(); j++) {
+                    Element element = tds.get(j);
+                    if (element.html().indexOf("el-tooltip") >= 0) {
+                        element = element.children().get(0);
+                    }
+                    if (element.children().size() >= 1) {
+                        String keyname = element.children().get(0).attr("keyname");
+                        if (StringUtils.isNotEmpty(keyname)) {
+                            tdList.add(keyname);
+                        }
+                    }
+                }
+                if (tdList != null && tdList.size() >= 1) {
+                    redata.add(tdList);
+                }
+            }
+
+            String[][] res = new String[redata.size()][]; // 存放转换结果的 二维数组
+            for (int i = 0; i < res.length; i++) { // 转换方法
+                res[i] = redata.get(i).toArray(new String[redata.get(i).size()]);
+            }
+            return R.data(res);
+        } catch (Exception e) {
+            return R.fail("暂无表单!");
+        }
+    }
+
+    @PostMapping("/saveServiceData")
+    @ApiOperationSupport(order = 9)
+    @ApiOperation(value = "保存服务计划数据", notes = "传入dataInfo")
+    @Transactional
+    public R<Long> saveServiceData(@Valid @RequestBody JSONObject dataInfo) throws Exception {
+        JSONArray dataArray = new JSONArray();
+        Long pkeyId = Long.valueOf(dataInfo.getString("pkeyId"));
+        Long projectId=Long.valueOf(dataInfo.getString("projectId"));
+        Long contractId=Long.valueOf(dataInfo.getString("contractId"));
+        Long groupId1 = null;
+        if(dataInfo.containsKey("group_id")){
+            if(StringUtils.isNotEmpty(dataInfo.getString("group_id"))){
+                groupId1=Long.valueOf(dataInfo.getString("group_id"));
+            }
+        }
+        dataArray.add(dataInfo);
+        List<TableInfo> tableInfoList=getServiceData(dataArray);
+        Long groupId = servicePlanService.saveServicePlan(pkeyId,projectId,contractId,dataInfo,groupId1);
+        servicePlanService.saveServiceData(tableInfoList,groupId,pkeyId);
+        servicePlanService.saveServicePlanPdf(projectId,pkeyId,Func.toLong(groupId));
+        return R.data(groupId);
+    }
+
+
+    private List<TableInfo> getServiceData(JSONArray dataInfo) {
+        if (dataInfo != null && !dataInfo.isEmpty()) {
+            List<TableInfo> result = new ArrayList<>();
+            for (int m = 0; m < dataInfo.size(); m++) {
+                TableInfo tableInfo = new TableInfo();
+                JSONObject dataInfo2 = dataInfo.getJSONObject(m);
+                tableInfo.setToBeUpdated(true);
+                tableInfo.setContractId(dataInfo2.getString("contractId"));
+                dataInfo2.fluentRemove("contractId")
+                    .fluentRemove("pkeyId")
+                    .fluentRemove("p_key_id")
+                    .fluentRemove("projectId")
+                    .fluentRemove("classify")
+                    .fluentRemove("pickerKey")
+                    .fluentRemove("id")
+                    .fluentRemove("isFirst")
+                    .fluentRemove("firstNodeId")
+                    .fluentRemove("isTheLog")
+                    .fluentRemove("theLogId")
+                    .fluentRemove("linkTabIds")
+                    .fluentRemove("recordTime")
+                    .fluentRemove("businessId")
+                    .fluentRemove("sourceUrl")
+                    .fluentRemove("pdfUrl")
+                    .fluentRemove("firstFileName")
+                    .fluentRemove("");
+                //计算数据
+                LinkedHashMap<String, List<String>> dataMap = dataInfo2.keySet().stream().filter(e -> e.contains("__")).collect(Collectors.groupingBy(e -> e.split("__")[0], LinkedHashMap<String, List<String>>::new, Collectors.toList()));
+                LinkedHashMap<String, String> dataMap2 = new LinkedHashMap<>();
+
+                //字段组合
+                for (String k : dataMap.keySet()) {
+                    if (dataMap.get(k).size() > 1 && !dataMap.get(k).contains("000Z")) {
+                        String[] ziduan = dataMap.get(k).toArray(new String[]{});
+                        String temp = "";
+                        for (int i = 0; i < ziduan.length - 1; i++) {
+                            for (int j = 0; j < ziduan.length - i - 1; j++) {
+                                int tr = Integer.parseInt((ziduan[j].split("__")[1]).split("_")[0]);
+                                int td = Integer.parseInt(ziduan[j].split("__")[1].split("_")[1]);
+                                int tr_1 = Integer.parseInt(ziduan[j + 1].split("__")[1].split("_")[0]);
+                                int td_1 = Integer.parseInt(ziduan[j + 1].split("__")[1].split("_")[1]);
+                                if (tr > tr_1 && td == td_1) { //纵向排序
+                                    temp = ziduan[j];
+                                    ziduan[j] = ziduan[j + 1];
+                                    ziduan[j + 1] = temp;
+                                }
+                            }
+                        }
+                        StringBuilder lastStr = new StringBuilder(dataInfo2.getString(ziduan[0]) + "_^_" + ziduan[0].split("__")[1]);
+                        for (int i = 1; i < ziduan.length; i++) {
+                            String keyData = dataInfo2.getString(ziduan[i]);
+                            if (keyData!=null && Func.isNotEmpty(keyData)) {
+                                lastStr.append("☆").append(dataInfo2.getString(ziduan[i])).append("_^_").append(ziduan[i].split("__")[1]);
+                            }
+
+                        }
+                        dataMap2.put(k, lastStr.toString());
+                    } else {
+                        String dataVal = dataInfo2.getString(dataMap.get(k).get(0));
+                        if(StringUtils.isNotEmpty(dataVal)){
+                            dataMap2.put(k, dataVal + "_^_" + dataMap.get(k).get(0).split("__")[1]);
+                        }
+                    }
+                }
+                tableInfo.setDataMap(dataMap2);
+                result.add(tableInfo);
+            }
+            return result;
+        }
+        return null;
+    }
+
+    @GetMapping("/getServiceBussData")
+    @ApiOperationSupport(order = 36)
+    @ApiOperation(value = "获取保存数据", notes = "获取保存数据")
+    @ApiImplicitParams(value = {
+        @ApiImplicitParam(name = "id", value = "id", required = true),
+        @ApiImplicitParam(name = "pkeyId", value = "1月度服务计划 2服务完成确认单", required = true)
+    })
+    public R<Map<String, Object>> getServiceBussData(Long id,Long pkeyId){
+        return R.data(servicePlanService.getServiceBussData(id,pkeyId));
+    }
+
+}

+ 129 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/ServicePlanTaskController.java

@@ -0,0 +1,129 @@
+/*
+ *      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.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import lombok.AllArgsConstructor;
+import javax.validation.Valid;
+
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.Func;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestParam;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.springblade.manager.entity.ServicePlanTask;
+import org.springblade.manager.vo.ServicePlanTaskVO;
+import org.springblade.manager.wrapper.ServicePlanTaskWrapper;
+import org.springblade.manager.service.IServicePlanTaskService;
+import org.springblade.core.boot.ctrl.BladeController;
+
+/**
+ *  控制器
+ *
+ * @author BladeX
+ * @since 2025-06-27
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/serviceplantask")
+@Api(value = "", tags = "接口")
+public class ServicePlanTaskController extends BladeController {
+
+	private final IServicePlanTaskService servicePlanTaskService;
+
+	/**
+	 * 详情
+	 */
+	@GetMapping("/detail")
+	@ApiOperationSupport(order = 1)
+	@ApiOperation(value = "详情", notes = "传入servicePlanTask")
+	public R<ServicePlanTaskVO> detail(ServicePlanTask servicePlanTask) {
+		ServicePlanTask detail = servicePlanTaskService.getOne(Condition.getQueryWrapper(servicePlanTask));
+		return R.data(ServicePlanTaskWrapper.build().entityVO(detail));
+	}
+
+	/**
+	 * 分页 
+	 */
+	@GetMapping("/list")
+	@ApiOperationSupport(order = 2)
+	@ApiOperation(value = "分页", notes = "传入servicePlanTask")
+	public R<IPage<ServicePlanTaskVO>> list(ServicePlanTask servicePlanTask, Query query) {
+		IPage<ServicePlanTask> pages = servicePlanTaskService.page(Condition.getPage(query), Condition.getQueryWrapper(servicePlanTask));
+		return R.data(ServicePlanTaskWrapper.build().pageVO(pages));
+	}
+
+
+	/**
+	 * 自定义分页 
+	 */
+	@GetMapping("/page")
+	@ApiOperationSupport(order = 3)
+	@ApiOperation(value = "分页", notes = "传入servicePlanTask")
+	public R<IPage<ServicePlanTaskVO>> page(ServicePlanTaskVO servicePlanTask, Query query) {
+		IPage<ServicePlanTaskVO> pages = servicePlanTaskService.selectServicePlanTaskPage(Condition.getPage(query), servicePlanTask);
+		return R.data(pages);
+	}
+
+	/**
+	 * 新增 
+	 */
+	@PostMapping("/save")
+	@ApiOperationSupport(order = 4)
+	@ApiOperation(value = "新增", notes = "传入servicePlanTask")
+	public R save(@Valid @RequestBody ServicePlanTask servicePlanTask) {
+		return R.status(servicePlanTaskService.save(servicePlanTask));
+	}
+
+	/**
+	 * 修改 
+	 */
+	@PostMapping("/update")
+	@ApiOperationSupport(order = 5)
+	@ApiOperation(value = "修改", notes = "传入servicePlanTask")
+	public R update(@Valid @RequestBody ServicePlanTask servicePlanTask) {
+		return R.status(servicePlanTaskService.updateById(servicePlanTask));
+	}
+
+	/**
+	 * 新增或修改 
+	 */
+	@PostMapping("/submit")
+	@ApiOperationSupport(order = 6)
+	@ApiOperation(value = "新增或修改", notes = "传入servicePlanTask")
+	public R submit(@Valid @RequestBody ServicePlanTask servicePlanTask) {
+		return R.status(servicePlanTaskService.saveOrUpdate(servicePlanTask));
+	}
+
+	
+	/**
+	 * 删除 
+	 */
+	@PostMapping("/remove")
+	@ApiOperationSupport(order = 7)
+	@ApiOperation(value = "逻辑删除", notes = "传入ids")
+	public R remove(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) {
+		return R.status(servicePlanTaskService.deleteLogic(Func.toLongList(ids)));
+	}
+
+	
+}

+ 150 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeSynchronousRecordController.java

@@ -0,0 +1,150 @@
+package org.springblade.manager.controller;
+
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.manager.entity.ProjectInfo;
+import org.springblade.manager.entity.WbsTreePrivate;
+import org.springblade.manager.entity.WbsTreeSynchronousRecord;
+import org.springblade.manager.service.WbsTreeSynchronousRecordService;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.tool.api.R;
+import org.springblade.manager.vo.WbsTreeSynchronousRecordVo;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * WBS同步记录表(MWbsTreeSynchronousRecord)表控制层
+ *
+ * @author makejava
+ * @since 2025-05-15 13:50:25
+ */
+@RestController
+@RequestMapping("/synchronousRecord")
+public class WbsTreeSynchronousRecordController {
+    /**
+     * 服务对象
+     */
+    @Resource
+    private WbsTreeSynchronousRecordService mWbsTreeSynchronousRecordService;
+
+    /**
+     * 分页查询所有数据
+     *
+     * @param record 查询对象
+     * @param query  分页对象
+     * @return 所有数据
+     */
+    @GetMapping("/page")
+    public R<IPage<WbsTreeSynchronousRecord>> selectAll(WbsTreeSynchronousRecord record, Query query) {
+        LambdaQueryWrapper<WbsTreeSynchronousRecord> lambda = new QueryWrapper().lambda();
+        lambda
+                .eq(record.getProjectId() != null, WbsTreeSynchronousRecord::getProjectId, record.getProjectId())
+                .eq(record.getStatus() != null, WbsTreeSynchronousRecord::getStatus, record.getStatus())
+                .eq(record.getRange() != null, WbsTreeSynchronousRecord::getRange, record.getRange());
+        if (!StringUtil.isBlank(record.getType())) {
+            lambda.apply("FIND_IN_SET({0},type)", record.getType());
+        }
+        IPage<WbsTreeSynchronousRecord> page = mWbsTreeSynchronousRecordService.page(Condition.getPage(query), lambda);
+        for (WbsTreeSynchronousRecord pageRecord : page.getRecords()) {
+            switch (pageRecord.getStatus()) {
+                case 0:
+                    pageRecord.setStatusName("待同步");
+                    break;
+                case 1:
+                    pageRecord.setStatusName("正在同步");
+                    break;
+                case 2:
+                    pageRecord.setStatusName("已同步");
+                    break;
+                case 3:
+                    pageRecord.setStatusName("同步失败");
+                    break;
+                default:
+                    break;
+            }
+        }
+        return R.data(page);
+    }
+
+    /**
+     * 通过主键查询单条数据
+     *
+     * @param id 主键
+     * @return 单条数据
+     */
+    @GetMapping("getById")
+    public R<WbsTreeSynchronousRecord> selectOne(@RequestParam Long id) {
+        return R.data(this.mWbsTreeSynchronousRecordService.getById(id));
+    }
+
+
+    /**
+     * 新增数据
+     *
+     * @param mWbsTreeSynchronousRecord 实体对象
+     * @return 新增结果
+     */
+    @PostMapping("add")
+    public R<WbsTreeSynchronousRecord> insert(@RequestBody WbsTreeSynchronousRecord mWbsTreeSynchronousRecord) {
+
+        if (mWbsTreeSynchronousRecord.getRange() == 1 || mWbsTreeSynchronousRecord.getRange() == 2) {
+            if (StringUtil.isBlank(mWbsTreeSynchronousRecord.getNodeId())) {
+                return R.fail("节点不能为空");
+            } else {
+                String[] split = mWbsTreeSynchronousRecord.getNodeId().split(",");
+                if (split.length > 100) {
+                    return R.fail("节点过多,如果勾选了父节点,请勿带子节点!");
+                }
+            }
+            if (StringUtil.isBlank(mWbsTreeSynchronousRecord.getType())) {
+                return R.fail("请选择同步类型");
+            }
+            if (mWbsTreeSynchronousRecord.getRange() == null) {
+                return R.fail("请选择同步范围");
+            }
+
+            if (mWbsTreeSynchronousRecord.getRange() == 2 && StringUtil.isBlank(mWbsTreeSynchronousRecord.getContractRange())) {
+                return R.fail("请选择合同同步范围");
+            }
+            if (mWbsTreeSynchronousRecord.getProjectId() == null) {
+                return R.fail("项目Id为空");
+            }
+            if (mWbsTreeSynchronousRecord.getRange() == 1 && mWbsTreeSynchronousRecord.getTemplateId() == null) {
+                return R.fail("同步源为空");
+            }
+        }
+
+        return R.data(this.mWbsTreeSynchronousRecordService.insert(mWbsTreeSynchronousRecord));
+    }
+
+    /**
+     * 认证接口是否正在同步
+     */
+    @PostMapping("getNodeStatus")
+    public R<WbsTreeSynchronousRecord> getNodeStatus(@RequestParam Long id) {
+        return R.data(this.mWbsTreeSynchronousRecordService.getNodeStatus(id));
+    }
+
+
+    /**
+     * 获取当前项目的模板项目
+     *
+     * @param nodeIds 节点ids 逗号拼接
+     */
+    @GetMapping("getTempProject")
+    public R<List<WbsTreeSynchronousRecordVo>> getProjectTemplate(@RequestParam String nodeIds) {
+        if (StringUtil.isBlank(nodeIds)) {
+            return R.fail("参数不能为空");
+        }
+        return R.data(this.mWbsTreeSynchronousRecordService.getProjectTemplate(nodeIds));
+    }
+
+}
+

+ 7 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/feign/ArchiveTreeContractImpl.java

@@ -177,5 +177,12 @@ public class ArchiveTreeContractImpl implements ArchiveTreeContractClient {
         return archiveTreeContractMapper.getTopAutoTypeNodeByProjectID(projectId);
     }
 
+    @Override
+    public Long getNodeIdByName( String projectName,
+                         String contractName,
+                          String nodeName){
+        return  archiveTreeContractService.getNodeIdByName(projectName,contractName,nodeName);
+    }
+
 
 }

+ 45 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/ServicePlanMapper.java

@@ -0,0 +1,45 @@
+/*
+ *      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 org.apache.ibatis.annotations.Param;
+import org.springblade.manager.dto.ServicePlanDTO;
+import org.springblade.manager.entity.ServicePlan;
+import org.springblade.manager.vo.ServicePlanVO;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import java.util.List;
+
+/**
+ *  Mapper 接口
+ *
+ * @author BladeX
+ * @since 2025-06-25
+ */
+public interface ServicePlanMapper extends BaseMapper<ServicePlan> {
+
+	/**
+	 * 自定义分页
+	 *
+	 * @param page
+	 * @param servicePlan
+	 * @return
+	 */
+	List<ServicePlan> selectServicePlanPage(IPage page, @Param("servicePlan")ServicePlanDTO servicePlan);
+
+    List<String> getUserID(@Param("projectId") Long projectId, @Param("contractId") Long contractId, @Param("type") int type);
+}

+ 53 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/ServicePlanMapper.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.springblade.manager.mapper.ServicePlanMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="servicePlanResultMap" type="org.springblade.manager.entity.ServicePlan">
+        <result column="id" property="id"/>
+        <result column="status" property="status"/>
+        <result column="create_user" property="createUser"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_user" property="updateUser"/>
+        <result column="update_time" property="updateTime"/>
+        <result column="file_in_type" property="fileInType"/>
+        <result column="plan_start_time" property="planStartTime"/>
+        <result column="plan_end_time" property="planEndTime"/>
+        <result column="send_user" property="sendUser"/>
+    </resultMap>
+
+
+    <select id="selectServicePlanPage" resultMap="servicePlanResultMap">
+        select * from m_service_plan where is_deleted = 0 and project_id = #{servicePlan.projectId} and contract_id = #{servicePlan.contractId}
+                                       and (FIND_IN_SET(#{servicePlan.userId},write_user) or FIND_IN_SET(#{servicePlan.userId},send_user))
+        <if test="servicePlan.fileInType != null">
+            and file_in_type = #{servicePlan.fileInType}
+        </if>
+        <if test="servicePlan.status!=null">
+            and status = #{servicePlan.status}
+        </if>
+        <if test="servicePlan.writeUser!=null">
+            and FIND_IN_SET(write_user,#{servicePlan.writeUser})
+        </if>
+        <if test="servicePlan.sendUser!=null">
+            and FIND_IN_SET(send_user,#{servicePlan.sendUser})
+        </if>
+        <if test="servicePlan.planStartTime1 != null and servicePlan.planEndTime1 != null">
+            and (
+            (#{servicePlan.planStartTime1} between plan_start_time and plan_end_time)
+            or (#{servicePlan.planEndTime1} between plan_start_time and plan_end_time)
+            or (plan_start_time between #{servicePlan.planStartTime1} and #{servicePlan.planEndTime1})
+            or (plan_end_time between #{servicePlan.planStartTime1} and #{servicePlan.planEndTime1})
+            )
+        </if>
+    </select>
+    <select id="getUserID" resultType="java.lang.String">
+        <if test="type == 1">
+            select send_user from m_service_plan where is_deleted = 0 and project_id = #{projectId} and contract_id = #{contractId}
+        </if>
+        <if test="type == 2">
+            select write_user from m_service_plan where is_deleted = 0 and project_id = #{projectId} and contract_id = #{contractId}
+        </if>
+    </select>
+
+</mapper>

+ 44 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/ServicePlanTaskMapper.java

@@ -0,0 +1,44 @@
+/*
+ *      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 io.lettuce.core.dynamic.annotation.Param;
+import org.springblade.manager.entity.ServicePlanTask;
+import org.springblade.manager.vo.ServicePlanTaskVO;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import java.util.List;
+
+/**
+ *  Mapper 接口
+ *
+ * @author BladeX
+ * @since 2025-06-27
+ */
+public interface ServicePlanTaskMapper extends BaseMapper<ServicePlanTask> {
+
+	/**
+	 * 自定义分页
+	 *
+	 * @param page
+	 * @param servicePlanTask
+	 * @return
+	 */
+	List<ServicePlanTaskVO> selectServicePlanTaskPage(IPage page, ServicePlanTaskVO servicePlanTask);
+
+    List<ServicePlanTask> getTaskByServicePlanId(@Param("id") Long id);
+}

+ 22 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/ServicePlanTaskMapper.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.springblade.manager.mapper.ServicePlanTaskMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="servicePlanTaskResultMap" type="org.springblade.manager.entity.ServicePlanTask">
+        <result column="id" property="id"/>
+        <result column="status" property="status"/>
+        <result column="is_deleted" property="isDeleted"/>
+        <result column="service_plan_id" property="servicePlanId"/>
+        <result column="user_id" property="userId"/>
+    </resultMap>
+
+
+    <select id="selectServicePlanTaskPage" resultMap="servicePlanTaskResultMap">
+        select * from m_service_plan_task where is_deleted = 0
+    </select>
+    <select id="getTaskByServicePlanId" resultType="org.springblade.manager.entity.ServicePlanTask">
+         select * from m_service_plan_task where service_plan_id=#{id} and is_deleted=0;
+    </select>
+
+</mapper>

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

@@ -816,6 +816,20 @@
             a.project_id = #{projectId}
           AND a.is_deleted = 0
     </update>
+    <update id="updateSortByPId">
+        update
+            m_wbs_tree_contract
+        set
+            sort = sort +1
+        where p_id = #{pId} and sort >= #{sort}
+    </update>
+    <update id="updateSortBatchByPKeyId">
+        <foreach item="item" collection="list" separator=";">
+            UPDATE m_wbs_tree_contract
+            <set>sort = #{item.sort}</set>
+            where p_key_id = #{item.pKeyId}
+        </foreach>
+    </update>
 
     <select id="selectQueryValueLikeNodeName" resultMap="ResultMap">
         select *

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

@@ -144,4 +144,8 @@ public interface WbsTreePrivateMapper extends EasyBaseMapper<WbsTreePrivate> {
      */
     void updateBatchAncestorsByPKeyId(@Param("allNodes") List<WbsTreePrivate> allNodes);
 
+    void updateSortByPId(@Param("pId") Long pId,
+                         @Param("sort") Integer sort);
+
+    void updateSortBatchByPKeyId(List<WbsTreePrivate> resourceData);
 }

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

@@ -929,6 +929,18 @@
             where p_key_id = #{item.pKeyId}
         </foreach>
     </update>
+    <update id="updateSortByPId">
+        update m_wbs_tree_private
+        set sort = sort + 1
+        where p_id = #{pId} and sort >= #{sort}
+    </update>
+    <update id="updateSortBatchByPKeyId">
+        <foreach item="item" collection="list" separator=";">
+            UPDATE m_wbs_tree_private
+            <set>sort = #{item.sort}</set>
+            where p_key_id = #{item.pKeyId}
+        </foreach>
+    </update>
 
     <select id="linkNodeTreeBynodeId" resultType="java.lang.Long" >
         select p_key_id from m_wbs_tree_private where `type` = 1 and is_deleted = 0

+ 21 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeSynchronousRecordMapper.java

@@ -0,0 +1,21 @@
+package org.springblade.manager.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Param;
+import org.springblade.manager.entity.WbsTreeSynchronousRecord;
+
+/**
+* @author LHB
+* @description 针对表【m_wbs_tree_synchronous_record(WBS同步记录表)】的数据库操作Mapper
+* @createDate 2025-05-15 13:52:09
+* @Entity generator.domain.MWbsTreeSynchronousRecord
+*/
+public interface WbsTreeSynchronousRecordMapper extends BaseMapper<WbsTreeSynchronousRecord> {
+
+}
+
+
+
+

+ 30 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeSynchronousRecordMapper.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.springblade.manager.mapper.WbsTreeSynchronousRecordMapper">
+
+    <resultMap id="BaseResultMap" type="org.springblade.manager.entity.WbsTreeSynchronousRecord">
+            <id property="id" column="id" />
+            <result property="projectId" column="project_id" />
+            <result property="projectName" column="project_name" />
+            <result property="range" column="range" />
+            <result property="source" column="source" />
+            <result property="type" column="type" />
+            <result property="nodeId" column="node_id" />
+            <result property="nodeName" column="node_name" />
+            <result property="isDeleted" column="is_deleted" />
+            <result property="createTime" column="create_time" />
+            <result property="createUser" column="create_user" />
+            <result property="createUserId" column="create_user_id" />
+            <result property="updateTime" column="update_time" />
+            <result property="updateUser" column="update_user" />
+            <result property="updateUserId" column="update_user_id" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,project_id,project_name,range,source,type,
+        node_id,node_name,is_deleted,create_time,create_user,
+        create_user_id,update_time,update_user,update_user_id
+    </sql>
+</mapper>

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

@@ -143,4 +143,6 @@ public interface IArchiveTreeContractService extends BaseService<ArchiveTreeCont
     List<ArchiveTreeContractDTO> getArchiveTreeContractDto(List<JiLinQueryDto>dtos,Long projectId);
 
     void addArchiveTreeContract(List<ArchiveTreeContract> archiveTreeContracts, Long rootId);
+
+    Long getNodeIdByName(String projectName, String contractName, String nodeName);
 }

+ 63 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/IServicePlanService.java

@@ -0,0 +1,63 @@
+/*
+ *      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.alibaba.fastjson.JSONObject;
+import org.springblade.manager.bean.TableInfo;
+import org.springblade.manager.dto.ServicePlanDTO;
+import org.springblade.manager.dto.ServiceUserDto;
+import org.springblade.manager.entity.ServicePlan;
+import org.springblade.manager.vo.ServicePlanVO;
+import org.springblade.core.mp.base.BaseService;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *  服务类
+ *
+ * @author BladeX
+ * @since 2025-06-25
+ */
+public interface IServicePlanService extends BaseService<ServicePlan> {
+
+	/**
+	 * 自定义分页
+	 *
+	 * @param page
+	 * @param servicePlan
+	 * @return
+	 */
+	IPage<ServicePlanVO> selectServicePlanPage(IPage<ServicePlanVO> page, ServicePlanDTO servicePlan);
+
+    String getServiceHtml(Long projectId,Long contractId,Long pkeyId);
+
+    boolean saveServiceData(List<TableInfo> tableInfoList,Long groupId,Long pkeyId);
+
+    void saveServicePlanPdf(Long projectId,Long pkeyId, Long id) throws Exception;
+
+    Map<String, Object>getServiceBussData(Long id, Long pkeyId);
+
+    Map<String,List<ServiceUserDto>> getSendUserAndWriteUser(Long projectId, Long contractId);
+
+    Long saveServicePlan(Long pkeyId,Long projectId,Long contractId,@Valid JSONObject dataInfo,Long groupId1);
+
+    ServicePlanVO getdetail(Long id);
+}

+ 44 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/IServicePlanTaskService.java

@@ -0,0 +1,44 @@
+/*
+ *      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 org.springblade.manager.entity.ServicePlanTask;
+import org.springblade.manager.vo.ServicePlanTaskVO;
+import org.springblade.core.mp.base.BaseService;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+
+import java.util.List;
+
+/**
+ *  服务类
+ *
+ * @author BladeX
+ * @since 2025-06-27
+ */
+public interface IServicePlanTaskService extends BaseService<ServicePlanTask> {
+
+	/**
+	 * 自定义分页
+	 *
+	 * @param page
+	 * @param servicePlanTask
+	 * @return
+	 */
+	IPage<ServicePlanTaskVO> selectServicePlanTaskPage(IPage<ServicePlanTaskVO> page, ServicePlanTaskVO servicePlanTask);
+
+    List<ServicePlanTask> getTaskByServicePlanId(Long id);
+}

+ 0 - 1
blade-service/blade-manager/src/main/java/org/springblade/manager/service/IWbsTreePrivateService.java

@@ -65,7 +65,6 @@ public interface IWbsTreePrivateService extends BaseService<WbsTreePrivate> {
     void eVisInfoRepeatDel(String pid);
 
     boolean syncNodeTable(String primaryKeyId);
-    List<WbsTreePrivate> syncNodeTable(String primaryKeyIdm,String a);
 
     R addWbsTreeContractInfo(String nodeId, String primaryKeyIds, Long contractId);
 

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

@@ -0,0 +1,25 @@
+package org.springblade.manager.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.core.mp.support.Query;
+import org.springblade.manager.entity.ProjectInfo;
+import org.springblade.manager.entity.WbsTreePrivate;
+import org.springblade.manager.entity.WbsTreeSynchronousRecord;
+import org.springblade.manager.vo.WbsTreeSynchronousRecordVo;
+
+import java.util.List;
+
+/**
+* @author LHB
+* @description 针对表【m_wbs_tree_synchronous_record(WBS同步记录表)】的数据库操作Service
+* @createDate 2025-05-15 13:52:09
+*/
+public interface WbsTreeSynchronousRecordService extends IService<WbsTreeSynchronousRecord> {
+
+    WbsTreeSynchronousRecord insert(WbsTreeSynchronousRecord mWbsTreeSynchronousRecord);
+
+    List<WbsTreeSynchronousRecordVo> getProjectTemplate(String nodeIds);
+
+    WbsTreeSynchronousRecord getNodeStatus(Long id);
+}

+ 32 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ArchiveTreeContractServiceImpl.java

@@ -52,6 +52,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestParam;
 
@@ -1789,5 +1790,36 @@ public class ArchiveTreeContractServiceImpl extends BaseServiceImpl<ArchiveTreeC
 		this.saveOrUpdateBatch(changeArchiveList);
 	}
 
+	public Long getNodeIdByName(String projectName, String contractName, String nodeName) {
+		// 1. 获取合同ID
+		Long contractId = projectInfoService.getContractIdbyName(projectName, contractName);
+
+		// 2. 检查合同ID有效性
+		if (contractId == null) {
+			System.out.println("Contract not found with projectName: " + projectName + ", contractName: " + contractName);
+			return null;
+		}
+
+		// 3. 根据合同ID和节点名称查询节点
+		List<ArchiveTreeContract> nodes = baseMapper.selectList(Wrappers.<ArchiveTreeContract>query().lambda()
+				.eq(ArchiveTreeContract::getContractId, contractId)
+				.like(ArchiveTreeContract::getNodeName, nodeName)
+				.eq(ArchiveTreeContract::getIsDeleted, 0));
+
+		// 4. 处理查询结果
+		if (nodes == null || nodes.isEmpty()) {
+			System.out.println("No node found for contractId: " + contractId + ", nodeName: " + nodeName);
+			return null;
+		}
+
+		// 打印找到的节点数
+		System.out.println("Found " + nodes.size() + " nodes matching the criteria");
+
+		// 返回第一个匹配节点的ID
+		Long nodeId = nodes.get(0).getId();
+		System.out.println("Returning nodeId: " + nodeId);
+		return nodeId;
+	}
+
 
 }

+ 5 - 5
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ExcelTabServiceImpl.java

@@ -2061,7 +2061,7 @@ public class ExcelTabServiceImpl extends BaseServiceImpl<ExcelTabMapper, ExcelTa
         }
         for (int i = 0; i < xle; i++) {
             CellRangeAddress mergedCell = sheet.getMergedRegion(i);
-            int xx = mergedCell.getNumberOfCells();
+            int xx = mergedCell.getLastColumn();
             if (xx >= all) {
                 all = xx;
             }
@@ -2069,7 +2069,7 @@ public class ExcelTabServiceImpl extends BaseServiceImpl<ExcelTabMapper, ExcelTa
 
         for (int i = 0; i < xle; i++) {
             CellRangeAddress mergedCell = sheet.getMergedRegion(i);
-            int xx = mergedCell.getNumberOfCells() + 2;
+            int xx = mergedCell.getLastColumn() + 2;
             if (xx >= all) {
                 int fisRow = mergedCell.getFirstRow();
                 int firsrCol = mergedCell.getFirstColumn();
@@ -2087,7 +2087,7 @@ public class ExcelTabServiceImpl extends BaseServiceImpl<ExcelTabMapper, ExcelTa
                 newStyle.cloneStyleFrom(cell.getCellStyle());
 
                 short fontHeightInPoints = redFont.getFontHeightInPoints();
-                if (fontHeightInPoints >= isWater && StringUtils.isEmpty(cell.getStringCellValue()) && fisRow <= 8) {
+                if (fontHeightInPoints >= isWater && StringUtils.isBlank(cell.getStringCellValue()) && fisRow <= 8) {
 
                     String title = projectInfo.getProjectName();
                     if (title.length() >= 30) {
@@ -3211,7 +3211,7 @@ public class ExcelTabServiceImpl extends BaseServiceImpl<ExcelTabMapper, ExcelTa
         int mergedCellCnt = sheet.getNumMergedRegions();
         for (int i = 0; i < mergedCellCnt - 1; i++) {
             CellRangeAddress mergedCell = sheet.getMergedRegion(i);
-            int xx = mergedCell.getNumberOfCells();
+            int xx = mergedCell.getLastColumn();
             if (xx <= all) {
                 int fisRow = mergedCell.getFirstRow();
                 int firstCol = mergedCell.getFirstColumn();
@@ -4310,7 +4310,7 @@ public class ExcelTabServiceImpl extends BaseServiceImpl<ExcelTabMapper, ExcelTa
                 int i = 0;
                 i < mergedCellCnt - 1; i++) {
             CellRangeAddress mergedCell = sheet.getMergedRegion(i);
-            int xx = mergedCell.getNumberOfCells();
+            int xx = mergedCell.getLastColumn();
             if (xx <= all) {
                 int fisRow = mergedCell.getFirstRow();
                 int firstCol = mergedCell.getFirstColumn();

+ 914 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ServicePlanServiceImpl.java

@@ -0,0 +1,914 @@
+/*
+ *      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.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.mixsmart.utils.ListUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.util.IOUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.springblade.business.dto.TrialSelfInspectionRecordDTO;
+import org.springblade.business.entity.EntrustInfo;
+import org.springblade.business.entity.TrialMaterialMobilization;
+import org.springblade.business.entity.TrialSampleInfo;
+import org.springblade.business.entity.TrialSelfInspectionRecord;
+import org.springblade.common.constant.CommonConstant;
+import org.springblade.common.utils.CommonUtil;
+import org.springblade.common.utils.SnowFlakeUtil;
+import org.springblade.common.vo.DataVO;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.oss.model.BladeFile;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.secure.utils.SecureUtil;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.*;
+import org.springblade.manager.bean.TableInfo;
+import org.springblade.manager.dto.ServicePlanDTO;
+import org.springblade.manager.dto.ServiceUserDto;
+import org.springblade.manager.entity.*;
+import org.springblade.manager.utils.FileUtils;
+import org.springblade.manager.vo.ServicePlanVO;
+import org.springblade.manager.mapper.ServicePlanMapper;
+import org.springblade.manager.service.IServicePlanService;
+import org.springblade.core.mp.base.BaseServiceImpl;
+import org.springblade.resource.feign.NewIOSSClient;
+import org.springblade.system.cache.ParamCache;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.amqp.RabbitTemplateConfigurer;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+import javax.annotation.Resource;
+import java.io.*;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ *  服务实现类
+ *
+ * @author BladeX
+ * @since 2025-06-25
+ */
+@Service
+public class ServicePlanServiceImpl extends BaseServiceImpl<ServicePlanMapper, ServicePlan> implements IServicePlanService {
+    @Resource
+    private  JdbcTemplate jdbcTemplate;
+    @Autowired
+    private DataSourceTransactionManager transactionManager1;
+    @Autowired
+    private  NewIOSSClient newIOSSClient;
+
+	@Override
+	public IPage<ServicePlanVO> selectServicePlanPage(IPage<ServicePlanVO> page, ServicePlanDTO servicePlan) {
+        servicePlan.setUserId(SecureUtil.getUserId()+"");
+        List<ServicePlan> vos = baseMapper.selectServicePlanPage(page, servicePlan);
+        List<ServicePlanVO>voList = new ArrayList<>();
+        for (ServicePlan sp : vos) {
+            ServicePlanVO vo = new ServicePlanVO();
+            BeanUtil.copyProperties(sp, vo);
+           vo.setStatusValue(vo.getStatus()==1?"计划中":vo.getStatus()==2?"确认中-甲方":vo.getStatus()==3?"反馈中-系统":"已计划");
+           vo.setIsEdit(checkIsEdit(vo));
+           if(vo.getPlanStartTime()!=null){
+               vo.setStartTime(vo.getPlanStartTime().format(DateTimeFormatter.ofPattern("yyyy年M月d日")));
+           }
+           if(vo.getPlanEndTime()!=null){
+               vo.setEndTime(vo.getPlanEndTime().format(DateTimeFormatter.ofPattern("yyyy年M月d日")));
+           }
+           if(StringUtils.isNotEmpty(vo.getStartTime())&&StringUtils.isNotEmpty(vo.getEndTime())){
+               vo.setPlanTime(vo.getStartTime()+"至"+vo.getEndTime());
+           }
+            vo.setWriteUserName(slectUserName(vo.getWriteUser()));
+            vo.setSendUserName(slectUserName(vo.getSendUser()));
+            voList.add(vo);
+        }
+        return page.setRecords(voList);
+	}
+
+
+
+    private String slectUserName(String sendUser) {
+        if(StringUtils.isNotEmpty(sendUser)){
+            String sql="select real_name from blade_user where id in("+sendUser+")";
+            List<String> list = jdbcTemplate.queryForList(sql, String.class);
+            if(!list.isEmpty()){
+                return list.stream().collect(Collectors.joining(","));
+            }
+        }
+        return "";
+    }
+
+    private Boolean checkIsEdit(ServicePlanVO vo) {
+        if(vo.getStatus()==1){
+            return true;
+        }else if(vo.getStatus()==2){
+            if(StringUtils.isNotEmpty(vo.getSendUser())){
+                return vo.getSendUser().contains(SecureUtil.getUserId()+"");
+            }
+        }else if(vo.getStatus()==2){
+            if(StringUtils.isNotEmpty(vo.getSendUser())){
+                return vo.getWriteUser().contains(SecureUtil.getUserId()+"");
+            }
+        }else {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String getServiceHtml(Long projectId, Long contractId,Long pkeyId) {
+        String sql="select * from m_wbs_tree_contract where p_key_id="+pkeyId;
+        WbsTreeContract wbsTreeContract = jdbcTemplate.queryForObject( sql, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+        if (wbsTreeContract == null) {
+            throw new ServiceException("暂无表单");
+        }
+        if (wbsTreeContract.getHtmlUrl() == null) {
+            throw new ServiceException("暂无表单");
+        }
+        try {
+            String fileUrl = wbsTreeContract.getHtmlUrl();
+            InputStream fileInputStream = FileUtils.getInputStreamByUrl(fileUrl);
+            String htmlString = IoUtil.readToString(fileInputStream);
+            htmlString = htmlString.replaceAll("placeholder", "placeholderxx");
+            htmlString = htmlString.replaceAll("title", "titlexx");
+
+            // 远程搜索配置
+            Document doc = Jsoup.parse(htmlString);
+            Element table = doc.select("table").first();
+            // 标题解决
+            String sql1="select * from m_project_info where id="+projectId;
+            ProjectInfo projectInfo = jdbcTemplate.queryForObject(sql1,new BeanPropertyRowMapper<>(ProjectInfo.class));
+            //判断是否是水利水电表,水利水电项目名14,表名12 。   其他表都是18
+            Boolean isWater = false;
+            // 添加标题显示
+            Elements trs = table.select("tr");
+            for (int i = 1; i < 6; i++) {
+                Element tr = trs.get(i);
+                Elements tds = tr.select("td");
+                for (int j = 0; j < tds.size(); j++) {
+                    Element data = tds.get(j);
+                    String style = data.attr("style");
+                    if (style.indexOf("font-size") >= 0) {
+                        int fontsize = Integer.parseInt(style.substring(style.indexOf("font-size:") + 10, style.indexOf(".0pt")));
+                        Element element = null;
+                        if (isWater) {
+                            if (StringUtils.isNotEmpty(data.text()) && fontsize >= 12) {
+                                element = trs.get(i - 1).select("td").get(0);
+
+                            }
+                        } else {
+                            if (StringUtils.isNotEmpty(data.text()) && fontsize >= 14) {
+                                element = trs.get(i - 1).select("td").get(0);
+                            }
+                        }
+                        if (element != null && Func.isNotEmpty(element)) {
+                            //
+                            Elements allElements = element.children();
+                            if (allElements.size() >= 1) {
+                                String defText = allElements.get(0).attr("deftext");
+                                if (defText != null && Func.isNotEmpty(defText)) {
+                                    element.text(defText);
+                                } else {
+                                    element.text(projectInfo.getProjectName());
+                                }
+                            } else {
+                                element.text(projectInfo.getProjectName());
+                            }
+                        }
+                    }
+                }
+            }
+            fileInputStream.close();
+            return table + "";
+        } catch (Exception e) {
+            throw new ServiceException("暂无表单");
+        }
+    }
+
+    @Override
+    public boolean saveServiceData(List<TableInfo> tableInfoList,Long groupId,Long  pkeyId) {
+        if (ListUtils.isNotEmpty(tableInfoList)) {
+            for (TableInfo tableInfo : tableInfoList) {
+                String sql="select * from m_wbs_tree_contract where p_key_id="+pkeyId;
+                WbsTreeContract wbsTreeContract = jdbcTemplate.queryForObject( sql, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+                if (wbsTreeContract == null || StringUtils.isEmpty(wbsTreeContract.getInitTableName())) {
+                    continue;
+                }
+                //删除SQL
+                String delSql = "delete from " + wbsTreeContract.getInitTableName() + " where p_key_id='" + pkeyId + "' and group_id = '" + groupId + "'";
+
+                //新增SQ
+                String sqlInfo = "";
+                LinkedHashMap<String, String> dataMap2 = tableInfo.getDataMap();
+                sqlInfo = "INSERT INTO " + wbsTreeContract.getInitTableName() + " ( ";
+                String keyStr = "id,p_key_id,group_id,";
+                String valStr = SnowFlakeUtil.getId() + "," + pkeyId + ","+groupId+",";
+                if(dataMap2.containsKey("p_key_id")){
+                    dataMap2.remove("p_key_id");
+                }
+                for (String keys : dataMap2.keySet()) {
+                    keyStr += keys + ",";
+                    valStr += "'" + dataMap2.get(keys) + "',";
+                }
+                keyStr = keyStr.substring(0, keyStr.lastIndexOf(","));
+                valStr = valStr.substring(0, valStr.lastIndexOf(","));
+                sqlInfo = sqlInfo + keyStr + ") VALUES (" + valStr + ")";
+                TransactionStatus transactionStatus = this.beginTransaction(transactionManager1);
+                try {
+                    //删除
+                    jdbcTemplate.execute(delSql);
+                    //新增
+                    jdbcTemplate.execute(sqlInfo);
+                    //提交事务
+                    transactionManager1.commit(transactionStatus);
+                } catch (Exception e) {
+                    //回滚
+                    transactionManager1.rollback(transactionStatus);
+                    throw new RuntimeException("500"+e.getCause().getMessage()+"  字段过长,新增失败");
+                }
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void saveServicePlanPdf(Long projectId,Long pkeyId, Long id) throws Exception {
+        String file_path = FileUtils.getSysLocalFileUrl();//ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+        String sys_file_net_url = ParamCache.getValue(CommonConstant.SYS_FILE_NET_URL);
+        String sql="select * from m_wbs_tree_contract where p_key_id="+pkeyId;
+        WbsTreeContract wbsTreeContract = jdbcTemplate.queryForObject( sql, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+        if (wbsTreeContract == null) {
+            throw new ServiceException("该数据下无此节点!");
+        }
+        if (wbsTreeContract.getHtmlUrl() == null) {
+            throw new ServiceException("请关联清表!");
+        }
+        String pdfPath = file_path + "/pdf//" + pkeyId + ".pdf";
+        String excelPath = file_path + "/pdf//" + pkeyId + ".xlsx";
+        File tabPdf = ResourceUtil.getFile(pdfPath);
+        if (tabPdf.exists()) {
+            tabPdf.delete();
+        }
+        String sql1="select * from m_excel_tab where id="+wbsTreeContract.getExcelId();
+        //获取清表信息
+        ExcelTab excelTab =jdbcTemplate.queryForObject( sql1, new BeanPropertyRowMapper<>(ExcelTab.class));
+
+        if (excelTab == null) {
+            throw new ServiceException("操作失败!");
+        }
+
+        //获取数据信息info
+        List<Map<String, Object>> bussDataInfoTrial = this.getBussDataInfoTrial(pkeyId,id);
+        Map<String, Object> DataInfo = new HashMap<>();
+        if (bussDataInfoTrial.size() > 0) {
+            DataInfo.putAll(bussDataInfoTrial.stream().findAny().orElse(null));
+        }
+
+        //获取清表excel文件
+        org.apache.poi.ss.usermodel.Workbook workbook = WorkbookFactory.create(Objects.requireNonNull(CommonUtil.getOSSInputStreamTow(excelTab.getFileUrl())));
+        Sheet sheet = workbook.getSheetAt(0);
+        sheet.setForceFormulaRecalculation(true);
+
+       String sql2="select * from m_project_info where id="+projectId;
+       ProjectInfo projectInfo = jdbcTemplate.queryForObject(sql2, new BeanPropertyRowMapper<>(ProjectInfo.class));
+        int all = sheet.getRow(0).getLastCellNum();
+        int mergedCellCnt = sheet.getNumMergedRegions();
+        for (
+            int i = 0;
+            i < mergedCellCnt - 1; i++) {
+            CellRangeAddress mergedCell = sheet.getMergedRegion(i);
+            int xx = mergedCell.getNumberOfCells();
+            if (xx <= all) {
+                int fisRow = mergedCell.getFirstRow();
+                int firstCol = mergedCell.getFirstColumn();
+                Cell cell = sheet.getRow(fisRow).getCell(firstCol);
+                short fontIndex = cell.getCellStyle().getFontIndex();
+                Font oldFontAt = workbook.getFontAt(fontIndex);
+                Font redFont = workbook.createFont();
+                redFont.setFontHeightInPoints(oldFontAt.getFontHeightInPoints());//设置字体大小
+                redFont.setFontName(oldFontAt.getFontName());//设置字体
+                CellStyle newStyle = workbook.createCellStyle();//创建单元格样式
+                newStyle.cloneStyleFrom(cell.getCellStyle());
+                short fontHeightInPoints = redFont.getFontHeightInPoints();
+                if (fontHeightInPoints >= 14 && StringUtils.isEmpty(cell.getStringCellValue()) && fisRow <= 8) {
+                    String title = projectInfo.getProjectName();
+                    if (title.length() >= 30) {
+                        sheet.getRow(fisRow).setHeight((short) 900);
+                        newStyle.setWrapText(true);
+                    }
+                    redFont.setBold(true);
+                    newStyle.setFont(redFont);
+                    cell.setCellStyle(newStyle);
+                    cell.setCellValue(title);
+                    break;
+                }
+            }
+        }
+
+        //数据不为空,构造数据
+        String fileUrl = wbsTreeContract.getHtmlUrl();
+        InputStream fileInputStream = FileUtils.getInputStreamByUrl(fileUrl);
+
+        String htmlString = IoUtil.readToString(fileInputStream);
+        htmlString = htmlString.replaceAll("placeholder", "placeholderxx");
+        htmlString = htmlString.replaceAll("title", "titlexx");
+
+        Document doc = Jsoup.parse(htmlString);
+        Element table = doc.select("table").first();
+        Elements trs = table.select("tr");
+
+        if (ObjectUtil.isNotEmpty(DataInfo)) {
+            for (String val : Objects.requireNonNull(DataInfo).keySet()) {
+                if (val.contains("__")) {
+                    String[] DataVal = val.split("__");
+                    String[] xy = DataVal[1].split("_");
+                    if (Integer.parseInt(xy[0]) < trs.size()) {
+                        Element ytzData = trs.get(Integer.parseInt(xy[0]));
+                        if (ytzData != null) {
+                            Elements tdsx = ytzData.select("td");
+                            if (Integer.parseInt(xy[1]) < tdsx.size()) {
+                                Element data = ytzData.select("td").get(Integer.parseInt(xy[1]));
+                                if (data != null) {
+                                    if (data.html().contains("x1") && data.html().contains("y1")) {
+                                        int x1 = 0;
+                                        int x2 = 0;
+                                        int y1 = 0;
+                                        int y2 = 0;
+                                        if (data.html().contains("el-tooltip")) {
+                                            x1 = Integer.parseInt(data.children().get(0).children().get(0).attr("x1"));
+                                            x2 = Integer.parseInt(data.children().get(0).children().get(0).attr("x2"));
+                                            y1 = Integer.parseInt(data.children().get(0).children().get(0).attr("y1"));
+                                        } else {
+                                            x1 = Integer.parseInt(data.children().get(0).attr("x1"));
+                                            y1 = Integer.parseInt(data.children().get(0).attr("y1"));
+                                        }
+                                        if (x1 == 0) {
+                                            x1 = 1;
+                                        }
+                                        String myData = DataInfo.get(val) + "";
+                                        if (myData.contains("T") && myData.contains("-") && myData.contains(":")) {
+                                            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+                                            sdf.setTimeZone(TimeZone.getTimeZone("GTM+8"));
+                                            SimpleDateFormat formatStr = new SimpleDateFormat("yyyy年MM月dd日");
+                                            if (myData.contains(",") && myData.contains("]")) {
+
+                                                myData = myData.replace("[", "").replace("]", "").replaceAll("'", "");
+                                                String[] dataVal = myData.split(",");
+
+                                                Date Start_dataStr = sdf.parse(dataVal[0]);
+                                                Date end_dataStr = sdf.parse(dataVal[1]);
+                                                String StartDate = formatStr.format(Start_dataStr);
+                                                String endDate = formatStr.format(end_dataStr);
+                                                if (StartDate.equals(endDate)) {
+                                                    myData = StartDate;
+                                                } else {
+                                                    myData = StartDate + "-" + endDate;
+                                                }
+                                            } else {
+                                                String[] dataStr = myData.split("T")[0].split("-");
+                                                myData = StringUtil.format("{}年{}月{}日", dataStr[0], dataStr[1], Integer.parseInt(dataStr[2]));
+                                            }
+                                        }
+
+                                        if (myData.contains("lang.String")) {
+                                            Object obj = DataInfo.get(val);
+                                            if (obj instanceof String[]) {
+                                                String[] dataDate = (String[]) obj;
+                                                myData = dataDate[0].trim() + "至" + dataDate[1].trim();
+                                                if (dataDate[0].trim().equals(dataDate[1].trim())) {
+                                                    myData = dataDate[0];
+                                                }
+                                            }
+                                        }
+
+                                        if (myData.contains("http") && myData.contains("aliyuncs")) {
+                                            InputStream imageIn = CommonUtil.getOSSInputStream(myData);
+                                            byte[] byteNew = new byte[0];
+                                            if (imageIn != null) {
+                                                byteNew = IOUtils.toByteArray(imageIn);
+                                            }
+
+                                            byte[] bytes = CommonUtil.compressImage(byteNew);
+
+                                            CreationHelper helper = workbook.getCreationHelper();
+                                            ClientAnchor anchor = helper.createClientAnchor();
+                                            anchor.setCol1(x1); // param1是列号
+                                            anchor.setCol2(x2);
+                                            anchor.setRow1(y1); // param2是行号
+                                            anchor.setRow2(y2); // param2是行号
+
+                                            Drawing<?> drawing = sheet.createDrawingPatriarch();
+                                            anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
+                                            // 插入图片
+                                            Picture picture = drawing.createPicture(anchor, workbook.addPicture(bytes, Workbook.PICTURE_TYPE_PNG));
+                                            picture.resize(1, 1);
+                                            FileUtils.imageOrientation(sheet, anchor, new DataVO(x1 - 1, y1 - 1));
+
+                                        } else if (myData.equals("1") && data.html().contains("hc-form-checkbox-group")) {
+                                            Row row = sheet.getRow(y1 - 1);
+                                            if (row != null) {
+                                                Cell cell = row.getCell(x1 - 1);
+                                                if (cell != null) {
+                                                    String exceVal = cell.getStringCellValue().replaceAll(" ", "");
+                                                    short fontIndex = cell.getCellStyle().getFontIndex();
+                                                    Font fontAt = workbook.getFontAt(fontIndex);
+                                                    fontAt.setFontName("EUDC");
+                                                    cell.setCellValue(exceVal.replace("□", "\u2611"));
+                                                } else {
+                                                    ObjectUtils.isNotEmpty(cell);
+                                                }
+                                            }
+                                        } else {
+                                            Row row = sheet.getRow(y1 - 1);
+                                            if (row != null) {
+                                                Cell cell = row.getCell(x1 - 1);
+                                                if (cell != null) {
+                                                    if(myData.contains("[")&&myData.contains("]")){
+                                                        myData=myData.replace("[", "").replace("]", "");
+                                                        if (myData.contains(",")){
+                                                            myData=  myData.replace(", ","至");
+                                                        }
+                                                        if (myData.contains(",")){
+                                                            myData=  myData.replace(", ","至");
+                                                        }
+                                                    }
+                                                    cell.setCellValue(myData);
+                                                } else {
+                                                    ObjectUtils.isNotEmpty(cell);
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        //输出流
+        FileOutputStream outputStream = new FileOutputStream(excelPath);
+        workbook.write(outputStream);
+        FileUtils.setExcelScaleToPdf(excelPath, pdfPath);
+        BladeFile bladeFile = newIOSSClient.uploadFile(id + ".pdf", pdfPath);
+        if (bladeFile != null) {
+            String sql3="update m_service_plan set pdf_url=' "+bladeFile.getLink()+" ' where id="+id;
+            jdbcTemplate.update(sql3);
+        }
+
+    }
+
+    @Override
+    public Map<String, Object> getServiceBussData(Long id, Long pkeyId) {
+        Map<String, Object> reData = new HashMap<>();
+        String sqll="select * from m_wbs_tree_contract where p_key_id="+pkeyId;
+        WbsTreeContract wbsTreeContract = jdbcTemplate.queryForObject(sqll, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+        if (wbsTreeContract == null) {
+            throw new ServiceException("该数据下无此节点!");
+        }
+        if (wbsTreeContract.getHtmlUrl() == null) {
+            throw new ServiceException("请关联清表!");
+        }
+        //表单是否存储在
+        String tabName = wbsTreeContract.getInitTableName();
+        String isExitSql = "select * from information_schema.TABLES where TABLE_NAME='" + tabName + "'";
+        List<Map<String, Object>> tabList = jdbcTemplate.queryForList(isExitSql);
+        if (tabList.size() <= 0) {
+            throw new ServiceException("请关联清表!");
+        }
+        //实体数据
+        String querySql = "select * from " + wbsTreeContract.getInitTableName() + " where p_key_id=" + pkeyId + " and group_id = " + id;
+        List<Map<String, Object>> dataIn = jdbcTemplate.queryForList(querySql);
+        //匹配关联
+        try {
+            if (dataIn.size() >= 1) {
+                Map<String, Object> mysqlData = dataIn.get(0);
+                for (String key : mysqlData.keySet()) {
+                    String tabVal = mysqlData.get(key) + "";
+                    // 时间段处理
+                    if (StringUtils.isNotEmpty(tabVal) && !tabVal.equals("null")) {
+                        if (tabVal.contains("T") && tabVal.contains(".000Z]")) {
+                            String[] tabData = tabVal.split("_\\^_");
+
+                            if (reData.containsKey("pickerKey")) {
+                                String pickerKey = reData.get("pickerKey") + "," + key + "__" + tabData[1];
+                                reData.put("pickerKey", pickerKey);
+                            } else {
+                                reData.put("pickerKey", key + "__" + tabData[1]);
+                            }
+
+                            String sql = tabData[0];
+                            sql = sql.replaceAll("\\[", "['");
+                            sql = sql.replaceAll("]", "']");
+                            sql = sql.replaceAll("000Z,", "000Z',");
+                            sql = sql.replaceAll(", 20", ", '20");
+                            //   sql = sql.replaceAll("'", "");
+                            if (StringUtils.isNotEmpty(tabData[0])) {
+                                reData.put(key + "__" + tabData[1], sql);
+                            }
+                        } else if (tabVal.indexOf("T") >= 0 && tabVal.indexOf(".000Z") >= 0) {//时间
+                            // 时间和字符串合作
+                            if (tabVal.indexOf("☆") >= 0) {
+                                String[] mysql = tabVal.split("☆");
+                                for (String data : mysql) {
+                                    String[] tabData = data.split("_\\^_");
+                                    if (StringUtils.isNotEmpty(tabData[0])) {
+                                        reData.put(key + "__" + tabData[1], tabData[0]);
+                                    }
+                                }
+                            } else {
+                                String[] tabData = tabVal.split("_\\^_");
+                                if (StringUtils.isNotEmpty(tabData[0])) {
+                                    reData.put(key + "__" + tabData[1], tabData[0]);
+                                }
+                            }
+                        } else if (tabVal.indexOf("☆") >= 0) {
+                            String[] mysql = tabVal.split("☆");
+                            for (String data : mysql) {
+                                String[] tabData = data.split("_\\^_");
+                                if (tabVal.contains("[")) {
+                                    String[] strings = StringUtils.strip(tabData[0], "[]").split(",");
+                                    reData.put(key + "__" + tabData[1], strings);
+                                } else {
+                                    reData.put(key + "__" + tabData[1], tabData[0]);
+                                }
+                            }
+                        } else if (tabVal.indexOf("_^_") >= 0) {
+                            String[] tabData = tabVal.split("_\\^_");
+                            if (StringUtils.isNotEmpty(tabData[0])) {
+                                if (tabVal.contains("[")) {
+                                    String[] strings = StringUtils.strip(tabData[0], "[]").split(",");
+                                    reData.put(key + "__" + tabData[1], strings);
+                                } else {
+                                    reData.put(key + "__" + tabData[1], tabData[0]);
+                                }
+                            }
+                        } else {
+                            reData.put(key, tabVal);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return reData;
+    }
+
+    @Override
+    public Map<String,List<ServiceUserDto>> getSendUserAndWriteUser(Long projectId, Long contractId) {
+        HashMap<String, List<ServiceUserDto>> map = new HashMap<>();
+        List<String> sendList=baseMapper.getUserID(projectId, contractId,1);
+        List<String> writeList=baseMapper.getUserID(projectId, contractId,2);
+        if (!sendList.isEmpty()) {
+            List<String> sendResult = sendList.stream()
+                .filter(Objects::nonNull)
+                .filter(str -> !str.isEmpty())
+                .flatMap(str -> Arrays.stream(str.split(",")))
+                .distinct()
+                .sorted()
+                .collect(Collectors.toList());
+            map.put("sendUser", getUserNamesByIds(sendResult));
+        }
+
+        if (!writeList.isEmpty()) {
+            List<String> writedResult = writeList.stream()
+                .filter(Objects::nonNull)
+                .filter(str -> !str.isEmpty())
+                .flatMap(str -> Arrays.stream(str.split(",")))
+                .distinct()
+                .sorted()
+                .collect(Collectors.toList());
+            map.put("writeUser", getUserNamesByIds(writedResult));
+        }
+        return map;
+    }
+
+    @Override
+    public Long saveServicePlan(Long pkeyId,Long projectId,Long contractId,JSONObject dataInfo,Long groupId1) {
+        ServicePlan plan = new ServicePlan();
+        if(dataInfo.containsKey("key_15__3_4")){
+            Object o = dataInfo.get("key_15__3_4");
+            String startDate = null;
+            String endDate = null;
+            if (o instanceof List<?>) {
+                List<?> list = (List<?>) o;
+                if (list.size() >= 2 && list.get(0) instanceof String && list.get(1) instanceof String) {
+                    startDate = formatDate((String) list.get(0));
+                    endDate = formatDate((String) list.get(1));
+                }
+            } else if (o != null && o.getClass().isArray()) {
+                Object[] array = (Object[]) o;
+                if (array.length >= 2 && array[0] instanceof String && array[1] instanceof String) {
+                    startDate = formatDate((String) array[0]);
+                    endDate = formatDate((String) array[1]);
+                }
+            }
+            if (startDate != null) {
+                plan.setPlanStartTime(LocalDate.parse(startDate));
+            }
+            if (endDate != null) {
+                plan.setPlanEndTime(LocalDate.parse(endDate));
+            }
+        }
+        if(dataInfo.containsKey("key_42__3_4")){
+            Object o = dataInfo.get("key_42__3_4");
+            String startDate = null;
+            String endDate = null;
+            if (o instanceof List<?>) {
+                List<?> list = (List<?>) o;
+                if (list.size() >= 2 && list.get(0) instanceof String && list.get(1) instanceof String) {
+                    startDate = formatDate((String) list.get(0));
+                    endDate = formatDate((String) list.get(1));
+                }
+            } else if (o != null && o.getClass().isArray()) {
+                Object[] array = (Object[]) o;
+                if (array.length >= 2 && array[0] instanceof String && array[1] instanceof String) {
+                    startDate = formatDate((String) array[0]);
+                    endDate = formatDate((String) array[1]);
+                }
+            }
+            if (startDate != null) {
+                plan.setPlanStartTime(LocalDate.parse(startDate));
+            }
+            if (endDate != null) {
+                plan.setPlanEndTime(LocalDate.parse(endDate));
+            }
+        }
+        if(groupId1 != null){
+            plan.setId(groupId1);
+            ServicePlan plan1 = this.getById(plan.getId());
+            if(plan1.getWriteUser()!=null){
+                if(plan1.getStatus()!=2&&!plan1.getWriteUser().contains(SecureUtil.getUserId()+"")){
+                    plan.setWriteUser(plan1.getWriteUser()+","+SecureUtil.getUserId());
+                }
+            }
+            if(plan1.getStatus()!=null){
+                plan.setStatus(plan1.getStatus());
+            }
+        }else {
+            plan.setProjectId(projectId);
+            plan.setContractId(contractId);
+            plan.setStatus(1);
+            plan.setWriteUser(SecureUtil.getUserId()+"");
+            if(pkeyId==1937773223861026820L){
+                plan.setFileInType(1);
+            }else {
+                plan.setFileInType(2);
+            }
+        }
+        this.saveOrUpdate(plan);
+        return plan.getId();
+    }
+
+    @Override
+    public ServicePlanVO getdetail(Long id) {
+        ServicePlan plan = this.getById(id);
+        ServicePlanVO vo = new ServicePlanVO();
+        BeanUtil.copyProperties(plan, vo);
+        vo.setStatusValue(vo.getStatus()==1?"计划中":vo.getStatus()==2?"协同中-甲方":vo.getStatus()==3?"协同中-系统":"已计划");
+        vo.setIsEdit(checkIsEdit(vo));
+        if(vo.getPlanStartTime()!=null){
+            vo.setStartTime(vo.getPlanStartTime().format(DateTimeFormatter.ofPattern("yyyy年M月d日")));
+        }
+        if(vo.getPlanEndTime()!=null){
+            vo.setEndTime(vo.getPlanEndTime().format(DateTimeFormatter.ofPattern("yyyy年M月d日")));
+        }
+        if(StringUtils.isNotEmpty(vo.getStartTime())&&StringUtils.isNotEmpty(vo.getEndTime())){
+            vo.setPlanTime(vo.getStartTime()+"至"+vo.getEndTime());
+        }
+        vo.setWriteUserName(slectUserName(vo.getWriteUser()));
+        vo.setSendUserName(slectUserName(vo.getSendUser()));
+        return vo;
+    }
+
+    private static String formatDate(String dateStr) {
+        if (dateStr == null || dateStr.trim().isEmpty()) return null;
+        try {
+            // 替换掉“年”、“月”、“日”字符,然后解析为LocalDate
+            return dateStr.replaceAll("年", "-")
+                .replaceAll("月", "-")
+                .replaceAll("日", "")
+                .trim();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    public List<ServiceUserDto> getUserNamesByIds(List<String> sendResult) {
+        if (sendResult == null || sendResult.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // 构建 SQL 查询语句
+        String inSql = sendResult.stream()
+            .map(id -> "?")
+            .collect(Collectors.joining(", "));
+
+        String sql = "SELECT id, real_name FROM blade_user WHERE id IN (" + inSql + ")";
+
+        try {
+            List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, sendResult.toArray());
+
+            return rows.stream()
+                .map(row -> new ServiceUserDto(
+                    row.get("id").toString(),
+                    row.get("real_name").toString()))
+                .collect(Collectors.toList());
+        } catch (DataAccessException e) {
+            // 日志记录异常信息
+            log.error("查询用户信息失败", e);
+            return Collections.emptyList();
+        }
+    }
+
+    private List<Map<String, Object>> getBussDataInfoTrial(Long pkeyId ,Long id) {
+        String file_path = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+        String sys_file_net_url = ParamCache.getValue(CommonConstant.SYS_FILE_NET_URL);
+        List<Map<String, Object>> list = new ArrayList<>();
+        Map<String, Object> reData = new HashMap<>();
+        String sqll="select * from m_wbs_tree_contract where p_key_id="+pkeyId;
+        WbsTreeContract wbsTreeContract = jdbcTemplate.queryForObject(sqll, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+        if (wbsTreeContract == null) {
+            return list;
+        }
+        if (wbsTreeContract.getHtmlUrl() == null) {
+            return list;
+        }
+        //表单是否存储在
+        String tabName = wbsTreeContract.getInitTableName();
+        String isExitSql = "select * from information_schema.TABLES where TABLE_NAME='" + tabName + "'";
+        List<Map<String, Object>> tabList = jdbcTemplate.queryForList(isExitSql);
+        if (tabList.size() <= 0) {
+            return list;
+        }
+
+        //实体数据
+        String querySql = "select * from " + wbsTreeContract.getInitTableName() + " where p_key_id=" + pkeyId + " and group_id = " + id;
+        List<Map<String, Object>> dataIn = jdbcTemplate.queryForList(querySql);
+        String keyNames="";
+        //匹配关联
+        try {
+            String fileUrl = wbsTreeContract.getHtmlUrl();
+            File file1 = ResourceUtil.getFile(fileUrl);
+            InputStream fileInputStream;
+            if (file1.exists()) {
+                fileInputStream = new FileInputStream(file1);
+            } else {
+                String path = sys_file_net_url + fileUrl.replaceAll("//", "/").replaceAll(file_path, "");
+                fileInputStream = CommonUtil.getOSSInputStream(path);
+            }
+            String htmlString = IoUtil.readToString(fileInputStream);
+            htmlString = htmlString.replaceAll("placeholder", "placeholderxx");
+            htmlString = htmlString.replaceAll("title", "titlexx");
+            Document doc = Jsoup.parse(htmlString);
+            keyNames= getKeyNameList(doc);
+                if (dataIn.size() >= 1) {
+                    Map<String, Object> mysqlData = dataIn.get(0);
+                    for (String key : mysqlData.keySet()) {
+                        String tabVal = mysqlData.get(key) + "";
+                        // 时间段处理
+                        if (StringUtils.isNotEmpty(tabVal) && !tabVal.equals("null")) {
+                            if (tabVal.contains("T") && tabVal.contains(".000Z]")) {
+                                String[] tabData = tabVal.split("_\\^_");
+
+                                if (reData.containsKey("pickerKey")) {
+                                    String pickerKey = reData.get("pickerKey") + "," + key + "__" + tabData[1];
+                                    reData.put("pickerKey", pickerKey);
+                                } else {
+                                    reData.put("pickerKey", key + "__" + tabData[1]);
+                                }
+
+                                String sql = tabData[0];
+                                sql = sql.replaceAll("\\[", "['");
+                                sql = sql.replaceAll("]", "']");
+                                sql = sql.replaceAll("000Z,", "000Z',");
+                                sql = sql.replaceAll(", 20", ", '20");
+                                //   sql = sql.replaceAll("'", "");
+                                if (StringUtils.isNotEmpty(tabData[0])) {
+                                    reData.put(key + "__" + tabData[1], sql);
+                                }
+                            } else if (tabVal.indexOf("T") >= 0 && tabVal.indexOf(".000Z") >= 0) {//时间
+                                // 时间和字符串合作
+                                if (tabVal.indexOf("☆") >= 0) {
+                                    String[] mysql = tabVal.split("☆");
+                                    for (String data : mysql) {
+                                        String[] tabData = data.split("_\\^_");
+                                        if (StringUtils.isNotEmpty(tabData[0])) {
+                                            reData.put(key + "__" + tabData[1], tabData[0]);
+                                        }
+                                    }
+                                } else {
+                                    String[] tabData = tabVal.split("_\\^_");
+                                    if (StringUtils.isNotEmpty(tabData[0])) {
+                                        reData.put(key + "__" + tabData[1], tabData[0]);
+                                    }
+                                }
+                            } else if (tabVal.indexOf("☆") >= 0) {
+                                String[] mysql = tabVal.split("☆");
+                                for (String data : mysql) {
+                                    String[] tabData = data.split("_\\^_");
+                                    if (StringUtils.isNotEmpty(tabData[0])) {
+                                        reData.put(key + "__" + tabData[1], tabData[0]);
+                                    }
+                                }
+                            } else if (tabVal.indexOf("_^_") >= 0) {
+                                String[] tabData = tabVal.split("_\\^_");
+                                if (StringUtils.isNotEmpty(tabData[0])) {
+                                    if (tabVal.contains("[") && tabVal.contains("年")) {
+                                        String[] strings = StringUtils.strip(tabData[0], "[]").split(",");
+                                        reData.put(key + "__" + tabData[1], strings);
+                                    }else if (tabVal.contains("[") && tabVal.contains("]") ) {
+                                        String[] strings = StringUtils.strip(tabData[0], "[]").split(",");
+                                        reData.put(key + "__" + tabData[1], strings);
+                                    } else {
+                                        reData.put(key + "__" + tabData[1], tabData[0]);
+                                    }
+                                }
+                            } else {
+                                reData.put(key, tabVal);
+                            }
+                        }
+                    }
+                }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        // 移除Id 和 p_key_id
+        reData.remove("id");
+        reData.remove("p_key_id");
+        reData.remove("classify");
+        reData.remove("contractId");
+        reData.remove("pkeyId");
+        reData.remove("projectId");
+        if(reData.size()>0){
+            //处理key重复导致pdf数据错位
+            for (Iterator<Map.Entry<String, Object>> iterator = reData.entrySet().iterator(); iterator.hasNext(); ) {
+                Map.Entry<String, Object> entry = iterator.next();
+                if (!keyNames.equals("") && keyNames.indexOf(entry.getKey()) <0) {
+                    iterator.remove();
+                }
+            }
+        }
+        list.add(reData);
+        return list;
+    }
+    public String getKeyNameList(Document doc){
+        StringBuilder allKeyName=new StringBuilder();
+        Elements select = doc.getElementsByAttributeValueContaining("keyname","key_");
+        if(!select.isEmpty()){
+            for (Element element : select) {
+                String keyName = element.attr("keyname");
+                allKeyName.append(keyName+",");
+            }
+        }
+        String allKeyNames = allKeyName.toString();
+        if(allKeyNames.endsWith(",")){
+            allKeyNames = allKeyNames.substring(0, allKeyNames.length() - 1);
+        }
+        return allKeyNames;
+    }
+
+    public TransactionStatus beginTransaction(DataSourceTransactionManager transactionManager) {
+        DefaultTransactionDefinition def = new DefaultTransactionDefinition();//事务定义类
+        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
+        return transactionManager.getTransaction(def);
+    }
+
+
+
+}

+ 50 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ServicePlanTaskServiceImpl.java

@@ -0,0 +1,50 @@
+/*
+ *      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.impl;
+
+import org.springblade.manager.entity.ServicePlanTask;
+import org.springblade.manager.vo.ServicePlanTaskVO;
+import org.springblade.manager.mapper.ServicePlanTaskMapper;
+import org.springblade.manager.service.IServicePlanTaskService;
+import org.springblade.core.mp.base.BaseServiceImpl;
+import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ *  服务实现类
+ *
+ * @author BladeX
+ * @since 2025-06-27
+ */
+@Service
+public class ServicePlanTaskServiceImpl extends BaseServiceImpl<ServicePlanTaskMapper, ServicePlanTask> implements IServicePlanTaskService {
+
+	@Override
+	public IPage<ServicePlanTaskVO> selectServicePlanTaskPage(IPage<ServicePlanTaskVO> page, ServicePlanTaskVO servicePlanTask) {
+		return page.setRecords(baseMapper.selectServicePlanTaskPage(page, servicePlanTask));
+	}
+
+	@Override
+	public List<ServicePlanTask> getTaskByServicePlanId(Long id) {
+
+		return baseMapper.getTaskByServicePlanId(id);
+	}
+
+}

+ 259 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousEViSaServiceImpl.java

@@ -0,0 +1,259 @@
+package org.springblade.manager.service.impl;
+
+import cn.hutool.core.date.DateTime;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import org.springblade.common.utils.SnowFlakeUtil;
+import org.springblade.core.tool.utils.CollectionUtil;
+import org.springblade.manager.entity.*;
+import org.springblade.manager.mapper.TextdictInfoMapper;
+import org.springblade.manager.mapper.WbsTreeContractMapper;
+import org.springblade.manager.mapper.WbsTreePrivateMapper;
+import org.springblade.manager.mapper.WbsTreeSynchronousRecordMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author LHB
+ */
+@Service
+public class WbsSynchronousEViSaServiceImpl {
+
+    @Autowired
+    private WbsTreePrivateMapper wbsTreePrivateMapper;
+    @Autowired
+    private WbsTreeContractMapper wbsTreeContractMapper;
+    //电签
+    @Autowired
+    private TextdictInfoMapper textdictInfoMapper;
+    @Autowired
+    private WbsTreeSynchronousRecordMapper synchronousRecordMapper;
+
+    //公式
+    @Autowired
+    private ElementFormulaMappingServiceImpl elementFormulaMappingService;
+
+    @Transactional(rollbackFor = Exception.class)
+    public void updateTextDictInfo(Long projectId, List<Long> editPrivateIds, List<TextdictInfo> addData) {
+        //删除 需要新增的节点的电签和默认值
+        textdictInfoMapper.delete(Wrappers.<TextdictInfo>lambdaQuery()
+                .eq(TextdictInfo::getProjectId, projectId)
+                .in(TextdictInfo::getTabId, editPrivateIds));
+
+        textdictInfoMapper.insertBatchSomeColumn(addData);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSyncPrivateForm(WbsTreePrivate wbsTreePrivate, List<WbsTreePrivate> collect, Long id,String errorMsg) {
+        List<Long> ids = collect.stream().map(WbsTreePrivate::getPKeyId).collect(Collectors.toList());
+        List<Long> parentIds = collect.stream().map(WbsTreePrivate::getParentId).collect(Collectors.toList());
+        //节点公式同步
+        for (Long parentId : parentIds) {
+            saveFormula(Long.valueOf(wbsTreePrivate.getProjectId()), wbsTreePrivate.getParentId(), parentId);
+        }
+
+        wbsTreePrivateMapper.update(null, Wrappers.<WbsTreePrivate>lambdaUpdate()
+                .set(WbsTreePrivate::getInitTableId, wbsTreePrivate.getInitTableId())
+                .set(WbsTreePrivate::getInitTableName, wbsTreePrivate.getInitTableName())
+                .set(WbsTreePrivate::getHtmlUrl, wbsTreePrivate.getHtmlUrl())
+                .set(wbsTreePrivate.getExcelId()!=null,WbsTreePrivate::getIsLinkTable, 2)
+                .set(WbsTreePrivate::getExcelId, wbsTreePrivate.getExcelId())
+                .in(WbsTreePrivate::getPKeyId, ids)
+        );
+        synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                .set(WbsTreeSynchronousRecord::getStatus, 2)
+                .set(WbsTreeSynchronousRecord::getErrorMsg, errorMsg)
+                .set(WbsTreeSynchronousRecord::getNodeNumEnd, 1)
+                .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                .eq(WbsTreeSynchronousRecord::getId, id));
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void syncPrivateForceForm(WbsTreePrivate wbsTreePrivate, String nodeId, Long id) {
+        WbsTreePrivate wbsTreePrivate1 = wbsTreePrivateMapper.selectById(nodeId);
+
+        wbsTreePrivateMapper.update(null, Wrappers.<WbsTreePrivate>lambdaUpdate()
+                .set(WbsTreePrivate::getExcelId, wbsTreePrivate.getExcelId())
+                .set(WbsTreePrivate::getInitTableName, wbsTreePrivate.getInitTableName())
+                .set(WbsTreePrivate::getInitTableId, wbsTreePrivate.getInitTableId())
+                .set(WbsTreePrivate::getHtmlUrl, wbsTreePrivate.getHtmlUrl())
+                .set(StringUtils.isBlank(wbsTreePrivate.getFullName()),WbsTreePrivate::getFullName, wbsTreePrivate1.getNodeName())
+                .set(WbsTreePrivate::getNodeName, wbsTreePrivate.getNodeName())
+                .in(WbsTreePrivate::getPKeyId, nodeId)
+        );
+        synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                .set(WbsTreeSynchronousRecord::getStatus, 2)
+                .set(WbsTreeSynchronousRecord::getErrorMsg, null)
+                .set(WbsTreeSynchronousRecord::getNodeNumEnd, 1)
+                .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                .eq(WbsTreeSynchronousRecord::getId, id));
+    }
+
+
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePrivate(String type, Long pId, Long createUserId, List<WbsTreePrivate> list) {
+        //排序调整
+        if (type.contains("7")) {
+            list.sort(Comparator.comparingInt(WbsTreePrivate::getSort));
+            List<Long> pKeyIds = list.stream().map(WbsTreePrivate::getPKeyId).collect(Collectors.toList());
+            //获取节点下的当前表单
+            List<WbsTreePrivate> resourceData = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                    .select(WbsTreePrivate::getPKeyId, WbsTreePrivate::getSort)
+                    .eq(WbsTreePrivate::getPId, pId)
+                    .eq(WbsTreePrivate::getIsDeleted, 0)
+                    .notIn(WbsTreePrivate::getPKeyId, pKeyIds)
+                    .orderByAsc(WbsTreePrivate::getSort));
+            if (CollectionUtil.isNotEmpty(resourceData)) {
+                for (int i = 0; i < resourceData.size(); i++) {
+                    resourceData.get(i).setSort(i + 1);
+                }
+                //修改排序为连续排序
+                wbsTreePrivateMapper.updateSortBatchByPKeyId(resourceData);
+
+            }
+
+            for (WbsTreePrivate wbsTreePrivate : list) {
+                wbsTreePrivateMapper.updateSortByPId(pId, wbsTreePrivate.getSort());
+            }
+        }
+
+        for (WbsTreePrivate treePrivate : list) {
+            treePrivate.setUpdateTime(DateTime.now());
+            treePrivate.setUpdateUser(createUserId);
+            wbsTreePrivateMapper.updateById(treePrivate);
+        }
+
+        //更新之后再去修改所有的排序
+        if (type.contains("7")) {
+            //获取节点下的当前表单
+            List<WbsTreePrivate> resourceData = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                    .select(WbsTreePrivate::getPKeyId, WbsTreePrivate::getSort)
+                    .eq(WbsTreePrivate::getPId, pId)
+                    .eq(WbsTreePrivate::getIsDeleted, 0)
+                    .orderByAsc(WbsTreePrivate::getSort));
+            if (CollectionUtil.isNotEmpty(resourceData)) {
+                for (int i = 0; i < resourceData.size(); i++) {
+                    resourceData.get(i).setSort(i + 1);
+                }
+                //修改排序为连续排序
+                wbsTreePrivateMapper.updateSortBatchByPKeyId(resourceData);
+            }
+        }
+
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void updateContract(String type, Long pId, Long createUserId, List<WbsTreeContract> list) {
+        //排序调整
+        if (type.contains("7")) {
+            list.sort(Comparator.comparingInt(WbsTreeContract::getSort));
+
+            List<Long> pKeyIds = list.stream().map(WbsTreeContract::getPKeyId).collect(Collectors.toList());
+            //获取节点下的当前表单
+            List<WbsTreeContract> resourceData = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+                    .select(WbsTreeContract::getPKeyId, WbsTreeContract::getSort)
+                    .eq(WbsTreeContract::getPId, pId)
+                    .eq(WbsTreeContract::getIsDeleted, 0)
+                    .notIn(WbsTreeContract::getPKeyId, pKeyIds)
+                    .orderByAsc(WbsTreeContract::getSort));
+            if (CollectionUtil.isNotEmpty(resourceData)) {
+                for (int i = 0; i < resourceData.size(); i++) {
+                    resourceData.get(i).setSort(i + 1);
+                }
+                //修改排序为连续排序
+                wbsTreeContractMapper.updateSortBatchByPKeyId(resourceData);
+            }
+
+            for (WbsTreeContract wbsTreePrivate : list) {
+                wbsTreeContractMapper.updateSortByPId(pId, wbsTreePrivate.getSort());
+            }
+        }
+
+        for (WbsTreeContract treeContract : list) {
+            treeContract.setUpdateTime(DateTime.now());
+            treeContract.setUpdateUser(createUserId);
+            wbsTreeContractMapper.updateById(treeContract);
+        }
+
+        //排序调整
+        if (type.contains("7")) {
+            //获取节点下的当前表单
+            List<WbsTreeContract> resourceData = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+                    .select(WbsTreeContract::getPKeyId, WbsTreeContract::getSort)
+                    .eq(WbsTreeContract::getPId, pId)
+                    .eq(WbsTreeContract::getIsDeleted, 0)
+                    .orderByAsc(WbsTreeContract::getSort));
+            if (CollectionUtil.isNotEmpty(resourceData)) {
+                for (int i = 0; i < resourceData.size(); i++) {
+                    resourceData.get(i).setSort(i + 1);
+                }
+                //修改排序为连续排序
+                wbsTreeContractMapper.updateSortBatchByPKeyId(resourceData);
+            }
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void saveFormula(WbsTreeSynchronousRecord wbsTreeSynchronousRecord, List<WbsTreePrivate> list) {
+        Map<Long, List<WbsTreePrivate>> collect = list.stream().collect(Collectors.groupingBy(WbsTreePrivate::getParentId));
+        Set<Long> ids = collect.keySet();
+        if (CollectionUtil.isEmpty(ids)) {
+            return;
+        }
+        List<ElementFormulaMapping> elementFormulaMappings = elementFormulaMappingService.list(Wrappers.<ElementFormulaMapping>lambdaQuery()
+                .eq(ElementFormulaMapping::getProjectId, wbsTreeSynchronousRecord.getTemplateId())
+                .in(ElementFormulaMapping::getNodeId, ids));
+        elementFormulaMappings.forEach(f -> {
+            f.setId(SnowFlakeUtil.getId());
+            f.setProjectId(wbsTreeSynchronousRecord.getProjectId());
+            f.setCreateTime(DateTime.now());
+        });
+
+        //删除当前节点的公式
+        elementFormulaMappingService.remove(Wrappers.<ElementFormulaMapping>lambdaQuery()
+                .eq(ElementFormulaMapping::getProjectId, wbsTreeSynchronousRecord.getProjectId())
+                .in(ElementFormulaMapping::getNodeId, ids));
+        //复制新增模板节点的公式
+        elementFormulaMappingService.saveBatch(elementFormulaMappings);
+
+    }
+
+    /**
+     * 同项目下不同节点 不同的节点公式
+     * @param projectId 项目id
+     * @param tempParentId 源节点父级id
+     * @param parentId 节点父级id
+     */
+    public void saveFormula(Long projectId, Long tempParentId, Long parentId) {
+
+        List<ElementFormulaMapping> tempElementFormulaMappings = elementFormulaMappingService.list(Wrappers.<ElementFormulaMapping>lambdaQuery()
+                .eq(ElementFormulaMapping::getProjectId, projectId)
+                .in(ElementFormulaMapping::getNodeId, tempParentId));
+        List<ElementFormulaMapping> elementFormulaMappings = elementFormulaMappingService.list(Wrappers.<ElementFormulaMapping>lambdaQuery()
+                .eq(ElementFormulaMapping::getProjectId, projectId)
+                .in(ElementFormulaMapping::getNodeId, parentId));
+        List<Long> collect = elementFormulaMappings.stream().map(ElementFormulaMapping::getElementId).collect(Collectors.toList());
+
+        //当前节点不存在的元素公式
+        List<ElementFormulaMapping> collect1 = tempElementFormulaMappings.stream().filter(f -> !collect.contains(f.getElementId())).collect(Collectors.toList());
+
+
+        collect1.forEach(f -> {
+            f.setId(SnowFlakeUtil.getId());
+            f.setProjectId(projectId);
+            f.setNodeId(parentId);
+            f.setCreateTime(DateTime.now());
+        });
+
+        //复制新增模板节点的公式
+        elementFormulaMappingService.saveBatch(collect1);
+
+    }
+}

+ 1441 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousServiceImpl.java

@@ -0,0 +1,1441 @@
+package org.springblade.manager.service.impl;
+
+import cn.hutool.core.date.DateTime;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.ObjectUtils;
+import org.springblade.business.entity.InformationQuery;
+import org.springblade.common.constant.CommonConstant;
+import org.springblade.common.utils.CommonUtil;
+import org.springblade.common.utils.SnowFlakeUtil;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.log.publisher.ErrorLogPublisher;
+import org.springblade.core.tool.utils.*;
+import org.springblade.manager.entity.*;
+import org.springblade.manager.enums.WbsSyncTypeEnum;
+import org.springblade.manager.mapper.*;
+import org.springblade.system.cache.ParamCache;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.rmi.ServerException;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Wbs 同步功能Service
+ *
+ * @author LHB
+ */
+@Service
+public class WbsSynchronousServiceImpl {
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+    //实体表
+    @Autowired
+    private TableInfoMapper tableInfoMapper;
+    //清表
+    @Autowired
+    private ExcelTabMapper excelTabMapper;
+
+    //电签同步事务类
+    @Autowired
+    private WbsSynchronousEViSaServiceImpl wbsSynchronousEViSaService;
+    //电签
+    @Autowired
+    private TextdictInfoMapper textdictInfoMapper;
+    //合同wbs
+    @Autowired
+    private WbsTreeContractMapper wbsTreeContractMapper;
+    //合同
+    @Autowired
+    private ContractInfoMapper contractInfoMapper;
+    //项目wbs
+    @Autowired
+    private WbsTreePrivateMapper wbsTreePrivateMapper;
+    //系统wbs
+    @Autowired
+    private WbsTreeMapper wbsTreeMapper;
+    //项目
+    @Autowired
+    private ProjectInfoMapper projectInfoMapper;
+    //同步信息
+    @Autowired
+    private WbsTreeSynchronousRecordMapper synchronousRecordMapper;
+
+    @Async("taskExecutor1")
+    public void syncExecute(WbsTreeSynchronousRecord wbsTreeSynchronousRecord) {
+        // 类型枚举 WbsSyncTypeEnum.ALREADY_FILLED_IN_NOT_REPORTED
+        //范围 1.项目,2.合同
+        //
+        try {
+            Integer range = wbsTreeSynchronousRecord.getRange();
+            switch (range) {
+                case 1:
+                    //同步项目
+                    insertPrivateNode(wbsTreeSynchronousRecord);
+                    break;
+                case 2:
+                    //同步合同段
+                    insertContractNode(wbsTreeSynchronousRecord);
+                    break;
+                case 3:
+                    //同步元素相同的所有表单
+                    syncPrivateAllForm(wbsTreeSynchronousRecord);
+                    break;
+                case 4:
+                    //强制同步表单
+                    syncPrivateForceForm(wbsTreeSynchronousRecord);
+                    break;
+                default:
+                    throw new ServiceException("同步类型错误");
+            }
+        } catch (Exception e) {
+            synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                    .set(WbsTreeSynchronousRecord::getStatus, 3)
+                    .set(WbsTreeSynchronousRecord::getErrorMsg, e.getMessage())
+                    .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                    .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
+            ErrorLogPublisher.publishEvent(e, null);
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 同步所有相同元素的表单
+     *
+     * @param wbsTreeSynchronousRecord
+     */
+    private void syncPrivateAllForm(WbsTreeSynchronousRecord wbsTreeSynchronousRecord) {
+        String nodeId = wbsTreeSynchronousRecord.getNodeId();
+        WbsTreePrivate wbsTreePrivate = wbsTreePrivateMapper.selectById(nodeId);
+        if (wbsTreePrivate == null) {
+            throw new ServiceException("节点/表单 不存在");
+        }
+        String initTableName = wbsTreePrivate.getInitTableName();
+        if (StringUtil.isBlank(initTableName)) {
+            throw new ServiceException("表单未绑定实体表");
+        }
+        if (wbsTreePrivate.getExcelId() == null) {
+            throw new ServiceException("表单未绑定清表");
+        }
+
+        //获取系统模板
+        WbsTree wbsTree = wbsTreeMapper.selectById(wbsTreePrivate.getId());
+        List<WbsTree> wbsTrees = wbsTreeMapper.selectList(Wrappers.<WbsTree>lambdaQuery()
+                .eq(WbsTree::getWbsId, wbsTree.getWbsId())
+                .ne(WbsTree::getId, wbsTree.getId())
+                .eq(WbsTree::getInitTableName, wbsTree.getInitTableName())
+                .eq(WbsTree::getIsDeleted, 0));
+        if(CollectionUtil.isEmpty(wbsTrees)){
+            throw new ServiceException("当前项目没有相同实体表的其他表单");
+        }
+
+        List<Long> collect3 = wbsTrees.stream().map(WbsTree::getId).collect(Collectors.toList());
+
+
+        //在指定项目下指定实体表相同的数据
+        List<WbsTreePrivate> wbsTreePrivates = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                .eq(WbsTreePrivate::getProjectId, wbsTreePrivate.getProjectId())
+                .ne(WbsTreePrivate::getPKeyId, nodeId)
+                .in(WbsTreePrivate::getId, collect3)
+        );
+        if (CollectionUtil.isEmpty(wbsTreePrivates)) {
+            throw new ServiceException("当前项目没有相同实体表的其他表单");
+        }
+
+        //筛选出有excelid的数据和无excelid的数据
+        List<WbsTreePrivate> collect1 = wbsTreePrivates.stream().filter(f -> f.getExcelId() == null).collect(Collectors.toList());
+        List<WbsTreePrivate> collect2 = wbsTreePrivates.stream().filter(f -> f.getExcelId() != null).collect(Collectors.toList());
+
+        //清表也相同的数据
+        List<WbsTreePrivate> collect = collect2.stream().filter(f ->
+                Objects.equals(f.getExcelId(), wbsTreePrivate.getExcelId())
+        ).collect(Collectors.toList());
+
+        if(CollectionUtil.isNotEmpty(collect1)){
+            collect.addAll(collect1);
+        }
+        //清表不同的数据
+        List<WbsTreePrivate> noExcelIdEq = wbsTreePrivates.stream().filter(f -> f.getExcelId()!=null && !Objects.equals(f.getExcelId(), wbsTreePrivate.getExcelId())).collect(Collectors.toList());
+        StringBuilder sb = new StringBuilder();
+        if (CollectionUtil.isNotEmpty(noExcelIdEq)) {
+            for (WbsTreePrivate treePrivate : noExcelIdEq) {
+                List<WbsTreePrivate> list = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery().in(WbsTreePrivate::getPKeyId, Func.toLongList(treePrivate.getAncestorsPId())));
+                List<String> nodeNames = list.stream().map(WbsTreePrivate::getNodeName).collect(Collectors.toList());
+
+                sb.append(StringUtil.join(nodeNames, "/")).append(";");
+            }
+//            throw new ServiceException("清表绑定错误:" + sb);
+        }
+
+        //修改数据
+        wbsSynchronousEViSaService.updateSyncPrivateForm(wbsTreePrivate, collect, wbsTreeSynchronousRecord.getId(),sb.toString());
+    }
+
+    /**
+     * 强制同步表单
+     *
+     * @param wbsTreeSynchronousRecord
+     */
+    private void syncPrivateForceForm(WbsTreeSynchronousRecord wbsTreeSynchronousRecord) {
+        String nodeId = wbsTreeSynchronousRecord.getNodeId();
+        if (nodeId.contains(",")) {
+            throw new ServiceException("不能选中多个节点");
+        }
+        String formIds = wbsTreeSynchronousRecord.getFormIds();
+        if (formIds.contains(",")) {
+            throw new ServiceException("不能选中多个节点");
+        }
+        //选中的表单
+        WbsTreePrivate wbsTreePrivate = wbsTreePrivateMapper.selectById(formIds);
+        if (wbsTreePrivate == null) {
+            throw new ServiceException("表单不存在");
+        }
+        wbsSynchronousEViSaService.syncPrivateForceForm(wbsTreePrivate, nodeId, wbsTreeSynchronousRecord.getId());
+    }
+
+    /**
+     * 项目同步 获取源节点数据和当前节点数据
+     *
+     * @param wbsTreeSynchronousRecord
+     */
+    public void insertPrivateNode(WbsTreeSynchronousRecord wbsTreeSynchronousRecord) throws IOException {
+        String sys_file_net_url = ParamCache.getValue(CommonConstant.SYS_FILE_NET_URL);
+        String file_path = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+        //同步类型 1.新增表单,2.新增节点,3.删除节点,4.删除表单,5.修改节点,6.修改表单,7.修改节点表单
+        String type = wbsTreeSynchronousRecord.getType();
+        Set<Integer> collect = Arrays.stream(type.split(","))
+                .map(Integer::parseInt)
+                .collect(Collectors.toSet());
+
+        // 选中节点
+        String nodeId = wbsTreeSynchronousRecord.getNodeId();
+        if (StringUtil.isBlank(nodeId)) {
+            return;
+        }
+        String[] nodeIds = nodeId.split(",");
+        //选中表单
+        String formIds = wbsTreeSynchronousRecord.getFormIds();
+        Set<Long> formList = StringUtil.isNotBlank(formIds)
+                ? Arrays.stream(formIds.split(",")).map(Long::parseLong).collect(Collectors.toSet())
+                : Collections.emptySet();
+
+        //获取项目信息
+        ProjectInfo projectInfo = projectInfoMapper.selectOne(Wrappers.<ProjectInfo>lambdaQuery()
+                .eq(ProjectInfo::getId, wbsTreeSynchronousRecord.getProjectId()));
+        //模板项目信息
+        ProjectInfo tempProject = projectInfoMapper.selectOne(Wrappers.<ProjectInfo>lambdaQuery()
+                .eq(ProjectInfo::getId, wbsTreeSynchronousRecord.getTemplateId()));
+
+        List<WbsTreePrivate> editData = new ArrayList<>();
+
+        for (String primaryKeyId : nodeIds) {
+            if (StringUtils.isNotEmpty(primaryKeyId)) {
+                //获取当前节点对应节点信息
+                WbsTreePrivate wbsTreePrivate = wbsTreePrivateMapper.selectOne(Wrappers.<WbsTreePrivate>lambdaQuery()
+                        .eq(WbsTreePrivate::getPKeyId, primaryKeyId));
+                if (wbsTreePrivate == null) {
+                    throw new ServiceException("当前节点不存在");
+                }
+                //当前项目绑定的是公有模板还是私有项目
+                Boolean isPublic = false;
+
+                if (tempProject == null) {
+                    isPublic = true;
+                }
+
+                //当前项目模板对应的节点信息
+                List<WbsTreePrivate> templateNodes = new ArrayList<>();
+                //质检
+                //当前节点引用的模板为公有模板
+
+                if (isPublic) {
+                    //获取模板中当前节点的子节点的数据------------------------------------------------------------------------------------------------------------------------
+                    LambdaQueryWrapper<WbsTree> wrapperTree = Wrappers.lambdaQuery();
+                    wrapperTree
+                            .eq(WbsTree::getIsDeleted, 0)
+                            .eq(WbsTree::getType, 2)
+                            .apply("FIND_IN_SET({0},ancestors)", wbsTreePrivate.getTreePId());
+                    //模板节点的所有子节点信息
+                    List<WbsTree> wbsTrees = wbsTreeMapper.selectList(wrapperTree);
+                    //3、获取需要新增的节点或者表单节点信息 以及他们对应的父级节点信息--------------------------------------------------------------------------------------------
+                    if (CollectionUtil.isEmpty(wbsTrees)) {
+                        throw new ServiceException("模板节点未找到");
+                    }
+                    templateNodes = BeanUtil.copyProperties(wbsTrees, WbsTreePrivate.class);
+                    templateNodes.forEach(f -> {
+                        f.setPKeyId(f.getId());
+                        f.setTreePId(f.getId());
+                    });
+
+                } else {
+                    //获取当前选中节点与私有模板对应的节点信息 父级模板项目的当前选中节点
+                    WbsTreePrivate wbsTreePrivateRoot = wbsTreePrivateMapper.selectOne(Wrappers.<WbsTreePrivate>lambdaQuery()
+                            .eq(WbsTreePrivate::getProjectId, wbsTreeSynchronousRecord.getTemplateId())
+                            .eq(WbsTreePrivate::getTreePId, wbsTreePrivate.getTreePId()));
+
+                    //获取模板中当前节点的子节点的数据------------------------------------------------------------------------------------------------------------------------
+                    LambdaQueryWrapper<WbsTreePrivate> wrapperPrivate = Wrappers.lambdaQuery();
+                    wrapperPrivate
+                            .eq(WbsTreePrivate::getProjectId, wbsTreePrivateRoot.getProjectId())
+                            .eq(WbsTreePrivate::getType, 2)
+                            .eq(WbsTreePrivate::getIsDeleted, 0);
+                    //判断如果为顶级顶级节点
+                    if (wbsTreePrivate.getTreePId() != 0) {
+                        wrapperPrivate.apply("FIND_IN_SET({0},ancestors_p_id)", wbsTreePrivateRoot.getPKeyId());
+                    }
+                    templateNodes = wbsTreePrivateMapper.selectList(wrapperPrivate);
+                }
+
+                if (CollectionUtil.isEmpty(templateNodes)) {
+                    throw new ServiceException("模板节点未找到");
+                }
+
+
+                //2、获取当前节点的子节点的数据------------------------------------------------------------------------------------------------------------------------
+                LambdaQueryWrapper<WbsTreePrivate> wrapperPrivate = Wrappers.lambdaQuery();
+                wrapperPrivate
+                        .eq(WbsTreePrivate::getWbsId, wbsTreePrivate.getWbsId())
+                        .eq(WbsTreePrivate::getIsDeleted, 0);
+                //判断如果为顶级顶级节点
+                if (wbsTreePrivate.getTreePId() != 0) {
+                    wrapperPrivate.apply("FIND_IN_SET({0},ancestors_p_id)", wbsTreePrivate.getPKeyId());
+                }
+
+                //当前表节点下的节点信息
+                List<WbsTreePrivate> wbsTreePrivates = wbsTreePrivateMapper.selectList(wrapperPrivate);
+                //把选中节点也加进去 方便后面使用
+                wbsTreePrivates.add(wbsTreePrivate);
+
+                if (wbsTreePrivates.isEmpty()) {
+                    throw new ServiceException("项目节点未找到");
+                }
+
+                //获取id 和 tree_p_id 组成的集合
+                Set<Long> ids = wbsTreePrivates.stream().map(WbsTreePrivate::getTreePId).collect(Collectors.toSet());
+                //3.1筛选出需要新增的节点
+                List<WbsTreePrivate> addPrivateNodes = templateNodes.stream()
+                        .filter(f -> !ids.contains(f.getTreePId()))
+                        .collect(Collectors.toList());
+
+                //新增数据二次筛选  只保留任务选中的表单   但可能新增的数据包含新节点
+                if (CollectionUtil.isNotEmpty(formList)) {
+                    addPrivateNodes = addPrivateNodes.stream().filter(f -> f.getType() == 1 || formList.contains(f.getPKeyId())).collect(Collectors.toList());
+                }
+
+                //筛选出需要更新的节点  同时做数据隔离
+                List<WbsTreePrivate> editPrivateNodes = new ArrayList<>();
+                for (WbsTreePrivate templateNode : templateNodes) {
+                    //数据修复-----------------------------------------------------------------------------------------------------------------------------------开始
+                    if (!isPublic && templateNode.getType() == 2 && collect.contains(2)) {
+                        if (CollectionUtil.isNotEmpty(formList) && !formList.contains(templateNode.getPKeyId())) {
+                            continue;
+                        }
+                        boolean isUpdate = false;
+                        if (templateNode.getExcelId() != null) {
+                            ExcelTab excelTab = excelTabMapper.selectOne(Wrappers.<ExcelTab>lambdaQuery()
+                                    .select(ExcelTab::getTabId)
+                                    .eq(ExcelTab::getId, templateNode.getExcelId()));
+                            if (excelTab == null) {
+                                throw new ServerException(templateNode.getNodeName() + " excel未知");
+                            }
+                            if (StringUtil.isBlank(templateNode.getInitTableName())) {
+                                TableInfo tableInfo = tableInfoMapper.selectOne(Wrappers.<TableInfo>lambdaQuery()
+                                        .select(TableInfo::getId)
+                                        .eq(TableInfo::getId, excelTab.getTabId()));
+                                if (tableInfo == null) {
+                                    throw new ServerException(templateNode.getNodeName() + " 实体表未知");
+                                }
+                                templateNode.setInitTableName(tableInfo.getTabEnName());
+                                isUpdate = true;
+                            }
+                            if (StringUtil.isBlank(templateNode.getHtmlUrl())) {
+                                List<WbsTreePrivate> list = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                                        .eq(WbsTreePrivate::getExcelId, templateNode.getExcelId())
+                                        .eq(WbsTreePrivate::getInitTableName, templateNode.getInitTableName())
+                                        .eq(WbsTreePrivate::getProjectId, templateNode.getProjectId()));
+                                if (CollectionUtil.isNotEmpty(list)) {
+                                    templateNode.setHtmlUrl(list.get(0).getHtmlUrl());
+                                    isUpdate = true;
+                                } else {
+                                    throw new ServerException(templateNode.getNodeName() + " html不存在");
+                                }
+                            }
+                        } else if (StringUtil.isNotBlank(templateNode.getInitTableName())) {
+                            TableInfo tableInfo = tableInfoMapper.selectOne(Wrappers.<TableInfo>lambdaQuery()
+                                    .select(TableInfo::getId)
+                                    .eq(TableInfo::getTabEnName, templateNode.getInitTableName()));
+                            if (tableInfo == null) {
+                                throw new ServerException(templateNode.getNodeName() + " 实体表未知");
+                            }
+                            if (templateNode.getExcelId() == null) {
+                                ExcelTab excelTab = excelTabMapper.selectOne(Wrappers.<ExcelTab>lambdaQuery()
+                                        .select(ExcelTab::getId)
+                                        .eq(ExcelTab::getTabId, tableInfo.getId()));
+                                if (excelTab == null) {
+                                    throw new ServerException(templateNode.getNodeName() + " excel未知");
+                                }
+                                templateNode.setExcelId(excelTab.getId());
+                                isUpdate = true;
+                            }
+                            if (StringUtil.isBlank(templateNode.getHtmlUrl())) {
+                                List<WbsTreePrivate> list = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                                        .eq(WbsTreePrivate::getInitTableName, templateNode.getInitTableName())
+                                        .eq(WbsTreePrivate::getExcelId, templateNode.getExcelId())
+                                        .eq(WbsTreePrivate::getProjectId, templateNode.getProjectId()));
+                                if (CollectionUtil.isNotEmpty(list)) {
+                                    templateNode.setHtmlUrl(list.get(0).getHtmlUrl());
+                                    isUpdate = true;
+                                } else {
+                                    throw new ServerException(templateNode.getNodeName() + " html不存在");
+                                }
+                            }
+                        } else if (StringUtil.isNotBlank(templateNode.getHtmlUrl())) {
+                            List<WbsTreePrivate> list = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                                    .eq(WbsTreePrivate::getInitTableName, templateNode.getInitTableName())
+                                    .eq(WbsTreePrivate::getExcelId, templateNode.getExcelId())
+                                    .eq(WbsTreePrivate::getProjectId, templateNode.getProjectId()));
+                            if (CollectionUtil.isNotEmpty(list)) {
+                                templateNode.setExcelId(templateNode.getExcelId() == null ? list.get(0).getExcelId() : templateNode.getExcelId());
+                                templateNode.setInitTableName(templateNode.getInitTableName() == null ? list.get(0).getInitTableName() : templateNode.getInitTableName());
+                                isUpdate = true;
+                            } else {
+                                throw new ServerException(templateNode.getNodeName() + " html不存在");
+                            }
+                        }
+                        if (isUpdate) {
+                            wbsTreePrivateMapper.update(templateNode, Wrappers.<WbsTreePrivate>lambdaUpdate()
+                                    .eq(WbsTreePrivate::getPKeyId, templateNode.getPKeyId()));
+                        }
+                    }
+                    //数据修复-----------------------------------------------------------------------------------------------------------------------------------完成
+                    //更新只跟新表单
+                    for (WbsTreePrivate editPrivateNode : wbsTreePrivates) {
+                        // 判断模板表与项目表 html是否一致
+                        if (ObjectUtils.equals(templateNode.getTreePId(), editPrivateNode.getTreePId()) &&
+                                (templateNode.getType() == 2 || templateNode.getType() == 3 || templateNode.getType() == 5 || templateNode.getType() == 7)) {
+                            //清表
+                            if (collect.contains(2)) {
+                                //绑定清表
+                                editPrivateNode.setExcelId(templateNode.getExcelId());
+                                editPrivateNode.setNodeName(templateNode.getNodeName());
+                                editPrivateNode.setFullName(templateNode.getFullName());
+                                //绑定实体表
+                                editPrivateNode.setInitTableName(templateNode.getInitTableName());
+
+                                if (StringUtil.isBlank(templateNode.getHtmlUrl())) {
+                                    throw new ServiceException(templateNode.getNodeName() + "HTML文件不存在");
+                                }
+                                //封装Html路径 根据模板html copy一份到自己项目节点上
+                                String[] split = templateNode.getHtmlUrl().split("/");
+                                String htmlUrl = file_path + "privateUrlCopy/" + projectInfo.getId() + "/" + split[split.length - 1];
+                                File file_in = ResourceUtil.getFile(templateNode.getHtmlUrl());
+                                InputStream fileInputStream;
+                                if (!file_in.exists()) {
+                                    String path = sys_file_net_url + templateNode.getHtmlUrl().replaceAll("//", "/").replaceAll(file_path, "");
+                                    fileInputStream = CommonUtil.getOSSInputStream(path);
+
+                                    file_in.createNewFile();
+                                    Files.copy(fileInputStream, file_in.toPath(), StandardCopyOption.REPLACE_EXISTING);
+                                }
+
+                                if (!file_in.exists() || file_in.length() == 0) {
+                                    //如果本地服务器上没有
+                                    throw new ServiceException(templateNode.getNodeName() + "HTML文件不存在");
+                                }
+                                File file_out = ResourceUtil.getFile(htmlUrl);
+                                //查询父级文件夹
+                                File fileParent = file_out.getParentFile();
+                                if (!fileParent.exists()) {
+                                    fileParent.mkdirs();
+                                }
+                                file_out.createNewFile();
+                                FileUtil.copy(file_in, file_out);
+                                editPrivateNode.setHtmlUrl(htmlUrl);
+                            }
+
+
+                            if (templateNode.getExcelId() != null) {
+                                editPrivateNode.setIsLinkTable(2);
+                            }
+
+                            //元素、公式 是根据init_table_id去查询的
+                            if (collect.contains(3) || collect.contains(5)) {
+                                editPrivateNode.setInitTableName(templateNode.getInitTableName());
+                                editPrivateNode.setInitTableId(templateNode.getInitTableId());
+                            }
+
+                            //排序
+                            if (collect.contains(7)) {
+                                editPrivateNode.setSort(templateNode.getSort());
+                            }
+
+                            //手动选中的表单 进行筛选
+                            if (CollectionUtil.isNotEmpty(formList)) {
+                                if (formList.contains(templateNode.getPKeyId())) {
+                                    if (collect.contains(2) || collect.contains(3) || collect.contains(4) || collect.contains(5) || collect.contains(6) || collect.contains(7)) {
+                                        editPrivateNodes.add(editPrivateNode);
+                                    }
+                                }
+                            } else {
+                                if (collect.contains(2) || collect.contains(3) || collect.contains(4) || collect.contains(5) || collect.contains(6) || collect.contains(7)) {
+                                    editPrivateNodes.add(editPrivateNode);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                //转List做排序
+                List<Integer> arrayList = new ArrayList<>(collect);
+                Collections.sort(arrayList);
+                for (Integer i : arrayList) {
+                    switch (i) {
+                        //添加表单
+                        case 1:
+                            insertPrivateForm(wbsTreeSynchronousRecord, wbsTreePrivates, addPrivateNodes);
+                            break;
+                        //清表
+                        case 2:
+                            //元素
+                        case 3:
+                            //公式配置
+                        case 5:
+                            editData.addAll(addPrivateNodes);
+                            editData.addAll(editPrivateNodes);
+                            wbsSynchronousEViSaService.saveFormula(wbsTreeSynchronousRecord, editData);
+                            //排序
+                        case 7:
+                            //元素配置
+                            editData.addAll(editPrivateNodes);
+                            break;
+                        //电签 如果有数据 与节点绑定
+                        case 4:
+                            //默认值  如果有数据 与节点绑定
+                        case 6:
+                            //现在电签使用的是模糊匹配 如果电签匹配方式改为精确匹配 则需要使用该功能
+//                            updateEViSa(collect, wbsTreeSynchronousRecord.getTemplateId(), projectInfo.getId(), templateNodes, editPrivateNodes);
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            }
+        }
+        //更新数据的同时统计最小节点数量
+        Map<Long, List<WbsTreePrivate>> collect1 = editData.stream().collect(Collectors.groupingBy(WbsTreePrivate::getPId));
+        Set<Long> pIds = collect1.keySet();
+        Integer nodeNumEnd = 0;
+        for (Long pId : pIds) {
+            nodeNumEnd++;
+            List<WbsTreePrivate> list = collect1.get(pId);
+
+            //更新最新节点
+            wbsSynchronousEViSaService.updatePrivate(wbsTreeSynchronousRecord.getType(), pId, wbsTreeSynchronousRecord.getCreateUserId(), list);
+
+            synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                    .set(WbsTreeSynchronousRecord::getNodeNumEnd, nodeNumEnd)
+                    .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                    .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
+        }
+        synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                .set(WbsTreeSynchronousRecord::getStatus, 2)
+                .set(WbsTreeSynchronousRecord::getErrorMsg, null)
+                .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
+    }
+
+
+    /**
+     * 合同段同步 获取源节点数据和当前节点数据
+     */
+    public void insertContractNode(WbsTreeSynchronousRecord wbsTreeSynchronousRecord) {
+        //同步类型 1.新增表单,2.新增节点,3.删除节点,4.删除表单,5.修改节点,6.修改表单,7.修改节点表单
+        String type = wbsTreeSynchronousRecord.getType();
+        Set<Integer> collect = Arrays.stream(type.split(","))
+                .map(Integer::parseInt)
+                .collect(Collectors.toSet());
+
+        // 选中节点
+        String nodeId = wbsTreeSynchronousRecord.getNodeId();
+        if (StringUtil.isBlank(nodeId)) {
+            return;
+        }
+        String[] nodeIds = nodeId.split(",");
+        //选中表单
+        String formIds = wbsTreeSynchronousRecord.getFormIds();
+        Set<Long> formList = StringUtil.isNotBlank(formIds)
+                ? Arrays.stream(formIds.split(",")).map(Long::parseLong).collect(Collectors.toSet())
+                : Collections.emptySet();
+
+        /**
+         * 合同段选择的类型
+         *  质检系统 101.未填报 102.已填报-未上报 104.待审批 105.已审批
+         *  试验系统 103.未上报 104.待审批 105.已审批
+         *  计量系统 103.未上报 104.待审批 105.已审批
+         */
+        String contractRange = wbsTreeSynchronousRecord.getContractRange();
+        Set<Integer> contractRanges = StringUtil.isNotBlank(contractRange)
+                ? Arrays.stream(contractRange.split(",")).map(Integer::parseInt).collect(Collectors.toSet())
+                : Collections.emptySet();
+
+
+        //获取当前项目所有合同---------------------------------------------------------------------------------------------------
+        List<ContractInfo> contractInfos = contractInfoMapper.selectContractIdByProjectId(String.valueOf(wbsTreeSynchronousRecord.getProjectId()));
+
+        List<WbsTreeContract> editData = new ArrayList<>();
+        for (String primaryKeyId : nodeIds) {
+
+            //获取当前节点对应节点信息
+            WbsTreePrivate wbsTreePrivate = wbsTreePrivateMapper.selectOne(Wrappers.<WbsTreePrivate>lambdaQuery()
+                    .eq(WbsTreePrivate::getPKeyId, primaryKeyId));
+            //如果wbs类型为空 则放弃该同步
+            if (StringUtil.isBlank(wbsTreePrivate.getWbsType())) {
+                throw new ServiceException("系统类型未知");
+            }
+
+            //当前项目选中的节点 获取当前节点的所有子节点数据
+            //1、获取当前节点的子节点 表单的数据------------------------------------------------------------------------------------------------------------------------
+            LambdaQueryWrapper<WbsTreePrivate> wrapperPrivate = Wrappers.lambdaQuery();
+            wrapperPrivate
+                    .eq(WbsTreePrivate::getWbsId, wbsTreePrivate.getWbsId())
+                    .eq(WbsTreePrivate::getType, 2)
+                    .eq(WbsTreePrivate::getIsDeleted, 0)
+                    .apply("FIND_IN_SET({0},ancestors_p_id)", wbsTreePrivate.getPKeyId());
+            //当前项目的子节点数据
+            List<WbsTreePrivate> wbsTreePrivates = wbsTreePrivateMapper.selectList(wrapperPrivate);
+            wbsTreePrivates.add(wbsTreePrivate);
+
+            if (wbsTreePrivates.isEmpty()) {
+                throw new ServiceException("无法找到模板对应节点,请检查模板节点");
+            }
+
+            //合同同步
+            for (ContractInfo contractInfo : contractInfos) {
+                //获取合同下当前节点的数据
+                List<WbsTreeContract> startContacts = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+                        .eq(WbsTreeContract::getContractId, contractInfo.getId())
+                        .eq(WbsTreeContract::getIsTypePrivatePid, primaryKeyId));
+                //如果没有查询到,表示该合同下不存在该节点
+                if (CollectionUtil.isEmpty(startContacts)) {
+                    //不能抛异常  不然就会中止程序  开发阶段先抛异常,后续统一处理
+                    throw new ServiceException("当前节点不存在");
+                }
+                for (WbsTreeContract wbsTreeContract : startContacts) {
+                    //获取合同 当前节点的所有子节点数据
+                    LambdaQueryWrapper<WbsTreeContract> wrapperContract = Wrappers.lambdaQuery();
+                    wrapperContract
+                            .eq(WbsTreeContract::getContractId, contractInfo.getId())
+                            .eq(WbsTreeContract::getIsDeleted, 0)
+                            .apply("FIND_IN_SET({0},ancestors_p_id)", wbsTreeContract.getPKeyId());
+                    wrapperContract.apply("FIND_IN_SET({0},ancestors_p_id)", wbsTreeContract.getPKeyId());
+
+                    //当前合同的子节点数据
+                    List<WbsTreeContract> wbsTreeContracts = wbsTreeContractMapper.selectList(wrapperContract);
+                    wbsTreeContracts.add(wbsTreeContract);
+
+                    HashMap<Long, Integer> informationQueryMap = new HashMap<>();
+                    //查询质检合同节点填表信息
+                    String sql = "SELECT  b.wbs_id, b.STATUS  FROM" +
+                            "( SELECT p_key_id FROM m_wbs_tree_contract WHERE is_deleted = 0 AND contract_id = " + contractInfo.getId() + " AND FIND_IN_SET( " + wbsTreeContract.getPKeyId() + ", ancestors_p_id ) ) a" +
+                            " INNER JOIN (  SELECT  c.wbs_id, c.STATUS FROM u_information_query c" +
+                            " JOIN ( SELECT wbs_id, MAX( update_time ) AS max_update_time FROM u_information_query WHERE contract_id = " + contractInfo.getId() + " GROUP BY wbs_id ) subquery ON c.wbs_id = subquery.wbs_id " +
+                            " AND c.update_time = subquery.max_update_time WHERE  c.contract_id = " + contractInfo.getId() + " and c.is_deleted = 0 ) b ON a.p_key_id = b.wbs_id";
+
+                    List<InformationQuery> informationQueries = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(InformationQuery.class));
+                    //质检节点填报信息
+
+                    for (InformationQuery informationQuery : informationQueries) {
+                        informationQueryMap.put(informationQuery.getWbsId(), informationQuery.getStatus());
+                    }
+
+
+                    //合同段节点对应的项目id
+                    List<Long> typePrivateIds = wbsTreeContracts.stream().map(WbsTreeContract::getIsTypePrivatePid).collect(Collectors.toList());
+
+                    //需要新增的表单
+                    List<WbsTreePrivate> addPrivateNodes = wbsTreePrivates.stream().filter(f -> !typePrivateIds.contains(f.getPKeyId())).collect(Collectors.toList());
+
+                    //如果没有选中 则默认同步所有
+                    if (CollectionUtil.isNotEmpty(formList)) {
+                        //二次筛选  只保留任务选中的表单   但可能新增的数据包含新节点
+                        addPrivateNodes = addPrivateNodes.stream().filter(f -> formList.contains(f.getPKeyId())).collect(Collectors.toList());
+                    }
+
+                    //筛选出需要更新的节点  同时做数据隔离
+                    List<WbsTreeContract> editContractNodes = new ArrayList<>();
+                    for (WbsTreePrivate templateNode : wbsTreePrivates) {
+                        //更新只跟新表单
+                        for (WbsTreeContract editContractNode : wbsTreeContracts) {
+
+                            // 判断模板表与项目表 html是否一致
+                            if (ObjectUtils.equals(templateNode.getPKeyId(), editContractNode.getIsTypePrivatePid())) {
+                                if (templateNode.getType() == 2) {
+                                    //同步范围 当前节点是否允许修改
+                                    Boolean isSync = false;
+                                    if (CollectionUtil.isNotEmpty(contractRanges)) {
+                                        switch (Integer.valueOf(wbsTreePrivate.getWbsType())) {
+                                            //质检
+                                            case 1:
+                                                Integer submit = informationQueryMap.get(editContractNode.getPKeyId());
+
+                                                if (submit == null && contractRanges.contains(WbsSyncTypeEnum.NOT_FILLED_IN.code)) {
+                                                    //未审批 101
+                                                    isSync = true;
+                                                } else {
+                                                    if (submit == null) {
+                                                        throw new ServiceException(wbsTreeContract.getNodeName() + "--下表单未查到填报信息");
+                                                    }
+                                                    if (submit == 0 && contractRanges.contains(WbsSyncTypeEnum.ALREADY_FILLED_IN_NOT_REPORTED.code)) {
+                                                        //已填报-未上报 102
+                                                        isSync = true;
+                                                    } else if (submit == 1 && contractRanges.contains(WbsSyncTypeEnum.PENDING_APPROVAL.code)) {
+                                                        //待审批 104
+                                                        isSync = true;
+                                                    } else if (submit == 2 && contractRanges.contains(WbsSyncTypeEnum.APPROVED.code)) {
+                                                        //已审批 105
+                                                        isSync = true;
+                                                    }
+                                                }
+                                                break;
+                                            //试验
+                                            case 2:
+                                                break;
+                                            //计量
+                                            case 3:
+                                                break;
+                                            //日志
+                                            case 4:
+                                                break;
+                                            //征地拆迁
+                                            case 5:
+                                                break;
+                                            default:
+                                                break;
+                                        }
+                                    } else {
+                                        isSync = true;
+                                    }
+
+
+                                    //清表 公式 元素
+                                    if (collect.contains(2) || collect.contains(3) || collect.contains(5)) {
+                                        editContractNode.setExcelId(templateNode.getExcelId());
+                                        editContractNode.setInitTableName(templateNode.getInitTableName());
+                                        editContractNode.setHtmlUrl(templateNode.getHtmlUrl());
+                                        if (templateNode.getExcelId() != null) {
+                                            editContractNode.setIsLinkTable(2);
+                                        }
+                                    }
+                                    //排序
+                                    if (collect.contains(7)) {
+                                        editContractNode.setSort(templateNode.getSort());
+                                    }
+
+                                    //手动选中的表单 进行筛选
+                                    if (CollectionUtil.isNotEmpty(formList)) {
+                                        if (formList.contains(templateNode.getPKeyId())) {
+                                            if (collect.contains(2) || collect.contains(3) || collect.contains(4) || collect.contains(5) || collect.contains(6) || collect.contains(7)) {
+                                                if (isSync) {
+                                                    editContractNodes.add(editContractNode);
+                                                }
+
+                                            }
+                                        }
+                                    } else {
+                                        if (collect.contains(2) || collect.contains(3) || collect.contains(4) || collect.contains(5) || collect.contains(6) || collect.contains(7)) {
+                                            if (isSync) {
+                                                editContractNodes.add(editContractNode);
+                                            }
+                                        }
+                                    }
+                                    //找到了某个选中节点下与项目节点想同的节点了  提前结束循环,节省资源
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    //修改数据二次筛选  只保留任务选中的表单   但可能新增的数据包含新节点
+                    if (CollectionUtil.isNotEmpty(formList)) {
+                        editContractNodes = editContractNodes.stream().filter(f -> formList.contains(f.getPKeyId())).collect(Collectors.toList());
+                    }
+
+                    //合同段新增节点
+                    List<WbsTreeContract> addContractNode = null;
+                    if (addPrivateNodes != null) {
+                        addContractNode = BeanUtil.copyProperties(addPrivateNodes, WbsTreeContract.class);
+                    }
+
+                    //转类型排序
+                    List<Integer> integers = new ArrayList<>(collect);
+                    Collections.sort(integers);
+                    for (Integer i : integers) {
+                        switch (i) {
+                            //添加表单
+                            case 1:
+                                insertContractForm(wbsTreeSynchronousRecord, contractInfo, wbsTreeContracts, addContractNode);
+                                //如果同时选择新增表单和其他的同步类型  在操作其他类型的时候需要添加新的表单
+//                                    if (CollectionUtil.isNotEmpty(addContractNode)) {
+//                                        editContractNodes.addAll(addContractNode.stream().filter(f -> f.getType() == 2).collect(Collectors.toList()));
+//                                    }
+                                break;
+                            //清表配置
+                            case 2:
+                                //元素配置
+                            case 3:
+                                //公式配置
+                            case 5:
+                                //排序
+                            case 7:
+                                editData.addAll(editContractNodes);
+                                break;
+                            default:
+                                break;
+                        }
+                    }
+                }
+            }
+
+        }
+
+
+        //合同段同步同时记录数量
+        Map<Long, List<WbsTreeContract>> collect1 = editData.stream().collect(Collectors.groupingBy(WbsTreeContract::getPId));
+        Set<Long> pIds = collect1.keySet();
+        Integer nodeNumEnd = 0;
+
+        for (Long pId : pIds) {
+            nodeNumEnd++;
+            List<WbsTreeContract> list = collect1.get(pId);
+
+            wbsSynchronousEViSaService.updateContract(wbsTreeSynchronousRecord.getType(), pId, wbsTreeSynchronousRecord.getCreateUserId(), list);
+            synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                    .set(WbsTreeSynchronousRecord::getNodeNumEnd, nodeNumEnd)
+                    .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                    .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
+        }
+
+        synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                .set(WbsTreeSynchronousRecord::getStatus, 2)
+                .set(WbsTreeSynchronousRecord::getErrorMsg, null)
+                .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
+    }
+
+
+    /**
+     * 项目
+     * 类型 1 添加表单
+     *
+     * @param wbsTreeSynchronousRecord 同步任务
+     * @param wbsTreePrivates          当前项目对应节点的子节点
+     * @param addPrivateNodes          需要新增的节点
+     */
+    public void insertPrivateForm(WbsTreeSynchronousRecord wbsTreeSynchronousRecord, List<WbsTreePrivate> wbsTreePrivates, List<WbsTreePrivate> addPrivateNodes) {
+        List<WbsTreePrivate> addData = new ArrayList<>();
+
+        //------------------------------------------------新增-------------------------------------------------------------------------
+        if (CollectionUtil.isNotEmpty(addPrivateNodes)) {
+            //先给每个新增节点赋唯一id,项目id,父级id
+            addPrivateNodes.forEach(f -> f.setPKeyId(SnowFlakeUtil.getId()));
+
+            //给每一个节点的父级id
+            for (WbsTreePrivate addPrivateNode : addPrivateNodes) {
+                if (addPrivateNode.getParentId() == 0) {
+                    continue;
+                }
+
+                //查询出当前模板节点的父节点 去获取对应项目节点  如果父节点为0就跳过
+                List<WbsTreePrivate> addPrivateParentNodes = wbsTreePrivates.stream().filter(f -> f.getTreePId().equals(addPrivateNode.getParentId())).collect(Collectors.toList());
+                //如果没有数据  就表示这条数据的父节点也时新增节点 就需要从新增节点集合中找父级节点
+                if (addPrivateParentNodes.isEmpty()) {
+                    addPrivateParentNodes = addPrivateNodes.stream().filter(f -> f.getTreePId().equals(addPrivateNode.getParentId())).collect(Collectors.toList());
+                }
+                //如果现在还找不到当前节点的父节点就表示数据有问题
+                if (addPrivateParentNodes.isEmpty()) {
+                    throw new ServiceException(addPrivateNode.getNodeName() + "-找不到父节点");
+                }
+                //当前新增节点的父节点
+                WbsTreePrivate parent = addPrivateParentNodes.get(0);
+
+                addPrivateNode.setPId(parent.getPKeyId());
+                addPrivateNode.setAncestorsPId(parent.getAncestorsPId() + "," + addPrivateNode.getPId());
+                addPrivateNode.setWbsId(parent.getWbsId());
+                addPrivateNode.setWbsType(parent.getWbsType());
+                addPrivateNode.setIsAddConceal(0);
+                addPrivateNode.setProjectId(wbsTreePrivates.get(0).getProjectId());
+                //TODO 后续如果把p_key_id改成了id做 唯一id
+                addPrivateNode.setTreePId(addPrivateNode.getTreePId());
+                //后续如果使用id做当前表主键 则先在赋值treePid之后再去赋值id    addPrivateNode.setId(SnowFlakeUtil.getId());
+                //更新创建时间
+                addPrivateNode.setCreateTime(DateTime.now());
+                addPrivateNode.setCreateUser(wbsTreeSynchronousRecord.getCreateUserId());
+                addData.add(addPrivateNode);
+            }
+        }
+        //设置html_url----------------------------------------------------------------------------------------------------------
+        //如果有新增的数据 旧设置html_url
+        if (CollectionUtil.isNotEmpty(addData)) {
+            try {
+                String file_path = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+                Set<String> urls = new HashSet<>();
+                for (WbsTreePrivate tree : addData) {
+                    if (org.apache.commons.lang3.StringUtils.isNotBlank(tree.getHtmlUrl())) {
+                        String[] split = tree.getHtmlUrl().split("/");
+                        String htmlUrl = file_path + "privateUrlCopy/" + wbsTreePrivates.get(0).getProjectId() + "/" + split[split.length - 1];
+                        if (!urls.contains(tree.getHtmlUrl())) {
+                            urls.add(tree.getHtmlUrl());
+                            File file_in = ResourceUtil.getFile(tree.getHtmlUrl());
+                            if (!file_in.exists() || file_in.length() == 0) {
+                                //如果本地服务器上没有
+                                throw new ServiceException(tree.getNodeName() + "HTML文件不存在");
+                            }
+                            File file_out = ResourceUtil.getFile(htmlUrl);
+                            //查询父级文件夹
+                            File fileParent = file_out.getParentFile();
+                            if (!fileParent.exists()) {
+                                fileParent.mkdirs();
+                            }
+                            file_out.createNewFile();
+                            FileUtil.copy(file_in, file_out);
+                        }
+                        tree.setHtmlUrl(htmlUrl);
+                    }
+                }
+            } catch (ServiceException e) {
+                throw new ServiceException(e.getMessage());
+            } catch (Exception e) {
+                e.printStackTrace();
+                throw new ServiceException("重置表单路径错误");
+            }
+
+            //查询出当前项目所有节点---------------------------------------------------------------------------------------------------
+//            List<WbsTreePrivate> addList = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+//                    .select(WbsTreePrivate::getPKeyId, WbsTreePrivate::getPId)
+//                    .eq(WbsTreePrivate::getIsDeleted, 0)
+//                    .eq(WbsTreePrivate::getProjectId, addData.get(0).getProjectId()));
+//
+//            addList.addAll(addData);
+//            //组合祖级路径 根据当前选中节点为开始
+//            Map<Long, WbsTreePrivate> collect = addList.stream().collect(Collectors.toMap(WbsTreePrivate::getPKeyId, Function.identity()));
+//
+//            addData.forEach(node -> {
+//                String correctAncestors = createAncestorsPId(node, collect);
+//                node.setAncestorsPId(correctAncestors);
+//            });
+
+            //新增-----------------------------------------------------------------------------------------------------------------
+            Map<Long, List<WbsTreePrivate>> collect = addData.stream().collect(Collectors.groupingBy(WbsTreePrivate::getPId));
+            Set<Long> longs = collect.keySet();
+
+            List<Long> pIds = new ArrayList<>(longs);
+            List<WbsTreePrivate> saveData = new ArrayList<>();
+            //按最小节点批量新增
+            List<List<Long>> partition = Lists.partition(pIds, 100);
+
+            for (List<Long> data : partition) {
+                for (Long pId : data) {
+                    List<WbsTreePrivate> list = collect.get(pId);
+                    //排序调整
+                    if (wbsTreeSynchronousRecord.getType().contains("7")) {
+                        list.sort(Comparator.comparingInt(WbsTreePrivate::getSort));
+                        //获取节点下的当前表单
+                        List<WbsTreePrivate> resourceData = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                                .select(WbsTreePrivate::getPKeyId, WbsTreePrivate::getSort)
+                                .eq(WbsTreePrivate::getPId, pId)
+                                .eq(WbsTreePrivate::getIsDeleted, 0)
+                                .orderByAsc(WbsTreePrivate::getSort));
+                        if (CollectionUtil.isNotEmpty(resourceData)) {
+                            for (int i = 0; i < resourceData.size(); i++) {
+                                resourceData.get(i).setSort(i + 1);
+                            }
+                            //修改排序为连续排序
+                            wbsTreePrivateMapper.updateSortBatchByPKeyId(resourceData);
+                        }
+
+
+                        for (WbsTreePrivate wbsTreePrivate : list) {
+                            wbsTreePrivateMapper.updateSortByPId(pId, wbsTreePrivate.getSort());
+                        }
+                    }
+
+                    boolean b = saveData.addAll(list);
+
+                    //单个批次一个事务,只会回滚当前批次数据
+                    Integer i = wbsTreePrivateMapper.insertBatchSomeColumn(saveData);
+                    //如果失败  -- - - - - 继续执行   或者把当前节点的p_key_id 记录到某个地方 方便后续处理
+                    if (i == 0) {
+                        List<Long> collect1 = addData.stream().map(WbsTreePrivate::getPKeyId).collect(Collectors.toList());
+                        //这里可以保存到数据库指定错误日志表
+                        throw new ServiceException("添加失败:" + StringUtil.join(collect1, ","));
+                    }else{
+                        //排序调整-连续排序
+                        if (wbsTreeSynchronousRecord.getType().contains("7")) {
+                            //获取节点下的当前表单
+                            List<WbsTreePrivate> resourceData = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                                    .select(WbsTreePrivate::getPKeyId, WbsTreePrivate::getSort)
+                                    .eq(WbsTreePrivate::getPId, pId)
+                                    .eq(WbsTreePrivate::getIsDeleted, 0)
+                                    .orderByAsc(WbsTreePrivate::getSort));
+                            if (CollectionUtil.isNotEmpty(resourceData)) {
+                                for (int j = 0; j < resourceData.size(); j++) {
+                                    resourceData.get(j).setSort(j + 1);
+                                }
+                                //修改排序为连续排序
+                                wbsTreePrivateMapper.updateSortBatchByPKeyId(resourceData);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 合同段添加表单
+     * 合同段添加表单
+     *
+     * @param wbsTreeSynchronousRecord 同步任务
+     * @param contractInfo             合同段信息
+     * @param wbsTreeContracts         当前合同节点数据
+     * @param addContractNodes         新增节点数据
+     */
+    public void insertContractForm(WbsTreeSynchronousRecord wbsTreeSynchronousRecord, ContractInfo contractInfo, List<WbsTreeContract> wbsTreeContracts, List<WbsTreeContract> addContractNodes) {
+        List<WbsTreeContract> addData = new ArrayList<>();
+
+
+        if (CollectionUtil.isNotEmpty(addContractNodes)) {
+
+            //选给新增的节点赋值
+            addContractNodes.forEach(f -> {
+                f.setIsTypePrivatePid(f.getPKeyId());
+                f.setPKeyId(SnowFlakeUtil.getId());
+            });
+
+            for (WbsTreeContract addContractNode : addContractNodes) {
+                if (addContractNode.getParentId() == 0) {
+                    continue;
+                }
+
+                //查询出当前模板节点的父节点 去获取对应项目节点  如果父节点为0就跳过
+                List<WbsTreeContract> addContractParentNodes = wbsTreeContracts.stream().filter(f -> Objects.equals(f.getTreePId(), addContractNode.getParentId())).collect(Collectors.toList());
+
+                //如果没有数据  就表示这条数据的父节点也时新增节点 就需要从新增节点集合中找父级节点
+                if (addContractParentNodes.isEmpty()) {
+                    addContractParentNodes = addContractNodes.stream().filter(f -> f.getTreePId().equals(addContractNode.getParentId())).collect(Collectors.toList());
+                }
+                //如果现在还找不到当前节点的父节点就表示数据有问题
+                if (addContractParentNodes.isEmpty()) {
+                    //TODO
+                    throw new ServiceException(addContractNode.getNodeName() + "-找不到父节点");
+                }
+                //当前新增节点的父节点
+                WbsTreeContract parent = addContractParentNodes.get(0);
+
+                addContractNode.setIsBussShow(1);
+                addContractNode.setIsDeleted(0);
+                addContractNode.setStatus(1);
+                addContractNode.setParentId(parent.getId());
+                addContractNode.setAncestors(parent.getAncestors() + "," + parent.getId());
+                //TODO
+                addContractNode.setTreePId(addContractNode.getTreePId());
+                //TODO
+                addContractNode.setPId(parent.getPKeyId());
+                //TODO
+                addContractNode.setAncestorsPId(parent.getAncestorsPId() + "," + addContractNode.getPId());
+                addContractNode.setContractId(String.valueOf(contractInfo.getId()));
+                //更新创建时间
+                addContractNode.setCreateTime(DateTime.now());
+                addContractNode.setCreateUser(wbsTreeSynchronousRecord.getCreateUserId());
+            }
+            addData.addAll(addContractNodes);
+        }
+
+
+        //查询出当前项目所有节点---------------------------------------------------------------------------------------------------
+//        List<WbsTreeContract> addList = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+//                .select(WbsTreeContract::getPKeyId, WbsTreeContract::getPId)
+//                .eq(WbsTreeContract::getIsDeleted, 0)
+//                .eq(WbsTreeContract::getContractId, contractInfo.getId()));
+//
+//        addList.addAll(addData);
+//
+//
+//        //组合祖级路径 根据当前选中节点为开始
+//        Map<Long, WbsTreeContract> collect = addList.stream().collect(Collectors.toMap(WbsTreeContract::getPKeyId, Function.identity()));
+//
+//        addData.forEach(node -> {
+//            //通过转换为WbsTreePrivate的方式 去获取祖级节点
+//            String correctAncestors = createAncestorsPId(node, collect);
+//            node.setAncestorsPId(correctAncestors);
+//        });
+
+        //新增-----------------------------------------------------------------------------------------------------------------
+        Map<Long, List<WbsTreeContract>> collect = addData.stream().collect(Collectors.groupingBy(WbsTreeContract::getPId));
+        Set<Long> longs = collect.keySet();
+
+        List<Long> pIds = new ArrayList<>(longs);
+        List<WbsTreeContract> saveData = new ArrayList<>();
+        //按最小节点批量新增
+        List<List<Long>> partition = Lists.partition(pIds, 100);
+        for (List<Long> data : partition) {
+            for (Long pId : data) {
+                List<WbsTreeContract> list = collect.get(pId);
+                //排序调整
+                if (wbsTreeSynchronousRecord.getType().contains("7")) {
+                    list.sort(Comparator.comparingInt(WbsTreeContract::getSort));
+                    //获取节点下的当前表单
+                    List<WbsTreeContract> resourceData = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+                            .select(WbsTreeContract::getPKeyId, WbsTreeContract::getSort)
+                            .eq(WbsTreeContract::getPId, pId)
+                            .eq(WbsTreeContract::getIsDeleted, 0)
+                            .orderByAsc(WbsTreeContract::getSort));
+                    if (CollectionUtil.isNotEmpty(resourceData)) {
+                        for (int i = 0; i < resourceData.size(); i++) {
+                            resourceData.get(i).setSort(i + 1);
+                        }
+                        //修改排序为连续排序
+                        wbsTreeContractMapper.updateSortBatchByPKeyId(resourceData);
+                    }
+
+
+                    for (WbsTreeContract wbsTreePrivate : list) {
+                        wbsTreeContractMapper.updateSortByPId(pId, wbsTreePrivate.getSort());
+                    }
+                }
+                boolean b = saveData.addAll(list);
+
+
+                //单个批次一个事务,只会回滚当前批次数据
+                Integer i = wbsTreeContractMapper.insertBatchSomeColumn(saveData);
+                //如果失败  -- - - - - 继续执行   或者把当前节点的p_key_id 记录到某个地方 方便后续处理
+                if (i == 0) {
+                    List<Long> collect1 = addData.stream().map(WbsTreeContract::getPKeyId).collect(Collectors.toList());
+                    //这里可以保存到数据库指定错误日志表
+                    //这里可以保存到数据库指定错误日志表
+                    throw new ServiceException("添加失败:" + StringUtil.join(collect1, ","));
+                }else{
+                    //排序调整
+                    if (wbsTreeSynchronousRecord.getType().contains("7")) {
+                        //获取节点下的当前表单
+                        List<WbsTreeContract> resourceData = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+                                .select(WbsTreeContract::getPKeyId, WbsTreeContract::getSort)
+                                .eq(WbsTreeContract::getPId, pId)
+                                .eq(WbsTreeContract::getIsDeleted, 0)
+                                .orderByAsc(WbsTreeContract::getSort));
+                        if (CollectionUtil.isNotEmpty(resourceData)) {
+                            for (int j = 0; i < resourceData.size(); j++) {
+                                resourceData.get(j).setSort(j + 1);
+                            }
+                            //修改排序为连续排序
+                            wbsTreeContractMapper.updateSortBatchByPKeyId(resourceData);
+                        }
+
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * 项目同步电签
+     *
+     * @param collect          需要修改的节点
+     * @param templateId       同步源项目ID
+     * @param projectId        当前项目ID
+     * @param editPrivateNodes 模板对应的节点
+     * @param editPrivateNodes 需要修改的节点
+     */
+    private void updateEViSa(Set<Integer> collect, Long templateId, Long projectId, List<WbsTreePrivate> treePrivates, List<WbsTreePrivate> editPrivateNodes) {
+
+        //判断 如果模板项目为公共项目  则无法同步电签
+        ProjectInfo projectInfo = projectInfoMapper.selectById(projectId);
+
+        if (projectInfo != null && ("private").equals(projectInfo.getReferenceWbsTemplateType()) && projectInfo.getReferenceWbsTemplateId() != null) {
+
+
+            //封装  源Pkeyid  修改pKeyId
+            Map<Long, Long> tempMap = new HashMap<>();
+
+            //模板的主键ids
+            List<Long> tempPKeyIds = new ArrayList<>();
+            List<Long> editPKeyIds = new ArrayList<>();
+
+
+            for (WbsTreePrivate tempPrivate : treePrivates) {
+                for (WbsTreePrivate editPrivate : editPrivateNodes) {
+                    if (Objects.equals(tempPrivate.getTreePId(), editPrivate.getTreePId())) {
+                        //封装  源Pkeyid  修改pKeyId
+                        tempMap.put(tempPrivate.getPKeyId(), editPrivate.getPKeyId());
+                        //记录
+                        tempPKeyIds.add(tempPrivate.getPKeyId());
+
+                        break;
+                    }
+                }
+
+            }
+
+            //源项目电签和默认值信息
+            List<TextdictInfo> tempTextDictInfo = textdictInfoMapper.selectList(Wrappers.<TextdictInfo>lambdaQuery()
+                    .eq(TextdictInfo::getProjectId, templateId)
+                    .eq(TextdictInfo::getIsDeleted, 0)
+                    .in(TextdictInfo::getTabId, tempPKeyIds)
+                    .in(TextdictInfo::getType, 4));
+            //当前值同步默认值
+//                    .in(TextdictInfo::getType, 2, 4, 6));
+            if (CollectionUtil.isEmpty(tempTextDictInfo)) {
+                throw new ServiceException("源数据无电签及默认值配置");
+            }
+            Map<Integer, List<TextdictInfo>> tempTextDictInfoMap = tempTextDictInfo.stream().collect(Collectors.groupingBy(TextdictInfo::getType));
+
+
+            List<TextdictInfo> tempViSa = new ArrayList<>();
+            //电签
+            if (collect.contains(3)) {
+                //个人电签
+                List<TextdictInfo> tempMyViSa = tempTextDictInfoMap.get(2);
+                if (CollectionUtil.isNotEmpty(tempMyViSa)) {
+                    tempViSa.addAll(tempMyViSa);
+
+                }
+                //企业电签
+                List<TextdictInfo> tempEnterViSa = tempTextDictInfoMap.get(6);
+                if (CollectionUtil.isNotEmpty(tempEnterViSa)) {
+                    tempViSa.addAll(tempEnterViSa);
+                }
+
+            }
+            //默认值
+            if (collect.contains(5)) {
+                List<TextdictInfo> tempDefault = tempTextDictInfoMap.get(4);
+                if (CollectionUtil.isNotEmpty(tempDefault)) {
+                    tempViSa.addAll(tempDefault);
+                }
+            }
+
+
+            List<TextdictInfo> addData = new ArrayList<>();
+            //新增电签
+            if (CollectionUtil.isNotEmpty(tempViSa)) {
+                for (TextdictInfo textdictInfo : tempViSa) {
+                    textdictInfo.setId(SnowFlakeUtil.getId());
+                    textdictInfo.setProjectId(String.valueOf(projectId));
+
+                    //根据源id找对应的修改pKid
+                    Long tabId = Long.valueOf(textdictInfo.getTabId());
+                    Long editId = tempMap.get(tabId);
+                    if (editId == null) {
+                        continue;
+                    }
+
+                    textdictInfo.setTabId(String.valueOf(editId));
+
+                    addData.add(textdictInfo);
+                }
+            }
+            if (CollectionUtil.isNotEmpty(addData)) {
+                //删除 需要新增的节点的电签和默认值
+                List<Long> editPrivateIds = editPrivateNodes.stream().map(WbsTreePrivate::getPKeyId).collect(Collectors.toList());
+                //调用其他类使事务生效
+                wbsSynchronousEViSaService.updateTextDictInfo(projectId, editPrivateIds, addData);
+            }
+        } else {
+            throw new ServiceException("模板为公共项目,无法同步电签");
+        }
+    }
+
+
+    /**
+     * 20250414-lhb-新增
+     * 创建祖级路径
+     * <p>
+     * 该方法用于构建给定节点的祖先路径标识符(PId)字符串
+     * 它通过追溯节点的父节点,直到达到根节点或满足特定条件为止
+     *
+     * @param node    WbsTreeContract类型的节点,表示需要构建路径的起始节点
+     * @param nodeMap 一个映射,其键为节点ID,值为WbsTreeContract类型的节点对象,用于快速查找节点
+     * @return 返回一个字符串,表示构建的祖先路径标识符序列,以逗号分隔
+     */
+    private String createAncestorsPId(WbsTreePrivate node, Map<Long, WbsTreePrivate> nodeMap) {
+        // 初始化路径列表,用于存储祖先节点的ID
+        List<Long> path = new ArrayList<>();
+        // 从给定的节点开始
+        WbsTreePrivate current = node;
+        // 初始化访问集合,用于检测循环引用
+        Set<Long> visited = new HashSet<>();
+
+        while (true) {
+            // 检查当前节点是否为根节点或无效节点
+            if (current == null || current.getPId() == null ||
+                    current.getPId() == 0 || current.getPId().equals(current.getPKeyId())) {
+                break;
+            }
+
+            // 检测循环引用
+            if (visited.contains(current.getPId())) {
+                break;
+            }
+            // 将当前节点的ID添加到已访问集合中
+            visited.add(current.getPKeyId());
+
+            // 从映射中获取当前节点的父节点
+            current = nodeMap.get(current.getPId());
+            // 如果父节点存在,则将其ID添加到路径列表的开头
+            if (current != null) {
+                path.add(0, current.getPKeyId());
+            }
+
+            // 安全限制,防止路径过长导致性能问题
+            if (path.size() > 50) {
+                break;
+            }
+        }
+        // 将根节点ID(0)添加到路径的最前面,表示路径的起点
+        path.add(0, 0L);
+        // 将路径列表转换为字符串并返回
+        return String.join(",", path.stream().map(String::valueOf).toArray(String[]::new));
+    }
+
+    /**
+     * 20250414-lhb-新增
+     * 创建祖级路径
+     * <p>
+     * 该方法用于构建给定节点的祖先路径标识符(PId)字符串
+     * 它通过追溯节点的父节点,直到达到根节点或满足特定条件为止
+     *
+     * @param node    WbsTreeContract类型的节点,表示需要构建路径的起始节点
+     * @param nodeMap 一个映射,其键为节点ID,值为WbsTreeContract类型的节点对象,用于快速查找节点
+     * @return 返回一个字符串,表示构建的祖先路径标识符序列,以逗号分隔
+     */
+    private String createAncestorsPId(WbsTreeContract node, Map<Long, WbsTreeContract> nodeMap) {
+        // 初始化路径列表,用于存储祖先节点的ID
+        List<Long> path = new ArrayList<>();
+        // 从给定的节点开始
+        WbsTreeContract current = node;
+        // 初始化访问集合,用于检测循环引用
+        Set<Long> visited = new HashSet<>();
+
+        while (true) {
+            // 检查当前节点是否为根节点或无效节点
+            if (current == null || current.getPId() == null ||
+                    current.getPId() == 0 || current.getPId().equals(current.getPKeyId())) {
+                break;
+            }
+
+            // 检测循环引用
+            if (visited.contains(current.getPId())) {
+                break;
+            }
+            // 将当前节点的ID添加到已访问集合中
+            visited.add(current.getPKeyId());
+
+            // 从映射中获取当前节点的父节点
+            current = nodeMap.get(current.getPId());
+            // 如果父节点存在,则将其ID添加到路径列表的开头
+            if (current != null) {
+                path.add(0, current.getPKeyId());
+            }
+
+            // 安全限制,防止路径过长导致性能问题
+            if (path.size() > 50) {
+                break;
+            }
+        }
+        // 将根节点ID(0)添加到路径的最前面,表示路径的起点
+        path.add(0, 0L);
+        // 将路径列表转换为字符串并返回
+        return String.join(",", path.stream().map(String::valueOf).toArray(String[]::new));
+    }
+
+
+    /**
+     * 获取指定节点的所有最新节点
+     */
+    public Set<Long> getMinNodes(WbsTreeSynchronousRecord record) {
+
+        Set<Long> collect = null;
+        try {
+
+
+            collect = new HashSet<>();
+            Integer range = record.getRange();
+            String nodeId = record.getNodeId();
+            if (range == 1) {
+                if (StringUtil.isNotBlank(record.getFormIds())) {
+                    List<WbsTreePrivate> list = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                            .select(WbsTreePrivate::getPKeyId, WbsTreePrivate::getPId)
+                            .eq(WbsTreePrivate::getIsDeleted, 0)
+                            .eq(WbsTreePrivate::getType, 2)
+                            .eq(WbsTreePrivate::getProjectId, record.getProjectId())
+                            .in(WbsTreePrivate::getPKeyId, Func.toLongList(record.getFormIds())));
+                    collect = list.stream().map(WbsTreePrivate::getPId).collect(Collectors.toSet());
+                } else {
+
+                    Set<Long> split = Arrays.stream(nodeId.split(","))
+                            .map(Long::parseLong)
+                            .collect(Collectors.toSet());
+                    for (Long id : split) {
+                        List<WbsTreePrivate> wbsTreePrivates = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                                .select(WbsTreePrivate::getPKeyId, WbsTreePrivate::getPId)
+                                .eq(WbsTreePrivate::getIsDeleted, 0)
+                                .eq(WbsTreePrivate::getType, 2)
+                                .apply("find_in_set({0},ancestors_p_id)", id));
+                        collect.addAll(wbsTreePrivates.stream().map(WbsTreePrivate::getPId).collect(Collectors.toSet()));
+                    }
+                }
+                return collect;
+            }
+            if (range == 2) {
+                if (StringUtil.isNotBlank(record.getFormIds())) {
+                    List<WbsTreeContract> list = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+                            .select(WbsTreeContract::getPKeyId, WbsTreeContract::getPId)
+                            .eq(WbsTreeContract::getIsDeleted, 0)
+                            .eq(WbsTreeContract::getProjectId, record.getProjectId())
+                            .in(WbsTreeContract::getIsTypePrivatePid, Func.toLongList(record.getFormIds())));
+                    collect = list.stream().map(WbsTreeContract::getPId).collect(Collectors.toSet());
+                } else {
+
+                    List<WbsTreeContract> initNode = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+                            .select(WbsTreeContract::getPKeyId, WbsTreeContract::getPId)
+                            .eq(WbsTreeContract::getIsDeleted, 0)
+                            .eq(WbsTreeContract::getProjectId, record.getProjectId())
+                            .in(WbsTreeContract::getIsTypePrivatePid, Func.toLongList(record.getNodeId())));
+
+                    for (WbsTreeContract wbsTreeContract : initNode) {
+                        List<WbsTreeContract> wbsTreeContracts = wbsTreeContractMapper.selectList(Wrappers.<WbsTreeContract>lambdaQuery()
+                                .select(WbsTreeContract::getPKeyId, WbsTreeContract::getPId)
+                                .eq(WbsTreeContract::getIsDeleted, 0)
+                                .eq(WbsTreeContract::getType, 2)
+                                .eq(WbsTreeContract::getProjectId, record.getProjectId())
+                                .apply("find_in_set({0},ancestors_p_id)", wbsTreeContract.getPKeyId()));
+                        collect.addAll(wbsTreeContracts.stream().map(WbsTreeContract::getPId).collect(Collectors.toSet()));
+                    }
+                }
+            }
+        } catch (ServiceException e) {
+            throw new ServiceException(e.getMessage());
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException("系统错误");
+        }
+        return collect;
+    }
+
+
+}

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

@@ -1537,168 +1537,6 @@ public class WbsTreePrivateServiceImpl extends BaseServiceImpl<WbsTreePrivateMap
     }
 
 
-    public List<WbsTreePrivate> syncNodeTable(String primaryKeyId, String projectId) {
-
-        if (StringUtils.isNotEmpty(primaryKeyId)) {
-            //获取当前节点对应节点信息
-            WbsTreePrivate wbsTreePrivate = baseMapper.selectOne(Wrappers.<WbsTreePrivate>lambdaQuery().eq(WbsTreePrivate::getPKeyId, primaryKeyId));
-
-            //当前节点对应的模板节点信息
-            WbsTree wbsTreeTemplate = wbsTreeMapper.selectOne(Wrappers.<WbsTree>lambdaQuery()
-                    .eq(WbsTree::getId, wbsTreePrivate.getTreePId()));
-
-            //获取项目信息
-            ProjectInfo projectInfo = projectInfoMapper.selectOne(Wrappers.<ProjectInfo>lambdaQuery().eq(ProjectInfo::getId, wbsTreePrivate.getProjectId()));
-
-            //结果集
-            List<WbsTreePrivate> addData = new ArrayList<>();
-            //质检
-            if (("1").equals(wbsTreePrivate.getWbsType())) {
-                //当前节点引用的模板为公有模板
-                if (("public").equals(projectInfo.getReferenceWbsTemplateType())) {
-
-                    //模板节点的所有子节点信息
-                    List<WbsTree> wbsTrees = null;
-                    //当前节点信息
-                    List<WbsTreePrivate> wbsTreePrivates = null;
-                    //判断是否为最顶级节点
-                    if (wbsTreeTemplate.getParentId() == 0) {
-                        //如果时最顶级节点  就获取所有节点信息
-                        wbsTrees = wbsTreeMapper.selectList(Wrappers.<WbsTree>lambdaQuery()
-                                .eq(WbsTree::getWbsId, wbsTreePrivate.getWbsId())
-                                .eq(WbsTree::getIsDeleted, 0));
-                    } else {
-                        //获取列明
-                        String fieldName = LambdaUtil.getFieldName(WbsTree::getAncestors);
-                        wbsTrees = wbsTreeMapper.selectList(Wrappers.<WbsTree>lambdaQuery()
-                                .eq(WbsTree::getIsDeleted, 0)
-                                .apply("FIND_IN_SET({0}," + fieldName + ")", wbsTreeTemplate.getId()));
-                    }
-
-
-                    //2、获取当前节点下的节点信息
-                    if (wbsTreePrivate.getTreePId() == 0) {
-                        wbsTreePrivates = baseMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
-                                .eq(WbsTreePrivate::getWbsId, wbsTreePrivate.getWbsId())
-                                .eq(WbsTreePrivate::getIsDeleted, 0));
-                    } else {
-                        //获取列明
-                        wbsTreePrivates = baseMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
-                                .eq(WbsTreePrivate::getIsDeleted, 0)
-                                .apply("FIND_IN_SET({0},ancestors_p_id)", wbsTreePrivate.getPId()));
-                    }
-
-                    if (wbsTreePrivates.isEmpty()) {
-                        throw new ServiceException("无法找到模板对应节点,请检查模板节点");
-                    }
-                    //3、获取需要新增的节点或者表单节点信息 以及他们对应的父级节点信息
-
-
-                    //获取id 和 tree_p_id 组成的集合
-                    Set<Long> ids = wbsTreePrivates.stream()
-                            .flatMap(p -> Stream.of(p.getId(), p.getTreePId()))
-                            .collect(Collectors.toSet());
-
-
-                    //3.1筛选出需要新增的节点
-                    List<WbsTree> addNodes = wbsTrees.stream()
-                            .filter(f -> !ids.contains(f.getId()))
-                            .collect(Collectors.toList());
-
-                    //将要新增的项目节点
-                    List<WbsTreePrivate> addPrivateNodes = BeanUtil.copyProperties(addNodes, WbsTreePrivate.class);
-
-
-                    //先给每个新增节点赋唯一id,项目id,父级id
-                    addPrivateNodes.forEach(f -> f.setPKeyId(SnowFlakeUtil.getId()));
-
-
-                    for (WbsTreePrivate addPrivateNode : addPrivateNodes) {
-                        if (addPrivateNode.getParentId() == 0) {
-                            continue;
-                        }
-
-                        //查询出当前模板节点的父节点 去获取对应项目节点  如果父节点为0就跳过
-                        List<WbsTreePrivate> addPrivateParentNodes = wbsTreePrivates.stream().filter(f -> f.getId().equals(addPrivateNode.getParentId())).collect(Collectors.toList());
-                        //如果没有数据  就表示这条数据的父节点也时新增节点 就需要从新增节点集合中找父级节点
-                        if (addPrivateParentNodes.isEmpty()) {
-                            addPrivateParentNodes = addPrivateNodes.stream().filter(f -> f.getId().equals(addPrivateNode.getParentId())).collect(Collectors.toList());
-                        }
-                        //如果现在还找不到当前节点的父节点就表示数据有问题
-                        if (addPrivateParentNodes.isEmpty()) {
-                            //TODO
-                            continue;
-                        }
-
-                        WbsTreePrivate parent = addPrivateParentNodes.get(0);
-                        addPrivateNode.setPId(parent.getPKeyId());
-                        addPrivateNode.setIsAddConceal(0);
-                        addPrivateNode.setProjectId(wbsTreePrivate.getProjectId());
-                        //TODO 后续如果把p_key_id改成了id做 唯一id
-                        addPrivateNode.setTreePId(addPrivateNode.getId());
-
-                    }
-                    addData.addAll(addPrivateNodes);
-                }
-            }
-
-
-            //通过递归的方式去获取祖级路径
-
-            if (addData.isEmpty()) {
-                return null;
-            } else {
-                try {
-                    String file_path = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
-                    Set<String> urls = new HashSet<>();
-                    for (WbsTreePrivate tree : addData) {
-                        if (org.apache.commons.lang3.StringUtils.isNotBlank(tree.getHtmlUrl())) {
-                            String[] split = tree.getHtmlUrl().split("/");
-                            String htmlUrl = file_path + "privateUrlCopy/" + tree.getPdfUrl() + "/" + split[split.length - 1];
-                            if (!urls.contains(tree.getHtmlUrl())) {
-                                urls.add(tree.getHtmlUrl());
-                                File file_in = ResourceUtil.getFile(tree.getHtmlUrl());
-                                if (!file_in.exists() || file_in.length() == 0) {
-                                    continue;
-                                }
-                                File file_out = ResourceUtil.getFile(htmlUrl);
-                                FileUtil.copy(file_in, file_out);
-                            }
-                            tree.setHtmlUrl(htmlUrl);
-                        }
-                    }
-                } catch (Exception e) {
-                    throw new ServiceException("重置表单路径错误");
-                }
-            }
-
-
-            //查询出当前项目所有节点
-            List<WbsTreePrivate> addList = baseMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
-                    .eq(WbsTreePrivate::getIsDeleted, 0)
-                    .eq(WbsTreePrivate::getProjectId, wbsTreePrivate.getProjectId()));
-
-            addList.addAll(addData);
-            //组合祖级路径 根据当前选中节点为开始
-            Map<Long, WbsTreePrivate> collect = addList.stream().collect(Collectors.toMap(WbsTreePrivate::getPKeyId, Function.identity()));
-
-            addData.forEach(node -> {
-                String correctAncestors = createAncestorsPId(node, collect);
-                node.setAncestorsPId(correctAncestors);
-            });
-
-
-            List<List<WbsTreePrivate>> partition = Lists.partition(addData, 1000);
-            for (List<WbsTreePrivate> wbsTreePrivates : partition) {
-                this.insertBatch(wbsTreePrivates, 1000);
-            }
-
-            return addData;
-        }
-        return null;
-    }
-
-
     @Override
     public boolean syncNodeTable(String primaryKeyId) {
         if (StringUtils.isNotEmpty(primaryKeyId)) {
@@ -3615,55 +3453,4 @@ public class WbsTreePrivateServiceImpl extends BaseServiceImpl<WbsTreePrivateMap
         return false;
     }
 
-
-    /**
-     * 20250414-lhb-新增
-     * 创建祖级路径
-     * <p>
-     * 该方法用于构建给定节点的祖先路径标识符(PId)字符串
-     * 它通过追溯节点的父节点,直到达到根节点或满足特定条件为止
-     *
-     * @param node    WbsTreeContract类型的节点,表示需要构建路径的起始节点
-     * @param nodeMap 一个映射,其键为节点ID,值为WbsTreeContract类型的节点对象,用于快速查找节点
-     * @return 返回一个字符串,表示构建的祖先路径标识符序列,以逗号分隔
-     */
-    private String createAncestorsPId(WbsTreePrivate node, Map<Long, WbsTreePrivate> nodeMap) {
-        // 初始化路径列表,用于存储祖先节点的ID
-        List<Long> path = new ArrayList<>();
-        // 从给定的节点开始
-        WbsTreePrivate current = node;
-        // 初始化访问集合,用于检测循环引用
-        Set<Long> visited = new HashSet<>();
-
-        while (true) {
-            // 检查当前节点是否为根节点或无效节点
-            if (current == null || current.getPId() == null ||
-                    current.getPId() == 0 || current.getPId().equals(current.getPKeyId())) {
-                break;
-            }
-
-            // 检测循环引用
-            if (visited.contains(current.getPId())) {
-                break;
-            }
-            // 将当前节点的ID添加到已访问集合中
-            visited.add(current.getPKeyId());
-
-            // 从映射中获取当前节点的父节点
-            current = nodeMap.get(current.getPId());
-            // 如果父节点存在,则将其ID添加到路径列表的开头
-            if (current != null) {
-                path.add(0, current.getPKeyId());
-            }
-
-            // 安全限制,防止路径过长导致性能问题
-            if (path.size() > 50) {
-                break;
-            }
-        }
-        // 将根节点ID(0)添加到路径的最前面,表示路径的起点
-        path.add(0, 0L);
-        // 将路径列表转换为字符串并返回
-        return String.join(",", path.stream().map(String::valueOf).toArray(String[]::new));
-    }
 }

+ 1 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeServiceImpl.java

@@ -606,6 +606,7 @@ public class WbsTreeServiceImpl extends BaseServiceImpl<WbsTreeMapper, WbsTree>
             if (ObjectUtil.isNotEmpty(item.getType()) && item.getType() == 1 && ObjectUtil.isNotEmpty(item.getHasChildren()) && item.getHasChildren()) {
                 sortListPrivate(item.getChildren());
             }
+            item.setPrimaryKeyId(item.getPKeyId());
         }
     }
 

+ 273 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeSynchronousRecordServiceImpl.java

@@ -0,0 +1,273 @@
+package org.springblade.manager.service.impl;
+
+import cn.hutool.core.date.DateTime;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.redisson.api.RedissonClient;
+import org.springblade.common.utils.SnowFlakeUtil;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.secure.BladeUser;
+import org.springblade.core.secure.utils.SecureUtil;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.manager.entity.*;
+import org.springblade.manager.mapper.*;
+import org.springblade.manager.service.WbsTreeSynchronousRecordService;
+import org.springblade.manager.vo.WbsTreeSynchronousRecordVo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author LHB
+ * @description 针对表【m_wbs_tree_synchronous_record(WBS同步记录表)】的数据库操作Service实现
+ * @createDate 2025-05-15 13:52:09
+ */
+@Service
+public class WbsTreeSynchronousRecordServiceImpl extends ServiceImpl<WbsTreeSynchronousRecordMapper, WbsTreeSynchronousRecord>
+        implements WbsTreeSynchronousRecordService {
+
+    @Autowired
+    private WbsTreePrivateMapper wbsTreePrivateMapper;
+
+    @Autowired
+    private WbsSynchronousServiceImpl wbsSynchronousService;
+
+    @Autowired
+    private WbsTreeMapper wbsTreeMapper;
+    @Autowired
+    private ProjectInfoMapper projectInfoMapper;
+    @Autowired
+    private WbsInfoMapper wbsInfoMapper;
+
+    @Override
+    public WbsTreeSynchronousRecord insert(WbsTreeSynchronousRecord mWbsTreeSynchronousRecord) {
+        //新增之前  判断当前选中节点是否在同步列表中,因为是多节点 所有要查询所有同项目下的同步节点再获取所有子节点 判断当前选中节点是否在这一批次当中
+        List<WbsTreeSynchronousRecord> wbsTreeSynchronousRecords = baseMapper.selectList(new QueryWrapper<WbsTreeSynchronousRecord>().lambda()
+                .select(WbsTreeSynchronousRecord::getNodeId)
+                .eq(WbsTreeSynchronousRecord::getProjectId, mWbsTreeSynchronousRecord.getProjectId())
+                .eq(WbsTreeSynchronousRecord::getIsDeleted, 0)
+                .in(WbsTreeSynchronousRecord::getStatus, 0, 1));
+        List<String> nodeIds = wbsTreeSynchronousRecords.stream().map(WbsTreeSynchronousRecord::getNodeId).collect(Collectors.toList());
+
+        //所有子节点集合
+        List<Long> privateIds = new ArrayList<>();
+        //通过 ancestors_p_id 查询所有 非表单子节点
+        for (String nodeId : nodeIds) {
+            String[] split = nodeId.split(",");
+            for (String s : split) {
+                List<WbsTreePrivate> wbsTreePrivates = wbsTreePrivateMapper.selectList(new QueryWrapper<WbsTreePrivate>().lambda()
+                        .select(WbsTreePrivate::getPKeyId)
+                        .eq(WbsTreePrivate::getIsDeleted, 0)
+                        .apply("find_in_set({0},ancestors_p_id)", s)
+                );
+                privateIds.add(Long.valueOf(s));
+                privateIds.addAll(wbsTreePrivates.stream().map(WbsTreePrivate::getPKeyId).collect(Collectors.toList()));
+
+
+                //  //判断节点类型  如果是试验或计量则不允许添加合同合同段
+                WbsTreePrivate wbsTreePrivate = wbsTreePrivateMapper.selectById(nodeId);
+                if (mWbsTreeSynchronousRecord.getRange() == 2 && wbsTreePrivate != null && !Objects.equals(wbsTreePrivate.getWbsType(), 1)) {
+                    throw new ServiceException(wbsTreePrivate.getNodeName() + " 节点不是质检类型,无法同步合同段");
+                }
+            }
+        }
+
+
+        //记录选中节点下的表单数
+        String[] newNodeIds = mWbsTreeSynchronousRecord.getNodeId().split(",");
+        for (String newNodeId : newNodeIds) {
+            Long nodeIdLong = Long.valueOf(newNodeId);
+            if (privateIds.contains(nodeIdLong)) {
+                throw new ServiceException("当前选中节点正在同步,请勿重复同步");
+            }
+        }
+
+        //模板项目信息
+        ProjectInfo tempProject = projectInfoMapper.selectOne(Wrappers.<ProjectInfo>lambdaQuery()
+                .eq(ProjectInfo::getId, mWbsTreeSynchronousRecord.getTemplateId()));
+        //当前项目绑定的是公有模板还是私有项目
+        if (mWbsTreeSynchronousRecord.getRange() == 1 && tempProject == null && (
+                mWbsTreeSynchronousRecord.getType().contains("2") ||
+                        mWbsTreeSynchronousRecord.getType().contains("3") ||
+                        mWbsTreeSynchronousRecord.getType().contains("4") ||
+                        mWbsTreeSynchronousRecord.getType().contains("5") ||
+                        mWbsTreeSynchronousRecord.getType().contains("6"))) {
+            throw new ServiceException("模板为公共模板,无法选择");
+        }
+
+
+        //获取项目名称
+        ProjectInfo projectInfo = projectInfoMapper.selectById(mWbsTreeSynchronousRecord.getProjectId());
+        if (projectInfo == null) {
+            throw new ServiceException("项目不存在");
+        }
+        mWbsTreeSynchronousRecord.setId(SnowFlakeUtil.getId());
+        mWbsTreeSynchronousRecord.setProjectName(projectInfo.getProjectName());
+
+        if (mWbsTreeSynchronousRecord.getRange() == 1 || mWbsTreeSynchronousRecord.getRange() == 2) {
+            Set<Long> minNodes = wbsSynchronousService.getMinNodes(mWbsTreeSynchronousRecord);
+            mWbsTreeSynchronousRecord.setNodeNum(minNodes.size());
+        }
+        //类型为3 、 4
+        if (mWbsTreeSynchronousRecord.getRange() == 3) {
+            mWbsTreeSynchronousRecord.setRangeName("单表单同步");
+            mWbsTreeSynchronousRecord.setNodeNum(1);
+        }
+        if (mWbsTreeSynchronousRecord.getRange() == 4) {
+            mWbsTreeSynchronousRecord.setRangeName("单表强制同步");
+            mWbsTreeSynchronousRecord.setNodeNum(1);
+            if (StringUtil.isBlank(mWbsTreeSynchronousRecord.getFormIds())) {
+                throw new ServiceException("请选择表单");
+            }
+        }
+        mWbsTreeSynchronousRecord.setNodeNumEnd(0);
+        List<Long> collect = Arrays.stream(mWbsTreeSynchronousRecord.getNodeId().split(",")).map(Long::parseLong).collect(Collectors.toList());
+        //节点名称
+        List<WbsTreePrivate> list = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                .select(WbsTreePrivate::getPKeyId, WbsTreePrivate::getNodeName)
+                .in(WbsTreePrivate::getPKeyId, collect));
+        List<String> nodeNames = list.stream().map(WbsTreePrivate::getNodeName).collect(Collectors.toList());
+        mWbsTreeSynchronousRecord.setNodeName(String.join(",", nodeNames));
+
+        //获取当前用户
+        BladeUser user = SecureUtil.getUser();
+        mWbsTreeSynchronousRecord.setCreateUserId(user.getUserId());
+        mWbsTreeSynchronousRecord.setCreateUser(user.getNickName());
+        //新增
+        baseMapper.insert(mWbsTreeSynchronousRecord);
+
+        return mWbsTreeSynchronousRecord;
+    }
+
+    @Override
+    public WbsTreeSynchronousRecord getNodeStatus(Long id) {
+        WbsTreePrivate wbsTreePrivate = wbsTreePrivateMapper.selectById(id);
+        if (wbsTreePrivate == null) {
+            throw new ServiceException("节点不存在");
+        }
+
+
+        //新增之前  判断当前选中节点是否在同步列表中,因为是多节点 所有要查询所有同项目下的同步节点再获取所有子节点 判断当前选中节点是否在这一批次当中
+        List<WbsTreeSynchronousRecord> wbsTreeSynchronousRecords = baseMapper.selectList(new QueryWrapper<WbsTreeSynchronousRecord>().lambda()
+                .eq(WbsTreeSynchronousRecord::getProjectId, wbsTreePrivate.getProjectId())
+                .eq(WbsTreeSynchronousRecord::getIsDeleted, 0)
+                .in(WbsTreeSynchronousRecord::getStatus, 0, 1));
+        for (WbsTreeSynchronousRecord wbsTreeSynchronousRecord : wbsTreeSynchronousRecords) {
+
+
+            List<Long> privateIds = new ArrayList<>();
+
+            String nodeId = wbsTreeSynchronousRecord.getNodeId();
+            String[] split = nodeId.split(",");
+            for (String s : split) {
+                List<WbsTreePrivate> wbsTreePrivates = wbsTreePrivateMapper.selectList(new QueryWrapper<WbsTreePrivate>().lambda()
+                        .select(WbsTreePrivate::getPKeyId)
+                        .eq(WbsTreePrivate::getIsDeleted, 0)
+                        .apply("find_in_set({0},ancestors_p_id)", s)
+                );
+                privateIds.add(Long.valueOf(s));
+                privateIds.addAll(wbsTreePrivates.stream().map(WbsTreePrivate::getPKeyId).collect(Collectors.toList()));
+            }
+            if (privateIds.size() > 0 && privateIds.contains(id)) {
+                return wbsTreeSynchronousRecord;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public List<WbsTreeSynchronousRecordVo> getProjectTemplate(String nodeIds) {
+
+        String nodeId = nodeIds.split(",")[0];
+        //选中节点
+        WbsTreePrivate wbsTreePrivate = wbsTreePrivateMapper.selectById(nodeId);
+        List<WbsTreeSynchronousRecordVo> returnData = new ArrayList<>();
+        //获取当前项目的初始节点
+        WbsTreePrivate start = wbsTreePrivateMapper.selectOne(Wrappers.<WbsTreePrivate>lambdaQuery()
+                .eq(WbsTreePrivate::getProjectId, wbsTreePrivate.getProjectId())
+                .eq(WbsTreePrivate::getWbsId, wbsTreePrivate.getWbsId())
+                .eq(WbsTreePrivate::getParentId, 0)
+                .last("limit 1")
+        );
+        //私有项目
+        List<WbsTreePrivate> list = wbsTreePrivateMapper.selectList(Wrappers.<WbsTreePrivate>lambdaQuery()
+                .eq(WbsTreePrivate::getTreePId, start.getTreePId()));
+        for (WbsTreePrivate treePrivate : list) {
+            //过滤当前项目
+            if (Objects.equals(treePrivate.getProjectId(), wbsTreePrivate.getProjectId())) {
+                continue;
+            }
+
+            ProjectInfo projectInfo2 = projectInfoMapper.selectById(treePrivate.getProjectId());
+            if (projectInfo2 == null) {
+                continue;
+            }
+            WbsTreeSynchronousRecordVo wbsTreeSynchronousRecordVo = new WbsTreeSynchronousRecordVo();
+            wbsTreeSynchronousRecordVo.setId(projectInfo2.getId());
+            wbsTreeSynchronousRecordVo.setName(projectInfo2.getProjectName());
+            wbsTreeSynchronousRecordVo.setType(2);
+            wbsTreeSynchronousRecordVo.setWbsId(treePrivate.getWbsId());
+            returnData.add(wbsTreeSynchronousRecordVo);
+        }
+        //公共项目
+        WbsTree wbsTree = wbsTreeMapper.selectById(start.getId());
+        WbsInfo wbsInfo = wbsInfoMapper.selectById(wbsTree.getWbsId());
+        WbsTreeSynchronousRecordVo wbsTreeSynchronousRecordVo = new WbsTreeSynchronousRecordVo();
+        wbsTreeSynchronousRecordVo.setId(Long.valueOf(nodeId));
+        wbsTreeSynchronousRecordVo.setName(wbsInfo.getWbsName());
+        wbsTreeSynchronousRecordVo.setType(1);
+        returnData.add(wbsTreeSynchronousRecordVo);
+
+        return returnData;
+    }
+
+    // 读取配置并设置默认值
+    @Value("${scheduler.enabled:true}")
+    private boolean schedulerEnabled;
+    /**
+     * 同步节点表单
+     * 定时检查同步任务,状态为1的数据如果最后更新时间与当前时间超过10分钟,则修改状态为1
+     */
+    @Scheduled(fixedDelay = 10000)
+    public void syncInit() {
+        // 本地环境跳过执行(可添加日志输出)
+        if (!schedulerEnabled) return;
+
+        List<WbsTreeSynchronousRecord> wbsTreeSynchronousRecords = baseMapper.selectList(new QueryWrapper<WbsTreeSynchronousRecord>().lambda()
+                .in(WbsTreeSynchronousRecord::getStatus, 0, 1)
+                .eq(WbsTreeSynchronousRecord::getIsDeleted, 0)
+                .orderByAsc(WbsTreeSynchronousRecord::getCreateTime)
+                .last("limit 10")
+        );
+
+        for (WbsTreeSynchronousRecord wbsTreeSynchronousRecord : wbsTreeSynchronousRecords) {
+            if (wbsTreeSynchronousRecord.getStatus() == 1) {
+                if (wbsTreeSynchronousRecord.getUpdateTime().getTime() + 600000 < System.currentTimeMillis()) {
+                    baseMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                            .set(WbsTreeSynchronousRecord::getStatus, 0)
+                            .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
+                }
+                continue;
+            }
+
+            //通过线程池执行同步任务
+            wbsSynchronousService.syncExecute(wbsTreeSynchronousRecord);
+
+            //修改数据状态为正在同步
+            baseMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                    .set(WbsTreeSynchronousRecord::getStatus, 1)
+                    .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                    .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
+        }
+    }
+}
+
+
+
+

+ 49 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/wrapper/ServicePlanTaskWrapper.java

@@ -0,0 +1,49 @@
+/*
+ *      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.wrapper;
+
+import org.springblade.core.mp.support.BaseEntityWrapper;
+import org.springblade.core.tool.utils.BeanUtil;
+import org.springblade.manager.entity.ServicePlanTask;
+import org.springblade.manager.vo.ServicePlanTaskVO;
+import java.util.Objects;
+
+/**
+ * 包装类,返回视图层所需的字段
+ *
+ * @author BladeX
+ * @since 2025-06-27
+ */
+public class ServicePlanTaskWrapper extends BaseEntityWrapper<ServicePlanTask, ServicePlanTaskVO>  {
+
+	public static ServicePlanTaskWrapper build() {
+		return new ServicePlanTaskWrapper();
+ 	}
+
+	@Override
+	public ServicePlanTaskVO entityVO(ServicePlanTask servicePlanTask) {
+		ServicePlanTaskVO servicePlanTaskVO = Objects.requireNonNull(BeanUtil.copy(servicePlanTask, ServicePlanTaskVO.class));
+
+		//User createUser = UserCache.getUser(servicePlanTask.getCreateUser());
+		//User updateUser = UserCache.getUser(servicePlanTask.getUpdateUser());
+		//servicePlanTaskVO.setCreateUserName(createUser.getName());
+		//servicePlanTaskVO.setUpdateUserName(updateUser.getName());
+
+		return servicePlanTaskVO;
+	}
+
+}

+ 49 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/wrapper/ServicePlanWrapper.java

@@ -0,0 +1,49 @@
+/*
+ *      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.wrapper;
+
+import org.springblade.core.mp.support.BaseEntityWrapper;
+import org.springblade.core.tool.utils.BeanUtil;
+import org.springblade.manager.entity.ServicePlan;
+import org.springblade.manager.vo.ServicePlanVO;
+import java.util.Objects;
+
+/**
+ * 包装类,返回视图层所需的字段
+ *
+ * @author BladeX
+ * @since 2025-06-25
+ */
+public class ServicePlanWrapper extends BaseEntityWrapper<ServicePlan, ServicePlanVO>  {
+
+	public static ServicePlanWrapper build() {
+		return new ServicePlanWrapper();
+ 	}
+
+	@Override
+	public ServicePlanVO entityVO(ServicePlan servicePlan) {
+		ServicePlanVO servicePlanVO = Objects.requireNonNull(BeanUtil.copy(servicePlan, ServicePlanVO.class));
+
+		//User createUser = UserCache.getUser(servicePlan.getCreateUser());
+		//User updateUser = UserCache.getUser(servicePlan.getUpdateUser());
+		//servicePlanVO.setCreateUserName(createUser.getName());
+		//servicePlanVO.setUpdateUserName(updateUser.getName());
+
+		return servicePlanVO;
+	}
+
+}