Răsfoiți Sursa

feature: 智能控制V2版本 第六版(日志结束)

Jayhaw 2 săptămâni în urmă
părinte
comite
f0380f4fed
45 a modificat fișierele cu 1116 adăugiri și 163 ștergeri
  1. 2 0
      src/main/java/cn/sciento/farm/autoconfigure/WebConditionAutoConfiguration.java
  2. 0 47
      src/main/java/cn/sciento/farm/automationv2/api/controller/ExecutionMonitorController.java
  3. 3 3
      src/main/java/cn/sciento/farm/automationv2/api/controller/IrrigationGroupController.java
  4. 52 3
      src/main/java/cn/sciento/farm/automationv2/api/controller/IrrigationTaskController.java
  5. 104 0
      src/main/java/cn/sciento/farm/automationv2/api/dto/DBSelectDTO.java
  6. 6 1
      src/main/java/cn/sciento/farm/automationv2/api/dto/GatewayControlDto.java
  7. 1 1
      src/main/java/cn/sciento/farm/automationv2/api/dto/IrrigationGroupDTO.java
  8. 25 11
      src/main/java/cn/sciento/farm/automationv2/app/handler/DeviceControlHelper.java
  9. 1 1
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/CloseGroupNodeHandler.java
  10. 1 1
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StartPumpNodeHandler.java
  11. 1 1
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StopPumpNodeHandler.java
  12. 17 17
      src/main/java/cn/sciento/farm/automationv2/app/service/SafeShutdownService.java
  13. 23 1
      src/main/java/cn/sciento/farm/automationv2/app/service/TaskExecutionEngine.java
  14. 445 0
      src/main/java/cn/sciento/farm/automationv2/app/service/TaskLogService.java
  15. 15 0
      src/main/java/cn/sciento/farm/automationv2/app/service/TaskTriggerService.java
  16. 4 4
      src/main/java/cn/sciento/farm/automationv2/app/service/irrigationGroup/impl/IrrigationGroupServiceImpl.java
  17. 33 0
      src/main/java/cn/sciento/farm/automationv2/app/service/irrigationTask/IrrigationTaskService.java
  18. 101 9
      src/main/java/cn/sciento/farm/automationv2/app/service/irrigationTask/impl/IrrigationTaskServiceImpl.java
  19. 7 0
      src/main/java/cn/sciento/farm/automationv2/domain/business/DeviceBusiness.java
  20. 8 1
      src/main/java/cn/sciento/farm/automationv2/domain/business/impl/DeviceBusinessImpl.java
  21. 5 1
      src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskGroupNodeVO.java
  22. 10 0
      src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskLog.java
  23. 10 1
      src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskMainVO.java
  24. 43 0
      src/main/java/cn/sciento/farm/automationv2/domain/enums/ZonePhase.java
  25. 1 1
      src/main/java/cn/sciento/farm/automationv2/domain/repository/IrrigationGroupRepository.java
  26. 23 0
      src/main/java/cn/sciento/farm/automationv2/domain/repository/IrrigationTaskLogRepository.java
  27. 5 1
      src/main/java/cn/sciento/farm/automationv2/domain/repository/IrrigationTaskRepository.java
  28. 9 0
      src/main/java/cn/sciento/farm/automationv2/domain/repository/TaskExecutionRepository.java
  29. 7 0
      src/main/java/cn/sciento/farm/automationv2/domain/repository/TaskGroupConfigRepository.java
  30. 36 19
      src/main/java/cn/sciento/farm/automationv2/domain/service/ExecutionPlanGenerator.java
  31. 5 0
      src/main/java/cn/sciento/farm/automationv2/domain/valueobject/DeviceInfo.java
  32. 7 3
      src/main/java/cn/sciento/farm/automationv2/domain/valueobject/ExecutionNode.java
  33. 2 2
      src/main/java/cn/sciento/farm/automationv2/domain/valueobject/IrrigationTaskVO.java
  34. 6 1
      src/main/java/cn/sciento/farm/automationv2/infra/constant/BaseConstant.java
  35. 3 0
      src/main/java/cn/sciento/farm/automationv2/infra/constant/RedisConstant.java
  36. 13 3
      src/main/java/cn/sciento/farm/automationv2/infra/feign/DeviceFeign.java
  37. 6 1
      src/main/java/cn/sciento/farm/automationv2/infra/feign/fallback/DeviceFallback.java
  38. 10 0
      src/main/java/cn/sciento/farm/automationv2/infra/repository/impl/IrrigationTaskRepositoryImpl.java
  39. 20 0
      src/main/java/cn/sciento/farm/automationv2/infra/repository/impl/TaskExecutionRepositoryImpl.java
  40. 8 0
      src/main/java/cn/sciento/farm/automationv2/infra/repository/impl/TaskGroupConfigRepositoryImpl.java
  41. 23 23
      src/main/resources/application.yml
  42. 2 4
      src/main/resources/bootstrap.yml
  43. 2 2
      src/main/resources/mapper/IrrigationTaskMapper.xml
  44. 5 0
      src/main/resources/messages/messages_wfautoV2_en_US.properties
  45. 6 0
      src/main/resources/messages/messages_wfautoV2_zh_CN.properties

+ 2 - 0
src/main/java/cn/sciento/farm/autoconfigure/WebConditionAutoConfiguration.java

@@ -4,6 +4,7 @@ import cn.sciento.resource.annoation.EnableSTongResourceServer;
 import org.springframework.cloud.openfeign.EnableFeignClients;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
 
 /**
  * @author wumu
@@ -14,5 +15,6 @@ import org.springframework.context.annotation.Configuration;
 @Configuration
 @EnableSTongResourceServer
 @EnableFeignClients("cn.sciento.farm.automationv2.infra.feign")
+@EnableMongoRepositories("cn.sciento.farm.automationv2.domain.repository")
 public class WebConditionAutoConfiguration {
 }

+ 0 - 47
src/main/java/cn/sciento/farm/automationv2/api/controller/ExecutionMonitorController.java

@@ -92,53 +92,6 @@ public class ExecutionMonitorController {
         return Result.success(execution.getStatus());
     }
 
-    /**
-     * 取消执行(触发安全关闭)
-     */
-    @PostMapping("/{id}/cancel")
-    public Result<Void> cancelExecution(@PathVariable Long id) {
-        log.info("取消执行,executionId={}", id);
-
-        try {
-            // 加载执行实例
-            TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(id);
-            if (execution == null) {
-                return Result.error("执行实例不存在");
-            }
-
-            // 检查是否可以取消
-            if (execution.isTerminal()) {
-                return Result.error("执行已终止,无法取消");
-            }
-
-            // 更新状态为CANCELLED
-            execution.markAsCancelled();
-            execution.setFinishedAt(LocalDateTime.now());
-            taskExecutionRepository.updateByPrimaryKeySelective(execution);
-
-            // 触发安全关闭
-            SafeShutdownService.ShutdownResult result = safeShutdownService.shutdown(id);
-
-            // 更新安全关闭结果
-            execution.setSafeCloseStatus(result.isSuccess() ? "SUCCESS" : "PARTIAL");
-            execution.setSafeCloseDetails(String.format(
-                    "{\"success\":[%s],\"failed\":[%s]}",
-                    String.join(",", result.getSuccessDevices().stream()
-                            .map(d -> "\"" + d + "\"").toArray(String[]::new)),
-                    String.join(",", result.getFailedDevices().stream()
-                            .map(d -> "\"" + d + "\"").toArray(String[]::new))
-            ));
-            taskExecutionRepository.updateByPrimaryKeySelective(execution);
-
-            log.info("执行已取消,executionId={}, shutdownResult={}", id, result.getSummary());
-
-            return Result.success();
-
-        } catch (Exception e) {
-            log.error("取消执行失败,executionId={}", id, e);
-            return Result.error("取消执行失败: " + e.getMessage());
-        }
-    }
 
 //    /**
 //     * 查询正在执行的任务

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

@@ -54,9 +54,9 @@ public class IrrigationGroupController {
 
     @ApiOperation("获取信息详情")
     @Permission(level = ResourceLevel.ORGANIZATION)
-    @GetMapping("/{planId}")
-    public ResponseEntity<IrrigationGroupDTO> get(@PathVariable Long tenantId, @PathVariable Long planId){
-        return Results.success(service.queryByDetail(tenantId,planId));
+    @GetMapping("/{groupId}")
+    public ResponseEntity<IrrigationGroupDTO> get(@PathVariable Long tenantId, @PathVariable Long groupId){
+        return Results.success(service.queryByDetail(tenantId,groupId));
     }
 
     @ApiOperation("通过id删除")

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

@@ -5,9 +5,15 @@ import cn.sciento.core.exception.CommonException;
 import cn.sciento.core.iam.ResourceLevel;
 import cn.sciento.core.util.Results;
 import cn.sciento.farm.automationv2.api.dto.CreateTaskRequest;
+import cn.sciento.farm.automationv2.api.dto.DBSelectDTO;
+import cn.sciento.farm.automationv2.api.dto.Result;
+import cn.sciento.farm.automationv2.app.service.SafeShutdownService;
 import cn.sciento.farm.automationv2.app.service.TaskTriggerService;
 import cn.sciento.farm.automationv2.app.service.irrigationTask.IrrigationTaskService;
 import cn.sciento.farm.automationv2.domain.entity.IrrigationTask;
+import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
+import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskLog;
+import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
 import cn.sciento.farm.automationv2.domain.valueobject.IrrigationTaskVO;
 import cn.sciento.swagger.annotation.Permission;
 import io.choerodon.mybatis.pagehelper.annotation.PageableDefault;
@@ -21,18 +27,18 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import javax.validation.Valid;
+import java.time.LocalDateTime;
 
 /**
  * 灌溉任务管理API
  */
-@Slf4j
 @Api("灌溉任务接口")
 @RestController
 @RequestMapping("/v2/{tenantId}/irrigation-task")
 public class IrrigationTaskController {
 
-    private final IrrigationTaskService taskService;
-    private final TaskTriggerService taskTriggerService;
+    private IrrigationTaskService taskService;
+    private TaskTriggerService taskTriggerService;
 
     public IrrigationTaskController(IrrigationTaskService taskService, TaskTriggerService taskTriggerService) {
         this.taskService = taskService;
@@ -197,6 +203,49 @@ public class IrrigationTaskController {
     public ResponseEntity<Long> triggerTask(@PathVariable Long id) {
         Long executionId = taskTriggerService.manualTrigger(id);
         return Results.success(executionId);
+    }
+
+
+    /**
+     * 查询任务-灌区头部数据
+     */
+    @GetMapping("/main-data/{taskId}")
+    @ApiOperation("查询任务-灌区头部数据")
+    @Permission(level = ResourceLevel.ORGANIZATION)
+    public ResponseEntity<IrrigationTaskMainVO> getTaskMainData(@PathVariable Long tenantId, @PathVariable Long taskId) {
+        return Results.success(taskService.getByTaskMainData(taskId));
+    }
+
+    /**
+     * 查询任务-日志详情
+     */
+    @GetMapping("/log/{executionId}")
+    @ApiOperation("查询任务-日志详情")
+    @Permission(level = ResourceLevel.ORGANIZATION)
+    public ResponseEntity<IrrigationTaskLog> getLog(@PathVariable Long tenantId, @PathVariable Long executionId) {
+        return Results.success(taskService.getTaskLog(executionId));
+    }
+
+
+    @ApiOperation("查询任务 - 分页获取控制实例")
+    @Permission(level = ResourceLevel.ORGANIZATION)
+    @GetMapping("/page/execution")
+    public ResponseEntity<Page<TaskExecution>> pageByTaskExecution(@PathVariable Long tenantId,
+                                                    DBSelectDTO dto,
+                                                    @PageableDefault(page = 1,size = 20,sort = {"creation_date"}, direction = Sort.Direction.DESC) PageRequest pageRequest){
+        if (dto.getTaskId() == null){
+            throw new CommonException("wfautoV2.parameter.incomplete");
+        }
+        dto.setTenantId(tenantId);
+        return Results.success(taskService.pageByTask(dto,pageRequest));
+    }
 
+    /**
+     * 取消执行(触发安全关闭)
+     */
+    @PostMapping("/{executionId}/cancel")
+    public ResponseEntity cancelExecution(@PathVariable Long tenantId,@PathVariable Long executionId) {
+        taskService.cancelExecution(executionId);
+        return Results.success();
     }
 }

+ 104 - 0
src/main/java/cn/sciento/farm/automationv2/api/dto/DBSelectDTO.java

@@ -0,0 +1,104 @@
+package cn.sciento.farm.automationv2.api.dto;
+
+import cn.sciento.farm.automationv2.domain.enums.ExecutionStatus;
+import cn.sciento.farm.automationv2.domain.enums.TriggerType;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.choerodon.mybatis.pagehelper.domain.PageRequest;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * <p>
+ *  MongoDB查询通用DTO
+ * </p>
+ *
+ * @author JayHaw
+ * @date 2026/3/12 15:24
+ */
+@Data
+public class DBSelectDTO {
+
+    /** 公共ID常量 */
+    public static final String FIELD_CONCENTRATOR_ID = "concentratorId";
+
+    /** 设备编号常量 */
+    public static final String FIELD_EUI = "eui";
+
+    /** 开始时间常量 */
+    public static final String FIELD_START_TIME = "startTime";
+
+    /** 结束时间常量 */
+    public static final String FIELD_END_TIME = "endTime";
+
+    /** 任务Id */
+    public static final String TASK_ID = "taskId";
+
+
+    /** 实际开始时间 */
+    public static final String START_AT = "startedAt";
+
+    public static final String TRIGGER_TYPE = "triggerType";
+
+    public static final String STATUS = "status";
+
+    public final static Integer FIELD_LOG_SHOW = 1;
+    public final static Integer FIELD_LOG_NOT_SHOW = 0;
+
+    /** 兼容之前的数据 */
+    public void setId(Long id) {
+        this.concentratorId = id;
+    }
+
+    /** 兼容之前的数据 */
+    @ApiModelProperty("公共Id")
+    private Long id;
+
+    @ApiModelProperty("公共ID")
+    private Long concentratorId;
+
+    @ApiModelProperty("租户ID")
+    private Long tenantId;
+
+    @ApiModelProperty("设备编号")
+    private String eui;
+
+    @ApiModelProperty("名称")
+    private String name;
+
+    /**
+     * 实际开始时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime startedAt;
+
+    /**
+     * 关联的轮灌任务ID
+     */
+    private Long taskId;
+
+    /**
+     * 触发类型:SCHEDULED / LINKAGE / MANUAL
+     */
+    private TriggerType triggerType;
+
+    /**
+     * 执行状态:PENDING / RUNNING / SUCCESS / FAILED / CANCELLED
+     */
+    private ExecutionStatus status;
+
+    @ApiModelProperty("开始时间")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime startTime;
+
+    @ApiModelProperty("结束时间")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime endTime;
+
+    @ApiModelProperty("分页参数")
+    private PageRequest pageRequest;
+
+}

+ 6 - 1
src/main/java/cn/sciento/farm/automationv2/api/dto/GatewayControlDto.java

@@ -34,7 +34,7 @@ public class GatewayControlDto {
     private Long switchId;
 
     @NotNull(message = "设备控制标志不能为空!")
-    private Long sw;
+    private Integer sw;
 
     @NotNull(message = "租户id不能为空!")
     private Long tenantId;
@@ -61,4 +61,9 @@ public class GatewayControlDto {
     @ApiModelProperty("给定压力(单位:KPa,分辨率:1)")
     private Float givenPressure;
 
+
+
+    @ApiModelProperty("开关(变频器(水泵用))")
+    private Integer en;
+
 }

+ 1 - 1
src/main/java/cn/sciento/farm/automationv2/api/dto/IrrigationGroupDTO.java

@@ -35,7 +35,7 @@ public class IrrigationGroupDTO {
     private Integer zonePressureKpa;
 
     /**
-     * 是否启用:1启用 0禁用
+         * 是否启用:1启用 0禁用
      */
     private Integer enabledFlag;
 

+ 25 - 11
src/main/java/cn/sciento/farm/automationv2/app/handler/DeviceControlHelper.java

@@ -19,7 +19,7 @@ public class DeviceControlHelper {
         dto.setEui(device.getEui());
         dto.setNode(device.getNode());
         dto.setCircuit(device.getCircuit());
-        dto.setSw(1L); // 开启
+        dto.setSw(1); // 开启
         dto.setTenantId(tenantId);
         dto.setRemark("本次控制由智能控制V2调用 - 开启电磁阀");
         dto.setStaffName("第三方远程调用");
@@ -34,7 +34,7 @@ public class DeviceControlHelper {
         dto.setEui(device.getEui());
         dto.setNode(device.getNode());
         dto.setCircuit(device.getCircuit());
-        dto.setSw(0L); // 关闭
+        dto.setSw(0); // 关闭
         dto.setTenantId(tenantId);
         dto.setRemark("本次控制由智能控制V2调用 - 关闭电磁阀");
         dto.setStaffName("第三方远程调用");
@@ -50,7 +50,7 @@ public class DeviceControlHelper {
 //        dto.setNode(device.getNode());
 //        dto.setCircuit(device.getCircuit());
         dto.setSwitchId(device.getSwitchId());
-        dto.setSw(1L); // 球阀控制开启
+        dto.setSw(1); // 球阀控制开启
         dto.setTenantId(tenantId);
         dto.setBallValuePercent(targetAngle); // 设置角度百分比
         dto.setRemark("本次控制由智能控制V2调用 - 球阀角度控制");
@@ -59,6 +59,22 @@ public class DeviceControlHelper {
     }
 
     /**
+     * 构造球阀角度控制的DTO(关闭)
+     */
+    public static GatewayControlDto buildBallValveAngleCloseDto(DeviceInfo device, Integer targetAngle, Long tenantId) {
+        GatewayControlDto dto = new GatewayControlDto();
+//        dto.setEui(device.getEui());
+//        dto.setNode(device.getNode());
+//        dto.setCircuit(device.getCircuit());
+        dto.setSwitchId(device.getSwitchId());
+        dto.setSw(0); // 球阀控制开启
+        dto.setTenantId(tenantId);
+//        dto.setBallValuePercent(targetAngle); // 设置角度百分比
+        dto.setRemark("本次控制由智能控制V2调用 - 球阀角度控制");
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+    /**
      * 构造启动水泵的控制DTO
      */
     public static GatewayControlDto buildStartPumpDto(DeviceInfo device, Long tenantId) {
@@ -66,8 +82,8 @@ public class DeviceControlHelper {
 //        dto.setEui(device.getEui());
 //        dto.setNode(device.getNode());
 //        dto.setCircuit(device.getCircuit());
-        dto.setSwitchId(device.getSwitchId());
-        dto.setSw(1L); // 启动
+        dto.setConcentratorId(device.getDeviceId());
+        dto.setEn(1); // 启动
         dto.setTenantId(tenantId);
         dto.setRemark("本次控制由智能控制V2调用 - 启动水泵");
         dto.setStaffName("第三方远程调用");
@@ -79,10 +95,8 @@ public class DeviceControlHelper {
      */
     public static GatewayControlDto buildStopPumpDto(DeviceInfo device, Long tenantId) {
         GatewayControlDto dto = new GatewayControlDto();
-        dto.setEui(device.getEui());
-        dto.setNode(device.getNode());
-        dto.setCircuit(device.getCircuit());
-        dto.setSw(0L); // 关闭
+        dto.setConcentratorId(device.getDeviceId());
+        dto.setEn(0); // 关闭
         dto.setTenantId(tenantId);
         dto.setRemark("本次控制由智能控制V2调用 - 关闭水泵");
         dto.setStaffName("第三方远程调用");
@@ -110,7 +124,7 @@ public class DeviceControlHelper {
         dto.setEui(device.getEui());
         dto.setNode(device.getNode());
         dto.setCircuit(device.getCircuit());
-        dto.setSw(1L); // 启动
+        dto.setSw(1); // 启动
         dto.setTenantId(tenantId);
         dto.setRemark("本次控制由智能控制V2调用 - 启动施肥机,程序号: " + programNo);
         dto.setStaffName("第三方远程调用");
@@ -125,7 +139,7 @@ public class DeviceControlHelper {
         dto.setEui(device.getEui());
         dto.setNode(device.getNode());
         dto.setCircuit(device.getCircuit());
-        dto.setSw(0L); // 关闭
+        dto.setSw(0); // 关闭
         dto.setTenantId(tenantId);
         dto.setRemark("本次控制由智能控制V2调用 - 关闭施肥机");
         dto.setStaffName("第三方远程调用");

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

@@ -64,7 +64,7 @@ public class CloseGroupNodeHandler implements NodeHandler {
                             executionId, device.getDeviceId(), result);
                 } else if ("BALL_VALVE".equals(device.getDeviceType())) {
                     // 球阀归零(角度设为0)
-                    GatewayControlDto dto = DeviceControlHelper.buildBallValveAngleDto(device, 0, tenantId);
+                    GatewayControlDto dto = DeviceControlHelper.buildBallValveAngleCloseDto(device, 0, tenantId);
                     result = deviceBusiness.control(dto);
                     log.info("球阀归零,executionId={}, deviceId={}, result={}",
                             executionId, device.getDeviceId(), result);

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

@@ -58,7 +58,7 @@ public class StartPumpNodeHandler implements NodeHandler {
         try {
             // 下发启动水泵指令
             GatewayControlDto dto = DeviceControlHelper.buildStartPumpDto(pumpDevice, tenantId);
-            Integer result = deviceBusiness.control(dto);
+            Integer result = deviceBusiness.pumpControl(dto);
 
             log.info("启动水泵,executionId={}, pumpId={}, result={}", executionId, pumpId, result);
 

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

@@ -58,7 +58,7 @@ public class StopPumpNodeHandler implements NodeHandler {
         try {
             // 下发关闭水泵指令
             GatewayControlDto dto = DeviceControlHelper.buildStopPumpDto(pumpDevice, tenantId);
-            Integer result = deviceBusiness.control(dto);
+            Integer result = deviceBusiness.pumpControl(dto);
 
             log.info("关闭水泵,executionId={}, pumpId={}, result={}", executionId, pumpId, result);
 

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

@@ -65,15 +65,15 @@ public class SafeShutdownService {
         List<Long> successDevices = new ArrayList<>();
 
         // 步骤1: 关闭施肥机
-        if (openedDevices.fertilizer != null) {
-            log.info("步骤1: 关闭施肥机,deviceId={}", openedDevices.fertilizer.getDeviceId());
-            boolean success = closeDevice(executionId, openedDevices.fertilizer, execution.getTenantId());
-            if (success) {
-                successDevices.add(openedDevices.fertilizer.getDeviceId());
-            } else {
-                failedDevices.add(openedDevices.fertilizer.getDeviceId());
-            }
-        }
+//        if (openedDevices.fertilizer != null) {
+//            log.info("步骤1: 关闭施肥机,deviceId={}", openedDevices.fertilizer.getDeviceId());
+//            boolean success = closeDevice(executionId, openedDevices.fertilizer, execution.getTenantId());
+//            if (success) {
+//                successDevices.add(openedDevices.fertilizer.getDeviceId());
+//            } else {
+//                failedDevices.add(openedDevices.fertilizer.getDeviceId());
+//            }
+//        }
 
         // 步骤2: 关闭水泵
         if (openedDevices.pump != null) {
@@ -147,7 +147,7 @@ public class SafeShutdownService {
                 log.info("关闭电磁阀,executionId={}, deviceId={}, result={}", executionId, deviceId, result);
             } else if ("BALL_VALVE".equals(deviceType)) {
                 // 球阀归零(角度=0)
-                dto = DeviceControlHelper.buildBallValveAngleDto(device, 0, tenantId);
+                dto = DeviceControlHelper.buildBallValveAngleCloseDto(device, 0, tenantId);
                 result = deviceBusiness.control(dto);
                 log.info("球阀归零,executionId={}, deviceId={}, result={}", executionId, deviceId, result);
             } else {
@@ -201,13 +201,13 @@ public class SafeShutdownService {
                             .orElse(null);
                     break;
 
-                case START_FERTILIZER:
-                    // 找到施肥机设备
-                    result.fertilizer = node.getDevices().stream()
-                            .filter(d -> "FERTILIZER".equals(d.getDeviceType()))
-                            .findFirst()
-                            .orElse(null);
-                    break;
+//                case START_FERTILIZER:
+//                    // 找到施肥机设备
+//                    result.fertilizer = node.getDevices().stream()
+//                            .filter(d -> "FERTILIZER".equals(d.getDeviceType()))
+//                            .findFirst()
+//                            .orElse(null);
+//                    break;
 
                 case OPEN_GROUP:
                     // 收集电磁阀和球阀

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

@@ -56,6 +56,7 @@ public class TaskExecutionEngine {
     private final AlarmRecordService alarmRecordService;
     private final AlarmNotificationService alarmNotificationService;
     private final ExecutionPlanStore executionPlanStore;
+    private final TaskLogService taskLogService;
 
     /**
      * 执行当前节点
@@ -119,6 +120,9 @@ public class TaskExecutionEngine {
             // 更新执行计划到Redis
             executionPlanStore.save(executionId, plan);
 
+            // 更新任务日志:节点开始执行
+            taskLogService.updateNodeStart(executionId, currentNode);
+
             // 构建执行上下文
             IrrigationTask task = irrigationTaskRepository.selectByPrimaryKey(execution.getTaskId());
             ExecutionContext context = ExecutionContext.builder()
@@ -156,6 +160,9 @@ public class TaskExecutionEngine {
                 // 更新执行计划到Redis
                 executionPlanStore.save(executionId, plan);
 
+                // 更新任务日志:WAIT节点执行成功
+                taskLogService.updateNodeSuccess(executionId, currentNode);
+
                 // 幂等性保护:标记节点已推进
 //                idempotencyManager.markProceed(executionId, nodeIndex);
 
@@ -242,6 +249,9 @@ public class TaskExecutionEngine {
             // 更新执行计划到Redis
             executionPlanStore.save(executionId, plan);
 
+            // 更新任务日志:节点执行成功
+            taskLogService.updateNodeSuccess(executionId, node);
+
             // 发送NEXT_NODE消息(立即,延迟=0)
             flowControlProducer.sendNextNode(executionId, nodeIndex, 0);
 
@@ -293,6 +303,10 @@ public class TaskExecutionEngine {
             log.error("执行实例不存在,executionId={}", executionId);
             return;
         }
+        if (!execution.canProceed()){
+            log.error("执行实例已停止,executionId={}", executionId);
+            return;
+        }
 
         // 检查是否有下一节点
         ExecutionPlan plan = executionPlanStore.get(executionId);
@@ -308,7 +322,10 @@ public class TaskExecutionEngine {
             execution.markAsSuccess();
             execution.setFinishedAt(LocalDateTime.now());
             taskExecutionRepository.updateByPrimaryKeySelective(execution);
-
+            // 重置任务状态
+            irrigationTaskRepository.taskStatusSuccessFail(execution.getTaskId());
+            // 任务日志结束
+            taskLogService.updateTaskSuccess(executionId);
             return;
         }
 
@@ -361,6 +378,9 @@ public class TaskExecutionEngine {
                 node.setFinishedAt(LocalDateTime.now());
                 // 更新执行计划到Redis
                 executionPlanStore.save(executionId, plan);
+
+                // 更新任务日志:节点执行失败
+                taskLogService.updateNodeFailure(executionId, node, failReason);
             }
         }
 
@@ -386,6 +406,8 @@ public class TaskExecutionEngine {
                         .collect(Collectors.joining(",")));
         execution.setSafeCloseDetails(closeDetails);
         taskExecutionRepository.updateByPrimaryKeySelective(execution);
+        // 重置
+        irrigationTaskRepository.taskStatusSuccessFail(execution.getTaskId());
 
         // 创建报警记录
         Long alarmId = alarmRecordService.createTaskFailureAlarm(executionId, failReason);

+ 445 - 0
src/main/java/cn/sciento/farm/automationv2/app/service/TaskLogService.java

@@ -0,0 +1,445 @@
+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.enums.NodeStatus;
+import cn.sciento.farm.automationv2.domain.enums.NodeType;
+import cn.sciento.farm.automationv2.domain.enums.TriggerType;
+import cn.sciento.farm.automationv2.domain.enums.ZonePhase;
+import cn.sciento.farm.automationv2.domain.repository.IrrigationTaskLogRepository;
+import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
+import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
+import cn.sciento.farm.automationv2.domain.valueobject.ExecutionPlan;
+import cn.sciento.farm.automationv2.domain.valueobject.ZoneConfigView;
+import cn.sciento.farm.automationv2.infra.constant.RedisConstant;
+import com.alibaba.fastjson.JSON;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * 任务日志服务
+ * 负责灌溉任务日志的创建和更新
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TaskLogService {
+
+    private final IrrigationTaskLogRepository taskLogRepository;
+
+    private final StringRedisTemplate stringRedisTemplate;
+
+    private static final long DEFAULT_TTL_HOUR = 3;
+
+    private void saveMain(Long taskId, IrrigationTaskMainVO mainVO) {
+        save(taskId, mainVO, DEFAULT_TTL_HOUR, TimeUnit.HOURS);
+    }
+
+    private String buildKey(Long taskId) {
+        return RedisConstant.TASK_EXECUTION_LOG + taskId;
+    }
+
+    private void save(Long taskId, IrrigationTaskMainVO mainVO, long ttl, TimeUnit unit) {
+        if (taskId == null || mainVO == null) {
+            return;
+        }
+        String key = buildKey(taskId);
+        String json = JSON.toJSONString(mainVO);
+        stringRedisTemplate.opsForValue().set(key, json, ttl, unit);
+        log.debug("Saved ExecutionPlan to Redis, key={}, ttl={} {}", key, ttl, unit);
+    }
+
+    /**
+     * 初始化任务日志
+     *
+     * @param task        任务信息
+     * @param executionId 执行实例ID
+     * @param triggerType 触发类型
+     * @param plan        执行计划
+     * @param zones       灌区配置列表
+     */
+    public void initializeLog(IrrigationTask task, Long executionId, TriggerType triggerType,
+                              ExecutionPlan plan, List<ZoneConfigView> zones) {
+        log.info("初始化任务日志,executionId={}, taskId={}", executionId, task.getId());
+
+        try {
+            // 构建主要数据
+            IrrigationTaskMainVO mainData = new IrrigationTaskMainVO();
+            mainData.setTriggerType(triggerType);
+            mainData.setExecutionId(executionId);
+            mainData.setTaskName(task.getTaskName());
+            mainData.setTotalPlanTime(calculateTotalPlanTime(plan));
+            mainData.setGroupName("预启动中");
+            mainData.setTotalGroup(zones.size());
+            mainData.setGroupNum(0);
+            // 存缓存
+            saveMain(task.getId(),mainData);
+
+            // 按 zoneIndex 分组节点
+            Map<Integer, List<ExecutionNode>> nodesByZone = plan.getNodes().stream()
+                    .filter(node -> node.getZoneIndex() != null && node.getZoneIndex() >= 0)
+                    .collect(Collectors.groupingBy(ExecutionNode::getZoneIndex));
+
+            // 构建灌区列表
+            List<IrrigationTaskGroupVO> groups = new ArrayList<>();
+            for (int i = 0; i < zones.size(); i++) {
+                ZoneConfigView zone = zones.get(i);
+                List<ExecutionNode> zoneNodes = nodesByZone.get(i);
+
+                IrrigationTaskGroupVO groupVO = new IrrigationTaskGroupVO();
+
+                // 直接从 ZoneConfigView 获取灌区名称
+                groupVO.setGroupName(zone.getGroupName());
+                groupVO.setPlanTime(zone.getIrrigationDuration());
+                groupVO.setStatus("PENDING");
+
+                // 构建节点列表
+                List<IrrigationTaskGroupNodeVO> nodeVOs = new ArrayList<>();
+                if (zoneNodes != null) {
+                    for (ExecutionNode node : zoneNodes) {
+                        // 只记录主要操作节点(开关阀、启停泵、设置压力)
+                        if (shouldRecordNode(node)) {
+                            List<IrrigationTaskGroupNodeVO> deviceNodes = convertNodeToDeviceNodes(node);
+                            nodeVOs.addAll(deviceNodes);
+                        }
+                    }
+                }
+                groupVO.setNodes(nodeVOs);
+
+                groups.add(groupVO);
+            }
+
+            // 创建日志对象
+            IrrigationTaskLog log = new IrrigationTaskLog();
+            log.setMainData(mainData);
+            log.setGroups(groups);
+            log.setTaskId(task.getId());
+            log.setTenantId(task.getTenantId());
+            log.setOrganizationId(task.getOrganizationId());
+            log.setExecutionId(executionId);
+            log.setTaskBeginTime(LocalDateTime.now());
+
+            // 保存到MongoDB
+            taskLogRepository.save(log);
+
+            this.log.info("任务日志初始化成功,executionId={}, 灌区数量={}", executionId, groups.size());
+        } catch (Exception e) {
+            this.log.error("初始化任务日志失败,executionId={}", executionId, e);
+        }
+    }
+
+    /**
+     * 更新节点开始执行
+     *
+     * @param executionId 执行实例ID
+     * @param node        节点信息
+     */
+    public void updateNodeStart(Long executionId, ExecutionNode node) {
+        log.debug("更新节点开始执行,executionId={}, nodeIndex={}, nodeType={}",
+                executionId, node.getIndex(), node.getNodeType());
+
+        try {
+            Optional<IrrigationTaskLog> logOpt = taskLogRepository.findByExecutionId(executionId);
+            if (!logOpt.isPresent()) {
+                log.warn("任务日志不存在,executionId={}", executionId);
+                return;
+            }
+
+            IrrigationTaskLog taskLog = logOpt.get();
+            Integer zoneIndex = node.getZoneIndex();
+            ZonePhase zonePhase = node.getZonePhase();
+
+            if (zoneIndex == null || zoneIndex < 0) {
+                return;
+            }
+
+            // 更新灌区状态
+            if (taskLog.getGroups() != null && zoneIndex < taskLog.getGroups().size()) {
+                IrrigationTaskGroupVO group = taskLog.getGroups().get(zoneIndex);
+
+                // 根据 zonePhase 更新灌区状态
+                if (ZonePhase.STARTING == zonePhase) {
+                    if (group.getGroupBeginTime() == null) {
+                        group.setGroupBeginTime(LocalDateTime.now());
+                    }
+                    group.setStatus(ZonePhase.STARTING.name());
+                } else if (ZonePhase.IRRIGATING == zonePhase) {
+                    group.setStatus(ZonePhase.IRRIGATING.name());
+                } else if (ZonePhase.SWITCHING == zonePhase) {
+                    group.setStatus(ZonePhase.SWITCHING.name());
+                } else if (ZonePhase.STOPPING == zonePhase) {
+                    group.setStatus(ZonePhase.STOPPING.name());
+                }
+
+                // 更新节点状态
+                updateGroupNodeStatus(group, node, "RUNNING", LocalDateTime.now(), null);
+            }
+
+            // 更新主数据当前灌区
+            if (taskLog.getMainData() != null && taskLog.getGroups() != null && zoneIndex < taskLog.getGroups().size()) {
+                IrrigationTaskGroupVO currentGroup = taskLog.getGroups().get(zoneIndex);
+                taskLog.getMainData().setGroupName(currentGroup.getGroupName());
+                taskLog.getMainData().setGroupBeginTime(currentGroup.getGroupBeginTime());
+                taskLog.getMainData().setGroupNum(zoneIndex);
+                saveMain(taskLog.getTaskId(),taskLog.getMainData());
+            }
+
+            taskLogRepository.save(taskLog);
+        } catch (Exception e) {
+            log.error("更新节点开始执行失败,executionId={}, nodeIndex={}",
+                    executionId, node.getIndex(), e);
+        }
+    }
+
+    /**
+     * 更新节点执行成功
+     *
+     * @param executionId 执行实例ID
+     * @param node        节点信息
+     */
+    public void updateNodeSuccess(Long executionId, ExecutionNode node) {
+        log.debug("更新节点执行成功,executionId={}, nodeIndex={}, nodeType={}",
+                executionId, node.getIndex(), node.getNodeType());
+
+        try {
+            Optional<IrrigationTaskLog> logOpt = taskLogRepository.findByExecutionId(executionId);
+            if (!logOpt.isPresent()) {
+                log.warn("任务日志不存在,executionId={}", executionId);
+                return;
+            }
+
+            IrrigationTaskLog taskLog = logOpt.get();
+            Integer zoneIndex = node.getZoneIndex();
+
+            if (zoneIndex == null || zoneIndex < 0) {
+                return;
+            }
+
+            // 更新灌区状态
+            if (taskLog.getGroups() != null && zoneIndex < taskLog.getGroups().size()) {
+                IrrigationTaskGroupVO group = taskLog.getGroups().get(zoneIndex);
+
+                // 更新节点状态
+                updateGroupNodeStatus(group, node, "SUCCESS", null, LocalDateTime.now());
+
+                // 检查是否是灌区的最后一个节点(CLOSE_GROUP 或最后的 STOPPING 阶段)
+                if (node.getNodeType() == NodeType.CLOSE_GROUP ||
+                        (ZonePhase.STOPPING == node.getZonePhase() && NodeStatus.SUCCESS.name().equals(node.getStatus()))) {
+                    group.setStatus("SUCCESS");
+                    group.setGroupEndTime(LocalDateTime.now());
+                }
+            }
+
+            taskLogRepository.save(taskLog);
+        } catch (Exception e) {
+            log.error("更新节点执行成功失败,executionId={}, nodeIndex={}",
+                    executionId, node.getIndex(), e);
+        }
+    }
+
+    /**
+     * 任务完成/失败
+     *
+     * @param executionId 执行实例ID
+     */
+    public void updateTaskSuccess(Long executionId) {
+        Optional<IrrigationTaskLog> logOpt = taskLogRepository.findByExecutionId(executionId);
+        if (!logOpt.isPresent()) {
+            log.warn("任务日志不存在,executionId={}", executionId);
+            return;
+        }
+        IrrigationTaskLog taskLog = logOpt.get();
+        taskLog.getMainData().setGroupNum(taskLog.getMainData().getTotalGroup());
+        saveMain(taskLog.getTaskId(),taskLog.getMainData());
+        taskLog.setTaskEndTime(LocalDateTime.now());
+        taskLogRepository.save(taskLog);
+    }
+
+    /**
+     * 更新节点执行失败
+     *
+     * @param executionId 执行实例ID
+     * @param node        节点信息
+     * @param failReason  失败原因
+     */
+    public void updateNodeFailure(Long executionId, ExecutionNode node, String failReason) {
+        log.debug("更新节点执行失败,executionId={}, nodeIndex={}, nodeType={}, failReason={}",
+                executionId, node.getIndex(), node.getNodeType(), failReason);
+
+        try {
+            Optional<IrrigationTaskLog> logOpt = taskLogRepository.findByExecutionId(executionId);
+            if (!logOpt.isPresent()) {
+                log.warn("任务日志不存在,executionId={}", executionId);
+                return;
+            }
+
+            IrrigationTaskLog taskLog = logOpt.get();
+            Integer zoneIndex = node.getZoneIndex();
+
+            if (zoneIndex == null || zoneIndex < 0) {
+                return;
+            }
+
+            // 更新灌区状态为失败
+            if (taskLog.getGroups() != null && zoneIndex < taskLog.getGroups().size()) {
+                IrrigationTaskGroupVO group = taskLog.getGroups().get(zoneIndex);
+                group.setStatus("FAILED");
+                group.setGroupEndTime(LocalDateTime.now());
+
+                // 更新节点状态
+                updateGroupNodeStatus(group, node, "FAILED", null, LocalDateTime.now());
+            }
+            // 失败视为 结束
+            taskLog.setTaskEndTime(LocalDateTime.now());
+            taskLogRepository.save(taskLog);
+        } catch (Exception e) {
+            log.error("更新节点执行失败失败,executionId={}, nodeIndex={}",
+                    executionId, node.getIndex(), e);
+        }
+    }
+
+    /**
+     * 更新灌区内节点的状态
+     */
+    private void updateGroupNodeStatus(IrrigationTaskGroupVO group, ExecutionNode node,
+                                       String status, LocalDateTime beginTime, LocalDateTime endTime) {
+        if (group.getNodes() == null || !shouldRecordNode(node)) {
+            return;
+        }
+
+        // 查找匹配的节点并更新
+        for (IrrigationTaskGroupNodeVO nodeVO : group.getNodes()) {
+            // 根据设备ID和节点类型匹配
+            if (matchesNode(nodeVO, node)) {
+                nodeVO.setStatus(status);
+                if (beginTime != null) {
+                    nodeVO.setDeviceBeginTime(beginTime);
+                }
+                if (endTime != null) {
+                    nodeVO.setDeviceEndTime(endTime);
+                }
+            }
+        }
+    }
+
+    /**
+     * 判断节点是否匹配
+     */
+    private boolean matchesNode(IrrigationTaskGroupNodeVO nodeVO, ExecutionNode node) {
+        if (node.getDevices() == null || node.getDevices().isEmpty()) {
+            return false;
+        }
+
+        // 通过节点类型和设备ID匹配
+        if (!node.getNodeType().equals(nodeVO.getAction())) {
+            return false;
+        }
+
+        // 检查设备ID是否匹配
+        for (DeviceInfo device : node.getDevices()) {
+            if (device.getDeviceId().toString().equals(nodeVO.getDeviceId())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 判断是否需要记录该节点
+     */
+    private boolean shouldRecordNode(ExecutionNode node) {
+        NodeType type = node.getNodeType();
+        return type == NodeType.OPEN_GROUP ||
+                type == NodeType.CLOSE_GROUP ||
+                type == NodeType.START_PUMP ||
+                type == NodeType.STOP_PUMP ||
+                type == NodeType.SET_PUMP_PRESSURE;
+    }
+
+    /**
+     * 将节点转换为设备节点VO列表
+     */
+    private List<IrrigationTaskGroupNodeVO> convertNodeToDeviceNodes(ExecutionNode node) {
+        List<IrrigationTaskGroupNodeVO> result = new ArrayList<>();
+
+        if (node.getDevices() != null) {
+            for (DeviceInfo device : node.getDevices()) {
+                IrrigationTaskGroupNodeVO nodeVO = new IrrigationTaskGroupNodeVO();
+                nodeVO.setDeviceId(device.getDeviceId().toString());
+                nodeVO.setDeviceName(device.getDeviceName());
+                nodeVO.setDeviceType(device.getDeviceType());
+                nodeVO.setSw(device.getSw());
+                nodeVO.setAction(node.getNodeType());
+                nodeVO.setStatus("PENDING");
+                result.add(nodeVO);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 根据节点类型获取设备类型
+     */
+    private String getDeviceType(NodeType nodeType) {
+        switch (nodeType) {
+            case OPEN_GROUP:
+            case CLOSE_GROUP:
+                return "BALL_VALVE";
+            case START_PUMP:
+            case STOP_PUMP:
+            case SET_PUMP_PRESSURE:
+                return "PUMP";
+            default:
+                return "UN_KNOW";
+        }
+    }
+
+    /**
+     * 根据节点类型获取操作动作
+     */
+    private String getActionFromNodeType(NodeType nodeType) {
+        switch (nodeType) {
+            case OPEN_GROUP:
+                return "开启球阀";
+            case CLOSE_GROUP:
+                return "关闭球阀";
+            case START_PUMP:
+                return "启动水泵";
+            case STOP_PUMP:
+                return "停止水泵";
+            case SET_PUMP_PRESSURE:
+                return "设置水泵压力";
+            default:
+                return "未知操作";
+        }
+    }
+
+    /**
+     * 计算总计划灌溉时长(秒)
+     */
+    private Long calculateTotalPlanTime(ExecutionPlan plan) {
+        if (plan.getNodes() == null) {
+            return 0L;
+        }
+
+        return plan.getNodes().stream()
+                .filter(node -> node.getNodeType() == NodeType.WAIT)
+                .mapToLong(node -> {
+                    Integer waitSeconds = node.getWaitSeconds();
+                    return waitSeconds != null ? waitSeconds : 0;
+                })
+                .sum();
+    }
+}

+ 15 - 0
src/main/java/cn/sciento/farm/automationv2/app/service/TaskTriggerService.java

@@ -1,5 +1,7 @@
 package cn.sciento.farm.automationv2.app.service;
 
+import cn.sciento.core.exception.CommonException;
+import cn.sciento.farm.automationv2.domain.enums.TaskStatus;
 import cn.sciento.farm.automationv2.domain.repository.IrrigationTaskRepository;
 import cn.sciento.farm.automationv2.domain.repository.TaskGroupConfigRepository;
 import cn.sciento.farm.automationv2.infra.redis.ExecutionPlanStore;
@@ -52,6 +54,7 @@ public class TaskTriggerService {
     private final ExecutionPlanGenerator executionPlanGenerator;
     private final FlowControlProducer flowControlProducer;
     private final ExecutionPlanStore executionPlanStore;
+    private final TaskLogService taskLogService;
 
     /**
      * 触发任务执行
@@ -76,6 +79,11 @@ public class TaskTriggerService {
             log.warn("任务未启用,taskId={}", taskId);
             throw new IllegalStateException("任务未启用,无法触发");
         }
+        // 检查任务是否已经启动
+        if (!TaskStatus.FREE.getCode().equals(task.getStatus())) {
+            log.warn("任务已经启动,taskId={}", taskId);
+            throw new CommonException("wfautoV2.task.hadAction");
+        }
 
         // 检查是否达到执行上限
 //        if (task.reachedExecutionLimit()) {
@@ -132,14 +140,21 @@ public class TaskTriggerService {
         // 计算预期完成时间
         int expectedMinutes = plan.calculateExpectedDurationMinutes();
         execution.setExpectedFinishAt(LocalDateTime.now().plusMinutes(expectedMinutes));
+        execution.markAsRunning();
         // 持久化执行实例
         taskExecutionRepository.insertSelective(execution);
+        // 灌溉中
+        task.setStatus(TaskStatus.WORKING.getCode());
+        irrigationTaskRepository.updateByPrimaryKeySelective(task);
         // 存储执行计划到Redis
         executionPlanStore.save(execution.getId(), plan);
 
         log.info("执行实例创建成功,executionId={}, taskId={}, triggerType={}, expectedFinishAt={}",
                 execution.getId(), taskId, triggerType, execution.getExpectedFinishAt());
 
+        // 初始化任务日志
+        taskLogService.initializeLog(task, execution.getId(), triggerType, plan, zones);
+
         // 更新任务执行次数
 //        task.incrementExecutedCount();
 //        irrigationTaskMapper.updateById(task);

+ 4 - 4
src/main/java/cn/sciento/farm/automationv2/app/service/irrigationGroup/impl/IrrigationGroupServiceImpl.java

@@ -103,7 +103,7 @@ public class IrrigationGroupServiceImpl implements IrrigationGroupService {
         }
         // 判断是否存在任务
         checkTask(irrigationGroup.getId());
-        resultIrrigationGroup.setEnabledFlag(BaseConstant.ENABLE);
+        resultIrrigationGroup.setEnabledFlag(BaseConstant.DISABLE);
         return irrigationGroupRepository.updateByPrimaryKeySelective(resultIrrigationGroup);
     }
 
@@ -121,9 +121,9 @@ public class IrrigationGroupServiceImpl implements IrrigationGroupService {
             return irrigationGroupRepository.selectByCondition(Condition.builder(IrrigationGroup.class)
                     .where(Sqls.custom()
                             .andEqualTo(BaseConstant.TENANT_ID,irrigationGroup.getTenantId())
-                            .andEqualTo(BaseConstant.ORGANIZATION_ID,irrigationGroup.getOrganizationId())
-                            .andLike(BaseConstant.IRRIGATION_GROUP_NAME,irrigationGroup.getGroupName(),true)
-                            .andEqualTo(BaseConstant.ENABLED_FLAG,irrigationGroup.getEnabledFlag(),true)
+                            .andEqualTo(BaseConstant.ORGANIZATION_ID,irrigationGroup.getOrganizationId(),Boolean.TRUE)
+                            .andLike(BaseConstant.IRRIGATION_GROUP_NAME,irrigationGroup.getGroupName(),Boolean.TRUE)
+                            .andEqualTo(BaseConstant.ENABLED_FLAG,irrigationGroup.getEnabledFlag(),Boolean.TRUE)
                     )
                     .build());
         });

+ 33 - 0
src/main/java/cn/sciento/farm/automationv2/app/service/irrigationTask/IrrigationTaskService.java

@@ -3,9 +3,13 @@ package cn.sciento.farm.automationv2.app.service.irrigationTask;
 
 import cn.sciento.core.domain.Page;
 import cn.sciento.farm.automationv2.api.dto.CreateTaskRequest;
+import cn.sciento.farm.automationv2.api.dto.DBSelectDTO;
 import cn.sciento.farm.automationv2.api.dto.IrrigationGroupDTO;
 import cn.sciento.farm.automationv2.domain.entity.IrrigationGroup;
 import cn.sciento.farm.automationv2.domain.entity.IrrigationTask;
+import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
+import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskLog;
+import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskMainVO;
 import cn.sciento.farm.automationv2.domain.valueobject.IrrigationTaskVO;
 import io.choerodon.mybatis.pagehelper.domain.PageRequest;
 
@@ -57,4 +61,33 @@ public interface IrrigationTaskService {
      * @return
      */
     Integer delete(IrrigationTask task);
+
+    /**
+     * 获取头部数据
+     * @param taskId
+     * @return irrigationGroup
+     */
+    IrrigationTaskMainVO getByTaskMainData(Long taskId);
+
+
+    /**
+     * 日志
+     * @param executionId
+     * @return irrigationGroup
+     */
+    IrrigationTaskLog getTaskLog(Long executionId);
+
+    /**
+     *获取page
+     * @param dto
+     * @return
+     */
+    Page<TaskExecution> pageByTask(DBSelectDTO dto, PageRequest pageRequest);
+
+
+    /**
+     * 停止任务(触发安全关闭)
+     * @param executionId
+     */
+    void cancelExecution(Long executionId);
 }

+ 101 - 9
src/main/java/cn/sciento/farm/automationv2/app/service/irrigationTask/impl/IrrigationTaskServiceImpl.java

@@ -4,35 +4,41 @@ package cn.sciento.farm.automationv2.app.service.irrigationTask.impl;
 import cn.sciento.core.domain.Page;
 import cn.sciento.core.exception.CommonException;
 import cn.sciento.core.util.ValidUtils;
-import cn.sciento.farm.automationv2.api.dto.BallValuesDTO;
-import cn.sciento.farm.automationv2.api.dto.CreateTaskRequest;
-import cn.sciento.farm.automationv2.api.dto.IrrigationGroupDTO;
+import cn.sciento.farm.automationv2.api.dto.*;
+import cn.sciento.farm.automationv2.app.service.SafeShutdownService;
 import cn.sciento.farm.automationv2.app.service.irrigationGroup.IrrigationGroupService;
 import cn.sciento.farm.automationv2.app.service.irrigationTask.IrrigationTaskService;
 import cn.sciento.farm.automationv2.domain.entity.IrrigationGroup;
 import cn.sciento.farm.automationv2.domain.entity.IrrigationTask;
+import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
 import cn.sciento.farm.automationv2.domain.entity.TaskGroupConfig;
+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.FertilizerControlMode;
 import cn.sciento.farm.automationv2.domain.enums.PressureMode;
 import cn.sciento.farm.automationv2.domain.enums.TaskStatus;
 import cn.sciento.farm.automationv2.domain.enums.TriggerType;
-import cn.sciento.farm.automationv2.domain.repository.IrrigationGroupRepository;
-import cn.sciento.farm.automationv2.domain.repository.IrrigationTaskRepository;
-import cn.sciento.farm.automationv2.domain.repository.TaskGroupConfigRepository;
+import cn.sciento.farm.automationv2.domain.repository.*;
+import cn.sciento.farm.automationv2.domain.valueobject.ExecutionPlan;
 import cn.sciento.farm.automationv2.domain.valueobject.IrrigationTaskVO;
 import cn.sciento.farm.automationv2.infra.constant.BaseConstant;
+import cn.sciento.farm.automationv2.infra.constant.RedisConstant;
 import cn.sciento.farm.automationv2.infra.utils.BeanCopyUtils;
 import cn.sciento.mybatis.domian.Condition;
 import cn.sciento.mybatis.util.Sqls;
 import com.alibaba.fastjson.JSON;
 import io.choerodon.mybatis.pagehelper.PageHelper;
 import io.choerodon.mybatis.pagehelper.domain.PageRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.validation.Validator;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 
 /**
@@ -46,14 +52,29 @@ public class IrrigationTaskServiceImpl implements IrrigationTaskService {
 
     private IrrigationTaskRepository irrigationTaskRepository;
 
+    private TaskExecutionRepository executionRepository;
+
     private TaskGroupConfigRepository taskGroupConfigRepository;
 
+    private StringRedisTemplate stringRedisTemplate;
+
+    private IrrigationTaskLogRepository logRepository;
+
     private Validator validator;
 
-    public IrrigationTaskServiceImpl(IrrigationTaskRepository irrigationTaskRepository, TaskGroupConfigRepository taskGroupConfigRepository, Validator validator) {
+    private TaskExecutionRepository taskExecutionRepository;
+
+    private SafeShutdownService safeShutdownService;
+
+    public IrrigationTaskServiceImpl(IrrigationTaskRepository irrigationTaskRepository, TaskExecutionRepository executionRepository, TaskGroupConfigRepository taskGroupConfigRepository, StringRedisTemplate stringRedisTemplate, IrrigationTaskLogRepository logRepository, Validator validator, TaskExecutionRepository taskExecutionRepository, SafeShutdownService safeShutdownService) {
         this.irrigationTaskRepository = irrigationTaskRepository;
+        this.executionRepository = executionRepository;
         this.taskGroupConfigRepository = taskGroupConfigRepository;
+        this.stringRedisTemplate = stringRedisTemplate;
+        this.logRepository = logRepository;
         this.validator = validator;
+        this.taskExecutionRepository = taskExecutionRepository;
+        this.safeShutdownService = safeShutdownService;
     }
 
     private void validDTO(IrrigationGroupDTO dto) {
@@ -115,6 +136,12 @@ public class IrrigationTaskServiceImpl implements IrrigationTaskService {
         if (result == null) {
             throw new CommonException("wfautoV2.parameter.error");
         }
+        if (BaseConstant.DISABLE.equals(task.getEnabledFlag())){
+            // 校验任务是否正在启动
+            if (TaskStatus.FREE.getCode().equals(task.getStatus())){
+                throw new CommonException("wfautoV2.task.hadRunning");
+            }
+        }
         result.setEnabledFlag(task.getEnabledFlag());
         return irrigationTaskRepository.updateByPrimaryKeySelective(result);
     }
@@ -128,7 +155,72 @@ public class IrrigationTaskServiceImpl implements IrrigationTaskService {
         if (BaseConstant.ENABLE.equals(result.getEnabledFlag())){
             throw new CommonException("wfautoV2.task.enabled");
         }
-        result.setEnabledFlag(BaseConstant.DISABLE);
-        return irrigationTaskRepository.updateByPrimaryKeySelective(result);
+        result.setEnabledFlag(BaseConstant.DELETE);
+        Integer delResult = irrigationTaskRepository.updateByPrimaryKeySelective(result);
+        if (delResult > 0){
+            taskGroupConfigRepository.deleteByTask(task.getId());
+        }
+        return delResult;
+    }
+
+    @Override
+    public IrrigationTaskMainVO getByTaskMainData(Long taskId) {
+        String key = RedisConstant.TASK_EXECUTION_LOG + taskId;
+        String json = stringRedisTemplate.opsForValue().get(key);
+        if (json == null || json.isEmpty()) {
+            return null;
+        }
+        return JSON.parseObject(json, IrrigationTaskMainVO.class);
+    }
+
+    @Override
+    public IrrigationTaskLog getTaskLog(Long executionId) {
+        Optional<IrrigationTaskLog> logOpt = logRepository.findByExecutionId(executionId);
+        return logOpt.orElse(null);
+    }
+
+    @Override
+    public Page<TaskExecution> pageByTask(DBSelectDTO dto, PageRequest pageRequest) {
+        return PageHelper.doPageAndSort(pageRequest,() -> {
+            return executionRepository.getByTask(dto);
+        });
+    }
+
+    @Override
+    public void cancelExecution(Long executionId) {
+        try {
+            // 加载执行实例
+            TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
+            if (execution == null) {
+                throw new CommonException("wfautoV2.task.execution.notFound");
+            }
+
+            // 检查是否可以取消
+            if (execution.isTerminal()) {
+                throw new CommonException("wfautoV2.task.execution.hadCancel");
+            }
+
+            // 更新状态为CANCELLED
+            execution.markAsCancelled();
+            execution.setFinishedAt(LocalDateTime.now());
+            taskExecutionRepository.updateByPrimaryKeySelective(execution);
+
+            // 触发安全关闭
+            SafeShutdownService.ShutdownResult result = safeShutdownService.shutdown(executionId);
+
+            // 更新安全关闭结果
+            execution.setSafeCloseStatus(result.isSuccess() ? "SUCCESS" : "PARTIAL");
+            execution.setSafeCloseDetails(String.format(
+                    "{\"success\":[%s],\"failed\":[%s]}",
+                    String.join(",", result.getSuccessDevices().stream()
+                            .map(d -> "\"" + d + "\"").toArray(String[]::new)),
+                    String.join(",", result.getFailedDevices().stream()
+                            .map(d -> "\"" + d + "\"").toArray(String[]::new))
+            ));
+            taskExecutionRepository.updateByPrimaryKeySelective(execution);
+
+        } catch (Exception e) {
+            throw new CommonException("wfautoV2.task.execution.cancelError",e.getMessage());
+        }
     }
 }

+ 7 - 0
src/main/java/cn/sciento/farm/automationv2/domain/business/DeviceBusiness.java

@@ -29,4 +29,11 @@ public interface DeviceBusiness {
      * @return 是否成功
      */
     Integer pumpPressure(GatewayControlDto dto);
+
+    /**
+     * 控制水泵
+     * @param dto 网关控制dto
+     * @return 是否成功
+     */
+    Integer pumpControl(GatewayControlDto dto);
 }

+ 8 - 1
src/main/java/cn/sciento/farm/automationv2/domain/business/impl/DeviceBusinessImpl.java

@@ -32,7 +32,14 @@ public class DeviceBusinessImpl implements DeviceBusiness {
 
     @Override
     public Integer pumpPressure(GatewayControlDto dto) {
-//        ResponseEntity<String> result = feign.PumpPressure(dto.getTenantId(), dto);
+//        ResponseEntity<String> result = feign.pumpPressure(dto.getTenantId(), dto);
+//        return ResponseUtils.getResponse(result, Integer.class);
+        return 1;
+    }
+
+    @Override
+    public Integer pumpControl(GatewayControlDto dto) {
+//        ResponseEntity<String> result = feign.pumpControl(dto.getTenantId(), dto);
 //        return ResponseUtils.getResponse(result, Integer.class);
         return 1;
     }

+ 5 - 1
src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskGroupNodeVO.java

@@ -1,5 +1,6 @@
 package cn.sciento.farm.automationv2.domain.entity.mongo;
 
+import cn.sciento.farm.automationv2.domain.enums.NodeType;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
@@ -20,8 +21,11 @@ public class IrrigationTaskGroupNodeVO {
     @ApiModelProperty("设备类型:球阀、水泵")
     private String deviceType;
 
+    @ApiModelProperty("控制方向")
+    private Integer sw;
+
     @ApiModelProperty("设备动作:开关水泵/开关球阀/设置水泵压力")
-    private String action;
+    private NodeType action;
 
     @ApiModelProperty("操作状态/结果")
     private String status;

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

@@ -1,5 +1,6 @@
 package cn.sciento.farm.automationv2.domain.entity.mongo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
@@ -8,6 +9,7 @@ import lombok.NoArgsConstructor;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.mongodb.core.mapping.Document;
 
+import java.time.LocalDateTime;
 import java.util.Date;
 import java.util.List;
 
@@ -47,4 +49,12 @@ public class IrrigationTaskLog {
     @ApiModelProperty("任务执行实例Id")
     private Long executionId;
 
+    @ApiModelProperty("任务开始时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime taskBeginTime;
+
+    @ApiModelProperty("任务结束时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime taskEndTime;
+
 }

+ 10 - 1
src/main/java/cn/sciento/farm/automationv2/domain/entity/mongo/IrrigationTaskMainVO.java

@@ -2,6 +2,7 @@ package cn.sciento.farm.automationv2.domain.entity.mongo;
 
 import cn.sciento.farm.automationv2.domain.enums.TaskStatus;
 import cn.sciento.farm.automationv2.domain.enums.TriggerType;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
@@ -16,11 +17,12 @@ import java.time.LocalDateTime;
 @Data
 public class IrrigationTaskMainVO {
 
+    @ApiModelProperty("任务执行实例Id")
+    private Long executionId;
 
     @ApiModelProperty("触发类型:SCHEDULED / LINKAGE / MANUAL")
     private TriggerType triggerType;
 
-
     @ApiModelProperty("任务名称")
     private String taskName;
 
@@ -28,6 +30,7 @@ public class IrrigationTaskMainVO {
     private String groupName;
 
     @ApiModelProperty("当前灌溉组开始时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime groupBeginTime;
 
     @ApiModelProperty("当前压力")
@@ -47,4 +50,10 @@ public class IrrigationTaskMainVO {
 
     @ApiModelProperty("总计划灌溉时长")
     private Long totalPlanTime;
+
+    @ApiModelProperty("总轮灌组数")
+    private Integer totalGroup;
+
+    @ApiModelProperty("当前轮灌组")
+    private Integer groupNum;
 }

+ 43 - 0
src/main/java/cn/sciento/farm/automationv2/domain/enums/ZonePhase.java

@@ -0,0 +1,43 @@
+package cn.sciento.farm.automationv2.domain.enums;
+
+/**
+ * 灌区阶段枚举
+ * 用于标识节点在灌区生命周期中的阶段
+ * 对应文档 5.6.3 灌区阶段枚举(ZonePhase)
+ */
+public enum ZonePhase {
+
+    /**
+     * 启动中(开阀、启泵)
+     * 包含节点:OPEN_GROUP、SET_PUMP_PRESSURE、START_PUMP
+     */
+    STARTING("启动中"),
+
+    /**
+     * 灌溉中(含施肥子状态)
+     * 包含节点:WAIT(source=IRRIGATE)
+     */
+    IRRIGATING("灌溉中"),
+
+    /**
+     * 切换中(稳压过渡,属于本灌区收尾)
+     * 包含节点:ZONE_SWITCH_WAIT、CLOSE_GROUP(非最后灌区)
+     */
+    SWITCHING("切换中"),
+
+    /**
+     * 结束关闭中
+     * 包含节点:STOP_PUMP、CLOSE_GROUP(最后灌区)
+     */
+    STOPPING("结束关闭中");
+
+    private final String description;
+
+    ZonePhase(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

+ 1 - 1
src/main/java/cn/sciento/farm/automationv2/domain/repository/IrrigationGroupRepository.java

@@ -16,7 +16,7 @@ public interface IrrigationGroupRepository extends BaseRepository<IrrigationGrou
     /**
      * 查询
      * @param tenantId
-     * @param Id
+     * @param id
      * @return
      */
     IrrigationGroup queryByTenantIdAndId(Long tenantId,Long id);

+ 23 - 0
src/main/java/cn/sciento/farm/automationv2/domain/repository/IrrigationTaskLogRepository.java

@@ -0,0 +1,23 @@
+package cn.sciento.farm.automationv2.domain.repository;
+
+import cn.sciento.farm.automationv2.domain.entity.mongo.IrrigationTaskLog;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 灌溉任务日志Repository
+ */
+public interface IrrigationTaskLogRepository extends MongoRepository<IrrigationTaskLog, String> {
+
+    /**
+     * 根据执行实例ID查询日志
+     */
+    Optional<IrrigationTaskLog> findByExecutionId(Long executionId);
+
+    /**
+     * 根据任务ID查询日志列表
+     */
+    List<IrrigationTaskLog> findByTaskId(Long taskId);
+}

+ 5 - 1
src/main/java/cn/sciento/farm/automationv2/domain/repository/IrrigationTaskRepository.java

@@ -26,5 +26,9 @@ public interface IrrigationTaskRepository extends BaseRepository<IrrigationTask>
      */
     List<IrrigationTaskVO> page(IrrigationTask irrigationTask);
 
-
+    /**
+     * 任务完成
+     * @param taskId
+     */
+    void taskStatusSuccessFail(Long taskId);
 }

+ 9 - 0
src/main/java/cn/sciento/farm/automationv2/domain/repository/TaskExecutionRepository.java

@@ -1,9 +1,12 @@
 package cn.sciento.farm.automationv2.domain.repository;
 
 
+import cn.sciento.farm.automationv2.api.dto.DBSelectDTO;
 import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
 import cn.sciento.mybatis.base.BaseRepository;
 
+import java.util.List;
+
 /**
  * <p>
  *
@@ -14,4 +17,10 @@ import cn.sciento.mybatis.base.BaseRepository;
  */
 public interface TaskExecutionRepository extends BaseRepository<TaskExecution> {
 
+    /**
+     * 根据任务获取实例
+     * @param dto
+     * @return
+     */
+    List<TaskExecution> getByTask(DBSelectDTO dto);
 }

+ 7 - 0
src/main/java/cn/sciento/farm/automationv2/domain/repository/TaskGroupConfigRepository.java

@@ -30,4 +30,11 @@ public interface TaskGroupConfigRepository extends BaseRepository<TaskGroupConfi
      */
     List<TaskGroupConfig> queryByTaskId(Long taskId);
 
+
+    /**
+     * 清空任务相关
+     * @param taskId
+     */
+    void deleteByTask(Long taskId);
+
 }

+ 36 - 19
src/main/java/cn/sciento/farm/automationv2/domain/service/ExecutionPlanGenerator.java

@@ -3,6 +3,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.NodeType;
+import cn.sciento.farm.automationv2.domain.enums.ZonePhase;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionPlan;
@@ -55,7 +56,7 @@ public class ExecutionPlanGenerator {
         ZoneConfigView firstZone = zones.get(0);
 
         // 1. 开启第一个灌区(S-01前置:先开灌区)
-        nodeIndex = addOpenGroupNode(nodes, nodeIndex, firstZone);
+        nodeIndex = addOpenGroupNode(nodes, nodeIndex, firstZone, 0, ZonePhase.STARTING);
 
         // 2. 设置水泵压力(PUMP_UNIFIED/PUMP_ZONE模式)
         if (task.needsPumpPressure()) {
@@ -63,17 +64,17 @@ public class ExecutionPlanGenerator {
                     ? task.getTargetPressureKpa()
                     : firstZone.getZonePressureKpa();
             if (pressureKpa != null) {
-                nodeIndex = addSetPumpPressureNode(nodes, nodeIndex, task.getPumpDeviceId(), pressureKpa);
+                nodeIndex = addSetPumpPressureNode(nodes, nodeIndex, task.getPumpDeviceId(), pressureKpa, 0, ZonePhase.STARTING);
             }
         }
 
         // 3. 启动水泵(S-01:在第一个灌区开启后启动)
-        nodeIndex = addStartPumpNode(nodes, nodeIndex, task.getPumpDeviceId());
+        nodeIndex = addStartPumpNode(nodes, nodeIndex, task.getPumpDeviceId(), 0, ZonePhase.STARTING);
 
         // 处理灌区切换(S-02:先开后关 + 稳压等待)
         if (zones.size() == 1) {
             // 只有一个灌区:直接等待后关闭
-            nodeIndex = addWaitNode(nodes, nodeIndex, firstZone.getIrrigationDuration(), "IRRIGATE");
+            nodeIndex = addWaitNode(nodes, nodeIndex, firstZone.getIrrigationDuration(), "IRRIGATE", 0, ZonePhase.IRRIGATING);
         } else {
             // 多个灌区:循环处理灌区切换
             for (int i = 1; i < zones.size(); i++) {
@@ -81,37 +82,38 @@ public class ExecutionPlanGenerator {
                 ZoneConfigView previousZone = zones.get(i - 1);
 
                 // 等待上一个灌区的灌溉时长
-                nodeIndex = addWaitNode(nodes, nodeIndex, previousZone.getIrrigationDuration(), "IRRIGATE");
+                nodeIndex = addWaitNode(nodes, nodeIndex, previousZone.getIrrigationDuration(), "IRRIGATE", i - 1, ZonePhase.IRRIGATING);
 
                 // 先开当前灌区(S-04:先开后关)
-                nodeIndex = addOpenGroupNode(nodes, nodeIndex, currentZone);
+                nodeIndex = addOpenGroupNode(nodes, nodeIndex, currentZone, i, ZonePhase.STARTING);
 
                 // PUMP_ZONE模式:切换时调整水泵压力
                 if (task.isPumpZoneMode() && currentZone.getZonePressureKpa() != null) {
-                    nodeIndex = addSetPumpPressureNode(nodes, nodeIndex, task.getPumpDeviceId(), currentZone.getZonePressureKpa());
+                    nodeIndex = addSetPumpPressureNode(nodes, nodeIndex, task.getPumpDeviceId(), currentZone.getZonePressureKpa(), i, ZonePhase.STARTING);
                 }
 
                 // 稳压等待(S-02:确保压力平稳后再关闭上一个灌区)
                 int switchStableSeconds = task.getSwitchStableSecondsOrDefault();
-                nodeIndex = addZoneSwitchWaitNode(nodes, nodeIndex, switchStableSeconds);
+                nodeIndex = addZoneSwitchWaitNode(nodes, nodeIndex, switchStableSeconds, i - 1, ZonePhase.SWITCHING);
 
                 // 再关上一个灌区(S-04:后关)
-                nodeIndex = addCloseGroupNode(nodes, nodeIndex, previousZone);
+                nodeIndex = addCloseGroupNode(nodes, nodeIndex, previousZone, i - 1, ZonePhase.SWITCHING);
             }
 
             // 最后一个灌区的等待
             ZoneConfigView lastZone = zones.get(zones.size() - 1);
-            nodeIndex = addWaitNode(nodes, nodeIndex, lastZone.getIrrigationDuration(), "IRRIGATE");
+            nodeIndex = addWaitNode(nodes, nodeIndex, lastZone.getIrrigationDuration(), "IRRIGATE", zones.size() - 1, ZonePhase.IRRIGATING);
         }
 
         // 最后阶段:安全关闭(S-03:水泵→电磁阀)
         ZoneConfigView lastZone = zones.get(zones.size() - 1);
+        int lastZoneIndex = zones.size() - 1;
 
         // 4. 关闭水泵
-        nodeIndex = addStopPumpNode(nodes, nodeIndex, task.getPumpDeviceId());
+        nodeIndex = addStopPumpNode(nodes, nodeIndex, task.getPumpDeviceId(), lastZoneIndex, ZonePhase.STOPPING);
 
         // 5. 关闭最后一个灌区
-        nodeIndex = addCloseGroupNode(nodes, nodeIndex, lastZone);
+        nodeIndex = addCloseGroupNode(nodes, nodeIndex, lastZone, lastZoneIndex, ZonePhase.STOPPING);
 
         ExecutionPlan plan = ExecutionPlan.builder().nodes(nodes).build();
 
@@ -123,7 +125,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加开启灌区节点
      */
-    private int addOpenGroupNode(List<ExecutionNode> nodes, int index, ZoneConfigView zone) {
+    private int addOpenGroupNode(List<ExecutionNode> nodes, int index, ZoneConfigView zone, int zoneIndex, ZonePhase zonePhase) {
         List<DeviceInfo> devices = new ArrayList<>();
 
         // 添加电磁阀设备
@@ -163,6 +165,8 @@ public class ExecutionPlanGenerator {
                 .retryCount(0)
                 .maxRetry(3)
                 .devices(devices)
+                .zoneIndex(zoneIndex)
+                .zonePhase(zonePhase)
                 .build();
 
         nodes.add(node);
@@ -172,7 +176,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加关闭灌区节点
      */
-    private int addCloseGroupNode(List<ExecutionNode> nodes, int index, ZoneConfigView zone) {
+    private int addCloseGroupNode(List<ExecutionNode> nodes, int index, ZoneConfigView zone, int zoneIndex, ZonePhase zonePhase) {
         List<DeviceInfo> devices = new ArrayList<>();
 
         // 添加电磁阀设备
@@ -191,6 +195,8 @@ public class ExecutionPlanGenerator {
                 .retryCount(0)
                 .maxRetry(3)
                 .devices(devices)
+                .zoneIndex(zoneIndex)
+                .zonePhase(zonePhase)
                 .build();
 
         nodes.add(node);
@@ -200,7 +206,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加设置水泵压力节点
      */
-    private int addSetPumpPressureNode(List<ExecutionNode> nodes, int index, Long pumpId, Integer pressureKpa) {
+    private int addSetPumpPressureNode(List<ExecutionNode> nodes, int index, Long pumpId, Integer pressureKpa, int zoneIndex, ZonePhase zonePhase) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
                 .deviceType("PUMP")
@@ -220,6 +226,8 @@ public class ExecutionPlanGenerator {
                 .retryCount(0)
                 .maxRetry(3)
                 .devices(Collections.singletonList(pumpDevice))
+                .zoneIndex(zoneIndex)
+                .zonePhase(zonePhase)
                 .build();
 
         nodes.add(node);
@@ -229,7 +237,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加启动水泵节点
      */
-    private int addStartPumpNode(List<ExecutionNode> nodes, int index, Long pumpId) {
+    private int addStartPumpNode(List<ExecutionNode> nodes, int index, Long pumpId, int zoneIndex, ZonePhase zonePhase) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
                 .deviceType("PUMP")
@@ -246,6 +254,8 @@ public class ExecutionPlanGenerator {
                 .retryCount(0)
                 .maxRetry(3)
                 .devices(Collections.singletonList(pumpDevice))
+                .zoneIndex(zoneIndex)
+                .zonePhase(zonePhase)
                 .build();
 
         nodes.add(node);
@@ -255,7 +265,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加关闭水泵节点
      */
-    private int addStopPumpNode(List<ExecutionNode> nodes, int index, Long pumpId) {
+    private int addStopPumpNode(List<ExecutionNode> nodes, int index, Long pumpId, int zoneIndex, ZonePhase zonePhase) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
                 .deviceType("PUMP")
@@ -272,6 +282,8 @@ public class ExecutionPlanGenerator {
                 .retryCount(0)
                 .maxRetry(3)
                 .devices(Collections.singletonList(pumpDevice))
+                .zoneIndex(zoneIndex)
+                .zonePhase(zonePhase)
                 .build();
 
         nodes.add(node);
@@ -283,7 +295,7 @@ public class ExecutionPlanGenerator {
      *
      * @param source 等待来源:IRRIGATE(灌溉等待)/ ZONE_SWITCH(稳压等待)
      */
-    private int addWaitNode(List<ExecutionNode> nodes, int index, Long waitSecond, String source) {
+    private int addWaitNode(List<ExecutionNode> nodes, int index, Long waitSecond, String source, int zoneIndex, ZonePhase zonePhase) {
 
         Map<String, Object> params = new HashMap<>();
         if (waitSecond <= 5){
@@ -302,6 +314,8 @@ public class ExecutionPlanGenerator {
                 .retryCount(0)
                 .maxRetry(0) // WAIT节点不需要重试
                 .devices(new ArrayList<>()) // WAIT节点没有设备
+                .zoneIndex(zoneIndex)
+                .zonePhase(zonePhase)
                 .build();
 
         nodes.add(node);
@@ -311,7 +325,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加灌区切换稳压等待节点
      */
-    private int addZoneSwitchWaitNode(List<ExecutionNode> nodes, int index, int seconds) {
+    private int addZoneSwitchWaitNode(List<ExecutionNode> nodes, int index, int seconds, int zoneIndex, ZonePhase zonePhase) {
         Map<String, Object> params = new HashMap<>();
         params.put("seconds", seconds);
         params.put("source", "ZONE_SWITCH");
@@ -326,6 +340,8 @@ public class ExecutionPlanGenerator {
                 .retryCount(0)
                 .maxRetry(0) // 不需要重试
                 .devices(new ArrayList<>())
+                .zoneIndex(zoneIndex)
+                .zonePhase(zonePhase)
                 .build();
 
         nodes.add(node);
@@ -351,6 +367,7 @@ public class ExecutionPlanGenerator {
                         .deviceType(deviceType)
                         .deviceName(json.getString("deviceName"))
                         .switchId(json.getLong("switchId"))
+                        .sw(json.getInteger("sw"))
                         .build();
                 devices.add(device);
             }

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

@@ -40,6 +40,11 @@ public class DeviceInfo {
     private Long switchId;
 
     /**
+     * 控制方向
+     */
+    private Integer sw;
+
+    /**
      * 设备编号(网关eui)
      */
     private String eui;

+ 7 - 3
src/main/java/cn/sciento/farm/automationv2/domain/valueobject/ExecutionNode.java

@@ -1,6 +1,8 @@
 package cn.sciento.farm.automationv2.domain.valueobject;
 
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
+import cn.sciento.farm.automationv2.domain.enums.ZonePhase;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -64,11 +66,13 @@ public class ExecutionNode {
     /**
      * 节点开始执行时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime startedAt;
 
     /**
      * 节点完成时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime finishedAt;
 
     /**
@@ -77,14 +81,14 @@ public class ExecutionNode {
     private List<DeviceInfo> devices;
 
     /**
-     *
+     * 灌区对应节点号
      */
     private Integer zoneIndex;
 
     /**
-     *
+     * 灌区动作对应状态
      */
-    private String zonePhase;
+    private ZonePhase zonePhase;
 
     /**
      * 获取等待时长(秒)- 仅WAIT和 ZONE_SWITCH_WAIT节点有效

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

@@ -50,7 +50,7 @@ public class IrrigationTaskVO {
     private Integer pressureMode;
 
     /**
-     * 统一目标压力值(kPa,仅 PUMP_UNIFIED 模式需配置)
+     * 统一目标压力值(kPa,仅 统一恒压 模式需配置)
      */
     private Integer targetPressureKpa;
 
@@ -62,7 +62,7 @@ public class IrrigationTaskVO {
     /**
      * 任务状态:FREE / WORKING / WAITING
      */
-    private TaskStatus status;
+    private Integer status;
 
     /**
      * 是否启用:1启用 0禁用 2删除

+ 6 - 1
src/main/java/cn/sciento/farm/automationv2/infra/constant/BaseConstant.java

@@ -25,7 +25,12 @@ public interface BaseConstant {
     Integer ENABLE = 1;
 
     /**
+     * 禁用
+     */
+    Integer DISABLE = 0;
+
+    /**
      * 删除
      */
-    Integer DISABLE = 2;
+    Integer DELETE = 2;
 }

+ 3 - 0
src/main/java/cn/sciento/farm/automationv2/infra/constant/RedisConstant.java

@@ -11,4 +11,7 @@ public interface RedisConstant {
     String TASK_EXECUTION = "wfauto_v2_automation_task_execution:";
 
 
+    String TASK_EXECUTION_LOG = "wfauto_v2_automation_task_execution_log:";
+
+
 }

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

@@ -27,7 +27,7 @@ import org.springframework.web.bind.annotation.*;
 public interface DeviceFeign {
 
     /**
-     * 根据网关编号控制网关
+     * 控制
      */
     @PostMapping(
             value = "/v1/{tenantId}/mqtt/control/switch",
@@ -36,12 +36,22 @@ public interface DeviceFeign {
                                    @RequestBody GatewayControlDto dto);
 
     /**
-     * feign控制变频器
+     * 配置变频器压力(水泵)
      */
     @RequestMapping(
             value = "/v1/{tenantId}/inverter/config",
             method = RequestMethod.POST,
             consumes = "application/json")
-    ResponseEntity<String> PumpPressure(@PathVariable(value = "tenantId") Long tenantId,
+    ResponseEntity<String> pumpPressure(@PathVariable(value = "tenantId") Long tenantId,
                                   @RequestBody GatewayControlDto dto);
+
+    /**
+     * 控制变频器(水泵)
+     */
+    @RequestMapping(
+            value = "/v1/{tenantId}/inverter/control",
+            method = RequestMethod.POST,
+            consumes = "application/json")
+    ResponseEntity<String> pumpControl(@PathVariable(value = "tenantId") Long tenantId,
+                                   @RequestBody GatewayControlDto dto);
 }

+ 6 - 1
src/main/java/cn/sciento/farm/automationv2/infra/feign/fallback/DeviceFallback.java

@@ -30,7 +30,12 @@ public class DeviceFallback implements FallbackFactory<DeviceFeign> {
             }
 
             @Override
-            public ResponseEntity<String> PumpPressure(Long tenantId, GatewayControlDto dto) {
+            public ResponseEntity<String> pumpPressure(Long tenantId, GatewayControlDto dto) {
+                throw new CommonException(throwable);
+            }
+
+            @Override
+            public ResponseEntity<String> pumpControl(Long tenantId, GatewayControlDto dto) {
                 throw new CommonException(throwable);
             }
 

+ 10 - 0
src/main/java/cn/sciento/farm/automationv2/infra/repository/impl/IrrigationTaskRepositoryImpl.java

@@ -1,6 +1,7 @@
 package cn.sciento.farm.automationv2.infra.repository.impl;
 
 import cn.sciento.farm.automationv2.domain.entity.IrrigationTask;
+import cn.sciento.farm.automationv2.domain.enums.TaskStatus;
 import cn.sciento.farm.automationv2.domain.repository.IrrigationTaskRepository;
 import cn.sciento.farm.automationv2.domain.valueobject.IrrigationTaskVO;
 import cn.sciento.farm.automationv2.infra.mapper.IrrigationTaskMapper;
@@ -30,4 +31,13 @@ public class IrrigationTaskRepositoryImpl extends BaseRepositoryImpl<IrrigationT
     public List<IrrigationTaskVO> page(IrrigationTask irrigationTask) {
         return mapper.selectList(irrigationTask);
     }
+
+    @Override
+    public void taskStatusSuccessFail(Long taskId) {
+        IrrigationTask irrigationTask = selectByPrimaryKey(taskId);
+        if (irrigationTask != null){
+            irrigationTask.setStatus(TaskStatus.FREE.getCode());
+            updateByPrimaryKeySelective(irrigationTask);
+        }
+    }
 }

+ 20 - 0
src/main/java/cn/sciento/farm/automationv2/infra/repository/impl/TaskExecutionRepositoryImpl.java

@@ -1,10 +1,16 @@
 package cn.sciento.farm.automationv2.infra.repository.impl;
 
+import cn.sciento.farm.automationv2.api.dto.DBSelectDTO;
 import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
 import cn.sciento.farm.automationv2.domain.repository.TaskExecutionRepository;
 import cn.sciento.mybatis.base.impl.BaseRepositoryImpl;
+import cn.sciento.mybatis.domian.Condition;
+import cn.sciento.mybatis.util.Sqls;
+import org.apache.commons.lang.StringUtils;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
+
 /**
  * <p>
  *       数据层
@@ -16,4 +22,18 @@ import org.springframework.stereotype.Service;
 @Service
 public class TaskExecutionRepositoryImpl extends BaseRepositoryImpl<TaskExecution> implements TaskExecutionRepository {
 
+
+    @Override
+    public List<TaskExecution> getByTask(DBSelectDTO dto) {
+        Sqls sqls = Sqls.custom();
+        sqls.andEqualTo(DBSelectDTO.TASK_ID,dto.getTaskId());
+        if (dto.getStartTime() != null && dto.getEndTime() != null) {
+            sqls.andBetween(DBSelectDTO.START_AT, dto.getStartTime(), dto.getEndTime());
+        }
+        sqls.andEqualTo(DBSelectDTO.TRIGGER_TYPE,dto.getTriggerType(),Boolean.TRUE);
+        sqls.andEqualTo(DBSelectDTO.STATUS,dto.getStatus(),Boolean.TRUE);
+        return selectByCondition(Condition.builder(TaskExecution.class)
+                .where(sqls)
+                .build());
+    }
 }

+ 8 - 0
src/main/java/cn/sciento/farm/automationv2/infra/repository/impl/TaskGroupConfigRepositoryImpl.java

@@ -31,4 +31,12 @@ public class TaskGroupConfigRepositoryImpl extends BaseRepositoryImpl<TaskGroupC
         taskGroupConfig.setTaskId(taskId);
         return select(taskGroupConfig);
     }
+
+    @Override
+    public void deleteByTask(Long taskId) {
+        List<TaskGroupConfig> taskGroupConfigs = queryByTaskId(taskId);
+        if (taskGroupConfigs.size() > 0){
+            batchDeleteByPrimaryKey(taskGroupConfigs);
+        }
+    }
 }

+ 23 - 23
src/main/resources/application.yml

@@ -78,29 +78,29 @@ ribbon:
   MaxAutoRetriesNextServer: 1
 
 
-  # Quartz 调度器配置(集群模式)
-  quartz:
-    job-store-type: jdbc
-    jdbc:
-      initialize-schema: never
-    properties:
-      org:
-        quartz:
-          scheduler:
-            instanceName: IrrigationScheduler
-            instanceId: AUTO
-          jobStore:
-            class: org.quartz.impl.jdbcjobstore.JobStoreTX
-            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
-            tablePrefix: QRTZ_
-            isClustered: true
-            clusterCheckinInterval: 10000
-            useProperties: false
-          threadPool:
-            class: org.quartz.simpl.SimpleThreadPool
-            threadCount: 20
-            threadPriority: 5
-            threadsInheritContextClassLoaderOfInitializingThread: true
+# Quartz 调度器配置(集群模式)
+quartz:
+  job-store-type: jdbc
+  jdbc:
+    initialize-schema: never
+  properties:
+    org:
+      quartz:
+        scheduler:
+          instanceName: IrrigationScheduler
+          instanceId: AUTO
+        jobStore:
+          class: org.quartz.impl.jdbcjobstore.JobStoreTX
+          driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
+          tablePrefix: QRTZ_
+          isClustered: true
+          clusterCheckinInterval: 10000
+          useProperties: false
+        threadPool:
+          class: org.quartz.simpl.SimpleThreadPool
+          threadCount: 20
+          threadPriority: 5
+          threadsInheritContextClassLoaderOfInitializingThread: true
 
 # RocketMQ 配置
 rocketmq:

+ 2 - 4
src/main/resources/bootstrap.yml

@@ -1,10 +1,8 @@
 server:
-  port: 8330
-  servlet:
-    context-path: /wfauto
+  port: 8300
 management:
   server:
-    port: 8331
+    port: 8301
   endpoints:
     web:
       exposure:

+ 2 - 2
src/main/resources/mapper/IrrigationTaskMapper.xml

@@ -136,7 +136,7 @@
         <result column="pressure_mode" property="pressureMode"/>
         <result column="target_pressure_kpa" property="targetPressureKpa"/>
         <result column="is_device_pump" property="isDevicePump"/>
-        <result column="status" property="status" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
+        <result column="status" property="status" />
         <result column="enabled_flag" property="enabledFlag"/>
         <result column="tenant_id" property="tenantId"/>
         <result column="organization_id" property="organizationId"/>
@@ -160,7 +160,7 @@
         FROM
             wfauto_v2_irrigation_task
         WHERE
-            tenant_id = #{tenantId}
+            tenant_id = #{task.tenantId}
             <if test="task.organizationId != null and task.organizationId != ''">
                 AND organization_id = #{task.organizationId}
             </if>

+ 5 - 0
src/main/resources/messages/messages_wfautoV2_en_US.properties

@@ -12,3 +12,8 @@ wfautoV2.parameter.error=error parameter
 
 wfautoV2.group.hadTask=If there are bound tasks in the current Wheel and Tank Group, delete the tasks first
 wfautoV2.task.enabled=Disable the task before deleting it
+wfautoV2.task.hadRunning=The task is running and cannot be disabled for the time being
+wfautoV2.task.hadAction=The task has already started and cannot be started repeatedly
+
+wfautoV2.task.execution.notFound=The task instance does not exist
+wfautoV2.task.execution.hadCancel=The task execution has been terminated and cannot be canceled

+ 6 - 0
src/main/resources/messages/messages_wfautoV2_zh_CN.properties

@@ -12,3 +12,9 @@ wfautoV2.parameter.error=\u53C2\u6570\u9519\u8BEF
 
 wfautoV2.group.hadTask=\u5F53\u524D\u8F6E\u7F50\u7EC4\u5B58\u5728\u5DF2\u7ED1\u5B9A\u4EFB\u52A1\uFF0C\u8BF7\u5148\u5220\u9664\u4EFB\u52A1
 wfautoV2.task.enabled=\u8BF7\u5148\u7981\u7528\u4EFB\u52A1\u518D\u5220\u9664
+wfautoV2.task.hadRunning=\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u6682\u65F6\u65E0\u6CD5\u7981\u7528
+wfautoV2.task.hadAction=\u4EFB\u52A1\u5DF2\u7ECF\u542F\u52A8\uFF0C\u65E0\u6CD5\u91CD\u590D\u542F\u52A8
+
+wfautoV2.task.execution.notFound=\u4EFB\u52A1\u5B9E\u4F8B\u4E0D\u5B58\u5728
+wfautoV2.task.execution.hadCancel=\u4EFB\u52A1\u6267\u884C\u5DF2\u7EC8\u6B62\uFF0C\u65E0\u6CD5\u53D6\u6D88
+wfautoV2.task.execution.cancelError=\u4EFB\u52A1\u6267\u884C\u53D6\u6D88\u5931\u8D25,\u9519\u8BEF\u4FE1\u606F: {0}