Переглянути джерело

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

cr 1 день тому
батько
коміт
d747e8f9d0
14 змінених файлів з 1559 додано та 105 видалено
  1. 17 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/dto/WbsTreeSynchronousRecordDTO.java
  2. 19 0
      blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/WbsTreeSynchronousRecord.java
  3. 67 3
      blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeSynchronousRecordController.java
  4. 1 1
      blade-service/blade-manager/src/main/java/org/springblade/manager/feign/ExcelTabClientImpl.java
  5. 17 19
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/ExcelTabServiceImpl.java
  6. 230 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/FormulaServiceImpl.java
  7. 55 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousEViSaServiceImpl.java
  8. 31 75
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsSynchronousServiceImpl.java
  9. 5 7
      blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/WbsTreeSynchronousRecordServiceImpl.java
  10. 36 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/ConvertUtils.java
  11. 272 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/PenetrationResistanceChart.java
  12. 217 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SieveAnalysisChart.java
  13. 278 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SmoothCurveChartToImage.java
  14. 314 0
      blade-service/blade-manager/src/main/java/org/springblade/manager/utils/SoilTestChart.java

+ 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;
     /**

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

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

@@ -3815,29 +3815,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;
     }
 
     /**

+ 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);
+    }
+}