Browse Source

大更新:定时任务整体完成与业务任务页面联动

刘桐 5 days ago
parent
commit
04f4675134
26 changed files with 1216 additions and 49 deletions
  1. 4 4
      xvji-admin/src/main/java/com/xvji/XvJiApplication.java
  2. 78 0
      xvji-admin/src/main/java/com/xvji/aspect/SysJobSyncAspect.java
  3. 19 0
      xvji-admin/src/main/java/com/xvji/config/RestTemplateConfig.java
  4. 1 1
      xvji-admin/src/main/java/com/xvji/domain/PredictTask.java
  5. 1 1
      xvji-admin/src/main/java/com/xvji/domain/TrainTask.java
  6. 9 0
      xvji-admin/src/main/java/com/xvji/mapper/ComponentMapper.java
  7. 134 0
      xvji-admin/src/main/java/com/xvji/quartz/TaskQuartzJob.java
  8. 2 0
      xvji-admin/src/main/java/com/xvji/service/PredictTaskService.java
  9. 3 0
      xvji-admin/src/main/java/com/xvji/service/TrainTaskService.java
  10. 77 1
      xvji-admin/src/main/java/com/xvji/service/impl/PredictTaskServiceImpl.java
  11. 92 8
      xvji-admin/src/main/java/com/xvji/service/impl/TrainTaskServiceImpl.java
  12. 171 0
      xvji-admin/src/main/java/com/xvji/service/task/TaskComponentExecutor.java
  13. 53 0
      xvji-admin/src/main/java/com/xvji/utils/FormDataHttpUtil.java
  14. 29 0
      xvji-admin/src/main/java/com/xvji/utils/InvokeTargetUtils.java
  15. 6 6
      xvji-admin/src/main/java/com/xvji/web/controller/PredictTaskController.java
  16. 8 5
      xvji-admin/src/main/java/com/xvji/web/controller/TrainTaskController.java
  17. 197 0
      xvji-admin/src/test/java/com/xvji/admin/PredictTaskServiceTest.java
  18. 23 0
      xvji-admin/src/test/java/com/xvji/admin/TaskComponentTest.java
  19. 6 0
      xvji-quartz/pom.xml
  20. 19 1
      xvji-quartz/src/main/java/com/xvji/quartz/mapper/SysJobMapper.java
  21. 36 4
      xvji-quartz/src/main/java/com/xvji/quartz/service/ISysJobService.java
  22. 216 15
      xvji-quartz/src/main/java/com/xvji/quartz/service/impl/SysJobServiceImpl.java
  23. 13 0
      xvji-quartz/src/main/java/com/xvji/quartz/task/XjTask.java
  24. 3 1
      xvji-quartz/src/main/java/com/xvji/quartz/util/QuartzJobExecution.java
  25. 4 2
      xvji-quartz/src/main/java/com/xvji/quartz/util/ScheduleUtils.java
  26. 12 0
      xvji-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml

+ 4 - 4
xvji-admin/src/main/java/com/xvji/XvJiApplication.java

@@ -49,8 +49,8 @@ public class XvJiApplication
 
 
     // RestTemplate实例 用于访问算法接口
-    @Bean
-    public RestTemplate restTemplate() {
-        return new RestTemplate();
-    }
+//    @Bean
+//    public RestTemplate restTemplate() {
+//        return new RestTemplate();
+//    }
 }

+ 78 - 0
xvji-admin/src/main/java/com/xvji/aspect/SysJobSyncAspect.java

@@ -0,0 +1,78 @@
+package com.xvji.aspect;
+
+import com.xvji.quartz.domain.SysJob;
+import com.xvji.quartz.service.ISysJobService;
+import com.xvji.domain.PredictTask;
+import com.xvji.domain.TrainTask;
+import com.xvji.service.PredictTaskService;
+import com.xvji.service.TrainTaskService;
+import com.xvji.utils.InvokeTargetUtils;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 定时任务同步切面:当若依定时任务页面修改后,同步回业务任务表
+ */
+@Aspect
+@Component
+public class SysJobSyncAspect {
+
+    private static final Logger log = LoggerFactory.getLogger(SysJobSyncAspect.class);
+
+    @Autowired
+    private PredictTaskService predictTaskService;
+
+    @Autowired(required = false)
+    private TrainTaskService trainTaskService;
+
+    /**
+     * 拦截若依定时任务更新方法,同步到业务任务
+     * 当定时任务页面修改后,自动更新业务任务表对应字段
+     */
+    @AfterReturning(
+            // 关键点:切到ServiceImpl实现类,而非ISysJobService接口
+            pointcut = "execution(* com.xvji.quartz.service.impl.SysJobServiceImpl.updateSysJob(..)) && args(sysJob)",
+            returning = "result"
+    )
+    public void afterUpdateSysJob(SysJob sysJob, boolean result) {
+        if (!result) {
+            log.info("定时任务更新失败,无需同步");
+            return;
+        }
+
+        // 1. 从invokeTarget解析业务任务ID
+        Long taskId = InvokeTargetUtils.parseTaskIdFromInvokeTarget(sysJob.getInvokeTarget());
+        if (taskId == null) {
+            log.info("定时任务调用目标格式错误,无法解析业务ID");
+            return;
+        }
+
+        // 2. 状态转换:定时任务String → 业务任务Integer
+        Integer taskStatus = "1".equals(sysJob.getStatus()) ? 1 : 0;
+
+        // 3. 同步到预测任务(训练任务同理)
+        if ("PREDICT_TASK".equals(sysJob.getJobGroup())) {
+            PredictTask predictTask = new PredictTask();
+            predictTask.setPTaskId(taskId);
+            predictTask.setPTaskName(sysJob.getJobName());
+            predictTask.setPCronExpression(sysJob.getCronExpression());
+            predictTask.setPTaskStatus(taskStatus);
+            predictTaskService.updateById(predictTask);
+            log.info("同步预测任务[ID:{}]成功", taskId);
+        }else if ("TRAIN_TASK".equals(sysJob.getJobGroup()) && trainTaskService != null) {
+            TrainTask trainTask = new TrainTask();
+            trainTask.setTTaskId(taskId);
+            trainTask.setTTaskName(sysJob.getJobName());
+            trainTask.setTCronExpression(sysJob.getCronExpression());
+            trainTask.setTTaskStatus(taskStatus);
+            trainTaskService.updateById(trainTask);
+            log.info("定时任务[ID:{}]同步训练任务[ID:{}]成功", sysJob.getJobId(), taskId);
+        } else {
+            log.warn("定时任务[ID:{}]任务组[{}]不支持同步,跳过", sysJob.getJobId(), sysJob.getJobGroup());
+        }
+    }
+}

+ 19 - 0
xvji-admin/src/main/java/com/xvji/config/RestTemplateConfig.java

@@ -0,0 +1,19 @@
+package com.xvji.config;
+
+import com.xvji.utils.FormDataHttpUtil;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+    @Bean
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+
+    @Bean
+    public FormDataHttpUtil formDataHttpUtil(RestTemplate restTemplate) {
+        return new FormDataHttpUtil(restTemplate);
+    }
+}

+ 1 - 1
xvji-admin/src/main/java/com/xvji/domain/PredictTask.java

@@ -29,7 +29,7 @@ public class PredictTask {
     private String pComponentIds;
 
     /**
-     * 任务状态:1成功 0失败
+     * 任务状态:1成功 0失败 2未运行
      */
     private Integer pTaskStatus;
 

+ 1 - 1
xvji-admin/src/main/java/com/xvji/domain/TrainTask.java

@@ -29,7 +29,7 @@ public class TrainTask {
     private String tComponentIds;
 
     /**
-     * 任务状态:1成功 0失败
+     * 任务状态:1成功 0失败 2未运行
      */
     private Integer tTaskStatus;
 

+ 9 - 0
xvji-admin/src/main/java/com/xvji/mapper/ComponentMapper.java

@@ -3,6 +3,10 @@ package com.xvji.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.xvji.domain.Component;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
 
 /**
  * 组件实体
@@ -10,4 +14,9 @@ import org.apache.ibatis.annotations.Mapper;
 @Mapper
 public interface ComponentMapper extends BaseMapper<Component> {
 
+    @Select("SELECT * FROM component WHERE TASK_ID = #{taskId} AND TASK_TYPE = #{taskType}")
+    List<Component> selectByTaskIdAndType(
+            @Param("taskId") Long taskId,
+            @Param("taskType") Integer taskType
+    );
 }

+ 134 - 0
xvji-admin/src/main/java/com/xvji/quartz/TaskQuartzJob.java

@@ -0,0 +1,134 @@
+package com.xvji.quartz;
+
+import com.xvji.domain.PredictTask;
+import com.xvji.domain.TrainTask;
+import com.xvji.service.PredictTaskService;
+import com.xvji.service.TrainTaskService;
+import com.xvji.service.task.TaskComponentExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+import org.quartz.DisallowConcurrentExecution;
+// 实现ApplicationContextAware,获取Spring上下文
+//阻止同一个任务并发执行
+//@DisallowConcurrentExecution
+@Component
+public class TaskQuartzJob implements ApplicationContextAware {
+
+    private static final Logger log = LoggerFactory.getLogger(TaskQuartzJob.class);
+    private static ApplicationContext applicationContext; // 静态Spring上下文
+
+    // 实现接口方法,保存Spring上下文
+    @Override
+    public void setApplicationContext(ApplicationContext context) throws BeansException {
+        TaskQuartzJob.applicationContext = context;
+    }
+
+    /**
+     * 手动获取Spring Bean(解决Quartz注入问题)
+     */
+    private <T> T getBean(Class<T> clazz) {
+        return applicationContext.getBean(clazz);
+    }
+
+    /**
+     * 预测任务执行入口:通过是否抛异常判断成功/失败(适配void返回值)
+     */
+    public void executePredictTask(Integer taskId) {
+        // 记录开始时间,用于计算执行耗时
+        long startTime = System.currentTimeMillis();
+
+        try {
+            // 1. 手动获取TaskComponentExecutor
+            TaskComponentExecutor componentExecutor = getBean(TaskComponentExecutor.class);
+
+            // 2. 执行核心业务逻辑(void方法,无返回值)
+            componentExecutor.executeComponents(taskId.longValue(), 1);
+
+            // 3. 若未抛异常,视为成功
+            PredictTaskService predictTaskService = getBean(PredictTaskService.class);
+            PredictTask predictTask = new PredictTask();
+            predictTask.setPTaskId(taskId.longValue());
+            predictTask.setPTaskStatus(1); // 1-成功
+            // 记录成功信息和耗时
+            long costTime = System.currentTimeMillis() - startTime;
+            predictTask.setPRunInfo("执行成功,耗时:" + costTime + "ms");
+
+            predictTaskService.updateById(predictTask);
+            log.info("预测任务[ID:{}]执行成功,状态更新为1,耗时:{}ms", taskId, costTime);
+
+        } catch (Exception e) {
+            // 4. 若抛异常,视为失败,记录异常信息
+            PredictTaskService predictTaskService = getBean(PredictTaskService.class);
+            PredictTask predictTask = new PredictTask();
+            predictTask.setPTaskId(taskId.longValue());
+            predictTask.setPTaskStatus(0); // 0-失败
+            // 记录失败原因和异常信息
+            long costTime = System.currentTimeMillis() - startTime;
+            predictTask.setPRunInfo("执行失败(耗时:" + costTime + "ms):" + e.getMessage());
+
+            predictTaskService.updateById(predictTask);
+            log.error("预测任务[ID:{}]执行失败,状态更新为0", taskId, e);
+            throw new RuntimeException("预测任务定时执行失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 训练任务执行入口:同理适配void返回值
+     */
+    public void executeTrainTask(Integer taskId) {
+        long startTime = System.currentTimeMillis();
+        String analysisReport = null;
+
+        try {
+            TaskComponentExecutor componentExecutor = getBean(TaskComponentExecutor.class);
+            // 执行void方法
+            componentExecutor.executeComponents(taskId.longValue(), 0);
+
+            // 获取分析报告组件的返回结果
+            analysisReport = componentExecutor.getAnalysisReportResult();
+
+            // 无异常  成功
+            TrainTaskService trainTaskService = getBean(TrainTaskService.class);
+            TrainTask trainTask = new TrainTask();
+            trainTask.setTTaskId(taskId.longValue());
+            trainTask.setTTaskStatus(1); // 1-成功
+            long costTime = System.currentTimeMillis() - startTime;
+            trainTask.setTRunInfo("执行成功,耗时:" + costTime + "ms");
+
+            // 将分析报告结果写入train_task表
+            if (analysisReport != null && !analysisReport.isEmpty()) {
+                trainTask.setTAnalysisReport(analysisReport);
+            }
+
+            trainTaskService.updateById(trainTask);
+            log.info("训练任务[ID:{}]执行成功,状态更新为1,耗时:{}ms,分析报告:{}", taskId, costTime, analysisReport);
+
+        } catch (Exception e) {
+            // 有异常  失败
+            TrainTaskService trainTaskService = getBean(TrainTaskService.class);
+            TrainTask trainTask = new TrainTask();
+            trainTask.setTTaskId(taskId.longValue());
+            trainTask.setTTaskStatus(0); // 0-失败
+            long costTime = System.currentTimeMillis() - startTime;
+            trainTask.setTRunInfo("执行失败(耗时:" + costTime + "ms):" + e.getMessage());
+
+            // 失败时也记录分析报告结果
+            if (analysisReport != null && !analysisReport.isEmpty()) {
+                trainTask.setTAnalysisReport(analysisReport);
+            }
+
+            trainTaskService.updateById(trainTask);
+            log.error("训练任务[ID:{}]执行失败,状态更新为0", taskId, e);
+            throw new RuntimeException("训练任务定时执行失败:" + e.getMessage(), e);
+
+        } finally {
+            // 清理ThreadLocal,避免内存泄漏
+            TaskComponentExecutor componentExecutor = getBean(TaskComponentExecutor.class);
+            componentExecutor.clearAnalysisReportResult();
+        }
+    }
+}

+ 2 - 0
xvji-admin/src/main/java/com/xvji/service/PredictTaskService.java

@@ -23,4 +23,6 @@ public interface PredictTaskService extends IService<PredictTask> {
 
     boolean deletePredictTaskWithComponents(Long taskId);
 
+    boolean updatePredictTask(PredictTask task);
+
 }

+ 3 - 0
xvji-admin/src/main/java/com/xvji/service/TrainTaskService.java

@@ -31,4 +31,7 @@ public interface TrainTaskService extends IService<TrainTask> {
      * @return
      */
     boolean deleteTrainTaskWithComponents(Long taskId);
+
+    boolean updateTrainTask(TrainTask task);
+
 }

+ 77 - 1
xvji-admin/src/main/java/com/xvji/service/impl/PredictTaskServiceImpl.java

@@ -2,15 +2,20 @@ package com.xvji.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.xvji.common.exception.job.TaskException;
 import com.xvji.domain.Component;
 import com.xvji.domain.PredictTask;
 import com.xvji.mapper.PredictTaskMapper;
+import com.xvji.quartz.domain.SysJob;
+import com.xvji.quartz.service.ISysJobService;
 import com.xvji.service.ComponentService;
 import com.xvji.service.PredictTaskService;
+import org.quartz.SchedulerException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import java.util.Date;
 import java.util.List;
 
@@ -18,8 +23,14 @@ import java.util.List;
 public class PredictTaskServiceImpl extends ServiceImpl<PredictTaskMapper, PredictTask> implements PredictTaskService {
 
     @Autowired
+    private ISysJobService sysJobService; // 若依定时任务Service
+
+    @Autowired
     private ComponentService componentService;
 
+    //Logger 对象
+    private static final Logger log = LoggerFactory.getLogger(PredictTaskServiceImpl.class);
+
     /**
      * 新增预测任务并关联组件
      */
@@ -79,8 +90,73 @@ public class PredictTaskServiceImpl extends ServiceImpl<PredictTaskMapper, Predi
                 .eq("TASK_TYPE" , 1); //预测任务类型为1
         componentService.remove(componentQueryWrapper);
         this.removeById(taskId);
+
+        //同步删除关联的定时任务
+        String targetInvokeTarget = "com.xvji.quartz.TaskQuartzJob.executePredictTask(" + taskId + ")";
+        sysJobService.deleteJobByParam(targetInvokeTarget);
+        log.info("预测任务[ID:{}]及关联组件、定时任务已删除", taskId);
+
         return true;
     }
 
+    /**
+     * 将业务任务同步到若依定时任务
+     */
+    private void syncToSysJob(PredictTask task) {
+        // 检查是否是定时任务同步过来的更新,如果是则跳过
+        // 通过判断任务名称是否包含特殊标记
+        if (task.getPTaskName() != null && task.getPTaskName().contains("[SYNCED]")) {
+            log.info("检测到是定时任务同步的更新,跳过反向同步");
+            // 移除标记,避免影响显示
+            task.setPTaskName(task.getPTaskName().replace("[SYNCED]", ""));
+            return;
+        }
+
+        Long taskId = task.getPTaskId();
+        String exactInvokeTarget = "com.xvji.quartz.TaskQuartzJob.executePredictTask(" + taskId + ")";
+        SysJob sysJob = sysJobService.selectJobByParam(exactInvokeTarget);
+
+        if (sysJob == null) {
+            log.error("未找到关联业务任务ID[{}]的定时任务,同步失败", taskId);
+            return;
+        }
+
+        // 校验Cron表达式
+        String cron = task.getPCronExpression();
+        if (cron != null && !sysJobService.checkCronExpressionIsValid(cron)) {
+            log.error("Cron表达式[{}]无效,同步终止", cron);
+            throw new RuntimeException("Cron表达式无效:" + cron);
+        }
+
+        // 同步核心字段
+        sysJob.setJobName(task.getPTaskName());
+        sysJob.setCronExpression(cron);
+        sysJob.setStatus(task.getPTaskStatus().toString());
+        sysJob.setJobGroup("PREDICT_TASK");
+
+        // 执行更新
+        boolean syncSuccess = sysJobService.updateSysJob(sysJob);
+        if (syncSuccess) {
+            log.info("预测任务[ID:{}]成功同步到定时任务[ID:{}]", taskId, sysJob.getJobId());
+        } else {
+            log.error("预测任务[ID:{}]同步到定时任务失败", taskId);
+        }
+    }
+
+
+    @Transactional
+    @Override
+    public boolean updatePredictTask(PredictTask task) {
+        // 更新业务任务表
+        boolean updateResult = this.updateById(task);
+        if (!updateResult) {
+            log.error("预测任务[ID:{}]更新失败,同步定时任务终止", task.getPTaskId());
+            return false;
+        }
+
+        //  调用同步方法,实现业务→定时同步
+        syncToSysJob(task);
+        return true;
+    }
 
 }

+ 92 - 8
xvji-admin/src/main/java/com/xvji/service/impl/TrainTaskServiceImpl.java

@@ -5,12 +5,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.xvji.domain.Component;
 import com.xvji.domain.TrainTask;
 import com.xvji.mapper.TrainTaskMapper;
+import com.xvji.quartz.domain.SysJob;
+import com.xvji.quartz.service.ISysJobService;
 import com.xvji.service.ComponentService;
 import com.xvji.service.TrainTaskService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import java.util.Date;
 import java.util.List;
 
@@ -22,6 +25,12 @@ public class TrainTaskServiceImpl extends ServiceImpl<TrainTaskMapper, TrainTask
 
     @Autowired
     private ComponentService componentService;
+    // 新增:注入定时任务Service
+    @Autowired
+    private ISysJobService sysJobService;
+
+    // 新增:日志对象
+    private static final Logger log = LoggerFactory.getLogger(TrainTaskServiceImpl.class);
 
     /**
      * 新增训练任务并关联单个组件
@@ -67,19 +76,94 @@ public class TrainTaskServiceImpl extends ServiceImpl<TrainTaskMapper, TrainTask
     @Transactional
     @Override
     public boolean deleteTrainTaskWithComponents(Long taskId) {
-
         TrainTask trainTask = this.getById(taskId);
-        if (trainTask == null){
-            //没有此id任务
+        if (trainTask == null) {
+            log.warn("训练任务[ID:{}]不存在,无需删除", taskId);
             return false;
         }
+
+        // 1. 删除关联组件(原有逻辑不变)
         QueryWrapper<Component> componentQueryWrapper = new QueryWrapper<>();
-        componentQueryWrapper
-                .eq("TASK_ID",taskId)
-                .eq("TASK_TYPE",0);//训练任务类型为0
-        componentService.remove(componentQueryWrapper);//删除任务相关组件
+        componentQueryWrapper.eq("TASK_ID", taskId).eq("TASK_TYPE", 0);
+        componentService.remove(componentQueryWrapper);
+
+        // 2. 删除训练任务(原有逻辑不变)
         this.removeById(taskId);
+
+        // 3. 新增:同步删除关联的定时任务
+        String targetInvokeTarget = "com.xvji.quartz.TaskQuartzJob.executeTrainTask(" + taskId + ")";
+        sysJobService.deleteJobByParam(targetInvokeTarget);
+        log.info("训练任务[ID:{}]及关联组件、定时任务已删除", taskId);
+
         return true;
+    }
+
+
+
+
+
+    /**
+     * 训练任务定时任务同步的核心方法
+     * @param task 训练任务对象
+     */
+    private void syncToSysJob(TrainTask task) {
+        // 防循环:如果是定时任务同步过来的更新(名称带[SYNCED]),跳过反向同步
+        if (task.getTTaskName() != null && task.getTTaskName().contains("[SYNCED]")) {
+            log.info("训练任务[ID:{}]是定时任务同步的更新,跳过反向同步", task.getTTaskId());
+            // 移除标记,避免显示异常
+            task.setTTaskName(task.getTTaskName().replace("[SYNCED]", ""));
+            this.updateById(task);
+            return;
+        }
+
+        Long taskId = task.getTTaskId();
+        // 精确匹配定时任务的invoke_target(格式:executeTrainTask(xxx))
+        String exactInvokeTarget = "com.xvji.quartz.TaskQuartzJob.executeTrainTask(" + taskId + ")";
+        SysJob sysJob = sysJobService.selectJobByParam(exactInvokeTarget);
 
+        if (sysJob == null) {
+            log.error("未找到训练任务[ID:{}]关联的定时任务,同步失败", taskId);
+            return;
+        }
+
+        // 校验Cron表达式有效性
+        String cron = task.getTCronExpression();
+        if (cron != null && !sysJobService.checkCronExpressionIsValid(cron)) {
+            log.error("训练任务[ID:{}]的Cron表达式[{}]无效,同步终止", taskId, cron);
+            throw new RuntimeException("Cron表达式无效:" + cron);
+        }
+
+        // 同步核心字段到定时任务
+        sysJob.setJobName(task.getTTaskName()); // 名称同步
+        sysJob.setCronExpression(cron); // Cron同步
+        sysJob.setStatus(task.getTTaskStatus().toString()); // 状态同步(Integer→String)
+        sysJob.setJobGroup("TRAIN_TASK"); // 固定任务组
+
+        // 执行定时任务更新
+        boolean syncSuccess = sysJobService.updateSysJob(sysJob);
+        if (syncSuccess) {
+            log.info("训练任务[ID:{}]成功同步到定时任务[ID:{}]", taskId, sysJob.getJobId());
+        } else {
+            log.error("训练任务[ID:{}]同步到定时任务失败", taskId);
+        }
+    }
+
+    @Transactional
+    @Override
+    public boolean updateTrainTask(TrainTask task) {
+        // 1. 先更新训练任务表
+        boolean updateResult = this.updateById(task);
+        if (!updateResult) {
+            log.error("训练任务[ID:{}]更新失败,同步定时任务终止", task.getTTaskId());
+            return false;
+        }
+
+        // 2. 调用已有同步方法,同步到定时任务
+        syncToSysJob(task);
+        return true;
     }
+
+
+
+
 }

+ 171 - 0
xvji-admin/src/main/java/com/xvji/service/task/TaskComponentExecutor.java

@@ -0,0 +1,171 @@
+package com.xvji.service.task;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.xvji.domain.Component;
+import com.xvji.mapper.ComponentMapper;
+import com.xvji.utils.FormDataHttpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 任务组件执行器:负责调用任务关联的所有组件接口
+ */
+@org.springframework.stereotype.Component
+public class TaskComponentExecutor {
+
+    private ThreadLocal<String> analysisReportResult = new ThreadLocal<>();
+
+    private static final Logger log = LoggerFactory.getLogger(TaskComponentExecutor.class);
+
+    @Autowired
+    private ComponentMapper componentMapper;
+
+    @Autowired
+    private RestTemplate restTemplate;
+
+    /**
+     * 执行指定任务的所有组件接口
+     * @param taskId 任务ID 训练任务或预测任务的ID
+     * @param taskType 任务类型0=训练任务,1=预测任务
+     */
+    public void executeComponents(Long taskId, Integer taskType) {
+        // 1. 查询关联组件
+        List<Component> components = componentMapper.selectByTaskIdAndType(taskId, taskType);
+        if (components.isEmpty()) {
+            log.warn("任务ID:{} 没有关联的组件,无需执行", taskId);
+            return;
+        }
+
+        // 记录任务开始时间(用于计算总耗时)
+        long taskStartTime = System.currentTimeMillis();
+
+        // 2. 循环调用组件(失败立即终止并抛异常)
+        for (Component component : components) {
+            // 跳过未启用的组件
+            if (!component.getIsEnable()) {
+                log.info("组件[{}]未启用,跳过执行", component.getComponentType());
+                continue;
+            }
+
+            String componentName = component.getComponentType();
+            String interfaceUrl = component.getInterfaceUrl();
+            Map<String, Object> params = component.getParamsMap();
+            long componentStartTime = System.currentTimeMillis();
+
+            log.info("开始调用组件[{}],接口地址:{},参数:{}",
+                    componentName, interfaceUrl, params);
+
+            try {
+                // 调用组件接口
+                ResponseEntity<String> response = FormDataHttpUtil.postFormData(interfaceUrl, params);
+                long costTime = System.currentTimeMillis() - componentStartTime;
+
+                // 处理HTTP响应(非2xx状态视为失败)
+                if (!response.getStatusCode().is2xxSuccessful()) {
+                    // 提取接口返回的错误信息(优先获取msg字段)
+                    String errorDetail = extractErrorMessage(response.getBody());
+                    String errorMsg = String.format(
+                            "组件[%s]调用失败(耗时:%dms),状态码:%d,错误信息:%s",
+                            componentName,
+                            costTime,
+                            response.getStatusCodeValue(),
+                            errorDetail
+                    );
+                    log.error(errorMsg);
+                    throw new RuntimeException(errorMsg); // 非2xx状态抛异常
+                }
+
+                log.info("组件[{}]调用成功(耗时:{}ms),返回结果:{}",
+                        componentName,
+                        System.currentTimeMillis() - componentStartTime,
+                        response.getBody());
+
+                // 修复:将componentType改为component.getComponentType(),result改为response.getBody()
+                if ("分析报告".equals(component.getComponentType())) {
+                    JSONObject resultJson = JSONObject.parseObject(response.getBody());
+                    // 优先取file_path(报告文件路径),无则取msg(结果信息)
+                    String report = resultJson.getString("file_path");
+                    if (report == null || report.isEmpty()) {
+                        report = resultJson.getString("msg");
+                    }
+                    analysisReportResult.set(report); // 存储到ThreadLocal
+                }
+
+            } catch (Exception e) {
+                // 捕获所有异常(包括连接失败、超时等),包装后重新抛出
+                long costTime = System.currentTimeMillis() - componentStartTime;
+                String errorMsg = String.format(
+                        "组件[%s]调用异常(耗时:%dms):%s",
+                        componentName,
+                        costTime,
+                        e.getMessage()
+                );
+                log.error(errorMsg, e);
+                throw new RuntimeException(errorMsg, e); // 向外层抛出异常,携带详细信息
+            }
+        }
+
+        log.info("任务ID:{} 所有组件执行完成,总耗时:{}ms",
+                taskId, System.currentTimeMillis() - taskStartTime);
+    }
+
+    /**
+     * 纯字符串解析方式提取msg字段,不依赖任何JSON库
+     * 适用于任何环境,兼容性100%
+     */
+    private String extractErrorMessage(String responseBody) {
+        if (responseBody == null || responseBody.trim().isEmpty()) {
+            return "无错误信息";
+        }
+
+        // 标准化处理:去除空格和制表符,便于匹配
+        String normalized = responseBody.replaceAll("\\s+", "");
+
+        // 查找"msg"字段的几种常见格式:"msg":"内容" 或 'msg':'内容' 或 msg:"内容"
+        String[] patterns = {"\"msg\":\"", "'msg':'", "msg\":\"", "\"msg':\"", "'msg\":\""};
+        for (String pattern : patterns) {
+            int startIndex = normalized.indexOf(pattern);
+            if (startIndex != -1) {
+                // 找到匹配的模式,计算内容起始位置
+                int contentStart = startIndex + pattern.length();
+
+                // 查找内容结束位置(引号)
+                int contentEnd = normalized.indexOf("\"", contentStart);
+                if (contentEnd == -1) {
+                    contentEnd = normalized.indexOf("'", contentStart);
+                }
+                if (contentEnd == -1) {
+                    contentEnd = normalized.indexOf("}", contentStart);
+                }
+                if (contentEnd == -1) {
+                    contentEnd = normalized.indexOf(",", contentStart);
+                }
+
+                // 提取内容
+                if (contentEnd > contentStart) {
+                    return normalized.substring(contentStart, contentEnd);
+                }
+            }
+        }
+
+        // 如果没找到msg字段,返回原始响应的前200个字符
+        int maxLength = Math.min(responseBody.length(), 200);
+        return responseBody.substring(0, maxLength) + (responseBody.length() > 200 ? "..." : "");
+    }
+
+    // 提供方法获取分析报告结果
+    public String getAnalysisReportResult() {
+        return analysisReportResult.get();
+    }
+
+    // 任务执行完成后清理ThreadLocal(避免内存泄漏)
+    public void clearAnalysisReportResult() {
+        analysisReportResult.remove();
+    }
+}

+ 53 - 0
xvji-admin/src/main/java/com/xvji/utils/FormDataHttpUtil.java

@@ -0,0 +1,53 @@
+package com.xvji.utils;
+
+import org.springframework.http.*;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Map;
+
+/**
+ * 自动将 Map 转换为 form-data 格式发送请求的工具类
+ */
+public class FormDataHttpUtil {
+
+    // 注入你项目中已有的 RestTemplate(如果是 Spring 环境,建议用 @Autowired 注入)
+    private static RestTemplate restTemplate = new RestTemplate();
+
+    // 如果你用的是 Spring 框架,建议用构造器注入 RestTemplate(更符合 Spring 规范)
+    public FormDataHttpUtil(RestTemplate restTemplate) {
+        FormDataHttpUtil.restTemplate = restTemplate;
+    }
+
+    /**
+     * 发送 form-data 格式的 POST 请求
+     * @param url 接口地址
+     * @param params 原有 Map 格式的参数(无需修改)
+     * @return 接口返回结果
+     */
+    public static ResponseEntity<String> postFormData(String url, Map<String, Object> params) {
+        // 1. 创建 form-data 格式的参数容器
+        MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
+
+        // 2. 将原有 Map 中的参数逐个添加到 form-data 中
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            // 注意:如果 value 是 null,可能需要处理
+            if (value != null) {
+                formData.add(key, value.toString()); // 转为字符串,兼容大多数接口
+            }
+        }
+
+        // 3. 设置请求头为 form-data 格式
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+
+        // 4. 包装请求体和头信息
+        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
+
+        // 5. 发送请求(复用原有 RestTemplate)
+        return restTemplate.postForEntity(url, requestEntity, String.class);
+    }
+}

+ 29 - 0
xvji-admin/src/main/java/com/xvji/utils/InvokeTargetUtils.java

@@ -0,0 +1,29 @@
+package com.xvji.utils;
+
+/**
+ * 从若依定时任务的invokeTarget中解析业务任务ID
+ */
+public class InvokeTargetUtils {
+
+    /**
+     * 解析invokeTarget,提取业务任务ID
+     * @param invokeTarget 格式:com.xvji.quartz.TaskQuartzJob.executePredictTask(1001)
+     * @return 业务任务ID(如1001),解析失败返回null
+     */
+    public static Long parseTaskIdFromInvokeTarget(String invokeTarget) {
+        if (invokeTarget == null || !invokeTarget.contains("(") || !invokeTarget.contains(")")) {
+            return null; // 格式不正确
+        }
+        // 截取括号中的内容(如从"xxx(1001)"中截取"1001")
+        String taskIdStr = invokeTarget.substring(
+                invokeTarget.indexOf("(") + 1,
+                invokeTarget.indexOf(")")
+        );
+        // 转为Long类型
+        try {
+            return Long.parseLong(taskIdStr.trim());
+        } catch (NumberFormatException e) {
+            return null; // 参数不是数字
+        }
+    }
+}

+ 6 - 6
xvji-admin/src/main/java/com/xvji/web/controller/PredictTaskController.java

@@ -57,8 +57,8 @@ public class PredictTaskController {
             if (ptaskStatusObj != null){
                 try {
                     Integer ptaskStatus = Integer.parseInt(ptaskStatusObj.toString());
-                    if (ptaskStatus != 1 && ptaskStatus != 0){
-                        return AjaxResult.error("任务状态必须是0或1,请检查输入");
+                    if (ptaskStatus != 1 && ptaskStatus != 0 && ptaskStatus != 2){
+                        return AjaxResult.error("任务状态必须是0,1,2,请检查输入");
                     }
                     task.setPTaskStatus(ptaskStatus);
                 }catch (Exception e){
@@ -138,7 +138,7 @@ public class PredictTaskController {
                 }
             }
 
-            // 模型组件(预测任务不需要额外添加modelTest组件)
+            // 模型组件
             Map<String, Object> model = (Map<String, Object>) taskInfo.get("model");
             if (model != null) {
                 Boolean isEnable = parseEnableValue(model.get("isEnable"), "模型");
@@ -211,7 +211,7 @@ public class PredictTaskController {
      * 分页查询预测任务及关联组件信息
      * @param pageNum 页码(默认1)
      * @param pageSize 每页条数(默认10)
-     * @param taskName 任务名称(可选,用于模糊查询)
+     * @param taskName 任务名称
      */
     @GetMapping("/queryTasks")
     public AjaxResult queryPredictTasks(
@@ -312,8 +312,8 @@ public class PredictTaskController {
                 Object pTaskStatusObj = taskInfo.get("pTaskStatus");
                 if (pTaskStatusObj != null){
                     pTaskStatus = Integer.parseInt(pTaskStatusObj.toString());
-                    if (pTaskStatus != 0 && pTaskStatus != 1){
-                        return AjaxResult.error("任务状态必须是0或1,请检查输入");
+                    if (pTaskStatus != 0 && pTaskStatus != 1 && pTaskStatus != 2){
+                        return AjaxResult.error("任务状态必须是0,1,2,请检查输入");
                     }
                 }
             }catch (Exception e){

+ 8 - 5
xvji-admin/src/main/java/com/xvji/web/controller/TrainTaskController.java

@@ -63,8 +63,8 @@ public class TrainTaskController {
             if (taskStatusObj != null){
                 try {
                     Integer tTaskStatus = Integer.parseInt(taskStatusObj.toString());
-                    if (tTaskStatus != 0 && tTaskStatus != 1) {
-                        return AjaxResult.error("任务状态必须是0或1,请检查输入");
+                    if (tTaskStatus != 0 && tTaskStatus != 1 && tTaskStatus != 2) {
+                        return AjaxResult.error("任务状态必须是0,1,2,请检查输入");
                     }
                     task.setTTaskStatus(tTaskStatus);
                 }catch (Exception e){
@@ -254,7 +254,10 @@ public class TrainTaskController {
                 TrainTaskVO vo = new TrainTaskVO();
                 BeanUtils.copyProperties(task, vo);
 
-                // 解析组件ID并查询组件
+                // 关键修改:将分析报告值添加到VO中,前端列表可直接获取
+                vo.setTAnalysisReport(task.getTAnalysisReport());
+
+                // 解析组件ID并查询组件(原有逻辑)
                 String componentIds = task.getTComponentIds();
                 if (StringUtils.hasText(componentIds)) {
                     List<Long> ids = Arrays.stream(componentIds.split(","))
@@ -332,8 +335,8 @@ public class TrainTaskController {
             if (taskStatusObj != null){
                 try {
                     tTaskStatus = Integer.parseInt(taskStatusObj.toString());
-                    if (tTaskStatus != 0 && tTaskStatus != 1) {
-                        return AjaxResult.error("任务状态必须是0或1,请检查输入");
+                    if (tTaskStatus != 0 && tTaskStatus != 1 && tTaskStatus != 2) {
+                        return AjaxResult.error("任务状态必须是0,1,2,请检查输入");
                     }
                 }catch (Exception e){
                     return AjaxResult.error("传入的任务状态格式不正确");

+ 197 - 0
xvji-admin/src/test/java/com/xvji/admin/PredictTaskServiceTest.java

@@ -0,0 +1,197 @@
+package com.xvji.admin;
+
+import com.xvji.domain.Component;
+import com.xvji.domain.PredictTask;
+import com.xvji.quartz.domain.SysJob;
+import com.xvji.quartz.service.ISysJobService;
+import com.xvji.service.ComponentService;
+import com.xvji.service.PredictTaskService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.quartz.SchedulerException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import static org.junit.jupiter.api.Assertions.*;
+
+@SpringBootTest
+@RunWith(SpringRunner.class)
+public class PredictTaskServiceTest {
+
+    @Autowired
+    private PredictTaskService predictTaskService;
+
+    @Autowired
+    private ComponentService componentService; // 如需测试组件关联
+
+    @Autowired
+    private ISysJobService sysJobService;
+
+    private static final Logger log = LoggerFactory.getLogger(PredictTaskServiceTest.class);
+
+
+    // 测试用例:预测任务ID(提前在数据库准备或通过测试新增)
+    private static final Long TEST_TASK_ID = 6005L;
+    // 临时Cron:10秒后触发(便于测试,避免长时间等待)
+    private static final String TEMP_CRON = "0/10 * * * * ?"; // 每10秒执行一次
+
+    /**
+     * 测试查询预测任务
+     */
+    @Test
+    public void testQueryPredictTask() {
+        // 1. 查询所有任务
+        List<PredictTask> allTasks = predictTaskService.getAllPredictTasks();
+        assertFalse(allTasks.isEmpty(), "预测任务列表不应为空");
+
+        // 2. 查找目标任务
+        PredictTask targetTask = allTasks.stream()
+                .filter(task -> TEST_TASK_ID.equals(task.getPTaskId()))
+                .findFirst()
+                .orElse(null);
+        
+        assertNotNull(targetTask, "ID=" + TEST_TASK_ID + "的预测任务应存在");
+        System.out.println("查询到目标任务:" + targetTask.getPTaskName());
+    }
+
+    /**
+     * 测试编辑预测任务并同步到定时任务
+     */
+    @Test
+    public void testUpdateAndSyncPredictTask() {
+        // 1. 构建更新对象
+        PredictTask updateTask = new PredictTask();
+        updateTask.setPTaskId(TEST_TASK_ID);
+        updateTask.setPTaskName("测试编辑-更新后名称"); // 新名称
+        updateTask.setPCronExpression("0 15 3 * * ?");  // 新Cron(每天3:15执行)
+        updateTask.setPTaskStatus(0);                   // 状态改为停用
+
+        // 2. 执行更新
+        boolean updateResult = predictTaskService.updatePredictTask(updateTask);
+        assertTrue(updateResult, "预测任务更新应成功");
+
+        // 3. 验证业务表更新结果
+        PredictTask updatedTask = predictTaskService.getById(TEST_TASK_ID);
+        assertEquals("测试编辑-更新后名称", updatedTask.getPTaskName(), "任务名称未同步");
+        assertEquals("0 15 3 * * ?", updatedTask.getPCronExpression(), "Cron表达式未同步");
+        assertEquals(0, updatedTask.getPTaskStatus(), "任务状态未同步");
+    }
+
+    /**
+     * 测试删除预测任务(级联删除组件和定时任务)
+     */
+    @Test
+    public void testDeletePredictTask() {
+        // 1. 执行删除
+        boolean deleteResult = predictTaskService.deletePredictTaskWithComponents(TEST_TASK_ID);
+        assertTrue(deleteResult, "预测任务删除应成功");
+
+        // 2. 验证业务任务已删除
+        PredictTask deletedTask = predictTaskService.getById(TEST_TASK_ID);
+        assertNull(deletedTask, "预测任务未从业务表删除");
+
+        // 3. 验证关联组件已删除(如需验证组件)
+        /*
+        List<Component> relatedComponents = componentService.list(
+            new QueryWrapper<Component>()
+                .eq("TASK_ID", TEST_TASK_ID)
+                .eq("TASK_TYPE", 1)
+        );
+        assertTrue(relatedComponents.isEmpty(), "关联组件未级联删除");
+        */
+    }
+
+    /**
+     * 测试无效Cron表达式同步拦截
+     */
+    @Test
+    public void testInvalidCronSync() {
+        // 1. 构建含无效Cron的更新对象
+        PredictTask invalidTask = new PredictTask();
+        invalidTask.setPTaskId(TEST_TASK_ID);
+        invalidTask.setPCronExpression("* * *"); // 无效Cron(缺少字段)
+
+        // 2. 执行更新并验证异常
+        assertThrows(RuntimeException.class, 
+            () -> predictTaskService.updatePredictTask(invalidTask), 
+            "无效Cron应抛出异常"
+        );
+    }
+
+    /**
+     * (可选)测试新增预测任务(如需完整覆盖)
+     */
+    @Test
+    public void testAddPredictTask() {
+        // 1. 构建新任务
+        PredictTask newTask = new PredictTask();
+        newTask.setPTaskName("测试新增任务");
+        newTask.setPCronExpression("0 0 12 * * ?");
+        newTask.setPTaskStatus(1);
+
+        // 2. 构建关联组件
+        Component component = new Component();
+        component.setComponentType("测试组件");
+        component.setIsEnable(true);
+
+        // 3. 执行新增
+        boolean addResult = predictTaskService.addPredictTaskWithComponent(newTask, component);
+        assertTrue(addResult, "新增预测任务应成功");
+        assertNotNull(newTask.getPTaskId(), "新增任务应自动生成ID");
+        System.out.println("新增任务ID:" + newTask.getPTaskId());
+    }
+
+    /**
+     * 测试流程:
+     * 1. 配置定时任务为10秒触发一次(临时Cron)
+     * 2. 等待定时任务触发并调用目标接口
+     * 3. 通过日志和执行结果验证接口是否被正确调用
+     */
+    @Test
+    public void testCronTriggerInterface() throws InterruptedException, SchedulerException {
+        // 1. 查找ID=6005关联的定时任务
+        String conditionValue = "com.xvji.quartz.TaskQuartzJob.executePredictTask(" + TEST_TASK_ID + ")";
+        SysJob targetJob = sysJobService.getSysJobByCondition(conditionValue);
+        assertNotNull(targetJob, "未找到ID=" + TEST_TASK_ID + "关联的定时任务");
+
+        // 2. 保存定时任务原有配置(测试后恢复,避免影响真实数据)
+        String originalCron = targetJob.getCronExpression();
+        String originalStatus = targetJob.getStatus();
+
+        try {
+            // 3. 修改定时任务为10秒触发一次,并启用
+            targetJob.setCronExpression(TEMP_CRON);
+            targetJob.setStatus("1"); // 启用任务
+            boolean updateResult = sysJobService.updateSysJob(targetJob);
+            assertTrue(updateResult, "修改定时任务Cron失败");
+            log.info("已将定时任务[ID:{}]修改为每10秒触发一次,等待执行...", targetJob.getJobId());
+
+            // 4. 等待30秒(确保至少触发2次)
+            TimeUnit.SECONDS.sleep(30);
+
+            // 5. 验证结果(通过日志或执行记录,以下为示例逻辑)
+            // 5.1 检查目标接口是否被调用(需结合业务日志或执行记录表)
+            log.info("请查看业务日志,确认是否输出 'executePredictTask(6005) 执行成功' 等类似记录");
+
+            // 5.2 (可选)如果接口有执行结果存储,可直接查询验证
+            /*
+            ExecutionRecord record = executionRecordService.getLastRecord(TEST_TASK_ID);
+            assertNotNull(record, "未找到接口执行记录");
+            assertEquals("SUCCESS", record.getStatus(), "接口执行失败");
+            */
+
+        } finally {
+            // 6. 恢复定时任务原有配置(关键:避免测试污染数据)
+            targetJob.setCronExpression(originalCron);
+            targetJob.setStatus(originalStatus);
+            sysJobService.updateSysJob(targetJob);
+            log.info("已恢复定时任务[ID:{}]原有配置:Cron={}, 状态={}",
+                    targetJob.getJobId(), originalCron, originalStatus);
+        }
+    }
+}

+ 23 - 0
xvji-admin/src/test/java/com/xvji/admin/TaskComponentTest.java

@@ -0,0 +1,23 @@
+package com.xvji.admin;
+
+import com.xvji.service.task.TaskComponentExecutor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@SpringBootTest
+@RunWith(SpringRunner.class)
+public class TaskComponentTest {
+
+    @Autowired
+    private TaskComponentExecutor componentExecutor;
+
+    @Test
+    public void testExecuteComponents() {
+        Long taskId = 6005L;  // 替换为实际任务ID
+        Integer taskType = 1; // 0=训练任务,1=预测任务
+        componentExecutor.executeComponents(taskId, taskType);
+    }
+}

+ 6 - 0
xvji-quartz/pom.xml

@@ -35,6 +35,12 @@
             <artifactId>xvji-common</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>3.5.3.1</version>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 19 - 1
xvji-quartz/src/main/java/com/xvji/quartz/mapper/SysJobMapper.java

@@ -2,12 +2,14 @@ package com.xvji.quartz.mapper;
 
 import java.util.List;
 import com.xvji.quartz.domain.SysJob;
-
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 /**
  * 调度任务信息 数据层
  * 
  * @author ruoyi
  */
+@Mapper
 public interface SysJobMapper
 {
     /**
@@ -64,4 +66,20 @@ public interface SysJobMapper
      * @return 结果
      */
     public int insertJob(SysJob job);
+
+    /**
+     * 根据字段条件查询单个定时任务
+     * @param field 数据库字段名
+     * @param value 字段值
+     * @return 单个定时任务对象
+     */
+    public SysJob selectJobByField(@Param("field") String field, @Param("value") String value);
+
+    /**
+     * 根据字段条件删除定时任务
+     * @param field 数据库字段名
+     * @param value 字段值
+     * @return 影响行数
+     */
+    public int deleteJobByField(@Param("field") String field, @Param("value") String value);
 }

+ 36 - 4
xvji-quartz/src/main/java/com/xvji/quartz/service/ISysJobService.java

@@ -1,6 +1,8 @@
 package com.xvji.quartz.service;
 
 import java.util.List;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.quartz.SchedulerException;
 import com.xvji.common.exception.job.TaskException;
 import com.xvji.quartz.domain.SysJob;
@@ -22,7 +24,6 @@ public interface ISysJobService
 
     /**
      * 通过调度任务ID查询调度信息
-     * 
      * @param jobId 调度任务ID
      * @return 调度任务对象信息
      */
@@ -46,7 +47,6 @@ public interface ISysJobService
 
     /**
      * 删除任务后,所对应的trigger也将被删除
-     * 
      * @param job 调度信息
      * @return 结果
      */
@@ -54,7 +54,6 @@ public interface ISysJobService
 
     /**
      * 批量删除调度信息
-     * 
      * @param jobIds 需要删除的任务ID
      * @return 结果
      */
@@ -62,7 +61,6 @@ public interface ISysJobService
 
     /**
      * 任务调度状态修改
-     * 
      * @param job 调度信息
      * @return 结果
      */
@@ -99,4 +97,38 @@ public interface ISysJobService
      * @return 结果
      */
     public boolean checkCronExpressionIsValid(String cronExpression);
+
+    /**
+     * 根据任务参数查询 匹配 invoke_target
+     * @param jobParam 完整invoke_target
+     * @return 定时任务对象
+     */
+    SysJob selectJobByParam(String jobParam);
+
+    /**
+     * 根据任务参数删除 匹配 invoke_target
+     * @param jobParam 完整的 invoke_target 值
+     */
+    void deleteJobByParam(String jobParam);
+
+    /**
+     * 按条件查询单个定时任务 模糊匹配 invoke_target
+     * @param conditionValue 模糊匹配值
+     * @return 定时任务对象
+     */
+    SysJob getSysJobByCondition(String conditionValue); // 与实现类参数一致
+
+    /**
+     * 按条件删除定时任务 匹配指定字段
+     * @param field 数据库字段名
+     * @param value 字段值
+     */
+    void deleteSysJobByCondition(String field, String value); // 与实现类参数一致
+
+    /**
+     * 更新定时任务
+     * @param sysJob 定时任务对象
+     * @return true=成功,false=失败
+     */
+    boolean updateSysJob(SysJob sysJob);
 }

+ 216 - 15
xvji-quartz/src/main/java/com/xvji/quartz/service/impl/SysJobServiceImpl.java

@@ -6,6 +6,7 @@ import org.quartz.JobDataMap;
 import org.quartz.JobKey;
 import org.quartz.Scheduler;
 import org.quartz.SchedulerException;
+import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -16,14 +17,19 @@ import com.xvji.quartz.mapper.SysJobMapper;
 import com.xvji.quartz.service.ISysJobService;
 import com.xvji.quartz.util.CronUtils;
 import com.xvji.quartz.util.ScheduleUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import java.lang.reflect.Method;
 
 /**
  * 定时任务调度信息 服务层
  * 
- * @author ruoyi
+ * @author lt
  */
 @Service
-public class SysJobServiceImpl implements ISysJobService
+public class SysJobServiceImpl implements ISysJobService, ApplicationContextAware
 {
     @Autowired
     private Scheduler scheduler;
@@ -31,6 +37,17 @@ public class SysJobServiceImpl implements ISysJobService
     @Autowired
     private SysJobMapper jobMapper;
 
+    private static final Logger log = LoggerFactory.getLogger(SysJobServiceImpl.class);
+
+
+    //Spring上下文(用于获取其他模块的Bean)
+    private ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+
     /**
      * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据)
      */
@@ -150,20 +167,79 @@ public class SysJobServiceImpl implements ISysJobService
      * 
      * @param job 调度信息
      */
+    // 修改 SysJobServiceImpl 的 changeStatus 方法
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public int changeStatus(SysJob job) throws SchedulerException
-    {
+    public int changeStatus(SysJob job) throws SchedulerException {
         int rows = 0;
-        String status = job.getStatus();
-        if (ScheduleConstants.Status.NORMAL.getValue().equals(status))
-        {
-            rows = resumeJob(job);
+        String cronStatus = job.getStatus(); // 定时任务状态:0=启用,1=暂停
+        Long jobId = job.getJobId();
+        String jobGroup = job.getJobGroup();
+
+        // 1. 处理定时任务自身状态变更
+        if (ScheduleConstants.Status.NORMAL.getValue().equals(cronStatus)) {
+            rows = resumeJob(job); // 启用:cronStatus=0
+        } else if (ScheduleConstants.Status.PAUSE.getValue().equals(cronStatus)) {
+            rows = pauseJob(job); // 暂停:cronStatus=1
         }
-        else if (ScheduleConstants.Status.PAUSE.getValue().equals(status))
-        {
-            rows = pauseJob(job);
+
+        // 2. 同步业务任务状态(核心:按你的需求映射)
+        if (rows > 0 && ("PREDICT_TASK".equals(jobGroup) || "TRAIN_TASK".equals(jobGroup))) {
+            try {
+                // 解析业务任务ID
+                Class<?> invokeTargetUtilsClass = Class.forName("com.xvji.utils.InvokeTargetUtils");
+                Method parseMethod = invokeTargetUtilsClass.getMethod("parseTaskIdFromInvokeTarget", String.class);
+                Long taskId = (Long) parseMethod.invoke(null, job.getInvokeTarget());
+                if (taskId == null) {
+                    log.error("定时任务[ID:{}]状态变更,无法解析业务任务ID", jobId);
+                    return rows;
+                }
+
+                // 按你的需求映射状态:
+                Integer bizStatus;
+                if ("1".equals(cronStatus)) {
+                    // 定时任务暂停(1)→ 业务任务状态=2(未启用)
+                    bizStatus = 2;
+                } else {
+                    // 定时任务启用(0)→ 保留上次执行结果(不修改)
+                    log.info("定时任务[ID:{}]启用,业务任务[ID:{}]保留上次状态(0失败/1成功)", jobId, taskId);
+                    return rows;
+                }
+
+                // 同步到预测任务
+                if ("PREDICT_TASK".equals(jobGroup)) {
+                    Object predictTaskService = applicationContext.getBean("predictTaskServiceImpl");
+                    Class<?> predictTaskClass = Class.forName("com.xvji.domain.PredictTask");
+                    Object predictTask = predictTaskClass.newInstance();
+                    // 只更新ID和状态
+                    Method setPTaskId = predictTaskClass.getMethod("setPTaskId", Long.class);
+                    Method setPTaskStatus = predictTaskClass.getMethod("setPTaskStatus", Integer.class);
+                    setPTaskId.invoke(predictTask, taskId);
+                    setPTaskStatus.invoke(predictTask, bizStatus);
+                    // 调用更新
+                    Method updateMethod = predictTaskService.getClass().getMethod("updateById", Object.class);
+                    updateMethod.invoke(predictTaskService, predictTask);
+                    log.info("定时任务[ID:{}]暂停,同步预测任务[ID:{}]状态为2(未启用)", jobId, taskId);
+                }
+                // 同步到训练任务
+                else if ("TRAIN_TASK".equals(jobGroup)) {
+                    Object trainTaskService = applicationContext.getBean("trainTaskServiceImpl");
+                    Class<?> trainTaskClass = Class.forName("com.xvji.domain.TrainTask");
+                    Object trainTask = trainTaskClass.newInstance();
+                    Method setTTaskId = trainTaskClass.getMethod("setTTaskId", Long.class);
+                    Method setTTaskStatus = trainTaskClass.getMethod("setTTaskStatus", Integer.class);
+                    setTTaskId.invoke(trainTask, taskId);
+                    setTTaskStatus.invoke(trainTask, bizStatus);
+                    Method updateMethod = trainTaskService.getClass().getMethod("updateById", Object.class);
+                    updateMethod.invoke(trainTaskService, trainTask);
+                    log.info("定时任务[ID:{}]暂停,同步训练任务[ID:{}]状态为2(未启用)", jobId, taskId);
+                }
+
+            } catch (Exception e) {
+                log.error("定时任务[ID:{}]状态变更,同步业务任务失败", jobId, e);
+            }
         }
+
         return rows;
     }
 
@@ -210,17 +286,103 @@ public class SysJobServiceImpl implements ISysJobService
      */
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public int updateJob(SysJob job) throws SchedulerException, TaskException
-    {
+    public int updateJob(SysJob job) throws SchedulerException, TaskException {
         SysJob properties = selectJobById(job.getJobId());
         int rows = jobMapper.updateJob(job);
-        if (rows > 0)
-        {
+        if (rows > 0) {
             updateSchedulerJob(job, properties.getJobGroup());
+
+            log.info("定时任务[ID:{}]更新后,开始同步业务任务", job.getJobId());
+            // 1. 预测任务同步(原有逻辑不变)
+            if ("PREDICT_TASK".equals(job.getJobGroup())) {
+                try {
+                    Class<?> invokeTargetUtilsClass = Class.forName("com.xvji.utils.InvokeTargetUtils");
+                    Method parseMethod = invokeTargetUtilsClass.getMethod("parseTaskIdFromInvokeTarget", String.class);
+                    Long taskId = (Long) parseMethod.invoke(null, job.getInvokeTarget());
+
+                    if (taskId == null) {
+                        log.error("无法解析预测任务ID");
+                        return rows;
+                    }
+
+                    Integer taskStatus = ScheduleConstants.Status.NORMAL.getValue().equals(job.getStatus()) ? 1 : 0;
+                    Object predictTaskService = applicationContext.getBean("predictTaskServiceImpl");
+                    if (predictTaskService == null) {
+                        log.error("未找到predictTaskService");
+                        return rows;
+                    }
+
+                    Class<?> predictTaskClass = Class.forName("com.xvji.domain.PredictTask");
+                    Object predictTask = predictTaskClass.newInstance();
+                    Method setPTaskId = predictTaskClass.getMethod("setPTaskId", Long.class);
+                    Method setPCronExpression = predictTaskClass.getMethod("setPCronExpression", String.class);
+                    Method setPTaskName = predictTaskClass.getMethod("setPTaskName", String.class);
+                    Method setPTaskStatus = predictTaskClass.getMethod("setPTaskStatus", Integer.class);
+
+                    setPTaskId.invoke(predictTask, taskId);
+                    setPCronExpression.invoke(predictTask, job.getCronExpression());
+                    setPTaskName.invoke(predictTask, job.getJobName() + "[SYNCED]");
+                    setPTaskStatus.invoke(predictTask, taskStatus);
+
+                    Method updateMethod = predictTaskService.getClass().getMethod("updateById", Object.class);
+                    updateMethod.invoke(predictTaskService, predictTask);
+                    log.info("同步预测任务[ID:{}]成功", taskId);
+
+                } catch (Exception e) {
+                    log.error("同步预测任务失败", e);
+                }
+            }
+            // 2. 新增:训练任务同步(和预测任务逻辑完全对齐)
+            else if ("TRAIN_TASK".equals(job.getJobGroup())) {
+                try {
+                    // 解析训练任务ID
+                    Class<?> invokeTargetUtilsClass = Class.forName("com.xvji.utils.InvokeTargetUtils");
+                    Method parseMethod = invokeTargetUtilsClass.getMethod("parseTaskIdFromInvokeTarget", String.class);
+                    Long taskId = (Long) parseMethod.invoke(null, job.getInvokeTarget());
+
+                    if (taskId == null) {
+                        log.error("无法解析训练任务ID");
+                        return rows;
+                    }
+
+                    // 状态转换
+                    Integer taskStatus = ScheduleConstants.Status.NORMAL.getValue().equals(job.getStatus()) ? 1 : 0;
+
+                    // 获取训练任务Service
+                    Object trainTaskService = applicationContext.getBean("trainTaskServiceImpl");
+                    if (trainTaskService == null) {
+                        log.error("未找到trainTaskService");
+                        return rows;
+                    }
+
+                    // 反射设置训练任务属性
+                    Class<?> trainTaskClass = Class.forName("com.xvji.domain.TrainTask");
+                    Object trainTask = trainTaskClass.newInstance();
+                    Method setTTaskId = trainTaskClass.getMethod("setTTaskId", Long.class);
+                    Method setTCronExpression = trainTaskClass.getMethod("setTCronExpression", String.class);
+                    Method setTTaskName = trainTaskClass.getMethod("setTTaskName", String.class);
+                    Method setTTaskStatus = trainTaskClass.getMethod("setTTaskStatus", Integer.class);
+
+                    setTTaskId.invoke(trainTask, taskId);
+                    setTCronExpression.invoke(trainTask, job.getCronExpression());
+                    setTTaskName.invoke(trainTask, job.getJobName() + "[SYNCED]"); // 防循环标记
+                    setTTaskStatus.invoke(trainTask, taskStatus);
+
+                    // 调用更新
+                    Method updateMethod = trainTaskService.getClass().getMethod("updateById", Object.class);
+                    updateMethod.invoke(trainTaskService, trainTask);
+                    log.info("同步训练任务[ID:{}]成功", taskId);
+
+                } catch (Exception e) {
+                    log.error("同步训练任务失败", e);
+                }
+            }
         }
         return rows;
     }
 
+
+
     /**
      * 更新任务
      * 
@@ -251,4 +413,43 @@ public class SysJobServiceImpl implements ISysJobService
     {
         return CronUtils.isValid(cronExpression);
     }
+
+    @Override
+    public SysJob selectJobByParam(String jobParam) {
+        // 调用 Mapper 新增的 selectJobByField,完整匹配 invoke_target
+        return jobMapper.selectJobByField("invoke_target", jobParam);
+    }
+
+    @Override
+    public void deleteJobByParam(String jobParam) {
+        // 调用 Mapper 新增的 deleteJobByField,完整匹配 invoke_target
+        jobMapper.deleteJobByField("invoke_target", jobParam);
+    }
+
+    @Override
+    public SysJob getSysJobByCondition(String conditionValue) {
+        // 模糊匹配 invoke_target(如包含"executePredictTask(1001)")
+        SysJob queryJob = new SysJob();
+        queryJob.setInvokeTarget("%" + conditionValue + "%");
+        List<SysJob> jobList = jobMapper.selectJobList(queryJob);
+        return jobList != null && !jobList.isEmpty() ? jobList.get(0) : null;
+    }
+
+    @Override
+    public void deleteSysJobByCondition(String field, String value) {
+        // 按指定字段删除(如按 invoke_target 完整匹配)
+        jobMapper.deleteJobByField(field, value);
+    }
+
+    @Override
+    public boolean updateSysJob(SysJob sysJob) {
+        try {
+            // 复用若依原生 updateJob 方法
+            int rows = updateJob(sysJob);
+            return rows > 0;
+        } catch (SchedulerException | TaskException e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
 }

+ 13 - 0
xvji-quartz/src/main/java/com/xvji/quartz/task/XjTask.java

@@ -0,0 +1,13 @@
+package com.xvji.quartz.task;
+import org.openxmlformats.schemas.spreadsheetml.x2006.main.STSourceType;
+import org.springframework.stereotype.Component;
+import com.xvji.common.utils.StringUtils;
+
+@Component("XjTask")
+public class XjTask {
+
+    public void test01(){
+        System.out.println("执行test01");
+
+    }
+}

+ 3 - 1
xvji-quartz/src/main/java/com/xvji/quartz/util/QuartzJobExecution.java

@@ -2,13 +2,15 @@ package com.xvji.quartz.util;
 
 import org.quartz.JobExecutionContext;
 import com.xvji.quartz.domain.SysJob;
-
+import org.quartz.DisallowConcurrentExecution;
 /**
  * 定时任务处理(允许并发执行)
  * 
  * @author ruoyi
  *
  */
+//阻止同一个任务并发执行
+@DisallowConcurrentExecution
 public class QuartzJobExecution extends AbstractQuartzJob
 {
     @Override

+ 4 - 2
xvji-quartz/src/main/java/com/xvji/quartz/util/ScheduleUtils.java

@@ -65,9 +65,11 @@ public class ScheduleUtils
         String jobGroup = job.getJobGroup();
         JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
 
+        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression())
+                .withMisfireHandlingInstructionDoNothing();
         // 表达式调度构建器
-        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
-        cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
+        //CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
+        //cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
 
         // 按新的cronExpression表达式构建一个新的trigger
         CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))

+ 12 - 0
xvji-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml

@@ -107,5 +107,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			sysdate()
  		)
 	</insert>
+	<!-- 根据字段条件查询单个定时任务 -->
+	<select id="selectJobByField" resultMap="SysJobResult">
+		SELECT * FROM sys_job
+		WHERE ${field} = #{value}
+		LIMIT 1 <!-- 确保只返回一条记录 -->
+	</select>
+
+	<!-- 根据字段条件删除定时任务 -->
+	<delete id="deleteJobByField">
+		DELETE FROM sys_job
+		WHERE ${field} = #{value}
+	</delete>
 
 </mapper>