Browse Source

feature: 智能控制V2版本 初版结束

Jayhaw 1 week ago
parent
commit
380fb8a3c8
24 changed files with 422 additions and 76 deletions
  1. 1 1
      pom.xml
  2. 1 3
      src/main/java/cn/sciento/farm/automationv2/api/controller/IrrigationTaskController.java
  3. 6 0
      src/main/java/cn/sciento/farm/automationv2/api/dto/CreateTaskRequest.java
  4. 3 2
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/CloseGroupNodeHandler.java
  5. 13 12
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/OpenGroupNodeHandler.java
  6. 2 1
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/SetPumpPressureNodeHandler.java
  7. 2 1
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StartFertilizerNodeHandler.java
  8. 2 1
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StartPumpNodeHandler.java
  9. 2 1
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StopFertilizerNodeHandler.java
  10. 2 1
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StopPumpNodeHandler.java
  11. 236 18
      src/main/java/cn/sciento/farm/automationv2/app/service/SafeShutdownService.java
  12. 5 1
      src/main/java/cn/sciento/farm/automationv2/app/service/TaskExecutionEngine.java
  13. 3 4
      src/main/java/cn/sciento/farm/automationv2/app/service/TaskLogService.java
  14. 7 1
      src/main/java/cn/sciento/farm/automationv2/domain/entity/IrrigationTask.java
  15. 24 0
      src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskFailVO.java
  16. 7 0
      src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskLog.java
  17. 44 0
      src/main/java/cn/sciento/farm/automationv2/domain/enums/DeviceType.java
  18. 7 1
      src/main/java/cn/sciento/farm/automationv2/domain/enums/ZonePhase.java
  19. 20 24
      src/main/java/cn/sciento/farm/automationv2/domain/service/ExecutionPlanGenerator.java
  20. 10 0
      src/main/java/cn/sciento/farm/automationv2/domain/valueobject/DeviceInfo.java
  21. 15 0
      src/main/java/cn/sciento/farm/automationv2/domain/valueobject/ExecutionPlan.java
  22. 5 0
      src/main/java/cn/sciento/farm/automationv2/domain/valueobject/IrrigationTaskVO.java
  23. 1 1
      src/main/java/cn/sciento/farm/automationv2/infra/feign/DeviceFeign.java
  24. 4 3
      src/main/resources/mapper/IrrigationTaskMapper.xml

+ 1 - 1
pom.xml

@@ -10,7 +10,7 @@
     </parent>
     <groupId>cn.sciento.farm</groupId>
     <artifactId>wf-equipment-automation-V2</artifactId>
-    <version>0.0.1-SNAPSHOT</version>
+    <version>1.1.1-RELEASE</version>
     <name>wf-equipment-automation-V2</name>
     <description>wf-equipment-automation-V2</description>
 

+ 1 - 3
src/main/java/cn/sciento/farm/automationv2/api/controller/IrrigationTaskController.java

@@ -239,9 +239,7 @@ public class IrrigationTaskController {
         return Results.success(taskService.pageByTask(dto,pageRequest));
     }
 
-    /**
-     * 取消执行(触发安全关闭)
-     */
+    @ApiOperation("取消执行(触发安全关闭)")
     @PostMapping("/{executionId}/cancel")
     @Permission(level = ResourceLevel.ORGANIZATION)
     public ResponseEntity cancelExecution(@PathVariable Long tenantId,@PathVariable Long executionId) {

+ 6 - 0
src/main/java/cn/sciento/farm/automationv2/api/dto/CreateTaskRequest.java

@@ -101,6 +101,11 @@ public class CreateTaskRequest {
     private String pumpDeviceName;
 
     /**
+     * 水泵设备品牌
+     */
+    private String pumpDeviceBrand;
+
+    /**
      * 水泵压力模式: 统一恒压=1 / 灌区恒压=2
      */
     @NotNull(message = "水泵压力模式不能为空")
@@ -231,6 +236,7 @@ public class CreateTaskRequest {
                 .taskName(taskName)
                 .pumpDeviceId(pumpDeviceId)
                 .pumpDeviceName(pumpDeviceName)
+                .pumpDeviceBrand(pumpDeviceBrand)
                 .isPump(isPump)
                 .pressureMode(pressureMode)
                 .targetPressureKpa(targetPressureKpa)

+ 3 - 2
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/CloseGroupNodeHandler.java

@@ -8,6 +8,7 @@ import cn.sciento.farm.automationv2.app.service.TaskLogService;
 import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
 import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
 import cn.sciento.farm.automationv2.domain.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
@@ -60,7 +61,7 @@ public class CloseGroupNodeHandler implements NodeHandler {
         for (DeviceInfo device : devices) {
             try {
                 Integer result = null;
-                if ("SOLENOID_VALVE".equals(device.getDeviceType())) {
+                if (DeviceType.SOLENOID_VALVE.getCode().equals(device.getDeviceType())) {
                     // 关闭电磁阀
                     GatewayControlDto dto = DeviceControlHelper.buildCloseSolenoidValveDto(device, tenantId);
                     result = deviceBusiness.control(dto);
@@ -71,7 +72,7 @@ public class CloseGroupNodeHandler implements NodeHandler {
                         mainVO.setGroupEndTime(LocalDateTime.now());
                         logService.saveMain(context.getTask().getId(),mainVO);
                     }
-                } else if ("BALL_VALVE".equals(device.getDeviceType())) {
+                } else if (DeviceType.BALL_VALVE.getCode().equals(device.getDeviceType())) {
                     // 球阀归零(角度设为0)
                     GatewayControlDto dto = DeviceControlHelper.buildBallValveAngleCloseDto(device, 0, tenantId);
                     result = deviceBusiness.control(dto);

+ 13 - 12
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/OpenGroupNodeHandler.java

@@ -8,6 +8,7 @@ import cn.sciento.farm.automationv2.app.service.TaskLogService;
 import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
 import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
 import cn.sciento.farm.automationv2.domain.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
@@ -53,7 +54,7 @@ public class OpenGroupNodeHandler implements NodeHandler {
         }
 
         // 获取球阀目标角度(如果有)
-        Integer targetAngle = node.getTargetAngle();
+//        Integer targetAngle = node.getTargetAngle();
 
         // 注册ACK期望
         ackManager.registerAck(executionId, nodeIndex, devices);
@@ -62,25 +63,25 @@ public class OpenGroupNodeHandler implements NodeHandler {
         for (DeviceInfo device : devices) {
             try {
                 Integer result = null;
-                if ("SOLENOID_VALVE".equals(device.getDeviceType())) {
+                if (DeviceType.SOLENOID_VALVE.getCode().equals(device.getDeviceType())) {
                     // 开启电磁阀
                     GatewayControlDto dto = DeviceControlHelper.buildOpenSolenoidValveDto(device, tenantId);
                     result = deviceBusiness.control(dto);
                     log.info("开启电磁阀,executionId={}, deviceId={}, result={}",
                             executionId, device.getDeviceId(), result);
-                } else if ("BALL_VALVE".equals(device.getDeviceType())) {
+                } else if (DeviceType.BALL_VALVE.getCode().equals(device.getDeviceType())) {
                     // 球阀到目标角度
-                    if (targetAngle != null) {
-                        GatewayControlDto dto = DeviceControlHelper.buildBallValveAngleDto(device, targetAngle, tenantId);
+//                    if (targetAngle != null) {
+                        GatewayControlDto dto = DeviceControlHelper.buildBallValveAngleDto(device, device.getTargetAngle(), tenantId);
                         result = deviceBusiness.control(dto);
                         log.info("球阀角度控制,executionId={}, deviceId={}, targetAngle={}, result={}",
-                                executionId, device.getDeviceId(), targetAngle, result);
-                    } else {
-                        log.warn("球阀未配置目标角度,deviceId={}", device.getDeviceId());
-                        ackManager.updateAckStatus(executionId, nodeIndex, device.getDeviceId(),
-                                AckStatus.FAIL, "球阀未配置目标角度");
-                        continue;
-                    }
+                                executionId, device.getDeviceId(), device.getTargetAngle(), result);
+//                    } else {
+//                        log.warn("球阀未配置目标角度,deviceId={}", device.getDeviceId());
+//                        ackManager.updateAckStatus(executionId, nodeIndex, device.getDeviceId(),
+//                                AckStatus.FAIL, "球阀未配置目标角度");
+//                        continue;
+//                    }
                 }
 
                 // 根据返回值更新ACK状态

+ 2 - 1
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/SetPumpPressureNodeHandler.java

@@ -8,6 +8,7 @@ import cn.sciento.farm.automationv2.app.service.TaskLogService;
 import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
 import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
 import cn.sciento.farm.automationv2.domain.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
@@ -59,7 +60,7 @@ public class SetPumpPressureNodeHandler implements NodeHandler {
                 ? node.getDevices().get(0)
                 : DeviceInfo.builder()
                     .deviceId(pumpId)
-                    .deviceType("PUMP")
+                    .deviceType(DeviceType.PUMP.getCode())
                     .deviceName("水泵")
                     .build();
 

+ 2 - 1
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StartFertilizerNodeHandler.java

@@ -6,6 +6,7 @@ import cn.sciento.farm.automationv2.app.handler.DeviceControlHelper;
 import cn.sciento.farm.automationv2.app.handler.NodeHandler;
 import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
 import cn.sciento.farm.automationv2.domain.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
@@ -63,7 +64,7 @@ public class StartFertilizerNodeHandler implements NodeHandler {
                 ? node.getDevices().get(0)
                 : DeviceInfo.builder()
                     .deviceId(fertilizerId)
-                    .deviceType("FERTILIZER")
+                    .deviceType(DeviceType.FERTILIZER.getCode())
                     .deviceName("施肥机")
                     .build();
 

+ 2 - 1
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StartPumpNodeHandler.java

@@ -8,6 +8,7 @@ import cn.sciento.farm.automationv2.app.service.TaskLogService;
 import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
 import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
 import cn.sciento.farm.automationv2.domain.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
@@ -55,7 +56,7 @@ public class StartPumpNodeHandler implements NodeHandler {
                 ? node.getDevices().get(0)
                 : DeviceInfo.builder()
                     .deviceId(pumpId)
-                    .deviceType("PUMP")
+                    .deviceType(DeviceType.PUMP.getCode())
                     .deviceName("水泵")
                     .build();
 

+ 2 - 1
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StopFertilizerNodeHandler.java

@@ -6,6 +6,7 @@ import cn.sciento.farm.automationv2.app.handler.DeviceControlHelper;
 import cn.sciento.farm.automationv2.app.handler.NodeHandler;
 import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
 import cn.sciento.farm.automationv2.domain.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
@@ -48,7 +49,7 @@ public class StopFertilizerNodeHandler implements NodeHandler {
                 ? node.getDevices().get(0)
                 : DeviceInfo.builder()
                     .deviceId(fertilizerId)
-                    .deviceType("FERTILIZER")
+                    .deviceType(DeviceType.FERTILIZER.getCode())
                     .deviceName("施肥机")
                     .build();
 

+ 2 - 1
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StopPumpNodeHandler.java

@@ -8,6 +8,7 @@ import cn.sciento.farm.automationv2.app.service.TaskLogService;
 import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
 import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
 import cn.sciento.farm.automationv2.domain.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
@@ -52,7 +53,7 @@ public class StopPumpNodeHandler implements NodeHandler {
                 ? node.getDevices().get(0)
                 : DeviceInfo.builder()
                     .deviceId(pumpId)
-                    .deviceType("PUMP")
+                    .deviceType(DeviceType.PUMP.getCode())
                     .deviceName("水泵")
                     .build();
 

+ 236 - 18
src/main/java/cn/sciento/farm/automationv2/app/service/SafeShutdownService.java

@@ -4,9 +4,13 @@ import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
 import cn.sciento.farm.automationv2.app.handler.DeviceControlHelper;
 import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
 import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
+import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskGroupNodeVO;
+import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskGroupVO;
 import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskLog;
 import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
 import cn.sciento.farm.automationv2.domain.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
+import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.repository.IrrigationTaskLogRepository;
 import cn.sciento.farm.automationv2.domain.repository.TaskExecutionRepository;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
@@ -71,6 +75,12 @@ public class SafeShutdownService {
         List<Long> failedDevices = new ArrayList<>();
         List<Long> successDevices = new ArrayList<>();
 
+        Optional<IrrigationTaskLog> logOpt = taskLogRepository.findByExecutionId(executionId);
+        IrrigationTaskLog taskLog = logOpt.get();
+
+        // 找出取消的灌区(未完成的灌区)并标记为失败
+        markCancelledZonesAsFailed(execution, taskLog);
+
         // 步骤1: 关闭施肥机
 //        if (openedDevices.fertilizer != null) {
 //            log.info("步骤1: 关闭施肥机,deviceId={}", openedDevices.fertilizer.getDeviceId());
@@ -85,7 +95,7 @@ public class SafeShutdownService {
         // 步骤2: 关闭水泵
         if (openedDevices.pump != null) {
             log.info("步骤2: 关闭水泵,deviceId={}", openedDevices.pump.getDeviceId());
-            boolean success = closeDevice(executionId, openedDevices.pump, execution.getTenantId());
+            boolean success = closeDevice(execution, openedDevices.pump, taskLog, null);
             if (success) {
                 successDevices.add(openedDevices.pump.getDeviceId());
                 IrrigationTaskMainVO mainVO = logService.getByTaskMainData(execution.getTaskId());
@@ -103,7 +113,9 @@ public class SafeShutdownService {
         for (DeviceInfo valve : openedDevices.valves) {
             log.info("步骤3: 关闭阀门,deviceId={}, deviceType={}",
                     valve.getDeviceId(), valve.getDeviceType());
-            boolean success = closeDevice(executionId, valve, execution.getTenantId());
+            // 找到该阀门所属的灌区
+            Integer zoneIndex = findZoneIndexForDevice(executionId, valve.getDeviceId());
+            boolean success = closeDevice(execution, valve, taskLog, zoneIndex);
             if (success) {
                 successDevices.add(valve.getDeviceId());
             } else {
@@ -111,6 +123,9 @@ public class SafeShutdownService {
             }
         }
 
+        // 保存更新后的任务日志
+        taskLogRepository.save(taskLog);
+
         // 构建结果
         ShutdownResult result;
         if (failedDevices.isEmpty()) {
@@ -129,14 +144,19 @@ public class SafeShutdownService {
     /**
      * 关闭单个设备
      *
-     * @param executionId 执行实例ID
+     * @param execution   执行实例
      * @param device      设备信息
-     * @param tenantId    租户ID
+     * @param taskLog     任务日志
+     * @param zoneIndex   灌区索引(可为null,表示不属于特定灌区)
      * @return true-成功,false-失败
      */
-    private boolean closeDevice(Long executionId, DeviceInfo device, Long tenantId) {
+    private boolean closeDevice(TaskExecution execution, DeviceInfo device,
+                                IrrigationTaskLog taskLog, Integer zoneIndex) {
+        Long executionId = execution.getId();
+        Long tenantId = execution.getTenantId();
         Long deviceId = device.getDeviceId();
         String deviceType = device.getDeviceType();
+        NodeType closeAction = getCloseActionType(deviceType);
 
         try {
             // 注册ACK期望
@@ -170,23 +190,33 @@ public class SafeShutdownService {
                 return false;
             }
 
+            boolean success = result != null && result == 1;
+            String status = success ? "SUCCESS" : "FAILED";
+
             // 根据返回值更新ACK状态
-            if (result != null && result == 1) {
+            if (success) {
                 ackManager.updateAckStatus(executionId, -1, deviceId,
                         AckStatus.SUCCESS, null);
                 log.info("设备关闭成功,deviceId={}", deviceId);
-                return true;
             } else {
                 ackManager.updateAckStatus(executionId, -1, deviceId,
                         AckStatus.FAIL, "设备控制返回失败,result=" + result);
                 log.error("设备关闭失败,deviceId={}, result={}", deviceId, result);
-                return false;
             }
 
+            // 记录关闭操作到灌区日志
+            recordDeviceCloseToLog(execution, taskLog, device, closeAction, status, zoneIndex);
+
+            return success;
+
         } catch (Exception e) {
             ackManager.updateAckStatus(executionId, -1, deviceId,
                     AckStatus.FAIL, "调用设备控制异常: " + e.getMessage());
             log.error("关闭设备异常,deviceId={}", deviceId, e);
+
+            // 记录失败到日志
+            recordDeviceCloseToLog(execution, taskLog, device, closeAction, "FAILED", zoneIndex);
+
             return false;
         }
     }
@@ -203,17 +233,22 @@ public class SafeShutdownService {
         }
 
         OpenedDevices result = new OpenedDevices();
-
-        for (ExecutionNode node : plan.getSuccessNodes()) {
-            switch (node.getNodeType()) {
-                case START_PUMP:
-                    // 找到水泵设备
-                    result.pump = node.getDevices().stream()
-                            .filter(d -> "PUMP".equals(d.getDeviceType()))
+        // 直接关闭水泵(含重复操作)
+        result.pump = plan.getWaterPumpNodes().getDevices().stream()
+                            .filter(d -> DeviceType.PUMP.getCode().equals(d.getDeviceType()))
                             .findFirst()
                             .orElse(null);
-                    break;
 
+        for (ExecutionNode node : plan.getSuccessNodes()) {
+            switch (node.getNodeType()) {
+//                case START_PUMP:
+//                    // 找到水泵设备
+//                    result.pump = node.getDevices().stream()
+//                            .filter(d -> "PUMP".equals(d.getDeviceType()))
+//                            .findFirst()
+//                            .orElse(null);
+//
+//                    break;
 //                case START_FERTILIZER:
 //                    // 找到施肥机设备
 //                    result.fertilizer = node.getDevices().stream()
@@ -225,8 +260,8 @@ public class SafeShutdownService {
                 case OPEN_GROUP:
                     // 收集电磁阀和球阀
                     List<DeviceInfo> valves = node.getDevices().stream()
-                            .filter(d -> "SOLENOID_VALVE".equals(d.getDeviceType()) ||
-                                    "BALL_VALVE".equals(d.getDeviceType()))
+                            .filter(d -> DeviceType.SOLENOID_VALVE.getCode().equals(d.getDeviceType()) ||
+                                    DeviceType.BALL_VALVE.getCode().equals(d.getDeviceType()))
                             .collect(Collectors.toList());
                     result.valves.addAll(valves);
                     // 如果存在同灌区ack部分成功的,需要额外查出
@@ -254,6 +289,189 @@ public class SafeShutdownService {
     }
 
     /**
+     * 标记取消的灌区为失败状态(仅标记当前正在执行的灌区)
+     *
+     * @param execution 执行实例
+     * @param taskLog   任务日志
+     */
+    private void markCancelledZonesAsFailed(TaskExecution execution, IrrigationTaskLog taskLog) {
+        if (taskLog == null || taskLog.getGroups() == null) {
+            return;
+        }
+
+        // 从执行计划获取当前节点的灌区索引
+        ExecutionPlan plan = executionPlanStore.get(execution.getId());
+        if (plan == null) {
+            log.warn("执行计划不存在,executionId={}", execution.getId());
+            return;
+        }
+
+        Integer currentIndex = execution.getCurrentIndex();
+        if (currentIndex == null) {
+            log.warn("当前节点索引为空,executionId={}", execution.getId());
+            return;
+        }
+
+        ExecutionNode currentNode = plan.getNode(currentIndex);
+        if (currentNode == null) {
+            log.warn("当前节点不存在,executionId={}, currentIndex={}", execution.getId(), currentIndex);
+            return;
+        }
+
+        Integer currentZoneIndex = currentNode.getZoneIndex();
+        if (currentZoneIndex == null || currentZoneIndex < 0 || currentZoneIndex >= taskLog.getGroups().size()) {
+            log.warn("当前灌区索引无效,executionId={}, zoneIndex={}", execution.getId(), currentZoneIndex);
+            return;
+        }
+
+        // 只标记当前正在执行的灌区为失败
+        IrrigationTaskGroupVO currentGroup = taskLog.getGroups().get(currentZoneIndex);
+        if (!"SUCCESS".equals(currentGroup.getStatus())) {
+            log.info("标记当前灌区为失败,灌区名称={}, 原状态={}",
+                    currentGroup.getGroupName(), currentGroup.getStatus());
+            LocalDateTime now = LocalDateTime.now();
+            currentGroup.setStatus("FAILED");
+            if (currentGroup.getGroupEndTime() == null) {
+                currentGroup.setGroupEndTime(now);
+            }
+            taskLog.getFailVO().setFailGroupIndex(currentZoneIndex);
+            taskLog.getFailVO().setCurrentIndex(currentIndex);
+        }
+    }
+
+    /**
+     * 查找设备所属的灌区索引
+     *
+     * @param executionId 执行实例ID
+     * @param deviceId    设备ID
+     * @return 灌区索引,如果未找到返回null
+     */
+    private Integer findZoneIndexForDevice(Long executionId, Long deviceId) {
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan == null) {
+            return null;
+        }
+
+        // 遍历所有节点,找到包含该设备的 OPEN_GROUP 节点
+        for (ExecutionNode node : plan.getNodes()) {
+            if (node.getNodeType() == NodeType.OPEN_GROUP && node.getDevices() != null) {
+                for (DeviceInfo device : node.getDevices()) {
+                    if (device.getDeviceId().equals(deviceId)) {
+                        return node.getZoneIndex();
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 记录设备关闭操作到灌区日志
+     *
+     * @param execution   执行实例
+     * @param taskLog     任务日志
+     * @param device      设备信息
+     * @param closeAction 关闭动作类型
+     * @param status      操作状态
+     * @param zoneIndex   灌区索引(可为null)
+     */
+    private void recordDeviceCloseToLog(TaskExecution execution, IrrigationTaskLog taskLog, DeviceInfo device,
+                                        NodeType closeAction, String status, Integer zoneIndex) {
+        if (taskLog == null || taskLog.getGroups() == null) {
+            return;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+
+        // 如果指定了灌区索引,只记录到该灌区
+        if (zoneIndex != null && zoneIndex >= 0 && zoneIndex < taskLog.getGroups().size()) {
+            IrrigationTaskGroupVO group = taskLog.getGroups().get(zoneIndex);
+            addCloseNodeToGroup(group, device, closeAction, status, now);
+            log.info("记录设备关闭到灌区日志,灌区={}, 设备ID={}, 状态={}",
+                    group.getGroupName(), device.getDeviceId(), status);
+        } else {
+            // 如果没有指定灌区(如水泵),只记录到当前正在执行的灌区
+            ExecutionPlan plan = executionPlanStore.get(execution.getId());
+            if (plan == null) {
+                log.warn("执行计划不存在,无法记录设备关闭日志,executionId={}", execution.getId());
+                return;
+            }
+
+            Integer currentIndex = execution.getCurrentIndex();
+            if (currentIndex == null) {
+                log.warn("当前节点索引为空,无法记录设备关闭日志,executionId={}", execution.getId());
+                return;
+            }
+
+            ExecutionNode currentNode = plan.getNode(currentIndex);
+            if (currentNode == null) {
+                log.warn("当前节点不存在,无法记录设备关闭日志,executionId={}, currentIndex={}",
+                        execution.getId(), currentIndex);
+                return;
+            }
+
+            Integer currentZoneIndex = currentNode.getZoneIndex();
+            if (currentZoneIndex != null && currentZoneIndex >= 0 && currentZoneIndex < taskLog.getGroups().size()) {
+                IrrigationTaskGroupVO currentGroup = taskLog.getGroups().get(currentZoneIndex);
+                addCloseNodeToGroup(currentGroup, device, closeAction, status, now);
+                log.info("记录设备关闭到当前灌区日志,灌区={}, 设备ID={}, 状态={}",
+                        currentGroup.getGroupName(), device.getDeviceId(), status);
+            }
+        }
+    }
+
+    /**
+     * 添加关闭节点到灌区
+     *
+     * @param group       灌区VO
+     * @param device      设备信息
+     * @param closeAction 关闭动作类型
+     * @param status      操作状态
+     * @param time        操作时间
+     */
+    private void addCloseNodeToGroup(IrrigationTaskGroupVO group, DeviceInfo device,
+                                     NodeType closeAction, String status, LocalDateTime time) {
+        if (group.getNodes() == null) {
+            group.setNodes(new ArrayList<>());
+        }
+
+        IrrigationTaskGroupNodeVO nodeVO = new IrrigationTaskGroupNodeVO();
+        nodeVO.setDeviceId(device.getDeviceId().toString());
+        nodeVO.setDeviceName(device.getDeviceName());
+        nodeVO.setDeviceType(device.getDeviceType());
+        nodeVO.setBrand(device.getBrand());
+        nodeVO.setVtype(device.getVtype());
+        nodeVO.setSw(device.getSw());
+        nodeVO.setAction(closeAction);
+        nodeVO.setStatus(status);
+        nodeVO.setDeviceBeginTime(time);
+        nodeVO.setDeviceEndTime(time);
+
+        group.getNodes().add(nodeVO);
+    }
+
+    /**
+     * 根据设备类型获取关闭动作类型
+     *
+     * @param deviceType 设备类型
+     * @return 关闭动作类型
+     */
+    private NodeType getCloseActionType(String deviceType) {
+        switch (deviceType) {
+            case "PUMP":
+                return NodeType.STOP_PUMP;
+            case "FERTILIZER":
+                return NodeType.STOP_FERTILIZER;
+            case "SOLENOID_VALVE":
+            case "BALL_VALVE":
+                return NodeType.CLOSE_GROUP;
+            default:
+                return NodeType.CLOSE_GROUP;
+        }
+    }
+
+    /**
      * 已开启设备容器
      */
     private static class OpenedDevices {

+ 5 - 1
src/main/java/cn/sciento/farm/automationv2/app/service/TaskExecutionEngine.java

@@ -367,6 +367,10 @@ public class TaskExecutionEngine {
             log.error("执行实例不存在,executionId={}", executionId);
             return;
         }
+        if (ExecutionStatus.FAILED.equals(execution.getStatus())){
+            log.error("执行实例已取消,executionId={}", executionId);
+            return;
+        }
 
         // 更新节点状态为FAILED
         ExecutionPlan plan = executionPlanStore.get(executionId);
@@ -391,7 +395,7 @@ public class TaskExecutionEngine {
 
         // 触发安全关闭
         log.info("触发安全关闭,executionId={}", executionId);
-        SafeShutdownService.ShutdownResult shutdownResult = safeShutdownService.shutdown(executionId);
+        SafeShutdownService.ShutdownResult shutdownResult = safeShutdownService.shutdown(execution);
 
         // 更新安全关闭结果
         execution.setSafeCloseStatus(shutdownResult.isSuccess() ? "SUCCESS" : "PARTIAL");

+ 3 - 4
src/main/java/cn/sciento/farm/automationv2/app/service/TaskLogService.java

@@ -1,10 +1,7 @@
 package cn.sciento.farm.automationv2.app.service;
 
 import cn.sciento.farm.automationv2.domain.entity.IrrigationTask;
-import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskGroupNodeVO;
-import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskGroupVO;
-import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskLog;
-import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
+import cn.sciento.farm.automationv2.domain.entity.mongo.*;
 import cn.sciento.farm.automationv2.domain.enums.NodeStatus;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.enums.TriggerType;
@@ -140,6 +137,8 @@ public class TaskLogService {
             taskLog.setTaskBeginTime(LocalDateTime.now());
             taskLog.setPumpId(task.getPumpDeviceId());
             taskLog.setPumpName(task.getPumpDeviceName());
+            taskLog.setPumpBrand(task.getPumpDeviceBrand());
+            taskLog.setFailVO(new IrrigationTaskFailVO());
             // 保存到MongoDB
             taskLogRepository.save(taskLog);
 

+ 7 - 1
src/main/java/cn/sciento/farm/automationv2/domain/entity/IrrigationTask.java

@@ -52,6 +52,11 @@ public class IrrigationTask extends AuditDomain {
     private String pumpDeviceName;
 
     /**
+     * 水泵设备品牌
+     */
+    private String pumpDeviceBrand;
+
+    /**
      * 是否启用首部恒压
      */
     private Integer isPump;
@@ -149,11 +154,12 @@ public class IrrigationTask extends AuditDomain {
     // ================== 业务方法 ==================
 
 
-    public IrrigationTask(Long id, String taskName, Long pumpDeviceId, String pumpDeviceName, Integer isPump, Integer pressureMode, Integer targetPressureKpa, Integer isDevicePump, Integer switchStableSeconds, Long fertilizerPumpId, Long stirMotorId, FertilizerControlMode fertilizerControlMode, Integer fertDelayMinutes, Integer preStirMinutes, Integer fertDurationMinutes, Integer fertTargetLiters, Integer status, Integer enabledFlag, Long tenantId, Long organizationId) {
+    public IrrigationTask(Long id, String taskName, Long pumpDeviceId, String pumpDeviceName, String pumpDeviceBrand, Integer isPump, Integer pressureMode, Integer targetPressureKpa, Integer isDevicePump, Integer switchStableSeconds, Long fertilizerPumpId, Long stirMotorId, FertilizerControlMode fertilizerControlMode, Integer fertDelayMinutes, Integer preStirMinutes, Integer fertDurationMinutes, Integer fertTargetLiters, Integer status, Integer enabledFlag, Long tenantId, Long organizationId) {
         this.id = id;
         this.taskName = taskName;
         this.pumpDeviceId = pumpDeviceId;
         this.pumpDeviceName = pumpDeviceName;
+        this.pumpDeviceBrand = pumpDeviceBrand;
         this.isPump = isPump;
         this.pressureMode = pressureMode;
         this.targetPressureKpa = targetPressureKpa;

+ 24 - 0
src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskFailVO.java

@@ -0,0 +1,24 @@
+package cn.sciento.farm.automationv2.domain.entity.mongo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 轮灌任务- 灌区VO
+ */
+@Data
+public class IrrigationTaskFailVO {
+
+
+    @ApiModelProperty("当前失败的灌区节点")
+    private Integer failGroupIndex;
+
+    /**
+     * 当前失败的节点
+     */
+    private Integer currentIndex;
+
+}

+ 7 - 0
src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskLog.java

@@ -46,6 +46,9 @@ public class IrrigationTaskLog {
     @ApiModelProperty("水泵名称")
     private String pumpName;
 
+    @ApiModelProperty("水泵品牌")
+    private String pumpBrand;
+
     @ApiModelProperty("基地Id")
     private Long organizationId;
 
@@ -60,4 +63,8 @@ public class IrrigationTaskLog {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime taskEndTime;
 
+    @ApiModelProperty("失败数据")
+    private IrrigationTaskFailVO failVO;
+
+
 }

+ 44 - 0
src/main/java/cn/sciento/farm/automationv2/domain/enums/DeviceType.java

@@ -0,0 +1,44 @@
+package cn.sciento.farm.automationv2.domain.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备类型枚举
+ */
+@Getter
+@AllArgsConstructor
+public enum DeviceType {
+
+    /**
+     * 电磁阀
+     */
+    SOLENOID_VALVE("SOLENOID_VALVE", "电磁阀"),
+
+    /**
+     * 球阀
+     */
+    BALL_VALVE("BALL_VALVE", "球阀"),
+
+    /**
+     * 水泵控制器
+     */
+    PUMP("PUMP", "水泵控制器"),
+
+    /**
+     * 施肥机
+     */
+    FERTILIZER("FERTILIZER", "施肥机");
+
+    private final String code;
+    private final String desc;
+
+    public static DeviceType fromCode(String code) {
+        for (DeviceType type : values()) {
+            if (type.getCode().equals(code)) {
+                return type;
+            }
+        }
+        throw new IllegalArgumentException("Unknown TriggerType code: " + code);
+    }
+}

+ 7 - 1
src/main/java/cn/sciento/farm/automationv2/domain/enums/ZonePhase.java

@@ -29,7 +29,13 @@ public enum ZonePhase {
      * 结束关闭中
      * 包含节点:STOP_PUMP、CLOSE_GROUP(最后灌区)
      */
-    STOPPING("结束关闭中");
+    STOPPING("结束关闭中"),
+
+    /**
+     * 失败
+     *
+     */
+    FAIL("失败");
 
     private final String description;
 

+ 20 - 24
src/main/java/cn/sciento/farm/automationv2/domain/service/ExecutionPlanGenerator.java

@@ -2,6 +2,7 @@ package cn.sciento.farm.automationv2.domain.service;
 
 import cn.sciento.core.redis.RedisHelper;
 import cn.sciento.farm.automationv2.domain.entity.IrrigationTask;
+import cn.sciento.farm.automationv2.domain.enums.DeviceType;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import cn.sciento.farm.automationv2.domain.enums.ZonePhase;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
@@ -134,33 +135,26 @@ public class ExecutionPlanGenerator {
         // 添加球阀设备
         devices.addAll(parseDeviceList(zone.getBallValves(), "BALL_VALVE"));
 
-        Map<String, Object> params = new HashMap<>();
-        // 如果球阀配置了目标角度和压力,从JSON中提取
-        if (zone.getBallValves() != null && !zone.getBallValves().isEmpty()) {
-            JSONArray ballValves = JSON.parseArray(zone.getBallValves());
-            if (!ballValves.isEmpty()) {
-                // 判断,如果任务设定了球阀恒压,则传压力,否则传角度
-                JSONObject firstBallValve = ballValves.getJSONObject(0);
-                if (zone.isDevicePump()) {
-                    // 球阀目标压力(可选,与水泵压力独立)
-                    if (firstBallValve.containsKey("targetPressureKpa")) {
-                        params.put("targetPressureKpa", firstBallValve.getInteger("targetPressureKpa"));
-                    }
-                }else {
-                    // 角度
-                    if (firstBallValve.containsKey("targetAngle")) {
-                        params.put("targetAngle", firstBallValve.getInteger("targetAngle"));
-                    }
-                }
-            }
-        }
+//        Map<String, Object> params = new HashMap<>();
+//        // 如果球阀配置了目标角度和压力,从JSON中提取
+//        if (zone.getBallValves() != null && !zone.getBallValves().isEmpty()) {
+//            JSONArray ballValves = JSON.parseArray(zone.getBallValves());
+//            if (!ballValves.isEmpty()) {
+//                // 判断,如果任务设定了球阀恒压,则传压力,否则传角度
+//                JSONObject firstBallValve = ballValves.getJSONObject(0);
+//                if (zone.isDevicePump()) {
+//                    // 球阀目标压力(可选,与水泵压力独立)
+//                    params.put("targetPressureKpa", zone.getZonePressureKpa().getInteger("targetPressureKpa"));
+//                }
+//            }
+//        }
 
         ExecutionNode node = ExecutionNode.builder()
                 .index(index)
                 .nodeType(NodeType.OPEN_GROUP)
                 .nodeName("开启" + zone.getGroupName())
                 .refId(zone.getGroupId())
-                .params(params)
+//                .params(params)
                 .status("PENDING")
                 .retryCount(0)
                 .maxRetry(3)
@@ -209,7 +203,7 @@ public class ExecutionPlanGenerator {
     private int addSetPumpPressureNode(List<ExecutionNode> nodes, int index, Long pumpId, Integer pressureKpa, int zoneIndex, ZonePhase zonePhase) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
-                .deviceType("PUMP")
+                .deviceType(DeviceType.PUMP.getCode())
                 .deviceName("水泵")
                 .build();
 
@@ -240,7 +234,7 @@ public class ExecutionPlanGenerator {
     private int addStartPumpNode(List<ExecutionNode> nodes, int index, Long pumpId, int zoneIndex, ZonePhase zonePhase) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
-                .deviceType("PUMP")
+                .deviceType(DeviceType.PUMP.getCode())
                 .deviceName("水泵")
                 .build();
 
@@ -268,7 +262,7 @@ public class ExecutionPlanGenerator {
     private int addStopPumpNode(List<ExecutionNode> nodes, int index, Long pumpId, int zoneIndex, ZonePhase zonePhase) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
-                .deviceType("PUMP")
+                .deviceType(DeviceType.PUMP.getCode())
                 .deviceName("水泵")
                 .build();
 
@@ -369,6 +363,8 @@ public class ExecutionPlanGenerator {
                         .vtype(json.getInteger("vtype"))
                         .deviceName(json.getString("deviceName"))
                         .switchId(json.getLong("switchId"))
+                        .targetAngle(json.getInteger("targetAngle"))
+                        .targetPressureKpa(json.getInteger("targetPressureKpa"))
                         .sw(json.getInteger("sw"))
                         .build();
                 devices.add(device);

+ 10 - 0
src/main/java/cn/sciento/farm/automationv2/domain/valueobject/DeviceInfo.java

@@ -86,6 +86,16 @@ public class DeviceInfo {
     private Object params;
 
     /**
+     * 设备角度
+     */
+    private Integer targetAngle;
+
+    /**
+     * 目标压力
+     */
+    private Integer targetPressureKpa;
+
+    /**
      * 球阀类型:直通/三通
      */
     private Integer vtype;

+ 15 - 0
src/main/java/cn/sciento/farm/automationv2/domain/valueobject/ExecutionPlan.java

@@ -1,5 +1,6 @@
 package cn.sciento.farm.automationv2.domain.valueobject;
 
+import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -73,6 +74,20 @@ public class ExecutionPlan {
     }
 
     /**
+     * 获取水泵节点
+     */
+    @JsonIgnore
+    public ExecutionNode getWaterPumpNodes() {
+        if (nodes == null) {
+            return null;
+        }
+        return nodes.stream()
+                .filter(d -> NodeType.START_PUMP.equals(d.getNodeType()))
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
      * 获取已成功的节点列表
      */
     @JsonIgnore

+ 5 - 0
src/main/java/cn/sciento/farm/automationv2/domain/valueobject/IrrigationTaskVO.java

@@ -40,6 +40,11 @@ public class IrrigationTaskVO {
     private String pumpDeviceName;
 
     /**
+     * 水泵设备品牌
+     */
+    private String pumpDeviceBrand;
+
+    /**
      * 是否启用首部恒压
      */
     private Integer isPump;

+ 1 - 1
src/main/java/cn/sciento/farm/automationv2/infra/feign/DeviceFeign.java

@@ -49,7 +49,7 @@ public interface DeviceFeign {
      * 控制变频器(水泵)
      */
     @RequestMapping(
-            value = "/v1/{tenantId}/inverter/control",
+            value = "/v1/{tenantId}/inverter/brand/control",
             method = RequestMethod.POST,
             consumes = "application/json")
     ResponseEntity<String> pumpControl(@PathVariable(value = "tenantId") Long tenantId,

+ 4 - 3
src/main/resources/mapper/IrrigationTaskMapper.xml

@@ -149,6 +149,7 @@
             task_name,
             pump_device_id,
             pump_device_name,
+            pump_device_brand,
             is_pump,
             pressure_mode,
             target_pressure_kpa,
@@ -161,13 +162,13 @@
             wfauto_v2_irrigation_task
         WHERE
             tenant_id = #{task.tenantId}
-            <if test="task.organizationId != null and task.organizationId != ''">
+            <if test="task.organizationId != null">
                 AND organization_id = #{task.organizationId}
             </if>
-            <if test="task.status != null and task.status != ''">
+            <if test="task.status != null">
                 AND `status` = #{task.status}
             </if>
-            <if test="task.enabledFlag != null and task.enabledFlag != ''">
+            <if test="task.enabledFlag != null">
                 AND enabled_flag = #{task.enabledFlag}
             </if>
     </select>