feat(inspection): 新增巡检仪表识别和人工判断功能
- 添加巡检仪表读数识别动作枚举和处理器 - 实现人工巡检判断任务创建功能 - 扩展巡检报警监听服务支持工作流上下文 - 新增统一巡检结果表和媒体证据表 - 更新巡检告警实体和数据传输对象 - 扩展工作流执行事件传递更多节点信息 - 优化巡检报警入库服务支持订阅模式
This commit is contained in:
parent
dfdf575932
commit
521e3e4e24
@ -0,0 +1,126 @@
|
|||||||
|
package com.cmvr.web.controller.inspection;
|
||||||
|
|
||||||
|
import com.cmvr.common.annotation.Log;
|
||||||
|
import com.cmvr.common.core.controller.BaseController;
|
||||||
|
import com.cmvr.common.core.domain.AjaxResult;
|
||||||
|
import com.cmvr.common.core.page.TableDataInfo;
|
||||||
|
import com.cmvr.common.enums.BusinessType;
|
||||||
|
import com.cmvr.common.utils.poi.ExcelUtil;
|
||||||
|
import com.cmvr.inspection.domain.dto.InspectionResultQuery;
|
||||||
|
import com.cmvr.inspection.domain.dto.InspectionResultReviewRequest;
|
||||||
|
import com.cmvr.inspection.domain.vo.InspectionResultVo;
|
||||||
|
import com.cmvr.inspection.service.IInspectionResultService;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiImplicitParam;
|
||||||
|
import io.swagger.annotations.ApiImplicitParams;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import io.swagger.annotations.ApiParam;
|
||||||
|
import io.swagger.annotations.ApiResponse;
|
||||||
|
import io.swagger.annotations.ApiResponses;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检结果、人工复核和任务报表接口。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/inspection/result")
|
||||||
|
@Api(tags = "智能巡检--巡检结果", description = "巡检结果查询、人工复核、三级告警统计和报表导出")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(code = 200, message = "请求处理成功"),
|
||||||
|
@ApiResponse(code = 400, message = "请求参数或状态不合法"),
|
||||||
|
@ApiResponse(code = 401, message = "未登录或登录已失效"),
|
||||||
|
@ApiResponse(code = 403, message = "无接口权限"),
|
||||||
|
@ApiResponse(code = 500, message = "系统内部异常")
|
||||||
|
})
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InspectionResultController extends BaseController
|
||||||
|
{
|
||||||
|
private final IInspectionResultService inspectionResultService;
|
||||||
|
|
||||||
|
@ApiOperation(value = "查询巡检结果列表",
|
||||||
|
notes = "分页查询PPE、仪表读数和人工判断结果;查询条件均为可选,结果按检测时间倒序。"
|
||||||
|
+ "返回rows元素为InspectionResultVo,包含taskName任务名称和evidenceType主要证据类型;"
|
||||||
|
+ "完整mediaList仅在详情接口返回")
|
||||||
|
@ApiImplicitParams({
|
||||||
|
@ApiImplicitParam(name = "pageNum", value = "页码,从1开始", dataType = "int", paramType = "query", example = "1"),
|
||||||
|
@ApiImplicitParam(name = "pageSize", value = "每页数量", dataType = "int", paramType = "query", example = "10"),
|
||||||
|
@ApiImplicitParam(name = "taskInstanceId", value = "巡检任务实例数据库ID", dataType = "string", paramType = "query"),
|
||||||
|
@ApiImplicitParam(name = "taskId", value = "巡检任务ID", dataType = "string", paramType = "query"),
|
||||||
|
@ApiImplicitParam(name = "itemId", value = "检测项ID", dataType = "string", paramType = "query"),
|
||||||
|
@ApiImplicitParam(name = "resultType", value = "结果类型:PPE、METER、MANUAL", dataType = "string", paramType = "query"),
|
||||||
|
@ApiImplicitParam(name = "resultStatus", value = "状态:PENDING、NORMAL、ABNORMAL、RECOGNIZE_FAILED", dataType = "string", paramType = "query"),
|
||||||
|
@ApiImplicitParam(name = "alarmLevel", value = "告警级别:1提示、2警告、3严重", dataType = "int", paramType = "query"),
|
||||||
|
@ApiImplicitParam(name = "resultName", value = "检查名称,支持模糊查询", dataType = "string", paramType = "query")
|
||||||
|
})
|
||||||
|
@PreAuthorize("@ss.hasPermi('inspection:result:list')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(@ApiParam("巡检结果查询条件") InspectionResultQuery query)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
return getDataTable(inspectionResultService.selectResultList(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "查询巡检结果详情",
|
||||||
|
notes = "返回结构化巡检结果及按顺序排列的全部图片、视频证据",
|
||||||
|
response = InspectionResultVo.class)
|
||||||
|
@PreAuthorize("@ss.hasPermi('inspection:result:query')")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public AjaxResult getInfo(@ApiParam(value = "巡检结果ID", required = true)
|
||||||
|
@PathVariable String id)
|
||||||
|
{
|
||||||
|
return success(inspectionResultService.selectResultById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "提交人工复核结果",
|
||||||
|
notes = "仅PENDING或RECOGNIZE_FAILED状态可复核。ABNORMAL必须传alarmLevel;reviewVersion用于并发控制")
|
||||||
|
@PreAuthorize("@ss.hasPermi('inspection:result:review')")
|
||||||
|
@Log(title = "巡检结果人工复核", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/{id}/review")
|
||||||
|
public AjaxResult review(@ApiParam(value = "巡检结果ID", required = true)
|
||||||
|
@PathVariable String id,
|
||||||
|
@ApiParam(value = "人工复核内容", required = true)
|
||||||
|
@Validated @RequestBody InspectionResultReviewRequest request)
|
||||||
|
{
|
||||||
|
return success(inspectionResultService.review(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "查询巡检任务报表汇总",
|
||||||
|
notes = "返回summary统计和results明细。reviewCompleted=false表示仍有待复核或识别失败结果")
|
||||||
|
@PreAuthorize("@ss.hasPermi('inspection:result:report')")
|
||||||
|
@GetMapping("/report/{taskInstanceId}")
|
||||||
|
public AjaxResult report(@ApiParam(value = "巡检任务实例数据库ID", required = true)
|
||||||
|
@PathVariable String taskInstanceId)
|
||||||
|
{
|
||||||
|
AjaxResult result = AjaxResult.success();
|
||||||
|
result.put("summary", inspectionResultService.buildReportSummary(taskInstanceId));
|
||||||
|
InspectionResultQuery query = new InspectionResultQuery();
|
||||||
|
query.setTaskInstanceId(taskInstanceId);
|
||||||
|
result.put("results", inspectionResultService.selectResultList(query));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "导出巡检结果报表",
|
||||||
|
notes = "按查询条件导出Excel;不传条件时导出全部巡检结果")
|
||||||
|
@PreAuthorize("@ss.hasPermi('inspection:result:export')")
|
||||||
|
@Log(title = "巡检结果", businessType = BusinessType.EXPORT)
|
||||||
|
@PostMapping("/export")
|
||||||
|
public void export(HttpServletResponse response,
|
||||||
|
@ApiParam("导出筛选条件") InspectionResultQuery query)
|
||||||
|
{
|
||||||
|
List<InspectionResultVo> list = inspectionResultService.selectResultList(query);
|
||||||
|
ExcelUtil<InspectionResultVo> util = new ExcelUtil<>(InspectionResultVo.class);
|
||||||
|
util.exportExcel(response, list, "巡检结果数据");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -156,8 +156,13 @@ public class TeFlowController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/action")
|
@PostMapping("/action")
|
||||||
@ApiOperation("单个节点执行")
|
@ApiOperation(value = "单个节点执行",
|
||||||
public AjaxResult actionExecute(@RequestBody FlowActionRequestVO flowActionRequestVO) {
|
notes = "巡检仪表节点action=INSPECTION_METER_RECOGNIZE,payload使用InspectionMeterRecognizeConfigVO;"
|
||||||
|
+ "人工判断节点action=INSPECTION_MANUAL_REVIEW_CREATE,payload使用InspectionManualReviewConfigVO。"
|
||||||
|
+ "该接口用于试调节点,不会生成正式巡检任务结果")
|
||||||
|
public AjaxResult actionExecute(
|
||||||
|
@io.swagger.annotations.ApiParam(value = "单节点动作和参数", required = true)
|
||||||
|
@RequestBody FlowActionRequestVO flowActionRequestVO) {
|
||||||
return AjaxResult.ok(flowActionExecutorService.actionExecute(flowActionRequestVO));
|
return AjaxResult.ok(flowActionExecutorService.actionExecute(flowActionRequestVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,11 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import com.cmvr.common.config.CmvrIotConfig;
|
import com.cmvr.common.config.CmvrIotConfig;
|
||||||
|
import com.cmvr.test.model.vo.inspection.InspectionAlarmRuleVO;
|
||||||
|
import com.cmvr.test.model.vo.inspection.InspectionManualReviewConfigVO;
|
||||||
|
import com.cmvr.test.model.vo.inspection.InspectionMediaVO;
|
||||||
|
import com.cmvr.test.model.vo.inspection.InspectionMeterRecognizeConfigVO;
|
||||||
|
import com.fasterxml.classmate.TypeResolver;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import io.swagger.models.auth.In;
|
import io.swagger.models.auth.In;
|
||||||
import springfox.documentation.builders.ApiInfoBuilder;
|
import springfox.documentation.builders.ApiInfoBuilder;
|
||||||
@ -34,6 +39,10 @@ public class SwaggerConfig
|
|||||||
@Autowired
|
@Autowired
|
||||||
private CmvrIotConfig cmvrIotConfig;
|
private CmvrIotConfig cmvrIotConfig;
|
||||||
|
|
||||||
|
/** 用于将动态工作流payload对应的参数模型显式加入Swagger文档。 */
|
||||||
|
@Autowired
|
||||||
|
private TypeResolver typeResolver;
|
||||||
|
|
||||||
/** 是否开启swagger */
|
/** 是否开启swagger */
|
||||||
@Value("${swagger.enabled}")
|
@Value("${swagger.enabled}")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
@ -62,6 +71,12 @@ public class SwaggerConfig
|
|||||||
// 扫描所有 .apis(RequestHandlerSelectors.any())
|
// 扫描所有 .apis(RequestHandlerSelectors.any())
|
||||||
.paths(PathSelectors.any())
|
.paths(PathSelectors.any())
|
||||||
.build()
|
.build()
|
||||||
|
// 工作流单节点接口使用JSONObject作为payload,需显式注册巡检节点参数模型。
|
||||||
|
.additionalModels(
|
||||||
|
typeResolver.resolve(InspectionMeterRecognizeConfigVO.class),
|
||||||
|
typeResolver.resolve(InspectionAlarmRuleVO.class),
|
||||||
|
typeResolver.resolve(InspectionManualReviewConfigVO.class),
|
||||||
|
typeResolver.resolve(InspectionMediaVO.class))
|
||||||
/* 设置安全模式,swagger可以设置访问token */
|
/* 设置安全模式,swagger可以设置访问token */
|
||||||
.securitySchemes(securitySchemes())
|
.securitySchemes(securitySchemes())
|
||||||
.securityContexts(securityContexts())
|
.securityContexts(securityContexts())
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
@ -21,16 +20,14 @@ import java.util.concurrent.ConcurrentMap;
|
|||||||
/**
|
/**
|
||||||
* 巡检报警监听状态服务。
|
* 巡检报警监听状态服务。
|
||||||
*
|
*
|
||||||
* <p>工作流开始监听时,根据终端ID解析终端IP,并将 IP + 报警类型保存到内存。
|
* <p>监听条件仍然按边缘端IP和报警类型匹配,同时保存工作流执行上下文,
|
||||||
* cmvr_edge_ai 推送报警后,巡检报警入库服务通过该服务判断是否需要保存。
|
* 使异步上报的PPE事件能够归属到具体巡检任务、检测项和节点。</p>
|
||||||
* 当前状态只在本 JVM 内有效,应用重启后会清空。</p>
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InspectionAlertListenService
|
public class InspectionAlertListenService
|
||||||
{
|
{
|
||||||
/** 当前支持的 PPE 违规类型。 */
|
|
||||||
private static final Set<String> SUPPORTED_EVENT_TYPES;
|
private static final Set<String> SUPPORTED_EVENT_TYPES;
|
||||||
|
|
||||||
static
|
static
|
||||||
@ -43,47 +40,59 @@ public class InspectionAlertListenService
|
|||||||
|
|
||||||
private final IDeDeviceTerminalConfigService terminalConfigService;
|
private final IDeDeviceTerminalConfigService terminalConfigService;
|
||||||
|
|
||||||
/**
|
/** key=grpcIp,value=当前IP下的工作流监听订阅。 */
|
||||||
* key 为边缘端 gRPC IP,value 为当前需要保存的报警类型集合。
|
private final ConcurrentMap<String, ConcurrentMap<String, ListenSubscription>> subscriptions =
|
||||||
*/
|
new ConcurrentHashMap<>();
|
||||||
private final ConcurrentMap<String, Set<String>> listenMap = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/**
|
/** 兼容原有调用方式,创建一个不绑定工作流的监听。 */
|
||||||
* 开始监听指定终端的指定报警类型。
|
|
||||||
*
|
|
||||||
* @param terminalId 终端ID
|
|
||||||
* @param eventTypes 报警类型,支持 No-Glove、No-Helmet,可多选
|
|
||||||
* @return 当前终端IP下正在监听的报警类型
|
|
||||||
*/
|
|
||||||
public ListenState startListen(String terminalId, Collection<String> eventTypes)
|
public ListenState startListen(String terminalId, Collection<String> eventTypes)
|
||||||
{
|
{
|
||||||
String grpcIp = resolveTerminalIp(terminalId);
|
return startListen(terminalId, eventTypes, null, null, null, null, null);
|
||||||
Set<String> normalizedTypes = normalizeEventTypes(eventTypes, true);
|
|
||||||
|
|
||||||
Set<String> 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public ListenState startListen(String terminalId, Collection<String> eventTypes,
|
||||||
* 结束监听指定终端的指定报警类型。
|
String flowInstanceId, String taskId, String itemId,
|
||||||
*
|
String nodeId, String listenerKey)
|
||||||
* <p>移除不存在的 IP + 类型不抛异常,只记录警告日志,避免工作流重复清理时失败。</p>
|
|
||||||
*
|
|
||||||
* @param terminalId 终端ID
|
|
||||||
* @param eventTypes 报警类型,支持 No-Glove、No-Helmet,可多选
|
|
||||||
* @return 当前终端IP下剩余正在监听的报警类型
|
|
||||||
*/
|
|
||||||
public ListenState stopListen(String terminalId, Collection<String> eventTypes)
|
|
||||||
{
|
{
|
||||||
String grpcIp = resolveTerminalIp(terminalId);
|
String grpcIp = resolveTerminalIp(terminalId);
|
||||||
Set<String> normalizedTypes = normalizeEventTypes(eventTypes, true);
|
Set<String> normalizedTypes = normalizeEventTypes(eventTypes, true);
|
||||||
Set<String> current = listenMap.get(grpcIp);
|
ConcurrentMap<String, ListenSubscription> target =
|
||||||
|
subscriptions.computeIfAbsent(grpcIp, key -> new ConcurrentHashMap<>());
|
||||||
|
|
||||||
if (current == null)
|
for (String eventType : normalizedTypes)
|
||||||
|
{
|
||||||
|
ListenSubscription subscription = new ListenSubscription();
|
||||||
|
subscription.setSubscriptionId(buildSubscriptionId(flowInstanceId, itemId, listenerKey, eventType));
|
||||||
|
subscription.setTerminalId(terminalId);
|
||||||
|
subscription.setGrpcIp(grpcIp);
|
||||||
|
subscription.setEventType(eventType);
|
||||||
|
subscription.setFlowInstanceId(flowInstanceId);
|
||||||
|
subscription.setTaskId(taskId);
|
||||||
|
subscription.setItemId(itemId);
|
||||||
|
subscription.setNodeId(nodeId);
|
||||||
|
subscription.setListenerKey(StringUtils.trimToNull(listenerKey));
|
||||||
|
subscription.setStartedAt(System.currentTimeMillis());
|
||||||
|
target.put(subscription.getSubscriptionId(), subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("开始监听巡检报警,terminalId={},grpcIp={},flowInstanceId={},itemId={},eventTypes={}",
|
||||||
|
terminalId, grpcIp, flowInstanceId, itemId, normalizedTypes);
|
||||||
|
return new ListenState(terminalId, grpcIp, currentTypes(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 兼容原有调用方式,移除该IP下指定类型的全部监听。 */
|
||||||
|
public ListenState stopListen(String terminalId, Collection<String> eventTypes)
|
||||||
|
{
|
||||||
|
return stopListen(terminalId, eventTypes, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenState stopListen(String terminalId, Collection<String> eventTypes,
|
||||||
|
String flowInstanceId, String itemId, String listenerKey)
|
||||||
|
{
|
||||||
|
String grpcIp = resolveTerminalIp(terminalId);
|
||||||
|
Set<String> normalizedTypes = normalizeEventTypes(eventTypes, true);
|
||||||
|
ConcurrentMap<String, ListenSubscription> current = subscriptions.get(grpcIp);
|
||||||
|
if (current == null || current.isEmpty())
|
||||||
{
|
{
|
||||||
log.warn("结束监听巡检报警时未找到IP监听状态,terminalId={},grpcIp={},eventTypes={}",
|
log.warn("结束监听巡检报警时未找到IP监听状态,terminalId={},grpcIp={},eventTypes={}",
|
||||||
terminalId, grpcIp, normalizedTypes);
|
terminalId, grpcIp, normalizedTypes);
|
||||||
@ -92,67 +101,124 @@ public class InspectionAlertListenService
|
|||||||
|
|
||||||
for (String eventType : normalizedTypes)
|
for (String eventType : normalizedTypes)
|
||||||
{
|
{
|
||||||
if (!current.remove(eventType))
|
int removed = removeMatching(current, eventType, flowInstanceId, itemId, listenerKey);
|
||||||
|
if (removed == 0)
|
||||||
{
|
{
|
||||||
log.warn("结束监听巡检报警时事件类型不存在,terminalId={},grpcIp={},eventType={}",
|
log.warn("结束监听巡检报警时未找到匹配订阅,terminalId={},grpcIp={},flowInstanceId={},itemId={},eventType={}",
|
||||||
terminalId, grpcIp, eventType);
|
terminalId, grpcIp, flowInstanceId, itemId, eventType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.isEmpty())
|
if (current.isEmpty())
|
||||||
{
|
{
|
||||||
listenMap.remove(grpcIp, current);
|
subscriptions.remove(grpcIp, current);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("结束监听巡检报警,terminalId={},grpcIp={},eventTypes={},remainingTypes={}",
|
List<String> remainingTypes = currentTypes(current);
|
||||||
terminalId, grpcIp, normalizedTypes, current);
|
log.info("结束监听巡检报警,terminalId={},grpcIp={},flowInstanceId={},itemId={},remainingTypes={}",
|
||||||
return new ListenState(terminalId, grpcIp, snapshot(current));
|
terminalId, grpcIp, flowInstanceId, itemId, remainingTypes);
|
||||||
|
return new ListenState(terminalId, grpcIp, remainingTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 返回命中IP和任一标签的全部活动订阅。 */
|
||||||
* 判断报警是否命中当前监听条件。
|
public List<ListenSubscription> matchSubscriptions(String grpcIp, Collection<String> labels)
|
||||||
*
|
|
||||||
* @param grpcIp 边缘端 gRPC IP
|
|
||||||
* @param labels 报警标签列表,来自 payload.labels
|
|
||||||
* @return true 表示允许入库,false 表示忽略
|
|
||||||
*/
|
|
||||||
public boolean shouldStore(String grpcIp, Collection<String> labels)
|
|
||||||
{
|
{
|
||||||
String normalizedIp = StringUtils.trimToNull(grpcIp);
|
String normalizedIp = StringUtils.trimToNull(grpcIp);
|
||||||
if (normalizedIp == null)
|
if (normalizedIp == null)
|
||||||
{
|
{
|
||||||
log.warn("忽略巡检报警:grpc_ip为空,labels={}", labels);
|
log.warn("忽略巡检报警:grpc_ip为空,labels={}", labels);
|
||||||
return false;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
ConcurrentMap<String, ListenSubscription> current = subscriptions.get(normalizedIp);
|
||||||
Set<String> listeningTypes = listenMap.get(normalizedIp);
|
if (current == null || current.isEmpty())
|
||||||
if (listeningTypes == null || listeningTypes.isEmpty())
|
|
||||||
{
|
{
|
||||||
log.info("忽略巡检报警:未开启该IP的报警监听,grpcIp={},labels={}", normalizedIp, labels);
|
log.info("忽略巡检报警:未开启该IP的报警监听,grpcIp={},labels={}", normalizedIp, labels);
|
||||||
return false;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> alertTypes = normalizeEventTypes(labels, false);
|
Set<String> alertTypes = normalizeEventTypes(labels, false);
|
||||||
for (String alertType : alertTypes)
|
List<ListenSubscription> matched = new ArrayList<>();
|
||||||
|
for (ListenSubscription subscription : current.values())
|
||||||
{
|
{
|
||||||
if (listeningTypes.contains(alertType))
|
if (alertTypes.contains(subscription.getEventType()))
|
||||||
{
|
{
|
||||||
return true;
|
matched.add(subscription.copy());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (matched.isEmpty())
|
||||||
log.info("忽略巡检报警:事件类型未命中监听条件,grpcIp={},labels={},listeningTypes={}",
|
{
|
||||||
normalizedIp, labels, listeningTypes);
|
log.info("忽略巡检报警:事件类型未命中监听条件,grpcIp={},labels={},listeningTypes={}",
|
||||||
return false;
|
normalizedIp, labels, currentTypes(current));
|
||||||
|
}
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldStore(String grpcIp, Collection<String> labels)
|
||||||
|
{
|
||||||
|
return !matchSubscriptions(grpcIp, labels).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务完成、失败或终止时清理该流程的全部监听。 */
|
||||||
|
public void clearByFlowInstanceId(String flowInstanceId)
|
||||||
|
{
|
||||||
|
String normalized = StringUtils.trimToNull(flowInstanceId);
|
||||||
|
if (normalized == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int removed = 0;
|
||||||
|
for (ConcurrentMap.Entry<String, ConcurrentMap<String, ListenSubscription>> ipEntry : subscriptions.entrySet())
|
||||||
|
{
|
||||||
|
ConcurrentMap<String, ListenSubscription> current = ipEntry.getValue();
|
||||||
|
for (ConcurrentMap.Entry<String, ListenSubscription> entry : current.entrySet())
|
||||||
|
{
|
||||||
|
if (normalized.equals(entry.getValue().getFlowInstanceId()) && current.remove(entry.getKey(), entry.getValue()))
|
||||||
|
{
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current.isEmpty())
|
||||||
|
{
|
||||||
|
subscriptions.remove(ipEntry.getKey(), current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removed > 0)
|
||||||
|
{
|
||||||
|
log.info("巡检任务结束,清理报警监听,flowInstanceId={},removed={}", normalized, removed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询指定终端当前监听状态,便于调试或前端展示。
|
|
||||||
*/
|
|
||||||
public ListenState getListenState(String terminalId)
|
public ListenState getListenState(String terminalId)
|
||||||
{
|
{
|
||||||
String grpcIp = resolveTerminalIp(terminalId);
|
String grpcIp = resolveTerminalIp(terminalId);
|
||||||
return new ListenState(terminalId, grpcIp, snapshot(listenMap.get(grpcIp)));
|
return new ListenState(terminalId, grpcIp, currentTypes(subscriptions.get(grpcIp)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int removeMatching(ConcurrentMap<String, ListenSubscription> current, String eventType,
|
||||||
|
String flowInstanceId, String itemId, String listenerKey)
|
||||||
|
{
|
||||||
|
int removed = 0;
|
||||||
|
for (ConcurrentMap.Entry<String, ListenSubscription> entry : current.entrySet())
|
||||||
|
{
|
||||||
|
ListenSubscription subscription = entry.getValue();
|
||||||
|
boolean legacyRemove = StringUtils.isBlank(flowInstanceId);
|
||||||
|
boolean contextMatches = StringUtils.equals(flowInstanceId, subscription.getFlowInstanceId())
|
||||||
|
&& (StringUtils.isBlank(itemId) || StringUtils.equals(itemId, subscription.getItemId()))
|
||||||
|
&& (StringUtils.isBlank(listenerKey) || StringUtils.equals(listenerKey, subscription.getListenerKey()));
|
||||||
|
if (eventType.equals(subscription.getEventType()) && (legacyRemove || contextMatches)
|
||||||
|
&& current.remove(entry.getKey(), subscription))
|
||||||
|
{
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildSubscriptionId(String flowInstanceId, String itemId, String listenerKey, String eventType)
|
||||||
|
{
|
||||||
|
String flow = StringUtils.defaultIfBlank(flowInstanceId, "legacy");
|
||||||
|
String item = StringUtils.defaultIfBlank(itemId, "all");
|
||||||
|
String key = StringUtils.defaultIfBlank(listenerKey, "default");
|
||||||
|
return flow + "|" + item + "|" + key + "|" + eventType;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveTerminalIp(String terminalId)
|
private String resolveTerminalIp(String terminalId)
|
||||||
@ -162,14 +228,12 @@ public class InspectionAlertListenService
|
|||||||
{
|
{
|
||||||
throw new GlobalException("terminalId不能为空");
|
throw new GlobalException("terminalId不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
DeDeviceTerminalConfig terminalConfig =
|
DeDeviceTerminalConfig terminalConfig =
|
||||||
terminalConfigService.selectDeDeviceTerminalConfigById(normalizedTerminalId);
|
terminalConfigService.selectDeDeviceTerminalConfigById(normalizedTerminalId);
|
||||||
if (terminalConfig == null)
|
if (terminalConfig == null)
|
||||||
{
|
{
|
||||||
throw new GlobalException("未找到终端配置,terminalId=" + normalizedTerminalId);
|
throw new GlobalException("未找到终端配置,terminalId=" + normalizedTerminalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
String grpcIp = StringUtils.trimToNull(terminalConfig.getHost());
|
String grpcIp = StringUtils.trimToNull(terminalConfig.getHost());
|
||||||
if (grpcIp == null)
|
if (grpcIp == null)
|
||||||
{
|
{
|
||||||
@ -192,7 +256,6 @@ public class InspectionAlertListenService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failOnEmpty && result.isEmpty())
|
if (failOnEmpty && result.isEmpty())
|
||||||
{
|
{
|
||||||
throw new GlobalException("eventTypes不能为空,当前支持No-Glove、No-Helmet");
|
throw new GlobalException("eventTypes不能为空,当前支持No-Glove、No-Helmet");
|
||||||
@ -207,7 +270,6 @@ public class InspectionAlertListenService
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String supported : SUPPORTED_EVENT_TYPES)
|
for (String supported : SUPPORTED_EVENT_TYPES)
|
||||||
{
|
{
|
||||||
if (supported.equalsIgnoreCase(value))
|
if (supported.equalsIgnoreCase(value))
|
||||||
@ -215,23 +277,24 @@ public class InspectionAlertListenService
|
|||||||
return supported;
|
return supported;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn("忽略不支持的巡检报警类型,eventType={},supported={}", value, SUPPORTED_EVENT_TYPES);
|
log.warn("忽略不支持的巡检报警类型,eventType={},supported={}", value, SUPPORTED_EVENT_TYPES);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> snapshot(Set<String> eventTypes)
|
private List<String> currentTypes(ConcurrentMap<String, ListenSubscription> current)
|
||||||
{
|
{
|
||||||
if (eventTypes == null || eventTypes.isEmpty())
|
if (current == null || current.isEmpty())
|
||||||
{
|
{
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
return new ArrayList<>(eventTypes);
|
Set<String> values = new LinkedHashSet<>();
|
||||||
|
for (ListenSubscription subscription : current.values())
|
||||||
|
{
|
||||||
|
values.add(subscription.getEventType());
|
||||||
|
}
|
||||||
|
return new ArrayList<>(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前监听状态。
|
|
||||||
*/
|
|
||||||
@Data
|
@Data
|
||||||
public static class ListenState
|
public static class ListenState
|
||||||
{
|
{
|
||||||
@ -239,4 +302,45 @@ public class InspectionAlertListenService
|
|||||||
private final String grpcIp;
|
private final String grpcIp;
|
||||||
private final List<String> eventTypes;
|
private final List<String> eventTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ListenSubscription
|
||||||
|
{
|
||||||
|
/** flowInstanceId、itemId、listenerKey、eventType组成的订阅唯一标识。 */
|
||||||
|
private String subscriptionId;
|
||||||
|
/** 工作流节点配置的终端ID。 */
|
||||||
|
private String terminalId;
|
||||||
|
/** 由终端配置解析出的gRPC IP,也是PPE事件匹配IP。 */
|
||||||
|
private String grpcIp;
|
||||||
|
/** 当前订阅监听的PPE事件类型。 */
|
||||||
|
private String eventType;
|
||||||
|
/** 工作流运行实例ID,用于关联巡检任务和结束时清理。 */
|
||||||
|
private String flowInstanceId;
|
||||||
|
/** 工作流编排任务ID。 */
|
||||||
|
private String taskId;
|
||||||
|
/** 当前巡检检测项ID。 */
|
||||||
|
private String itemId;
|
||||||
|
/** 启动监听的工作流节点ID。 */
|
||||||
|
private String nodeId;
|
||||||
|
/** 同一检测项存在多个监听器时使用的可选业务标识。 */
|
||||||
|
private String listenerKey;
|
||||||
|
/** 开始监听时间戳,单位毫秒。 */
|
||||||
|
private long startedAt;
|
||||||
|
|
||||||
|
private ListenSubscription copy()
|
||||||
|
{
|
||||||
|
ListenSubscription copy = new ListenSubscription();
|
||||||
|
copy.subscriptionId = subscriptionId;
|
||||||
|
copy.terminalId = terminalId;
|
||||||
|
copy.grpcIp = grpcIp;
|
||||||
|
copy.eventType = eventType;
|
||||||
|
copy.flowInstanceId = flowInstanceId;
|
||||||
|
copy.taskId = taskId;
|
||||||
|
copy.itemId = itemId;
|
||||||
|
copy.nodeId = nodeId;
|
||||||
|
copy.listenerKey = listenerKey;
|
||||||
|
copy.startedAt = startedAt;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,18 @@ public class InspectionAlarm extends BaseEntity
|
|||||||
@ApiModelProperty("任务执行实例ID")
|
@ApiModelProperty("任务执行实例ID")
|
||||||
private String taskInstanceId;
|
private String taskInstanceId;
|
||||||
|
|
||||||
|
@ApiModelProperty("关联巡检结果ID")
|
||||||
|
private String resultId;
|
||||||
|
|
||||||
|
@ApiModelProperty("告警来源:PPE、METER、MANUAL")
|
||||||
|
private String alarmSource;
|
||||||
|
|
||||||
|
@ApiModelProperty("检测项ID")
|
||||||
|
private String itemId;
|
||||||
|
|
||||||
|
@ApiModelProperty("工作流节点ID")
|
||||||
|
private String nodeId;
|
||||||
|
|
||||||
@Excel(name = "任务ID")
|
@Excel(name = "任务ID")
|
||||||
@ApiModelProperty("任务ID")
|
@ApiModelProperty("任务ID")
|
||||||
private String taskId;
|
private String taskId;
|
||||||
|
|||||||
@ -0,0 +1,130 @@
|
|||||||
|
package com.cmvr.inspection.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.cmvr.common.annotation.Excel;
|
||||||
|
import com.cmvr.common.core.domain.BaseEntity;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一巡检结果,承载PPE、仪表读数和人工判断结果。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("inspection_result")
|
||||||
|
@ApiModel(value = "InspectionResult", description = "PPE识别、仪表读数识别和人工复核共用的巡检结果")
|
||||||
|
public class InspectionResult extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.INPUT)
|
||||||
|
@ApiModelProperty(value = "巡检结果主键", example = "8f3e1f3a2db84de2a84eebdf8951b86a")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Excel(name = "结果编码")
|
||||||
|
@ApiModelProperty(value = "巡检结果业务编码", example = "IR20260723143000123ABCDE")
|
||||||
|
private String resultCode;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "结果幂等键,平台内部使用", hidden = true)
|
||||||
|
private String dedupeKey;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "巡检任务实例数据库ID", example = "task-instance-id")
|
||||||
|
private String taskInstanceId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "工作流运行实例ID", example = "flow-instance-id")
|
||||||
|
private String flowInstanceId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "巡检任务ID", example = "inspection-task-id")
|
||||||
|
private String taskId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "检测项ID", example = "detection-item-id")
|
||||||
|
private String itemId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "工作流节点ID", example = "meter-node-1")
|
||||||
|
private String nodeId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "工作流节点名称", example = "1号压力表读数识别")
|
||||||
|
private String nodeName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "循环节点迭代路径JSON;非循环节点通常为[]", example = "[1,2]")
|
||||||
|
private String iterationPath;
|
||||||
|
|
||||||
|
@Excel(name = "结果类型")
|
||||||
|
@ApiModelProperty(value = "结果类型", allowableValues = "PPE,METER,MANUAL", example = "METER")
|
||||||
|
private String resultType;
|
||||||
|
|
||||||
|
@Excel(name = "结果状态")
|
||||||
|
@ApiModelProperty(value = "结果状态", allowableValues = "PENDING,NORMAL,ABNORMAL,RECOGNIZE_FAILED", example = "ABNORMAL")
|
||||||
|
private String resultStatus;
|
||||||
|
|
||||||
|
@Excel(name = "检查名称")
|
||||||
|
@ApiModelProperty(value = "检查项显示名称", example = "1号压力表")
|
||||||
|
private String resultName;
|
||||||
|
|
||||||
|
@Excel(name = "数值")
|
||||||
|
@ApiModelProperty(value = "结构化仪表读数;PPE和人工结果通常为空", example = "1.72")
|
||||||
|
private BigDecimal valueNumber;
|
||||||
|
|
||||||
|
@Excel(name = "结果内容")
|
||||||
|
@ApiModelProperty(value = "文本结果或原始识别值", example = "1.72")
|
||||||
|
private String valueText;
|
||||||
|
|
||||||
|
@Excel(name = "单位")
|
||||||
|
@ApiModelProperty(value = "仪表读数单位", example = "MPa")
|
||||||
|
private String unit;
|
||||||
|
|
||||||
|
@Excel(name = "告警级别")
|
||||||
|
@ApiModelProperty(value = "最终命中的最高告警级别:1提示、2警告、3严重", allowableValues = "1,2,3", example = "2")
|
||||||
|
private Integer alarmLevel;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "告警描述", example = "压力过高")
|
||||||
|
private String alarmMessage;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "模型置信度,范围0到1", example = "0.96")
|
||||||
|
private BigDecimal confidence;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "PPE边缘事件event_id", example = "event-abc")
|
||||||
|
private String sourceEventId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "主要证据的永久MinIO地址")
|
||||||
|
private String evidenceUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "主要证据类型,与evidenceUrl同时写入",
|
||||||
|
allowableValues = "IMAGE,VIDEO", example = "IMAGE")
|
||||||
|
private String evidenceType;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "模型或边缘端原始返回内容")
|
||||||
|
private String rawResult;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "最终命中的告警规则JSON快照")
|
||||||
|
private String matchedRuleJson;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "本次执行使用的全部告警规则JSON快照")
|
||||||
|
private String ruleSnapshotJson;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "人工复核账号", example = "admin")
|
||||||
|
private String reviewer;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "人工复核时间")
|
||||||
|
private Date reviewTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "人工复核说明")
|
||||||
|
private String reviewRemark;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "人工复核乐观锁版本,首次复核前为0", example = "0")
|
||||||
|
private Integer reviewVersion;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "异常结果关联的巡检告警ID")
|
||||||
|
private String alarmId;
|
||||||
|
|
||||||
|
@Excel(name = "检测时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@ApiModelProperty(value = "结果发生时间")
|
||||||
|
private Date occurredTime;
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package com.cmvr.inspection.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检结果关联的图片或视频证据。
|
||||||
|
*
|
||||||
|
* <p>媒体内容保存在MinIO,本表只保存永久访问地址,不保存文件字节。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("inspection_result_media")
|
||||||
|
@ApiModel(value = "InspectionResultMedia", description = "巡检结果关联的图片或视频证据")
|
||||||
|
public class InspectionResultMedia implements Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.INPUT)
|
||||||
|
@ApiModelProperty("媒体记录主键")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty("关联巡检结果ID")
|
||||||
|
private String resultId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "媒体类型", allowableValues = "IMAGE,VIDEO", example = "IMAGE")
|
||||||
|
private String mediaType;
|
||||||
|
|
||||||
|
@ApiModelProperty("MinIO永久媒体地址")
|
||||||
|
private String mediaUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "同一巡检结果内的展示顺序", example = "0")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
@ApiModelProperty("记录创建时间")
|
||||||
|
private Date createTime;
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.cmvr.inspection.domain.dto;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** 巡检结果列表和报表导出的查询条件。 */
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "InspectionResultQuery", description = "巡检结果查询条件,所有字段均为可选条件")
|
||||||
|
public class InspectionResultQuery
|
||||||
|
{
|
||||||
|
@ApiModelProperty("巡检任务实例数据库ID")
|
||||||
|
private String taskInstanceId;
|
||||||
|
|
||||||
|
@ApiModelProperty("巡检任务ID")
|
||||||
|
private String taskId;
|
||||||
|
|
||||||
|
@ApiModelProperty("检测项ID")
|
||||||
|
private String itemId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "结果类型", allowableValues = "PPE,METER,MANUAL")
|
||||||
|
private String resultType;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "结果状态", allowableValues = "PENDING,NORMAL,ABNORMAL,RECOGNIZE_FAILED")
|
||||||
|
private String resultStatus;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "告警级别", allowableValues = "1,2,3")
|
||||||
|
private Integer alarmLevel;
|
||||||
|
|
||||||
|
@ApiModelProperty("检查名称,支持模糊查询")
|
||||||
|
private String resultName;
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.cmvr.inspection.domain.dto;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人工复核提交参数。
|
||||||
|
*
|
||||||
|
* <p>只能复核PENDING或RECOGNIZE_FAILED状态的结果。提交ABNORMAL时必须指定alarmLevel。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ApiModel("巡检结果人工复核参数")
|
||||||
|
public class InspectionResultReviewRequest
|
||||||
|
{
|
||||||
|
@NotBlank(message = "resultStatus不能为空")
|
||||||
|
@ApiModelProperty(value = "复核结果", required = true,
|
||||||
|
allowableValues = "NORMAL,ABNORMAL", example = "ABNORMAL")
|
||||||
|
private String resultStatus;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "异常时必填:1提示、2警告、3严重",
|
||||||
|
allowableValues = "1,2,3", example = "2")
|
||||||
|
private Integer alarmLevel;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "人工复核说明", example = "设备右侧存在漏油")
|
||||||
|
private String reviewRemark;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "当前reviewVersion,用于防止多人重复复核;首次通常为0", example = "0")
|
||||||
|
private Integer reviewVersion;
|
||||||
|
}
|
||||||
@ -30,6 +30,18 @@ public class InspectionAlarmVo extends BaseEntity
|
|||||||
@ApiModelProperty("id")
|
@ApiModelProperty("id")
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty("关联巡检结果ID")
|
||||||
|
private String resultId;
|
||||||
|
|
||||||
|
@ApiModelProperty("告警来源:PPE、METER、MANUAL")
|
||||||
|
private String alarmSource;
|
||||||
|
|
||||||
|
@ApiModelProperty("检测项ID")
|
||||||
|
private String itemId;
|
||||||
|
|
||||||
|
@ApiModelProperty("工作流节点ID")
|
||||||
|
private String nodeId;
|
||||||
|
|
||||||
/** 告警编码 */
|
/** 告警编码 */
|
||||||
@Excel(name = "告警编码")
|
@Excel(name = "告警编码")
|
||||||
@ApiModelProperty("告警编码")
|
@ApiModelProperty("告警编码")
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.cmvr.inspection.domain.vo;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** 单次巡检任务的结果汇总。 */
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "InspectionReportSummaryVo", description = "单次巡检任务的结果统计汇总")
|
||||||
|
public class InspectionReportSummaryVo
|
||||||
|
{
|
||||||
|
@ApiModelProperty("巡检任务实例数据库ID")
|
||||||
|
private String taskInstanceId;
|
||||||
|
@ApiModelProperty("结果总数")
|
||||||
|
private long totalCount;
|
||||||
|
@ApiModelProperty("正常结果数")
|
||||||
|
private long normalCount;
|
||||||
|
@ApiModelProperty("异常结果数")
|
||||||
|
private long abnormalCount;
|
||||||
|
@ApiModelProperty("待人工复核数")
|
||||||
|
private long pendingCount;
|
||||||
|
@ApiModelProperty("识别失败数")
|
||||||
|
private long recognizeFailedCount;
|
||||||
|
@ApiModelProperty("按告警级别统计,key为1、2、3")
|
||||||
|
private Map<Integer, Long> alarmLevelCounts = new LinkedHashMap<>();
|
||||||
|
@ApiModelProperty("按结果类型统计,key为PPE、METER、MANUAL")
|
||||||
|
private Map<String, Long> resultTypeCounts = new LinkedHashMap<>();
|
||||||
|
@ApiModelProperty("是否不存在待复核和识别失败结果")
|
||||||
|
private boolean reviewCompleted;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.cmvr.inspection.domain.vo;
|
||||||
|
|
||||||
|
import com.cmvr.common.annotation.Excel;
|
||||||
|
import com.cmvr.inspection.domain.InspectionResult;
|
||||||
|
import com.cmvr.inspection.domain.InspectionResultMedia;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 巡检结果详情,包含全部媒体证据。 */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ApiModel(value = "InspectionResultVo", description = "巡检结果详情及其全部图片、视频证据")
|
||||||
|
public class InspectionResultVo extends InspectionResult
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Excel(name = "任务名称")
|
||||||
|
@ApiModelProperty(value = "巡检任务名称", example = "锅炉房日常巡检")
|
||||||
|
private String taskName;
|
||||||
|
|
||||||
|
@ApiModelProperty("巡检结果关联的全部媒体证据,按sortOrder升序")
|
||||||
|
private List<InspectionResultMedia> mediaList;
|
||||||
|
}
|
||||||
@ -4,12 +4,14 @@ package com.cmvr.inspection.listener;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.cmvr.common.utils.DateUtils;
|
import com.cmvr.common.utils.DateUtils;
|
||||||
import com.cmvr.framework.websocket.service.MessagePushService;
|
import com.cmvr.framework.websocket.service.MessagePushService;
|
||||||
|
import com.cmvr.device.service.InspectionAlertListenService;
|
||||||
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
||||||
import com.cmvr.inspection.domain.InspectionTaskLog;
|
import com.cmvr.inspection.domain.InspectionTaskLog;
|
||||||
import com.cmvr.inspection.enums.InspectionLogTypeEnum;
|
import com.cmvr.inspection.enums.InspectionLogTypeEnum;
|
||||||
import com.cmvr.inspection.enums.TaskStatusEnum;
|
import com.cmvr.inspection.enums.TaskStatusEnum;
|
||||||
import com.cmvr.inspection.service.IInspectionTaskInstanceService;
|
import com.cmvr.inspection.service.IInspectionTaskInstanceService;
|
||||||
import com.cmvr.inspection.service.IInspectionTaskLogService;
|
import com.cmvr.inspection.service.IInspectionTaskLogService;
|
||||||
|
import com.cmvr.inspection.service.IInspectionResultService;
|
||||||
import com.cmvr.test.flow.runtime.event.FlowExecutionEvent;
|
import com.cmvr.test.flow.runtime.event.FlowExecutionEvent;
|
||||||
import com.cmvr.test.flow.runtime.event.FlowExecutionListener;
|
import com.cmvr.test.flow.runtime.event.FlowExecutionListener;
|
||||||
import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes;
|
import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes;
|
||||||
@ -24,18 +26,24 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
private final IInspectionTaskInstanceService inspectionTaskInstanceService;
|
private final IInspectionTaskInstanceService inspectionTaskInstanceService;
|
||||||
private final IInspectionTaskLogService inspectionTaskLogService;
|
private final IInspectionTaskLogService inspectionTaskLogService;
|
||||||
private final MessagePushService messagePushService;
|
private final MessagePushService messagePushService;
|
||||||
|
private final IInspectionResultService inspectionResultService;
|
||||||
|
private final InspectionAlertListenService inspectionAlertListenService;
|
||||||
|
|
||||||
/** key:itemId, value:已完成节点集合 */
|
/** key:flowInstId|itemId, value:已完成节点集合 */
|
||||||
private final Map<String, Set<String>> itemNodeExecutMap = new HashMap<>();
|
private final Map<String, Set<String>> itemNodeExecutMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
/** key:taskInsId(流程实例id=taskInsId), value:已执行item集合 */
|
/** key:taskInsId(流程实例id=taskInsId), value:已执行item集合 */
|
||||||
private final Map<String, Set<String>> taskItemExecutMap = new HashMap<>();
|
private final Map<String, Set<String>> taskItemExecutMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
public InspectionFlowExecutionListener(IInspectionTaskInstanceService inspectionTaskInstanceService,
|
public InspectionFlowExecutionListener(IInspectionTaskInstanceService inspectionTaskInstanceService,
|
||||||
IInspectionTaskLogService inspectionTaskLogService,
|
IInspectionTaskLogService inspectionTaskLogService,
|
||||||
MessagePushService messagePushService) {
|
MessagePushService messagePushService,
|
||||||
|
IInspectionResultService inspectionResultService,
|
||||||
|
InspectionAlertListenService inspectionAlertListenService) {
|
||||||
this.inspectionTaskInstanceService = inspectionTaskInstanceService;
|
this.inspectionTaskInstanceService = inspectionTaskInstanceService;
|
||||||
this.inspectionTaskLogService = inspectionTaskLogService;
|
this.inspectionTaskLogService = inspectionTaskLogService;
|
||||||
this.messagePushService = messagePushService;
|
this.messagePushService = messagePushService;
|
||||||
|
this.inspectionResultService = inspectionResultService;
|
||||||
|
this.inspectionAlertListenService = inspectionAlertListenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -52,19 +60,27 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 无论数据库是否可用,任务结束事件都必须先清理内存监听和进度状态。
|
||||||
|
if (FlowExecutionEvent.EventType.TASK_COMPLETED == event.getEventType()
|
||||||
|
|| FlowExecutionEvent.EventType.TASK_FAILED == event.getEventType()
|
||||||
|
|| FlowExecutionEvent.EventType.TASK_STOPPED == event.getEventType()) {
|
||||||
|
clearCache(instId);
|
||||||
|
inspectionAlertListenService.clearByFlowInstanceId(instId);
|
||||||
|
}
|
||||||
|
|
||||||
// 1.节点完成才记录当前item下node
|
// 1.节点完成才记录当前item下node
|
||||||
if (StrUtil.isNotBlank(itemId) && StrUtil.isNotBlank(nodeId)
|
if (StrUtil.isNotBlank(itemId) && StrUtil.isNotBlank(nodeId)
|
||||||
&& FlowExecutionEvent.EventType.NODE_COMPLETED == event.getEventType()) {
|
&& FlowExecutionEvent.EventType.NODE_COMPLETED == event.getEventType()) {
|
||||||
itemNodeExecutMap.computeIfAbsent(itemId, k -> new HashSet<>()).add(nodeId);
|
itemNodeExecutMap.computeIfAbsent(itemExecutionKey(instId, itemId),
|
||||||
|
k -> java.util.concurrent.ConcurrentHashMap.newKeySet()).add(nodeId);
|
||||||
}
|
}
|
||||||
// 任意事件:当前任务绑定item(item一启动就入Map,所以统计已完成item要-1)
|
// 任意事件:当前任务绑定item(item一启动就入Map,所以统计已完成item要-1)
|
||||||
if (StrUtil.isNotBlank(instId) && StrUtil.isNotBlank(itemId)) {
|
if (StrUtil.isNotBlank(instId) && StrUtil.isNotBlank(itemId)) {
|
||||||
taskItemExecutMap.computeIfAbsent(instId, k -> new HashSet<>()).add(itemId);
|
taskItemExecutMap.computeIfAbsent(instId,
|
||||||
|
k -> java.util.concurrent.ConcurrentHashMap.newKeySet()).add(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
InspectionTaskInstance taskInstance = inspectionTaskInstanceService.lambdaQuery()
|
InspectionTaskInstance taskInstance = findTaskInstance(instId);
|
||||||
.eq(InspectionTaskInstance::getTaskInsId, instId)
|
|
||||||
.one();
|
|
||||||
if (taskInstance == null) {
|
if (taskInstance == null) {
|
||||||
log.warn("未查询到巡检实例,taskInsId:{}", instId);
|
log.warn("未查询到巡检实例,taskInsId:{}", instId);
|
||||||
return;
|
return;
|
||||||
@ -77,13 +93,13 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
case TASK_COMPLETED:
|
case TASK_COMPLETED:
|
||||||
targetStatus = TaskStatusEnum.SUCCESS.getCode();
|
targetStatus = TaskStatusEnum.SUCCESS.getCode();
|
||||||
needUpdateDb = true;
|
needUpdateDb = true;
|
||||||
clearCache(instId);
|
|
||||||
pushCompleteMsg(taskInstance.getId(), now, targetStatus);
|
pushCompleteMsg(taskInstance.getId(), now, targetStatus);
|
||||||
break;
|
break;
|
||||||
case TASK_FAILED:
|
case TASK_FAILED:
|
||||||
targetStatus = TaskStatusEnum.FAILED.getCode();
|
targetStatus = TaskStatusEnum.FAILED.getCode();
|
||||||
needUpdateDb = true;
|
needUpdateDb = true;
|
||||||
clearCache(instId);
|
break;
|
||||||
|
case TASK_STOPPED:
|
||||||
break;
|
break;
|
||||||
case NODE_COMPLETED:
|
case NODE_COMPLETED:
|
||||||
default:
|
default:
|
||||||
@ -96,6 +112,15 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
inspectionTaskInstanceService.updateById(taskInstance);
|
inspectionTaskInstanceService.updateById(taskInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FlowExecutionEvent.EventType.NODE_COMPLETED == event.getEventType()) {
|
||||||
|
try {
|
||||||
|
inspectionResultService.recordWorkflowResult(event, taskInstance);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存工作流巡检结果失败,taskInstanceId:{},nodeId:{}",
|
||||||
|
taskInstance.getId(), nodeId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 节点推送
|
// 节点推送
|
||||||
if (StrUtil.isNotBlank(nodeId)) {
|
if (StrUtil.isNotBlank(nodeId)) {
|
||||||
int waypointCount = taskInstance.getWaypointCount() == null ? 0 : taskInstance.getWaypointCount();
|
int waypointCount = taskInstance.getWaypointCount() == null ? 0 : taskInstance.getWaypointCount();
|
||||||
@ -107,7 +132,8 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
int finishedItemNum = itemTotal - 1;
|
int finishedItemNum = itemTotal - 1;
|
||||||
|
|
||||||
// 当前item已完成节点数
|
// 当前item已完成节点数
|
||||||
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(itemId, new HashSet<>());
|
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(
|
||||||
|
itemExecutionKey(instId, itemId), new HashSet<>());
|
||||||
int finishedNode = finishedNodeSet.size();
|
int finishedNode = finishedNodeSet.size();
|
||||||
int totalNode = event.getNodeCount() == 0 ? 1 : event.getNodeCount();
|
int totalNode = event.getNodeCount() == 0 ? 1 : event.getNodeCount();
|
||||||
|
|
||||||
@ -159,10 +185,35 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
/** 任务结束统一清理缓存 */
|
/** 任务结束统一清理缓存 */
|
||||||
private void clearCache(String taskInsId) {
|
private void clearCache(String taskInsId) {
|
||||||
Set<String> itemList = taskItemExecutMap.getOrDefault(taskInsId, new HashSet<>());
|
Set<String> itemList = taskItemExecutMap.getOrDefault(taskInsId, new HashSet<>());
|
||||||
itemList.forEach(itemNodeExecutMap::remove);
|
itemList.forEach(itemId -> itemNodeExecutMap.remove(itemExecutionKey(taskInsId, itemId)));
|
||||||
taskItemExecutMap.remove(taskInsId);
|
taskItemExecutMap.remove(taskInsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String itemExecutionKey(String taskInsId, String itemId) {
|
||||||
|
return taskInsId + "|" + itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流是异步启动的,首个节点事件可能略早于巡检实例写入task_ins_id,短暂重试消除该竞态。
|
||||||
|
*/
|
||||||
|
private InspectionTaskInstance findTaskInstance(String taskInsId) {
|
||||||
|
for (int attempt = 0; attempt < 5; attempt++) {
|
||||||
|
InspectionTaskInstance instance = inspectionTaskInstanceService.lambdaQuery()
|
||||||
|
.eq(InspectionTaskInstance::getTaskInsId, taskInsId)
|
||||||
|
.one();
|
||||||
|
if (instance != null) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(50L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推送完成任务信息(参数dbInsId为数据库主键ID)
|
* 推送完成任务信息(参数dbInsId为数据库主键ID)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.cmvr.inspection.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.cmvr.inspection.domain.InspectionResult;
|
||||||
|
|
||||||
|
/** 巡检结果Mapper。 */
|
||||||
|
public interface InspectionResultMapper extends BaseMapper<InspectionResult>
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.cmvr.inspection.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.cmvr.inspection.domain.InspectionResultMedia;
|
||||||
|
|
||||||
|
/** 巡检结果媒体Mapper。 */
|
||||||
|
public interface InspectionResultMediaMapper extends BaseMapper<InspectionResultMedia>
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.cmvr.inspection.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.cmvr.device.service.InspectionAlertListenService;
|
||||||
|
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||||
|
import com.cmvr.inspection.domain.InspectionResult;
|
||||||
|
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
||||||
|
import com.cmvr.inspection.domain.dto.InspectionResultQuery;
|
||||||
|
import com.cmvr.inspection.domain.dto.InspectionResultReviewRequest;
|
||||||
|
import com.cmvr.inspection.domain.vo.InspectionReportSummaryVo;
|
||||||
|
import com.cmvr.inspection.domain.vo.InspectionResultVo;
|
||||||
|
import com.cmvr.test.flow.runtime.event.FlowExecutionEvent;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 统一巡检结果业务接口。 */
|
||||||
|
public interface IInspectionResultService extends IService<InspectionResult>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 按任务、类型、状态、级别等条件查询巡检结果。
|
||||||
|
*
|
||||||
|
* @param query 查询条件,允许为空
|
||||||
|
* @return 按发生时间倒序排列的巡检结果
|
||||||
|
*/
|
||||||
|
List<InspectionResultVo> selectResultList(InspectionResultQuery query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询巡检结果和全部媒体证据。
|
||||||
|
*
|
||||||
|
* @param id 巡检结果ID
|
||||||
|
* @return 结果详情,不存在时返回null
|
||||||
|
*/
|
||||||
|
InspectionResultVo selectResultById(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对待复核或识别失败结果进行人工判定。
|
||||||
|
*
|
||||||
|
* @param id 巡检结果ID
|
||||||
|
* @param request 复核结论、告警级别、说明和版本
|
||||||
|
* @return 更新后的巡检结果
|
||||||
|
*/
|
||||||
|
InspectionResult review(String id, InspectionResultReviewRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总单个巡检任务实例的结果数量、告警级别和复核完成状态。
|
||||||
|
*
|
||||||
|
* @param taskInstanceId 巡检任务实例数据库ID
|
||||||
|
* @return 报表汇总
|
||||||
|
*/
|
||||||
|
InspectionReportSummaryVo buildReportSummary(String taskInstanceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从工作流节点标准输出中提取并保存巡检结果。
|
||||||
|
*
|
||||||
|
* @param event 节点完成事件
|
||||||
|
* @param taskInstance 巡检任务实例
|
||||||
|
* @return 保存后的结果;节点无巡检结果输出时返回null
|
||||||
|
*/
|
||||||
|
InspectionResult recordWorkflowResult(FlowExecutionEvent event, InspectionTaskInstance taskInstance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将边缘端PPE事件转换为统一巡检结果,并按异常级别生成告警。
|
||||||
|
*
|
||||||
|
* @param alert 已保存的PPE原始事件
|
||||||
|
* @param subscription 命中的工作流监听订阅
|
||||||
|
* @param taskInstance 巡检任务实例,极端竞态下允许为空
|
||||||
|
* @return 保存后的PPE巡检结果
|
||||||
|
*/
|
||||||
|
InspectionResult recordPpeResult(InspectionDetectionAlert alert,
|
||||||
|
InspectionAlertListenService.ListenSubscription subscription,
|
||||||
|
InspectionTaskInstance taskInstance);
|
||||||
|
}
|
||||||
@ -4,13 +4,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.cmvr.common.config.properties.MinioProperties;
|
import com.cmvr.common.config.properties.MinioProperties;
|
||||||
import com.cmvr.common.core.minio.MinioService;
|
import com.cmvr.common.core.minio.MinioService;
|
||||||
import com.cmvr.device.service.InspectionAlertListenService;
|
import com.cmvr.device.service.InspectionAlertListenService;
|
||||||
|
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
||||||
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||||
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||||
import com.cmvr.inspection.domain.dto.alert.AlertImage;
|
import com.cmvr.inspection.domain.dto.alert.AlertImage;
|
||||||
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
|
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
|
||||||
import com.cmvr.inspection.exception.DetectionAlertTemporaryException;
|
import com.cmvr.inspection.exception.DetectionAlertTemporaryException;
|
||||||
import com.cmvr.inspection.mapper.InspectionDetectionAlertMapper;
|
import com.cmvr.inspection.mapper.InspectionDetectionAlertMapper;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionTaskInstanceMapper;
|
||||||
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
|
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
|
||||||
|
import com.cmvr.inspection.service.IInspectionResultService;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -26,6 +29,7 @@ import java.util.Base64;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PPE 违规报警接收业务实现。
|
* PPE 违规报警接收业务实现。
|
||||||
@ -49,6 +53,8 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
|
|||||||
private final MinioProperties minioProperties;
|
private final MinioProperties minioProperties;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final InspectionAlertListenService inspectionAlertListenService;
|
private final InspectionAlertListenService inspectionAlertListenService;
|
||||||
|
private final InspectionTaskInstanceMapper taskInstanceMapper;
|
||||||
|
private final IInspectionResultService inspectionResultService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@ -58,7 +64,10 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
|
|||||||
String idempotencyKey = trimToNull(requestIdempotencyKey);
|
String idempotencyKey = trimToNull(requestIdempotencyKey);
|
||||||
|
|
||||||
// 工作流未开启对应 IP + 事件类型监听时,报警只确认接收,不做持久化。
|
// 工作流未开启对应 IP + 事件类型监听时,报警只确认接收,不做持久化。
|
||||||
if (!inspectionAlertListenService.shouldStore(envelope.getGrpcIp(), envelope.getPayload().getLabels()))
|
List<InspectionAlertListenService.ListenSubscription> matchedSubscriptions =
|
||||||
|
inspectionAlertListenService.matchSubscriptions(
|
||||||
|
envelope.getGrpcIp(), envelope.getPayload().getLabels());
|
||||||
|
if (matchedSubscriptions.isEmpty())
|
||||||
{
|
{
|
||||||
log.info("忽略未监听的PPE报警,eventId={},grpcIp={},labels={}",
|
log.info("忽略未监听的PPE报警,eventId={},grpcIp={},labels={}",
|
||||||
eventId, envelope.getGrpcIp(), envelope.getPayload().getLabels());
|
eventId, envelope.getGrpcIp(), envelope.getPayload().getLabels());
|
||||||
@ -88,6 +97,17 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (InspectionAlertListenService.ListenSubscription subscription : matchedSubscriptions)
|
||||||
|
{
|
||||||
|
InspectionTaskInstance taskInstance = findTaskInstance(subscription.getFlowInstanceId());
|
||||||
|
if (taskInstance == null)
|
||||||
|
{
|
||||||
|
log.warn("PPE报警未找到巡检任务实例,eventId={},flowInstanceId={}",
|
||||||
|
eventId, subscription.getFlowInstanceId());
|
||||||
|
}
|
||||||
|
inspectionResultService.recordPpeResult(alert, subscription, taskInstance);
|
||||||
|
}
|
||||||
|
|
||||||
log.info("PPE报警接收成功,alertId={},eventId={},sourceId={}",
|
log.info("PPE报警接收成功,alertId={},eventId={},sourceId={}",
|
||||||
alert.getId(), eventId, envelope.getSourceId());
|
alert.getId(), eventId, envelope.getSourceId());
|
||||||
return true;
|
return true;
|
||||||
@ -103,6 +123,16 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private InspectionTaskInstance findTaskInstance(String flowInstanceId)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(flowInstanceId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return taskInstanceMapper.selectOne(new LambdaQueryWrapper<InspectionTaskInstance>()
|
||||||
|
.eq(InspectionTaskInstance::getTaskInsId, flowInstanceId).last("limit 1"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验协议关键字段,并返回用于重复过滤的 event_id。
|
* 校验协议关键字段,并返回用于重复过滤的 event_id。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,739 @@
|
|||||||
|
package com.cmvr.inspection.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.cmvr.common.exception.ServiceException;
|
||||||
|
import com.cmvr.common.utils.DateUtils;
|
||||||
|
import com.cmvr.common.utils.SecurityUtils;
|
||||||
|
import com.cmvr.device.service.InspectionAlertListenService;
|
||||||
|
import com.cmvr.framework.websocket.service.MessagePushService;
|
||||||
|
import com.cmvr.inspection.domain.InspectionAlarm;
|
||||||
|
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||||
|
import com.cmvr.inspection.domain.InspectionResult;
|
||||||
|
import com.cmvr.inspection.domain.InspectionResultMedia;
|
||||||
|
import com.cmvr.inspection.domain.InspectionRobot;
|
||||||
|
import com.cmvr.inspection.domain.InspectionTask;
|
||||||
|
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
||||||
|
import com.cmvr.inspection.domain.InspectionTaskLog;
|
||||||
|
import com.cmvr.inspection.domain.dto.InspectionResultQuery;
|
||||||
|
import com.cmvr.inspection.domain.dto.InspectionResultReviewRequest;
|
||||||
|
import com.cmvr.inspection.domain.vo.InspectionReportSummaryVo;
|
||||||
|
import com.cmvr.inspection.domain.vo.InspectionResultVo;
|
||||||
|
import com.cmvr.inspection.enums.InspectionLogTypeEnum;
|
||||||
|
import com.cmvr.inspection.enums.TaskStatusEnum;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionAlarmMapper;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionResultMapper;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionResultMediaMapper;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionRobotMapper;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionTaskMapper;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionTaskInstanceMapper;
|
||||||
|
import com.cmvr.inspection.service.IInspectionResultService;
|
||||||
|
import com.cmvr.inspection.service.IInspectionTaskLogService;
|
||||||
|
import com.cmvr.test.flow.runtime.event.FlowExecutionEvent;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一巡检结果业务实现。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InspectionResultServiceImpl extends ServiceImpl<InspectionResultMapper, InspectionResult>
|
||||||
|
implements IInspectionResultService
|
||||||
|
{
|
||||||
|
private static final String RESULT_MARKER = "_inspectionResult";
|
||||||
|
private static final String STATUS_PENDING = "PENDING";
|
||||||
|
private static final String STATUS_NORMAL = "NORMAL";
|
||||||
|
private static final String STATUS_ABNORMAL = "ABNORMAL";
|
||||||
|
private static final String STATUS_RECOGNIZE_FAILED = "RECOGNIZE_FAILED";
|
||||||
|
|
||||||
|
private final InspectionResultMediaMapper mediaMapper;
|
||||||
|
private final InspectionAlarmMapper alarmMapper;
|
||||||
|
private final InspectionTaskMapper taskMapper;
|
||||||
|
private final InspectionTaskInstanceMapper taskInstanceMapper;
|
||||||
|
private final InspectionRobotMapper robotMapper;
|
||||||
|
private final IInspectionTaskLogService taskLogService;
|
||||||
|
private final MessagePushService messagePushService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<InspectionResultVo> selectResultList(InspectionResultQuery query)
|
||||||
|
{
|
||||||
|
InspectionResultQuery condition = query == null ? new InspectionResultQuery() : query;
|
||||||
|
LambdaQueryWrapper<InspectionResult> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(StringUtils.isNotBlank(condition.getTaskInstanceId()),
|
||||||
|
InspectionResult::getTaskInstanceId, condition.getTaskInstanceId())
|
||||||
|
.eq(StringUtils.isNotBlank(condition.getTaskId()),
|
||||||
|
InspectionResult::getTaskId, condition.getTaskId())
|
||||||
|
.eq(StringUtils.isNotBlank(condition.getItemId()),
|
||||||
|
InspectionResult::getItemId, condition.getItemId())
|
||||||
|
.eq(StringUtils.isNotBlank(condition.getResultType()),
|
||||||
|
InspectionResult::getResultType, condition.getResultType())
|
||||||
|
.eq(StringUtils.isNotBlank(condition.getResultStatus()),
|
||||||
|
InspectionResult::getResultStatus, condition.getResultStatus())
|
||||||
|
.eq(condition.getAlarmLevel() != null,
|
||||||
|
InspectionResult::getAlarmLevel, condition.getAlarmLevel())
|
||||||
|
.like(StringUtils.isNotBlank(condition.getResultName()),
|
||||||
|
InspectionResult::getResultName, condition.getResultName())
|
||||||
|
.orderByDesc(InspectionResult::getOccurredTime, InspectionResult::getCreateTime);
|
||||||
|
return toResultVoList(list(wrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InspectionResultVo selectResultById(String id)
|
||||||
|
{
|
||||||
|
InspectionResult result = getById(id);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
InspectionResultVo vo = new InspectionResultVo();
|
||||||
|
BeanUtils.copyProperties(result, vo);
|
||||||
|
if (StringUtils.isNotBlank(result.getTaskId()))
|
||||||
|
{
|
||||||
|
InspectionTask task = taskMapper.selectById(result.getTaskId());
|
||||||
|
vo.setTaskName(task == null ? null : task.getTaskName());
|
||||||
|
}
|
||||||
|
List<InspectionResultMedia> mediaList = mediaMapper.selectList(new LambdaQueryWrapper<InspectionResultMedia>()
|
||||||
|
.eq(InspectionResultMedia::getResultId, id)
|
||||||
|
.orderByAsc(InspectionResultMedia::getSortOrder));
|
||||||
|
vo.setMediaList(mediaList);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量补充任务名称并转换为列表视图,避免按结果逐条查询任务表。
|
||||||
|
*/
|
||||||
|
private List<InspectionResultVo> toResultVoList(List<InspectionResult> results)
|
||||||
|
{
|
||||||
|
if (results == null || results.isEmpty())
|
||||||
|
{
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> taskIds = results.stream()
|
||||||
|
.map(InspectionResult::getTaskId)
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.collect(Collectors.toCollection(HashSet::new));
|
||||||
|
Map<String, InspectionTask> taskMap = taskIds.isEmpty()
|
||||||
|
? Collections.emptyMap()
|
||||||
|
: taskMapper.selectBatchIds(taskIds).stream()
|
||||||
|
.collect(Collectors.toMap(InspectionTask::getId, Function.identity(), (left, right) -> left));
|
||||||
|
|
||||||
|
List<InspectionResultVo> resultVos = new ArrayList<>(results.size());
|
||||||
|
for (InspectionResult result : results)
|
||||||
|
{
|
||||||
|
InspectionResultVo vo = new InspectionResultVo();
|
||||||
|
BeanUtils.copyProperties(result, vo);
|
||||||
|
InspectionTask task = taskMap.get(result.getTaskId());
|
||||||
|
vo.setTaskName(task == null ? null : task.getTaskName());
|
||||||
|
resultVos.add(vo);
|
||||||
|
}
|
||||||
|
return resultVos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交人工复核。使用reviewVersion做条件更新,保证并发审核只有一个请求成功。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public InspectionResult review(String id, InspectionResultReviewRequest request)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("复核参数不能为空");
|
||||||
|
}
|
||||||
|
String targetStatus = StringUtils.upperCase(StringUtils.trimToEmpty(request.getResultStatus()));
|
||||||
|
if (!STATUS_NORMAL.equals(targetStatus) && !STATUS_ABNORMAL.equals(targetStatus))
|
||||||
|
{
|
||||||
|
throw new ServiceException("人工复核结果只能为NORMAL或ABNORMAL");
|
||||||
|
}
|
||||||
|
if (STATUS_ABNORMAL.equals(targetStatus)
|
||||||
|
&& (request.getAlarmLevel() == null || request.getAlarmLevel() < 1 || request.getAlarmLevel() > 3))
|
||||||
|
{
|
||||||
|
throw new ServiceException("异常结果必须指定1到3级告警级别");
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectionResult current = getById(id);
|
||||||
|
if (current == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("巡检结果不存在");
|
||||||
|
}
|
||||||
|
if (!STATUS_PENDING.equals(current.getResultStatus())
|
||||||
|
&& !STATUS_RECOGNIZE_FAILED.equals(current.getResultStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("该巡检结果已完成复核,不能重复提交");
|
||||||
|
}
|
||||||
|
|
||||||
|
int expectedVersion = request.getReviewVersion() == null
|
||||||
|
? valueOrZero(current.getReviewVersion()) : request.getReviewVersion();
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
Date now = DateUtils.getNowDate();
|
||||||
|
LambdaUpdateWrapper<InspectionResult> update = new LambdaUpdateWrapper<>();
|
||||||
|
update.eq(InspectionResult::getId, id)
|
||||||
|
.eq(InspectionResult::getReviewVersion, expectedVersion)
|
||||||
|
.set(InspectionResult::getResultStatus, targetStatus)
|
||||||
|
.set(InspectionResult::getAlarmLevel,
|
||||||
|
STATUS_ABNORMAL.equals(targetStatus) ? request.getAlarmLevel() : null)
|
||||||
|
.set(InspectionResult::getAlarmMessage,
|
||||||
|
STATUS_ABNORMAL.equals(targetStatus) ? "人工复核判定异常" : null)
|
||||||
|
.set(InspectionResult::getReviewer, username)
|
||||||
|
.set(InspectionResult::getReviewTime, now)
|
||||||
|
.set(InspectionResult::getReviewRemark, request.getReviewRemark())
|
||||||
|
.set(InspectionResult::getReviewVersion, expectedVersion + 1)
|
||||||
|
.set(InspectionResult::getUpdateBy, username)
|
||||||
|
.set(InspectionResult::getUpdateTime, now);
|
||||||
|
if (!update(update))
|
||||||
|
{
|
||||||
|
throw new ServiceException("巡检结果已被其他人员修改,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectionResult reviewed = getById(id);
|
||||||
|
if (STATUS_ABNORMAL.equals(reviewed.getResultStatus()))
|
||||||
|
{
|
||||||
|
createAlarmIfNecessary(reviewed);
|
||||||
|
}
|
||||||
|
appendResultLog(reviewed, "人工复核完成");
|
||||||
|
pushResult(reviewed);
|
||||||
|
return reviewed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InspectionReportSummaryVo buildReportSummary(String taskInstanceId)
|
||||||
|
{
|
||||||
|
List<InspectionResult> results = list(new LambdaQueryWrapper<InspectionResult>()
|
||||||
|
.eq(InspectionResult::getTaskInstanceId, taskInstanceId));
|
||||||
|
InspectionReportSummaryVo summary = new InspectionReportSummaryVo();
|
||||||
|
summary.setTaskInstanceId(taskInstanceId);
|
||||||
|
summary.setTotalCount(results.size());
|
||||||
|
|
||||||
|
Map<Integer, Long> levelCounts = new LinkedHashMap<>();
|
||||||
|
levelCounts.put(1, 0L);
|
||||||
|
levelCounts.put(2, 0L);
|
||||||
|
levelCounts.put(3, 0L);
|
||||||
|
Map<String, Long> typeCounts = new LinkedHashMap<>();
|
||||||
|
for (InspectionResult result : results)
|
||||||
|
{
|
||||||
|
switch (StringUtils.defaultString(result.getResultStatus()))
|
||||||
|
{
|
||||||
|
case STATUS_NORMAL:
|
||||||
|
summary.setNormalCount(summary.getNormalCount() + 1);
|
||||||
|
break;
|
||||||
|
case STATUS_ABNORMAL:
|
||||||
|
summary.setAbnormalCount(summary.getAbnormalCount() + 1);
|
||||||
|
break;
|
||||||
|
case STATUS_PENDING:
|
||||||
|
summary.setPendingCount(summary.getPendingCount() + 1);
|
||||||
|
break;
|
||||||
|
case STATUS_RECOGNIZE_FAILED:
|
||||||
|
summary.setRecognizeFailedCount(summary.getRecognizeFailedCount() + 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (result.getAlarmLevel() != null)
|
||||||
|
{
|
||||||
|
levelCounts.put(result.getAlarmLevel(), levelCounts.getOrDefault(result.getAlarmLevel(), 0L) + 1);
|
||||||
|
}
|
||||||
|
String type = StringUtils.defaultIfBlank(result.getResultType(), "UNKNOWN");
|
||||||
|
typeCounts.put(type, typeCounts.getOrDefault(type, 0L) + 1);
|
||||||
|
}
|
||||||
|
summary.setAlarmLevelCounts(levelCounts);
|
||||||
|
summary.setResultTypeCounts(typeCounts);
|
||||||
|
summary.setReviewCompleted(summary.getPendingCount() == 0 && summary.getRecognizeFailedCount() == 0);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消费工作流节点的标准巡检输出。普通节点没有_inspectionResult时直接忽略。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public InspectionResult recordWorkflowResult(FlowExecutionEvent event, InspectionTaskInstance taskInstance)
|
||||||
|
{
|
||||||
|
if (event == null || event.getOutputParams() == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject source = event.getOutputParams().getJSONObject(RESULT_MARKER);
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String iterationPath = JSON.toJSONString(event.getIterations() == null
|
||||||
|
? Collections.emptyList() : event.getIterations());
|
||||||
|
String dedupeKey = limit("WF|" + event.getInstId() + "|" + event.getItemId() + "|"
|
||||||
|
+ event.getNodeId() + "|" + iterationPath, 255);
|
||||||
|
InspectionResult existing = findByDedupeKey(dedupeKey);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectionResult result = baseResult(taskInstance, event.getInstId(), event.getItemId(), event.getNodeId());
|
||||||
|
result.setDedupeKey(dedupeKey);
|
||||||
|
result.setNodeName(event.getNodeName());
|
||||||
|
result.setIterationPath(iterationPath);
|
||||||
|
result.setResultType(upper(source.getString("resultType")));
|
||||||
|
result.setResultStatus(upper(source.getString("resultStatus")));
|
||||||
|
result.setResultName(StringUtils.defaultIfBlank(source.getString("resultName"), event.getNodeName()));
|
||||||
|
result.setValueNumber(source.getBigDecimal("valueNumber"));
|
||||||
|
result.setValueText(source.getString("valueText"));
|
||||||
|
result.setUnit(source.getString("unit"));
|
||||||
|
result.setAlarmLevel(source.getInteger("alarmLevel"));
|
||||||
|
result.setAlarmMessage(source.getString("alarmMessage"));
|
||||||
|
result.setConfidence(source.getBigDecimal("confidence"));
|
||||||
|
result.setEvidenceUrl(source.getString("evidenceUrl"));
|
||||||
|
result.setEvidenceType(resolveSourceEvidenceType(source));
|
||||||
|
result.setRawResult(toJsonText(source.get("rawResult")));
|
||||||
|
result.setMatchedRuleJson(toJsonText(source.get("matchedRule")));
|
||||||
|
result.setRuleSnapshotJson(toJsonText(source.get("ruleSnapshot")));
|
||||||
|
validateStandardResult(result);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
insertResult(result);
|
||||||
|
}
|
||||||
|
catch (DuplicateKeyException ex)
|
||||||
|
{
|
||||||
|
return findByDedupeKey(dedupeKey);
|
||||||
|
}
|
||||||
|
saveMedia(result.getId(), source);
|
||||||
|
postCreate(result, "巡检节点产生结果");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将一条PPE原始报警按命中的工作流订阅转换为巡检结果。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public InspectionResult recordPpeResult(InspectionDetectionAlert alert,
|
||||||
|
InspectionAlertListenService.ListenSubscription subscription,
|
||||||
|
InspectionTaskInstance taskInstance)
|
||||||
|
{
|
||||||
|
String dedupeKey = limit("PPE|" + alert.getEventId() + "|" + subscription.getSubscriptionId(), 255);
|
||||||
|
InspectionResult existing = findByDedupeKey(dedupeKey);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectionResult result = baseResult(taskInstance, subscription.getFlowInstanceId(),
|
||||||
|
subscription.getItemId(), subscription.getNodeId());
|
||||||
|
result.setDedupeKey(dedupeKey);
|
||||||
|
result.setResultType("PPE");
|
||||||
|
result.setResultStatus(STATUS_ABNORMAL);
|
||||||
|
result.setResultName(ppeName(subscription.getEventType()));
|
||||||
|
result.setValueText(subscription.getEventType());
|
||||||
|
result.setAlarmLevel("No-Helmet".equals(subscription.getEventType()) ? 3 : 2);
|
||||||
|
result.setAlarmMessage(ppeMessage(subscription.getEventType()));
|
||||||
|
result.setConfidence(alert.getMaxConfidence() == null
|
||||||
|
? null : BigDecimal.valueOf(alert.getMaxConfidence()));
|
||||||
|
result.setSourceEventId(alert.getEventId());
|
||||||
|
result.setEvidenceUrl(alert.getImagePath());
|
||||||
|
result.setEvidenceType(StringUtils.isBlank(alert.getImagePath()) ? null : "IMAGE");
|
||||||
|
result.setRawResult(alert.getRawJson());
|
||||||
|
result.setOccurredTime(fromUnixNs(alert.getTriggeredAtNs()));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
insertResult(result);
|
||||||
|
}
|
||||||
|
catch (DuplicateKeyException ex)
|
||||||
|
{
|
||||||
|
return findByDedupeKey(dedupeKey);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(result.getEvidenceUrl()))
|
||||||
|
{
|
||||||
|
insertMedia(result.getId(), "IMAGE", result.getEvidenceUrl(), 0);
|
||||||
|
}
|
||||||
|
postCreate(result, "接收到PPE违规事件");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InspectionResult baseResult(InspectionTaskInstance taskInstance, String flowInstanceId,
|
||||||
|
String itemId, String nodeId)
|
||||||
|
{
|
||||||
|
InspectionResult result = new InspectionResult();
|
||||||
|
result.setId(newId());
|
||||||
|
result.setResultCode("IR" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())
|
||||||
|
+ result.getId().substring(0, 5).toUpperCase(Locale.ROOT));
|
||||||
|
result.setFlowInstanceId(flowInstanceId);
|
||||||
|
result.setItemId(itemId);
|
||||||
|
result.setNodeId(nodeId);
|
||||||
|
if (taskInstance != null)
|
||||||
|
{
|
||||||
|
result.setTaskInstanceId(taskInstance.getId());
|
||||||
|
result.setTaskId(taskInstance.getTaskId());
|
||||||
|
}
|
||||||
|
result.setReviewVersion(0);
|
||||||
|
result.setOccurredTime(DateUtils.getNowDate());
|
||||||
|
result.setCreateBy("system");
|
||||||
|
result.setCreateTime(DateUtils.getNowDate());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertResult(InspectionResult result)
|
||||||
|
{
|
||||||
|
if (baseMapper.insert(result) != 1)
|
||||||
|
{
|
||||||
|
throw new ServiceException("保存巡检结果失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postCreate(InspectionResult result, String logPrefix)
|
||||||
|
{
|
||||||
|
if (STATUS_ABNORMAL.equals(result.getResultStatus()))
|
||||||
|
{
|
||||||
|
createAlarmIfNecessary(result);
|
||||||
|
}
|
||||||
|
appendResultLog(result, logPrefix);
|
||||||
|
pushResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为异常结果幂等创建巡检告警。数据库result_id唯一索引是并发场景的最终保障。
|
||||||
|
*/
|
||||||
|
private void createAlarmIfNecessary(InspectionResult result)
|
||||||
|
{
|
||||||
|
InspectionAlarm existing = alarmMapper.selectOne(new LambdaQueryWrapper<InspectionAlarm>()
|
||||||
|
.eq(InspectionAlarm::getResultId, result.getId()).last("limit 1"));
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
if (!StringUtils.equals(existing.getId(), result.getAlarmId()))
|
||||||
|
{
|
||||||
|
result.setAlarmId(existing.getId());
|
||||||
|
baseMapper.updateById(result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectionTask task = StringUtils.isBlank(result.getTaskId())
|
||||||
|
? null : taskMapper.selectById(result.getTaskId());
|
||||||
|
InspectionTaskInstance taskInstance = null;
|
||||||
|
if (StringUtils.isNotBlank(result.getTaskInstanceId()))
|
||||||
|
{
|
||||||
|
taskInstance = new InspectionTaskInstance();
|
||||||
|
taskInstance.setId(result.getTaskInstanceId());
|
||||||
|
}
|
||||||
|
InspectionRobot robot = null;
|
||||||
|
if (taskInstance != null)
|
||||||
|
{
|
||||||
|
// 仅在需要构建告警时查询完整任务实例。
|
||||||
|
taskInstance = taskInstanceMapper.selectById(result.getTaskInstanceId());
|
||||||
|
if (taskInstance != null && StringUtils.isNotBlank(taskInstance.getRobotId()))
|
||||||
|
{
|
||||||
|
robot = robotMapper.selectById(taskInstance.getRobotId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectionAlarm alarm = new InspectionAlarm();
|
||||||
|
alarm.setId(newId());
|
||||||
|
alarm.setAlarmCode("IA" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())
|
||||||
|
+ alarm.getId().substring(0, 4).toUpperCase(Locale.ROOT));
|
||||||
|
alarm.setResultId(result.getId());
|
||||||
|
alarm.setAlarmSource(result.getResultType());
|
||||||
|
alarm.setItemId(result.getItemId());
|
||||||
|
alarm.setNodeId(result.getNodeId());
|
||||||
|
alarm.setTaskInstanceId(result.getTaskInstanceId());
|
||||||
|
alarm.setTaskId(result.getTaskId());
|
||||||
|
alarm.setTaskName(task == null ? null : task.getTaskName());
|
||||||
|
alarm.setRobotId(taskInstance == null ? null : taskInstance.getRobotId());
|
||||||
|
alarm.setRobotName(robot == null ? null : robot.getRobotName());
|
||||||
|
alarm.setAlarmLevel(String.valueOf(result.getAlarmLevel() == null ? 2 : result.getAlarmLevel()));
|
||||||
|
alarm.setAlarmType("METER".equals(result.getResultType()) ? "1" : "2");
|
||||||
|
alarm.setAlarmTitle(result.getResultName() + "异常");
|
||||||
|
alarm.setAlarmContent(buildAlarmContent(result));
|
||||||
|
alarm.setAlarmTime(result.getOccurredTime());
|
||||||
|
alarm.setHandleStatus("0");
|
||||||
|
alarm.setEvidenceImage(result.getEvidenceUrl());
|
||||||
|
alarm.setCreateBy("system");
|
||||||
|
alarm.setCreateTime(DateUtils.getNowDate());
|
||||||
|
alarmMapper.insert(alarm);
|
||||||
|
|
||||||
|
result.setAlarmId(alarm.getId());
|
||||||
|
baseMapper.updateById(result);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
messagePushService.pushToChannel("InspectionAlarm", alarm);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.info("巡检告警无订阅者,alarmId={}", alarm.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAlarmContent(InspectionResult result)
|
||||||
|
{
|
||||||
|
if ("METER".equals(result.getResultType()))
|
||||||
|
{
|
||||||
|
return result.getResultName() + "读数为" + StringUtils.defaultString(result.getValueText())
|
||||||
|
+ StringUtils.defaultString(result.getUnit()) + ",触发" + result.getAlarmLevel()
|
||||||
|
+ "级告警:" + StringUtils.defaultString(result.getAlarmMessage());
|
||||||
|
}
|
||||||
|
return StringUtils.defaultIfBlank(result.getAlarmMessage(), result.getResultName() + "检测异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将业务结果追加到现有巡检任务时间线,并通知任务详情订阅者。 */
|
||||||
|
private void appendResultLog(InspectionResult result, String prefix)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(result.getTaskInstanceId()))
|
||||||
|
{
|
||||||
|
log.warn("巡检结果未绑定任务实例,跳过任务日志,resultId={},flowInstanceId={}",
|
||||||
|
result.getId(), result.getFlowInstanceId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
InspectionTaskLog taskLog = InspectionTaskLog.builder()
|
||||||
|
.id(newId())
|
||||||
|
.taskInstanceId(result.getTaskInstanceId())
|
||||||
|
.taskId(result.getTaskId())
|
||||||
|
.itemId(result.getItemId())
|
||||||
|
.nodeId(result.getNodeId())
|
||||||
|
.nodeName(result.getNodeName())
|
||||||
|
.logType(resolveLogType(result.getEvidenceUrl()))
|
||||||
|
.logContent(prefix + ":" + resultDescription(result))
|
||||||
|
.mediaUrl(result.getEvidenceUrl())
|
||||||
|
.status(TaskStatusEnum.SUCCESS.getCode())
|
||||||
|
.logTime(DateUtils.getNowDate())
|
||||||
|
.extraInfo(JSON.toJSONString(Collections.singletonMap("resultId", result.getId())))
|
||||||
|
.build();
|
||||||
|
taskLogService.insertInspectionTaskLog(taskLog);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
messagePushService.pushToChannel("InspectionTaskInstance", taskLog);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.info("巡检结果日志无订阅者,resultId={}", result.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resultDescription(InspectionResult result)
|
||||||
|
{
|
||||||
|
if ("METER".equals(result.getResultType()))
|
||||||
|
{
|
||||||
|
return result.getResultName() + "=" + StringUtils.defaultString(result.getValueText())
|
||||||
|
+ StringUtils.defaultString(result.getUnit()) + ",结果=" + result.getResultStatus()
|
||||||
|
+ (result.getAlarmLevel() == null ? "" : ",告警级别=" + result.getAlarmLevel());
|
||||||
|
}
|
||||||
|
return result.getResultName() + ",结果=" + result.getResultStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushResult(InspectionResult result)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
messagePushService.pushToChannel("InspectionResult", result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.info("巡检结果无订阅者,resultId={}", result.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存人工巡检的多媒体证据;只有单一证据时也统一落媒体表。 */
|
||||||
|
private void saveMedia(String resultId, JSONObject source)
|
||||||
|
{
|
||||||
|
JSONArray mediaList = source.getJSONArray("mediaList");
|
||||||
|
int order = 0;
|
||||||
|
if (mediaList != null)
|
||||||
|
{
|
||||||
|
for (Object item : mediaList)
|
||||||
|
{
|
||||||
|
if (item instanceof JSONObject)
|
||||||
|
{
|
||||||
|
JSONObject media = (JSONObject) item;
|
||||||
|
insertMedia(resultId, upper(media.getString("mediaType")), media.getString("mediaUrl"), order++);
|
||||||
|
}
|
||||||
|
else if (item != null)
|
||||||
|
{
|
||||||
|
String url = String.valueOf(item);
|
||||||
|
insertMedia(resultId, detectMediaType(url), url, order++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (order == 0 && StringUtils.isNotBlank(source.getString("evidenceUrl")))
|
||||||
|
{
|
||||||
|
insertMedia(resultId, detectMediaType(source.getString("evidenceUrl")),
|
||||||
|
source.getString("evidenceUrl"), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertMedia(String resultId, String mediaType, String mediaUrl, int order)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(mediaUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
InspectionResultMedia media = new InspectionResultMedia();
|
||||||
|
media.setId(newId());
|
||||||
|
media.setResultId(resultId);
|
||||||
|
media.setMediaType(normalizeEvidenceType(
|
||||||
|
StringUtils.defaultIfBlank(mediaType, detectMediaType(mediaUrl))));
|
||||||
|
media.setMediaUrl(mediaUrl);
|
||||||
|
media.setSortOrder(order);
|
||||||
|
media.setCreateTime(DateUtils.getNowDate());
|
||||||
|
mediaMapper.insert(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InspectionResult findByDedupeKey(String dedupeKey)
|
||||||
|
{
|
||||||
|
return baseMapper.selectOne(new LambdaQueryWrapper<InspectionResult>()
|
||||||
|
.eq(InspectionResult::getDedupeKey, dedupeKey).last("limit 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateStandardResult(InspectionResult result)
|
||||||
|
{
|
||||||
|
if (!("PPE".equals(result.getResultType()) || "METER".equals(result.getResultType())
|
||||||
|
|| "MANUAL".equals(result.getResultType())))
|
||||||
|
{
|
||||||
|
throw new ServiceException("工作流巡检结果类型不支持: " + result.getResultType());
|
||||||
|
}
|
||||||
|
if (!(STATUS_PENDING.equals(result.getResultStatus()) || STATUS_NORMAL.equals(result.getResultStatus())
|
||||||
|
|| STATUS_ABNORMAL.equals(result.getResultStatus())
|
||||||
|
|| STATUS_RECOGNIZE_FAILED.equals(result.getResultStatus())))
|
||||||
|
{
|
||||||
|
throw new ServiceException("工作流巡检结果状态不支持: " + result.getResultStatus());
|
||||||
|
}
|
||||||
|
if (STATUS_ABNORMAL.equals(result.getResultStatus())
|
||||||
|
&& (result.getAlarmLevel() == null || result.getAlarmLevel() < 1 || result.getAlarmLevel() > 3))
|
||||||
|
{
|
||||||
|
throw new ServiceException("异常巡检结果必须指定1到3级告警级别");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveLogType(String url)
|
||||||
|
{
|
||||||
|
String type = detectMediaType(url);
|
||||||
|
if ("VIDEO".equals(type))
|
||||||
|
{
|
||||||
|
return InspectionLogTypeEnum.VIDEO.getCode();
|
||||||
|
}
|
||||||
|
if ("IMAGE".equals(type))
|
||||||
|
{
|
||||||
|
return InspectionLogTypeEnum.IMAGE.getCode();
|
||||||
|
}
|
||||||
|
return InspectionLogTypeEnum.TEXT.getCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detectMediaType(String url)
|
||||||
|
{
|
||||||
|
String normalized = StringUtils.lowerCase(StringUtils.defaultString(url));
|
||||||
|
if (normalized.matches(".*\\.(mp4|avi|mov|mkv|webm)(\\?.*)?$"))
|
||||||
|
{
|
||||||
|
return "VIDEO";
|
||||||
|
}
|
||||||
|
return "IMAGE";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在结果首次入库时确定主要证据类型,列表查询无需再访问媒体明细表。
|
||||||
|
*/
|
||||||
|
private String resolveSourceEvidenceType(JSONObject source)
|
||||||
|
{
|
||||||
|
String explicitType = normalizeEvidenceType(source.getString("evidenceType"));
|
||||||
|
if (StringUtils.isNotBlank(explicitType))
|
||||||
|
{
|
||||||
|
return explicitType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 人工巡检以mediaList第一条作为主要证据,和evidenceUrl的赋值规则保持一致。
|
||||||
|
JSONArray mediaList = source.getJSONArray("mediaList");
|
||||||
|
if (mediaList != null && !mediaList.isEmpty() && mediaList.get(0) instanceof JSONObject)
|
||||||
|
{
|
||||||
|
String firstType = normalizeEvidenceType(mediaList.getJSONObject(0).getString("mediaType"));
|
||||||
|
if (StringUtils.isNotBlank(firstType))
|
||||||
|
{
|
||||||
|
return firstType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String evidenceUrl = source.getString("evidenceUrl");
|
||||||
|
return StringUtils.isBlank(evidenceUrl) ? null : detectMediaType(evidenceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 媒体类型只允许IMAGE或VIDEO,避免主表和媒体明细出现不一致的自由文本。 */
|
||||||
|
private String normalizeEvidenceType(String value)
|
||||||
|
{
|
||||||
|
String normalized = upper(value);
|
||||||
|
if (StringUtils.isBlank(normalized))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!"IMAGE".equals(normalized) && !"VIDEO".equals(normalized))
|
||||||
|
{
|
||||||
|
throw new ServiceException("不支持的巡检证据类型: " + value);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ppeName(String eventType)
|
||||||
|
{
|
||||||
|
return "No-Helmet".equals(eventType) ? "未佩戴安全帽" : "未佩戴手套";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ppeMessage(String eventType)
|
||||||
|
{
|
||||||
|
return "No-Helmet".equals(eventType) ? "检测到人员未佩戴安全帽" : "检测到人员未佩戴手套";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date fromUnixNs(Long timestampNs)
|
||||||
|
{
|
||||||
|
return timestampNs == null ? DateUtils.getNowDate() : new Date(timestampNs / 1_000_000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toJsonText(Object value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value instanceof String ? String.valueOf(value) : JSON.toJSONString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String upper(String value)
|
||||||
|
{
|
||||||
|
return StringUtils.upperCase(StringUtils.trimToEmpty(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int valueOrZero(Integer value)
|
||||||
|
{
|
||||||
|
return value == null ? 0 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String limit(String value, int maxLength)
|
||||||
|
{
|
||||||
|
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String newId()
|
||||||
|
{
|
||||||
|
return UUID.randomUUID().toString().replace("-", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="remark" column="remark" />
|
<result property="remark" column="remark" />
|
||||||
<result property="alarmCode" column="alarm_code" />
|
<result property="alarmCode" column="alarm_code" />
|
||||||
<result property="taskInstanceId" column="task_instance_id" />
|
<result property="taskInstanceId" column="task_instance_id" />
|
||||||
|
<result property="resultId" column="result_id" />
|
||||||
|
<result property="alarmSource" column="alarm_source" />
|
||||||
|
<result property="itemId" column="item_id" />
|
||||||
|
<result property="nodeId" column="node_id" />
|
||||||
<result property="taskId" column="task_id" />
|
<result property="taskId" column="task_id" />
|
||||||
<result property="taskName" column="task_name" />
|
<result property="taskName" column="task_name" />
|
||||||
<result property="robotId" column="robot_id" />
|
<result property="robotId" column="robot_id" />
|
||||||
@ -39,6 +43,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="remark" column="remark" />
|
<result property="remark" column="remark" />
|
||||||
<result property="alarmCode" column="alarm_code" />
|
<result property="alarmCode" column="alarm_code" />
|
||||||
<result property="taskInstanceId" column="task_instance_id" />
|
<result property="taskInstanceId" column="task_instance_id" />
|
||||||
|
<result property="resultId" column="result_id" />
|
||||||
|
<result property="alarmSource" column="alarm_source" />
|
||||||
|
<result property="itemId" column="item_id" />
|
||||||
|
<result property="nodeId" column="node_id" />
|
||||||
<result property="taskId" column="task_id" />
|
<result property="taskId" column="task_id" />
|
||||||
<result property="taskName" column="task_name" />
|
<result property="taskName" column="task_name" />
|
||||||
<result property="robotId" column="robot_id" />
|
<result property="robotId" column="robot_id" />
|
||||||
@ -59,7 +67,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectInspectionAlarmVo">
|
<sql id="selectInspectionAlarmVo">
|
||||||
select id, create_by, create_time, update_by, update_time, remark, alarm_code, task_instance_id, task_id, task_name, robot_id, robot_name, alarm_level, alarm_type, alarm_title, alarm_content, alarm_location, alarm_time, handle_status, handler, handle_time, handle_remark, evidence_image from inspection_alarm
|
select id, create_by, create_time, update_by, update_time, remark, alarm_code,
|
||||||
|
task_instance_id, result_id, alarm_source, item_id, node_id,
|
||||||
|
task_id, task_name, robot_id, robot_name, alarm_level, alarm_type,
|
||||||
|
alarm_title, alarm_content, alarm_location, alarm_time, handle_status,
|
||||||
|
handler, handle_time, handle_remark, evidence_image from inspection_alarm
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectInspectionAlarmList" parameterType="InspectionAlarm" resultMap="InspectionAlarmResult">
|
<select id="selectInspectionAlarmList" parameterType="InspectionAlarm" resultMap="InspectionAlarmResult">
|
||||||
@ -90,6 +102,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">remark,</if>
|
<if test="remark != null">remark,</if>
|
||||||
<if test="alarmCode != null">alarm_code,</if>
|
<if test="alarmCode != null">alarm_code,</if>
|
||||||
<if test="taskInstanceId != null">task_instance_id,</if>
|
<if test="taskInstanceId != null">task_instance_id,</if>
|
||||||
|
<if test="resultId != null">result_id,</if>
|
||||||
|
<if test="alarmSource != null">alarm_source,</if>
|
||||||
|
<if test="itemId != null">item_id,</if>
|
||||||
|
<if test="nodeId != null">node_id,</if>
|
||||||
<if test="taskId != null">task_id,</if>
|
<if test="taskId != null">task_id,</if>
|
||||||
<if test="taskName != null">task_name,</if>
|
<if test="taskName != null">task_name,</if>
|
||||||
<if test="robotId != null">robot_id,</if>
|
<if test="robotId != null">robot_id,</if>
|
||||||
@ -115,6 +131,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">#{remark},</if>
|
<if test="remark != null">#{remark},</if>
|
||||||
<if test="alarmCode != null">#{alarmCode},</if>
|
<if test="alarmCode != null">#{alarmCode},</if>
|
||||||
<if test="taskInstanceId != null">#{taskInstanceId},</if>
|
<if test="taskInstanceId != null">#{taskInstanceId},</if>
|
||||||
|
<if test="resultId != null">#{resultId},</if>
|
||||||
|
<if test="alarmSource != null">#{alarmSource},</if>
|
||||||
|
<if test="itemId != null">#{itemId},</if>
|
||||||
|
<if test="nodeId != null">#{nodeId},</if>
|
||||||
<if test="taskId != null">#{taskId},</if>
|
<if test="taskId != null">#{taskId},</if>
|
||||||
<if test="taskName != null">#{taskName},</if>
|
<if test="taskName != null">#{taskName},</if>
|
||||||
<if test="robotId != null">#{robotId},</if>
|
<if test="robotId != null">#{robotId},</if>
|
||||||
@ -143,6 +163,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">remark = #{remark},</if>
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
<if test="alarmCode != null">alarm_code = #{alarmCode},</if>
|
<if test="alarmCode != null">alarm_code = #{alarmCode},</if>
|
||||||
<if test="taskInstanceId != null">task_instance_id = #{taskInstanceId},</if>
|
<if test="taskInstanceId != null">task_instance_id = #{taskInstanceId},</if>
|
||||||
|
<if test="resultId != null">result_id = #{resultId},</if>
|
||||||
|
<if test="alarmSource != null">alarm_source = #{alarmSource},</if>
|
||||||
|
<if test="itemId != null">item_id = #{itemId},</if>
|
||||||
|
<if test="nodeId != null">node_id = #{nodeId},</if>
|
||||||
<if test="taskId != null">task_id = #{taskId},</if>
|
<if test="taskId != null">task_id = #{taskId},</if>
|
||||||
<if test="taskName != null">task_name = #{taskName},</if>
|
<if test="taskName != null">task_name = #{taskName},</if>
|
||||||
<if test="robotId != null">robot_id = #{robotId},</if>
|
<if test="robotId != null">robot_id = #{robotId},</if>
|
||||||
@ -175,7 +199,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<select id="selectInspectionAlarmVoList" parameterType="InspectionAlarm" resultMap="InspectionAlarmVoResult">
|
<select id="selectInspectionAlarmVoList" parameterType="InspectionAlarm" resultMap="InspectionAlarmVoResult">
|
||||||
select a.id, a.create_by, a.create_time, a.update_by, a.update_time, a.remark,
|
select a.id, a.create_by, a.create_time, a.update_by, a.update_time, a.remark,
|
||||||
a.alarm_code, a.task_instance_id, a.task_id, t.task_name, a.robot_id, r.robot_name,
|
a.alarm_code, a.task_instance_id, a.result_id, a.alarm_source, a.item_id, a.node_id,
|
||||||
|
a.task_id, t.task_name, a.robot_id, r.robot_name,
|
||||||
a.alarm_level, a.alarm_type, a.alarm_title, a.alarm_content, a.alarm_location,
|
a.alarm_level, a.alarm_type, a.alarm_title, a.alarm_content, a.alarm_location,
|
||||||
a.alarm_time, a.handle_status, a.handler, a.handle_time, a.handle_remark, a.evidence_image,
|
a.alarm_time, a.handle_status, a.handler, a.handle_time, a.handle_remark, a.evidence_image,
|
||||||
u1.nick_name as create_by_name, u2.nick_name as update_by_name
|
u1.nick_name as create_by_name, u2.nick_name as update_by_name
|
||||||
|
|||||||
@ -74,6 +74,7 @@ public enum ActionEnum {
|
|||||||
INTENT_RECOGNITION("LLM", "INTENT_RECOGNITION", "意图识别"),
|
INTENT_RECOGNITION("LLM", "INTENT_RECOGNITION", "意图识别"),
|
||||||
GENERATE_ADVANCED_AUDIO("LLM", "GENERATE_ADVANCED_AUDIO", "tts语音合成"),
|
GENERATE_ADVANCED_AUDIO("LLM", "GENERATE_ADVANCED_AUDIO", "tts语音合成"),
|
||||||
AI_AGENT_PLATFORM("LLM", "AI_AGENT_PLATFORM", "商道智能体"),
|
AI_AGENT_PLATFORM("LLM", "AI_AGENT_PLATFORM", "商道智能体"),
|
||||||
|
INSPECTION_METER_RECOGNIZE("LLM", "INSPECTION_METER_RECOGNIZE", "巡检仪表读数识别"),
|
||||||
AI_TTS("LLM", "AI_TTS", "tts语音播放"),
|
AI_TTS("LLM", "AI_TTS", "tts语音播放"),
|
||||||
GET_CURRENT_PAGE("LLM", "GET_CURRENT_PAGE", "获取当前页面名称"),
|
GET_CURRENT_PAGE("LLM", "GET_CURRENT_PAGE", "获取当前页面名称"),
|
||||||
|
|
||||||
@ -83,7 +84,8 @@ public enum ActionEnum {
|
|||||||
|
|
||||||
// 巡检报警
|
// 巡检报警
|
||||||
INSPECTION_ALERT_LISTEN_START("EDGE", "INSPECTION_ALERT_LISTEN_START", "开始监听报警事件"),
|
INSPECTION_ALERT_LISTEN_START("EDGE", "INSPECTION_ALERT_LISTEN_START", "开始监听报警事件"),
|
||||||
INSPECTION_ALERT_LISTEN_STOP("EDGE", "INSPECTION_ALERT_LISTEN_STOP", "结束监听报警事件")
|
INSPECTION_ALERT_LISTEN_STOP("EDGE", "INSPECTION_ALERT_LISTEN_STOP", "结束监听报警事件"),
|
||||||
|
INSPECTION_MANUAL_REVIEW_CREATE("EDGE", "INSPECTION_MANUAL_REVIEW_CREATE", "创建人工巡检判断任务")
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
package com.cmvr.test.flow.runtime.event;
|
package com.cmvr.test.flow.runtime.event;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.test.enums.ActionEnum;
|
||||||
import com.cmvr.test.enums.TaskStatusEnum;
|
import com.cmvr.test.enums.TaskStatusEnum;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程执行事件。
|
* 流程执行事件。
|
||||||
* <p>
|
* <p>
|
||||||
@ -62,6 +66,26 @@ public class FlowExecutionEvent {
|
|||||||
*/
|
*/
|
||||||
private String nodeName;
|
private String nodeName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前节点动作,用于业务监听器识别标准化节点输出。
|
||||||
|
*/
|
||||||
|
private ActionEnum action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点成功执行后的输出参数。
|
||||||
|
*/
|
||||||
|
private JSONObject outputParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前工作流绑定的终端ID。
|
||||||
|
*/
|
||||||
|
private String terminalId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 循环节点迭代路径,用于区分同一节点的多次执行结果。
|
||||||
|
*/
|
||||||
|
private List<Integer> iterations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前任务状态。
|
* 当前任务状态。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -55,6 +56,11 @@ public class FlowAfterInterceptor extends AbstractFlowMsgPreInterceptor{
|
|||||||
.nodeId(message.getNodeId())
|
.nodeId(message.getNodeId())
|
||||||
.nodeCount(message.getGraph().allNodeIds().size())
|
.nodeCount(message.getGraph().allNodeIds().size())
|
||||||
.nodeType(message.getNodeType())
|
.nodeType(message.getNodeType())
|
||||||
|
.action(message.getAction())
|
||||||
|
.outputParams(result != null ? result.getOutputParams() : null)
|
||||||
|
.terminalId(message.getTerminalId())
|
||||||
|
.iterations(message.getIterations() == null
|
||||||
|
? new ArrayList<>() : new ArrayList<>(message.getIterations()))
|
||||||
.errorMessage(errorMessage)
|
.errorMessage(errorMessage)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|||||||
@ -37,15 +37,19 @@ public class EdgeInspectionAlertOperateService implements EdgeOperateService
|
|||||||
JSONObject inputParams = message.getInputParams() == null ? new JSONObject() : message.getInputParams();
|
JSONObject inputParams = message.getInputParams() == null ? new JSONObject() : message.getInputParams();
|
||||||
String terminalId = StringUtils.defaultIfBlank(inputParams.getString("terminalId"), message.getTerminalId());
|
String terminalId = StringUtils.defaultIfBlank(inputParams.getString("terminalId"), message.getTerminalId());
|
||||||
Collection<String> eventTypes = resolveEventTypes(inputParams);
|
Collection<String> eventTypes = resolveEventTypes(inputParams);
|
||||||
|
String listenerKey = inputParams.getString("listenerKey");
|
||||||
|
|
||||||
InspectionAlertListenService.ListenState state;
|
InspectionAlertListenService.ListenState state;
|
||||||
switch (message.getAction())
|
switch (message.getAction())
|
||||||
{
|
{
|
||||||
case INSPECTION_ALERT_LISTEN_START:
|
case INSPECTION_ALERT_LISTEN_START:
|
||||||
state = inspectionAlertListenService.startListen(terminalId, eventTypes);
|
state = inspectionAlertListenService.startListen(terminalId, eventTypes,
|
||||||
|
message.getInstId(), message.getTaskId(), message.getItemId(),
|
||||||
|
message.getNodeId(), listenerKey);
|
||||||
break;
|
break;
|
||||||
case INSPECTION_ALERT_LISTEN_STOP:
|
case INSPECTION_ALERT_LISTEN_STOP:
|
||||||
state = inspectionAlertListenService.stopListen(terminalId, eventTypes);
|
state = inspectionAlertListenService.stopListen(terminalId, eventTypes,
|
||||||
|
message.getInstId(), message.getItemId(), listenerKey);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new GlobalException("不支持的巡检报警监听动作: " + message.getAction());
|
throw new GlobalException("不支持的巡检报警监听动作: " + message.getAction());
|
||||||
|
|||||||
@ -0,0 +1,127 @@
|
|||||||
|
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.test.enums.ActionEnum;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||||
|
import com.cmvr.test.model.vo.inspection.InspectionManualReviewConfigVO;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建待人工复核巡检结果,工作流不等待人工处理。
|
||||||
|
*
|
||||||
|
* <p>输入参数见{@link InspectionManualReviewConfigVO}。节点只登记已由前置组件保存到MinIO的
|
||||||
|
* 图片或视频地址,不复制媒体文件。节点执行后立即返回PENDING,人工通过巡检结果接口复核。</p>
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class EdgeManualInspectionOperateService implements EdgeOperateService
|
||||||
|
{
|
||||||
|
private static final String RESULT_MARKER = "_inspectionResult";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(ActionEnum action)
|
||||||
|
{
|
||||||
|
return ActionEnum.INSPECTION_MANUAL_REVIEW_CREATE.equals(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message)
|
||||||
|
{
|
||||||
|
JSONObject input = message.getInputParams() == null ? new JSONObject() : message.getInputParams();
|
||||||
|
InspectionManualReviewConfigVO config = input.to(InspectionManualReviewConfigVO.class);
|
||||||
|
String resultName = StringUtils.defaultIfBlank(config.getResultName(), "人工巡检");
|
||||||
|
JSONArray mediaList = resolveMediaList(input);
|
||||||
|
if (mediaList.isEmpty())
|
||||||
|
{
|
||||||
|
throw new GlobalException("人工巡检至少需要一张图片或一个视频");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject inspectionResult = new JSONObject();
|
||||||
|
inspectionResult.put("resultType", "MANUAL");
|
||||||
|
inspectionResult.put("resultStatus", "PENDING");
|
||||||
|
inspectionResult.put("resultName", resultName);
|
||||||
|
inspectionResult.put("evidenceUrl", mediaList.getJSONObject(0).getString("mediaUrl"));
|
||||||
|
inspectionResult.put("evidenceType", mediaList.getJSONObject(0).getString("mediaType"));
|
||||||
|
inspectionResult.put("mediaList", mediaList);
|
||||||
|
inspectionResult.put("rawResult", new JSONObject()
|
||||||
|
.fluentPut("reviewCriteria", config.getReviewCriteria())
|
||||||
|
.fluentPut("defaultAlarmLevel", config.getDefaultAlarmLevel()));
|
||||||
|
|
||||||
|
JSONObject output = new JSONObject();
|
||||||
|
output.put("resultStatus", "PENDING");
|
||||||
|
output.put("mediaList", mediaList);
|
||||||
|
output.put(RESULT_MARKER, inspectionResult);
|
||||||
|
return TaskNodeExecuteResult.success(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并mediaList、imageUrl和videoUrl,并统一为标准媒体对象数组。
|
||||||
|
*/
|
||||||
|
private JSONArray resolveMediaList(JSONObject input)
|
||||||
|
{
|
||||||
|
JSONArray result = new JSONArray();
|
||||||
|
Object raw = input.get("mediaList");
|
||||||
|
if (raw instanceof JSONArray)
|
||||||
|
{
|
||||||
|
for (Object item : (JSONArray) raw)
|
||||||
|
{
|
||||||
|
addMedia(result, item, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (raw instanceof Collection)
|
||||||
|
{
|
||||||
|
for (Object item : (Collection<?>) raw)
|
||||||
|
{
|
||||||
|
addMedia(result, item, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
addMedia(result, raw, null);
|
||||||
|
}
|
||||||
|
addMedia(result, input.get("imageUrl"), "IMAGE");
|
||||||
|
addMedia(result, input.get("videoUrl"), "VIDEO");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 支持标准媒体对象,也兼容历史工作流直接传入URL字符串。 */
|
||||||
|
private void addMedia(JSONArray target, Object value, String defaultType)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value instanceof JSONObject)
|
||||||
|
{
|
||||||
|
JSONObject source = (JSONObject) value;
|
||||||
|
String url = StringUtils.trimToNull(source.getString("mediaUrl"));
|
||||||
|
if (url != null)
|
||||||
|
{
|
||||||
|
target.add(new JSONObject()
|
||||||
|
.fluentPut("mediaUrl", url)
|
||||||
|
.fluentPut("mediaType", StringUtils.defaultIfBlank(
|
||||||
|
source.getString("mediaType"), detectType(url))));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String url = StringUtils.trimToNull(String.valueOf(value));
|
||||||
|
if (url != null)
|
||||||
|
{
|
||||||
|
target.add(new JSONObject()
|
||||||
|
.fluentPut("mediaUrl", url)
|
||||||
|
.fluentPut("mediaType", StringUtils.defaultIfBlank(defaultType, detectType(url))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 未显式指定类型时,根据常见视频后缀推断,其余按图片处理。 */
|
||||||
|
private String detectType(String url)
|
||||||
|
{
|
||||||
|
String normalized = StringUtils.lowerCase(url);
|
||||||
|
return normalized.matches(".*\\.(mp4|avi|mov|mkv|webm)(\\?.*)?$") ? "VIDEO" : "IMAGE";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +1,23 @@
|
|||||||
package com.cmvr.test.flow.runtime.operator.edge.ti;
|
package com.cmvr.test.flow.runtime.operator.edge.ti;
|
||||||
|
|
||||||
import cn.hutool.core.io.FileUtil;
|
|
||||||
import cn.hutool.core.text.StrPool;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.cmvr.common.config.properties.MinioProperties;
|
|
||||||
import com.cmvr.common.core.minio.MinioService;
|
|
||||||
import com.cmvr.common.exception.GlobalException;
|
import com.cmvr.common.exception.GlobalException;
|
||||||
import com.cmvr.common.utils.http.CallAPIUtil;
|
import com.cmvr.common.utils.http.CallAPIUtil;
|
||||||
import com.cmvr.llm.config.APIProperties;
|
import com.cmvr.llm.config.APIProperties;
|
||||||
import com.cmvr.llm.config.AgentConfig;
|
import com.cmvr.llm.config.AgentConfig;
|
||||||
import com.cmvr.llm.util.LargeModelFileUploadUtil;
|
|
||||||
import com.cmvr.test.enums.ActionEnum;
|
import com.cmvr.test.enums.ActionEnum;
|
||||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||||
import com.cmvr.test.flow.runtime.operator.edge.EdgeOperateService;
|
import com.cmvr.test.flow.runtime.operator.edge.EdgeOperateService;
|
||||||
import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
|
import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
|
||||||
|
import com.cmvr.test.service.ShangdaoFileService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -34,9 +29,8 @@ import java.util.Map;
|
|||||||
public class TiTouchOperateService implements EdgeOperateService {
|
public class TiTouchOperateService implements EdgeOperateService {
|
||||||
|
|
||||||
private final ExTiVehicleFunctionService exTiVehicleFunctionService;
|
private final ExTiVehicleFunctionService exTiVehicleFunctionService;
|
||||||
private final MinioService minioService;
|
|
||||||
private final MinioProperties minioProps;
|
|
||||||
private final APIProperties apiProperties;
|
private final APIProperties apiProperties;
|
||||||
|
private final ShangdaoFileService shangdaoFileService;
|
||||||
|
|
||||||
// 设置运动模式
|
// 设置运动模式
|
||||||
public static final List<String> DRIVE_MODE_PATH = Arrays.asList("主页", "设置", "驾驶模式", "运动模式");
|
public static final List<String> DRIVE_MODE_PATH = Arrays.asList("主页", "设置", "驾驶模式", "运动模式");
|
||||||
@ -81,11 +75,11 @@ public class TiTouchOperateService implements EdgeOperateService {
|
|||||||
case TI_TOUCH_COORDINATES: {
|
case TI_TOUCH_COORDINATES: {
|
||||||
// 页面图片(相机)
|
// 页面图片(相机)
|
||||||
String imageUrl = inputParams.getString("imageUrl");
|
String imageUrl = inputParams.getString("imageUrl");
|
||||||
String pageImageUrl = uploadImageFromMinio(imageUrl);
|
String pageImageUrl = shangdaoFileService.uploadFromMinio(imageUrl);
|
||||||
|
|
||||||
// icon图片
|
// icon图片
|
||||||
String iconUrl = inputParams.getString("iconUrl");
|
String iconUrl = inputParams.getString("iconUrl");
|
||||||
String iconImageUrl = uploadImageFromMinio(iconUrl);
|
String iconImageUrl = shangdaoFileService.uploadFromMinio(iconUrl);
|
||||||
|
|
||||||
// 获取触控坐标
|
// 获取触控坐标
|
||||||
String touchCoordinates = getTouchCoordinates(pageImageUrl, iconImageUrl);
|
String touchCoordinates = getTouchCoordinates(pageImageUrl, iconImageUrl);
|
||||||
@ -109,10 +103,10 @@ public class TiTouchOperateService implements EdgeOperateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String test(String imageUrl,String iconUrl) {
|
public String test(String imageUrl,String iconUrl) {
|
||||||
String pageImageUrl = uploadImageFromMinio(imageUrl);
|
String pageImageUrl = shangdaoFileService.uploadFromMinio(imageUrl);
|
||||||
|
|
||||||
// icon图片
|
// icon图片
|
||||||
String iconImageUrl = uploadImageFromMinio(iconUrl);
|
String iconImageUrl = shangdaoFileService.uploadFromMinio(iconUrl);
|
||||||
|
|
||||||
// 获取触控坐标
|
// 获取触控坐标
|
||||||
return getTouchCoordinates(pageImageUrl, iconImageUrl);
|
return getTouchCoordinates(pageImageUrl, iconImageUrl);
|
||||||
@ -142,29 +136,6 @@ public class TiTouchOperateService implements EdgeOperateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 MinIO 文件访问地址上传到商道,返回商道文件地址
|
|
||||||
*/
|
|
||||||
private String uploadImageFromMinio(String url) {
|
|
||||||
try {
|
|
||||||
String bucketName = minioProps.getBucketName();
|
|
||||||
String prefix = StrPool.SLASH + bucketName + StrPool.SLASH;
|
|
||||||
|
|
||||||
String objectName = StrUtil.removePrefix(
|
|
||||||
url,
|
|
||||||
StrUtil.subBefore(url, prefix, true) + prefix
|
|
||||||
);
|
|
||||||
|
|
||||||
File imageFile = minioService.getFile(bucketName, objectName);
|
|
||||||
byte[] imageBytes = FileUtil.readBytes(imageFile);
|
|
||||||
|
|
||||||
return LargeModelFileUploadUtil.uploadFile(imageBytes);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("上传 MinIO 文件到商道失败,url={}", url, e);
|
|
||||||
throw new GlobalException("图片上传失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String queryResultByRunId(String processId, AgentConfig agentConfig) {
|
private String queryResultByRunId(String processId, AgentConfig agentConfig) {
|
||||||
Map<String, String> headers = new HashMap<>();
|
Map<String, String> headers = new HashMap<>();
|
||||||
headers.put("Apikey", agentConfig.getAppKey());
|
headers.put("Apikey", agentConfig.getAppKey());
|
||||||
|
|||||||
@ -0,0 +1,310 @@
|
|||||||
|
package com.cmvr.test.flow.runtime.operator.llm;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.common.exception.GlobalException;
|
||||||
|
import com.cmvr.llm.service.LLMAiAgentPlatformService;
|
||||||
|
import com.cmvr.test.enums.ActionEnum;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||||
|
import com.cmvr.test.model.vo.inspection.InspectionAlarmRuleVO;
|
||||||
|
import com.cmvr.test.model.vo.inspection.InspectionMeterRecognizeConfigVO;
|
||||||
|
import com.cmvr.test.service.ShangdaoFileService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检仪表读数识别节点。
|
||||||
|
*
|
||||||
|
* <p>输入参数见{@link InspectionMeterRecognizeConfigVO}。节点先将MinIO图片上传商道,
|
||||||
|
* 再调用固定智能体识别读数;识别成功后执行全部告警规则,命中多条规则时取最高级别。</p>
|
||||||
|
*
|
||||||
|
* <p>节点输出包含value、unit、resultStatus和alarmLevel,同时通过内部字段
|
||||||
|
* {@code _inspectionResult}交给巡检事件监听器持久化。该内部字段不需要前端配置。</p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InspectionMeterRecognizeOperateService implements LLMOperateService
|
||||||
|
{
|
||||||
|
private static final String API_KEY = "d9gqfkd4shheenomcfcg";
|
||||||
|
private static final String RESULT_MARKER = "_inspectionResult";
|
||||||
|
private static final Pattern NUMBER_PATTERN =
|
||||||
|
Pattern.compile("[-+]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)");
|
||||||
|
|
||||||
|
private final ShangdaoFileService shangdaoFileService;
|
||||||
|
private final LLMAiAgentPlatformService llmAiAgentPlatformService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(ActionEnum action)
|
||||||
|
{
|
||||||
|
return ActionEnum.INSPECTION_METER_RECOGNIZE.equals(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message)
|
||||||
|
{
|
||||||
|
JSONObject input = message.getInputParams() == null ? new JSONObject() : message.getInputParams();
|
||||||
|
InspectionMeterRecognizeConfigVO config = input.to(InspectionMeterRecognizeConfigVO.class);
|
||||||
|
String imageUrl = StringUtils.trimToNull(config.getImageUrl());
|
||||||
|
String resultName = StringUtils.defaultIfBlank(config.getResultName(), "仪表读数");
|
||||||
|
String configuredUnit = StringUtils.trimToNull(config.getUnit());
|
||||||
|
JSONArray ruleArray = input.getJSONArray("alarmRules");
|
||||||
|
List<AlarmRule> rules = parseRules(config.getAlarmRules());
|
||||||
|
|
||||||
|
// 商道文件地址有效期较短,仅用于本次识别;长期证据仍保存原始MinIO地址。
|
||||||
|
String shangdaoUrl = shangdaoFileService.uploadFromMinio(imageUrl);
|
||||||
|
JSONObject response = llmAiAgentPlatformService.query(
|
||||||
|
ActionEnum.INSPECTION_METER_RECOGNIZE.getAction(), shangdaoUrl,
|
||||||
|
API_KEY, Boolean.FALSE, new JSONObject());
|
||||||
|
String rawResult = response == null ? null : response.getString("result");
|
||||||
|
|
||||||
|
Reading reading = parseReading(rawResult, configuredUnit);
|
||||||
|
JSONObject output = new JSONObject();
|
||||||
|
output.put("imageUrl", imageUrl);
|
||||||
|
output.put("rawResult", rawResult);
|
||||||
|
output.put("resultName", resultName);
|
||||||
|
|
||||||
|
JSONObject inspectionResult = new JSONObject();
|
||||||
|
inspectionResult.put("resultType", "METER");
|
||||||
|
inspectionResult.put("resultName", resultName);
|
||||||
|
inspectionResult.put("evidenceUrl", imageUrl);
|
||||||
|
inspectionResult.put("evidenceType", "IMAGE");
|
||||||
|
inspectionResult.put("rawResult", rawResult);
|
||||||
|
inspectionResult.put("ruleSnapshot", ruleArray == null ? new JSONArray() : ruleArray);
|
||||||
|
|
||||||
|
// 无法提取数值时保留原始响应并进入人工复核,不执行阈值规则,避免误报警。
|
||||||
|
if (reading == null)
|
||||||
|
{
|
||||||
|
output.put("resultStatus", "RECOGNIZE_FAILED");
|
||||||
|
inspectionResult.put("resultStatus", "RECOGNIZE_FAILED");
|
||||||
|
inspectionResult.put("valueText", rawResult);
|
||||||
|
inspectionResult.put("alarmMessage", "商道返回结果无法解析为仪表读数");
|
||||||
|
output.put(RESULT_MARKER, inspectionResult);
|
||||||
|
return TaskNodeExecuteResult.success(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlarmRule matchedRule = matchHighestRule(reading.value, rules);
|
||||||
|
String resultStatus = matchedRule == null ? "NORMAL" : "ABNORMAL";
|
||||||
|
String unit = StringUtils.defaultIfBlank(configuredUnit, reading.unit);
|
||||||
|
output.put("value", reading.value);
|
||||||
|
output.put("unit", unit);
|
||||||
|
output.put("resultStatus", resultStatus);
|
||||||
|
output.put("alarmLevel", matchedRule == null ? null : matchedRule.level);
|
||||||
|
|
||||||
|
inspectionResult.put("resultStatus", resultStatus);
|
||||||
|
inspectionResult.put("valueNumber", reading.value);
|
||||||
|
inspectionResult.put("valueText", reading.value.toPlainString());
|
||||||
|
inspectionResult.put("unit", unit);
|
||||||
|
if (matchedRule != null)
|
||||||
|
{
|
||||||
|
inspectionResult.put("alarmLevel", matchedRule.level);
|
||||||
|
inspectionResult.put("alarmMessage", matchedRule.message);
|
||||||
|
inspectionResult.put("matchedRule", matchedRule.source);
|
||||||
|
}
|
||||||
|
output.put(RESULT_MARKER, inspectionResult);
|
||||||
|
return TaskNodeExecuteResult.success(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验并转换前端配置的多级告警规则。
|
||||||
|
*/
|
||||||
|
private List<AlarmRule> parseRules(List<InspectionAlarmRuleVO> sourceRules)
|
||||||
|
{
|
||||||
|
List<AlarmRule> result = new ArrayList<>();
|
||||||
|
if (sourceRules == null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < sourceRules.size(); i++)
|
||||||
|
{
|
||||||
|
InspectionAlarmRuleVO source = sourceRules.get(i);
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
throw new GlobalException("alarmRules[" + i + "]格式错误");
|
||||||
|
}
|
||||||
|
Integer level = source.getLevel();
|
||||||
|
if (level == null || level < 1 || level > 3)
|
||||||
|
{
|
||||||
|
throw new GlobalException("alarmRules[" + i + "].level只能为1、2、3");
|
||||||
|
}
|
||||||
|
String operator = StringUtils.upperCase(StringUtils.trimToEmpty(source.getOperator()));
|
||||||
|
validateOperator(operator, i);
|
||||||
|
|
||||||
|
AlarmRule rule = new AlarmRule();
|
||||||
|
rule.level = level;
|
||||||
|
rule.operator = operator;
|
||||||
|
rule.threshold = source.getThreshold();
|
||||||
|
rule.minValue = source.getMinValue();
|
||||||
|
rule.maxValue = source.getMaxValue();
|
||||||
|
rule.includeMin = source.getIncludeMin() == null || source.getIncludeMin();
|
||||||
|
rule.includeMax = source.getIncludeMax() == null || source.getIncludeMax();
|
||||||
|
rule.message = StringUtils.defaultIfBlank(source.getMessage(), "仪表读数触发告警");
|
||||||
|
rule.source = JSON.parseObject(JSON.toJSONString(source));
|
||||||
|
validateThreshold(rule, i);
|
||||||
|
result.add(rule);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOperator(String operator, int index)
|
||||||
|
{
|
||||||
|
if (!("GT".equals(operator) || "GTE".equals(operator) || "LT".equals(operator)
|
||||||
|
|| "LTE".equals(operator) || "EQ".equals(operator) || "BETWEEN".equals(operator)
|
||||||
|
|| "NOT_BETWEEN".equals(operator)))
|
||||||
|
{
|
||||||
|
throw new GlobalException("alarmRules[" + index + "].operator不支持: " + operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateThreshold(AlarmRule rule, int index)
|
||||||
|
{
|
||||||
|
boolean between = "BETWEEN".equals(rule.operator) || "NOT_BETWEEN".equals(rule.operator);
|
||||||
|
if (between)
|
||||||
|
{
|
||||||
|
if (rule.minValue == null || rule.maxValue == null || rule.minValue.compareTo(rule.maxValue) > 0)
|
||||||
|
{
|
||||||
|
throw new GlobalException("alarmRules[" + index + "]区间上下界配置错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.threshold == null)
|
||||||
|
{
|
||||||
|
throw new GlobalException("alarmRules[" + index + "].threshold不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 命中多条重叠规则时按level取最高严重级别,与前端数组顺序无关。 */
|
||||||
|
private AlarmRule matchHighestRule(BigDecimal value, List<AlarmRule> rules)
|
||||||
|
{
|
||||||
|
return rules.stream()
|
||||||
|
.filter(rule -> rule.matches(value))
|
||||||
|
.max(Comparator.comparingInt(rule -> rule.level))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析商道结果:优先读取标准JSON字段,兼容纯文本数字返回。
|
||||||
|
*/
|
||||||
|
private Reading parseReading(String rawResult, String configuredUnit)
|
||||||
|
{
|
||||||
|
String text = StringUtils.trimToNull(rawResult);
|
||||||
|
if (text == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String cleaned = text.replace("```json", "").replace("```", "").trim();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Object parsed = JSON.parse(cleaned);
|
||||||
|
if (parsed instanceof JSONObject)
|
||||||
|
{
|
||||||
|
JSONObject object = (JSONObject) parsed;
|
||||||
|
BigDecimal value = firstDecimal(object, "value", "reading", "meterValue", "result");
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
return new Reading(value,
|
||||||
|
StringUtils.defaultIfBlank(object.getString("unit"), configuredUnit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
log.debug("商道仪表结果不是标准JSON,尝试提取数字,rawResult={}", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher matcher = NUMBER_PATTERN.matcher(cleaned);
|
||||||
|
if (!matcher.find())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new Reading(new BigDecimal(matcher.group()), configuredUnit);
|
||||||
|
}
|
||||||
|
catch (NumberFormatException ex)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal firstDecimal(JSONObject object, String... keys)
|
||||||
|
{
|
||||||
|
for (String key : keys)
|
||||||
|
{
|
||||||
|
Object value = object.get(key);
|
||||||
|
if (value instanceof Number || value instanceof String)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new BigDecimal(String.valueOf(value));
|
||||||
|
}
|
||||||
|
catch (NumberFormatException ignored)
|
||||||
|
{
|
||||||
|
// 继续尝试下一个标准字段。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Reading
|
||||||
|
{
|
||||||
|
private final BigDecimal value;
|
||||||
|
private final String unit;
|
||||||
|
|
||||||
|
private Reading(BigDecimal value, String unit)
|
||||||
|
{
|
||||||
|
this.value = value;
|
||||||
|
this.unit = unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AlarmRule
|
||||||
|
{
|
||||||
|
private int level;
|
||||||
|
private String operator;
|
||||||
|
private BigDecimal threshold;
|
||||||
|
private BigDecimal minValue;
|
||||||
|
private BigDecimal maxValue;
|
||||||
|
private boolean includeMin;
|
||||||
|
private boolean includeMax;
|
||||||
|
private String message;
|
||||||
|
private JSONObject source;
|
||||||
|
|
||||||
|
private boolean matches(BigDecimal value)
|
||||||
|
{
|
||||||
|
int thresholdCompare = threshold == null ? 0 : value.compareTo(threshold);
|
||||||
|
switch (operator.toUpperCase(Locale.ROOT))
|
||||||
|
{
|
||||||
|
case "GT": return thresholdCompare > 0;
|
||||||
|
case "GTE": return thresholdCompare >= 0;
|
||||||
|
case "LT": return thresholdCompare < 0;
|
||||||
|
case "LTE": return thresholdCompare <= 0;
|
||||||
|
case "EQ": return thresholdCompare == 0;
|
||||||
|
case "BETWEEN": return within(value);
|
||||||
|
case "NOT_BETWEEN": return !within(value);
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean within(BigDecimal value)
|
||||||
|
{
|
||||||
|
int minCompare = value.compareTo(minValue);
|
||||||
|
int maxCompare = value.compareTo(maxValue);
|
||||||
|
boolean lowerMatched = includeMin ? minCompare >= 0 : minCompare > 0;
|
||||||
|
boolean upperMatched = includeMax ? maxCompare <= 0 : maxCompare < 0;
|
||||||
|
return lowerMatched && upperMatched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,21 +1,28 @@
|
|||||||
package com.cmvr.test.model.vo;
|
package com.cmvr.test.model.vo;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流节点执行请求
|
* 工作流节点执行请求
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@ApiModel(value = "FlowActionRequestVO", description = "工作流单节点试执行请求")
|
||||||
public class FlowActionRequestVO {
|
public class FlowActionRequestVO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动作
|
* 动作
|
||||||
*/
|
*/
|
||||||
|
@ApiModelProperty(value = "动作编码。巡检新增动作:INSPECTION_METER_RECOGNIZE、INSPECTION_MANUAL_REVIEW_CREATE",
|
||||||
|
required = true, example = "INSPECTION_METER_RECOGNIZE")
|
||||||
private String action;
|
private String action;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行参数
|
* 执行参数
|
||||||
*/
|
*/
|
||||||
|
@ApiModelProperty(value = "节点参数。仪表识别参见InspectionMeterRecognizeConfigVO,人工判断参见InspectionManualReviewConfigVO",
|
||||||
|
required = true)
|
||||||
private JSONObject payload;
|
private JSONObject payload;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.cmvr.test.model.vo.inspection;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/** 仪表读数的单条告警规则配置。 */
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "InspectionAlarmRuleVO", description = "仪表读数多级告警规则")
|
||||||
|
public class InspectionAlarmRuleVO
|
||||||
|
{
|
||||||
|
@ApiModelProperty(value = "告警级别:1提示、2警告、3严重", required = true,
|
||||||
|
allowableValues = "1,2,3", example = "2")
|
||||||
|
private Integer level;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "比较运算符", required = true,
|
||||||
|
allowableValues = "GT,GTE,LT,LTE,EQ,BETWEEN,NOT_BETWEEN", example = "GTE")
|
||||||
|
private String operator;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "GT/GTE/LT/LTE/EQ使用的比较阈值", example = "1.6")
|
||||||
|
private BigDecimal threshold;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "BETWEEN/NOT_BETWEEN使用的区间下界", example = "1.6")
|
||||||
|
private BigDecimal minValue;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "BETWEEN/NOT_BETWEEN使用的区间上界", example = "2.0")
|
||||||
|
private BigDecimal maxValue;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "区间规则是否包含下界,默认true", example = "true")
|
||||||
|
private Boolean includeMin;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "区间规则是否包含上界,默认true", example = "false")
|
||||||
|
private Boolean includeMax;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "命中规则后的告警描述", example = "压力过高")
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.cmvr.test.model.vo.inspection;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 创建人工巡检复核任务的工作流节点参数。 */
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "InspectionManualReviewConfigVO",
|
||||||
|
description = "INSPECTION_MANUAL_REVIEW_CREATE节点参数;imageUrl、videoUrl、mediaList至少提供一个")
|
||||||
|
public class InspectionManualReviewConfigVO
|
||||||
|
{
|
||||||
|
@ApiModelProperty(value = "人工检查项名称,默认人工巡检", example = "设备外观检查")
|
||||||
|
private String resultName;
|
||||||
|
|
||||||
|
@ApiModelProperty("单张MinIO图片地址,可引用上游拍照节点输出")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("单个MinIO视频地址,可引用上游录像节点输出")
|
||||||
|
private String videoUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("多张图片或视频;可与imageUrl、videoUrl同时使用")
|
||||||
|
private List<InspectionMediaVO> mediaList;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "供审核人员参考的判断标准", example = "检查设备是否破损、漏油")
|
||||||
|
private String reviewCriteria;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "建议告警级别,仅作为审核参考;提交异常复核时仍需明确选择",
|
||||||
|
allowableValues = "1,2,3", example = "2")
|
||||||
|
private Integer defaultAlarmLevel;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.cmvr.test.model.vo.inspection;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** 人工巡检节点的一条媒体证据。 */
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "InspectionMediaVO", description = "人工巡检图片或视频参数")
|
||||||
|
public class InspectionMediaVO
|
||||||
|
{
|
||||||
|
@ApiModelProperty(value = "媒体类型", required = true,
|
||||||
|
allowableValues = "IMAGE,VIDEO", example = "IMAGE")
|
||||||
|
private String mediaType;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "MinIO永久媒体地址", required = true)
|
||||||
|
private String mediaUrl;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.cmvr.test.model.vo.inspection;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 仪表读数识别工作流节点参数。 */
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "InspectionMeterRecognizeConfigVO",
|
||||||
|
description = "INSPECTION_METER_RECOGNIZE节点参数")
|
||||||
|
public class InspectionMeterRecognizeConfigVO
|
||||||
|
{
|
||||||
|
@ApiModelProperty(value = "前置拍照节点输出的MinIO图片地址", required = true,
|
||||||
|
example = "http://127.0.0.1:9000/cmvr/inspection/pressure.jpg")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "仪表或检查项名称,默认仪表读数", example = "1号压力表")
|
||||||
|
private String resultName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "仪表单位", example = "MPa")
|
||||||
|
private String unit;
|
||||||
|
|
||||||
|
@ApiModelProperty("多级告警规则;为空时只识别和保存,不产生读数告警")
|
||||||
|
private List<InspectionAlarmRuleVO> alarmRules;
|
||||||
|
}
|
||||||
@ -18,6 +18,9 @@ import com.cmvr.edge.client.service.EdgeArmService;
|
|||||||
import com.cmvr.device.service.InspectionAlertListenService;
|
import com.cmvr.device.service.InspectionAlertListenService;
|
||||||
import com.cmvr.llm.service.LLMAiAgentPlatformService;
|
import com.cmvr.llm.service.LLMAiAgentPlatformService;
|
||||||
import com.cmvr.test.enums.ActionEnum;
|
import com.cmvr.test.enums.ActionEnum;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||||
|
import com.cmvr.test.flow.runtime.operator.edge.EdgeManualInspectionOperateService;
|
||||||
|
import com.cmvr.test.flow.runtime.operator.llm.InspectionMeterRecognizeOperateService;
|
||||||
import com.cmvr.test.model.vo.FlowActionRequestVO;
|
import com.cmvr.test.model.vo.FlowActionRequestVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -36,6 +39,8 @@ public class FlowActionExecutorService {
|
|||||||
private final EdgeAgvService edgeAgvService;
|
private final EdgeAgvService edgeAgvService;
|
||||||
private final InspectionAlertListenService inspectionAlertListenService;
|
private final InspectionAlertListenService inspectionAlertListenService;
|
||||||
private final EdgeArmService edgeArmService;
|
private final EdgeArmService edgeArmService;
|
||||||
|
private final EdgeManualInspectionOperateService edgeManualInspectionOperateService;
|
||||||
|
private final InspectionMeterRecognizeOperateService inspectionMeterRecognizeOperateService;
|
||||||
|
|
||||||
public String actionExecute(FlowActionRequestVO req) {
|
public String actionExecute(FlowActionRequestVO req) {
|
||||||
|
|
||||||
@ -67,6 +72,10 @@ public class FlowActionExecutorService {
|
|||||||
|| ActionEnum.INSPECTION_ALERT_LISTEN_STOP.equals(action)) {
|
|| ActionEnum.INSPECTION_ALERT_LISTEN_STOP.equals(action)) {
|
||||||
return executeInspectionAlertAction(action, payload);
|
return executeInspectionAlertAction(action, payload);
|
||||||
}
|
}
|
||||||
|
if (ActionEnum.INSPECTION_MANUAL_REVIEW_CREATE.equals(action)) {
|
||||||
|
TaskNodeExecuteMessage message = buildSingleNodeMessage(action, payload);
|
||||||
|
return edgeManualInspectionOperateService.execute(message).getOutputParams().toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
if (StrUtil.isEmpty(terminalId) || StrUtil.isEmpty(deviceId)) {
|
if (StrUtil.isEmpty(terminalId) || StrUtil.isEmpty(deviceId)) {
|
||||||
throw new GlobalException("EDGE 类型动作必须提供 terminalId 和 deviceId");
|
throw new GlobalException("EDGE 类型动作必须提供 terminalId 和 deviceId");
|
||||||
@ -248,9 +257,19 @@ public class FlowActionExecutorService {
|
|||||||
return "LLM 高级音频生成 OK";
|
return "LLM 高级音频生成 OK";
|
||||||
case AI_AGENT_PLATFORM:
|
case AI_AGENT_PLATFORM:
|
||||||
return llmAiAgentPlatformService.query(action.getAction(), req.getPayload().getJSONObject("config").getString("text"), req.getPayload().getJSONObject("config").getString("apiKey"), req.getPayload().getBoolean("invokeTts"), req.getPayload().getJSONObject("tts")).toString();
|
return llmAiAgentPlatformService.query(action.getAction(), req.getPayload().getJSONObject("config").getString("text"), req.getPayload().getJSONObject("config").getString("apiKey"), req.getPayload().getBoolean("invokeTts"), req.getPayload().getJSONObject("tts")).toString();
|
||||||
|
case INSPECTION_METER_RECOGNIZE:
|
||||||
|
return inspectionMeterRecognizeOperateService.execute(
|
||||||
|
buildSingleNodeMessage(action, req.getPayload())).getOutputParams().toJSONString();
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedOperationException("未实现的 LLM Action: " + action);
|
throw new UnsupportedOperationException("未实现的 LLM Action: " + action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TaskNodeExecuteMessage buildSingleNodeMessage(ActionEnum action, JSONObject payload) {
|
||||||
|
TaskNodeExecuteMessage message = new TaskNodeExecuteMessage();
|
||||||
|
message.setAction(action);
|
||||||
|
message.setInputParams(payload == null ? new JSONObject() : payload);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,121 @@
|
|||||||
|
package com.cmvr.test.service;
|
||||||
|
|
||||||
|
import com.cmvr.common.config.properties.MinioProperties;
|
||||||
|
import com.cmvr.common.core.minio.MinioService;
|
||||||
|
import com.cmvr.common.exception.GlobalException;
|
||||||
|
import com.cmvr.llm.util.LargeModelFileUploadUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO文件转存商道服务,统一处理对象地址解析和大小限制。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ShangdaoFileService
|
||||||
|
{
|
||||||
|
private static final int MAX_FILE_BYTES = 20 * 1024 * 1024;
|
||||||
|
|
||||||
|
private final MinioService minioService;
|
||||||
|
private final MinioProperties minioProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载指定MinIO对象并上传商道。
|
||||||
|
*
|
||||||
|
* @param fileUrl 包含bucket的MinIO访问地址,或bucket内对象路径
|
||||||
|
* @return 商道临时下载地址,仅用于随后调用大模型
|
||||||
|
*/
|
||||||
|
public String uploadFromMinio(String fileUrl)
|
||||||
|
{
|
||||||
|
String objectName = resolveObjectName(fileUrl);
|
||||||
|
File temporaryFile = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
temporaryFile = minioService.getFile(minioProperties.getBucketName(), objectName);
|
||||||
|
if (temporaryFile.length() > MAX_FILE_BYTES)
|
||||||
|
{
|
||||||
|
throw new GlobalException("巡检媒体文件不能超过20MB");
|
||||||
|
}
|
||||||
|
byte[] bytes = Files.readAllBytes(temporaryFile.toPath());
|
||||||
|
String shangdaoUrl = LargeModelFileUploadUtil.uploadFile(bytes);
|
||||||
|
if (StringUtils.isBlank(shangdaoUrl))
|
||||||
|
{
|
||||||
|
throw new GlobalException("商道文件上传未返回文件地址");
|
||||||
|
}
|
||||||
|
return shangdaoUrl;
|
||||||
|
}
|
||||||
|
catch (GlobalException ex)
|
||||||
|
{
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.error("MinIO文件上传商道失败,fileUrl={},objectName={}", fileUrl, objectName, ex);
|
||||||
|
throw new GlobalException("巡检图片上传商道失败");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (temporaryFile != null && temporaryFile.exists() && !temporaryFile.delete())
|
||||||
|
{
|
||||||
|
log.warn("删除MinIO临时文件失败,path={}", temporaryFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从完整URL或对象路径中提取当前bucket内的objectName,并拒绝目录穿越路径。
|
||||||
|
*/
|
||||||
|
private String resolveObjectName(String fileUrl)
|
||||||
|
{
|
||||||
|
String value = StringUtils.trimToNull(fileUrl);
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new GlobalException("imageUrl不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String bucketName = StringUtils.trimToEmpty(minioProperties.getBucketName());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
String path = value;
|
||||||
|
if (value.contains("://"))
|
||||||
|
{
|
||||||
|
path = new URI(value).getRawPath();
|
||||||
|
}
|
||||||
|
path = URLDecoder.decode(path, "UTF-8").replace('\\', '/');
|
||||||
|
String bucketPrefix = "/" + bucketName + "/";
|
||||||
|
int bucketIndex = path.indexOf(bucketPrefix);
|
||||||
|
if (bucketIndex >= 0)
|
||||||
|
{
|
||||||
|
path = path.substring(bucketIndex + bucketPrefix.length());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
path = StringUtils.removeStart(path, "/");
|
||||||
|
path = StringUtils.removeStart(path, bucketName + "/");
|
||||||
|
}
|
||||||
|
String objectName = StringUtils.trimToNull(path);
|
||||||
|
if (objectName == null || objectName.contains(".."))
|
||||||
|
{
|
||||||
|
throw new GlobalException("imageUrl不是有效的MinIO对象地址");
|
||||||
|
}
|
||||||
|
return objectName;
|
||||||
|
}
|
||||||
|
catch (GlobalException ex)
|
||||||
|
{
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new GlobalException("无法解析MinIO文件地址: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -124,6 +124,10 @@ CREATE TABLE `inspection_alarm` (
|
|||||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||||
`alarm_code` varchar(64) DEFAULT NULL COMMENT '告警编码',
|
`alarm_code` varchar(64) DEFAULT NULL COMMENT '告警编码',
|
||||||
`task_instance_id` varchar(64) DEFAULT NULL COMMENT '任务执行实例ID',
|
`task_instance_id` varchar(64) DEFAULT NULL COMMENT '任务执行实例ID',
|
||||||
|
`result_id` varchar(64) DEFAULT NULL COMMENT '关联巡检结果ID',
|
||||||
|
`alarm_source` varchar(20) DEFAULT NULL COMMENT '告警来源(PPE/METER/MANUAL)',
|
||||||
|
`item_id` varchar(64) DEFAULT NULL COMMENT '检测项ID',
|
||||||
|
`node_id` varchar(64) DEFAULT NULL COMMENT '工作流节点ID',
|
||||||
`task_id` varchar(64) DEFAULT NULL COMMENT '任务ID',
|
`task_id` varchar(64) DEFAULT NULL COMMENT '任务ID',
|
||||||
`task_name` varchar(100) DEFAULT NULL COMMENT '任务名称',
|
`task_name` varchar(100) DEFAULT NULL COMMENT '任务名称',
|
||||||
`robot_id` varchar(64) DEFAULT NULL COMMENT '机器人ID',
|
`robot_id` varchar(64) DEFAULT NULL COMMENT '机器人ID',
|
||||||
@ -141,6 +145,7 @@ CREATE TABLE `inspection_alarm` (
|
|||||||
`evidence_image` varchar(500) DEFAULT NULL COMMENT '图片证据',
|
`evidence_image` varchar(500) DEFAULT NULL COMMENT '图片证据',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_alarm_code` (`alarm_code`),
|
UNIQUE KEY `uk_alarm_code` (`alarm_code`),
|
||||||
|
UNIQUE KEY `uk_alarm_result_id` (`result_id`),
|
||||||
KEY `idx_task_instance_id` (`task_instance_id`),
|
KEY `idx_task_instance_id` (`task_instance_id`),
|
||||||
KEY `idx_task_id` (`task_id`),
|
KEY `idx_task_id` (`task_id`),
|
||||||
KEY `idx_robot_id` (`robot_id`),
|
KEY `idx_robot_id` (`robot_id`),
|
||||||
@ -177,3 +182,64 @@ CREATE TABLE `inspection_task_log` (
|
|||||||
KEY `idx_log_time` (`log_time`)
|
KEY `idx_log_time` (`log_time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检任务执行日志表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检任务执行日志表';
|
||||||
|
|
||||||
|
-- 6. 统一巡检结果表
|
||||||
|
DROP TABLE IF EXISTS `inspection_result_media`;
|
||||||
|
DROP TABLE IF EXISTS `inspection_result`;
|
||||||
|
CREATE TABLE `inspection_result` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||||
|
`result_code` varchar(64) NOT NULL COMMENT '结果编码',
|
||||||
|
`dedupe_key` varchar(255) NOT NULL COMMENT '结果幂等键',
|
||||||
|
`task_instance_id` varchar(64) DEFAULT NULL COMMENT '巡检任务实例数据库ID',
|
||||||
|
`flow_instance_id` varchar(64) DEFAULT NULL COMMENT '工作流实例ID',
|
||||||
|
`task_id` varchar(64) DEFAULT NULL COMMENT '巡检任务ID',
|
||||||
|
`item_id` varchar(64) DEFAULT NULL COMMENT '检测项ID',
|
||||||
|
`node_id` varchar(64) DEFAULT NULL COMMENT '工作流节点ID',
|
||||||
|
`node_name` varchar(255) DEFAULT NULL COMMENT '工作流节点名称',
|
||||||
|
`iteration_path` varchar(255) DEFAULT NULL COMMENT '循环迭代路径JSON',
|
||||||
|
`result_type` varchar(20) NOT NULL COMMENT '结果类型(PPE/METER/MANUAL)',
|
||||||
|
`result_status` varchar(32) NOT NULL COMMENT 'PENDING/NORMAL/ABNORMAL/RECOGNIZE_FAILED',
|
||||||
|
`result_name` varchar(255) NOT NULL COMMENT '检查名称',
|
||||||
|
`value_number` decimal(20,8) DEFAULT NULL COMMENT '结构化数值',
|
||||||
|
`value_text` varchar(1000) DEFAULT NULL COMMENT '文本结果',
|
||||||
|
`unit` varchar(32) DEFAULT NULL COMMENT '单位',
|
||||||
|
`alarm_level` tinyint DEFAULT NULL COMMENT '告警级别(1提示 2警告 3严重)',
|
||||||
|
`alarm_message` varchar(1000) DEFAULT NULL COMMENT '告警描述',
|
||||||
|
`confidence` decimal(10,8) DEFAULT NULL COMMENT '模型置信度',
|
||||||
|
`source_event_id` varchar(128) DEFAULT NULL COMMENT '外部PPE事件ID',
|
||||||
|
`evidence_url` varchar(1000) DEFAULT NULL COMMENT '主要MinIO证据地址',
|
||||||
|
`evidence_type` varchar(20) DEFAULT NULL COMMENT '主要证据类型(IMAGE/VIDEO)',
|
||||||
|
`raw_result` mediumtext COMMENT '模型或边缘端原始返回',
|
||||||
|
`matched_rule_json` text COMMENT '最终命中的报警规则快照',
|
||||||
|
`rule_snapshot_json` mediumtext COMMENT '本次执行使用的完整规则快照',
|
||||||
|
`reviewer` varchar(64) DEFAULT NULL COMMENT '人工复核人',
|
||||||
|
`review_time` datetime DEFAULT NULL COMMENT '人工复核时间',
|
||||||
|
`review_remark` varchar(1000) DEFAULT NULL COMMENT '人工复核说明',
|
||||||
|
`review_version` int NOT NULL DEFAULT 0 COMMENT '人工复核乐观锁版本',
|
||||||
|
`alarm_id` varchar(64) DEFAULT NULL COMMENT '关联告警ID',
|
||||||
|
`occurred_time` datetime NOT NULL COMMENT '结果发生时间',
|
||||||
|
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_inspection_result_code` (`result_code`),
|
||||||
|
UNIQUE KEY `uk_inspection_result_dedupe` (`dedupe_key`),
|
||||||
|
KEY `idx_result_task_instance` (`task_instance_id`),
|
||||||
|
KEY `idx_result_flow_instance` (`flow_instance_id`),
|
||||||
|
KEY `idx_result_status` (`result_status`),
|
||||||
|
KEY `idx_result_type` (`result_type`),
|
||||||
|
KEY `idx_result_occurred_time` (`occurred_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='统一巡检结果表';
|
||||||
|
|
||||||
|
-- 7. 巡检结果媒体证据表
|
||||||
|
CREATE TABLE `inspection_result_media` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||||
|
`result_id` varchar(32) NOT NULL COMMENT '巡检结果ID',
|
||||||
|
`media_type` varchar(20) NOT NULL COMMENT 'IMAGE或VIDEO',
|
||||||
|
`media_url` varchar(1000) NOT NULL COMMENT 'MinIO媒体地址',
|
||||||
|
`sort_order` int NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||||
|
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_result_media_result_id` (`result_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检结果媒体证据表';
|
||||||
|
|||||||
91
sql/inspection_result.sql
Normal file
91
sql/inspection_result.sql
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
-- 统一巡检结果和三级告警增量脚本。
|
||||||
|
-- 执行前请先备份数据库;本脚本不删除现有巡检数据。
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `inspection_result` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||||
|
`result_code` varchar(64) NOT NULL COMMENT '结果编码',
|
||||||
|
`dedupe_key` varchar(255) NOT NULL COMMENT '结果幂等键',
|
||||||
|
`task_instance_id` varchar(64) DEFAULT NULL COMMENT '巡检任务实例数据库ID',
|
||||||
|
`flow_instance_id` varchar(64) DEFAULT NULL COMMENT '工作流实例ID',
|
||||||
|
`task_id` varchar(64) DEFAULT NULL COMMENT '巡检任务ID',
|
||||||
|
`item_id` varchar(64) DEFAULT NULL COMMENT '检测项ID',
|
||||||
|
`node_id` varchar(64) DEFAULT NULL COMMENT '工作流节点ID',
|
||||||
|
`node_name` varchar(255) DEFAULT NULL COMMENT '工作流节点名称',
|
||||||
|
`iteration_path` varchar(255) DEFAULT NULL COMMENT '循环迭代路径JSON',
|
||||||
|
`result_type` varchar(20) NOT NULL COMMENT 'PPE/METER/MANUAL',
|
||||||
|
`result_status` varchar(32) NOT NULL COMMENT 'PENDING/NORMAL/ABNORMAL/RECOGNIZE_FAILED',
|
||||||
|
`result_name` varchar(255) NOT NULL COMMENT '检查名称',
|
||||||
|
`value_number` decimal(20,8) DEFAULT NULL COMMENT '结构化数值',
|
||||||
|
`value_text` varchar(1000) DEFAULT NULL COMMENT '文本结果',
|
||||||
|
`unit` varchar(32) DEFAULT NULL COMMENT '单位',
|
||||||
|
`alarm_level` tinyint DEFAULT NULL COMMENT '告警级别(1提示 2警告 3严重)',
|
||||||
|
`alarm_message` varchar(1000) DEFAULT NULL COMMENT '告警描述',
|
||||||
|
`confidence` decimal(10,8) DEFAULT NULL COMMENT '模型置信度',
|
||||||
|
`source_event_id` varchar(128) DEFAULT NULL COMMENT '外部PPE事件ID',
|
||||||
|
`evidence_url` varchar(1000) DEFAULT NULL COMMENT '主要MinIO证据地址',
|
||||||
|
`evidence_type` varchar(20) DEFAULT NULL COMMENT '主要证据类型(IMAGE/VIDEO)',
|
||||||
|
`raw_result` mediumtext COMMENT '模型或边缘端原始返回',
|
||||||
|
`matched_rule_json` text COMMENT '最终命中的报警规则',
|
||||||
|
`rule_snapshot_json` mediumtext COMMENT '完整报警规则快照',
|
||||||
|
`reviewer` varchar(64) DEFAULT NULL COMMENT '人工复核人',
|
||||||
|
`review_time` datetime DEFAULT NULL COMMENT '人工复核时间',
|
||||||
|
`review_remark` varchar(1000) DEFAULT NULL COMMENT '人工复核说明',
|
||||||
|
`review_version` int NOT NULL DEFAULT 0 COMMENT '人工复核版本',
|
||||||
|
`alarm_id` varchar(64) DEFAULT NULL COMMENT '关联告警ID',
|
||||||
|
`occurred_time` datetime NOT NULL COMMENT '结果发生时间',
|
||||||
|
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_inspection_result_code` (`result_code`),
|
||||||
|
UNIQUE KEY `uk_inspection_result_dedupe` (`dedupe_key`),
|
||||||
|
KEY `idx_result_task_instance` (`task_instance_id`),
|
||||||
|
KEY `idx_result_flow_instance` (`flow_instance_id`),
|
||||||
|
KEY `idx_result_status` (`result_status`),
|
||||||
|
KEY `idx_result_type` (`result_type`),
|
||||||
|
KEY `idx_result_occurred_time` (`occurred_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='统一巡检结果表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `inspection_result_media` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||||
|
`result_id` varchar(32) NOT NULL COMMENT '巡检结果ID',
|
||||||
|
`media_type` varchar(20) NOT NULL COMMENT 'IMAGE或VIDEO',
|
||||||
|
`media_url` varchar(1000) NOT NULL COMMENT 'MinIO媒体地址',
|
||||||
|
`sort_order` int NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||||
|
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_result_media_result_id` (`result_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检结果媒体证据表';
|
||||||
|
|
||||||
|
-- 已存在inspection_result表时补充主要证据类型字段。
|
||||||
|
SET @sql := IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='inspection_result' AND column_name='evidence_type')=0,
|
||||||
|
'ALTER TABLE inspection_result ADD COLUMN evidence_type varchar(20) DEFAULT NULL COMMENT ''主要证据类型(IMAGE/VIDEO)'' AFTER evidence_url', 'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 回填历史结果,使用与主表evidence_url完全一致的媒体记录,避免取错非主要证据。
|
||||||
|
UPDATE inspection_result r
|
||||||
|
JOIN inspection_result_media m ON m.result_id = r.id AND m.media_url = r.evidence_url
|
||||||
|
SET r.evidence_type = m.media_type
|
||||||
|
WHERE r.evidence_type IS NULL;
|
||||||
|
|
||||||
|
SET @sql := IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='inspection_alarm' AND column_name='result_id')=0,
|
||||||
|
'ALTER TABLE inspection_alarm ADD COLUMN result_id varchar(64) DEFAULT NULL COMMENT ''关联巡检结果ID'' AFTER task_instance_id', 'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='inspection_alarm' AND column_name='alarm_source')=0,
|
||||||
|
'ALTER TABLE inspection_alarm ADD COLUMN alarm_source varchar(20) DEFAULT NULL COMMENT ''告警来源'' AFTER result_id', 'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='inspection_alarm' AND column_name='item_id')=0,
|
||||||
|
'ALTER TABLE inspection_alarm ADD COLUMN item_id varchar(64) DEFAULT NULL COMMENT ''检测项ID'' AFTER alarm_source', 'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='inspection_alarm' AND column_name='node_id')=0,
|
||||||
|
'ALTER TABLE inspection_alarm ADD COLUMN node_id varchar(64) DEFAULT NULL COMMENT ''工作流节点ID'' AFTER item_id', 'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF((SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='inspection_alarm' AND index_name='uk_alarm_result_id')=0,
|
||||||
|
'ALTER TABLE inspection_alarm ADD UNIQUE KEY uk_alarm_result_id (result_id)', 'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
Loading…
Reference in New Issue
Block a user