From 53d60c10165ebec650cea57d7365b2bee7d51042 Mon Sep 17 00:00:00 2001 From: Joshi <3040996759@qq.com> Date: Tue, 7 Oct 2025 13:14:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(video):=20=E6=B7=BB=E5=8A=A0=E5=86=85?= =?UTF-8?q?=E9=83=A8=E5=B7=A1=E6=A3=80=E4=BB=BB=E5=8A=A1=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E5=99=A8=E5=BD=93=E7=82=B9=E5=87=BB=E5=90=AF=E5=8A=A8=E7=9A=84?= =?UTF-8?q?=E6=97=B6=E5=80=99=E5=BA=94=E8=AF=A5=E6=89=A7=E8=A1=8C=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1,=E6=AD=A4=E5=A4=84=E4=B8=8D?= =?UTF-8?q?=E5=80=9F=E7=94=A8=E8=8B=A5=E4=BE=9D=E8=87=AA=E5=B8=A6=E7=9A=84?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=20=E5=9B=A0=E4=B8=BA?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E5=9C=A8=E7=B3=BB=E7=BB=9F=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E9=87=8C=E9=9D=A2=E5=8A=A0=E5=85=A5=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=89=8D=E8=83=BD=E4=BD=BF=E7=94=A8=E5=A4=9A=E4=BA=86?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E6=AD=A5=E9=AA=A4=20=E9=99=A4=E4=BA=86status?= =?UTF-8?q?=E4=B8=BA1(=E5=81=9C=E6=AD=A2)=E7=9A=84=E5=85=B6=E4=BD=99?= =?UTF-8?q?=E9=83=BD=E9=9C=80=E8=A6=81=E5=8A=A0=E5=85=A5=E8=BD=AE=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 javax.annotation-api 依赖以支持注解生命周期管理- 实现 InspectionCronScheduler 调度器组件 - 使用 ScheduledExecutorService 每10秒轮询一次巡检任务- 支持解析 Cron 表达式并计算下次执行时间 - 自动更新任务的下次执行时间和状态 - 添加触发去抖机制防止重复执行 - 异步调用 inspectionTaskService 执行具体任务 -任务执行后自动规范化状态为启用- 增加详细的日志记录和异常处理机制 --- ruoyi-video/pom.xml | 6 + .../scheduler/InspectionCronScheduler.java | 186 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 ruoyi-video/src/main/java/com/ruoyi/video/scheduler/InspectionCronScheduler.java diff --git a/ruoyi-video/pom.xml b/ruoyi-video/pom.xml index 248cc58..79ebf93 100644 --- a/ruoyi-video/pom.xml +++ b/ruoyi-video/pom.xml @@ -18,6 +18,12 @@ + + javax.annotation + javax.annotation-api + 1.3.2 + + com.ruoyi diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/scheduler/InspectionCronScheduler.java b/ruoyi-video/src/main/java/com/ruoyi/video/scheduler/InspectionCronScheduler.java new file mode 100644 index 0000000..5eec258 --- /dev/null +++ b/ruoyi-video/src/main/java/com/ruoyi/video/scheduler/InspectionCronScheduler.java @@ -0,0 +1,186 @@ +package com.ruoyi.video.scheduler; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.video.domain.InspectionTask; +import com.ruoyi.video.mapper.InspectionTaskMapper; +import com.ruoyi.video.service.InspectionTaskService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.support.CronExpression; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 内部巡检任务调度器(不依赖若依Quartz) + * - 周期性轮询 v_inspection_task 中启用(status='0')的任务 + * - 使用 Spring CronExpression 计算下一次执行 + * - 到点调用 InspectionTaskService.executeInspectionTask(taskId) + */ +@Slf4j +@Component +public class InspectionCronScheduler { + + @Autowired + private InspectionTaskMapper inspectionTaskMapper; + + @Autowired + private InspectionTaskService inspectionTaskService; + + // 轮询调度线程池 + private final ScheduledExecutorService poller = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "inspection-cron-poller"); + t.setDaemon(true); + return t; + }); + + // 触发去抖:记录最近一次触发时间,避免同一窗口重复触发 + private final Map lastTriggeredAt = new ConcurrentHashMap<>(); + + // 最小触发间隔(毫秒),防重入 + private static final long MIN_TRIGGER_GAP_MS = 5_000L; + + @PostConstruct + public void start() { + // 延迟3秒启动,之后每10秒轮询一次 + poller.scheduleWithFixedDelay(this::tick, 3, 10, TimeUnit.SECONDS); + log.info("InspectionCronScheduler started."); + } + + @PreDestroy + public void shutdown() { + try { + poller.shutdownNow(); + } catch (Exception ignore) { + } + log.info("InspectionCronScheduler stopped."); + } + + private void tick() { + try { + // 查询所有任务(通过通用查询,传空对象获取全量),避免被服务把status改为“2”后无法再次调度 + List tasks = inspectionTaskMapper.selectInspectionTaskList(new InspectionTask()); + if (tasks == null || tasks.isEmpty()) { + return; + } + + LocalDateTime now = LocalDateTime.now(); + for (InspectionTask task : tasks) { + Long taskId = task.getTaskId(); + String cronStr = task.getCronExpression(); + String status = task.getStatus(); + if (taskId == null || cronStr == null || cronStr.trim().isEmpty()) { + continue; + } + + // 跳过停用状态(1=停用)。其余状态(0=启用, 2=服务执行后置状态)都按启用处理 + if ("1".equals(status)) { + continue; + } + + // 解析 cron + CronExpression cron; + try { + cron = CronExpression.parse(cronStr); + } catch (Exception e) { + log.warn("巡检任务cron无效,跳过: taskId={}, cron={}", taskId, cronStr); + continue; + } + + // 计算下次执行时间(基于当前时刻) + LocalDateTime next = cron.next(now); + if (next == null) { + // 无后续执行时间,跳过 + continue; + } + + Date nextDate = Date.from(next.atZone(ZoneId.systemDefault()).toInstant()); + + // 如果数据库中的 nextExecuteTime 为空或不同步,则回填 + try { + if (task.getNextExecuteTime() == null || !Objects.equals(task.getNextExecuteTime(), nextDate)) { + InspectionTask patch = new InspectionTask(); + patch.setTaskId(taskId); + patch.setNextExecuteTime(nextDate); + patch.setUpdateTime(DateUtils.getNowDate()); + inspectionTaskMapper.updateInspectionTask(patch); + } + } catch (Exception e) { + log.debug("更新nextExecuteTime失败: taskId={}, err={}", taskId, e.getMessage()); + } + + // 若发现状态为“2”(服务执行完成后置状态),将其规范化回“0”以维持启用 + if ("2".equals(status)) { + try { + InspectionTask patch = new InspectionTask(); + patch.setTaskId(taskId); + patch.setStatus("0"); + patch.setUpdateTime(DateUtils.getNowDate()); + inspectionTaskMapper.updateInspectionTask(patch); + task.setStatus("0"); + } catch (Exception e) { + log.debug("规范化任务状态失败: taskId={}, err={}", taskId, e.getMessage()); + } + } + + // 到点触发判定:如果 next <= now + 10s 窗口内,就触发一次 + long nowMs = System.currentTimeMillis(); + long nextMs = nextDate.getTime(); + if (nextMs - nowMs <= 10_000L) { + // 防抖:避免同一时间窗口重复触发 + Long last = lastTriggeredAt.get(taskId); + if (last != null && (nowMs - last) < MIN_TRIGGER_GAP_MS) { + continue; + } + + lastTriggeredAt.put(taskId, nowMs); + + // 回填最后执行时间(预写),并异步执行任务 + try { + InspectionTask patch = new InspectionTask(); + patch.setTaskId(taskId); + patch.setLastExecuteTime(new Date()); + patch.setUpdateTime(DateUtils.getNowDate()); + inspectionTaskMapper.updateInspectionTask(patch); + } catch (Exception e) { + log.debug("更新lastExecuteTime失败: taskId={}, err={}", taskId, e.getMessage()); + } + + log.info("触发巡检任务执行: taskId={}, cron={}, next={}", taskId, cronStr, next); + try { + // 交给服务的异步方法执行 + inspectionTaskService.executeInspectionTask(taskId); + } catch (Exception e) { + log.error("执行巡检任务触发失败: taskId={}, err={}", taskId, e.getMessage(), e); + } + + // 延迟规范化状态为启用(服务执行期间可能将其改为1或2) + try { + poller.schedule(() -> { + try { + InspectionTask patch2 = new InspectionTask(); + patch2.setTaskId(taskId); + patch2.setStatus("0"); + patch2.setUpdateTime(DateUtils.getNowDate()); + inspectionTaskMapper.updateInspectionTask(patch2); + } catch (Exception ignored) {} + }, 3, TimeUnit.SECONDS); + } catch (Exception ignore) {} + } + } + } catch (Exception e) { + log.error("InspectionCronScheduler tick 异常: {}", e.getMessage(), e); + } + } +} From 4a34892ea90ac25708ce142b8e5ab60f8b2d5b2e Mon Sep 17 00:00:00 2001 From: Joshi <3040996759@qq.com> Date: Tue, 7 Oct 2025 13:37:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(video):=20=E6=9B=B4=E6=96=B0Python?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=8F=A3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将视频分析服务中的Python API URL端口从8000更改为10083 - 确保与容器化环境中的实际服务端口保持一致 --- .../main/java/com/ruoyi/video/service/VideoAnalysisService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/service/VideoAnalysisService.java b/ruoyi-video/src/main/java/com/ruoyi/video/service/VideoAnalysisService.java index 2f5eaee..be3b11f 100644 --- a/ruoyi-video/src/main/java/com/ruoyi/video/service/VideoAnalysisService.java +++ b/ruoyi-video/src/main/java/com/ruoyi/video/service/VideoAnalysisService.java @@ -66,7 +66,7 @@ public class VideoAnalysisService { private com.ruoyi.video.mapper.InspectionTaskRecordMapper inspectionTaskRecordMapper; // 检测器配置 - 使用容器名而不是localhost - private static final String PYTHON_API_URL = "http://rtsp-python-service:8000/api/detect/file"; + private static final String PYTHON_API_URL = "http://rtsp-python-service:10083/api/detect/file"; private static final String MODEL_NAME = "yolov8_detector"; /**