Browse Source

feature: 智能控制V2版本 第四版(任务流程)

Jayhaw 3 weeks ago
parent
commit
c2abc78c1a
51 changed files with 1306 additions and 501 deletions
  1. 65 64
      src/main/java/cn/sciento/farm/automationv2/api/controller/ExecutionMonitorController.java
  2. 1 1
      src/main/java/cn/sciento/farm/automationv2/api/controller/IrrigationGroupController.java
  3. 36 124
      src/main/java/cn/sciento/farm/automationv2/api/controller/IrrigationTaskController.java
  4. 7 0
      src/main/java/cn/sciento/farm/automationv2/api/dto/BallValuesDTO.java
  5. 14 8
      src/main/java/cn/sciento/farm/automationv2/api/dto/CreateTaskRequest.java
  6. 52 0
      src/main/java/cn/sciento/farm/automationv2/api/dto/GatewayControlDto.java
  7. 4 4
      src/main/java/cn/sciento/farm/automationv2/app/context/ExecutionContext.java
  8. 134 0
      src/main/java/cn/sciento/farm/automationv2/app/handler/DeviceControlHelper.java
  9. 42 13
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/CloseGroupNodeHandler.java
  10. 47 15
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/OpenGroupNodeHandler.java
  11. 45 7
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/SetPumpPressureNodeHandler.java
  12. 45 7
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StartFertilizerNodeHandler.java
  13. 43 7
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StartPumpNodeHandler.java
  14. 44 7
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StopFertilizerNodeHandler.java
  15. 43 7
      src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StopPumpNodeHandler.java
  16. 1 1
      src/main/java/cn/sciento/farm/automationv2/app/job/IrrigationScheduledJob.java
  17. 26 17
      src/main/java/cn/sciento/farm/automationv2/app/service/AlarmRecordService.java
  18. 26 8
      src/main/java/cn/sciento/farm/automationv2/app/service/RetryManager.java
  19. 67 60
      src/main/java/cn/sciento/farm/automationv2/app/service/SafeShutdownService.java
  20. 69 25
      src/main/java/cn/sciento/farm/automationv2/app/service/TaskExecutionEngine.java
  21. 16 20
      src/main/java/cn/sciento/farm/automationv2/app/service/TaskTriggerService.java
  22. 15 2
      src/main/java/cn/sciento/farm/automationv2/app/service/irrigationGroup/impl/IrrigationGroupServiceImpl.java
  23. 27 0
      src/main/java/cn/sciento/farm/automationv2/app/service/irrigationTask/IrrigationTaskService.java
  24. 42 4
      src/main/java/cn/sciento/farm/automationv2/app/service/irrigationTask/impl/IrrigationTaskServiceImpl.java
  25. 22 0
      src/main/java/cn/sciento/farm/automationv2/config/Business.java
  26. 25 0
      src/main/java/cn/sciento/farm/automationv2/domain/business/DeviceBusiness.java
  27. 33 0
      src/main/java/cn/sciento/farm/automationv2/domain/business/impl/DeviceBusinessImpl.java
  28. 2 1
      src/main/java/cn/sciento/farm/automationv2/domain/entity/AlarmRecord.java
  29. 1 1
      src/main/java/cn/sciento/farm/automationv2/domain/entity/IrrigationGroup.java
  30. 40 29
      src/main/java/cn/sciento/farm/automationv2/domain/entity/IrrigationTask.java
  31. 3 17
      src/main/java/cn/sciento/farm/automationv2/domain/entity/TaskExecution.java
  32. 8 3
      src/main/java/cn/sciento/farm/automationv2/domain/entity/TaskGroupConfig.java
  33. 5 5
      src/main/java/cn/sciento/farm/automationv2/domain/enums/PressureMode.java
  34. 16 0
      src/main/java/cn/sciento/farm/automationv2/domain/repository/TaskGroupConfigRepository.java
  35. 22 22
      src/main/java/cn/sciento/farm/automationv2/domain/service/ExecutionPlanGenerator.java
  36. 16 1
      src/main/java/cn/sciento/farm/automationv2/domain/valueobject/DeviceInfo.java
  37. 18 4
      src/main/java/cn/sciento/farm/automationv2/domain/valueobject/ZoneConfigView.java
  38. 7 0
      src/main/java/cn/sciento/farm/automationv2/infra/constant/BaseConstant.java
  39. 1 1
      src/main/java/cn/sciento/farm/automationv2/infra/constant/RedisConstant.java
  40. 39 0
      src/main/java/cn/sciento/farm/automationv2/infra/feign/DeviceFeign.java
  41. 34 0
      src/main/java/cn/sciento/farm/automationv2/infra/feign/fallback/DeviceFallback.java
  42. 1 1
      src/main/java/cn/sciento/farm/automationv2/infra/mq/message/DeviceAckMessage.java
  43. 1 1
      src/main/java/cn/sciento/farm/automationv2/infra/mq/message/DeviceCommandMessage.java
  44. 5 5
      src/main/java/cn/sciento/farm/automationv2/infra/mq/producer/DeviceCommandProducer.java
  45. 4 4
      src/main/java/cn/sciento/farm/automationv2/infra/redis/AckManager.java
  46. 66 0
      src/main/java/cn/sciento/farm/automationv2/infra/redis/ExecutionPlanStore.java
  47. 15 0
      src/main/java/cn/sciento/farm/automationv2/infra/repository/impl/TaskGroupConfigRepositoryImpl.java
  48. 1 1
      src/main/resources/mapper/IrrigationGroupMapper.xml
  49. 4 4
      src/main/resources/mapper/IrrigationTaskMapper.xml
  50. 3 0
      src/main/resources/messages/messages_wfautoV2_en_US.properties
  51. 3 0
      src/main/resources/messages/messages_wfautoV2_zh_CN.properties

+ 65 - 64
src/main/java/cn/sciento/farm/automationv2/api/controller/ExecutionMonitorController.java

@@ -5,6 +5,7 @@ import cn.sciento.farm.automationv2.api.dto.Result;
 import cn.sciento.farm.automationv2.app.service.SafeShutdownService;
 import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
 import cn.sciento.farm.automationv2.domain.enums.ExecutionStatus;
+import cn.sciento.farm.automationv2.domain.repository.TaskExecutionRepository;
 import cn.sciento.farm.automationv2.infra.mapper.TaskExecutionMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -22,7 +23,7 @@ import java.util.List;
 @RequiredArgsConstructor
 public class ExecutionMonitorController {
 
-    private final TaskExecutionMapper taskExecutionMapper;
+    private final TaskExecutionRepository taskExecutionRepository;
     private final SafeShutdownService safeShutdownService;
 
     /**
@@ -32,7 +33,7 @@ public class ExecutionMonitorController {
     public Result<TaskExecution> getExecution(@PathVariable Long id) {
         log.info("查询执行详情,executionId={}", id);
 
-        TaskExecution execution = taskExecutionMapper.selectById(id);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(id);
         if (execution == null) {
             return Result.error("执行实例不存在");
         }
@@ -40,41 +41,41 @@ public class ExecutionMonitorController {
         return Result.success(execution);
     }
 
-    /**
-     * 分页查询执行历史
-     */
-    @GetMapping
-    public Result<List<TaskExecution>> listExecutions(PageRequest pageRequest) {
-        log.info("分页查询执行历史,pageNum={}, pageSize={}, tenantId={}",
-                pageRequest.getPageNum(), pageRequest.getPageSize(), pageRequest.getTenantId());
-
-        List<TaskExecution> executions = taskExecutionMapper.selectByTenant(
-                pageRequest.getTenantId(),
-                pageRequest.getOffset(),
-                pageRequest.getLimit()
-        );
-
-        return Result.success(executions);
-    }
-
-    /**
-     * 根据任务ID查询执行历史
-     */
-    @GetMapping("/task/{taskId}")
-    public Result<List<TaskExecution>> listExecutionsByTask(
-            @PathVariable Long taskId,
-            PageRequest pageRequest) {
-        log.info("查询任务执行历史,taskId={}, pageNum={}, pageSize={}",
-                taskId, pageRequest.getPageNum(), pageRequest.getPageSize());
-
-        List<TaskExecution> executions = taskExecutionMapper.selectByTaskId(
-                taskId,
-                pageRequest.getOffset(),
-                pageRequest.getLimit()
-        );
-
-        return Result.success(executions);
-    }
+//    /**
+//     * 分页查询执行历史
+//     */
+//    @GetMapping
+//    public Result<List<TaskExecution>> listExecutions(PageRequest pageRequest) {
+//        log.info("分页查询执行历史,pageNum={}, pageSize={}, tenantId={}",
+//                pageRequest.getPageNum(), pageRequest.getPageSize(), pageRequest.getTenantId());
+//
+//        List<TaskExecution> executions = taskExecutionMapper.selectByTenant(
+//                pageRequest.getTenantId(),
+//                pageRequest.getOffset(),
+//                pageRequest.getLimit()
+//        );
+//
+//        return Result.success(executions);
+//    }
+
+//    /**
+//     * 根据任务ID查询执行历史
+//     */
+//    @GetMapping("/task/{taskId}")
+//    public Result<List<TaskExecution>> listExecutionsByTask(
+//            @PathVariable Long taskId,
+//            PageRequest pageRequest) {
+//        log.info("查询任务执行历史,taskId={}, pageNum={}, pageSize={}",
+//                taskId, pageRequest.getPageNum(), pageRequest.getPageSize());
+//
+//        List<TaskExecution> executions = taskExecutionMapper.selectByTaskId(
+//                taskId,
+//                pageRequest.getOffset(),
+//                pageRequest.getLimit()
+//        );
+//
+//        return Result.success(executions);
+//    }
 
     /**
      * 查询执行状态
@@ -83,7 +84,7 @@ public class ExecutionMonitorController {
     public Result<ExecutionStatus> getExecutionStatus(@PathVariable Long id) {
         log.info("查询执行状态,executionId={}", id);
 
-        TaskExecution execution = taskExecutionMapper.selectById(id);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(id);
         if (execution == null) {
             return Result.error("执行实例不存在");
         }
@@ -100,7 +101,7 @@ public class ExecutionMonitorController {
 
         try {
             // 加载执行实例
-            TaskExecution execution = taskExecutionMapper.selectById(id);
+            TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(id);
             if (execution == null) {
                 return Result.error("执行实例不存在");
             }
@@ -113,7 +114,7 @@ public class ExecutionMonitorController {
             // 更新状态为CANCELLED
             execution.markAsCancelled();
             execution.setFinishedAt(LocalDateTime.now());
-            taskExecutionMapper.updateByVersion(execution);
+            taskExecutionRepository.updateByPrimaryKeySelective(execution);
 
             // 触发安全关闭
             SafeShutdownService.ShutdownResult result = safeShutdownService.shutdown(id);
@@ -127,7 +128,7 @@ public class ExecutionMonitorController {
                     String.join(",", result.getFailedDevices().stream()
                             .map(d -> "\"" + d + "\"").toArray(String[]::new))
             ));
-            taskExecutionMapper.updateByVersion(execution);
+            taskExecutionRepository.updateByPrimaryKeySelective(execution);
 
             log.info("执行已取消,executionId={}, shutdownResult={}", id, result.getSummary());
 
@@ -139,27 +140,27 @@ public class ExecutionMonitorController {
         }
     }
 
-    /**
-     * 查询正在执行的任务
-     */
-    @GetMapping("/running")
-    public Result<List<TaskExecution>> listRunningExecutions(@RequestParam Long tenantId) {
-        log.info("查询正在执行的任务,tenantId={}", tenantId);
-
-        List<TaskExecution> executions = taskExecutionMapper.selectRunningByTenant(tenantId);
-
-        return Result.success(executions);
-    }
-
-    /**
-     * 统计执行次数
-     */
-    @GetMapping("/task/{taskId}/count")
-    public Result<Integer> countExecutions(@PathVariable Long taskId) {
-        log.info("统计任务执行次数,taskId={}", taskId);
-
-        int count = taskExecutionMapper.countByTaskId(taskId);
-
-        return Result.success(count);
-    }
+//    /**
+//     * 查询正在执行的任务
+//     */
+//    @GetMapping("/running")
+//    public Result<List<TaskExecution>> listRunningExecutions(@RequestParam Long tenantId) {
+//        log.info("查询正在执行的任务,tenantId={}", tenantId);
+//
+//        List<TaskExecution> executions = taskExecutionMapper.selectRunningByTenant(tenantId);
+//
+//        return Result.success(executions);
+//    }
+//
+//    /**
+//     * 统计执行次数
+//     */
+//    @GetMapping("/task/{taskId}/count")
+//    public Result<Integer> countExecutions(@PathVariable Long taskId) {
+//        log.info("统计任务执行次数,taskId={}", taskId);
+//
+//        int count = taskExecutionMapper.countByTaskId(taskId);
+//
+//        return Result.success(count);
+//    }
 }

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

@@ -59,7 +59,7 @@ public class IrrigationGroupController {
         return Results.success(service.queryByDetail(tenantId,planId));
     }
 
-    @ApiOperation("通过id删除信息")
+    @ApiOperation("通过id删除")
     @Permission(level = ResourceLevel.ORGANIZATION)
     @DeleteMapping
     public ResponseEntity<Integer> delete(@PathVariable Long tenantId, @RequestBody IrrigationGroup irrigationGroup){

+ 36 - 124
src/main/java/cn/sciento/farm/automationv2/api/controller/IrrigationTaskController.java

@@ -1,63 +1,52 @@
 package cn.sciento.farm.automationv2.api.controller;
 
 import cn.sciento.core.domain.Page;
+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.app.service.TaskTriggerService;
 import cn.sciento.farm.automationv2.app.service.irrigationTask.IrrigationTaskService;
-import cn.sciento.farm.automationv2.domain.entity.*;
-import cn.sciento.farm.automationv2.domain.enums.*;
+import cn.sciento.farm.automationv2.domain.entity.IrrigationTask;
 import cn.sciento.farm.automationv2.domain.valueobject.IrrigationTaskVO;
-import cn.sciento.farm.automationv2.infra.constant.BaseConstant;
-import cn.sciento.farm.automationv2.infra.mapper.IrrigationTaskMapper;
-import cn.sciento.farm.automationv2.infra.mapper.LinkageRuleMapper;
-import cn.sciento.farm.automationv2.infra.mapper.TaskGroupConfigMapper;
-import cn.sciento.farm.automationv2.infra.mapper.TaskScheduleRuleMapper;
 import cn.sciento.swagger.annotation.Permission;
 import io.choerodon.mybatis.pagehelper.annotation.PageableDefault;
 import io.choerodon.mybatis.pagehelper.domain.PageRequest;
 import io.choerodon.mybatis.pagehelper.domain.Sort;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import javax.validation.Valid;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * 灌溉任务管理API
  */
+@Slf4j
 @Api("灌溉任务接口")
 @RestController
-@RequestMapping("/api/tasks")
+@RequestMapping("/v2/{tenantId}/irrigation-task")
 public class IrrigationTaskController {
 
-//    private final TaskGroupConfigMapper taskGroupConfigMapper;
-//    private final TaskScheduleRuleMapper taskScheduleRuleMapper;
-//    private final LinkageRuleMapper linkageRuleMapper;
-//    private final QuartzManagementService quartzManagementService;
+    private final IrrigationTaskService taskService;
     private final TaskTriggerService taskTriggerService;
 
-    private IrrigationTaskService taskService;
-
-    public IrrigationTaskController(IrrigationTaskService taskService) {
+    public IrrigationTaskController(IrrigationTaskService taskService, TaskTriggerService taskTriggerService) {
         this.taskService = taskService;
+        this.taskTriggerService = taskTriggerService;
     }
 
     /**
      * 创建任务
      */
     @PostMapping
+    @ApiOperation("创建任务")
     @Permission(level = ResourceLevel.ORGANIZATION)
-    public ResponseEntity<IrrigationTask> createTask(@Valid @RequestBody CreateTaskRequest request) {
+    public ResponseEntity<IrrigationTask> createTask(@PathVariable Long tenantId,
+                                                     @Valid @RequestBody CreateTaskRequest request) {
 
 
 
@@ -135,6 +124,7 @@ public class IrrigationTaskController {
 //                log.info("任务联动规则保存成功,taskId={}, linkageRuleCount={}",
 //                        taskId, request.getLinkageRules().size());
 //            }
+        request.setTenantId(tenantId);
         return Results.success(taskService.create(request));
     }
 
@@ -142,15 +132,10 @@ public class IrrigationTaskController {
      * 查询任务详情
      */
     @GetMapping("/{id}")
-    public Result<IrrigationTask> getTask(@PathVariable Long id) {
-        log.info("查询任务详情,taskId={}", id);
-
-        IrrigationTask task = irrigationTaskMapper.selectById(id);
-        if (task == null) {
-            return Result.error("任务不存在");
-        }
-
-        return Result.success(task);
+    @ApiOperation("查询任务详情")
+    @Permission(level = ResourceLevel.ORGANIZATION)
+    public ResponseEntity<IrrigationTask> getTask(@PathVariable Long tenantId,@PathVariable Long id) {
+        return Results.success(taskService.getById(id));
     }
 
 
@@ -167,108 +152,41 @@ public class IrrigationTaskController {
     /**
      * 更新任务
      */
-    @PutMapping("/{id}")
-    public Result<Void> updateTask(@PathVariable Long id, @RequestBody IrrigationTask task) {
-        log.info("更新任务,taskId={}", id);
-
-        task.setId(id);
-
-        int updated = irrigationTaskMapper.updateById(task);
-        if (updated == 0) {
-            return Result.error("任务不存在或更新失败");
+    @PutMapping
+    @ApiOperation("更新任务")
+    @Permission(level = ResourceLevel.ORGANIZATION)
+    public ResponseEntity<IrrigationTask> updateTask(@PathVariable Long tenantId,@RequestBody IrrigationTask task) {
+        if (task.getId() == null){
+            throw new CommonException("wfautoV2.parameter.incomplete");
         }
+        task.setTenantId(tenantId);
+        return Results.success(taskService.update(task));
 
-        return Result.success();
     }
 
     /**
      * 删除任务
      */
     @DeleteMapping("/{id}")
+    @ApiOperation("删除任务")
+    @Permission(level = ResourceLevel.ORGANIZATION)
     @Transactional(rollbackFor = Exception.class)
-    public Result<Void> deleteTask(@PathVariable Long id) {
-        log.info("删除任务,taskId={}", id);
-
-        try {
-            // 删除所有定时规则的调度
-            quartzManagementService.unscheduleAllTaskRules(id);
-
-            // 逻辑删除所有定时规则
-            taskScheduleRuleMapper.deleteByTaskId(id);
-
-            // 逻辑删除所有联动规则
-            linkageRuleMapper.deleteByTaskId(id);
-
-            // 逻辑删除任务
-            int deleted = irrigationTaskMapper.deleteById(id);
-            if (deleted == 0) {
-                return Result.error("任务不存在或删除失败");
-            }
-
-            log.info("任务删除成功,taskId={}", id);
-            return Result.success();
-
-        } catch (Exception e) {
-            log.error("删除任务失败,taskId={}", id, e);
-            return Result.error("删除任务失败: " + e.getMessage());
-        }
+    public ResponseEntity<Integer> deleteTask(@PathVariable Long id,@RequestBody IrrigationTask task) {
+        task.setId(id);
+        return Results.success(taskService.delete(task));
     }
 
     /**
-     * 启用任务(为所有启用的规则添加定时调度)
+     * 启用/禁用任务
      */
     @PostMapping("/{id}/enable")
-    public Result<Void> enableTask(@PathVariable Long id) {
-        log.info("启用任务,taskId={}", id);
-
-        try {
-            IrrigationTask task = irrigationTaskMapper.selectById(id);
-            if (task == null) {
-                return Result.error("任务不存在");
-            }
-
-            // 为所有启用的规则创建调度
-            quartzManagementService.scheduleAllTaskRules(id);
-
-            // 更新任务状态
-            task.setEnabledFlag(BaseConstant.ENABLE);
-            task.setStatus(TaskStatus.FREE);
-            irrigationTaskMapper.updateById(task);
-
-            return Result.success();
-
-        } catch (Exception e) {
-            log.error("启用任务失败,taskId={}", id, e);
-            return Result.error("启用任务失败: " + e.getMessage());
-        }
+    @ApiOperation("启用任务")
+    @Permission(level = ResourceLevel.ORGANIZATION)
+    public ResponseEntity<Integer> enableTask(@PathVariable Long id,@RequestBody IrrigationTask task) {
+        task.setId(id);
+        return Results.success(taskService.enable(task));
     }
 
-    /**
-     * 禁用任务(删除所有规则的定时调度)
-     */
-    @PostMapping("/{id}/disable")
-    public Result<Void> disableTask(@PathVariable Long id) {
-        log.info("禁用任务,taskId={}", id);
-
-        try {
-            // 删除所有规则的Quartz调度
-            quartzManagementService.unscheduleAllTaskRules(id);
-
-            // 更新任务状态
-            IrrigationTask task = irrigationTaskMapper.selectById(id);
-            if (task != null) {
-                task.setEnabled(false);
-                task.setStatus(TaskStatus.DISABLED);
-                irrigationTaskMapper.updateById(task);
-            }
-
-            return Result.success();
-
-        } catch (Exception e) {
-            log.error("禁用任务失败,taskId={}", id, e);
-            return Result.error("禁用任务失败: " + e.getMessage());
-        }
-    }
 
     /**
      * 手动触发任务
@@ -277,14 +195,8 @@ public class IrrigationTaskController {
     @ApiOperation("手动触发任务")
     @Permission(level = ResourceLevel.ORGANIZATION)
     public ResponseEntity<Long> triggerTask(@PathVariable Long id) {
+        Long executionId = taskTriggerService.manualTrigger(id);
+        return Results.success(executionId);
 
-        try {
-            Long executionId = taskTriggerService.manualTrigger(id);
-            return Result.success(executionId);
-
-        } catch (Exception e) {
-            log.error("手动触发任务失败,taskId={}", id, e);
-            return Result.error("触发任务失败: " + e.getMessage());
-        }
     }
 }

+ 7 - 0
src/main/java/cn/sciento/farm/automationv2/api/dto/BallValuesDTO.java

@@ -24,6 +24,12 @@ public class BallValuesDTO {
     private String deviceName;
 
     /**
+     * 控制节点
+     */
+    @NotNull(message = "控制节点不能为空")
+    private Long switchId;
+
+    /**
      * 控制方向
      */
     @NotNull(message = "阀口方向不能为空")
@@ -40,3 +46,4 @@ public class BallValuesDTO {
     private Integer targetPressureKpa;
 
 }
+

+ 14 - 8
src/main/java/cn/sciento/farm/automationv2/api/dto/CreateTaskRequest.java

@@ -164,7 +164,7 @@ public class CreateTaskRequest {
     private Integer isPump;
 
     /**
-     * 统一目标压力值(kPa,PUMP_UNIFIED模式必填)
+     * 统一目标压力值(kPa,统一恒压模式必填)
      */
     private Integer targetPressureKpa;
 
@@ -184,12 +184,12 @@ public class CreateTaskRequest {
     /**
      * 施肥泵设备ID
      */
-    private String fertilizerPumpId;
+    private Long fertilizerPumpId;
 
     /**
      * 搅拌电机设备ID
      */
-    private String stirMotorId;
+    private Long stirMotorId;
 
     /**
      * 施肥控制模式:TIME / VOLUME
@@ -253,6 +253,12 @@ public class CreateTaskRequest {
         private Long groupId;
 
         /**
+         * 灌溉组名称
+         */
+        @NotBlank(message = "灌溉组ID不能为空")
+        private String groupName;
+
+        /**
          * 执行顺序(0开始)
          */
         @NotNull(message = "执行顺序不能为空")
@@ -265,11 +271,11 @@ public class CreateTaskRequest {
         private Long irrigationDuration;
 
         /**
-         * 球阀目标压力(bar)
-         * 仅在启用球阀恒压 时生效,球阀设备下发新压力值
+         * 灌区目标压力(bar)
+         * 仅在灌区恒压 时生效,下发灌区
          */
-        @NotNull(message = "灌溉时长不能为空")
-        private Integer devicePressureKpa;
+        @NotNull(message = "目标压力不能为空")
+        private Integer groupPressureKpa;
     }
     
     public IrrigationTask buildTask(){
@@ -290,7 +296,7 @@ public class CreateTaskRequest {
                 .preStirMinutes(preStirMinutes)
                 .fertDurationMinutes(fertDurationMinutes)
                 .fertTargetLiters(fertTargetLiters)
-                .status(TaskStatus.FREE)
+                .status(TaskStatus.FREE.getCode())
                 .enabledFlag(enabledFlag != null ? enabledFlag : BaseConstant.ENABLE)
                 .tenantId(tenantId)
                 .organizationId(organizationId)

+ 52 - 0
src/main/java/cn/sciento/farm/automationv2/api/dto/GatewayControlDto.java

@@ -0,0 +1,52 @@
+package cn.sciento.farm.automationv2.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * <p>
+ *      网关控制dto
+ * </p>
+ *
+ * @author MouthMouth
+ * @since 2022/4/1
+ */
+@Data
+public class GatewayControlDto {
+
+
+    @NotBlank(message = "设备编号不能为空")
+    private String eui;
+
+    @NotBlank(message = "设备节点不能为空")
+    private String node;
+
+    @NotBlank(message = "设备控制回路不能为空")
+    private String circuit;
+
+    @NotNull(message = "设备控制标志不能为空!")
+    private Long sw;
+
+    @NotNull(message = "租户id不能为空!")
+    private Long tenantId;
+
+    private String remark = "本次控制由智能控制V2调用";
+
+    private String staffName = "第三方远程调用";
+
+    @ApiModelProperty("4G球阀 - 开度百分比")
+    private Integer ballValuePercent;
+
+    @ApiModelProperty("持续时间")
+    private Integer duration;
+
+    @ApiModelProperty("4G球阀 - 控制类型")
+    private Integer ballValueControlType;
+
+    @ApiModelProperty("4G球阀 - 设置容量值,大于0且`type`非0时不生效。")
+    private Float capacity;
+
+}

+ 4 - 4
src/main/java/cn/sciento/farm/automationv2/app/context/ExecutionContext.java

@@ -55,21 +55,21 @@ public class ExecutionContext {
     /**
      * 获取水泵ID
      */
-    public String getPumpId() {
-        return task != null ? task.getPumpId() : null;
+    public Long getPumpId() {
+        return task != null ? task.getPumpDeviceId() : null;
     }
 
     /**
      * 获取施肥泵ID
      */
-    public String getFertilizerPumpId() {
+    public Long getFertilizerPumpId() {
         return task != null ? task.getFertilizerPumpId() : null;
     }
 
     /**
      * 获取搅拌电机ID
      */
-    public String getStirMotorId() {
+    public Long getStirMotorId() {
         return task != null ? task.getStirMotorId() : null;
     }
 }

+ 134 - 0
src/main/java/cn/sciento/farm/automationv2/app/handler/DeviceControlHelper.java

@@ -0,0 +1,134 @@
+package cn.sciento.farm.automationv2.app.handler;
+
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
+import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 设备控制辅助类
+ * 功能:构造GatewayControlDto和映射sw值
+ */
+@Slf4j
+public class DeviceControlHelper {
+
+    /**
+     * 构造开启电磁阀的控制DTO
+     */
+    public static GatewayControlDto buildOpenSolenoidValveDto(DeviceInfo device, Long tenantId) {
+        GatewayControlDto dto = new GatewayControlDto();
+        dto.setEui(device.getEui());
+        dto.setNode(device.getNode());
+        dto.setCircuit(device.getCircuit());
+        dto.setSw(1L); // 开启
+        dto.setTenantId(tenantId);
+        dto.setRemark("本次控制由智能控制V2调用 - 开启电磁阀");
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+
+    /**
+     * 构造关闭电磁阀的控制DTO
+     */
+    public static GatewayControlDto buildCloseSolenoidValveDto(DeviceInfo device, Long tenantId) {
+        GatewayControlDto dto = new GatewayControlDto();
+        dto.setEui(device.getEui());
+        dto.setNode(device.getNode());
+        dto.setCircuit(device.getCircuit());
+        dto.setSw(0L); // 关闭
+        dto.setTenantId(tenantId);
+        dto.setRemark("本次控制由智能控制V2调用 - 关闭电磁阀");
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+
+    /**
+     * 构造球阀角度控制的DTO
+     */
+    public static GatewayControlDto buildBallValveAngleDto(DeviceInfo device, Integer targetAngle, Long tenantId) {
+        GatewayControlDto dto = new GatewayControlDto();
+        dto.setEui(device.getEui());
+        dto.setNode(device.getNode());
+        dto.setCircuit(device.getCircuit());
+        dto.setSw(1L); // 球阀控制开启
+        dto.setTenantId(tenantId);
+        dto.setBallValuePercent(targetAngle); // 设置角度百分比
+        dto.setRemark("本次控制由智能控制V2调用 - 球阀角度控制");
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+
+    /**
+     * 构造启动水泵的控制DTO
+     */
+    public static GatewayControlDto buildStartPumpDto(DeviceInfo device, Long tenantId) {
+        GatewayControlDto dto = new GatewayControlDto();
+        dto.setEui(device.getEui());
+        dto.setNode(device.getNode());
+        dto.setCircuit(device.getCircuit());
+        dto.setSw(1L); // 启动
+        dto.setTenantId(tenantId);
+        dto.setRemark("本次控制由智能控制V2调用 - 启动水泵");
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+
+    /**
+     * 构造关闭水泵的控制DTO
+     */
+    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.setTenantId(tenantId);
+        dto.setRemark("本次控制由智能控制V2调用 - 关闭水泵");
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+
+    /**
+     * 构造设置水泵压力的控制DTO
+     */
+    public static GatewayControlDto buildSetPumpPressureDto(DeviceInfo device, Integer pressureKpa, Long tenantId) {
+        GatewayControlDto dto = new GatewayControlDto();
+        dto.setEui(device.getEui());
+        dto.setNode(device.getNode());
+        dto.setCircuit(device.getCircuit());
+        dto.setSw(2L); // 设置压力使用特殊标志
+        dto.setTenantId(tenantId);
+        dto.setRemark("本次控制由智能控制V2调用 - 设置水泵压力: " + pressureKpa + "kPa");
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+
+    /**
+     * 构造启动施肥机的控制DTO
+     */
+    public static GatewayControlDto buildStartFertilizerDto(DeviceInfo device, Integer programNo, Long tenantId) {
+        GatewayControlDto dto = new GatewayControlDto();
+        dto.setEui(device.getEui());
+        dto.setNode(device.getNode());
+        dto.setCircuit(device.getCircuit());
+        dto.setSw(1L); // 启动
+        dto.setTenantId(tenantId);
+        dto.setRemark("本次控制由智能控制V2调用 - 启动施肥机,程序号: " + programNo);
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+
+    /**
+     * 构造关闭施肥机的控制DTO
+     */
+    public static GatewayControlDto buildStopFertilizerDto(DeviceInfo device, Long tenantId) {
+        GatewayControlDto dto = new GatewayControlDto();
+        dto.setEui(device.getEui());
+        dto.setNode(device.getNode());
+        dto.setCircuit(device.getCircuit());
+        dto.setSw(0L); // 关闭
+        dto.setTenantId(tenantId);
+        dto.setRemark("本次控制由智能控制V2调用 - 关闭施肥机");
+        dto.setStaffName("第三方远程调用");
+        return dto;
+    }
+}

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

@@ -1,11 +1,14 @@
 package cn.sciento.farm.automationv2.app.handler.impl;
 
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
 import cn.sciento.farm.automationv2.app.context.ExecutionContext;
+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.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
-import cn.sciento.farm.automationv2.infra.mq.producer.DeviceCommandProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -22,7 +25,7 @@ import java.util.List;
 @RequiredArgsConstructor
 public class CloseGroupNodeHandler implements NodeHandler {
 
-    private final DeviceCommandProducer deviceCommandProducer;
+    private final DeviceBusiness deviceBusiness;
     private final AckManager ackManager;
 
     @Override
@@ -46,21 +49,47 @@ public class CloseGroupNodeHandler implements NodeHandler {
             return;
         }
 
-        // 下发指令给所有设备
+        // 注册ACK期望
+        ackManager.registerAck(executionId, nodeIndex, devices);
+
+        // 下发指令给所有设备并处理ACK
         for (DeviceInfo device : devices) {
-            if ("SOLENOID_VALVE".equals(device.getDeviceType())) {
-                // 关闭电磁阀
-                deviceCommandProducer.sendCloseSolenoidValveCommand(executionId, nodeIndex, device, tenantId);
-            } else if ("BALL_VALVE".equals(device.getDeviceType())) {
-                // 球阀归零(角度设为0)
-                deviceCommandProducer.sendBallValveAngleCommand(executionId, nodeIndex, device, 0, tenantId);
+            try {
+                Integer result = null;
+                if ("SOLENOID_VALVE".equals(device.getDeviceType())) {
+                    // 关闭电磁阀
+                    GatewayControlDto dto = DeviceControlHelper.buildCloseSolenoidValveDto(device, tenantId);
+                    result = deviceBusiness.control(dto);
+                    log.info("关闭电磁阀,executionId={}, deviceId={}, result={}",
+                            executionId, device.getDeviceId(), result);
+                } else if ("BALL_VALVE".equals(device.getDeviceType())) {
+                    // 球阀归零(角度设为0)
+                    GatewayControlDto dto = DeviceControlHelper.buildBallValveAngleDto(device, 0, tenantId);
+                    result = deviceBusiness.control(dto);
+                    log.info("球阀归零,executionId={}, deviceId={}, result={}",
+                            executionId, device.getDeviceId(), result);
+                }
+
+                // 根据返回值更新ACK状态
+                if (result != null && result == 1) {
+                    ackManager.updateAckStatus(executionId, nodeIndex, device.getDeviceId(),
+                            AckStatus.SUCCESS, null);
+                    log.info("设备控制成功,executionId={}, deviceId={}", executionId, device.getDeviceId());
+                } else {
+                    ackManager.updateAckStatus(executionId, nodeIndex, device.getDeviceId(),
+                            AckStatus.FAIL, "设备控制返回失败,result=" + result);
+                    log.error("设备控制失败,executionId={}, deviceId={}, result={}",
+                            executionId, device.getDeviceId(), result);
+                }
+            } catch (Exception e) {
+                ackManager.updateAckStatus(executionId, nodeIndex, device.getDeviceId(),
+                        AckStatus.FAIL, "调用设备控制异常: " + e.getMessage());
+                log.error("调用设备控制异常,executionId={}, deviceId={}",
+                        executionId, device.getDeviceId(), e);
             }
         }
 
-        // 注册ACK期望
-        ackManager.registerAck(executionId, nodeIndex, devices);
-
-        log.info("关闭灌区指令已发送,executionId={}, nodeIndex={}, deviceCount={}",
+        log.info("关闭灌区指令处理完成,executionId={}, nodeIndex={}, deviceCount={}",
                 executionId, nodeIndex, devices.size());
     }
 }

+ 47 - 15
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/OpenGroupNodeHandler.java

@@ -1,11 +1,14 @@
 package cn.sciento.farm.automationv2.app.handler.impl;
 
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
 import cn.sciento.farm.automationv2.app.context.ExecutionContext;
+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.NodeType;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
-import cn.sciento.farm.automationv2.infra.mq.producer.DeviceCommandProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -22,7 +25,7 @@ import java.util.List;
 @RequiredArgsConstructor
 public class OpenGroupNodeHandler implements NodeHandler {
 
-    private final DeviceCommandProducer deviceCommandProducer;
+    private final DeviceBusiness deviceBusiness;
     private final AckManager ackManager;
 
     @Override
@@ -49,25 +52,54 @@ public class OpenGroupNodeHandler implements NodeHandler {
         // 获取球阀目标角度(如果有)
         Integer targetAngle = node.getTargetAngle();
 
-        // 下发指令给所有设备
+        // 注册ACK期望
+        ackManager.registerAck(executionId, nodeIndex, devices);
+
+        // 下发指令给所有设备并处理ACK
         for (DeviceInfo device : devices) {
-            if ("SOLENOID_VALVE".equals(device.getDeviceType())) {
-                // 开启电磁阀
-                deviceCommandProducer.sendOpenSolenoidValveCommand(executionId, nodeIndex, device, tenantId);
-            } else if ("BALL_VALVE".equals(device.getDeviceType())) {
-                // 球阀到目标角度
-                if (targetAngle != null) {
-                    deviceCommandProducer.sendBallValveAngleCommand(executionId, nodeIndex, device, targetAngle, tenantId);
+            try {
+                Integer result = null;
+                if ("SOLENOID_VALVE".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())) {
+                    // 球阀到目标角度
+                    if (targetAngle != null) {
+                        GatewayControlDto dto = DeviceControlHelper.buildBallValveAngleDto(device, targetAngle, 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;
+                    }
+                }
+
+                // 根据返回值更新ACK状态
+                if (result != null && result == 1) {
+                    ackManager.updateAckStatus(executionId, nodeIndex, device.getDeviceId(),
+                            AckStatus.SUCCESS, null);
+                    log.info("设备控制成功,executionId={}, deviceId={}", executionId, device.getDeviceId());
                 } else {
-                    log.warn("球阀未配置目标角度,deviceId={}", device.getDeviceId());
+                    ackManager.updateAckStatus(executionId, nodeIndex, device.getDeviceId(),
+                            AckStatus.FAIL, "设备控制返回失败,result=" + result);
+                    log.error("设备控制失败,executionId={}, deviceId={}, result={}",
+                            executionId, device.getDeviceId(), result);
                 }
+            } catch (Exception e) {
+                ackManager.updateAckStatus(executionId, nodeIndex, device.getDeviceId(),
+                        AckStatus.FAIL, "调用设备控制异常: " + e.getMessage());
+                log.error("调用设备控制异常,executionId={}, deviceId={}",
+                        executionId, device.getDeviceId(), e);
             }
         }
 
-        // 注册ACK期望
-        ackManager.registerAck(executionId, nodeIndex, devices);
-
-        log.info("开启灌区指令已发送,executionId={}, nodeIndex={}, deviceCount={}",
+        log.info("开启灌区指令处理完成,executionId={}, nodeIndex={}, deviceCount={}",
                 executionId, nodeIndex, devices.size());
     }
 }

+ 45 - 7
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/SetPumpPressureNodeHandler.java

@@ -1,15 +1,21 @@
 package cn.sciento.farm.automationv2.app.handler.impl;
 
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
 import cn.sciento.farm.automationv2.app.context.ExecutionContext;
+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.NodeType;
+import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
-import cn.sciento.farm.automationv2.infra.mq.producer.DeviceCommandProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.util.Collections;
+
 /**
  * 设置水泵压力节点处理器
  * 功能:设置恒压水泵的目标压力值
@@ -19,7 +25,7 @@ import org.springframework.stereotype.Component;
 @RequiredArgsConstructor
 public class SetPumpPressureNodeHandler implements NodeHandler {
 
-    private final DeviceCommandProducer deviceCommandProducer;
+    private final DeviceBusiness deviceBusiness;
     private final AckManager ackManager;
 
     @Override
@@ -33,7 +39,7 @@ public class SetPumpPressureNodeHandler implements NodeHandler {
         Long executionId = context.getExecutionId();
         Integer nodeIndex = context.getCurrentIndex();
         Long tenantId = context.getTenantId();
-        String pumpId = context.getPumpId();
+        Long pumpId = context.getPumpId();
 
         // 获取目标压力
         Integer targetPressure = node.getTargetPressure();
@@ -45,13 +51,45 @@ public class SetPumpPressureNodeHandler implements NodeHandler {
         log.info("执行设置水泵压力节点,executionId={}, nodeIndex={}, pumpId={}, pressureKpa={}",
                 executionId, nodeIndex, pumpId, targetPressure);
 
-        // 下发设置压力指令
-        deviceCommandProducer.sendSetPumpPressureCommand(executionId, nodeIndex, pumpId, targetPressure, tenantId);
+        // 构造水泵设备信息
+        DeviceInfo pumpDevice = node.getDevices() != null && !node.getDevices().isEmpty()
+                ? node.getDevices().get(0)
+                : DeviceInfo.builder()
+                    .deviceId(pumpId)
+                    .deviceType("PUMP")
+                    .deviceName("水泵")
+                    .build();
 
         // 注册ACK期望
-        ackManager.registerAck(executionId, nodeIndex, node.getDevices());
+        ackManager.registerAck(executionId, nodeIndex, Collections.singletonList(pumpDevice));
+
+        try {
+            // 下发设置压力指令
+            GatewayControlDto dto = DeviceControlHelper.buildSetPumpPressureDto(pumpDevice, targetPressure, tenantId);
+            Integer result = deviceBusiness.control(dto);
+
+            log.info("设置水泵压力,executionId={}, pumpId={}, pressureKpa={}, result={}",
+                    executionId, pumpId, targetPressure, result);
+
+            // 根据返回值更新ACK状态
+            if (result != null && result == 1) {
+                ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                        AckStatus.SUCCESS, null);
+                log.info("设置水泵压力成功,executionId={}, pumpId={}, pressureKpa={}",
+                        executionId, pumpId, targetPressure);
+            } else {
+                ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                        AckStatus.FAIL, "设置水泵压力返回失败,result=" + result);
+                log.error("设置水泵压力失败,executionId={}, pumpId={}, result={}",
+                        executionId, pumpId, result);
+            }
+        } catch (Exception e) {
+            ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                    AckStatus.FAIL, "调用设置水泵压力异常: " + e.getMessage());
+            log.error("调用设置水泵压力异常,executionId={}, pumpId={}", executionId, pumpId, e);
+        }
 
-        log.info("设置水泵压力指令已发送,executionId={}, nodeIndex={}, pressureKpa={}",
+        log.info("设置水泵压力指令处理完成,executionId={}, nodeIndex={}, pressureKpa={}",
                 executionId, nodeIndex, targetPressure);
     }
 }

+ 45 - 7
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StartFertilizerNodeHandler.java

@@ -1,15 +1,21 @@
 package cn.sciento.farm.automationv2.app.handler.impl;
 
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
 import cn.sciento.farm.automationv2.app.context.ExecutionContext;
+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.NodeType;
+import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
-import cn.sciento.farm.automationv2.infra.mq.producer.DeviceCommandProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.util.Collections;
+
 /**
  * 启动施肥机节点处理器
  * 功能:启动施肥机并下发程序编号
@@ -19,7 +25,7 @@ import org.springframework.stereotype.Component;
 @RequiredArgsConstructor
 public class StartFertilizerNodeHandler implements NodeHandler {
 
-    private final DeviceCommandProducer deviceCommandProducer;
+    private final DeviceBusiness deviceBusiness;
     private final AckManager ackManager;
 
     @Override
@@ -33,7 +39,7 @@ public class StartFertilizerNodeHandler implements NodeHandler {
         Long executionId = context.getExecutionId();
         Integer nodeIndex = context.getCurrentIndex();
         Long tenantId = context.getTenantId();
-        String fertilizerId = context.getFertilizerPumpId();
+        Long fertilizerId = context.getFertilizerPumpId();
 
         // 获取施肥程序编号
         Object programNoObj = node.getParams().get("programNo");
@@ -52,13 +58,45 @@ public class StartFertilizerNodeHandler implements NodeHandler {
         log.info("执行启动施肥机节点,executionId={}, nodeIndex={}, fertilizerId={}, programNo={}",
                 executionId, nodeIndex, fertilizerId, programNo);
 
-        // 下发启动施肥机指令
-        deviceCommandProducer.sendStartFertilizerCommand(executionId, nodeIndex, fertilizerId, programNo, tenantId);
+        // 构造施肥机设备信息
+        DeviceInfo fertilizerDevice = node.getDevices() != null && !node.getDevices().isEmpty()
+                ? node.getDevices().get(0)
+                : DeviceInfo.builder()
+                    .deviceId(fertilizerId)
+                    .deviceType("FERTILIZER")
+                    .deviceName("施肥机")
+                    .build();
 
         // 注册ACK期望
-        ackManager.registerAck(executionId, nodeIndex, node.getDevices());
+        ackManager.registerAck(executionId, nodeIndex, Collections.singletonList(fertilizerDevice));
+
+        try {
+            // 下发启动施肥机指令
+            GatewayControlDto dto = DeviceControlHelper.buildStartFertilizerDto(fertilizerDevice, programNo, tenantId);
+            Integer result = deviceBusiness.control(dto);
+
+            log.info("启动施肥机,executionId={}, fertilizerId={}, programNo={}, result={}",
+                    executionId, fertilizerId, programNo, result);
+
+            // 根据返回值更新ACK状态
+            if (result != null && result == 1) {
+                ackManager.updateAckStatus(executionId, nodeIndex, fertilizerId,
+                        AckStatus.SUCCESS, null);
+                log.info("启动施肥机成功,executionId={}, fertilizerId={}, programNo={}",
+                        executionId, fertilizerId, programNo);
+            } else {
+                ackManager.updateAckStatus(executionId, nodeIndex, fertilizerId,
+                        AckStatus.FAIL, "启动施肥机返回失败,result=" + result);
+                log.error("启动施肥机失败,executionId={}, fertilizerId={}, result={}",
+                        executionId, fertilizerId, result);
+            }
+        } catch (Exception e) {
+            ackManager.updateAckStatus(executionId, nodeIndex, fertilizerId,
+                    AckStatus.FAIL, "调用启动施肥机异常: " + e.getMessage());
+            log.error("调用启动施肥机异常,executionId={}, fertilizerId={}", executionId, fertilizerId, e);
+        }
 
-        log.info("启动施肥机指令已发送,executionId={}, nodeIndex={}, programNo={}",
+        log.info("启动施肥机指令处理完成,executionId={}, nodeIndex={}, programNo={}",
                 executionId, nodeIndex, programNo);
     }
 }

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

@@ -1,15 +1,21 @@
 package cn.sciento.farm.automationv2.app.handler.impl;
 
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
 import cn.sciento.farm.automationv2.app.context.ExecutionContext;
+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.NodeType;
+import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
-import cn.sciento.farm.automationv2.infra.mq.producer.DeviceCommandProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.util.Collections;
+
 /**
  * 启动水泵节点处理器
  */
@@ -18,7 +24,7 @@ import org.springframework.stereotype.Component;
 @RequiredArgsConstructor
 public class StartPumpNodeHandler implements NodeHandler {
 
-    private final DeviceCommandProducer deviceCommandProducer;
+    private final DeviceBusiness deviceBusiness;
     private final AckManager ackManager;
 
     @Override
@@ -32,18 +38,48 @@ public class StartPumpNodeHandler implements NodeHandler {
         Long executionId = context.getExecutionId();
         Integer nodeIndex = context.getCurrentIndex();
         Long tenantId = context.getTenantId();
-        String pumpId = context.getPumpId();
+        Long pumpId = context.getPumpId();
 
         log.info("执行启动水泵节点,executionId={}, nodeIndex={}, pumpId={}",
                 executionId, nodeIndex, pumpId);
 
-        // 下发启动水泵指令
-        deviceCommandProducer.sendStartPumpCommand(executionId, nodeIndex, pumpId, tenantId);
+        // 构造水泵设备信息
+        DeviceInfo pumpDevice = node.getDevices() != null && !node.getDevices().isEmpty()
+                ? node.getDevices().get(0)
+                : DeviceInfo.builder()
+                    .deviceId(pumpId)
+                    .deviceType("PUMP")
+                    .deviceName("水泵")
+                    .build();
 
         // 注册ACK期望
-        ackManager.registerAck(executionId, nodeIndex, node.getDevices());
+        ackManager.registerAck(executionId, nodeIndex, Collections.singletonList(pumpDevice));
+
+        try {
+            // 下发启动水泵指令
+            GatewayControlDto dto = DeviceControlHelper.buildStartPumpDto(pumpDevice, tenantId);
+            Integer result = deviceBusiness.control(dto);
+
+            log.info("启动水泵,executionId={}, pumpId={}, result={}", executionId, pumpId, result);
+
+            // 根据返回值更新ACK状态
+            if (result != null && result == 1) {
+                ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                        AckStatus.SUCCESS, null);
+                log.info("启动水泵成功,executionId={}, pumpId={}", executionId, pumpId);
+            } else {
+                ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                        AckStatus.FAIL, "启动水泵返回失败,result=" + result);
+                log.error("启动水泵失败,executionId={}, pumpId={}, result={}",
+                        executionId, pumpId, result);
+            }
+        } catch (Exception e) {
+            ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                    AckStatus.FAIL, "调用启动水泵异常: " + e.getMessage());
+            log.error("调用启动水泵异常,executionId={}, pumpId={}", executionId, pumpId, e);
+        }
 
-        log.info("启动水泵指令已发送,executionId={}, nodeIndex={}, pumpId={}",
+        log.info("启动水泵指令处理完成,executionId={}, nodeIndex={}, pumpId={}",
                 executionId, nodeIndex, pumpId);
     }
 }

+ 44 - 7
src/main/java/cn/sciento/farm/automationv2/app/handler/impl/StopFertilizerNodeHandler.java

@@ -1,15 +1,21 @@
 package cn.sciento.farm.automationv2.app.handler.impl;
 
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
 import cn.sciento.farm.automationv2.app.context.ExecutionContext;
+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.NodeType;
+import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
-import cn.sciento.farm.automationv2.infra.mq.producer.DeviceCommandProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.util.Collections;
+
 /**
  * 关闭施肥机节点处理器
  */
@@ -18,7 +24,7 @@ import org.springframework.stereotype.Component;
 @RequiredArgsConstructor
 public class StopFertilizerNodeHandler implements NodeHandler {
 
-    private final DeviceCommandProducer deviceCommandProducer;
+    private final DeviceBusiness deviceBusiness;
     private final AckManager ackManager;
 
     @Override
@@ -32,18 +38,49 @@ public class StopFertilizerNodeHandler implements NodeHandler {
         Long executionId = context.getExecutionId();
         Integer nodeIndex = context.getCurrentIndex();
         Long tenantId = context.getTenantId();
-        String fertilizerId = context.getFertilizerPumpId();
+        Long fertilizerId = context.getFertilizerPumpId();
 
         log.info("执行关闭施肥机节点,executionId={}, nodeIndex={}, fertilizerId={}",
                 executionId, nodeIndex, fertilizerId);
 
-        // 下发关闭施肥机指令
-        deviceCommandProducer.sendStopFertilizerCommand(executionId, nodeIndex, fertilizerId, tenantId);
+        // 构造施肥机设备信息
+        DeviceInfo fertilizerDevice = node.getDevices() != null && !node.getDevices().isEmpty()
+                ? node.getDevices().get(0)
+                : DeviceInfo.builder()
+                    .deviceId(fertilizerId)
+                    .deviceType("FERTILIZER")
+                    .deviceName("施肥机")
+                    .build();
 
         // 注册ACK期望
-        ackManager.registerAck(executionId, nodeIndex, node.getDevices());
+        ackManager.registerAck(executionId, nodeIndex, Collections.singletonList(fertilizerDevice));
+
+        try {
+            // 下发关闭施肥机指令
+            GatewayControlDto dto = DeviceControlHelper.buildStopFertilizerDto(fertilizerDevice, tenantId);
+            Integer result = deviceBusiness.control(dto);
+
+            log.info("关闭施肥机,executionId={}, fertilizerId={}, result={}",
+                    executionId, fertilizerId, result);
+
+            // 根据返回值更新ACK状态
+            if (result != null && result == 1) {
+                ackManager.updateAckStatus(executionId, nodeIndex, fertilizerId,
+                        AckStatus.SUCCESS, null);
+                log.info("关闭施肥机成功,executionId={}, fertilizerId={}", executionId, fertilizerId);
+            } else {
+                ackManager.updateAckStatus(executionId, nodeIndex, fertilizerId,
+                        AckStatus.FAIL, "关闭施肥机返回失败,result=" + result);
+                log.error("关闭施肥机失败,executionId={}, fertilizerId={}, result={}",
+                        executionId, fertilizerId, result);
+            }
+        } catch (Exception e) {
+            ackManager.updateAckStatus(executionId, nodeIndex, fertilizerId,
+                    AckStatus.FAIL, "调用关闭施肥机异常: " + e.getMessage());
+            log.error("调用关闭施肥机异常,executionId={}, fertilizerId={}", executionId, fertilizerId, e);
+        }
 
-        log.info("关闭施肥机指令已发送,executionId={}, nodeIndex={}, fertilizerId={}",
+        log.info("关闭施肥机指令处理完成,executionId={}, nodeIndex={}, fertilizerId={}",
                 executionId, nodeIndex, fertilizerId);
     }
 }

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

@@ -1,15 +1,21 @@
 package cn.sciento.farm.automationv2.app.handler.impl;
 
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
 import cn.sciento.farm.automationv2.app.context.ExecutionContext;
+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.NodeType;
+import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
-import cn.sciento.farm.automationv2.infra.mq.producer.DeviceCommandProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.util.Collections;
+
 /**
  * 关闭水泵节点处理器
  */
@@ -18,7 +24,7 @@ import org.springframework.stereotype.Component;
 @RequiredArgsConstructor
 public class StopPumpNodeHandler implements NodeHandler {
 
-    private final DeviceCommandProducer deviceCommandProducer;
+    private final DeviceBusiness deviceBusiness;
     private final AckManager ackManager;
 
     @Override
@@ -32,18 +38,48 @@ public class StopPumpNodeHandler implements NodeHandler {
         Long executionId = context.getExecutionId();
         Integer nodeIndex = context.getCurrentIndex();
         Long tenantId = context.getTenantId();
-        String pumpId = context.getPumpId();
+        Long pumpId = context.getPumpId();
 
         log.info("执行关闭水泵节点,executionId={}, nodeIndex={}, pumpId={}",
                 executionId, nodeIndex, pumpId);
 
-        // 下发关闭水泵指令
-        deviceCommandProducer.sendStopPumpCommand(executionId, nodeIndex, pumpId, tenantId);
+        // 构造水泵设备信息
+        DeviceInfo pumpDevice = node.getDevices() != null && !node.getDevices().isEmpty()
+                ? node.getDevices().get(0)
+                : DeviceInfo.builder()
+                    .deviceId(pumpId)
+                    .deviceType("PUMP")
+                    .deviceName("水泵")
+                    .build();
 
         // 注册ACK期望
-        ackManager.registerAck(executionId, nodeIndex, node.getDevices());
+        ackManager.registerAck(executionId, nodeIndex, Collections.singletonList(pumpDevice));
+
+        try {
+            // 下发关闭水泵指令
+            GatewayControlDto dto = DeviceControlHelper.buildStopPumpDto(pumpDevice, tenantId);
+            Integer result = deviceBusiness.control(dto);
+
+            log.info("关闭水泵,executionId={}, pumpId={}, result={}", executionId, pumpId, result);
+
+            // 根据返回值更新ACK状态
+            if (result != null && result == 1) {
+                ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                        AckStatus.SUCCESS, null);
+                log.info("关闭水泵成功,executionId={}, pumpId={}", executionId, pumpId);
+            } else {
+                ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                        AckStatus.FAIL, "关闭水泵返回失败,result=" + result);
+                log.error("关闭水泵失败,executionId={}, pumpId={}, result={}",
+                        executionId, pumpId, result);
+            }
+        } catch (Exception e) {
+            ackManager.updateAckStatus(executionId, nodeIndex, pumpId,
+                    AckStatus.FAIL, "调用关闭水泵异常: " + e.getMessage());
+            log.error("调用关闭水泵异常,executionId={}, pumpId={}", executionId, pumpId, e);
+        }
 
-        log.info("关闭水泵指令已发送,executionId={}, nodeIndex={}, pumpId={}",
+        log.info("关闭水泵指令处理完成,executionId={}, nodeIndex={}, pumpId={}",
                 executionId, nodeIndex, pumpId);
     }
 }

+ 1 - 1
src/main/java/cn/sciento/farm/automationv2/app/job/IrrigationScheduledJob.java

@@ -54,7 +54,7 @@ public class IrrigationScheduledJob extends QuartzJobBean {
 
         try {
             // 触发任务(发送MQ消息后立即返回)
-            Long executionId = taskTriggerService.scheduledTrigger(taskId, ruleId);
+            Long executionId = taskTriggerService.scheduledTrigger(taskId);
 
             log.info("定时规则触发成功,taskId={}, ruleId={}, executionId={}, nextFireTime={}",
                     taskId, ruleId, executionId, context.getNextFireTime());

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

@@ -3,9 +3,13 @@ package cn.sciento.farm.automationv2.app.service;
 import cn.sciento.farm.automationv2.domain.entity.AlarmRecord;
 import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
 import cn.sciento.farm.automationv2.domain.enums.AlarmType;
+import cn.sciento.farm.automationv2.domain.repository.AlarmRecordRepository;
+import cn.sciento.farm.automationv2.domain.repository.TaskExecutionRepository;
 import cn.sciento.farm.automationv2.domain.valueobject.DeviceInfo;
+import cn.sciento.farm.automationv2.domain.valueobject.ExecutionPlan;
 import cn.sciento.farm.automationv2.infra.mapper.AlarmRecordMapper;
 import cn.sciento.farm.automationv2.infra.mapper.TaskExecutionMapper;
+import cn.sciento.farm.automationv2.infra.redis.ExecutionPlanStore;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -25,8 +29,9 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public class AlarmRecordService {
 
-    private final AlarmRecordMapper alarmRecordMapper;
-    private final TaskExecutionMapper taskExecutionMapper;
+    private final AlarmRecordRepository alarmRecordRepository;
+    private final TaskExecutionRepository taskExecutionRepository;
+    private final ExecutionPlanStore executionPlanStore;
 
     /**
      * 创建任务执行失败报警记录
@@ -40,7 +45,7 @@ public class AlarmRecordService {
         log.info("创建任务失败报警记录,executionId={}, failReason={}", executionId, failReason);
 
         // 加载执行实例
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             log.error("执行实例不存在,executionId={}", executionId);
             return null;
@@ -68,7 +73,7 @@ public class AlarmRecordService {
                 .build();
 
         // 持久化报警记录
-        alarmRecordMapper.insert(alarm);
+        alarmRecordRepository.insertSelective(alarm);
 
         log.info("报警记录创建成功,alarmId={}, executionId={}, failedDeviceCount={}",
                 alarm.getId(), executionId, failedDevices.size());
@@ -89,7 +94,7 @@ public class AlarmRecordService {
         log.info("创建设备超时报警记录,executionId={}, nodeIndex={}, deviceCount={}",
                 executionId, nodeIndex, deviceIds.size());
 
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             return null;
         }
@@ -109,7 +114,7 @@ public class AlarmRecordService {
                 .handled(false)
                 .build();
 
-        alarmRecordMapper.insert(alarm);
+        alarmRecordRepository.insertSelective(alarm);
 
         log.info("设备超时报警记录创建成功,alarmId={}, executionId={}",
                 alarm.getId(), executionId);
@@ -126,7 +131,7 @@ public class AlarmRecordService {
      */
     @Transactional(rollbackFor = Exception.class)
     public void markAsHandled(Long alarmId, Long handledBy, String remark) {
-        AlarmRecord alarm = alarmRecordMapper.selectById(alarmId);
+        AlarmRecord alarm = alarmRecordRepository.selectByPrimaryKey(alarmId);
         if (alarm == null) {
             log.warn("报警记录不存在,alarmId={}", alarmId);
             return;
@@ -137,7 +142,7 @@ public class AlarmRecordService {
         alarm.setHandledAt(LocalDateTime.now());
         alarm.setHandleRemark(remark);
 
-        alarmRecordMapper.updateById(alarm);
+        alarmRecordRepository.updateByPrimaryKeySelective(alarm);
 
         log.info("报警记录已标记为处理,alarmId={}, handledBy={}", alarmId, handledBy);
     }
@@ -146,15 +151,16 @@ public class AlarmRecordService {
      * 从执行计划中提取失败设备
      */
     private List<String> extractFailedDevices(TaskExecution execution) {
-        if (execution.getExecutionPlan() == null) {
+        ExecutionPlan plan = executionPlanStore.get(execution.getId());
+        if (plan == null) {
             return Collections.emptyList();
         }
 
-        return execution.getExecutionPlan().getNodes().stream()
+        return plan.getNodes().stream()
                 .flatMap(node -> node.getDevices().stream())
                 .filter(device -> "FAIL".equals(device.getAckStatus()) ||
                         "TIMEOUT".equals(device.getAckStatus()))
-                .map(DeviceInfo::getDeviceId)
+                .map(DeviceInfo::getDeviceName)
                 .distinct()
                 .collect(Collectors.toList());
     }
@@ -163,14 +169,15 @@ public class AlarmRecordService {
      * 从执行计划中提取成功设备
      */
     private List<String> extractSuccessDevices(TaskExecution execution) {
-        if (execution.getExecutionPlan() == null) {
+        ExecutionPlan plan = executionPlanStore.get(execution.getId());
+        if (plan == null) {
             return Collections.emptyList();
         }
 
-        return execution.getExecutionPlan().getNodes().stream()
+        return plan.getNodes().stream()
                 .flatMap(node -> node.getDevices().stream())
                 .filter(device -> "SUCCESS".equals(device.getAckStatus()))
-                .map(DeviceInfo::getDeviceId)
+                .map(DeviceInfo::getDeviceName)
                 .distinct()
                 .collect(Collectors.toList());
     }
@@ -179,20 +186,22 @@ public class AlarmRecordService {
      * 查询报警记录
      */
     public AlarmRecord getAlarmById(Long alarmId) {
-        return alarmRecordMapper.selectById(alarmId);
+        return alarmRecordRepository.selectByPrimaryKey(alarmId);
     }
 
     /**
      * 分页查询未处理报警
      */
     public List<AlarmRecord> listUnhandledAlarms(Long tenantId, Integer limit) {
-        return alarmRecordMapper.selectUnhandledAlarms(tenantId, limit);
+//        return alarmRecordMapper.selectUnhandledAlarms(tenantId, limit);
+        return null;
     }
 
     /**
      * 分页查询所有报警
      */
     public List<AlarmRecord> listAlarms(Long tenantId, Integer offset, Integer limit) {
-        return alarmRecordMapper.selectByTenant(tenantId, offset, limit);
+//        return alarmRecordMapper.selectByTenant(tenantId, offset, limit);
+        return null;
     }
 }

+ 26 - 8
src/main/java/cn/sciento/farm/automationv2/app/service/RetryManager.java

@@ -1,10 +1,13 @@
 package cn.sciento.farm.automationv2.app.service;
 
 import cn.sciento.farm.automationv2.domain.entity.TaskExecution;
+import cn.sciento.farm.automationv2.domain.repository.TaskExecutionRepository;
 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.infra.mq.producer.FlowControlProducer;
 import cn.sciento.farm.automationv2.infra.mapper.TaskExecutionMapper;
+import cn.sciento.farm.automationv2.infra.redis.ExecutionPlanStore;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -26,13 +29,14 @@ import java.util.List;
 @RequiredArgsConstructor
 public class RetryManager {
 
-    private final TaskExecutionMapper taskExecutionMapper;
+    private final TaskExecutionRepository taskExecutionRepository;
     private final FlowControlProducer flowControlProducer;
+    private final ExecutionPlanStore executionPlanStore;
 
     /**
      * 最大重试次数
      */
-    private static final int MAX_RETRY_COUNT = 3;
+    private static final int MAX_RETRY_COUNT = 2;
 
     /**
      * 重试延迟(秒):第1次0秒,第2次5秒,第3次15秒
@@ -49,14 +53,20 @@ public class RetryManager {
      */
     public boolean retry(Long executionId, Integer nodeIndex, List<String> failedDevices) {
         // 加载执行实例
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             log.error("执行实例不存在,executionId={}", executionId);
             return false;
         }
 
         // 获取当前节点
-        ExecutionNode node = execution.getExecutionPlan().getNode(nodeIndex);
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan == null) {
+            log.error("执行计划不存在,executionId={}", executionId);
+            return false;
+        }
+
+        ExecutionNode node = plan.getNode(nodeIndex);
         if (node == null) {
             log.error("节点不存在,executionId={}, nodeIndex={}", executionId, nodeIndex);
             return false;
@@ -82,13 +92,16 @@ public class RetryManager {
         }
 
         // 持久化更新
-        int updated = taskExecutionMapper.updateByVersion(execution);
+        int updated = taskExecutionRepository.updateByPrimaryKeySelective(execution);
         if (updated == 0) {
             log.error("更新执行实例失败(乐观锁冲突),executionId={}, version={}",
-                    executionId, execution.getVersion());
+                    executionId, execution.getObjectVersionNumber());
             return false;
         }
 
+        // 更新执行计划到Redis
+        executionPlanStore.save(executionId, plan);
+
         log.info("触发设备重试,executionId={}, nodeIndex={}, retryCount={}/{}, failedDeviceCount={}",
                 executionId, nodeIndex, node.getRetryCount(), MAX_RETRY_COUNT, failedDevices.size());
 
@@ -108,12 +121,17 @@ public class RetryManager {
      * 检查节点是否已达重试上限
      */
     public boolean hasReachedMaxRetry(Long executionId, Integer nodeIndex) {
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             return true;
         }
 
-        ExecutionNode node = execution.getExecutionPlan().getNode(nodeIndex);
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan == null) {
+            return true;
+        }
+
+        ExecutionNode node = plan.getNode(nodeIndex);
         if (node == null) {
             return true;
         }

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

@@ -1,12 +1,16 @@
 package cn.sciento.farm.automationv2.app.service;
 
+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.enums.AckStatus;
+import cn.sciento.farm.automationv2.domain.repository.TaskExecutionRepository;
 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.infra.mq.producer.DeviceCommandProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
+import cn.sciento.farm.automationv2.infra.redis.ExecutionPlanStore;
 import cn.sciento.farm.automationv2.infra.mapper.TaskExecutionMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -23,29 +27,16 @@ import java.util.stream.Collectors;
  * 1. 关闭施肥机
  * 2. 关闭水泵
  * 3. 关闭所有电磁阀和球阀
- *
- * 超时策略:
- * - 每个设备等待ACK最多30秒
- * - 设备关闭失败不中断流程,继续关闭其他设备
  */
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class SafeShutdownService {
 
-    private final TaskExecutionMapper taskExecutionMapper;
-    private final DeviceCommandProducer deviceCommandProducer;
+    private final TaskExecutionRepository taskExecutionRepository;
+    private final DeviceBusiness deviceBusiness;
     private final AckManager ackManager;
-
-    /**
-     * ACK超时时间(毫秒)
-     */
-    private static final long ACK_TIMEOUT_MS = 30000; // 30秒
-
-    /**
-     * ACK检查间隔(毫秒)
-     */
-    private static final long ACK_CHECK_INTERVAL_MS = 1000; // 1秒
+    private final ExecutionPlanStore executionPlanStore;
 
     /**
      * 安全关闭所有已开启的设备
@@ -57,17 +48,21 @@ public class SafeShutdownService {
         log.info("开始安全关闭设备,executionId={}", executionId);
 
         // 加载执行实例
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             log.error("执行实例不存在,executionId={}", executionId);
             return ShutdownResult.error("执行实例不存在");
         }
 
         // 提取已开启的设备
-        OpenedDevices openedDevices = extractOpenedDevices(execution.getExecutionPlan());
+        OpenedDevices openedDevices = extractOpenedDevices(executionId);
+        if (openedDevices == null) {
+            log.error("无法提取已开启设备,executionId={}", executionId);
+            return ShutdownResult.error("执行计划不存在");
+        }
 
-        List<String> failedDevices = new ArrayList<>();
-        List<String> successDevices = new ArrayList<>();
+        List<Long> failedDevices = new ArrayList<>();
+        List<Long> successDevices = new ArrayList<>();
 
         // 步骤1: 关闭施肥机
         if (openedDevices.fertilizer != null) {
@@ -127,52 +122,57 @@ public class SafeShutdownService {
      * @return true-成功,false-失败
      */
     private boolean closeDevice(Long executionId, DeviceInfo device, Long tenantId) {
-        String deviceId = device.getDeviceId();
+        Long deviceId = device.getDeviceId();
         String deviceType = device.getDeviceType();
 
         try {
-            // 下发关闭指令
+            // 注册ACK期望
+            ackManager.registerAck(executionId, -1, Collections.singletonList(device));
+
+            Integer result = null;
+            GatewayControlDto dto = null;
+
+            // 根据设备类型构造控制DTO并调用Feign
             if ("PUMP".equals(deviceType)) {
-                deviceCommandProducer.sendStopPumpCommand(executionId, -1, deviceId, tenantId);
+                dto = DeviceControlHelper.buildStopPumpDto(device, tenantId);
+                result = deviceBusiness.control(dto);
+                log.info("关闭水泵,executionId={}, deviceId={}, result={}", executionId, deviceId, result);
             } else if ("FERTILIZER".equals(deviceType)) {
-                deviceCommandProducer.sendStopFertilizerCommand(executionId, -1, deviceId, tenantId);
+                dto = DeviceControlHelper.buildStopFertilizerDto(device, tenantId);
+                result = deviceBusiness.control(dto);
+                log.info("关闭施肥机,executionId={}, deviceId={}, result={}", executionId, deviceId, result);
             } else if ("SOLENOID_VALVE".equals(deviceType)) {
-                deviceCommandProducer.sendCloseSolenoidValveCommand(executionId, -1, device, tenantId);
+                dto = DeviceControlHelper.buildCloseSolenoidValveDto(device, tenantId);
+                result = deviceBusiness.control(dto);
+                log.info("关闭电磁阀,executionId={}, deviceId={}, result={}", executionId, deviceId, result);
             } else if ("BALL_VALVE".equals(deviceType)) {
                 // 球阀归零(角度=0)
-                deviceCommandProducer.sendBallValveAngleCommand(executionId, -1, device, 0, tenantId);
+                dto = DeviceControlHelper.buildBallValveAngleDto(device, 0, tenantId);
+                result = deviceBusiness.control(dto);
+                log.info("球阀归零,executionId={}, deviceId={}, result={}", executionId, deviceId, result);
             } else {
                 log.warn("未知设备类型,deviceId={}, deviceType={}", deviceId, deviceType);
+                ackManager.updateAckStatus(executionId, -1, deviceId,
+                        AckStatus.FAIL, "未知设备类型: " + deviceType);
                 return false;
             }
 
-            // 注册ACK期望
-            ackManager.registerAck(executionId, -1, Collections.singletonList(device));
-
-            // 等待ACK响应(最多30秒)
-            long startTime = System.currentTimeMillis();
-            while (System.currentTimeMillis() - startTime < ACK_TIMEOUT_MS) {
-                Map<String, AckStatus> ackMap = ackManager.checkAckStatus(executionId, -1,
-                        Collections.singletonList(device));
-
-                AckStatus status = ackMap.get(deviceId);
-                if (status == AckStatus.SUCCESS) {
-                    log.info("设备关闭成功,deviceId={}", deviceId);
-                    return true;
-                } else if (status == AckStatus.FAIL || status == AckStatus.TIMEOUT) {
-                    log.error("设备关闭失败,deviceId={}, status={}", deviceId, status);
-                    return false;
-                }
-
-                // 继续等待
-                Thread.sleep(ACK_CHECK_INTERVAL_MS);
+            // 根据返回值更新ACK状态
+            if (result != null && result == 1) {
+                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;
             }
 
-            // 超时
-            log.error("设备关闭超时,deviceId={}", deviceId);
-            return false;
-
         } catch (Exception e) {
+            ackManager.updateAckStatus(executionId, -1, deviceId,
+                    AckStatus.FAIL, "调用设备控制异常: " + e.getMessage());
             log.error("关闭设备异常,deviceId={}", deviceId, e);
             return false;
         }
@@ -181,7 +181,14 @@ public class SafeShutdownService {
     /**
      * 从执行计划中提取已开启的设备
      */
-    private OpenedDevices extractOpenedDevices(ExecutionPlan plan) {
+    private OpenedDevices extractOpenedDevices(Long executionId) {
+        // 从Redis获取执行计划
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan == null) {
+            log.error("执行计划不存在,executionId={}", executionId);
+            return null;
+        }
+
         OpenedDevices result = new OpenedDevices();
 
         for (ExecutionNode node : plan.getSuccessNodes()) {
@@ -246,23 +253,23 @@ public class SafeShutdownService {
      */
     public static class ShutdownResult {
         private final boolean success;
-        private final List<String> successDevices;
-        private final List<String> failedDevices;
+        private final List<Long> successDevices;
+        private final List<Long> failedDevices;
         private final String errorMessage;
 
-        private ShutdownResult(boolean success, List<String> successDevices,
-                               List<String> failedDevices, String errorMessage) {
+        private ShutdownResult(boolean success, List<Long> successDevices,
+                               List<Long> failedDevices, String errorMessage) {
             this.success = success;
             this.successDevices = successDevices;
             this.failedDevices = failedDevices;
             this.errorMessage = errorMessage;
         }
 
-        public static ShutdownResult success(List<String> successDevices) {
+        public static ShutdownResult success(List<Long> successDevices) {
             return new ShutdownResult(true, successDevices, Collections.emptyList(), null);
         }
 
-        public static ShutdownResult partial(List<String> successDevices, List<String> failedDevices) {
+        public static ShutdownResult partial(List<Long> successDevices, List<Long> failedDevices) {
             return new ShutdownResult(false, successDevices, failedDevices, "部分设备关闭失败");
         }
 
@@ -275,11 +282,11 @@ public class SafeShutdownService {
             return success;
         }
 
-        public List<String> getSuccessDevices() {
+        public List<Long> getSuccessDevices() {
             return successDevices;
         }
 
-        public List<String> getFailedDevices() {
+        public List<Long> getFailedDevices() {
             return failedDevices;
         }
 

+ 69 - 25
src/main/java/cn/sciento/farm/automationv2/app/service/TaskExecutionEngine.java

@@ -9,10 +9,14 @@ import cn.sciento.farm.automationv2.domain.enums.AckStatus;
 import cn.sciento.farm.automationv2.domain.enums.ExecutionStatus;
 import cn.sciento.farm.automationv2.domain.enums.NodeStatus;
 import cn.sciento.farm.automationv2.domain.enums.NodeType;
+import cn.sciento.farm.automationv2.domain.repository.IrrigationTaskRepository;
+import cn.sciento.farm.automationv2.domain.repository.TaskExecutionRepository;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionNode;
+import cn.sciento.farm.automationv2.domain.valueobject.ExecutionPlan;
 import cn.sciento.farm.automationv2.infra.mq.producer.FlowControlProducer;
 import cn.sciento.farm.automationv2.infra.redis.AckManager;
 import cn.sciento.farm.automationv2.infra.redis.IdempotencyManager;
+import cn.sciento.farm.automationv2.infra.redis.ExecutionPlanStore;
 import cn.sciento.farm.automationv2.infra.mapper.IrrigationTaskMapper;
 import cn.sciento.farm.automationv2.infra.mapper.TaskExecutionMapper;
 import lombok.RequiredArgsConstructor;
@@ -41,8 +45,8 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public class TaskExecutionEngine {
 
-    private final TaskExecutionMapper taskExecutionMapper;
-    private final IrrigationTaskMapper irrigationTaskMapper;
+    private final TaskExecutionRepository taskExecutionRepository;
+    private final IrrigationTaskRepository irrigationTaskRepository;
     private final NodeHandlerFactory nodeHandlerFactory;
     private final FlowControlProducer flowControlProducer;
     private final AckManager ackManager;
@@ -51,6 +55,7 @@ public class TaskExecutionEngine {
     private final SafeShutdownService safeShutdownService;
     private final AlarmRecordService alarmRecordService;
     private final AlarmNotificationService alarmNotificationService;
+    private final ExecutionPlanStore executionPlanStore;
 
     /**
      * 执行当前节点
@@ -62,7 +67,7 @@ public class TaskExecutionEngine {
         log.info("开始执行当前节点,executionId={}", executionId);
 
         // 加载执行实例
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             log.error("执行实例不存在,executionId={}", executionId);
             return;
@@ -76,7 +81,14 @@ public class TaskExecutionEngine {
         }
 
         // 获取当前节点
-        ExecutionNode currentNode = execution.getCurrentNode();
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan == null) {
+            log.error("执行计划不存在,executionId={}", executionId);
+            handleNodeFailure(executionId, execution.getCurrentIndex(), "执行计划不存在");
+            return;
+        }
+
+        ExecutionNode currentNode = plan.getNode(execution.getCurrentIndex());
         if (currentNode == null) {
             log.error("当前节点不存在,executionId={}, currentIndex={}",
                     executionId, execution.getCurrentIndex());
@@ -97,15 +109,18 @@ public class TaskExecutionEngine {
             execution.setStatus(ExecutionStatus.RUNNING);
             execution.updateHeartbeat();
 
-            int updated = taskExecutionMapper.updateByVersion(execution);
+            int updated = taskExecutionRepository.updateByPrimaryKeySelective(execution);
             if (updated == 0) {
                 log.error("更新执行实例失败(乐观锁冲突),executionId={}, version={}",
-                        executionId, execution.getVersion());
+                        executionId, execution.getObjectVersionNumber());
                 return;
             }
 
+            // 更新执行计划到Redis
+            executionPlanStore.save(executionId, plan);
+
             // 构建执行上下文
-            IrrigationTask task = irrigationTaskMapper.selectById(execution.getTaskId());
+            IrrigationTask task = irrigationTaskRepository.selectByPrimaryKey(execution.getTaskId());
             ExecutionContext context = ExecutionContext.builder()
                     .execution(execution)
                     .currentNode(currentNode)
@@ -137,7 +152,10 @@ public class TaskExecutionEngine {
                 // 更新节点状态为SUCCESS
                 currentNode.setStatus(NodeStatus.SUCCESS.name());
                 currentNode.setFinishedAt(LocalDateTime.now());
-                taskExecutionMapper.updateByVersion(execution);
+                taskExecutionRepository.updateByPrimaryKeySelective(execution);
+
+                // 更新执行计划到Redis
+                executionPlanStore.save(executionId, plan);
 
                 // 标记推进(幂等)
                 idempotencyManager.markProceed(executionId, nodeIndex);
@@ -166,7 +184,7 @@ public class TaskExecutionEngine {
         log.info("检查ACK状态,executionId={}, nodeIndex={}", executionId, nodeIndex);
 
         // 加载执行实例
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             log.error("执行实例不存在,executionId={}", executionId);
             return;
@@ -180,7 +198,13 @@ public class TaskExecutionEngine {
         }
 
         // 获取节点
-        ExecutionNode node = execution.getExecutionPlan().getNode(nodeIndex);
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan == null) {
+            log.error("执行计划不存在,executionId={}", executionId);
+            return;
+        }
+
+        ExecutionNode node = plan.getNode(nodeIndex);
         if (node == null) {
             log.error("节点不存在,executionId={}, nodeIndex={}", executionId, nodeIndex);
             return;
@@ -214,7 +238,10 @@ public class TaskExecutionEngine {
             node.setStatus(NodeStatus.SUCCESS.name());
             node.setFinishedAt(LocalDateTime.now());
             execution.updateHeartbeat();
-            taskExecutionMapper.updateByVersion(execution);
+            taskExecutionRepository.updateByPrimaryKeySelective(execution);
+
+            // 更新执行计划到Redis
+            executionPlanStore.save(executionId, plan);
 
             // 发送NEXT_NODE消息(立即,延迟=0)
             flowControlProducer.sendNextNode(executionId, nodeIndex, 0);
@@ -262,20 +289,26 @@ public class TaskExecutionEngine {
         log.info("推进到下一节点,executionId={}", executionId);
 
         // 加载执行实例
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             log.error("执行实例不存在,executionId={}", executionId);
             return;
         }
 
         // 检查是否有下一节点
-        if (!execution.hasNextNode()) {
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan == null) {
+            log.error("执行计划不存在,executionId={}", executionId);
+            return;
+        }
+
+        if (!plan.hasNext(execution.getCurrentIndex())) {
             // 全部节点执行完成
             log.info("所有节点执行完成,任务成功,executionId={}", executionId);
 
             execution.markAsSuccess();
             execution.setFinishedAt(LocalDateTime.now());
-            taskExecutionMapper.updateByVersion(execution);
+            taskExecutionRepository.updateByPrimaryKeySelective(execution);
 
             return;
         }
@@ -288,11 +321,11 @@ public class TaskExecutionEngine {
         // 乐观锁更新
         execution.setCurrentIndex(nextIndex);
         execution.updateHeartbeat();
-        int updated = taskExecutionMapper.updateByVersion(execution);
+        int updated = taskExecutionRepository.updateByPrimaryKeySelective(execution);
 
         if (updated == 0) {
             log.error("推进节点失败(乐观锁冲突),executionId={}, version={}",
-                    executionId, execution.getVersion());
+                    executionId, execution.getObjectVersionNumber());
             return;
         }
 
@@ -313,23 +346,28 @@ public class TaskExecutionEngine {
                 executionId, nodeIndex, failReason);
 
         // 加载执行实例
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             log.error("执行实例不存在,executionId={}", executionId);
             return;
         }
 
         // 更新节点状态为FAILED
-        ExecutionNode node = execution.getExecutionPlan().getNode(nodeIndex);
-        if (node != null) {
-            node.setStatus(NodeStatus.FAILED.name());
-            node.setFinishedAt(LocalDateTime.now());
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan != null) {
+            ExecutionNode node = plan.getNode(nodeIndex);
+            if (node != null) {
+                node.setStatus(NodeStatus.FAILED.name());
+                node.setFinishedAt(LocalDateTime.now());
+                // 更新执行计划到Redis
+                executionPlanStore.save(executionId, plan);
+            }
         }
 
         // 更新执行实例状态为FAILED
         execution.markAsFailed(failReason);
         execution.setFinishedAt(LocalDateTime.now());
-        taskExecutionMapper.updateByVersion(execution);
+        taskExecutionRepository.updateByPrimaryKeySelective(execution);
 
         // 触发安全关闭
         log.info("触发安全关闭,executionId={}", executionId);
@@ -347,7 +385,7 @@ public class TaskExecutionEngine {
                         .map(d -> "\"" + d + "\"")
                         .collect(Collectors.joining(",")));
         execution.setSafeCloseDetails(closeDetails);
-        taskExecutionMapper.updateByVersion(execution);
+        taskExecutionRepository.updateByPrimaryKeySelective(execution);
 
         // 创建报警记录
         Long alarmId = alarmRecordService.createTaskFailureAlarm(executionId, failReason);
@@ -386,14 +424,20 @@ public class TaskExecutionEngine {
         // (已在Consumer中处理)
 
         // 加载执行实例
-        TaskExecution execution = taskExecutionMapper.selectById(executionId);
+        TaskExecution execution = taskExecutionRepository.selectByPrimaryKey(executionId);
         if (execution == null) {
             log.error("执行实例不存在,executionId={}", executionId);
             return;
         }
 
         // 检查ACK状态
-        ExecutionNode node = execution.getExecutionPlan().getNode(nodeIndex);
+        ExecutionPlan plan = executionPlanStore.get(executionId);
+        if (plan == null) {
+            log.error("执行计划不存在,executionId={}", executionId);
+            return;
+        }
+
+        ExecutionNode node = plan.getNode(nodeIndex);
         if (node == null) {
             log.error("节点不存在,executionId={}, nodeIndex={}", executionId, nodeIndex);
             return;

+ 16 - 20
src/main/java/cn/sciento/farm/automationv2/app/service/TaskTriggerService.java

@@ -1,12 +1,15 @@
 package cn.sciento.farm.automationv2.app.service;
 
-import cn.sciento.core.redis.RedisHelper;
+import cn.sciento.farm.automationv2.domain.repository.IrrigationTaskRepository;
+import cn.sciento.farm.automationv2.domain.repository.TaskGroupConfigRepository;
+import cn.sciento.farm.automationv2.infra.redis.ExecutionPlanStore;
 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.enums.ExecutionStatus;
 import cn.sciento.farm.automationv2.domain.enums.TriggerType;
+import cn.sciento.farm.automationv2.domain.repository.TaskExecutionRepository;
 import cn.sciento.farm.automationv2.domain.service.ExecutionPlanGenerator;
 import cn.sciento.farm.automationv2.domain.valueobject.ExecutionPlan;
 import cn.sciento.farm.automationv2.domain.valueobject.ZoneConfigView;
@@ -42,16 +45,13 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public class TaskTriggerService {
 
-    private final IrrigationTaskMapper irrigationTaskMapper;
+    private final IrrigationTaskRepository irrigationTaskRepository;
     private final IrrigationGroupMapper irrigationGroupMapper;
-    private final TaskGroupConfigMapper taskGroupConfigMapper;
-    private final TaskExecutionMapper taskExecutionMapper;
+    private final TaskGroupConfigRepository taskGroupConfigRepository;
+    private final TaskExecutionRepository taskExecutionRepository;
     private final ExecutionPlanGenerator executionPlanGenerator;
     private final FlowControlProducer flowControlProducer;
-
-    @Qualifier("redisHelper")
-    @Autowired
-    private RedisHelper redisHelper;
+    private final ExecutionPlanStore executionPlanStore;
 
     /**
      * 触发任务执行
@@ -65,7 +65,7 @@ public class TaskTriggerService {
         log.info("触发任务,taskId={}, triggerType={}", taskId, triggerType);
 
         // 加载任务
-        IrrigationTask task = irrigationTaskMapper.selectById(taskId);
+        IrrigationTask task = irrigationTaskRepository.selectByPrimaryKey(taskId);
         if (task == null) {
             log.error("任务不存在,taskId={}", taskId);
             throw new IllegalArgumentException("任务不存在");
@@ -85,7 +85,7 @@ public class TaskTriggerService {
 //        }
 
         // 加载任务灌区配置(灌区列表 + 灌溉时长)
-        List<TaskGroupConfig> groupConfigs = taskGroupConfigMapper.selectByTaskId(taskId);
+        List<TaskGroupConfig> groupConfigs = taskGroupConfigRepository.queryByTaskId(taskId);
         if (groupConfigs == null || groupConfigs.isEmpty()) {
             // TODO 执行结束
             log.error("任务未配置灌区,taskId={}", taskId);
@@ -109,13 +109,11 @@ public class TaskTriggerService {
                 log.error("灌溉组不存在,groupId={}", config.getGroupId());
                 throw new IllegalStateException("灌溉组不存在,groupId=" + config.getGroupId());
             }
-            zones.add(ZoneConfigView.from(group, config));
+            zones.add(ZoneConfigView.from(group, config,task));
         }
 
         // 生成执行计划
         ExecutionPlan plan = executionPlanGenerator.generate(task, zones);
-        //存缓存
-        redisHelper.strSet(RedisConstant.TASK_NODE + task.getId(),JSON.toJSONString(plan),2, TimeUnit.DAYS);
 
         log.info("执行计划生成完成,taskId={}, totalNodes={}, expectedDurationMinutes={}",
                 taskId, plan.getNodes().size(), plan.calculateExpectedDurationMinutes());
@@ -125,28 +123,26 @@ public class TaskTriggerService {
                 .taskId(taskId)
                 .taskName(task.getTaskName())
                 .triggerType(triggerType)
-//                .executionPlan(plan)
+                .executionPlan(plan)
                 .currentIndex(0)
                 .status(ExecutionStatus.PENDING)
-                .version(0)
                 .tenantId(task.getTenantId())
-                .createdAt(LocalDateTime.now())
-                .updatedAt(LocalDateTime.now())
                 .build();
 
         // 计算预期完成时间
         int expectedMinutes = plan.calculateExpectedDurationMinutes();
         execution.setExpectedFinishAt(LocalDateTime.now().plusMinutes(expectedMinutes));
-
         // 持久化执行实例
-        taskExecutionMapper.insert(execution);
+        taskExecutionRepository.insertSelective(execution);
+        // 存储执行计划到Redis
+        executionPlanStore.save(execution.getId(), plan);
 
         log.info("执行实例创建成功,executionId={}, taskId={}, triggerType={}, expectedFinishAt={}",
                 execution.getId(), taskId, triggerType, execution.getExpectedFinishAt());
 
         // 更新任务执行次数
 //        task.incrementExecutedCount();
-        irrigationTaskMapper.updateById(task);
+//        irrigationTaskMapper.updateById(task);
 
         // 发送任务启动消息
         flowControlProducer.sendTaskStart(execution.getId(), taskId, task.getTenantId());

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

@@ -10,7 +10,9 @@ import cn.sciento.farm.automationv2.api.dto.IrrigationGroupDTO;
 import cn.sciento.farm.automationv2.app.service.irrigationGroup.IrrigationGroupService;
 
 import cn.sciento.farm.automationv2.domain.entity.IrrigationGroup;
+import cn.sciento.farm.automationv2.domain.entity.TaskGroupConfig;
 import cn.sciento.farm.automationv2.domain.repository.IrrigationGroupRepository;
+import cn.sciento.farm.automationv2.domain.repository.TaskGroupConfigRepository;
 import cn.sciento.farm.automationv2.infra.constant.BaseConstant;
 import cn.sciento.farm.automationv2.infra.utils.BeanCopyUtils;
 import cn.sciento.mybatis.domian.Condition;
@@ -36,11 +38,13 @@ public class IrrigationGroupServiceImpl implements IrrigationGroupService {
 
     private IrrigationGroupRepository irrigationGroupRepository;
 
+    private TaskGroupConfigRepository taskGroupConfigRepository;
+
     private Validator validator;
 
-    public IrrigationGroupServiceImpl(IrrigationGroupRepository irrigationGroupRepository,
-                                      Validator validator) {
+    public IrrigationGroupServiceImpl(IrrigationGroupRepository irrigationGroupRepository, TaskGroupConfigRepository taskGroupConfigRepository, Validator validator) {
         this.irrigationGroupRepository = irrigationGroupRepository;
+        this.taskGroupConfigRepository = taskGroupConfigRepository;
         this.validator = validator;
     }
 
@@ -97,10 +101,19 @@ public class IrrigationGroupServiceImpl implements IrrigationGroupService {
         if (resultIrrigationGroup == null) {
             throw new CommonException("wfautoV2.parameter.error");
         }
+        // 判断是否存在任务
+        checkTask(irrigationGroup.getId());
         resultIrrigationGroup.setEnabledFlag(BaseConstant.ENABLE);
         return irrigationGroupRepository.updateByPrimaryKeySelective(resultIrrigationGroup);
     }
 
+    private void checkTask(Long id) {
+        List<TaskGroupConfig> taskGroupConfigs = taskGroupConfigRepository.queryByGroupId(id);
+        if (taskGroupConfigs.size() > 0){
+            throw new CommonException("wfautoV2.group.hadTask");
+        }
+    }
+
 
     @Override
     public Page<IrrigationGroup> page(IrrigationGroup irrigationGroup,PageRequest pageRequest) {

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

@@ -22,6 +22,19 @@ public interface IrrigationTaskService {
      */
     IrrigationTask create(CreateTaskRequest request);
 
+    /**
+     * 详情
+     * @param id
+     * @return irrigationGroup
+     */
+    IrrigationTask getById(Long id);
+
+    /**
+     * 编辑
+     * @param task
+     * @return irrigationGroup
+     */
+    IrrigationTask update(IrrigationTask task);
 
     /**
      *获取page
@@ -30,4 +43,18 @@ public interface IrrigationTaskService {
      */
     Page<IrrigationTaskVO> page(IrrigationTask irrigationTask, PageRequest pageRequest);
 
+
+    /**
+     * 启用/禁用
+     * @param task
+     * @return
+     */
+    Integer enable(IrrigationTask task);
+
+    /**
+     * 删除
+     * @param task
+     * @return
+     */
+    Integer delete(IrrigationTask task);
 }

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

@@ -50,13 +50,12 @@ public class IrrigationTaskServiceImpl implements IrrigationTaskService {
 
     private Validator validator;
 
-    public IrrigationTaskServiceImpl(IrrigationTaskRepository irrigationTaskRepository,
-                                     Validator validator) {
+    public IrrigationTaskServiceImpl(IrrigationTaskRepository irrigationTaskRepository, TaskGroupConfigRepository taskGroupConfigRepository, Validator validator) {
         this.irrigationTaskRepository = irrigationTaskRepository;
+        this.taskGroupConfigRepository = taskGroupConfigRepository;
         this.validator = validator;
     }
 
-
     private void validDTO(IrrigationGroupDTO dto) {
         ValidUtils.valid(validator, dto);
         if (dto.getBallValuesDTOS() != null) {
@@ -77,9 +76,10 @@ public class IrrigationTaskServiceImpl implements IrrigationTaskService {
             TaskGroupConfig config = TaskGroupConfig.builder()
                     .taskId(task.getId())
                     .groupId(dto.getGroupId())
+                    .groupName(dto.getGroupName())
                     .sortOrder(dto.getSortOrder())
                     .irrigationDuration(dto.getIrrigationDuration())
-                    .devicePressureKpa(dto.getDevicePressureKpa())
+                    .groupPressureKpa(dto.getGroupPressureKpa())
                     .build();
             groupConfigs.add(config);
         }
@@ -88,9 +88,47 @@ public class IrrigationTaskServiceImpl implements IrrigationTaskService {
     }
 
     @Override
+    public IrrigationTask getById(Long id) {
+        return irrigationTaskRepository.selectByPrimaryKey(id);
+    }
+
+    @Override
+    public IrrigationTask update(IrrigationTask task) {
+        IrrigationTask result = getById(task.getId());
+        if (result == null) {
+            throw new CommonException("wfautoV2.parameter.error");
+        }
+        irrigationTaskRepository.updateByPrimaryKeySelective(task);
+        return task;
+    }
+
+    @Override
     public Page<IrrigationTaskVO> page(IrrigationTask irrigationTask, PageRequest pageRequest) {
         return PageHelper.doPageAndSort(pageRequest,() -> {
             return irrigationTaskRepository.page(irrigationTask);
         });
     }
+
+    @Override
+    public Integer enable(IrrigationTask task) {
+        IrrigationTask result = getById(task.getId());
+        if (result == null) {
+            throw new CommonException("wfautoV2.parameter.error");
+        }
+        result.setEnabledFlag(task.getEnabledFlag());
+        return irrigationTaskRepository.updateByPrimaryKeySelective(result);
+    }
+
+    @Override
+    public Integer delete(IrrigationTask task) {
+        IrrigationTask result = getById(task.getId());
+        if (result == null) {
+            throw new CommonException("wfautoV2.parameter.error");
+        }
+        if (BaseConstant.ENABLE.equals(result.getEnabledFlag())){
+            throw new CommonException("wfautoV2.task.enabled");
+        }
+        result.setEnabledFlag(BaseConstant.DISABLE);
+        return irrigationTaskRepository.updateByPrimaryKeySelective(result);
+    }
 }

+ 22 - 0
src/main/java/cn/sciento/farm/automationv2/config/Business.java

@@ -0,0 +1,22 @@
+package cn.sciento.farm.automationv2.config;
+
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface Business {
+
+	/**
+	 * The value may indicate a suggestion for a logical component name,
+	 * to be turned into a Spring bean in case of an autodetected component.
+	 * @return the suggested component name, if any (or empty String otherwise)
+	 */
+	@AliasFor(annotation = Component.class)
+	String value() default "";
+
+}

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

@@ -0,0 +1,25 @@
+package cn.sciento.farm.automationv2.domain.business;
+
+
+
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
+
+/**
+ * <p>
+ *      网关业务层
+ * </p>
+ *
+ * @author MouthMouth
+ * @since 2021/10/14
+ */
+public interface DeviceBusiness {
+
+
+    /**
+     * 根据编号控制网关节点
+     * @param dto 网关控制dto
+     * @return 是否成功
+     */
+    Integer control(GatewayControlDto dto);
+
+}

+ 33 - 0
src/main/java/cn/sciento/farm/automationv2/domain/business/impl/DeviceBusinessImpl.java

@@ -0,0 +1,33 @@
+package cn.sciento.farm.automationv2.domain.business.impl;
+
+import cn.sciento.core.util.ResponseUtils;
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
+import cn.sciento.farm.automationv2.config.Business;
+import cn.sciento.farm.automationv2.domain.business.DeviceBusiness;
+import cn.sciento.farm.automationv2.infra.feign.DeviceFeign;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+
+/**
+ * <p>
+ *      网关业务层实现类
+ * </p>
+ *
+ * @author JayHaw
+ * @since 2021/10/14
+ */
+@Business
+public class DeviceBusinessImpl implements DeviceBusiness {
+
+    @Autowired
+    private DeviceFeign feign;
+
+
+    @Override
+    public Integer control(GatewayControlDto dto) {
+        ResponseEntity<String> result = feign.control(dto.getTenantId(), dto);
+        return ResponseUtils.getResponse(result, Integer.class);
+    }
+
+
+}

+ 2 - 1
src/main/java/cn/sciento/farm/automationv2/domain/entity/AlarmRecord.java

@@ -2,6 +2,7 @@ package cn.sciento.farm.automationv2.domain.entity;
 
 import cn.sciento.farm.automationv2.domain.enums.AlarmType;
 import com.fasterxml.jackson.annotation.JsonFormat;
+import io.choerodon.mybatis.domain.AuditDomain;
 import io.swagger.annotations.ApiModel;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -23,7 +24,7 @@ import java.time.LocalDateTime;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class AlarmRecord {
+public class AlarmRecord extends AuditDomain {
 
     /**
      * 报警记录唯一ID

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

@@ -55,7 +55,7 @@ public class IrrigationGroup extends AuditDomain {
 
     /**
      * 球阀列表(JSON格式)
-     * 结构:[{"deviceId":ball-001, "deviceName":"1号球阀", "sw":1, "targetAngle":80, "targetPressureKpa":250}]
+     * 结构:[{"deviceId":ball-001, "deviceName":"1号球阀", "switchId":10001, "sw":1, "targetAngle":80, "targetPressureKpa":250}]
      * targetPressureKpa 为可选字段,若球阀支持压力控制则配置
      */
     private String ballValves;

+ 40 - 29
src/main/java/cn/sciento/farm/automationv2/domain/entity/IrrigationTask.java

@@ -1,6 +1,7 @@
 package cn.sciento.farm.automationv2.domain.entity;
 
 import cn.sciento.farm.automationv2.domain.enums.*;
+import cn.sciento.farm.automationv2.infra.constant.BaseConstant;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.choerodon.mybatis.domain.AuditDomain;
 import io.swagger.annotations.ApiModel;
@@ -23,7 +24,6 @@ import java.time.LocalDateTime;
 @ApiModel("轮灌任务配置")
 @Builder
 @NoArgsConstructor
-@AllArgsConstructor
 public class IrrigationTask extends AuditDomain {
 
     /**
@@ -38,13 +38,6 @@ public class IrrigationTask extends AuditDomain {
      */
     private String taskName;
 
-    /**
-     * 触发类型:SCHEDULED / LINKAGE / MANUAL
-     * 注意:定时触发规则已迁移至 task_schedule_rule 表
-     * 此字段保留用于向后兼容和标识任务触发方式
-     */
-    private TriggerType triggerType;
-
     // ================== 水泵配置 ==================
 
     /**
@@ -68,7 +61,7 @@ public class IrrigationTask extends AuditDomain {
     private Integer pressureMode;
 
     /**
-     * 统一目标压力值(kPa,仅 PUMP_UNIFIED 模式需配置)
+     * 统一目标压力值(kPa,仅 统一恒压 模式需配置)
      */
     private Integer targetPressureKpa;
 
@@ -91,12 +84,12 @@ public class IrrigationTask extends AuditDomain {
     /**
      * 施肥泵设备ID
      */
-    private String fertilizerPumpId;
+    private Long fertilizerPumpId;
 
     /**
      * 搅拌电机设备ID
      */
-    private String stirMotorId;
+    private Long stirMotorId;
 
     /**
      * 施肥控制模式:TIME / VOLUME
@@ -130,9 +123,9 @@ public class IrrigationTask extends AuditDomain {
     // ================== 状态与控制 ==================
 
     /**
-     * 任务状态:ENABLED / DISABLED / DELETED
+     * 任务状态:空闲中 / 灌溉中 / 暂停中
      */
-    private TaskStatus status;
+    private Integer status;
 
     /**
      * 是否启用:1启用 0禁用 2删除
@@ -154,47 +147,65 @@ public class IrrigationTask extends AuditDomain {
 
     // ================== 业务方法 ==================
 
-    /**
-     * 是否为定时触发
-     */
-    public boolean isScheduled() {
-        return TriggerType.SCHEDULED.equals(triggerType);
-    }
 
-    /**
-     * 是否为联动触发
-     */
-    public boolean isLinkage() {
-        return TriggerType.LINKAGE.equals(triggerType);
+    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) {
+        this.id = id;
+        this.taskName = taskName;
+        this.pumpDeviceId = pumpDeviceId;
+        this.pumpDeviceName = pumpDeviceName;
+        this.isPump = isPump;
+        this.pressureMode = pressureMode;
+        this.targetPressureKpa = targetPressureKpa;
+        this.isDevicePump = isDevicePump;
+        this.switchStableSeconds = switchStableSeconds;
+        this.fertilizerPumpId = fertilizerPumpId;
+        this.stirMotorId = stirMotorId;
+        this.fertilizerControlMode = fertilizerControlMode;
+        this.fertDelayMinutes = fertDelayMinutes;
+        this.preStirMinutes = preStirMinutes;
+        this.fertDurationMinutes = fertDurationMinutes;
+        this.fertTargetLiters = fertTargetLiters;
+        this.status = status;
+        this.enabledFlag = enabledFlag;
+        this.tenantId = tenantId;
+        this.organizationId = organizationId;
     }
 
     /**
      * 是否配置了施肥机
      */
     public boolean hasFertilizer() {
-        return fertilizerPumpId != null && !fertilizerPumpId.isEmpty()
-                && stirMotorId != null && !stirMotorId.isEmpty();
+        return fertilizerPumpId != null
+                && stirMotorId != null;
     }
 
     /**
      * 是否为统一压力模式
      */
     public boolean isPumpUnifiedMode() {
-        return PressureMode.PUMP_UNIFIED.equals(pressureMode);
+        return isPump() && PressureMode.PUMP_UNIFIED.getCode().equals(pressureMode);
     }
 
     /**
      * 是否为分区压力模式
      */
     public boolean isPumpZoneMode() {
-        return PressureMode.PUMP_ZONE.equals(pressureMode);
+        return isPump() && PressureMode.PUMP_ZONE.getCode().equals(pressureMode);
+    }
+
+    /**
+     * 是否为首部恒压模式
+     */
+    public boolean isPump() {
+        return BaseConstant.ENABLE.equals(isPump);
     }
 
     /**
      * 是否需要下发水泵压力指令
      */
     public boolean needsPumpPressure() {
-        return isPumpUnifiedMode() || isPumpZoneMode();
+        return isPump();
+//        return isPumpUnifiedMode() || isPumpZoneMode();
     }
 
     /**

+ 3 - 17
src/main/java/cn/sciento/farm/automationv2/domain/entity/TaskExecution.java

@@ -7,6 +7,7 @@ import cn.sciento.farm.automationv2.domain.valueobject.ExecutionPlan;
 import cn.sciento.mybatis.handler.JSONHandler;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.choerodon.mybatis.annotation.ColumnType;
+import io.choerodon.mybatis.domain.AuditDomain;
 import io.swagger.annotations.ApiModel;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -29,7 +30,7 @@ import java.time.LocalDateTime;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class TaskExecution {
+public class TaskExecution extends AuditDomain {
 
     /**
      * 执行实例唯一ID
@@ -72,11 +73,6 @@ public class TaskExecution {
     private ExecutionStatus status;
 
     /**
-     * 乐观锁版本号(防止并发更新冲突)
-     */
-    private Integer version;
-
-    /**
      * 实际开始时间
      */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -120,17 +116,6 @@ public class TaskExecution {
      */
     private Long tenantId;
 
-    /**
-     * 创建时间
-     */
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    private LocalDateTime createdAt;
-
-    /**
-     * 更新时间
-     */
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    private LocalDateTime updatedAt;
 
     // ================== 业务方法 ==================
 
@@ -138,6 +123,7 @@ public class TaskExecution {
      * 获取当前执行的节点
      */
     public ExecutionNode getCurrentNode() {
+        // TODO 改成从Redis获取
         if (executionPlan == null || currentIndex == null) {
             return null;
         }

+ 8 - 3
src/main/java/cn/sciento/farm/automationv2/domain/entity/TaskGroupConfig.java

@@ -44,6 +44,11 @@ public class TaskGroupConfig extends AuditDomain{
     private Long groupId;
 
     /**
+     * 灌溉组名称
+     */
+    private String groupName;
+
+    /**
      * 执行顺序(0开始,用户配置的灌溉意图顺序)
      */
     private Integer sortOrder;
@@ -55,10 +60,10 @@ public class TaskGroupConfig extends AuditDomain{
     private Long irrigationDuration;
 
     /**
-     * 球阀目标压力(bar)
-     * 仅在启用球阀恒压 时生效,球阀设备下发新压力值
+     * 目标压力(bar)
+     * 仅在灌区恒压 时生效,灌区恒压
      */
-    private Integer devicePressureKpa;
+    private Integer groupPressureKpa;
 
 
 }

+ 5 - 5
src/main/java/cn/sciento/farm/automationv2/domain/enums/PressureMode.java

@@ -11,22 +11,22 @@ public enum PressureMode {
     /**
      * 无压力控制(普通水泵启停)
      */
-    NONE("NONE", "普通启停"),
+    NONE(0, "普通启停"),
 
     /**
      * 统一压力(所有灌区使用同一压力值)
      */
-    PUMP_UNIFIED("PUMP_UNIFIED", "统一压力"),
+    PUMP_UNIFIED(1, "统一压力"),
 
     /**
      * 分区压力(每个灌区可设置不同的水泵目标压力)
      */
-    PUMP_ZONE("PUMP_ZONE", "分区压力");
+    PUMP_ZONE(2, "分区压力");
 
-    private final String code;
+    private final Integer code;
     private final String description;
 
-    PressureMode(String code, String description) {
+    PressureMode(Integer code, String description) {
         this.code = code;
         this.description = description;
     }

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

@@ -4,6 +4,8 @@ package cn.sciento.farm.automationv2.domain.repository;
 import cn.sciento.farm.automationv2.domain.entity.TaskGroupConfig;
 import cn.sciento.mybatis.base.BaseRepository;
 
+import java.util.List;
+
 /**
  * <p>
  *
@@ -14,4 +16,18 @@ import cn.sciento.mybatis.base.BaseRepository;
  */
 public interface TaskGroupConfigRepository extends BaseRepository<TaskGroupConfig> {
 
+    /**
+     * 根据灌溉组获取列表
+     * @param groupId
+     * @return
+     */
+    List<TaskGroupConfig> queryByGroupId(Long groupId);
+
+    /**
+     * 根据任务获取列表
+     * @param taskId
+     * @return
+     */
+    List<TaskGroupConfig> queryByTaskId(Long taskId);
+
 }

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

@@ -63,17 +63,17 @@ public class ExecutionPlanGenerator {
                     ? task.getTargetPressureKpa()
                     : firstZone.getZonePressureKpa();
             if (pressureKpa != null) {
-                nodeIndex = addSetPumpPressureNode(nodes, nodeIndex, task.getPumpId(), pressureKpa);
+                nodeIndex = addSetPumpPressureNode(nodes, nodeIndex, task.getPumpDeviceId(), pressureKpa);
             }
         }
 
         // 3. 启动水泵(S-01:在第一个灌区开启后启动)
-        nodeIndex = addStartPumpNode(nodes, nodeIndex, task.getPumpId());
+        nodeIndex = addStartPumpNode(nodes, nodeIndex, task.getPumpDeviceId());
 
         // 处理灌区切换(S-02:先开后关 + 稳压等待)
         if (zones.size() == 1) {
             // 只有一个灌区:直接等待后关闭
-            nodeIndex = addWaitNode(nodes, nodeIndex, firstZone.getIrrigationDurationMinutes(), "IRRIGATE");
+            nodeIndex = addWaitNode(nodes, nodeIndex, firstZone.getIrrigationDuration(), "IRRIGATE");
         } else {
             // 多个灌区:循环处理灌区切换
             for (int i = 1; i < zones.size(); i++) {
@@ -81,14 +81,14 @@ public class ExecutionPlanGenerator {
                 ZoneConfigView previousZone = zones.get(i - 1);
 
                 // 等待上一个灌区的灌溉时长
-                nodeIndex = addWaitNode(nodes, nodeIndex, previousZone.getIrrigationDurationMinutes(), "IRRIGATE");
+                nodeIndex = addWaitNode(nodes, nodeIndex, previousZone.getIrrigationDuration(), "IRRIGATE");
 
                 // 先开当前灌区(S-04:先开后关)
                 nodeIndex = addOpenGroupNode(nodes, nodeIndex, currentZone);
 
                 // PUMP_ZONE模式:切换时调整水泵压力
                 if (task.isPumpZoneMode() && currentZone.getZonePressureKpa() != null) {
-                    nodeIndex = addSetPumpPressureNode(nodes, nodeIndex, task.getPumpId(), currentZone.getZonePressureKpa());
+                    nodeIndex = addSetPumpPressureNode(nodes, nodeIndex, task.getPumpDeviceId(), currentZone.getZonePressureKpa());
                 }
 
                 // 稳压等待(S-02:确保压力平稳后再关闭上一个灌区)
@@ -101,14 +101,14 @@ public class ExecutionPlanGenerator {
 
             // 最后一个灌区的等待
             ZoneConfigView lastZone = zones.get(zones.size() - 1);
-            nodeIndex = addWaitNode(nodes, nodeIndex, lastZone.getIrrigationDurationMinutes(), "IRRIGATE");
+            nodeIndex = addWaitNode(nodes, nodeIndex, lastZone.getIrrigationDuration(), "IRRIGATE");
         }
 
         // 最后阶段:安全关闭(S-03:水泵→电磁阀)
         ZoneConfigView lastZone = zones.get(zones.size() - 1);
 
         // 4. 关闭水泵
-        nodeIndex = addStopPumpNode(nodes, nodeIndex, task.getPumpId());
+        nodeIndex = addStopPumpNode(nodes, nodeIndex, task.getPumpDeviceId());
 
         // 5. 关闭最后一个灌区
         nodeIndex = addCloseGroupNode(nodes, nodeIndex, lastZone);
@@ -137,17 +137,18 @@ public class ExecutionPlanGenerator {
         if (zone.getBallValves() != null && !zone.getBallValves().isEmpty()) {
             JSONArray ballValves = JSON.parseArray(zone.getBallValves());
             if (!ballValves.isEmpty()) {
-                // TODO 判断,如果任务设定了球阀恒压,则传压力,否则传角度
-                if () {
-                    JSONObject firstBallValve = ballValves.getJSONObject(0);
-                    // 角度
-                    if (firstBallValve.containsKey("targetAngle")) {
-                        params.put("targetAngle", firstBallValve.getInteger("targetAngle"));
-                    }
+                // 判断,如果任务设定了球阀恒压,则传压力,否则传角度
+                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"));
+                    }
                 }
             }
         }
@@ -199,7 +200,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加设置水泵压力节点
      */
-    private int addSetPumpPressureNode(List<ExecutionNode> nodes, int index, String pumpId, Integer pressureKpa) {
+    private int addSetPumpPressureNode(List<ExecutionNode> nodes, int index, Long pumpId, Integer pressureKpa) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
                 .deviceType("PUMP")
@@ -228,7 +229,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加启动水泵节点
      */
-    private int addStartPumpNode(List<ExecutionNode> nodes, int index, String pumpId) {
+    private int addStartPumpNode(List<ExecutionNode> nodes, int index, Long pumpId) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
                 .deviceType("PUMP")
@@ -254,7 +255,7 @@ public class ExecutionPlanGenerator {
     /**
      * 添加关闭水泵节点
      */
-    private int addStopPumpNode(List<ExecutionNode> nodes, int index, String pumpId) {
+    private int addStopPumpNode(List<ExecutionNode> nodes, int index, Long pumpId) {
         DeviceInfo pumpDevice = DeviceInfo.builder()
                 .deviceId(pumpId)
                 .deviceType("PUMP")
@@ -282,17 +283,16 @@ public class ExecutionPlanGenerator {
      *
      * @param source 等待来源:IRRIGATE(灌溉等待)/ ZONE_SWITCH(稳压等待)
      */
-    private int addWaitNode(List<ExecutionNode> nodes, int index, Integer minutes, String source) {
-        int seconds = minutes * 60;
+    private int addWaitNode(List<ExecutionNode> nodes, int index, Long waitSecond, String source) {
 
         Map<String, Object> params = new HashMap<>();
-        params.put("seconds", seconds);
+        params.put("seconds", waitSecond);
         params.put("source", source);
 
         ExecutionNode node = ExecutionNode.builder()
                 .index(index)
                 .nodeType(NodeType.WAIT)
-                .nodeName("等待" + minutes + "分钟")
+                .nodeName("等待" + waitSecond + "秒")
                 .refId(null)
                 .params(params)
                 .status("PENDING")
@@ -344,7 +344,7 @@ public class ExecutionPlanGenerator {
             for (int i = 0; i < jsonArray.size(); i++) {
                 JSONObject json = jsonArray.getJSONObject(i);
                 DeviceInfo device = DeviceInfo.builder()
-                        .deviceId(json.getString("deviceId"))
+                        .deviceId(Long.parseLong(json.getString("deviceId")))
                         .deviceType(deviceType)
                         .deviceName(json.getString("deviceName"))
                         .build();

+ 16 - 1
src/main/java/cn/sciento/farm/automationv2/domain/valueobject/DeviceInfo.java

@@ -21,7 +21,7 @@ public class DeviceInfo {
     /**
      * 设备ID
      */
-    private String deviceId;
+    private Long deviceId;
 
     /**
      * 设备类型(SOLENOID_VALVE / BALL_VALVE / PUMP / FERTILIZER)
@@ -34,6 +34,21 @@ public class DeviceInfo {
     private String deviceName;
 
     /**
+     * 设备编号(网关eui)
+     */
+    private String eui;
+
+    /**
+     * 设备节点(node)
+     */
+    private String node;
+
+    /**
+     * 设备控制回路(circuit)
+     */
+    private String circuit;
+
+    /**
      * ACK状态:PENDING / SUCCESS / FAIL / TIMEOUT
      */
     private AckStatus ackStatus;

+ 18 - 4
src/main/java/cn/sciento/farm/automationv2/domain/valueobject/ZoneConfigView.java

@@ -1,7 +1,9 @@
 package cn.sciento.farm.automationv2.domain.valueobject;
 
 import cn.sciento.farm.automationv2.domain.entity.IrrigationGroup;
+import cn.sciento.farm.automationv2.domain.entity.IrrigationTask;
 import cn.sciento.farm.automationv2.domain.entity.TaskGroupConfig;
+import cn.sciento.farm.automationv2.infra.constant.BaseConstant;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
@@ -53,24 +55,36 @@ public class ZoneConfigView {
     private Long irrigationDuration;
 
     /**
+     * 是否启用球阀恒压
+     */
+    private Integer isDevicePump;
+
+    /**
      * 从灌溉组和任务配置合并创建
      */
-    public static ZoneConfigView from(IrrigationGroup group, TaskGroupConfig config) {
+    public static ZoneConfigView from(IrrigationGroup group, TaskGroupConfig config, IrrigationTask task) {
         return ZoneConfigView.builder()
                 .groupId(group.getId())
                 .groupName(group.getGroupName())
                 .sortOrder(config.getSortOrder())
-                .zonePressureKpa(group.getZonePressureKpa())
+                // 使用任务里面的分区压力
+                .zonePressureKpa(config.getGroupPressureKpa())
                 .solenoidValves(group.getSolenoidValves())
                 .ballValves(group.getBallValves())
                 .irrigationDuration(config.getIrrigationDuration())
+                .isDevicePump(task.getIsDevicePump())
                 .build();
     }
 
     /**
      * 获取灌溉时长(秒)
      */
-    public int getIrrigationDurationSeconds() {
-        return irrigationDurationMinutes != null ? irrigationDurationMinutes * 60 : 0;
+    public Long getIrrigationDurationSeconds() {
+        return irrigationDuration != null ? irrigationDuration  : 60;
+    }
+
+
+    public Boolean isDevicePump(){
+        return BaseConstant.ENABLE.equals(isDevicePump);
     }
 }

+ 7 - 0
src/main/java/cn/sciento/farm/automationv2/infra/constant/BaseConstant.java

@@ -19,6 +19,13 @@ public interface BaseConstant {
     /** 是否可用 */
     String ENABLED_FLAG = "enabledFlag";
 
+    /**
+     * 可用
+     */
     Integer ENABLE = 1;
 
+    /**
+     * 删除
+     */
+    Integer DISABLE = 2;
 }

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

@@ -8,7 +8,7 @@ package cn.sciento.farm.automationv2.infra.constant;
  */
 public interface RedisConstant {
 
-    String TASK_NODE = "wfauto_v2_automation_task:";
+    String TASK_EXECUTION = "wfauto_v2_automation_task_execution:";
 
 
 }

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

@@ -0,0 +1,39 @@
+package cn.sciento.farm.automationv2.infra.feign;
+
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
+import cn.sciento.farm.automationv2.infra.feign.fallback.DeviceFallback;
+
+import io.swagger.annotations.Api;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * <p>
+ *      网关远程调用
+ * </p>
+ *
+ * @author MouthMouth
+ * @since 2021/10/14
+ */
+@Api("网关远程调用服务")
+@Component
+@FeignClient(
+        name = "wf-platform-equipment",
+        contextId = "DeviceFeign",
+        fallbackFactory = DeviceFallback.class
+)
+public interface DeviceFeign {
+
+    /**
+     * 根据网关编号控制网关
+     */
+    @PostMapping(
+            value = "/v1/{tenantId}/mqtt/control/switch",
+            consumes = "application/json")
+    ResponseEntity<String> control(@PathVariable(value = "tenantId") Long tenantId,
+                                   @RequestBody GatewayControlDto dto);
+
+
+}

+ 34 - 0
src/main/java/cn/sciento/farm/automationv2/infra/feign/fallback/DeviceFallback.java

@@ -0,0 +1,34 @@
+package cn.sciento.farm.automationv2.infra.feign.fallback;
+
+
+import cn.sciento.core.exception.CommonException;
+import cn.sciento.farm.automationv2.api.dto.GatewayControlDto;
+import cn.sciento.farm.automationv2.infra.feign.DeviceFeign;
+
+import feign.hystrix.FallbackFactory;
+import io.choerodon.mybatis.pagehelper.domain.PageRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+
+
+/**
+ * @author mouthmouth
+ */
+@Component
+@Slf4j
+public class DeviceFallback implements FallbackFactory<DeviceFeign> {
+
+
+    @Override
+    public DeviceFeign create(Throwable throwable) {
+        return new DeviceFeign() {
+
+            @Override
+            public ResponseEntity<String> control(Long tenantId, GatewayControlDto dto) {
+                throw new CommonException(throwable);
+            }
+
+        };
+    }
+}

+ 1 - 1
src/main/java/cn/sciento/farm/automationv2/infra/mq/message/DeviceAckMessage.java

@@ -32,7 +32,7 @@ public class DeviceAckMessage implements Serializable {
     /**
      * 设备ID
      */
-    private String deviceId;
+    private Long deviceId;
 
     /**
      * 设备类型

+ 1 - 1
src/main/java/cn/sciento/farm/automationv2/infra/mq/message/DeviceCommandMessage.java

@@ -33,7 +33,7 @@ public class DeviceCommandMessage implements Serializable {
     /**
      * 设备ID
      */
-    private String deviceId;
+    private Long deviceId;
 
     /**
      * 设备类型:SOLENOID_VALVE / BALL_VALVE / PUMP / FERTILIZER

+ 5 - 5
src/main/java/cn/sciento/farm/automationv2/infra/mq/producer/DeviceCommandProducer.java

@@ -96,7 +96,7 @@ public class DeviceCommandProducer {
      * 发送启动水泵指令
      */
     public void sendStartPumpCommand(Long executionId, Integer nodeIndex,
-                                    String pumpId, Long tenantId) {
+                                    Long pumpId, Long tenantId) {
         DeviceCommandMessage message = DeviceCommandMessage.builder()
                 .executionId(executionId)
                 .nodeIndex(nodeIndex)
@@ -116,7 +116,7 @@ public class DeviceCommandProducer {
      * 发送关闭水泵指令
      */
     public void sendStopPumpCommand(Long executionId, Integer nodeIndex,
-                                   String pumpId, Long tenantId) {
+                                    Long pumpId, Long tenantId) {
         DeviceCommandMessage message = DeviceCommandMessage.builder()
                 .executionId(executionId)
                 .nodeIndex(nodeIndex)
@@ -136,7 +136,7 @@ public class DeviceCommandProducer {
      * 发送设置水泵压力指令
      */
     public void sendSetPumpPressureCommand(Long executionId, Integer nodeIndex,
-                                          String pumpId, Integer pressureKpa, Long tenantId) {
+                                           Long pumpId, Integer pressureKpa, Long tenantId) {
         Map<String, Object> params = createSingleParamMap("pressureKpa", pressureKpa);
 
         DeviceCommandMessage message = DeviceCommandMessage.builder()
@@ -158,7 +158,7 @@ public class DeviceCommandProducer {
      * 发送启动施肥机指令
      */
     public void sendStartFertilizerCommand(Long executionId, Integer nodeIndex,
-                                          String fertilizerId, Integer programNo, Long tenantId) {
+                                           Long fertilizerId, Integer programNo, Long tenantId) {
         Map<String, Object> params = createSingleParamMap("programNo", programNo);
 
         DeviceCommandMessage message = DeviceCommandMessage.builder()
@@ -180,7 +180,7 @@ public class DeviceCommandProducer {
      * 发送关闭施肥机指令
      */
     public void sendStopFertilizerCommand(Long executionId, Integer nodeIndex,
-                                         String fertilizerId, Long tenantId) {
+                                          Long fertilizerId, Long tenantId) {
         DeviceCommandMessage message = DeviceCommandMessage.builder()
                 .executionId(executionId)
                 .nodeIndex(nodeIndex)

+ 4 - 4
src/main/java/cn/sciento/farm/automationv2/infra/redis/AckManager.java

@@ -74,7 +74,7 @@ public class AckManager {
      * @param ackStatus   ACK状态
      * @param failReason  失败原因(可选)
      */
-    public void updateAckStatus(Long executionId, Integer nodeIndex, String deviceId,
+    public void updateAckStatus(Long executionId, Integer nodeIndex, Long deviceId,
                                AckStatus ackStatus, String failReason) {
         String key = buildAckKey(executionId, nodeIndex, deviceId);
 
@@ -119,9 +119,9 @@ public class AckManager {
 
             if (statusCode == null) {
                 // Key不存在,可能已过期,视为TIMEOUT
-                statusMap.put(device.getDeviceId(), AckStatus.TIMEOUT);
+                statusMap.put(String.valueOf(device.getDeviceId()), AckStatus.TIMEOUT);
             } else {
-                statusMap.put(device.getDeviceId(), AckStatus.fromCode(statusCode));
+                statusMap.put(String.valueOf(device.getDeviceId()), AckStatus.fromCode(statusCode));
             }
         }
 
@@ -179,7 +179,7 @@ public class AckManager {
     /**
      * 构建ACK Redis Key
      */
-    private String buildAckKey(Long executionId, Integer nodeIndex, String deviceId) {
+    private String buildAckKey(Long executionId, Integer nodeIndex, Long deviceId) {
         return ACK_KEY_PREFIX + executionId + ":" + nodeIndex + ":" + deviceId;
     }
 }

+ 66 - 0
src/main/java/cn/sciento/farm/automationv2/infra/redis/ExecutionPlanStore.java

@@ -0,0 +1,66 @@
+package cn.sciento.farm.automationv2.infra.redis;
+
+import cn.sciento.farm.automationv2.domain.valueobject.ExecutionPlan;
+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.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * ExecutionPlan 缓存存取(Redis)
+ * 作用:
+ * - 以 executionId 为 key 读写执行计划
+ * - 统一 JSON 序列化/反序列化 与 TTL 策略
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ExecutionPlanStore {
+
+    private static final long DEFAULT_TTL_DAYS = 2;
+
+    private final StringRedisTemplate stringRedisTemplate;
+
+    private String buildKey(Long executionId) {
+        return RedisConstant.TASK_EXECUTION + executionId;
+    }
+
+    public void save(Long executionId, ExecutionPlan plan) {
+        save(executionId, plan, DEFAULT_TTL_DAYS, TimeUnit.DAYS);
+    }
+
+    public void save(Long executionId, ExecutionPlan plan, long ttl, TimeUnit unit) {
+        if (executionId == null || plan == null) {
+            return;
+        }
+        String key = buildKey(executionId);
+        String json = JSON.toJSONString(plan);
+        stringRedisTemplate.opsForValue().set(key, json, ttl, unit);
+        log.debug("Saved ExecutionPlan to Redis, key={}, ttl={} {}", key, ttl, unit);
+    }
+
+    public ExecutionPlan get(Long executionId) {
+        if (executionId == null) return null;
+        String key = buildKey(executionId);
+        String json = stringRedisTemplate.opsForValue().get(key);
+        if (json == null || json.isEmpty()) {
+            log.warn("ExecutionPlan missing in Redis, key={}", key);
+            return null;
+        }
+        try {
+            return JSON.parseObject(json, ExecutionPlan.class);
+        } catch (Exception e) {
+            log.error("Failed to parse ExecutionPlan json, key={}", key, e);
+            return null;
+        }
+    }
+
+    public void delete(Long executionId) {
+        if (executionId == null) return;
+        stringRedisTemplate.delete(buildKey(executionId));
+    }
+}

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

@@ -5,6 +5,8 @@ import cn.sciento.farm.automationv2.domain.repository.TaskGroupConfigRepository;
 import cn.sciento.mybatis.base.impl.BaseRepositoryImpl;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
+
 /**
  * <p>
  *       数据层
@@ -16,4 +18,17 @@ import org.springframework.stereotype.Service;
 @Service
 public class TaskGroupConfigRepositoryImpl extends BaseRepositoryImpl<TaskGroupConfig> implements TaskGroupConfigRepository {
 
+    @Override
+    public List<TaskGroupConfig> queryByGroupId(Long groupId) {
+        TaskGroupConfig taskGroupConfig = new TaskGroupConfig();
+        taskGroupConfig.setGroupId(groupId);
+        return select(taskGroupConfig);
+    }
+
+    @Override
+    public List<TaskGroupConfig> queryByTaskId(Long taskId) {
+        TaskGroupConfig taskGroupConfig = new TaskGroupConfig();
+        taskGroupConfig.setTaskId(taskId);
+        return select(taskGroupConfig);
+    }
 }

+ 1 - 1
src/main/resources/mapper/IrrigationGroupMapper.xml

@@ -39,7 +39,7 @@
     </select>
 
     <select id="selectByIdsList" resultMap="BaseResultMap">
-        SELECT * FROM irrigation_group
+        SELECT * FROM wfauto_v2_irrigation_group
         WHERE id IN
         <foreach collection="ids" item="id" open="(" separator="," close=")">
             #{id}

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

@@ -45,17 +45,17 @@
     </insert>
 
     <!-- 根据ID查询 -->
-    <select id="selectById" resultMap="BaseResultMap">
+    <select id="selectById" resultType="cn.sciento.farm.automationv2.domain.entity.IrrigationTask">
         SELECT * FROM irrigation_task WHERE id = #{id} AND deleted = 0
     </select>
 
     <!-- 根据任务编码查询 -->
-    <select id="selectByCode" resultMap="BaseResultMap">
+    <select id="selectByCode" resultType="cn.sciento.farm.automationv2.domain.entity.IrrigationTask">
         SELECT * FROM irrigation_task WHERE task_code = #{taskCode} AND deleted = 0
     </select>
 
     <!-- 查询所有启用的任务 -->
-    <select id="selectEnabledTasks" resultMap="BaseResultMap">
+    <select id="selectEnabledTasks" resultType="cn.sciento.farm.automationv2.domain.entity.IrrigationTask">
         SELECT * FROM irrigation_task
         WHERE tenant_id = #{tenantId}
           AND enabled = 1
@@ -65,7 +65,7 @@
     </select>
 
     <!-- 分页查询 -->
-    <select id="selectByTenant" resultMap="BaseResultMap">
+    <select id="selectByTenant" resultType="cn.sciento.farm.automationv2.domain.entity.IrrigationTask">
         SELECT * FROM irrigation_task
         WHERE tenant_id = #{tenantId} AND deleted = 0
         ORDER BY created_at DESC

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

@@ -9,3 +9,6 @@ wfautoV2.bean.updateError=Failed to update
 
 wfautoV2.parameter.incomplete=Incomplete parameter
 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

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

@@ -9,3 +9,6 @@ wfautoV2.bean.updateError=\u66F4\u65B0\u5931\u8D25
 
 wfautoV2.parameter.incomplete=\u53C2\u6570\u4E0D\u5B8C\u6574
 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