From dfdf575932a0bc3a7ee7e3581227d7a2f50cd6aa Mon Sep 17 00:00:00 2001
From: lixiaolong <702156524@qq.com>
Date: Thu, 23 Jul 2026 13:52:37 +0800
Subject: [PATCH] =?UTF-8?q?feat(workflow):=20=E6=B7=BB=E5=8A=A0=E5=B7=A1?=
=?UTF-8?q?=E6=A3=80=E6=8A=A5=E8=AD=A6=E7=9B=91=E5=90=AC=E5=92=8C=E6=9C=BA?=
=?UTF-8?q?=E6=A2=B0=E8=87=82=E6=8E=A7=E5=88=B6=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在ActionEnum中新增巡检报警监听相关枚举值
- 集成InspectionAlertListenService和EdgeArmService依赖
- 实现巡检报警监听开始和停止功能
- 添加机械臂移动到指定坐标点功能
- 在工作流执行器中增加报警事件类型解析逻辑
- 实现基于终端ID的边缘端IP解析和报警过滤机制
- 新增巡检报警监听工作流组件服务类
- 创建巡检报警监听状态管理服务
---
.../service/InspectionAlertListenService.java | 242 ++++++++++++++++++
cmvr-iot-inspection/pom.xml | 6 +
.../InspectionDetectionAlertServiceImpl.java | 10 +
cmvr-iot-test/pom.xml | 8 +-
.../java/com/cmvr/test/enums/ActionEnum.java | 6 +-
.../EdgeInspectionAlertOperateService.java | 105 ++++++++
.../service/FlowActionExecutorService.java | 91 +++++++
7 files changed, 466 insertions(+), 2 deletions(-)
create mode 100644 cmvr-iot-device/src/main/java/com/cmvr/device/service/InspectionAlertListenService.java
create mode 100644 cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeInspectionAlertOperateService.java
diff --git a/cmvr-iot-device/src/main/java/com/cmvr/device/service/InspectionAlertListenService.java b/cmvr-iot-device/src/main/java/com/cmvr/device/service/InspectionAlertListenService.java
new file mode 100644
index 0000000..1643068
--- /dev/null
+++ b/cmvr-iot-device/src/main/java/com/cmvr/device/service/InspectionAlertListenService.java
@@ -0,0 +1,242 @@
+package com.cmvr.device.service;
+
+import com.cmvr.common.exception.GlobalException;
+import com.cmvr.device.domain.DeDeviceTerminalConfig;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * 巡检报警监听状态服务。
+ *
+ *
工作流开始监听时,根据终端ID解析终端IP,并将 IP + 报警类型保存到内存。
+ * cmvr_edge_ai 推送报警后,巡检报警入库服务通过该服务判断是否需要保存。
+ * 当前状态只在本 JVM 内有效,应用重启后会清空。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class InspectionAlertListenService
+{
+ /** 当前支持的 PPE 违规类型。 */
+ private static final Set SUPPORTED_EVENT_TYPES;
+
+ static
+ {
+ Set values = new LinkedHashSet<>();
+ values.add("No-Glove");
+ values.add("No-Helmet");
+ SUPPORTED_EVENT_TYPES = Collections.unmodifiableSet(values);
+ }
+
+ private final IDeDeviceTerminalConfigService terminalConfigService;
+
+ /**
+ * key 为边缘端 gRPC IP,value 为当前需要保存的报警类型集合。
+ */
+ private final ConcurrentMap> listenMap = new ConcurrentHashMap<>();
+
+ /**
+ * 开始监听指定终端的指定报警类型。
+ *
+ * @param terminalId 终端ID
+ * @param eventTypes 报警类型,支持 No-Glove、No-Helmet,可多选
+ * @return 当前终端IP下正在监听的报警类型
+ */
+ public ListenState startListen(String terminalId, Collection eventTypes)
+ {
+ String grpcIp = resolveTerminalIp(terminalId);
+ Set normalizedTypes = normalizeEventTypes(eventTypes, true);
+
+ Set target = listenMap.computeIfAbsent(grpcIp, key -> ConcurrentHashMap.newKeySet());
+ target.addAll(normalizedTypes);
+
+ log.info("开始监听巡检报警,terminalId={},grpcIp={},eventTypes={},currentTypes={}",
+ terminalId, grpcIp, normalizedTypes, target);
+ return new ListenState(terminalId, grpcIp, snapshot(target));
+ }
+
+ /**
+ * 结束监听指定终端的指定报警类型。
+ *
+ * 移除不存在的 IP + 类型不抛异常,只记录警告日志,避免工作流重复清理时失败。
+ *
+ * @param terminalId 终端ID
+ * @param eventTypes 报警类型,支持 No-Glove、No-Helmet,可多选
+ * @return 当前终端IP下剩余正在监听的报警类型
+ */
+ public ListenState stopListen(String terminalId, Collection eventTypes)
+ {
+ String grpcIp = resolveTerminalIp(terminalId);
+ Set normalizedTypes = normalizeEventTypes(eventTypes, true);
+ Set current = listenMap.get(grpcIp);
+
+ if (current == null)
+ {
+ log.warn("结束监听巡检报警时未找到IP监听状态,terminalId={},grpcIp={},eventTypes={}",
+ terminalId, grpcIp, normalizedTypes);
+ return new ListenState(terminalId, grpcIp, Collections.emptyList());
+ }
+
+ for (String eventType : normalizedTypes)
+ {
+ if (!current.remove(eventType))
+ {
+ log.warn("结束监听巡检报警时事件类型不存在,terminalId={},grpcIp={},eventType={}",
+ terminalId, grpcIp, eventType);
+ }
+ }
+
+ if (current.isEmpty())
+ {
+ listenMap.remove(grpcIp, current);
+ }
+
+ log.info("结束监听巡检报警,terminalId={},grpcIp={},eventTypes={},remainingTypes={}",
+ terminalId, grpcIp, normalizedTypes, current);
+ return new ListenState(terminalId, grpcIp, snapshot(current));
+ }
+
+ /**
+ * 判断报警是否命中当前监听条件。
+ *
+ * @param grpcIp 边缘端 gRPC IP
+ * @param labels 报警标签列表,来自 payload.labels
+ * @return true 表示允许入库,false 表示忽略
+ */
+ public boolean shouldStore(String grpcIp, Collection labels)
+ {
+ String normalizedIp = StringUtils.trimToNull(grpcIp);
+ if (normalizedIp == null)
+ {
+ log.warn("忽略巡检报警:grpc_ip为空,labels={}", labels);
+ return false;
+ }
+
+ Set listeningTypes = listenMap.get(normalizedIp);
+ if (listeningTypes == null || listeningTypes.isEmpty())
+ {
+ log.info("忽略巡检报警:未开启该IP的报警监听,grpcIp={},labels={}", normalizedIp, labels);
+ return false;
+ }
+
+ Set alertTypes = normalizeEventTypes(labels, false);
+ for (String alertType : alertTypes)
+ {
+ if (listeningTypes.contains(alertType))
+ {
+ return true;
+ }
+ }
+
+ log.info("忽略巡检报警:事件类型未命中监听条件,grpcIp={},labels={},listeningTypes={}",
+ normalizedIp, labels, listeningTypes);
+ return false;
+ }
+
+ /**
+ * 查询指定终端当前监听状态,便于调试或前端展示。
+ */
+ public ListenState getListenState(String terminalId)
+ {
+ String grpcIp = resolveTerminalIp(terminalId);
+ return new ListenState(terminalId, grpcIp, snapshot(listenMap.get(grpcIp)));
+ }
+
+ private String resolveTerminalIp(String terminalId)
+ {
+ String normalizedTerminalId = StringUtils.trimToNull(terminalId);
+ if (normalizedTerminalId == null)
+ {
+ throw new GlobalException("terminalId不能为空");
+ }
+
+ DeDeviceTerminalConfig terminalConfig =
+ terminalConfigService.selectDeDeviceTerminalConfigById(normalizedTerminalId);
+ if (terminalConfig == null)
+ {
+ throw new GlobalException("未找到终端配置,terminalId=" + normalizedTerminalId);
+ }
+
+ String grpcIp = StringUtils.trimToNull(terminalConfig.getHost());
+ if (grpcIp == null)
+ {
+ throw new GlobalException("终端配置host不能为空,terminalId=" + normalizedTerminalId);
+ }
+ return grpcIp;
+ }
+
+ private Set normalizeEventTypes(Collection eventTypes, boolean failOnEmpty)
+ {
+ Set result = new LinkedHashSet<>();
+ if (eventTypes != null)
+ {
+ for (String eventType : eventTypes)
+ {
+ String normalized = normalizeEventType(eventType);
+ if (normalized != null)
+ {
+ result.add(normalized);
+ }
+ }
+ }
+
+ if (failOnEmpty && result.isEmpty())
+ {
+ throw new GlobalException("eventTypes不能为空,当前支持No-Glove、No-Helmet");
+ }
+ return result;
+ }
+
+ private String normalizeEventType(String eventType)
+ {
+ String value = StringUtils.trimToNull(eventType);
+ if (value == null)
+ {
+ return null;
+ }
+
+ for (String supported : SUPPORTED_EVENT_TYPES)
+ {
+ if (supported.equalsIgnoreCase(value))
+ {
+ return supported;
+ }
+ }
+
+ log.warn("忽略不支持的巡检报警类型,eventType={},supported={}", value, SUPPORTED_EVENT_TYPES);
+ return null;
+ }
+
+ private List snapshot(Set eventTypes)
+ {
+ if (eventTypes == null || eventTypes.isEmpty())
+ {
+ return Collections.emptyList();
+ }
+ return new ArrayList<>(eventTypes);
+ }
+
+ /**
+ * 当前监听状态。
+ */
+ @Data
+ public static class ListenState
+ {
+ private final String terminalId;
+ private final String grpcIp;
+ private final List eventTypes;
+ }
+}
diff --git a/cmvr-iot-inspection/pom.xml b/cmvr-iot-inspection/pom.xml
index 3f8d31a..b158199 100644
--- a/cmvr-iot-inspection/pom.xml
+++ b/cmvr-iot-inspection/pom.xml
@@ -29,5 +29,11 @@
cmvr-iot-test
+
+
+ com.cmvr
+ cmvr-iot-device
+
+
diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionDetectionAlertServiceImpl.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionDetectionAlertServiceImpl.java
index 23acf7c..a6cce88 100644
--- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionDetectionAlertServiceImpl.java
+++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionDetectionAlertServiceImpl.java
@@ -3,6 +3,7 @@ package com.cmvr.inspection.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cmvr.common.config.properties.MinioProperties;
import com.cmvr.common.core.minio.MinioService;
+import com.cmvr.device.service.InspectionAlertListenService;
import com.cmvr.inspection.domain.InspectionDetectionAlert;
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
import com.cmvr.inspection.domain.dto.alert.AlertImage;
@@ -47,6 +48,7 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
private final MinioService minioService;
private final MinioProperties minioProperties;
private final ObjectMapper objectMapper;
+ private final InspectionAlertListenService inspectionAlertListenService;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -55,6 +57,14 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
String eventId = validateAndResolveEventId(envelope);
String idempotencyKey = trimToNull(requestIdempotencyKey);
+ // 工作流未开启对应 IP + 事件类型监听时,报警只确认接收,不做持久化。
+ if (!inspectionAlertListenService.shouldStore(envelope.getGrpcIp(), envelope.getPayload().getLabels()))
+ {
+ log.info("忽略未监听的PPE报警,eventId={},grpcIp={},labels={}",
+ eventId, envelope.getGrpcIp(), envelope.getPayload().getLabels());
+ return false;
+ }
+
if (exists(eventId))
{
log.info("忽略重复PPE报警,eventId={}", eventId);
diff --git a/cmvr-iot-test/pom.xml b/cmvr-iot-test/pom.xml
index d339b69..fae65ce 100644
--- a/cmvr-iot-test/pom.xml
+++ b/cmvr-iot-test/pom.xml
@@ -35,6 +35,12 @@
cmvr-iot-grpc-client
+
+
+ com.cmvr
+ cmvr-iot-device
+
+
org.graalvm.js
js
@@ -47,4 +53,4 @@
-
\ No newline at end of file
+
diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/enums/ActionEnum.java b/cmvr-iot-test/src/main/java/com/cmvr/test/enums/ActionEnum.java
index 047db03..d0f76d2 100644
--- a/cmvr-iot-test/src/main/java/com/cmvr/test/enums/ActionEnum.java
+++ b/cmvr-iot-test/src/main/java/com/cmvr/test/enums/ActionEnum.java
@@ -79,7 +79,11 @@ public enum ActionEnum {
// 触控交互
TI_PATH_SEARCH("EDGE", "TI_PATH_SEARCH", "路径搜索"),
- TI_TOUCH_COORDINATES("EDGE", "TI_TOUCH_COORDINATES", "获取触控二维坐标")
+ TI_TOUCH_COORDINATES("EDGE", "TI_TOUCH_COORDINATES", "获取触控二维坐标"),
+
+ // 巡检报警
+ INSPECTION_ALERT_LISTEN_START("EDGE", "INSPECTION_ALERT_LISTEN_START", "开始监听报警事件"),
+ INSPECTION_ALERT_LISTEN_STOP("EDGE", "INSPECTION_ALERT_LISTEN_STOP", "结束监听报警事件")
;
diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeInspectionAlertOperateService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeInspectionAlertOperateService.java
new file mode 100644
index 0000000..39cf150
--- /dev/null
+++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/operator/edge/EdgeInspectionAlertOperateService.java
@@ -0,0 +1,105 @@
+package com.cmvr.test.flow.runtime.operator.edge;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.cmvr.common.exception.GlobalException;
+import com.cmvr.device.service.InspectionAlertListenService;
+import com.cmvr.test.enums.ActionEnum;
+import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
+import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 巡检报警监听工作流组件。
+ */
+@Service
+@RequiredArgsConstructor
+public class EdgeInspectionAlertOperateService implements EdgeOperateService
+{
+ private final InspectionAlertListenService inspectionAlertListenService;
+
+ @Override
+ public boolean supports(ActionEnum action)
+ {
+ return ActionEnum.INSPECTION_ALERT_LISTEN_START.equals(action)
+ || ActionEnum.INSPECTION_ALERT_LISTEN_STOP.equals(action);
+ }
+
+ @Override
+ public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message)
+ {
+ JSONObject inputParams = message.getInputParams() == null ? new JSONObject() : message.getInputParams();
+ String terminalId = StringUtils.defaultIfBlank(inputParams.getString("terminalId"), message.getTerminalId());
+ Collection eventTypes = resolveEventTypes(inputParams);
+
+ InspectionAlertListenService.ListenState state;
+ switch (message.getAction())
+ {
+ case INSPECTION_ALERT_LISTEN_START:
+ state = inspectionAlertListenService.startListen(terminalId, eventTypes);
+ break;
+ case INSPECTION_ALERT_LISTEN_STOP:
+ state = inspectionAlertListenService.stopListen(terminalId, eventTypes);
+ break;
+ default:
+ throw new GlobalException("不支持的巡检报警监听动作: " + message.getAction());
+ }
+
+ JSONObject output = new JSONObject();
+ output.put("terminalId", state.getTerminalId());
+ output.put("grpcIp", state.getGrpcIp());
+ output.put("eventTypes", state.getEventTypes());
+ return TaskNodeExecuteResult.success(output);
+ }
+
+ private Collection resolveEventTypes(JSONObject inputParams)
+ {
+ List eventTypes = new ArrayList<>();
+
+ Object rawEventTypes = inputParams.get("eventTypes");
+ if (rawEventTypes instanceof JSONArray)
+ {
+ JSONArray array = (JSONArray) rawEventTypes;
+ for (int i = 0; i < array.size(); i++)
+ {
+ eventTypes.add(array.getString(i));
+ }
+ }
+ else if (rawEventTypes instanceof Collection)
+ {
+ for (Object item : (Collection>) rawEventTypes)
+ {
+ if (item != null)
+ {
+ eventTypes.add(String.valueOf(item));
+ }
+ }
+ }
+ else if (rawEventTypes != null)
+ {
+ addDelimited(eventTypes, String.valueOf(rawEventTypes));
+ }
+
+ addDelimited(eventTypes, inputParams.getString("eventType"));
+ return eventTypes;
+ }
+
+ private void addDelimited(List eventTypes, String value)
+ {
+ if (StringUtils.isBlank(value))
+ {
+ return;
+ }
+ String[] split = value.split(",");
+ for (String item : split)
+ {
+ eventTypes.add(StringUtils.trim(item));
+ }
+ }
+}
diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/service/FlowActionExecutorService.java b/cmvr-iot-test/src/main/java/com/cmvr/test/service/FlowActionExecutorService.java
index b1ce6fa..a7a61e8 100644
--- a/cmvr-iot-test/src/main/java/com/cmvr/test/service/FlowActionExecutorService.java
+++ b/cmvr-iot-test/src/main/java/com/cmvr/test/service/FlowActionExecutorService.java
@@ -14,6 +14,8 @@ import com.cmvr.edge.client.service.EdgeHlcService;
import com.cmvr.edge.client.service.EdgeMicrophoneService;
import com.cmvr.edge.client.service.EdgeSpeakerService;
import com.cmvr.edge.client.service.EdgeAgvService;
+import com.cmvr.edge.client.service.EdgeArmService;
+import com.cmvr.device.service.InspectionAlertListenService;
import com.cmvr.llm.service.LLMAiAgentPlatformService;
import com.cmvr.test.enums.ActionEnum;
import com.cmvr.test.model.vo.FlowActionRequestVO;
@@ -32,6 +34,8 @@ public class FlowActionExecutorService {
private final EdgeHlcService edgeHlcService;
private final LLMAiAgentPlatformService llmAiAgentPlatformService;
private final EdgeAgvService edgeAgvService;
+ private final InspectionAlertListenService inspectionAlertListenService;
+ private final EdgeArmService edgeArmService;
public String actionExecute(FlowActionRequestVO req) {
@@ -58,6 +62,12 @@ public class FlowActionExecutorService {
String terminalId = payload.getString("terminalId");
String deviceId = payload.getString("deviceId");
+ // 巡检报警监听组件只需要终端ID和事件类型,不需要设备ID。
+ if (ActionEnum.INSPECTION_ALERT_LISTEN_START.equals(action)
+ || ActionEnum.INSPECTION_ALERT_LISTEN_STOP.equals(action)) {
+ return executeInspectionAlertAction(action, payload);
+ }
+
if (StrUtil.isEmpty(terminalId) || StrUtil.isEmpty(deviceId)) {
throw new GlobalException("EDGE 类型动作必须提供 terminalId 和 deviceId");
}
@@ -89,6 +99,10 @@ public class FlowActionExecutorService {
EdgeTouchVO edgeTouchVO = payload.to(EdgeTouchVO.class);
return edgeHlcService.touch(edgeTouchVO);
+ // ==== 机械臂 ====
+ case ARM_MOVE_TO_POINT:
+ return executeArmMoveToPoint(edgeCommonVO, payload);
+
// ==== 语料 ====
case VI_PLAY_CORPUS:
String audioPath = payload.getString("audioPath");
@@ -145,6 +159,83 @@ public class FlowActionExecutorService {
}
}
+ private String executeArmMoveToPoint(EdgeCommonVO edgeCommonVO, JSONObject payload) {
+ Double x = payload.getDouble("x");
+ Double y = payload.getDouble("y");
+ Double z = payload.getDouble("z");
+ Double rx = payload.getDouble("rx");
+ Double ry = payload.getDouble("ry");
+ Double rz = payload.getDouble("rz");
+ String frame = payload.getString("frame");
+ Double velocity = payload.getDouble("velocity");
+ Double acceleration = payload.getDouble("acceleration");
+ Double blendRadius = payload.getDouble("blendRadius");
+
+ if (x == null || y == null || z == null) {
+ throw new GlobalException("X、Y、Z坐标不能为空");
+ }
+ if (rx == null || ry == null || rz == null) {
+ throw new GlobalException("RX、RY、RZ旋转角度不能为空");
+ }
+
+ // 单节点执行和正式工作流保持一致:先开启力矩,再下发笛卡尔直线运动。
+ edgeArmService.torqueOn(edgeCommonVO);
+ edgeArmService.moveL(
+ edgeCommonVO,
+ x,
+ y,
+ z,
+ rx,
+ ry,
+ rz,
+ frame,
+ velocity,
+ acceleration,
+ blendRadius
+ );
+ return "机械臂末端运动任务下发成功";
+ }
+
+ private String executeInspectionAlertAction(ActionEnum action, JSONObject payload) {
+ String terminalId = payload.getString("terminalId");
+ java.util.List types = resolveInspectionAlertEventTypes(payload);
+
+ InspectionAlertListenService.ListenState state;
+ if (ActionEnum.INSPECTION_ALERT_LISTEN_START.equals(action)) {
+ state = inspectionAlertListenService.startListen(terminalId, types);
+ } else {
+ state = inspectionAlertListenService.stopListen(terminalId, types);
+ }
+ return JSONObject.toJSONString(state);
+ }
+
+ private java.util.List resolveInspectionAlertEventTypes(JSONObject payload) {
+ java.util.List types = new java.util.ArrayList<>();
+ Object eventTypes = payload.get("eventTypes");
+
+ if (eventTypes instanceof Iterable) {
+ for (Object item : (Iterable>) eventTypes) {
+ if (item != null) {
+ types.add(String.valueOf(item));
+ }
+ }
+ } else if (eventTypes != null) {
+ addInspectionAlertEventTypes(types, String.valueOf(eventTypes));
+ }
+
+ addInspectionAlertEventTypes(types, payload.getString("eventType"));
+ return types;
+ }
+
+ private void addInspectionAlertEventTypes(java.util.List types, String value) {
+ if (StrUtil.isBlank(value)) {
+ return;
+ }
+ for (String item : value.split(",")) {
+ types.add(item.trim());
+ }
+ }
+
private String executeLLMAction(ActionEnum action, FlowActionRequestVO req) {
log.info("LLM 执行动作: {}", action);