Parcourir la source

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

# Conflicts:
#	blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ArchivesAutoServiceImpl.java
#	blade-service/blade-manager/src/main/java/org/springblade/manager/service/IWbsTreeService.java
#	blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreePrivateServiceImpl.java
lvy il y a 9 heures
Parent
commit
d97e265a48
29 fichiers modifiés avec 2090 ajouts et 110 suppressions
  1. 4 0
      blade-common/pom.xml
  2. 25 0
      blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/dto/AutoOCRDTO.java
  3. 17 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/WbsTreeSynchronousRecordDTO.java
  4. 19 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/WbsTreeSynchronousRecord.java
  5. 10 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/copyNodeVo.java
  6. 10 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/controller/ArchivesAutoController.java
  7. 4 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/service/IArchivesAutoService.java
  8. 145 5
      blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ArchivesAutoServiceImpl.java
  9. 31 0
      blade-service/blade-archive/src/main/java/org/springblade/archive/utils/FileUtils.java
  10. 6 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeController.java
  11. 67 3
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeSynchronousRecordController.java
  12. 1 1
      blade-service/blade-manager/src/main/java/org/springblade/manager/feign/ExcelTabClientImpl.java
  13. 2 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeMapper.java
  14. 12 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreeMapper.xml
  15. 2 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreePrivateMapper.java
  16. 1 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/WbsTreePrivateMapper.xml
  17. 2 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/IWbsTreeService.java
  18. 17 19
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ExcelTabServiceImpl.java
  19. 230 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/FormulaServiceImpl.java
  20. 55 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousEViSaServiceImpl.java
  21. 31 75
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousServiceImpl.java
  22. 127 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreePrivateServiceImpl.java
  23. 150 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeServiceImpl.java
  24. 5 7
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeSynchronousRecordServiceImpl.java
  25. 36 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/ConvertUtils.java
  26. 272 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/PenetrationResistanceChart.java
  27. 217 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SieveAnalysisChart.java
  28. 278 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SmoothCurveChartToImage.java
  29. 314 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SoilTestChart.java

+ 4 - 0
blade-common/pom.xml

@@ -89,6 +89,10 @@
             <artifactId>gson</artifactId>
             <version>2.8.9</version>
         </dependency>
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-okhttp</artifactId>
+        </dependency>
 
     </dependencies>
 

+ 25 - 0
blade-service-api/blade-archive-api/src/main/java/org/springblade/archive/dto/AutoOCRDTO.java

@@ -0,0 +1,25 @@
+package org.springblade.archive.dto;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class AutoOCRDTO {
+    private String ids;  // 改为String类型
+
+    // 添加方法获取Long列表
+    public List<Long> getIdList() {
+        if (ids == null || ids.trim().isEmpty()) {
+            return new ArrayList<>();
+        }
+        return Arrays.stream(ids.split(","))
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .map(Long::valueOf)
+                .collect(Collectors.toList());
+    }
+}

+ 17 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/WbsTreeSynchronousRecordDTO.java

@@ -0,0 +1,17 @@
+package org.springblade.manager.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springblade.manager.entity.WbsTreeSynchronousRecord;
+
+/**
+ * @author LHB
+ */
+@Data
+public class WbsTreeSynchronousRecordDTO extends WbsTreeSynchronousRecord {
+    @ApiModelProperty("进度")
+    private Integer progress = 0;
+
+    @ApiModelProperty("剩余数量")
+    private Integer surplusCount = 0;
+}

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

@@ -4,6 +4,7 @@ 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 io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.util.Date;
@@ -26,69 +27,84 @@ public class WbsTreeSynchronousRecord {
     /**
      * 项目id
      */
+    @ApiModelProperty(value = "项目id")
     private Long projectId;
 
     /**
      * 项目名称
      */
+    @ApiModelProperty(value = "项目名称")
     private String projectName;
 
 
+    @ApiModelProperty(value = "同步范围(1-从后管同步,2-同步到合同段)")
     @TableField("`range`")
     private Integer range;
     /**
      * 同步范围名称
      */
+    @ApiModelProperty(value = "同步范围名称")
     private String rangeName;
     /**
      * 合同段范围 逗号拼接的编号 101.未填报 102.已填报-未上报 103.未上报 104.待审批 105.已审批
      */
+    @ApiModelProperty(value = "合同段范围 逗号拼接的编号 101.未填报 102.已填报-未上报 103.未上报 104.待审批 105.已审批")
     private String contractRange;
     /**
      * 合同段范围名称
      */
+    @ApiModelProperty(value = "合同段范围名称")
     private String contractRangeName;
 
     /**
      * 同步源Id
      */
+    @ApiModelProperty(value = "同步源Id")
     private Long templateId;
 
     /**
      * 同步源名称
      */
+    @ApiModelProperty(value = "同步源名称")
     private String templateName;
 
     /**
      * 同步类型 逗号拼接的编号 1.新增表单 2.清表配置 3.元素配置 4.电签配置 5.公式配置 6.默认值配置 7.表单排序
      */
+    @ApiModelProperty(value = "同步类型 逗号拼接的编号 1.新增表单 2.清表配置 3.元素配置 4.电签配置 5.公式配置 6.默认值配置 7.表单排序")
     private String type;
 
     /**
      * 同步类型名称
      */
+    @ApiModelProperty(value = "同步类型名称")
     private String typeName;
 
     /**
      * 同步节点id 多个节点
      */
+    @ApiModelProperty(value = "同步节点id 多个节点")
     private String nodeId;
 
     /**
      * 同步节点名称
      */
+    @ApiModelProperty(value = "同步节点名称")
     private String nodeName;
     /**
      * 表单Ids     range = 4 强制同步时  当前数据为同步源
      */
+    @ApiModelProperty(value = "表单Ids     range = 4 强制同步时  当前数据为同步源")
     private String formIds;
     /**
      * 同步节点数量
      */
+    @ApiModelProperty(value = "同步节点数量")
     private Integer nodeNum;
     /**
      * 已同步数量
      */
+    @ApiModelProperty(value = "已同步数量")
     private Integer nodeNumEnd;
 
     /**
@@ -99,15 +115,18 @@ public class WbsTreeSynchronousRecord {
     /**
      * 状态(0-未同步,1-正在同步,2-已同步,3-同步失败)
      */
+    @ApiModelProperty(value = "状态(0-未同步,1-正在同步,2-已同步,3-同步失败)")
     private Integer status;
     /**
      * 状态(0-未同步,1-正在同步,2-已同步,3-同步失败)
      */
+    @ApiModelProperty(value = "状态名称")
     @TableField(exist = false)
     private String statusName;
     /**
      * 错误信息
      */
+    @ApiModelProperty(value = "错误信息")
     @TableField(updateStrategy = FieldStrategy.NOT_EMPTY)
     private String errorMsg;
     /**

+ 10 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/copyNodeVo.java

@@ -0,0 +1,10 @@
+package org.springblade.manager.vo;
+
+import lombok.Data;
+
+import java.util.List;
+@Data
+public class copyNodeVo {
+  private   List<Long> leftIds;
+  private  List<Long>rightIds;
+}

+ 10 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/controller/ArchivesAutoController.java

@@ -34,6 +34,7 @@ import org.apache.http.message.BasicNameValuePair;
 import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.*;
 import org.springblade.archive.dto.ArchiveWarningDTO;
+import org.springblade.archive.dto.AutoOCRDTO;
 import org.springblade.archive.dto.FindAndReplaceDto;
 import org.springblade.archive.dto.SaveApplyDTO;
 import org.springblade.archive.entity.ArchiveConclusion;
@@ -282,6 +283,15 @@ public class ArchivesAutoController extends BladeController {
     }
 
 
+	@PostMapping("/atuoOCR")
+	@ApiOperationSupport(order = 5)
+	@ApiOperation(value = "档案自动识别")
+	public R atuoOCR(@RequestBody AutoOCRDTO dto) throws Exception {
+		archivesAutoService.atuoOCR(dto.getIdList());
+		return R.success("正在识别中");
+	}
+
+
 
 
 

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

@@ -188,5 +188,9 @@ public interface IArchivesAutoService extends BaseService<ArchivesAuto> {
 
     void reCreateArchiveAuto1(String ids);
 
+	Boolean atuoOCR(List<Long> ids) throws Exception;
+
 	void reCreateArchiveAuto2(List<ArchivesAuto> archivesAutoList, String ids, String name);
+
+
 }

+ 145 - 5
blade-service/blade-archive/src/main/java/org/springblade/archive/service/impl/ArchivesAutoServiceImpl.java

@@ -19,6 +19,9 @@ package org.springblade.archive.service.impl;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -36,10 +39,7 @@ import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang.StringUtils;
 
-import org.springblade.archive.dto.ArchiveWarningDTO;
-import org.springblade.archive.dto.FindAndReplaceDto;
-import org.springblade.archive.dto.JiLinQueryDto;
-import org.springblade.archive.dto.SaveApplyDTO;
+import org.springblade.archive.dto.*;
 import org.springblade.archive.entity.*;
 import org.springblade.archive.mapper.ArchiveConclusionMapper;
 import org.springblade.archive.service.*;
@@ -89,7 +89,6 @@ import org.springframework.transaction.annotation.Transactional;
 
 import org.springframework.web.multipart.MultipartFile;
 
-import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import java.io.*;
@@ -100,7 +99,9 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.rmi.ServerException;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
@@ -5351,6 +5352,145 @@ public class ArchivesAutoServiceImpl extends BaseServiceImpl<ArchivesAutoMapper,
 				.sum();
 	}
 
+	@Override
+	@Async
+	public Boolean atuoOCR(List<Long> idsList) throws Exception {
+		String url="/mnt/sdc/AutoPdf/";
+		//String url="D:\\AutoPdf\\";
+		//List<Long> idsList=Func.toLongList(ids);
+		List<ArchivesAuto> archivesAutoList = this.list(new LambdaQueryWrapper<ArchivesAuto>().in(ArchivesAuto::getId, idsList));
+		for (ArchivesAuto auto : archivesAutoList) {
+			if(auto.getOutUrl()==null||auto.getOutUrl().isEmpty()){
+				continue;
+			}
+			String fileUrl=auto.getOutUrl().substring(0,auto.getOutUrl().indexOf("@"));
+			String filePath=url+auto.getName()+".pdf";
+			System.out.println("开始保存:"+fileUrl);
+			Boolean b = FileUtils.saveInputStreamByUrl(fileUrl, filePath);
+			System.out.println("保存完成:"+b);
+			try {
+				if(b){
+					System.out.println("开始识别:"+filePath);
+					List<String> list = extractTextFromPDF(filePath);
+					System.out.println("识别完成:"+list);
+					if(!list.isEmpty()){
+						StringBuilder fileName=new StringBuilder();
+						for (String result : list) {
+							if(result.contains("档号")){
+								String fileNum=result.replace("档号","").replace(":","").replace(":","");
+								auto.setFileNumber(fileNum);
+							}else if(result.contains("立卷单位")){
+								String unit=result.replace("立卷单位","").replace(":","").replace(":","");
+								auto.setUnit(unit);
+							} else if (result.contains("起止日期")) {
+								String time=result.replace("起止日期","").replace(":","").replace(":","");
+								if(result.contains("~")){
+									LocalDateTime[] localDateTimes = convertDateRange(time, "~");
+									auto.setStartDate(localDateTimes[0]);
+									auto.setEndDate(localDateTimes[1]);
+								} else if (result.contains("-")) {
+									LocalDateTime[] localDateTimes = convertDateRange(time, "-");
+									auto.setStartDate(localDateTimes[0]);
+									auto.setEndDate(localDateTimes[1]);
+								}
+							} else if (result.contains("保管期限")||result.contains("保管限期")) {
+								String storageTime=result.replace("保管期限","").replace("保管限期","").replace(":","").replace(":","");
+								auto.setStorageTime(storageTime);
+							} else if (result.contains("密1")||result.contains("密级")) {
+								String secretLevel=result.replace("密1","").replace("密级","");
+								auto.setSecretLevel(secretLevel);
+							} else {
+								fileName.append(result);
+							}
+						}
+						auto.setName(fileName.toString());
+					}
+				}
+			}catch (Exception e){
+
+			}finally {
+				FileUtils.removeFile(filePath);
+			}
+		}
+		this.updateBatchById(archivesAutoList);
+		return true;
+	}
+
+	public static LocalDateTime[] convertDateRange(String dateRange,String split) {
+		String[] dates = dateRange.split(split);
+
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+		LocalDate startLocalDate = LocalDate.parse(dates[0], formatter);
+		LocalDate endLocalDate = LocalDate.parse(dates[1], formatter);
+
+		LocalDateTime startDateTime = startLocalDate.atStartOfDay(); // 00:00:00
+		LocalDateTime endDateTime = endLocalDate.atStartOfDay();; // 00:00:00
+
+		return new LocalDateTime[]{startDateTime, endDateTime};
+	}
+
+	public List<String> extractTextFromPDF(String pdfFilePath) throws IOException, InterruptedException {
+		//String PYTHON_SCRIPT_PATH = "C:\\Users\\hc01\\AppData\\Local\\Programs\\Python\\Python310\\Python\\pdfTextExtractorWindows.py";
+		//String PYTHON_INTERPRETER = "C:\\Users\\hc01\\AppData\\Local\\Programs\\Python\\Python310\\python.exe";
+		System.out.println("进入识别1");
+		String PYTHON_SCRIPT_PATH = "/www/wwwlogs/python/pdfTextExtractorWindows.py";
+		String PYTHON_INTERPRETER = "python3";
+		String[] command = {
+				PYTHON_INTERPRETER,
+				PYTHON_SCRIPT_PATH,
+				pdfFilePath
+		};
+
+		Process process = new ProcessBuilder(command)
+				.redirectErrorStream(true)
+				.start();
+		System.out.println("进入识别2");
+		// 读取Python输出
+		StringBuilder output = new StringBuilder();
+		try (InputStream inputStream = process.getInputStream();
+			 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) {
+
+			String line;
+			while ((line = reader.readLine()) != null) {
+				output.append(line);
+			}
+		}
+		System.out.println("进入识别3");
+		int exitCode = process.waitFor();
+		if (exitCode != 0) {
+			System.out.println("进入识别4");
+			throw new RuntimeException("Python脚本执行失败,退出码: " + exitCode + ", 输出: " + output.toString());
+		}
+
+		// -------------------------- 关键修改:提取纯JSON部分 --------------------------
+		String rawOutput = output.toString();
+		// 找到JSON的起始位置(第一个'{')和结束位置(最后一个'}')
+		int jsonStart = rawOutput.indexOf('{');
+		int jsonEnd = rawOutput.lastIndexOf('}');
+		System.out.println("进入识别5");
+		if (jsonStart == -1 || jsonEnd == -1 || jsonStart >= jsonEnd) {
+			System.out.println("进入识别6");
+			throw new RuntimeException("无法提取有效的JSON结果,原始输出: " + rawOutput);
+		}
+		// 截取纯JSON字符串
+		String jsonStr = rawOutput.substring(jsonStart, jsonEnd + 1);
+		System.out.println("进入识别7");
+		// 解析清理后的JSON
+		Gson gson = new Gson();
+		Type type = new TypeToken<Map<String, Object>>(){}.getType();
+		Map<String, Object> resultMap = gson.fromJson(jsonStr, type);
+
+		if (!"success".equals(resultMap.get("status"))) {
+			System.out.println("进入识别8");
+			String message = (String) resultMap.get("message");
+			throw new RuntimeException("处理PDF失败: " + (message != null ? message : "未知错误"));
+		}
+		System.out.println("进入识别9");
+		Type listType = new TypeToken<List<String>>(){}.getType();
+		return gson.fromJson(gson.toJson(resultMap.get("lines")), listType);
+	}
+
 	@Scheduled(fixedDelay = 1000 * 60 * 10)
 	public void reCreateArchiveAuto() {
 		if (SystemUtils.isWindows() || SystemUtils.isMacOs()) {

+ 31 - 0
blade-service/blade-archive/src/main/java/org/springblade/archive/utils/FileUtils.java

@@ -897,4 +897,35 @@ public class FileUtils {
         return "";
     }
 
+    // 获取OSS文件流并且保存到本地
+    public static Boolean saveInputStreamByUrl(String fileUrl,String filePath) throws Exception {
+        InputStream fileInputStream = CommonUtil.getOSSInputStream(fileUrl);
+        if(fileInputStream==null){
+            return false;
+        }
+        try (FileOutputStream outputStream = new FileOutputStream(filePath)) {
+            byte[] buffer = new byte[1024];
+            int length;
+
+            while ((length = fileInputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+            }
+            return true;
+        } catch (IOException e) {
+            e.printStackTrace();
+            System.err.println("文件下载失败: " + e.getMessage());
+            return false;
+        }
+
+    }
+
+    public static Boolean removeFile(String filePath){
+        File file=new File(filePath);
+        if(file.exists()){
+           return file.delete();
+        }
+        return true;
+    }
+
+
 }

+ 6 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeController.java

@@ -580,6 +580,12 @@ public class WbsTreeController extends BladeController {
         }
         System.out.println(updateMap.size());
     }
+    @PostMapping("/copyNode")
+    @ApiOperationSupport(order = 50)
+    @ApiOperation(value = "复制节点")
+    public R copyNode(@RequestBody copyNodeVo vo){
+        return R.status(wbsTreeService.copyNode(vo.getLeftIds(),vo.getRightIds()));
+    }
 
 }
 

+ 67 - 3
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeSynchronousRecordController.java

@@ -4,9 +4,13 @@ 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 com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.apache.commons.beanutils.BeanUtils;
+import org.springblade.core.tool.utils.BeanUtil;
 import org.springblade.core.tool.utils.StringUtil;
-import org.springblade.manager.entity.ProjectInfo;
-import org.springblade.manager.entity.WbsTreePrivate;
+import org.springblade.manager.dto.WbsTreeSynchronousRecordDTO;
 import org.springblade.manager.entity.WbsTreeSynchronousRecord;
 import org.springblade.manager.service.WbsTreeSynchronousRecordService;
 import org.springblade.core.mp.support.Condition;
@@ -16,8 +20,9 @@ import org.springblade.manager.vo.WbsTreeSynchronousRecordVo;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
-import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * WBS同步记录表(MWbsTreeSynchronousRecord)表控制层
@@ -27,6 +32,7 @@ import java.util.List;
  */
 @RestController
 @RequestMapping("/synchronousRecord")
+@Api(value = "WBS同步记录表接口", tags = "WBS同步记录表接口")
 public class WbsTreeSynchronousRecordController {
     /**
      * 服务对象
@@ -41,6 +47,7 @@ public class WbsTreeSynchronousRecordController {
      * @param query  分页对象
      * @return 所有数据
      */
+    @ApiOperation(value = "分页查询")
     @GetMapping("/page")
     public R<IPage<WbsTreeSynchronousRecord>> selectAll(WbsTreeSynchronousRecord record, Query query) {
         LambdaQueryWrapper<WbsTreeSynchronousRecord> lambda = new QueryWrapper().lambda();
@@ -79,6 +86,7 @@ public class WbsTreeSynchronousRecordController {
      * @param id 主键
      * @return 单条数据
      */
+    @ApiOperation(value = "通过主键查询单条数据")
     @GetMapping("getById")
     public R<WbsTreeSynchronousRecord> selectOne(@RequestParam Long id) {
         return R.data(this.mWbsTreeSynchronousRecordService.getById(id));
@@ -91,6 +99,7 @@ public class WbsTreeSynchronousRecordController {
      * @param mWbsTreeSynchronousRecord 实体对象
      * @return 新增结果
      */
+    @ApiOperation(value = "新增数据")
     @PostMapping("add")
     public R<WbsTreeSynchronousRecord> insert(@RequestBody WbsTreeSynchronousRecord mWbsTreeSynchronousRecord) {
 
@@ -127,6 +136,7 @@ public class WbsTreeSynchronousRecordController {
     /**
      * 认证接口是否正在同步
      */
+    @ApiOperation(value = "认证接口是否正在同步")
     @PostMapping("getNodeStatus")
     public R<WbsTreeSynchronousRecord> getNodeStatus(@RequestParam Long id) {
         return R.data(this.mWbsTreeSynchronousRecordService.getNodeStatus(id));
@@ -138,6 +148,7 @@ public class WbsTreeSynchronousRecordController {
      *
      * @param nodeIds 节点ids 逗号拼接
      */
+    @ApiOperation(value = "获取当前项目的模板项目")
     @GetMapping("getTempProject")
     public R<List<WbsTreeSynchronousRecordVo>> getProjectTemplate(@RequestParam String nodeIds) {
         if (StringUtil.isBlank(nodeIds)) {
@@ -146,5 +157,58 @@ public class WbsTreeSynchronousRecordController {
         return R.data(this.mWbsTreeSynchronousRecordService.getProjectTemplate(nodeIds));
     }
 
+    /**
+     * 添加同步记录查询功能
+     *  采用定时查询模式展示进度条
+     */
+    @ApiOperation(value = "添加同步记录查询功能")
+    @GetMapping("querySyncRecord")
+    public R<List<WbsTreeSynchronousRecordDTO>> querySyncRecord(@RequestParam Long projectId) {
+        List<WbsTreeSynchronousRecord> list = this.mWbsTreeSynchronousRecordService.list(new QueryWrapper<WbsTreeSynchronousRecord>().lambda()
+                .eq(WbsTreeSynchronousRecord::getProjectId, projectId)
+                .eq(WbsTreeSynchronousRecord::getIsDeleted, 0)
+                .ne(WbsTreeSynchronousRecord::getStatus, 2)
+        );
+        List<WbsTreeSynchronousRecordDTO> wbsTreeSynchronousRecordDTOS = BeanUtil.copyProperties(list, WbsTreeSynchronousRecordDTO.class);
+        wbsTreeSynchronousRecordDTOS.forEach(m -> {
+            if(m.getNodeNum() != 0){
+                m.setSurplusCount(m.getNodeNum() - m.getNodeNumEnd());
+                int i = m.getNodeNumEnd() * 100 / m.getNodeNum();
+                if (m.getStatus() == 2 || i > 100) {
+                    i = 100;
+                }
+                m.setProgress(i);
+            }else{
+                m.setSurplusCount(0);
+                m.setProgress(0);
+            }
+        });
+        return R.data(wbsTreeSynchronousRecordDTOS);
+    }
+
+    /**
+     * 删除接口
+     */
+    @ApiOperation(value = "删除接口")
+    @GetMapping("delete")
+    public R<Boolean> delete(@RequestParam Long id) {
+        return R.data(this.mWbsTreeSynchronousRecordService.update(Wrappers.<WbsTreeSynchronousRecord>update().lambda()
+                .set(WbsTreeSynchronousRecord::getIsDeleted, 1)
+                .eq(WbsTreeSynchronousRecord::getId, id)
+        ));
+    }
+
+
+    /**
+     * 重刷接口
+     */
+    @ApiOperation(value = "重刷接口")
+    @GetMapping("reFlush")
+    public R<Boolean> reFlush(@RequestParam Long id) {
+        return R.data(this.mWbsTreeSynchronousRecordService.update(Wrappers.<WbsTreeSynchronousRecord>update().lambda()
+                .set(WbsTreeSynchronousRecord::getStatus, 0)
+                .eq(WbsTreeSynchronousRecord::getId, id)
+        ));
+    }
 }
 

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

@@ -120,7 +120,7 @@ public class ExcelTabClientImpl implements ExcelTabClient {
             if (isBatchSave == 0) {
                 //------单表PDF保存------
                 Integer type1 = Integer.parseInt(table.getString("type"));
-                TableInfo tableInfo = tableInfoList.stream().findAny().orElse(null);
+                TableInfo tableInfo = tableInfoList.stream().filter(t -> t.getPkeyId().equals(tabIds)).findAny().orElse(null);
                 if (tableInfo != null && tabIds.contains(tableInfo.getPkeyId())) {
                     String bussPDFTrial = excelTabService.getBussPDFTrial(Long.valueOf(tableInfo.getPkeyId()), contractId, id, 0, 0, dto);
                     if (StringUtils.isNotEmpty(bussPDFTrial)) {

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

@@ -102,4 +102,6 @@ public interface WbsTreeMapper extends EasyBaseMapper<WbsTree> {
 
     List<WbsTreePrivate> getTitleRange(@Param("projectId")Long projectId,
                                        @Param("wbsType")Integer wbsType);
+
+    List<WbsTree> selectAllChildNode(@Param("leftIds") List<Long> leftIds);
 }

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

@@ -572,5 +572,17 @@
             and wbs_type = #{wbsType}
         </if>
     </select>
+    <select id="selectAllChildNode" resultType="org.springblade.manager.entity.WbsTree">
+        SELECT DISTINCT *
+        FROM m_wbs_tree
+        WHERE is_deleted = 0
+        <if test="leftIds != null and leftIds.size() > 0">
+            AND (
+            <foreach collection="leftIds" item="item" separator=" OR ">
+                FIND_IN_SET(#{item}, ancestors) OR id = #{item}
+            </foreach>
+            )
+        </if>
+    </select>
 
 </mapper>

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

@@ -156,4 +156,6 @@ public interface WbsTreePrivateMapper extends EasyBaseMapper<WbsTreePrivate> {
     List<ArchiveSyncLogVO> getContractAllLogMonthPack(@Param("contractId") Long contractId);
 
     List<Long> getContractAllLogWbsNodeIds(@Param("contractId") Long contractId);
+
+    List<WbsTreePrivate> selectAllChildNode(List<String> leftIds);
 }

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

@@ -1074,4 +1074,5 @@
         select DISTINCT wbs_node_id from u_contract_log  WHERE
             contract_id = #{contractId}
     </select>
+    <select id="selectAllChildNode" resultType="org.springblade.manager.entity.WbsTreePrivate"></select>
 </mapper>

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

@@ -81,4 +81,6 @@ public interface IWbsTreeService extends BaseService<WbsTree> {
     List<WbsTreePrivate> getTitleRange(String projectId,Integer wbsType);
 
     Object getQueryValueByNodeType(WbsTreePrivateQueryVO vo);
+
+    boolean copyNode(List<Long> leftIds, List<Long> rightIds);
 }

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

@@ -3902,29 +3902,27 @@ public class ExcelTabServiceImpl extends BaseServiceImpl<ExcelTabMapper, ExcelTa
                                         }
 
                                         // 特殊处理多选框
-                                        if (myData.contains("http") && myData.contains("aliyuncs")) {
+                                        if (myData.indexOf("http") >= 0 && (myData.indexOf("aliyuncs") >= 0 ||myData.indexOf("183.247.216.148") >= 0||myData.indexOf("xinan1.zos.ctyun.cn") >= 0)) {
                                             InputStream imageIn = CommonUtil.getOSSInputStream(myData);
-                                            byte[] byteNew = new byte[0];
                                             if (imageIn != null) {
-                                                byteNew = IOUtils.toByteArray(imageIn);
+                                                byte[] bytes = CommonUtil.compressImage3(myData);
+                                                // 这里根据实际需求选择图片类型
+                                                int pictureIdx = workbook.addPicture(bytes, 6);
+                                                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 pict = drawing.createPicture(anchor, pictureIdx); // 调整图片占单元格百分比的大小,1.0就是100%
+                                                pict.resize(0.9, 0.9);
+                                                FileUtils.imageOrientation(sheet, anchor, new DataVO(x1 - 1, y1 - 1));
                                             }
 
-                                            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) {

+ 230 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/FormulaServiceImpl.java

@@ -8,6 +8,7 @@ import cn.hutool.log.StaticLog;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@@ -5068,8 +5069,221 @@ public class FormulaServiceImpl extends BaseServiceImpl<FormulaMapper, Formula>
      */
     public void preCalc(FormData fd, TableElementConverter tec) {
         try {
+            if(fd.getCode().equals("m_20250506162803_1919670481296818176:key_9")){
+                System.out.println();
+            }
             Formula formula = fd.getFormula();
             String f = formula.getFormula();
+            //图表公式
+            if(f.contains("chart")){
+                Matcher m = RegexUtils.matcher(FC_REG + "(chart)\\(([^)]+)\\)", f);
+                while (m.find()) {
+                    String[] args = m.group(2).split(",");
+                    //y轴源数据
+                    List<FormData> target = getFormDataByCode(args[0], tec);
+                    //x轴源数据
+                    List<FormData> target1 = getFormDataByCode(args[1], tec);
+
+                    List<Double> x = new ArrayList<>();
+                    List<Double> y = new ArrayList<>();
+
+                    if (!target.isEmpty()) {
+                        FormData a = target.get(0);
+                        for (ElementData value : a.getValues()) {
+                            if(ConvertUtils.canConvertToDouble(value.getValue())){
+                                y.add(Double.valueOf(value.getValue().toString()));
+                            }
+                        }
+
+                    }
+                    //含水率是x轴
+                    if (!target1.isEmpty()) {
+                        FormData a = target1.get(0);
+                        for (ElementData value : a.getValues()) {
+                            Object obj = value.getValue();
+                            //处理 小时:分钟
+                            if(obj !=null && obj.toString().contains(":")){
+                                String[] split = obj.toString().split(":");
+                                obj = Integer.parseInt(split[0]) * 60 + Integer.parseInt(split[1]);
+                            }
+                            if(ConvertUtils.canConvertToDouble(obj)){
+                                x.add(Double.valueOf(obj.toString()));
+                            }
+                        }
+                    }
+                    String url = "";
+                    if(CollectionUtil.isNotEmpty(x) && CollectionUtil.isNotEmpty(y)){
+                        //数据错误
+                        if(x.size() != y.size()){
+                            break;
+                        }
+                        double[][] points = new double[x.size()][2];
+                        for (int i = 0; i < x.size(); i++) {
+                            points[i][0] = x.get(i);
+                            points[i][1] = y.get(i);
+                        }
+
+                        Long id = SnowFlakeUtil.getId();
+
+                        //图片存放路径
+                        String filePath = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+                        String listPdf = filePath + "/pdf/" + id + ".png";
+//                        String listPdf = "C:\\upload\\pdf\\" + id + ".jpg";
+
+                        //土界含水率试验检测记录表
+                        if(fd.getCode().contains("m_20250506162329_1919669335463297024")){
+                            // 创建图表生成器
+                            SoilTestChart generator = new SoilTestChart(points);
+                            // 生成图表并保存为图片(不显示GUI窗口)
+                            generator.generateChart(listPdf, 330, 360);
+                        }else if (fd.getCode().contains("m_20250929094210_1972476967416496128")){
+                            //土工击试验
+                            // 创建图表生成器
+                            SmoothCurveChartToImage generator = new SmoothCurveChartToImage(points, "含水率(%)", "干密度(g/cm³)", "最大干密度曲线");
+
+                            // 生成图表并保存为图片
+                            generator.generateChart(listPdf,800,185);
+                        }else if (fd.getCode().contains("m_20250506162803_1919670481296818176")){
+                            //速凝剂
+                            PenetrationResistanceChart generator = new PenetrationResistanceChart();
+
+                            // 生成图表并保存为图片
+                            generator.generateChart(points, listPdf,420, 210);
+                        }
+
+                        File tabPDF = ResourceUtil.getFile(listPdf);
+                        //上传至oss
+                        BladeFile bladeFile = this.newIOSSClient.uploadFile(id + ".jpg", listPdf);
+                        if (tabPDF.exists()) {
+                            tabPDF.delete();
+                        }
+                        if (bladeFile != null) {
+                            url = bladeFile.getLink();
+                        }
+                    }
+
+                    //生成图片返回
+                    f = f.replace(m.group(), putDataWithKey(url, tec));
+                }
+            }
+            //细集料图表公式
+            if (f.contains("aggregateChart")) {
+                //x轴坐标 固定的
+                HashMap<Integer, Double> map1 = new HashMap<>();
+                map1.put(0, 4.75);
+                map1.put(1, 2.36);
+                map1.put(2, 1.18);
+                map1.put(3, 0.6);
+                map1.put(4, 0.3);
+                map1.put(5, 0.15);
+                HashMap<Integer, Double> map = new HashMap<>();
+
+
+                String tf = f.replaceAll("^T\\(com.mixsmart.utils.CustomFunction\\)\\.\\w+\\(", "").replaceAll("[)]$", "");
+                List<String> list = Arrays.asList(tf.split(","));
+                if(list.size() == 18){
+
+                    List<List<String>> partition1 = Lists.partition(list, 3);
+                    List<FormData> target = new ArrayList<>();
+                    //重新计算索引
+                    int x = 0;
+                    for (int i = 0; i < partition1.size(); i++) {
+                        List<String> strings = partition1.get(i);
+                        //按组取获取数据
+                        List<FormData> target1 = getFormDataByCode(StringUtils.join( strings,","), tec, null);
+                        //全部都能获取到数据
+                        if(target1.size() == strings.size()){
+                            target.addAll(target1);
+                            map.put(x,map1.get(i));
+                            x++;
+                        }
+                    }
+
+                    if(CollectionUtil.isNotEmpty(target)){
+                        //分组
+                        List<List<FormData>> partition = Lists.partition(target, 3);
+                        //长度是否匹配
+                        String url = "";
+
+                        double[][] data = new double[partition.size()][5];
+
+                        for (int i = 0; i < partition.size(); i++) {
+                            double[] doubles = new double[5];
+                            //每组的x坐标
+                            doubles[0] = map.get(i);
+                            List<FormData> formData = partition.get(i);
+
+                            //数据是否正常
+                            boolean flag = true;
+
+                            for (int j = 0; j < formData.size(); j++) {
+                                FormData formData1 = formData.get(j);
+                                if (CollectionUtil.isEmpty(formData1.getValues())) {
+                                    flag = false;
+                                    break;
+                                }
+                                if (formData1.getValues().get(0).getValue() == null) {
+                                    flag = false;
+                                    break;
+                                }
+                                String string = formData1.getValues().get(0).getValue().toString();
+                                if (StringUtil.isBlank(string)) {
+                                    flag = false;
+                                    break;
+                                }
+                                //最后一个范围数据
+                                if (string.contains("~")) {
+                                    String[] split = string.split("~");
+                                    if (ConvertUtils.canConvertToDouble(split[0])) {
+                                        doubles[j + 1] = Double.valueOf(split[0]);
+                                    }
+                                    if (ConvertUtils.canConvertToDouble(split[1])) {
+                                        doubles[j + 2] = Double.valueOf(split[1]);
+                                    }
+                                } else {
+                                    if (ConvertUtils.canConvertToDouble(string)) {
+                                        doubles[j + 1] = Double.valueOf(string);
+                                    } else {
+                                        flag = false;
+                                    }
+                                }
+
+
+                            }
+                            //数据正常才添加坐标
+                            if (flag) {
+                                data[i] = doubles;
+                            }
+                        }
+
+                        Long id = SnowFlakeUtil.getId();
+
+                        //图片存放路径
+                        String filePath = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+                        String listPdf = filePath + "/pdf/" + id + ".png";
+//                        String listPdf = "C:\\upload\\pdf\\" + id + ".jpg";
+
+
+                        // 创建图表生成器
+                        SieveAnalysisChart generator = new SieveAnalysisChart(data);
+
+                        // 生成图表并保存为图片
+                        generator.generateChart(listPdf, 800, 400);
+
+                        File tabPDF = ResourceUtil.getFile(listPdf);
+                        //上传至oss
+                        BladeFile bladeFile = this.newIOSSClient.uploadFile(id + ".jpg", listPdf);
+                        if (tabPDF.exists()) {
+                            tabPDF.delete();
+                        }
+                        if (bladeFile != null) {
+                            url = bladeFile.getLink();
+                        }
+                        //生成图片返回
+                        f = putDataWithKey(url, tec);
+                    }
+                }
+            }
             if (f.contains("converge")) {
                 Matcher m = RegexUtils.matcher(FC_REG + "(converge)\\(([^)]+)\\)", f);
                 while (m.find()) {
@@ -5692,6 +5906,22 @@ public class FormulaServiceImpl extends BaseServiceImpl<FormulaMapper, Formula>
         return target;
     }
 
+    /**
+     * 细集料图表获取数据单独调整 排除空数据和/数据
+     * */
+    public List<FormData> getFormDataByCode(String codes, TableElementConverter tec , String key) {
+        List<FormData> target = new ArrayList<>();
+        String[] tfa = codes.split(",");
+        for (String code : tfa) {
+            code = code.replace("E['", "").replace("']", "");
+            FormData fdt = tec.formDataMap.get(code);
+            if (fdt != null && CollectionUtil.isNotEmpty(fdt.getValues()) && !Objects.equals(fdt.getValues().get(0).getValue(),"/")) {
+                target.add(fdt);
+            }
+        }
+        return target;
+    }
+
     @Override
     public Map<String, Object> getElementInfoByCodes(String codes) {
         final String ekey = "ekey";

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

@@ -319,4 +319,59 @@ public class WbsSynchronousEViSaServiceImpl {
         Integer i = wbsTreeContractMapper.insertBatchSomeColumn(list);
         return i;
     }
+
+    /**
+     * 新增表单
+     * @param wbsTreeSynchronousRecord
+     * @param list
+     * @param pId
+     */
+    public void insertPrivateForm(WbsTreeSynchronousRecord wbsTreeSynchronousRecord, List<WbsTreePrivate> list, Long 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());
+            }
+        }
+        //单个批次一个事务,只会回滚当前批次数据
+        Integer i = wbsTreePrivateMapper.insertBatchSomeColumn(list);
+
+        //如果失败  -- - - - - 继续执行   或者把当前节点的p_key_id 记录到某个地方 方便后续处理
+        if (i == 0) {
+            //这里可以保存到数据库指定错误日志表
+        } 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);
+                }
+            }
+        }
+    }
 }

+ 31 - 75
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousServiceImpl.java

@@ -246,6 +246,7 @@ public class WbsSynchronousServiceImpl {
         ProjectInfo tempProject = projectInfoMapper.selectOne(Wrappers.<ProjectInfo>lambdaQuery()
                 .eq(ProjectInfo::getId, wbsTreeSynchronousRecord.getTemplateId()));
 
+        List<WbsTreePrivate> addData = new ArrayList<>();
         List<WbsTreePrivate> editData = new ArrayList<>();
 
         for (String primaryKeyId : nodeIds) {
@@ -512,7 +513,8 @@ public class WbsSynchronousServiceImpl {
                     switch (i) {
                         //添加表单
                         case 1:
-                            insertPrivateForm(wbsTreeSynchronousRecord, wbsTreePrivates, addPrivateNodes);
+                            List<WbsTreePrivate> list = insertPrivateForm(wbsTreeSynchronousRecord, wbsTreePrivates, addPrivateNodes);
+                            addData.addAll(list);
                             updateEViSa(collect, wbsTreeSynchronousRecord.getTemplateId(), projectInfo.getId(), templateNodes, addPrivateNodes);
                             break;
                         //清表
@@ -541,17 +543,32 @@ public class WbsSynchronousServiceImpl {
                 }
             }
         }
+        Set<Long> pIdsNew = new HashSet<>();
+        Map<Long, List<WbsTreePrivate>> addMap = addData.stream().collect(Collectors.groupingBy(WbsTreePrivate::getPId));
+        pIdsNew.addAll(addMap.keySet());
+
+
         //更新数据的同时统计最小节点数量
         Map<Long, List<WbsTreePrivate>> collect1 = editData.stream().collect(Collectors.groupingBy(WbsTreePrivate::getPId));
-        Set<Long> pIds = collect1.keySet();
+        pIdsNew.addAll(collect1.keySet());
         Integer nodeNumEnd = 0;
-        for (Long pId : pIds) {
+        //修改需要操作的数量
+        synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                .set(WbsTreeSynchronousRecord::getNodeNum, pIdsNew.size())
+                .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
+        for (Long pId : pIdsNew) {
             nodeNumEnd++;
+            //新增数据
+            List<WbsTreePrivate> addList = addMap.get(pId);
+            if (CollectionUtil.isNotEmpty(addList)) {
+                wbsSynchronousEViSaService.insertPrivateForm(wbsTreeSynchronousRecord, addList, pId);
+            }
             List<WbsTreePrivate> list = collect1.get(pId);
-
-            //更新最新节点
-            wbsSynchronousEViSaService.updatePrivate(wbsTreeSynchronousRecord.getType(), pId, wbsTreeSynchronousRecord.getCreateUserId(), list);
-
+            if(CollectionUtil.isNotEmpty(list)){
+                //更新最新节点
+                wbsSynchronousEViSaService.updatePrivate(wbsTreeSynchronousRecord.getType(), pId, wbsTreeSynchronousRecord.getCreateUserId(), list);
+            }
             synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
                     .set(WbsTreeSynchronousRecord::getNodeNumEnd, nodeNumEnd)
                     .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
@@ -871,7 +888,11 @@ public class WbsSynchronousServiceImpl {
 
         //需要记录历史html的表单
         Map<Long, List<WbsTreeContract>> collect3 = oldHtml.stream().collect(Collectors.groupingBy(WbsTreeContract::getPId));
-
+        //修改需要操作的数量
+        synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
+                .set(WbsTreeSynchronousRecord::getNodeNum, pIdsNew.size())
+                .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
+                .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
         for (Long pId : pIdsNew) {
             nodeNumEnd++;
 
@@ -937,7 +958,7 @@ public class WbsSynchronousServiceImpl {
      * @param wbsTreePrivates          当前项目对应节点的子节点
      * @param addPrivateNodes          需要新增的节点
      */
-    public void insertPrivateForm(WbsTreeSynchronousRecord wbsTreeSynchronousRecord, List<WbsTreePrivate> wbsTreePrivates, List<WbsTreePrivate> addPrivateNodes) {
+    public List<WbsTreePrivate> insertPrivateForm(WbsTreeSynchronousRecord wbsTreeSynchronousRecord, List<WbsTreePrivate> wbsTreePrivates, List<WbsTreePrivate> addPrivateNodes) {
         List<WbsTreePrivate> addData = new ArrayList<>();
 
         //------------------------------------------------新增-------------------------------------------------------------------------
@@ -1014,73 +1035,8 @@ public class WbsSynchronousServiceImpl {
                 e.printStackTrace();
                 throw new ServiceException("重置表单路径错误");
             }
-
-            //新增-----------------------------------------------------------------------------------------------------------------
-            Map<Long, List<WbsTreePrivate>> collect = addData.stream().collect(Collectors.groupingBy(WbsTreePrivate::getPId));
-            Set<Long> longs = collect.keySet();
-
-            List<Long> pIds = new ArrayList<>(longs);
-            //按最小节点批量新增
-            List<List<Long>> partition = Lists.partition(pIds, 100);
-            int sum = 0;
-            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());
-                        }
-                    }
-
-                    //单个批次一个事务,只会回滚当前批次数据
-                    Integer i = wbsTreePrivateMapper.insertBatchSomeColumn(list);
-                    //如果失败  -- - - - - 继续执行   或者把当前节点的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);
-                            }
-                        }
-                        synchronousRecordMapper.update(null, Wrappers.<WbsTreeSynchronousRecord>lambdaUpdate()
-                                .set(WbsTreeSynchronousRecord::getNodeNumEnd, sum++)
-                                .set(WbsTreeSynchronousRecord::getUpdateTime, DateTime.now())
-                                .eq(WbsTreeSynchronousRecord::getId, wbsTreeSynchronousRecord.getId()));
-                    }
-                }
-            }
         }
+        return addData;
     }
 
     /**

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

@@ -3216,6 +3216,133 @@ public class WbsTreePrivateServiceImpl extends BaseServiceImpl<WbsTreePrivateMap
         return baseMapper.getContractAllLogWbsNodeIds(contractId);
     }
 
+//    @Override
+//    @Transactional(rollbackFor = Exception.class)
+//    public boolean copyNode(List<String> leftIds, List<String> rightIds) {
+//        List<WbsTreePrivate> leftLists = baseMapper.selectAllChildNode(leftIds);
+//        for (String rightId : rightIds) {
+//            // 每次循环都创建一个新的集合作为副本
+//            List<WbsTreePrivate> workingList = leftLists.stream()
+//                    .map(node -> {
+//                        // 创建每个节点的副本
+//                        WbsTreePrivate copy = new WbsTreePrivate();
+//                        BeanUtils.copyProperties(copy, node);
+//                        return copy;
+//                    })
+//                    .collect(Collectors.toList());
+//            WbsTreePrivate rightWbsTreePrivate = baseMapper.getByPKeyId(Long.parseLong(rightId));
+//            Integer rightNodeType = rightWbsTreePrivate.getNodeType();
+//            if (rightNodeType == 1) {
+//                rightNodeType=1;
+//            }else if(rightNodeType==18){
+//                rightNodeType=2;
+//            }else {
+//                rightNodeType=rightNodeType++;
+//            }
+//            for (WbsTreePrivate leftList : leftLists) {
+//                Integer leftType=leftList.getNodeType();
+//                if(leftType==1){
+//                    leftType=1;
+//                }else if(leftType==18){
+//                    leftType=2;
+//                }else {
+//                    leftType=leftType++;
+//                }
+//                if(leftType<=rightNodeType){
+//                    throw new ServiceException(leftList.getNodeName()+"不能复制到"+rightWbsTreePrivate.getNodeName()+",原因节点类型不能复制");
+//                }
+//            }
+//            // 找到leftLists中所有的根节点(没有在leftLists中作为子节点出现的节点)
+//            Set<Long> allPIds = workingList.stream()
+//                    .map(WbsTreePrivate::getPId)
+//                    .filter(Objects::nonNull)
+//                    .collect(Collectors.toSet());
+//
+//            List<WbsTreePrivate> rootNodes = workingList.stream()
+//                    .filter(node -> !allPIds.contains(node.getPKeyId()))
+//                    .collect(Collectors.toList());
+//
+//            // 为每个根节点重新设置属性,并将其放到rightWbsTreePrivate节点下
+//            for (WbsTreePrivate rootNode : rootNodes) {
+//                // 重新分配节点值
+//                reassignNodeValues(workingList, rootNode, rightWbsTreePrivate);
+//            }
+//            this.insertBatch(workingList,500);
+//        }
+//        return true;
+//    }
+    // 添加一个辅助方法来重新分配节点值
+    private void reassignNodeValues(List<WbsTreePrivate> leftLists, WbsTreePrivate rootNode, WbsTreePrivate rightWbsTreePrivate) {
+        // 创建节点映射以便快速查找
+        Map<Long, WbsTreePrivate> nodeMap = leftLists.stream()
+                .collect(Collectors.toMap(WbsTreePrivate::getPKeyId, node -> node));
+
+        // 为根节点生成新的随机ID
+        Long newRootId = SnowFlakeUtil.getId();
+        Long newRootPKeyId = SnowFlakeUtil.getId();
+
+        // 设置根节点属性,使其成为rightWbsTreePrivate的子节点
+        rootNode.setId(newRootId);
+        rootNode.setPKeyId(newRootPKeyId);
+        rootNode.setParentId(rightWbsTreePrivate.getId());
+        rootNode.setPId(rightWbsTreePrivate.getPKeyId());
+
+        // ancestors设置为rightWbsTreePrivate的ancestors加上rightWbsTreePrivate的id
+        if (rightWbsTreePrivate.getAncestors() != null && !rightWbsTreePrivate.getAncestors().isEmpty()) {
+            rootNode.setAncestors(rightWbsTreePrivate.getAncestors() + "," + rightWbsTreePrivate.getId());
+        } else {
+            rootNode.setAncestors(String.valueOf(rightWbsTreePrivate.getId()));
+        }
+
+        // ancestors_p_id设置为rightWbsTreePrivate的ancestors_p_id加上rightWbsTreePrivate的pKeyId
+        if (rightWbsTreePrivate.getAncestorsPId() != null && !rightWbsTreePrivate.getAncestorsPId().isEmpty()) {
+            rootNode.setAncestorsPId(rightWbsTreePrivate.getAncestorsPId() + "," + rightWbsTreePrivate.getPKeyId());
+        } else {
+            rootNode.setAncestorsPId(String.valueOf(rightWbsTreePrivate.getPKeyId()));
+        }
+
+        // 更新所有子节点
+        updateChildNodes(nodeMap, rootNode, rootNode.getAncestors(), rootNode.getAncestorsPId(), newRootPKeyId);
+    }
+
+    // 递归更新子节点属性
+    private void updateChildNodes(Map<Long, WbsTreePrivate> nodeMap, WbsTreePrivate parentNode, String parentAncestors, String parentAncestorsPId, Long parentPKeyId) {
+        // 查找当前节点的所有直接子节点(通过pId匹配父节点的pKeyId)
+        List<WbsTreePrivate> childNodes = nodeMap.values().stream()
+                .filter(node -> node.getPId() != null && node.getPId().equals(parentNode.getPKeyId()))
+                .collect(Collectors.toList());
+
+        // 更新每个子节点的属性
+        for (WbsTreePrivate childNode : childNodes) {
+            // 为子节点生成新的随机ID
+            Long newChildId = SnowFlakeUtil.getId();
+            Long newChildPKeyId = SnowFlakeUtil.getId();
+
+            // id设置为新的随机ID
+            childNode.setId(newChildId);
+
+            // pKeyId设置为新的随机ID
+            childNode.setPKeyId(newChildPKeyId);
+
+            // parentId设置为父节点的id
+            childNode.setParentId(parentNode.getId());
+
+            // pId设置为父节点的pKeyId
+            childNode.setPId(parentPKeyId);
+
+            // ancestors设置为父节点的ancestors加上父节点的id
+            childNode.setAncestors(parentAncestors + "," + parentNode.getId());
+
+            // ancestors_p_id设置为父节点的ancestors_p_id加上父节点的pKeyId
+            childNode.setAncestorsPId(parentAncestorsPId + "," + parentPKeyId);
+
+            // 递归更新孙子节点
+            updateChildNodes(nodeMap, childNode, childNode.getAncestors(), childNode.getAncestorsPId(), newChildPKeyId);
+        }
+    }
+
+
+
     public void diGuiWbs(int i) {
         QueryWrapper<WbsTreePrivate> wbsTreePrivateQueryWrapper = new QueryWrapper<>();
         wbsTreePrivateQueryWrapper.select("p_key_id", "id", "p_id", "wbs_id", "project_id", "parent_id", "ancestors");

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

@@ -29,6 +29,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
 import org.springblade.manager.utils.DiffListUtil;
 import org.springblade.manager.utils.WbsElementUtil;
 import org.springblade.manager.vo.*;
+import org.springframework.beans.BeanUtils;
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.scheduling.annotation.Async;
@@ -599,6 +600,155 @@ public class WbsTreeServiceImpl extends BaseServiceImpl<WbsTreeMapper, WbsTree>
         return null;
     }
 
+    @Override
+    public boolean copyNode(List<Long> leftIds, List<Long> rightIds) {
+        // 获取所有需要复制的节点
+        List<WbsTree> originalNodes = baseMapper.selectAllChildNode(leftIds);
+
+        for (Long rightId : rightIds) {
+            // 第一要求:每次循环重新创建新的leftLists
+            List<WbsTree> leftLists = originalNodes.stream()
+                    .map(node -> {
+                        WbsTree copy = new WbsTree();
+                        BeanUtils.copyProperties(node, copy);
+                        return copy;
+                    })
+                    .collect(Collectors.toList());
+
+            // 第二要求:找出新的leftLists里面id在leftIds里面的作为根节点
+            List<WbsTree> rootNodes = leftLists.stream()
+                    .filter(node -> leftIds.contains(node.getId()))
+                    .collect(Collectors.toList());
+
+            // 获取目标父节点信息
+            WbsTree wbsTree = baseMapper.selectById(rightId);
+               Integer rightNodeType=wbsTree.getNodeType();
+            if (rightNodeType == 1) {
+                rightNodeType=1;
+            }else if(rightNodeType==18){
+                rightNodeType=2;
+            }else {
+                rightNodeType=rightNodeType+1;
+            }
+            Set<Integer> typeSet = leftLists.stream().filter(o -> o.getType()==1).map(o -> o.getNodeType()).collect(Collectors.toSet());
+            for (Integer type : typeSet) {
+                Integer leftType;
+                if(type==1){
+                    leftType=1;
+                }else if(type==18){
+                    leftType=2;
+                }else {
+                    leftType=type+1;
+                }
+                if(leftType<=rightNodeType){
+                    WbsTree leftList = leftLists.stream().filter(f -> f.getNodeType().equals(type)).findFirst().get();
+                    throw new ServiceException("【"+leftList.getNodeName()+"】不能复制到【"+wbsTree.getNodeName()+"】,原因:节点类型不能复制");
+                }
+            }
+            // 第三要求:重新构建整个树的id、parentId和ancestors
+            rebuildTreeStructure(leftLists, rootNodes, rightId, wbsTree);
+
+            // 保存到数据库
+            if (!leftLists.isEmpty()) {
+                this.insertBatchPublic(leftLists, 500);
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 重新构建整个树的id、parentId和ancestors
+     */
+    private void rebuildTreeStructure(List<WbsTree> allNodes,
+                                      List<WbsTree> rootNodes,
+                                      Long targetParentId,
+                                      WbsTree targetParentNode) {
+        // 存储原ID到新ID的映射
+        Map<Long, Long> idMapping = new HashMap<>();
+
+        // 第一步:为所有节点生成新的ID并建立映射
+        for (WbsTree node : allNodes) {
+            Long newId = SnowFlakeUtil.getId();
+            idMapping.put(node.getId(), newId);
+            node.setId(newId);
+        }
+
+        // 第二步:处理根节点
+        for (WbsTree rootNode : rootNodes) {
+            // 根节点的parentId设置为targetParentId
+            rootNode.setParentId(targetParentId);
+
+            // 根节点的ancestors = 目标父节点的ancestors + "," + targetParentId
+            String targetAncestors = targetParentNode.getAncestors();
+            if (targetAncestors == null || targetAncestors.isEmpty()) {
+                rootNode.setAncestors(targetParentId.toString());
+            } else {
+                rootNode.setAncestors(targetAncestors + "," + targetParentId);
+            }
+        }
+
+        // 第三步:使用队列进行广度优先遍历,处理所有子节点
+        Queue<WbsTree> queue = new LinkedList<>(rootNodes);
+
+        while (!queue.isEmpty()) {
+            WbsTree currentNode = queue.poll();
+            Long currentNewId = currentNode.getId();
+
+            // 查找当前节点的所有直接子节点(根据原始父子关系)
+            List<WbsTree> children = allNodes.stream()
+                    .filter(node -> {
+                        // 获取节点的原始parentId
+                        Long originalParentId = getOriginalParentId(node, idMapping);
+                        // 获取当前节点的原始ID
+                        Long currentOriginalId = getKeyByValue(idMapping, currentNewId);
+                        // 判断是否是当前节点的子节点
+                        return originalParentId != null && originalParentId.equals(currentOriginalId);
+                    })
+                    .collect(Collectors.toList());
+
+            for (WbsTree child : children) {
+                // 设置子节点的parentId为当前节点的新ID
+                child.setParentId(currentNewId);
+
+                // 设置子节点的ancestors为当前节点的ancestors + "," + 当前节点的新ID
+                child.setAncestors(currentNode.getAncestors() + "," + currentNewId);
+
+                queue.offer(child);
+            }
+        }
+    }
+
+    /**
+     * 获取节点的原始parentId
+     */
+    private Long getOriginalParentId(WbsTree node, Map<Long, Long> idMapping) {
+        // 节点的parentId在复制后可能已经被修改,我们需要找到它原始的parentId
+        // 通过反向查找idMapping来找到原始的parentId
+        Long currentParentId = node.getParentId();
+
+        // 如果当前parentId不在idMapping的value中,说明这是原始parentId
+        if (!idMapping.containsValue(currentParentId)) {
+            return currentParentId;
+        }
+
+        // 如果当前parentId在idMapping的value中,找到对应的原始ID
+        return getKeyByValue(idMapping, currentParentId);
+    }
+
+    /**
+     * 根据value从Map中获取key
+     */
+    private Long getKeyByValue(Map<Long, Long> map, Long value) {
+        for (Map.Entry<Long, Long> entry : map.entrySet()) {
+            if (entry.getValue().equals(value)) {
+                return entry.getKey();
+            }
+        }
+        return null;
+    }
+
+
+
     /**
      * 公有排序
      */

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

@@ -56,6 +56,11 @@ public class WbsTreeSynchronousRecordServiceImpl extends ServiceImpl<WbsTreeSync
                 .in(WbsTreeSynchronousRecord::getStatus, 0, 1));
         List<String> nodeIds = wbsTreeSynchronousRecords.stream().map(WbsTreeSynchronousRecord::getNodeId).collect(Collectors.toList());
 
+        //  //判断节点类型  如果是试验或计量则不允许添加合同合同段
+        WbsTreePrivate wbsTreePrivate1 = wbsTreePrivateMapper.selectById(mWbsTreeSynchronousRecord.getNodeId());
+        if (mWbsTreeSynchronousRecord.getRange() == 2 && wbsTreePrivate1 != null && !Objects.equals(wbsTreePrivate1.getWbsType(), "1")) {
+            throw new ServiceException(wbsTreePrivate1.getNodeName() + " 节点不是质检类型,无法同步合同段");
+        }
         //所有子节点集合
         List<Long> privateIds = new ArrayList<>();
         //通过 ancestors_p_id 查询所有 非表单子节点
@@ -69,13 +74,6 @@ public class WbsTreeSynchronousRecordServiceImpl extends ServiceImpl<WbsTreeSync
                 );
                 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() + " 节点不是质检类型,无法同步合同段");
-                }
             }
         }
 

+ 36 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/ConvertUtils.java

@@ -0,0 +1,36 @@
+package org.springblade.manager.utils;
+
+import java.util.Optional;
+
+/**
+ * 类型转换
+ * @author LHB
+ */
+public class ConvertUtils {
+
+    public static boolean canConvertToDouble(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        // 如果是数字类型,除了Double,还有Integer、Long等都可以转成double
+        if (obj instanceof Number) {
+            return true;
+        }
+        // 如果是字符串
+        if (obj instanceof String) {
+            String str = (String) obj;
+            // 去除千分位中的逗号
+            String withoutCommas = str.replace(",", "");
+            try {
+                // 尝试转换
+                Double.parseDouble(withoutCommas);
+                return true;
+            } catch (NumberFormatException e) {
+                // 转换失败
+                return false;
+            }
+        }
+        // 其他类型,无法转换
+        return false;
+    }
+}

+ 272 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/PenetrationResistanceChart.java

@@ -0,0 +1,272 @@
+package org.springblade.manager.utils;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.annotations.XYLineAnnotation;
+import org.jfree.chart.annotations.XYTextAnnotation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.chart.ui.RectangleEdge;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.chart.ui.TextAnchor;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * 凝结时间差
+ * @author LHB
+ */
+public class PenetrationResistanceChart {
+
+    /**
+     * 生成贯入阻力图表并保存为图片
+     * @param data 二维数组,第一列为时间(分钟),第二列为贯入阻力(MPa)
+     * @param outputPath 输出图片路径
+     * @return 成功返回true,失败返回false
+     */
+    public static boolean generateChart(double[][] data, String outputPath, int width, int height) {
+        try {
+            // 创建数据集 - 主曲线
+            XYSeries mainSeries = new XYSeries("贯入阻力");
+            for (double[] point : data) {
+                mainSeries.add(point[0], point[1]);
+            }
+
+            XYSeriesCollection dataset = new XYSeriesCollection();
+            dataset.addSeries(mainSeries);
+
+            // 创建支持中文的字体
+            Font chineseFont = new Font("SimSun", Font.BOLD, 12);
+            Font titleFont = new Font("SimHei", Font.BOLD, 16);
+            Font legendFont = new Font("SimSun", Font.BOLD, 10);
+
+            // 创建图表
+            JFreeChart chart = ChartFactory.createXYLineChart(
+                    "减水剂凝结时间差试验曲线",
+                    "时间 (分钟)",
+                    "贯入阻力 (MPa)",
+                    dataset,
+                    PlotOrientation.VERTICAL,
+                    true,  // 包含图例
+                    true,
+                    false
+            );
+
+            // 设置中文字体
+            chart.getTitle().setFont(titleFont);
+            chart.getLegend().setItemFont(legendFont);
+
+            // 设置图表背景和边距
+            chart.setBackgroundPaint(Color.WHITE);
+            chart.setPadding(new RectangleInsets(15, 15, 15, 15));
+
+            // 获取图表区域对象
+            XYPlot plot = (XYPlot) chart.getPlot();
+            plot.setBackgroundPaint(Color.WHITE);
+            plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
+            plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
+            plot.setAxisOffset(new RectangleInsets(5, 5, 5, 5));
+
+            // 配置X轴(设置中文字体)
+            NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
+            xAxis.setAutoRangeIncludesZero(true);
+            xAxis.setLabelFont(chineseFont);
+            xAxis.setTickLabelFont(chineseFont);
+            xAxis.setTickMarkOutsideLength(5.0f);
+
+            // 配置Y轴(设置中文字体)
+            NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
+            yAxis.setAutoRangeIncludesZero(true);
+            yAxis.setLabelFont(chineseFont);
+            yAxis.setTickLabelFont(chineseFont);
+            yAxis.setTickMarkOutsideLength(5.0f);
+
+            plot.setAxisOffset(new RectangleInsets(0,0,0,0));
+
+            // 设置渲染器
+            XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
+
+            // 主曲线样式
+            renderer.setSeriesPaint(0, Color.BLUE);
+            renderer.setSeriesFillPaint(0, new Color(255,255,255,255));
+            //设置空心
+            renderer.setUseFillPaint(true);
+            renderer.setSeriesShape(0, new java.awt.geom.Ellipse2D.Double(-3, -3, 6, 6));
+            // 主曲线不显示点
+            renderer.setSeriesShapesVisible(0, true);
+            plot.setRenderer(0, renderer);
+
+            // 查找3.5MPa和28MPa对应的点
+            double target1 = 3.5;
+            double target2 = 28.0;
+            Double timeAt35 = null;
+            Double resistanceAt35 = target1;
+            Double timeAt28 = null;
+            Double resistanceAt28 = target2;
+
+            for (int i = 0; i < data.length - 1; i++) {
+                // 查找3.5MPa
+                if (timeAt35 == null &&
+                        ((data[i][1] <= target1 && data[i+1][1] >= target1) ||
+                                (data[i][1] >= target1 && data[i+1][1] <= target1))) {
+                    // 线性插值计算时间
+                    double x1 = data[i][0];
+                    double y1 = data[i][1];
+                    double x2 = data[i+1][0];
+                    double y2 = data[i+1][1];
+                    timeAt35 = x1 + (target1 - y1) * (x2 - x1) / (y2 - y1);
+                }
+
+                // 查找28MPa
+                if (timeAt28 == null &&
+                        ((data[i][1] <= target2 && data[i+1][1] >= target2) ||
+                                (data[i][1] >= target2 && data[i+1][1] <= target2))) {
+                    // 线性插值计算时间
+                    double x1 = data[i][0];
+                    double y1 = data[i][1];
+                    double x2 = data[i+1][0];
+                    double y2 = data[i+1][1];
+                    timeAt28 = x1 + (target2 - y1) * (x2 - x1) / (y2 - y1);
+                }
+
+                if (timeAt35 != null && timeAt28 != null) break;
+            }
+
+
+            // 为A点和B点添加虚拟系列用于图例
+            XYSeriesCollection pointDataset = new XYSeriesCollection();
+            renderer = new XYLineAndShapeRenderer();
+            int index = 0;
+            // 添加特殊点到数据集
+            if (timeAt35 != null) {
+                // 创建特殊点数据集
+                XYSeries point35Series = new XYSeries(String.format("初凝点(3.5MPa, %.0f分钟)", timeAt35));
+                point35Series.add(timeAt35, resistanceAt35);
+                pointDataset.addSeries(point35Series);
+                // 3.5MPa点样式
+                renderer.setSeriesPaint(index, Color.RED);
+                renderer.setSeriesStroke(index, new BasicStroke(1.0f));
+                // 圆形标记
+                renderer.setSeriesShape(index, new java.awt.geom.Ellipse2D.Double(-3, -3, 5, 5));
+                renderer.setSeriesShapesVisible(index, true);
+                // 不连接线
+                renderer.setSeriesLinesVisible(index, false);
+                index++;
+            }
+
+            if (timeAt28 != null) {
+                XYSeries point28Series = new XYSeries(String.format("终凝点(28MPa,%.0f分钟)", timeAt28));
+                point28Series.add(timeAt28, resistanceAt28);
+                pointDataset.addSeries(point28Series);
+                // 28MPa点样式
+                renderer.setSeriesPaint(index, Color.GREEN);
+                renderer.setSeriesStroke(index, new BasicStroke(1.0f));
+                // 圆形标记
+                renderer.setSeriesShape(index, new java.awt.geom.Ellipse2D.Double(-3, -3, 5, 5));
+                renderer.setSeriesShapesVisible(index, true);
+                // 不连接线
+                renderer.setSeriesLinesVisible(index, false);
+
+            }
+            //自定义点位
+            plot.setDataset(1, pointDataset);
+            //自定义点位样式
+            plot.setRenderer(1,renderer);
+
+
+
+
+            // 添加3.5MPa的标注和虚线
+            if (timeAt35 != null) {
+
+
+                // 水平虚线
+                XYLineAnnotation hLine35 = new XYLineAnnotation(
+                        plot.getDomainAxis().getLowerBound(), target1,
+                        plot.getDomainAxis().getUpperBound(), target1,
+                        new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                                10.0f, new float[]{5.0f, 5.0f}, 0.0f),
+                        Color.RED
+                );
+                plot.addAnnotation(hLine35);
+
+                // 垂直虚线
+                XYLineAnnotation vLine35 = new XYLineAnnotation(
+                        timeAt35, plot.getRangeAxis().getLowerBound(),
+                        timeAt35, plot.getRangeAxis().getUpperBound(),
+                        new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                                10.0f, new float[]{5.0f, 5.0f}, 0.0f),
+                        Color.RED
+                );
+                plot.addAnnotation(vLine35);
+            }
+
+            // 添加28MPa的标注和虚线
+            if (timeAt28 != null) {
+                // 水平虚线
+                XYLineAnnotation hLine28 = new XYLineAnnotation(
+                        plot.getDomainAxis().getLowerBound(), target2,
+                        plot.getDomainAxis().getUpperBound(), target2,
+                        new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                                10.0f, new float[]{5.0f, 5.0f}, 0.0f),
+                        Color.GREEN
+                );
+                plot.addAnnotation(hLine28);
+
+                // 垂直虚线
+                XYLineAnnotation vLine28 = new XYLineAnnotation(
+                        timeAt28, plot.getRangeAxis().getLowerBound(),
+                        timeAt28, plot.getRangeAxis().getUpperBound(),
+                        new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                                10.0f, new float[]{5.0f, 5.0f}, 0.0f),
+                        Color.GREEN
+                );
+                plot.addAnnotation(vLine28);
+            }
+
+            // 调整图例位置到左上角
+            chart.getLegend().setBackgroundPaint(new Color(255, 255, 255, 200)); // 半透明背景
+            chart.getLegend().setPosition(RectangleEdge.TOP);
+
+
+            // 保存图表为PNG文件
+            File outputFile = new File(outputPath);
+            ChartUtils.saveChartAsPNG(outputFile, chart, width, height);
+
+            return true;
+        } catch (IOException e) {
+            System.err.println("保存图表时发生错误: " + e.getMessage());
+            return false;
+        } catch (Exception e) {
+            System.err.println("生成图表时发生错误: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    public static void main11(String[] args) {
+        // 示例数据:时间(分钟)和贯入阻力(MPa)
+        double[][] data = {
+                {0, 0.5}, {10, 1.2}, {20, 2.1}, {30, 3.0}, {40, 3.8},
+                {50, 5.2}, {60, 7.5}, {70, 10.3}, {80, 14.2}, {90, 18.5},
+                {100, 22.8}, {110, 26.5}, {120, 29.2}, {130, 31.5}, {140, 33.2},
+                {150, 34.5}, {160, 35.2}, {170, 35.8}, {180, 36.0}
+        };
+
+        // 生成图表并保存为图片
+        boolean success = generateChart(data, "penetration_resistance_chart.png",420,210);
+
+        if (success) {
+            System.out.println("图表已成功生成并保存为 'penetration_resistance_chart.png'");
+        } else {
+            System.out.println("图表生成失败");
+        }
+    }
+}

+ 217 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SieveAnalysisChart.java

@@ -0,0 +1,217 @@
+package org.springblade.manager.utils;
+
+import com.mixsmart.utils.RegexUtils;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.NumberTickUnit;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.DatasetRenderingOrder;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.category.LineAndShapeRenderer;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.data.category.DefaultCategoryDataset;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.regex.Matcher;
+
+/**
+ * 筛分分析图表生成工具类
+ */
+public class SieveAnalysisChart {
+    private double[][] data;
+
+    /**
+     * 构造函数
+     * @param data 筛分分析数据,格式: {{筛孔尺寸, 通过率, 累计筛余率, 第二种数据下限, 第二种数据上限}, ...}
+     */
+    public SieveAnalysisChart(double[][] data) {
+        this.data = data;
+    }
+
+    /**
+     * 生成筛分分析图表并保存为图片
+     * @param filename 输出文件名
+     * @param width 图片宽度
+     * @param height 图片高度
+     * @throws IOException 当保存文件出错时抛出
+     */
+    public void generateChart(String filename, int width, int height) throws IOException {
+        // 创建数据集 - 累计筛余率
+        DefaultCategoryDataset retainedDataset = new DefaultCategoryDataset();
+        // 创建数据集 - 通过率
+        DefaultCategoryDataset passingDataset = new DefaultCategoryDataset();
+        // 创建第二种数据 - 范围下限
+        DefaultCategoryDataset secondDataLowerDataset = new DefaultCategoryDataset();
+        // 创建第二种数据 - 范围上限
+        DefaultCategoryDataset secondDataUpperDataset = new DefaultCategoryDataset();
+
+        // 填充数据
+        for (double[] point : data) {
+            String sieveSize = String.valueOf(point[0]);
+            double retainedPercent = point[1];
+            double passingPercent = point[2];
+            double secondLower = point[3];
+            double secondUpper = point[4];
+
+            retainedDataset.addValue(retainedPercent, "累计筛余率", sieveSize);
+            passingDataset.addValue(passingPercent, "通过率", sieveSize);
+            secondDataLowerDataset.addValue(secondLower, "第二种数据下限", sieveSize);
+            secondDataUpperDataset.addValue(secondUpper, "第二种数据上限", sieveSize);
+        }
+
+        // 创建图表(不显示图例)
+        JFreeChart chart = ChartFactory.createLineChart(
+                // 标题
+                "集料级配曲线",
+                // X轴标题
+                "筛孔尺寸(mm)",
+                // 不显示左侧Y轴标题
+                "",
+                // 数据集
+                retainedDataset,
+                PlotOrientation.VERTICAL,
+                // 不显示图例
+                false,
+                false,
+                false
+        );
+
+        // 设置中文字体,防止乱码
+        Font chineseFont = new Font("SimSun", Font.BOLD, 12);
+        chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 16));
+
+        // 获取图表区域对象
+        CategoryPlot plot = chart.getCategoryPlot();
+
+        // 设置左侧Y轴 (累计筛余率)
+        NumberAxis retainedAxis = (NumberAxis) plot.getRangeAxis();
+        retainedAxis.setRange(0, 100);
+        retainedAxis.setTickUnit(new NumberTickUnit(10));
+        retainedAxis.setLabelFont(chineseFont);
+        retainedAxis.setTickLabelFont(chineseFont);
+        // 在轴线内侧添加标签
+        retainedAxis.setLabel("累计筛余率(%)");
+        retainedAxis.setLabelInsets(new RectangleInsets(0, 0, 0, 0));
+
+        // 创建并设置右侧Y轴 (通过率)
+        NumberAxis passingAxis = new NumberAxis("");
+        // 正常范围设置
+        passingAxis.setRange(0, 100);
+        // 反转Y轴,使100在顶部,0在底部
+        passingAxis.setInverted(true);
+        passingAxis.setTickUnit(new NumberTickUnit(10));
+        passingAxis.setLabelFont(chineseFont);
+        passingAxis.setTickLabelFont(chineseFont);
+        // 在轴线内侧添加标签
+        passingAxis.setLabel("通过率百分比(%)");
+        passingAxis.setLabelInsets(new RectangleInsets(0, 0, 0, 0));
+
+        // 设置X轴
+        CategoryAxis domainAxis = plot.getDomainAxis();
+        domainAxis.setLabelFont(chineseFont);
+        domainAxis.setTickLabelFont(chineseFont);
+
+        // 将右侧Y轴添加到图表
+        plot.setRangeAxis(1, passingAxis);
+
+        plot.setAxisOffset(new RectangleInsets(0,0,0,0));
+
+        // 添加通过率数据集
+        plot.setDataset(1, passingDataset);
+        // 将第二个数据集映射到第二个Y轴
+        plot.mapDatasetToRangeAxis(1, 1);
+
+        // 添加第二种数据范围数据集
+        plot.setDataset(2, secondDataLowerDataset);
+        plot.setDataset(3, secondDataUpperDataset);
+        // 映射到左侧Y轴
+        plot.mapDatasetToRangeAxis(2, 0);
+        // 映射到左侧Y轴
+        plot.mapDatasetToRangeAxis(3, 0);
+
+        // 设置渲染器 - 累计筛余率(实线,不显示数据点)
+        LineAndShapeRenderer retainedRenderer = (LineAndShapeRenderer) plot.getRenderer();
+        // 不显示数据点
+        retainedRenderer.setDefaultShapesVisible(false);
+        retainedRenderer.setSeriesPaint(0, Color.BLACK);
+        // 设置渲染器 - 通过率(实线,不显示数据点)
+        LineAndShapeRenderer passingRenderer = new LineAndShapeRenderer();
+        // 不显示数据点
+        passingRenderer.setDefaultShapesVisible(false);
+        passingRenderer.setSeriesPaint(0, Color.BLACK);
+        plot.setRenderer(1, passingRenderer);
+
+        // 设置渲染器 - 第二种数据范围(虚线,不显示数据点)
+        LineAndShapeRenderer secondLowerRenderer = new LineAndShapeRenderer();
+        // 不显示数据点
+        secondLowerRenderer.setDefaultShapesVisible(false);
+
+        // 设置虚线样式
+        secondLowerRenderer.setSeriesStroke(0, new BasicStroke(
+                1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
+                1.0f, new float[] {6.0f, 6.0f}, 0.0f
+        ));
+        // 设置颜色
+        secondLowerRenderer.setSeriesPaint(0, Color.BLACK);
+
+        LineAndShapeRenderer secondUpperRenderer = new LineAndShapeRenderer();
+        // 不显示数据点
+        secondUpperRenderer.setDefaultShapesVisible(false);
+        // 设置虚线样式
+        secondUpperRenderer.setSeriesStroke(0, new BasicStroke(
+                1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
+                1.0f, new float[] {6.0f, 6.0f}, 0.0f
+        ));
+        // 设置颜色
+        secondUpperRenderer.setSeriesPaint(0, Color.BLACK);
+
+        // 设置渲染器
+        plot.setRenderer(2, secondLowerRenderer);
+        plot.setRenderer(3, secondUpperRenderer);
+
+        // 设置数据集渲染顺序,确保所有线都可见
+        plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
+
+        // 设置背景网格线为实线
+        plot.setDomainGridlineStroke(new BasicStroke(1.0f));
+        plot.setRangeGridlineStroke(new BasicStroke(1.0f));
+        plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
+        plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
+        // 确保网格线可见
+        plot.setDomainGridlinesVisible(true);
+        plot.setRangeGridlinesVisible(true);
+
+        // 设置背景颜色为白色
+        plot.setBackgroundPaint(Color.WHITE);
+
+        // 保存为图片文件
+        ChartUtils.saveChartAsPNG(new File(filename), chart, width, height);
+    }
+    public static final String FC_REG = "T\\(com.mixsmart.utils.CustomFunction\\)\\.";
+    /**
+     * 使用示例
+     */
+    public static void main11(String[] args) {
+        try {
+            // 示例数据格式: {{筛孔尺寸, 通过率, 累计筛余率, 第二种数据下限, 第二种数据上限}, ...}
+            double[][] data = {
+                    {4.75, 5.0, 85.0, 0.0, 10.0},
+                    {2.36, 25.0, 55.0, 0.0, 35.0}
+            };
+
+            // 创建图表生成器
+            SieveAnalysisChart generator = new SieveAnalysisChart(data);
+
+            // 生成图表并保存为图片
+            generator.generateChart("sieve_analysis_chart.png", 800, 400);
+            System.out.println("图表已保存为 sieve_analysis_chart.png");
+        } catch (IOException e) {
+            System.err.println("保存图表时出错: " + e.getMessage());
+        }
+    }
+}

+ 278 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SmoothCurveChartToImage.java

@@ -0,0 +1,278 @@
+package org.springblade.manager.utils;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.annotations.XYLineAnnotation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYSplineRenderer;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+import java.awt.*;
+import java.awt.geom.Ellipse2D;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * 最大干密度曲线生成器
+ */
+public class SmoothCurveChartToImage {
+    private double[][] dataPoints;
+    private String xAxisLabel;
+    private String yAxisLabel;
+    private String chartTitle;
+
+    /**
+     * 构造函数
+     * @param dataPoints 二维数组,每行表示一个点 [x, y]
+     * @param xAxisLabel X轴标签
+     * @param yAxisLabel Y轴标签
+     * @param chartTitle 图表标题
+     */
+    public SmoothCurveChartToImage(double[][] dataPoints, String xAxisLabel, String yAxisLabel, String chartTitle) {
+        this.dataPoints = dataPoints;
+        this.xAxisLabel = xAxisLabel;
+        this.yAxisLabel = yAxisLabel;
+        this.chartTitle = chartTitle;
+    }
+
+    /**
+     * 生成图表并保存为图片
+     * @param filePath 文件路径
+     * @param width 图片宽度
+     * @param height 图片高度
+     */
+    public void generateChart(String filePath, int width, int height) {
+        // 创建数据集
+        XYSeries series = new XYSeries("数据点");
+
+        // 添加数据点
+        for (double[] point : dataPoints) {
+            if (point.length >= 2) {
+                series.add(point[0], point[1]);
+            }
+        }
+
+        XYSeriesCollection dataset = new XYSeriesCollection(series);
+
+        // 创建图表,不显示图例
+        JFreeChart chart = ChartFactory.createXYLineChart(
+                chartTitle,
+                xAxisLabel,
+                yAxisLabel,
+                dataset,
+                PlotOrientation.VERTICAL,
+                false,
+                true,
+                false
+        );
+
+        // 设置中文字体,防止乱码
+        chart.getTitle().setFont(new Font("微软雅黑", Font.BOLD, 16));
+
+        // 获取图表区域
+        XYPlot plot = chart.getXYPlot();
+
+        // 设置X轴和Y轴标签字体
+        NumberAxis domainAxis = (NumberAxis) plot.getDomainAxis();
+        domainAxis.setLabelFont(new Font("微软雅黑", Font.PLAIN, 12));
+        domainAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10));
+
+        NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
+        rangeAxis.setLabelFont(new Font("微软雅黑", Font.PLAIN, 12));
+        rangeAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10));
+
+        // 调整Y轴范围,不从0开始
+        double minY = series.getMinY();
+        double maxY = series.getMaxY();
+        double range = maxY - minY;
+
+        // 设置Y轴范围,从最小值下方10%开始,到最大值上方10%结束
+        double lowerBound = minY - range * 0.1;
+        double upperBound = maxY + range * 0.1;
+
+        rangeAxis.setRange(lowerBound, upperBound);
+
+        // 调整X轴范围,使其更加合适
+        double minX = series.getMinX();
+        double maxX = series.getMaxX();
+        double xRange = maxX - minX;
+        domainAxis.setRange(minX - xRange * 0.1, maxX + xRange * 0.1);
+
+        // 使用样条渲染器创建平滑曲线
+        XYSplineRenderer renderer = new XYSplineRenderer();
+        renderer.setSeriesPaint(0, Color.BLUE);
+        renderer.setSeriesStroke(0, new BasicStroke(2.0f));
+        // 显示数据点
+        renderer.setSeriesShapesVisible(0, true);
+        // 设置数据点的形状为圆形
+        renderer.setSeriesShape(0, new Ellipse2D.Double(-3, -3, 6, 6));
+
+        plot.setRenderer(renderer);
+
+        // 设置图表背景和网格线
+        plot.setBackgroundPaint(Color.LIGHT_GRAY);
+        plot.setDomainGridlinePaint(Color.WHITE);
+        plot.setRangeGridlinePaint(Color.WHITE);
+        plot.setDomainGridlinesVisible(true);
+        plot.setRangeGridlinesVisible(true);
+
+        // 计算曲线上的峰值点
+        double[] peakPoint = findPeakOnCurve(dataset);
+        double peakX = peakPoint[0];
+        double peakY = peakPoint[1];
+
+        System.out.println("找到峰值点: (" + peakX + ", " + peakY + ")");
+
+        // 创建一个新的系列只包含峰值点
+        XYSeries peakSeries = new XYSeries("峰值点");
+        peakSeries.add(peakX, peakY);
+        dataset.addSeries(peakSeries);
+
+        // 为峰值点系列设置不同的渲染属性
+        renderer.setSeriesPaint(1, Color.RED);
+        renderer.setSeriesShapesVisible(1, true);
+        renderer.setSeriesShape(1, new Ellipse2D.Double(-3, -3, 6, 6));
+        renderer.setSeriesLinesVisible(1, false); // 不连接线
+
+        // 添加垂直线到X轴
+        XYLineAnnotation xLine = new XYLineAnnotation(
+                peakX, peakY, peakX, plot.getRangeAxis().getLowerBound(),
+                new BasicStroke(1.5f), Color.RED);
+        plot.addAnnotation(xLine);
+
+        // 添加垂直线到Y轴
+        XYLineAnnotation yLine = new XYLineAnnotation(
+                plot.getDomainAxis().getLowerBound(), peakY, peakX, peakY,
+                new BasicStroke(1.5f), Color.RED);
+        plot.addAnnotation(yLine);
+
+        // 设置背景网格线为实线
+        plot.setDomainGridlineStroke(new BasicStroke(1.0f));
+        plot.setRangeGridlineStroke(new BasicStroke(1.0f));
+        plot.setDomainGridlinePaint(Color.BLACK);
+        plot.setRangeGridlinePaint(Color.BLACK);
+        // 确保网格线可见
+        plot.setDomainGridlinesVisible(true);
+        plot.setRangeGridlinesVisible(true);
+        plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0));
+
+        // 保存图表为图片文件
+        try {
+            File file = new File(filePath);
+            ChartUtils.saveChartAsPNG(file, chart, width, height);
+            System.out.println("图表已保存为: " + file.getAbsolutePath());
+        } catch (IOException e) {
+            System.err.println("保存图表时出错: " + e.getMessage());
+        }
+    }
+
+    // 在曲线上查找峰值点的方法
+    private static double[] findPeakOnCurve(XYSeriesCollection dataset) {
+        XYSeries series = dataset.getSeries(0);
+        int pointCount = series.getItemCount();
+
+        // 获取X和Y范围
+        double minX = series.getMinX();
+        double maxX = series.getMaxX();
+
+        // 在曲线上采样多个点来找到峰值
+        int samples = 1000; // 采样点数量
+        double step = (maxX - minX) / samples;
+
+        double peakX = minX;
+        double peakY = Double.NEGATIVE_INFINITY;
+
+        // 遍历采样点
+        for (int i = 0; i <= samples; i++) {
+            double x = minX + i * step;
+            double y = calculateSplineValue(series, x);
+
+            if (y > peakY) {
+                peakY = y;
+                peakX = x;
+            }
+        }
+
+        return new double[]{peakX, peakY};
+    }
+
+    // 计算样条曲线上某点的Y值
+    private static double calculateSplineValue(XYSeries series, double x) {
+        int n = series.getItemCount();
+
+        // 如果x超出数据范围,返回边界值
+        if (n == 0) return 0;
+        if (n == 1) return series.getY(0).doubleValue();
+
+        if (x <= series.getX(0).doubleValue()) {
+            return series.getY(0).doubleValue();
+        }
+        if (x >= series.getX(n - 1).doubleValue()) {
+            return series.getY(n - 1).doubleValue();
+        }
+
+        // 找到x所在的区间
+        int i = 0;
+        while (i < n - 1 && series.getX(i + 1).doubleValue() < x) {
+            i++;
+        }
+
+        // 获取相邻四个点用于三次样条计算
+        double x0 = (i > 0) ? series.getX(i - 1).doubleValue() : series.getX(i).doubleValue();
+        double x1 = series.getX(i).doubleValue();
+        double x2 = series.getX(i + 1).doubleValue();
+        double x3 = (i < n - 2) ? series.getX(i + 2).doubleValue() : series.getX(i + 1).doubleValue();
+
+        double y0 = (i > 0) ? series.getY(i - 1).doubleValue() : series.getY(i).doubleValue();
+        double y1 = series.getY(i).doubleValue();
+        double y2 = series.getY(i + 1).doubleValue();
+        double y3 = (i < n - 2) ? series.getY(i + 2).doubleValue() : series.getY(i + 1).doubleValue();
+
+        // 使用Catmull-Rom样条插值计算y值
+        return catmullRomSpline(x, x0, x1, x2, x3, y0, y1, y2, y3);
+    }
+
+    // Catmull-Rom样条插值算法
+    private static double catmullRomSpline(double x, double x0, double x1, double x2, double x3,
+                                           double y0, double y1, double y2, double y3) {
+        // 参数化t (0到1之间)
+        double t = (x - x1) / (x2 - x1);
+
+        // Catmull-Rom样条公式
+        double t2 = t * t;
+        double t3 = t2 * t;
+
+        return 0.5 * ((2 * y1) +
+                (-y0 + y2) * t +
+                (2 * y0 - 5 * y1 + 4 * y2 - y3) * t2 +
+                (-y0 + 3 * y1 - 3 * y2 + y3) * t3);
+    }
+
+    // 示例使用方法
+    public static void main11(String[] args) {
+        // 示例数据:二维数组,每行表示一个点 [含水率, 干密度]
+        double[][] points = {
+                {8.0, 1.75},
+                {12.0, 1.92},
+                {16.0, 2.05},
+                {20.0, 1.98},
+                {24.0, 1.86}
+        };
+
+        // 创建图表生成器
+        SmoothCurveChartToImage generator = new SmoothCurveChartToImage(
+                points,
+                "含水率(%)",
+                "干密度(g/cm³)",
+                "最大干密度曲线"
+        );
+
+        // 生成图表并保存为图片
+        generator.generateChart("SmoothCurveChart.png", 800, 185);
+    }
+}

+ 314 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SoilTestChart.java

@@ -0,0 +1,314 @@
+package org.springblade.manager.utils;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtils;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.annotations.XYTextAnnotation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.NumberTickUnit;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.data.xy.XYDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ * 界限含水率
+ * @author LHB
+ */
+public class SoilTestChart {
+
+    // 定义关键数据点
+    private double[][] dataPoints; // 存储原始数据点
+    private double w_avg; // C'点含水率
+    private double wl;    // WL含水率
+
+    public SoilTestChart(double[][] points) {
+        // 确保数据点按含水率排序(a点含水率最大,在最右边)
+        this.dataPoints = sortDataPoints(points);
+    }
+
+    // 按含水率排序数据点(确保a点在最右边)
+    private double[][] sortDataPoints(double[][] points) {
+        // 复制数组以避免修改原始数据
+        double[][] sorted = new double[points.length][2];
+        for (int i = 0; i < points.length; i++) {
+            sorted[i][0] = points[i][0]; // 含水率
+            sorted[i][1] = points[i][1]; // 锥入深度
+        }
+
+        // 按含水率升序排序
+        Arrays.sort(sorted, Comparator.comparingDouble(a -> a[0]));
+        return sorted;
+    }
+
+    private XYDataset createDataset() {
+        // 提取排序后的点
+        double moistureC = dataPoints[0][0]; // C点含水率(最小)
+        double depthC = dataPoints[0][1];    // C点锥入深度
+
+        double moistureB = dataPoints[1][0]; // B点含水率
+        double depthB = dataPoints[1][1];    // B点锥入深度
+
+        double moistureA = dataPoints[2][0]; // A点含水率(最大)
+        double depthA = dataPoints[2][1];    // A点锥入深度
+
+        // 计算AB和AC线上h=2mm时的含水率
+        double w_ab = calculateWaterContent(moistureA, depthA, moistureB, depthB, 2.0);
+        double w_ac = calculateWaterContent(moistureA, depthA, moistureC, depthC, 2.0);
+
+        // 计算平均含水率作为C'点
+        w_avg = (w_ab + w_ac) / 2.0;
+
+        // 创建AC'线数据集 - 只包含A点和C'点
+        XYSeries seriesACPrime = new XYSeries("AC'线");
+        seriesACPrime.add(moistureA, depthA); // A点
+        seriesACPrime.add(w_avg, 2.0);        // C'点
+
+        // 计算h=20mm时的含水率(WL)
+        wl = calculateWaterContent(moistureA, depthA, w_avg, 2.0, 20.0);
+
+        // 创建WL和WP的虚线数据集
+        XYSeries seriesWL = new XYSeries("WL");
+        seriesWL.add(wl, 0);
+        seriesWL.add(wl, 20);
+
+        XYSeries seriesWP = new XYSeries("WP");
+        seriesWP.add(w_avg, 0);
+        seriesWP.add(w_avg, 2);
+
+        XYSeries seriesWLHorizontal = new XYSeries("WL_H");
+        seriesWLHorizontal.add(0, 20);
+        seriesWLHorizontal.add(wl, 20);
+
+        XYSeries seriesWPHorizontal = new XYSeries("WP_H");
+        seriesWPHorizontal.add(0, 2);
+        seriesWPHorizontal.add(w_avg, 2);
+
+        // 创建原始点数据集
+        XYSeries seriesPoints = new XYSeries("原始点");
+        seriesPoints.add(moistureA, depthA); // A点
+        seriesPoints.add(moistureB, depthB); // B点
+        seriesPoints.add(moistureC, depthC); // C点
+
+        // 创建C'点数据集(单独显示)
+        XYSeries seriesCPrime = new XYSeries("C'点");
+        seriesCPrime.add(w_avg, 2.0);
+
+        // 将所有系列添加到数据集
+        XYSeriesCollection dataset = new XYSeriesCollection();
+        dataset.addSeries(seriesACPrime);
+        dataset.addSeries(seriesWL);
+        dataset.addSeries(seriesWP);
+        dataset.addSeries(seriesWLHorizontal);
+        dataset.addSeries(seriesWPHorizontal);
+        dataset.addSeries(seriesPoints);
+        dataset.addSeries(seriesCPrime);
+
+        return dataset;
+    }
+
+    // 计算直线上给定深度对应的含水率
+    private double calculateWaterContent(double x1, double y1, double x2, double y2, double targetY) {
+        double k = (y2 - y1) / (x2 - x1);
+        double b = y1 - k * x1;
+        return (targetY - b) / k;
+    }
+
+    private JFreeChart createChart(XYDataset dataset) {
+        // 提取排序后的点
+        double moistureC = dataPoints[0][0]; // C点含水率(最小)
+        double depthC = dataPoints[0][1];    // C点锥入深度
+
+        double moistureB = dataPoints[1][0]; // B点含水率
+        double depthB = dataPoints[1][1];    // B点锥入深度
+
+        double moistureA = dataPoints[2][0]; // A点含水率(最大)
+        double depthA = dataPoints[2][1];    // A点锥入深度
+
+        // 创建折线图
+        JFreeChart chart = ChartFactory.createXYLineChart(
+                "锥入深度与含水率(h-w)关系",      // 图表标题
+                "含水率w(%)",           // x轴标签
+                "锥入深度h(mm)",       // y轴标签
+                dataset,              // 数据集
+                PlotOrientation.VERTICAL,
+                false,               // 不显示图例
+                false,               // 不显示提示
+                false                // 不显示URL
+        );
+
+        // 调整图表边距
+        chart.setPadding(new RectangleInsets(10, 10, 10, 10));
+
+        // 解决中文乱码问题
+        chart.getTitle().setFont(new Font("宋体", Font.BOLD, 18));
+
+        // 获取图表区域对象
+        XYPlot plot = chart.getXYPlot();
+
+        // 设置x轴
+        NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
+        xAxis.setLabelFont(new Font("宋体", Font.BOLD, 14));
+        xAxis.setTickLabelFont(new Font("宋体", Font.BOLD, 12));
+        xAxis.setTickMarkOutsideLength(-5.0f);
+        xAxis.setTickMarksVisible(true);
+        xAxis.setAutoRangeIncludesZero(false);
+        // 设置x轴刻度间隔为10
+        xAxis.setTickUnit(new NumberTickUnit(10));
+        // 设置x轴范围,根据数据调整
+        xAxis.setRange(0, 100);
+
+        // 设置y轴
+        NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
+        yAxis.setLabelFont(new Font("宋体", Font.BOLD, 14));
+        yAxis.setTickLabelFont(new Font("宋体", Font.PLAIN, 12));
+        yAxis.setTickMarkOutsideLength(-5.0f);
+        yAxis.setTickMarksVisible(true);
+
+        //设置坐标轴偏移
+        plot.setAxisOffset(new RectangleInsets(0,0,0,0));
+        // 设置渲染器
+        XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
+
+        // AC'线 - 实线(只连接A点和C'点)
+        renderer.setSeriesPaint(0, Color.BLACK);
+        renderer.setSeriesStroke(0, new BasicStroke(2.0f));
+        renderer.setSeriesShapesVisible(0, false); // 不显示形状
+
+        // WL虚线 - 红色
+        renderer.setSeriesPaint(1, Color.BLACK);
+        renderer.setSeriesStroke(1, new BasicStroke(
+                1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                10.0f, new float[]{5.0f, 5.0f}, 0.0f
+        ));
+        renderer.setSeriesShapesVisible(1, false);
+
+        // WP虚线 - 绿色
+        renderer.setSeriesPaint(2, Color.BLACK);
+        renderer.setSeriesStroke(2, new BasicStroke(
+                1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                10.0f, new float[]{5.0f, 5.0f}, 0.0f
+        ));
+        renderer.setSeriesShapesVisible(2, false);
+
+        // WL水平虚线 - 红色
+        renderer.setSeriesPaint(3, Color.BLACK);
+        renderer.setSeriesStroke(3, new BasicStroke(
+                1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                10.0f, new float[]{5.0f, 5.0f}, 0.0f
+        ));
+        renderer.setSeriesShapesVisible(3, false);
+
+        // WP水平虚线 - 绿色
+        renderer.setSeriesPaint(4, Color.BLACK);
+        renderer.setSeriesStroke(4, new BasicStroke(
+                1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
+                10.0f, new float[]{5.0f, 5.0f}, 0.0f
+        ));
+        renderer.setSeriesShapesVisible(4, false);
+
+        // 原始点 - 不连线,只显示形状
+        renderer.setSeriesPaint(5, Color.BLACK);
+        renderer.setSeriesLinesVisible(5, false);
+        renderer.setSeriesShapesVisible(5, true);
+        renderer.setSeriesShape(5, new java.awt.geom.Ellipse2D.Double(-4, -4, 8, 8));
+
+        // C'点 - 不连线,只显示形状
+        renderer.setSeriesPaint(6, Color.BLACK);
+        renderer.setSeriesLinesVisible(6, false);
+        renderer.setSeriesShapesVisible(6, true);
+        renderer.setSeriesShape(6, new java.awt.geom.Ellipse2D.Double(-4, -4, 8, 8));
+
+        plot.setRenderer(renderer);
+
+        // 设置背景色
+        plot.setBackgroundPaint(Color.WHITE);
+        // 移除水平方向网格线
+        plot.setDomainGridlinesVisible(false);
+        // 移除垂直方向网格线
+        plot.setRangeGridlinesVisible(false);
+        // 禁用绘图区域外边框
+//        plot.setOutlineVisible(false);
+
+        // A点标签(含水率最大,在最右边)
+        XYTextAnnotation aLabel = new XYTextAnnotation("A", moistureA - 1, depthA + 0.5);
+        aLabel.setFont(new Font("宋体", Font.BOLD, 14));
+        aLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(aLabel);
+
+        // B点标签
+        XYTextAnnotation bLabel = new XYTextAnnotation("B", moistureB - 1, depthB + 0.5);
+        bLabel.setFont(new Font("宋体", Font.BOLD, 14));
+        bLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(bLabel);
+
+        // C点标签(含水率最小,在最左边)
+        XYTextAnnotation cLabel = new XYTextAnnotation("C", moistureC - 1, depthC + 0.5);
+        cLabel.setFont(new Font("宋体", Font.BOLD, 14));
+        cLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(cLabel);
+
+        // C'点标签
+        XYTextAnnotation cPrimeLabel = new XYTextAnnotation("C'", w_avg - 1, 2.0 + 0.5);
+        cPrimeLabel.setFont(new Font("宋体", Font.BOLD, 14));
+        cPrimeLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(cPrimeLabel);
+
+        // WL标签(x轴上方)
+        XYTextAnnotation wlLabel = new XYTextAnnotation("WL", wl - 1, 0.5);
+        wlLabel.setFont(new Font("宋体", Font.BOLD, 12));
+        wlLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(wlLabel);
+
+        // WP标签(x轴上方)
+        XYTextAnnotation wpLabel = new XYTextAnnotation("WP", w_avg - 1, 0.5);
+        wpLabel.setFont(new Font("宋体", Font.BOLD, 12));
+        wpLabel.setPaint(Color.BLACK);
+        plot.addAnnotation(wpLabel);
+
+        return chart;
+    }
+
+    // 生成图表并保存为图片
+    public void generateChart(String filename, int width, int height) {
+        try {
+            // 创建数据集
+            XYDataset dataset = createDataset();
+
+            // 创建图表
+            JFreeChart chart = createChart(dataset);
+
+            // 保存图表为图片
+            ChartUtils.saveChartAsPNG(new File(filename), chart, width, height);
+            System.out.println("图表已保存为 " + filename);
+        } catch (IOException e) {
+            System.err.println("保存图表时出错: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    public static void main11(String[] args) {
+        // 示例数据:二维数组,每行表示一个点 [含水率, 锥入深度]
+        // 注意:a点应该含水率最大,在最右边
+        double[][] points = {
+                {25.0, 5.0},  // C点(含水率最小)
+                {35.0, 15.0}, // B点
+                {45.0, 25.0}  // A点(含水率最大)
+        };
+
+        // 创建图表生成器
+        SoilTestChart generator = new SoilTestChart(points);
+
+        // 生成图表并保存为图片(不显示GUI窗口)
+        generator.generateChart("SoilTestChart.png", 500, 600);
+    }
+}