Compare commits
4 Commits
152cae0323
...
521e3e4e24
| Author | SHA1 | Date | |
|---|---|---|---|
| 521e3e4e24 | |||
| dfdf575932 | |||
| 922d30632b | |||
| ecda2b0ebc |
@ -6,12 +6,10 @@ import com.cmvr.edge.client.model.microphone.EdgeMicrophoneVolumeVO;
|
||||
import com.cmvr.edge.client.service.EdgeMicrophoneService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 麦克风管理
|
||||
@ -27,14 +25,17 @@ public class EdgeMicrophoneController {
|
||||
@ApiOperation("获取麦克风状态")
|
||||
@PreAuthorize("@ss.hasPermi('system:microphone:query')")
|
||||
@GetMapping("/status")
|
||||
public AjaxResult getStatus(EdgeCommonVO edgeCommonVO) {
|
||||
public AjaxResult getStatus(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
|
||||
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
|
||||
edgeCommonVO.setTerminalId(terminalId);
|
||||
edgeCommonVO.setDeviceId(deviceId);
|
||||
return AjaxResult.success(microphoneService.getStatus(edgeCommonVO));
|
||||
}
|
||||
|
||||
@ApiOperation("开始录音")
|
||||
@PreAuthorize("@ss.hasPermi('system:microphone:record')")
|
||||
@PostMapping("/start")
|
||||
public AjaxResult startRecord(EdgeCommonVO edgeCommonVO) {
|
||||
public AjaxResult startRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
|
||||
String filePath = microphoneService.startRecord(edgeCommonVO);
|
||||
return AjaxResult.success(filePath);
|
||||
}
|
||||
@ -42,7 +43,7 @@ public class EdgeMicrophoneController {
|
||||
@ApiOperation("停止录音")
|
||||
@PreAuthorize("@ss.hasPermi('system:microphone:stop')")
|
||||
@PostMapping("/stop")
|
||||
public AjaxResult stopRecord(EdgeCommonVO edgeCommonVO) {
|
||||
public AjaxResult stopRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
|
||||
microphoneService.stopRecord(edgeCommonVO);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
@ -50,7 +51,7 @@ public class EdgeMicrophoneController {
|
||||
@ApiOperation("暂停录音")
|
||||
@PreAuthorize("@ss.hasPermi('system:microphone:pause')")
|
||||
@PostMapping("/pause")
|
||||
public AjaxResult pauseRecord(EdgeCommonVO edgeCommonVO) {
|
||||
public AjaxResult pauseRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
|
||||
microphoneService.pauseRecord(edgeCommonVO);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
@ -58,7 +59,7 @@ public class EdgeMicrophoneController {
|
||||
@ApiOperation("恢复录音")
|
||||
@PreAuthorize("@ss.hasPermi('system:microphone:resume')")
|
||||
@PostMapping("/resume")
|
||||
public AjaxResult resumeRecord(EdgeCommonVO edgeCommonVO) {
|
||||
public AjaxResult resumeRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
|
||||
microphoneService.resumeRecord(edgeCommonVO);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
@ -66,7 +67,7 @@ public class EdgeMicrophoneController {
|
||||
@ApiOperation("设置音量")
|
||||
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
|
||||
@PostMapping("/volume")
|
||||
public AjaxResult setVolume(EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
|
||||
public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
|
||||
microphoneService.setVolume(edgeMicrophoneVolumeVO);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
@ -74,7 +75,10 @@ public class EdgeMicrophoneController {
|
||||
@ApiOperation("获取音量")
|
||||
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
|
||||
@GetMapping("/volume")
|
||||
public AjaxResult getVolume(EdgeCommonVO edgeCommonVO) {
|
||||
public AjaxResult getVolume(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
|
||||
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
|
||||
edgeCommonVO.setTerminalId(terminalId);
|
||||
edgeCommonVO.setDeviceId(deviceId);
|
||||
return AjaxResult.success(microphoneService.getVolume(edgeCommonVO));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package com.cmvr.web.controller.api;
|
||||
|
||||
import cmvr.api.SpeakerCommand;
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
import com.cmvr.edge.client.model.microphone.EdgeMicrophoneVolumeVO;
|
||||
import com.cmvr.edge.client.model.speaker.EdgeSpeakerPlayAudioVO;
|
||||
import com.cmvr.edge.client.service.EdgeSpeakerService;
|
||||
import com.cmvr.common.core.domain.AjaxResult;
|
||||
import io.swagger.annotations.Api;
|
||||
@ -69,14 +72,8 @@ public class EdgeSpeakerController {
|
||||
@ApiOperation("播放音频")
|
||||
@PreAuthorize("@ss.hasPermi('system:speaker:play')")
|
||||
@PostMapping("/play")
|
||||
public AjaxResult playAudio(
|
||||
@ApiParam(value = "终端设备ID", required = true)
|
||||
@RequestParam String terminalId,
|
||||
@ApiParam(value = "设备ID", required = true)
|
||||
@RequestParam String deviceId,
|
||||
@ApiParam(value = "音频文件路径", required = true)
|
||||
@RequestParam String audioPath) {
|
||||
speakerService.playAudio(terminalId, deviceId, audioPath);
|
||||
public AjaxResult playAudio(@RequestBody EdgeSpeakerPlayAudioVO edgeSpeakerPlayAudioVO) {
|
||||
speakerService.playAudio(edgeSpeakerPlayAudioVO.getTerminalId(), edgeSpeakerPlayAudioVO.getDeviceId(), edgeSpeakerPlayAudioVO.getAudioPath());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@ -86,12 +83,8 @@ public class EdgeSpeakerController {
|
||||
@ApiOperation("停止播放")
|
||||
@PreAuthorize("@ss.hasPermi('system:speaker:stop')")
|
||||
@PostMapping("/stop")
|
||||
public AjaxResult stopPlayback(
|
||||
@ApiParam(value = "终端设备ID", required = true)
|
||||
@RequestParam String terminalId,
|
||||
@ApiParam(value = "设备ID", required = true)
|
||||
@RequestParam String deviceId) {
|
||||
speakerService.stopPlayback(terminalId, deviceId);
|
||||
public AjaxResult stopPlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
|
||||
speakerService.stopPlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@ -101,12 +94,8 @@ public class EdgeSpeakerController {
|
||||
@ApiOperation("暂停播放")
|
||||
@PreAuthorize("@ss.hasPermi('system:speaker:pause')")
|
||||
@PostMapping("/pause")
|
||||
public AjaxResult pausePlayback(
|
||||
@ApiParam(value = "终端设备ID", required = true)
|
||||
@RequestParam String terminalId,
|
||||
@ApiParam(value = "设备ID", required = true)
|
||||
@RequestParam String deviceId) {
|
||||
speakerService.pausePlayback(terminalId, deviceId);
|
||||
public AjaxResult pausePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
|
||||
speakerService.pausePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@ -116,12 +105,8 @@ public class EdgeSpeakerController {
|
||||
@ApiOperation("恢复播放")
|
||||
@PreAuthorize("@ss.hasPermi('system:speaker:resume')")
|
||||
@PostMapping("/resume")
|
||||
public AjaxResult resumePlayback(
|
||||
@ApiParam(value = "终端设备ID", required = true)
|
||||
@RequestParam String terminalId,
|
||||
@ApiParam(value = "设备ID", required = true)
|
||||
@RequestParam String deviceId) {
|
||||
speakerService.resumePlayback(terminalId, deviceId);
|
||||
public AjaxResult resumePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
|
||||
speakerService.resumePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@ -131,14 +116,8 @@ public class EdgeSpeakerController {
|
||||
@ApiOperation("设置音量")
|
||||
@PreAuthorize("@ss.hasPermi('system:speaker:volume')")
|
||||
@PostMapping("/volume")
|
||||
public AjaxResult setVolume(
|
||||
@ApiParam(value = "终端设备ID", required = true)
|
||||
@RequestParam String terminalId,
|
||||
@ApiParam(value = "设备ID", required = true)
|
||||
@RequestParam String deviceId,
|
||||
@ApiParam(value = "音量大小(0-100)", required = true)
|
||||
@RequestParam int volume) {
|
||||
speakerService.setVolume(terminalId, deviceId, volume);
|
||||
public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
|
||||
speakerService.setVolume(edgeMicrophoneVolumeVO.getTerminalId(), edgeMicrophoneVolumeVO.getDeviceId(), edgeMicrophoneVolumeVO.getVolume());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
package com.cmvr.web.controller.inspection;
|
||||
|
||||
import com.cmvr.common.annotation.Anonymous;
|
||||
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
|
||||
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* cmvr_edge_ai 检测报警推送入口。
|
||||
*
|
||||
* 该接口由边缘服务主动调用,使用HTTP状态码表达处理结果,不使用项目通用AjaxResult包装。
|
||||
*/
|
||||
@Slf4j
|
||||
@Anonymous
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Api(tags = "智能巡检--PPE报警接收")
|
||||
public class InspectionDetectionAlertController
|
||||
{
|
||||
/** 包含Base64图片的JSON请求最大允许20MB,防止异常请求耗尽服务端内存。 */
|
||||
private static final int MAX_REQUEST_BYTES = 20 * 1024 * 1024;
|
||||
|
||||
private final IInspectionDetectionAlertService detectionAlertService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 接收PPE违规报警。重复事件视为已处理,仍返回204,避免边缘端持续重试。
|
||||
*/
|
||||
@ApiOperation("接收边缘AI的PPE违规报警")
|
||||
@PostMapping(value = "/v1/detection-alerts", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Void> receive(
|
||||
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
|
||||
HttpServletRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
String requestBody = readRequestBody(request);
|
||||
AlertEnvelope envelope = objectMapper.readValue(requestBody, AlertEnvelope.class);
|
||||
detectionAlertService.receive(envelope, idempotencyKey);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
catch (JsonProcessingException | DetectionAlertBadRequestException ex)
|
||||
{
|
||||
log.warn("拒绝不合法的PPE报警请求,idempotencyKey={},原因={}", idempotencyKey, ex.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
catch (DuplicateKeyException ex)
|
||||
{
|
||||
// 预查与插入之间仍可能发生并发,由数据库唯一索引完成最终幂等。
|
||||
log.info("忽略并发重复的PPE报警,idempotencyKey={}", idempotencyKey);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
log.warn("读取PPE报警请求失败,idempotencyKey={},原因={}", idempotencyKey, ex.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.error("PPE报警处理暂时失败,idempotencyKey={}", idempotencyKey, ex);
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
|
||||
}
|
||||
}
|
||||
|
||||
private String readRequestBody(HttpServletRequest request) throws IOException
|
||||
{
|
||||
long contentLength = request.getContentLengthLong();
|
||||
if (contentLength > MAX_REQUEST_BYTES)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("请求体不能超过20MB");
|
||||
}
|
||||
|
||||
try (ServletInputStream inputStream = request.getInputStream();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(initialCapacity(contentLength)))
|
||||
{
|
||||
byte[] buffer = new byte[8192];
|
||||
int total = 0;
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1)
|
||||
{
|
||||
total += length;
|
||||
if (total > MAX_REQUEST_BYTES)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("请求体不能超过20MB");
|
||||
}
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
if (total == 0)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("请求体不能为空");
|
||||
}
|
||||
return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private int initialCapacity(long contentLength)
|
||||
{
|
||||
if (contentLength <= 0 || contentLength > MAX_REQUEST_BYTES)
|
||||
{
|
||||
return 8192;
|
||||
}
|
||||
return (int) contentLength;
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
@ApiOperation("单个节点执行")
|
||||
public AjaxResult actionExecute(@RequestBody FlowActionRequestVO flowActionRequestVO) {
|
||||
@ApiOperation(value = "单个节点执行",
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,11 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.models.auth.In;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
@ -34,6 +39,10 @@ public class SwaggerConfig
|
||||
@Autowired
|
||||
private CmvrIotConfig cmvrIotConfig;
|
||||
|
||||
/** 用于将动态工作流payload对应的参数模型显式加入Swagger文档。 */
|
||||
@Autowired
|
||||
private TypeResolver typeResolver;
|
||||
|
||||
/** 是否开启swagger */
|
||||
@Value("${swagger.enabled}")
|
||||
private boolean enabled;
|
||||
@ -62,6 +71,12 @@ public class SwaggerConfig
|
||||
// 扫描所有 .apis(RequestHandlerSelectors.any())
|
||||
.paths(PathSelectors.any())
|
||||
.build()
|
||||
// 工作流单节点接口使用JSONObject作为payload,需显式注册巡检节点参数模型。
|
||||
.additionalModels(
|
||||
typeResolver.resolve(InspectionMeterRecognizeConfigVO.class),
|
||||
typeResolver.resolve(InspectionAlarmRuleVO.class),
|
||||
typeResolver.resolve(InspectionManualReviewConfigVO.class),
|
||||
typeResolver.resolve(InspectionMediaVO.class))
|
||||
/* 设置安全模式,swagger可以设置访问token */
|
||||
.securitySchemes(securitySchemes())
|
||||
.securityContexts(securityContexts())
|
||||
|
||||
@ -21,6 +21,9 @@ server:
|
||||
tomcat:
|
||||
# tomcat的URI编码
|
||||
uri-encoding: UTF-8
|
||||
# PPE报警包含Base64图片,允许读取较大的JSON请求,同时限制异常请求的吞入大小
|
||||
max-http-form-post-size: 20MB
|
||||
max-swallow-size: 20MB
|
||||
# 连接数满后的排队数,默认为100
|
||||
accept-count: 1000
|
||||
threads:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -47,8 +47,10 @@ public class GrpcServiceManager {
|
||||
clientFactories.put(SystemServiceGrpc.SystemServiceBlockingStub.class, new GrpcClientFactory<>(SystemServiceGrpc::newBlockingStub));
|
||||
// 注册扬声器服务的 Stub
|
||||
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceBlockingStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newBlockingStub));
|
||||
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newStub));
|
||||
// 注册麦克风服务的 Stub
|
||||
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newBlockingStub));
|
||||
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newStub));
|
||||
// 注册机器人头部服务的 Stub
|
||||
clientFactories.put(BioHeadServiceGrpc.BioHeadServiceBlockingStub.class, new GrpcClientFactory<>(BioHeadServiceGrpc::newBlockingStub));
|
||||
// 注册机器人灵巧手服务的 Stub
|
||||
@ -60,7 +62,6 @@ public class GrpcServiceManager {
|
||||
// 注册agv服务的stub
|
||||
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub));
|
||||
// 注册机器人实时语音双向流服务的异步Stub
|
||||
clientFactories.put(RobotAudioServiceGrpc.RobotAudioServiceStub.class, new GrpcClientFactory<>(RobotAudioServiceGrpc::newStub));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package com.cmvr.edge.client.model.speaker;
|
||||
|
||||
import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 扬声器播放音频请求参数。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("扬声器播放音频VO")
|
||||
public class EdgeSpeakerPlayAudioVO extends EdgeCommonVO {
|
||||
|
||||
@ApiModelProperty("音频文件路径")
|
||||
@NotEmpty(message = "音频文件路径不能为空")
|
||||
private String audioPath;
|
||||
}
|
||||
@ -21,6 +21,12 @@ import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@ -40,6 +46,12 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
|
||||
/** 机器人心跳超时时间:机器人每 1 秒上报一次,超过 5 秒判定离线。 */
|
||||
private static final long ROBOT_HEARTBEAT_TIMEOUT_MS = 5000L;
|
||||
private static final int BROWSER_AUDIO_FRAME_BYTES = 960;
|
||||
private static final int BROWSER_AUDIO_FRAME_MS = 10;
|
||||
private static final int TARGET_BROWSER_AUDIO_QUEUE_FRAMES = 4;
|
||||
private static final int MAX_BROWSER_AUDIO_CATCH_UP_FRAMES = 4;
|
||||
private static final int WARN_BROWSER_AUDIO_QUEUE_FRAMES = 100;
|
||||
private static final long BROWSER_AUDIO_PUSH_INTERVAL_MS = 5L;
|
||||
|
||||
private final RobotAudioGrpcAdapter robotAudioGrpcAdapter;
|
||||
|
||||
@ -166,13 +178,25 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
private void handleJoin(WebSocketSession webSocketSession, JsonNode jsonNode) {
|
||||
String terminalId = getText(jsonNode, "terminalId");
|
||||
String deviceId = getText(jsonNode, "deviceId");
|
||||
String speakerDeviceId = getText(jsonNode, "speakerDeviceId");
|
||||
String micDeviceId = getText(jsonNode, "micDeviceId");
|
||||
String operatorId = getText(jsonNode, "operatorId");
|
||||
String sessionId = getText(jsonNode, "sessionId");
|
||||
|
||||
if (terminalId == null || deviceId == null) {
|
||||
sendError(webSocketSession, sessionId, "terminalId和deviceId不能为空");
|
||||
if (speakerDeviceId == null) {
|
||||
speakerDeviceId = deviceId;
|
||||
}
|
||||
if (micDeviceId == null) {
|
||||
micDeviceId = deviceId;
|
||||
}
|
||||
|
||||
if (terminalId == null || speakerDeviceId == null || micDeviceId == null) {
|
||||
sendError(webSocketSession, sessionId, "terminalId、speakerDeviceId、micDeviceId不能为空");
|
||||
return;
|
||||
}
|
||||
if (deviceId == null) {
|
||||
deviceId = speakerDeviceId;
|
||||
}
|
||||
if (sessionId == null) {
|
||||
sessionId = IdUtils.fastSimpleUUID();
|
||||
}
|
||||
@ -191,24 +215,28 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
session.sessionId = sessionId;
|
||||
session.terminalId = terminalId;
|
||||
session.deviceId = deviceId;
|
||||
session.speakerDeviceId = speakerDeviceId;
|
||||
session.micDeviceId = micDeviceId;
|
||||
session.operatorId = operatorId;
|
||||
session.webSocketSession = webSocketSession;
|
||||
session.state = AudioSessionState.CONNECTING;
|
||||
session.createdAt = System.currentTimeMillis();
|
||||
session.lastBrowserSignalAt = session.createdAt;
|
||||
session.lastRobotHeartbeatAt = session.createdAt;
|
||||
startBrowserAudioPusher(session);
|
||||
|
||||
sessionMap.put(sessionId, session);
|
||||
webSocketSessionMap.put(webSocketSession.getId(), sessionId);
|
||||
try {
|
||||
session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, deviceId, sessionId, operatorId, this);
|
||||
session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, speakerDeviceId, micDeviceId, sessionId, operatorId, this);
|
||||
} catch (Exception e) {
|
||||
sessionMap.remove(sessionId);
|
||||
webSocketSessionMap.remove(webSocketSession.getId());
|
||||
stopBrowserAudioPusher(session);
|
||||
throw e;
|
||||
}
|
||||
sendState(session, "joined");
|
||||
log.info("实时语音会话创建成功,sessionId={},terminalId={},deviceId={}", sessionId, terminalId, deviceId);
|
||||
log.info("实时语音会话创建成功,sessionId={},terminalId={},speakerDeviceId={},micDeviceId={}", sessionId, terminalId, speakerDeviceId, micDeviceId);
|
||||
} catch (Exception e) {
|
||||
log.error("创建实时语音会话失败,sessionId={}", sessionId, e);
|
||||
sendError(webSocketSession, sessionId, "创建实时语音会话失败:" + e.getMessage());
|
||||
@ -294,6 +322,7 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
try {
|
||||
session.state = AudioSessionState.CLOSED;
|
||||
removeWebSocketMapping(session);
|
||||
stopBrowserAudioPusher(session);
|
||||
if (session.robotStream != null) {
|
||||
session.robotStream.sendStop(reason);
|
||||
}
|
||||
@ -318,12 +347,13 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
continue;
|
||||
}
|
||||
if (now - session.lastRobotHeartbeatAt > ROBOT_HEARTBEAT_TIMEOUT_MS) {
|
||||
log.warn("机器人心跳超时,sessionId={},terminalId={},deviceId={}",
|
||||
session.sessionId, session.terminalId, session.deviceId);
|
||||
log.warn("机器人心跳超时,sessionId={},terminalId={},speakerDeviceId={},micDeviceId={}",
|
||||
session.sessionId, session.terminalId, session.speakerDeviceId, session.micDeviceId);
|
||||
// 先从业务会话表摘除,避免 cancel/close 触发的异步回调再次进入同一个会话并重复关闭 native 对象。
|
||||
iterator.remove();
|
||||
session.state = AudioSessionState.ROBOT_OFFLINE;
|
||||
sendState(session, "robotOffline");
|
||||
stopBrowserAudioPusher(session);
|
||||
if (session.robotStream != null) {
|
||||
session.robotStream.cancel("robot heartbeat timeout");
|
||||
}
|
||||
@ -359,7 +389,7 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
}
|
||||
session.lastRobotAudioAt = System.currentTimeMillis();
|
||||
session.lastRobotHeartbeatAt = session.lastRobotAudioAt;
|
||||
sendBinary(session.webSocketSession, pcm);
|
||||
enqueueRobotAudioForBrowser(session, pcm);
|
||||
Consumer<byte[]> consumer = session.robotAudioConsumer;
|
||||
if (consumer != null) {
|
||||
try {
|
||||
@ -412,12 +442,14 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
sendState(session, "grpcCompleted");
|
||||
}
|
||||
sessionMap.remove(sessionId);
|
||||
stopBrowserAudioPusher(session);
|
||||
removeWebSocketMapping(session);
|
||||
}
|
||||
|
||||
private void failSession(AudioSession session, String reason, boolean cancelRobotStream) {
|
||||
sessionMap.remove(session.sessionId, session);
|
||||
session.state = AudioSessionState.FAILED;
|
||||
stopBrowserAudioPusher(session);
|
||||
ObjectNode event = baseMessage("error", session.sessionId);
|
||||
event.put("message", reason == null ? "未知错误" : reason);
|
||||
event.put("state", session.state.name());
|
||||
@ -436,6 +468,8 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
ObjectNode event = baseMessage(type, session.sessionId);
|
||||
event.put("terminalId", session.terminalId);
|
||||
event.put("deviceId", session.deviceId);
|
||||
event.put("speakerDeviceId", session.speakerDeviceId);
|
||||
event.put("micDeviceId", session.micDeviceId);
|
||||
event.put("state", session.state.name());
|
||||
event.put("createdAt", session.createdAt);
|
||||
event.put("lastRobotHeartbeatAt", session.lastRobotHeartbeatAt);
|
||||
@ -462,6 +496,108 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
sendText(session, toJson(event));
|
||||
}
|
||||
|
||||
private void startBrowserAudioPusher(AudioSession session) {
|
||||
if (session == null || session.robotBrowserAudioExecutor != null) {
|
||||
return;
|
||||
}
|
||||
session.robotBrowserAudioExecutor = Executors.newSingleThreadScheduledExecutor(
|
||||
new NamedThreadFactory("robot-browser-audio-" + session.sessionId));
|
||||
session.robotBrowserAudioFuture = session.robotBrowserAudioExecutor.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
int queued = session.robotBrowserAudioQueue.size();
|
||||
int framesToSend = queued > TARGET_BROWSER_AUDIO_QUEUE_FRAMES
|
||||
? Math.min(MAX_BROWSER_AUDIO_CATCH_UP_FRAMES, queued - TARGET_BROWSER_AUDIO_QUEUE_FRAMES + 1)
|
||||
: 1;
|
||||
for (int i = 0; i < framesToSend; i++) {
|
||||
byte[] frame = session.robotBrowserAudioQueue.poll();
|
||||
if (frame == null) {
|
||||
break;
|
||||
}
|
||||
sendBinary(session.webSocketSession, frame);
|
||||
session.robotBrowserAudioSent++;
|
||||
}
|
||||
logRobotBrowserAudioStatsIfNeeded(session, "robotAudioPush");
|
||||
} catch (Exception e) {
|
||||
log.error("推送机器人音频到浏览器失败,sessionId={}", session.sessionId, e);
|
||||
}
|
||||
}
|
||||
}, 0L, BROWSER_AUDIO_PUSH_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void enqueueRobotAudioForBrowser(AudioSession session, byte[] pcm) {
|
||||
if (session == null || pcm == null || pcm.length == 0) {
|
||||
return;
|
||||
}
|
||||
synchronized (session.robotBrowserAudioLock) {
|
||||
byte[] merged = new byte[session.robotBrowserRemainder.length + pcm.length];
|
||||
System.arraycopy(session.robotBrowserRemainder, 0, merged, 0, session.robotBrowserRemainder.length);
|
||||
System.arraycopy(pcm, 0, merged, session.robotBrowserRemainder.length, pcm.length);
|
||||
|
||||
int offset = 0;
|
||||
while (merged.length - offset >= BROWSER_AUDIO_FRAME_BYTES) {
|
||||
byte[] frame = new byte[BROWSER_AUDIO_FRAME_BYTES];
|
||||
System.arraycopy(merged, offset, frame, 0, BROWSER_AUDIO_FRAME_BYTES);
|
||||
offerRobotAudioFrame(session, frame);
|
||||
offset += BROWSER_AUDIO_FRAME_BYTES;
|
||||
}
|
||||
|
||||
int remain = merged.length - offset;
|
||||
session.robotBrowserRemainder = new byte[remain];
|
||||
if (remain > 0) {
|
||||
System.arraycopy(merged, offset, session.robotBrowserRemainder, 0, remain);
|
||||
}
|
||||
}
|
||||
logRobotBrowserAudioStatsIfNeeded(session, "robotAudioEnqueue");
|
||||
}
|
||||
|
||||
private void offerRobotAudioFrame(AudioSession session, byte[] frame) {
|
||||
session.robotBrowserAudioQueue.offer(frame);
|
||||
session.robotBrowserAudioEnqueued++;
|
||||
}
|
||||
|
||||
private void stopBrowserAudioPusher(AudioSession session) {
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
ScheduledFuture<?> future = session.robotBrowserAudioFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
session.robotBrowserAudioFuture = null;
|
||||
}
|
||||
ScheduledExecutorService executor = session.robotBrowserAudioExecutor;
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
session.robotBrowserAudioExecutor = null;
|
||||
}
|
||||
synchronized (session.robotBrowserAudioLock) {
|
||||
session.robotBrowserAudioQueue.clear();
|
||||
session.robotBrowserRemainder = new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
private void logRobotBrowserAudioStatsIfNeeded(AudioSession session, String source) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - session.lastRobotBrowserAudioLogAt < 5000L) {
|
||||
return;
|
||||
}
|
||||
session.lastRobotBrowserAudioLogAt = now;
|
||||
int queued = session.robotBrowserAudioQueue.size();
|
||||
log.info("机器人到浏览器音频统计,source={},sessionId={},enqueued={},sent={},dropped={},queued={},remainderBytes={}",
|
||||
source,
|
||||
session.sessionId,
|
||||
session.robotBrowserAudioEnqueued,
|
||||
session.robotBrowserAudioSent,
|
||||
session.robotBrowserAudioDropped,
|
||||
queued,
|
||||
session.robotBrowserRemainder.length);
|
||||
if (queued > WARN_BROWSER_AUDIO_QUEUE_FRAMES) {
|
||||
log.warn("机器人到浏览器音频队列积压较高,sessionId={},queued={},estimatedDelayMs={},请检查前端播放消费或网络发送是否跟不上",
|
||||
session.sessionId, queued, queued * BROWSER_AUDIO_FRAME_MS);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBinary(WebSocketSession session, byte[] pcm) {
|
||||
if (session == null || !session.isOpen() || pcm == null || pcm.length == 0) {
|
||||
return;
|
||||
@ -571,10 +707,21 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
private String sessionId;
|
||||
private String terminalId;
|
||||
private String deviceId;
|
||||
private String speakerDeviceId;
|
||||
private String micDeviceId;
|
||||
private String operatorId;
|
||||
private volatile WebSocketSession webSocketSession;
|
||||
private volatile RobotAudioStream robotStream;
|
||||
private volatile Consumer<byte[]> robotAudioConsumer;
|
||||
private volatile ScheduledExecutorService robotBrowserAudioExecutor;
|
||||
private volatile ScheduledFuture<?> robotBrowserAudioFuture;
|
||||
private final LinkedBlockingQueue<byte[]> robotBrowserAudioQueue = new LinkedBlockingQueue<>();
|
||||
private final Object robotBrowserAudioLock = new Object();
|
||||
private byte[] robotBrowserRemainder = new byte[0];
|
||||
private volatile long robotBrowserAudioEnqueued;
|
||||
private volatile long robotBrowserAudioSent;
|
||||
private volatile long robotBrowserAudioDropped;
|
||||
private volatile long lastRobotBrowserAudioLogAt;
|
||||
private volatile AudioSessionState state;
|
||||
private volatile long createdAt;
|
||||
private volatile long lastBrowserSignalAt;
|
||||
@ -588,4 +735,19 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
private String sdpMid;
|
||||
private int sdpMLineIndex;
|
||||
}
|
||||
|
||||
private static class NamedThreadFactory implements ThreadFactory {
|
||||
private final String name;
|
||||
|
||||
private NamedThreadFactory(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = new Thread(runnable, name);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -170,6 +170,37 @@ public final class MicPhoneServiceGrpc {
|
||||
return getResumeRecordMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "StreamAudio",
|
||||
requestType = cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request.class,
|
||||
responseType = cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
||||
public static io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod() {
|
||||
io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request, cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod;
|
||||
if ((getStreamAudioMethod = MicPhoneServiceGrpc.getStreamAudioMethod) == null) {
|
||||
synchronized (MicPhoneServiceGrpc.class) {
|
||||
if ((getStreamAudioMethod = MicPhoneServiceGrpc.getStreamAudioMethod) == null) {
|
||||
MicPhoneServiceGrpc.getStreamAudioMethod = getStreamAudioMethod =
|
||||
io.grpc.MethodDescriptor.<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request, cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamAudio"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new MicPhoneServiceMethodDescriptorSupplier("StreamAudio"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getStreamAudioMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> getSetVolumeMethod;
|
||||
|
||||
@ -281,9 +312,6 @@ public final class MicPhoneServiceGrpc {
|
||||
public static abstract class MicPhoneServiceImplBase implements io.grpc.BindableService {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
||||
@ -319,9 +347,13 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public void streamAudio(cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> responseObserver) {
|
||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getStreamAudioMethod(), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
||||
@ -372,6 +404,13 @@ public final class MicPhoneServiceGrpc {
|
||||
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>(
|
||||
this, METHODID_RESUME_RECORD)))
|
||||
.addMethod(
|
||||
getStreamAudioMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncServerStreamingCall(
|
||||
new MethodHandlers<
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>(
|
||||
this, METHODID_STREAM_AUDIO)))
|
||||
.addMethod(
|
||||
getSetVolumeMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||
@ -405,9 +444,6 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
||||
@ -448,9 +484,14 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public void streamAudio(cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> responseObserver) {
|
||||
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
|
||||
getChannel().newCall(getStreamAudioMethod(), getCallOptions()), request, responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
||||
@ -482,9 +523,6 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
@ -520,9 +558,14 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public java.util.Iterator<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> streamAudio(
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
|
||||
getChannel(), getStreamAudioMethod(), getCallOptions(), request);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
@ -552,9 +595,6 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> getStatus(
|
||||
cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
||||
@ -595,9 +635,6 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> setVolume(
|
||||
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
||||
@ -619,8 +656,9 @@ public final class MicPhoneServiceGrpc {
|
||||
private static final int METHODID_STOP_RECORD = 2;
|
||||
private static final int METHODID_PAUSE_RECORD = 3;
|
||||
private static final int METHODID_RESUME_RECORD = 4;
|
||||
private static final int METHODID_SET_VOLUME = 5;
|
||||
private static final int METHODID_GET_VOLUME = 6;
|
||||
private static final int METHODID_STREAM_AUDIO = 5;
|
||||
private static final int METHODID_SET_VOLUME = 6;
|
||||
private static final int METHODID_GET_VOLUME = 7;
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
@ -659,6 +697,10 @@ public final class MicPhoneServiceGrpc {
|
||||
serviceImpl.resumeRecord((cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request) request,
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>) responseObserver);
|
||||
break;
|
||||
case METHODID_STREAM_AUDIO:
|
||||
serviceImpl.streamAudio((cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request) request,
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>) responseObserver);
|
||||
break;
|
||||
case METHODID_SET_VOLUME:
|
||||
serviceImpl.setVolume((cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request) request,
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback>) responseObserver);
|
||||
@ -733,6 +775,7 @@ public final class MicPhoneServiceGrpc {
|
||||
.addMethod(getStopRecordMethod())
|
||||
.addMethod(getPauseRecordMethod())
|
||||
.addMethod(getResumeRecordMethod())
|
||||
.addMethod(getStreamAudioMethod())
|
||||
.addMethod(getSetVolumeMethod())
|
||||
.addMethod(getGetVolumeMethod())
|
||||
.build();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ public final class MicrophoneService {
|
||||
java.lang.String[] descriptorData = {
|
||||
"\n!cmvr/api/microphone_service.proto\022\010cmv" +
|
||||
"r.api\032!cmvr/api/microphone_command.proto" +
|
||||
"2\327\005\n\017MicPhoneService\022X\n\tGetStatus\022$.cmvr" +
|
||||
"2\273\006\n\017MicPhoneService\022X\n\tGetStatus\022$.cmvr" +
|
||||
".api.GetMicStateCommand.Request\032%.cmvr.a" +
|
||||
"pi.GetMicStateCommand.Feedback\022f\n\013StartR" +
|
||||
"ecord\022*.cmvr.api.StartMicRecordingComman" +
|
||||
@ -38,12 +38,14 @@ public final class MicrophoneService {
|
||||
"ingCommand.Feedback\022i\n\014ResumeRecord\022+.cm" +
|
||||
"vr.api.ResumeMicRecordingCommand.Request" +
|
||||
"\032,.cmvr.api.ResumeMicRecordingCommand.Fe" +
|
||||
"edback\022d\n\tSetVolume\022*.cmvr.api.SetMicPho" +
|
||||
"neVolumeCommand.Request\032+.cmvr.api.SetMi" +
|
||||
"cPhoneVolumeCommand.Feedback\022d\n\tGetVolum" +
|
||||
"e\022*.cmvr.api.GetMicPhoneVolumeCommand.Re" +
|
||||
"quest\032+.cmvr.api.GetMicPhoneVolumeComman" +
|
||||
"d.Feedbackb\006proto3"
|
||||
"edback\022b\n\013StreamAudio\022\'.cmvr.api.StreamM" +
|
||||
"icAudioCommand.Request\032(.cmvr.api.Stream" +
|
||||
"MicAudioCommand.Feedback0\001\022d\n\tSetVolume\022" +
|
||||
"*.cmvr.api.SetMicPhoneVolumeCommand.Requ" +
|
||||
"est\032+.cmvr.api.SetMicPhoneVolumeCommand." +
|
||||
"Feedback\022d\n\tGetVolume\022*.cmvr.api.GetMicP" +
|
||||
"honeVolumeCommand.Request\032+.cmvr.api.Get" +
|
||||
"MicPhoneVolumeCommand.Feedbackb\006proto3"
|
||||
};
|
||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||
.internalBuildGeneratedFileFrom(descriptorData,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,280 +0,0 @@
|
||||
package cmvr.api;
|
||||
|
||||
import static io.grpc.MethodDescriptor.generateFullMethodName;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
@javax.annotation.Generated(
|
||||
value = "by gRPC proto compiler (version 1.52.0)",
|
||||
comments = "Source: cmvr/api/robot_audio.proto")
|
||||
@io.grpc.stub.annotations.GrpcGenerated
|
||||
public final class RobotAudioServiceGrpc {
|
||||
|
||||
private RobotAudioServiceGrpc() {}
|
||||
|
||||
public static final String SERVICE_NAME = "cmvr.api.RobotAudioService";
|
||||
|
||||
// Static method descriptors that strictly reflect the proto.
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest,
|
||||
cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "AudioTalk",
|
||||
requestType = cmvr.api.RobotAudio.RobotAudioRequest.class,
|
||||
responseType = cmvr.api.RobotAudio.RobotAudioResponse.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
|
||||
public static io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest,
|
||||
cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod() {
|
||||
io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest, cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod;
|
||||
if ((getAudioTalkMethod = RobotAudioServiceGrpc.getAudioTalkMethod) == null) {
|
||||
synchronized (RobotAudioServiceGrpc.class) {
|
||||
if ((getAudioTalkMethod = RobotAudioServiceGrpc.getAudioTalkMethod) == null) {
|
||||
RobotAudioServiceGrpc.getAudioTalkMethod = getAudioTalkMethod =
|
||||
io.grpc.MethodDescriptor.<cmvr.api.RobotAudio.RobotAudioRequest, cmvr.api.RobotAudio.RobotAudioResponse>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "AudioTalk"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.RobotAudio.RobotAudioRequest.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.RobotAudio.RobotAudioResponse.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new RobotAudioServiceMethodDescriptorSupplier("AudioTalk"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getAudioTalkMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new async stub that supports all call types for the service
|
||||
*/
|
||||
public static RobotAudioServiceStub newStub(io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceStub>() {
|
||||
@java.lang.Override
|
||||
public RobotAudioServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return RobotAudioServiceStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new blocking-style stub that supports unary and streaming output calls on the service
|
||||
*/
|
||||
public static RobotAudioServiceBlockingStub newBlockingStub(
|
||||
io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceBlockingStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceBlockingStub>() {
|
||||
@java.lang.Override
|
||||
public RobotAudioServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceBlockingStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return RobotAudioServiceBlockingStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ListenableFuture-style stub that supports unary calls on the service
|
||||
*/
|
||||
public static RobotAudioServiceFutureStub newFutureStub(
|
||||
io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceFutureStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceFutureStub>() {
|
||||
@java.lang.Override
|
||||
public RobotAudioServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceFutureStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return RobotAudioServiceFutureStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
public static abstract class RobotAudioServiceImplBase implements io.grpc.BindableService {
|
||||
|
||||
/**
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioRequest> audioTalk(
|
||||
io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse> responseObserver) {
|
||||
return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getAudioTalkMethod(), responseObserver);
|
||||
}
|
||||
|
||||
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
|
||||
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
|
||||
.addMethod(
|
||||
getAudioTalkMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncBidiStreamingCall(
|
||||
new MethodHandlers<
|
||||
cmvr.api.RobotAudio.RobotAudioRequest,
|
||||
cmvr.api.RobotAudio.RobotAudioResponse>(
|
||||
this, METHODID_AUDIO_TALK)))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
public static final class RobotAudioServiceStub extends io.grpc.stub.AbstractAsyncStub<RobotAudioServiceStub> {
|
||||
private RobotAudioServiceStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected RobotAudioServiceStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceStub(channel, callOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioRequest> audioTalk(
|
||||
io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse> responseObserver) {
|
||||
return io.grpc.stub.ClientCalls.asyncBidiStreamingCall(
|
||||
getChannel().newCall(getAudioTalkMethod(), getCallOptions()), responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
public static final class RobotAudioServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub<RobotAudioServiceBlockingStub> {
|
||||
private RobotAudioServiceBlockingStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected RobotAudioServiceBlockingStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceBlockingStub(channel, callOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
public static final class RobotAudioServiceFutureStub extends io.grpc.stub.AbstractFutureStub<RobotAudioServiceFutureStub> {
|
||||
private RobotAudioServiceFutureStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected RobotAudioServiceFutureStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceFutureStub(channel, callOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int METHODID_AUDIO_TALK = 0;
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.ServerStreamingMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.ClientStreamingMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.BidiStreamingMethod<Req, Resp> {
|
||||
private final RobotAudioServiceImplBase serviceImpl;
|
||||
private final int methodId;
|
||||
|
||||
MethodHandlers(RobotAudioServiceImplBase serviceImpl, int methodId) {
|
||||
this.serviceImpl = serviceImpl;
|
||||
this.methodId = methodId;
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@java.lang.SuppressWarnings("unchecked")
|
||||
public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@java.lang.SuppressWarnings("unchecked")
|
||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
case METHODID_AUDIO_TALK:
|
||||
return (io.grpc.stub.StreamObserver<Req>) serviceImpl.audioTalk(
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse>) responseObserver);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class RobotAudioServiceBaseDescriptorSupplier
|
||||
implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier {
|
||||
RobotAudioServiceBaseDescriptorSupplier() {}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() {
|
||||
return cmvr.api.RobotAudio.getDescriptor();
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() {
|
||||
return getFileDescriptor().findServiceByName("RobotAudioService");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RobotAudioServiceFileDescriptorSupplier
|
||||
extends RobotAudioServiceBaseDescriptorSupplier {
|
||||
RobotAudioServiceFileDescriptorSupplier() {}
|
||||
}
|
||||
|
||||
private static final class RobotAudioServiceMethodDescriptorSupplier
|
||||
extends RobotAudioServiceBaseDescriptorSupplier
|
||||
implements io.grpc.protobuf.ProtoMethodDescriptorSupplier {
|
||||
private final String methodName;
|
||||
|
||||
RobotAudioServiceMethodDescriptorSupplier(String methodName) {
|
||||
this.methodName = methodName;
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() {
|
||||
return getServiceDescriptor().findMethodByName(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile io.grpc.ServiceDescriptor serviceDescriptor;
|
||||
|
||||
public static io.grpc.ServiceDescriptor getServiceDescriptor() {
|
||||
io.grpc.ServiceDescriptor result = serviceDescriptor;
|
||||
if (result == null) {
|
||||
synchronized (RobotAudioServiceGrpc.class) {
|
||||
result = serviceDescriptor;
|
||||
if (result == null) {
|
||||
serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME)
|
||||
.setSchemaDescriptor(new RobotAudioServiceFileDescriptorSupplier())
|
||||
.addMethod(getAudioTalkMethod())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -77,6 +77,37 @@ public final class SpeakerServiceGrpc {
|
||||
return getPlayAudioMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "StreamAudio",
|
||||
requestType = cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request.class,
|
||||
responseType = cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)
|
||||
public static io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod() {
|
||||
io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request, cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod;
|
||||
if ((getStreamAudioMethod = SpeakerServiceGrpc.getStreamAudioMethod) == null) {
|
||||
synchronized (SpeakerServiceGrpc.class) {
|
||||
if ((getStreamAudioMethod = SpeakerServiceGrpc.getStreamAudioMethod) == null) {
|
||||
SpeakerServiceGrpc.getStreamAudioMethod = getStreamAudioMethod =
|
||||
io.grpc.MethodDescriptor.<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request, cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamAudio"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new SpeakerServiceMethodDescriptorSupplier("StreamAudio"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getStreamAudioMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StopSpeakerCommand.Request,
|
||||
cmvr.api.SpeakerCommand.StopSpeakerCommand.Feedback> getStopPlaybackMethod;
|
||||
|
||||
@ -281,9 +312,6 @@ public final class SpeakerServiceGrpc {
|
||||
public static abstract class SpeakerServiceImplBase implements io.grpc.BindableService {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
||||
@ -297,6 +325,13 @@ public final class SpeakerServiceGrpc {
|
||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getPlayAudioMethod(), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request> streamAudio(
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> responseObserver) {
|
||||
return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getStreamAudioMethod(), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
||||
@ -319,9 +354,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
||||
@ -351,6 +383,13 @@ public final class SpeakerServiceGrpc {
|
||||
cmvr.api.SpeakerCommand.PlayAudioCommand.Request,
|
||||
cmvr.api.SpeakerCommand.PlayAudioCommand.Feedback>(
|
||||
this, METHODID_PLAY_AUDIO)))
|
||||
.addMethod(
|
||||
getStreamAudioMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncClientStreamingCall(
|
||||
new MethodHandlers<
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>(
|
||||
this, METHODID_STREAM_AUDIO)))
|
||||
.addMethod(
|
||||
getStopPlaybackMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||
@ -405,9 +444,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
||||
@ -423,6 +459,14 @@ public final class SpeakerServiceGrpc {
|
||||
getChannel().newCall(getPlayAudioMethod(), getCallOptions()), request, responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request> streamAudio(
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> responseObserver) {
|
||||
return io.grpc.stub.ClientCalls.asyncClientStreamingCall(
|
||||
getChannel().newCall(getStreamAudioMethod(), getCallOptions()), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
||||
@ -448,9 +492,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
||||
@ -482,9 +523,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
@ -520,9 +558,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
@ -552,9 +587,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> getStatus(
|
||||
cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
||||
@ -595,9 +627,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> setVolume(
|
||||
cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
||||
@ -621,6 +650,7 @@ public final class SpeakerServiceGrpc {
|
||||
private static final int METHODID_RESUME_PLAYBACK = 4;
|
||||
private static final int METHODID_SET_VOLUME = 5;
|
||||
private static final int METHODID_GET_VOLUME = 6;
|
||||
private static final int METHODID_STREAM_AUDIO = 7;
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
@ -677,6 +707,9 @@ public final class SpeakerServiceGrpc {
|
||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
case METHODID_STREAM_AUDIO:
|
||||
return (io.grpc.stub.StreamObserver<Req>) serviceImpl.streamAudio(
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>) responseObserver);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
@ -730,6 +763,7 @@ public final class SpeakerServiceGrpc {
|
||||
.setSchemaDescriptor(new SpeakerServiceFileDescriptorSupplier())
|
||||
.addMethod(getGetStatusMethod())
|
||||
.addMethod(getPlayAudioMethod())
|
||||
.addMethod(getStreamAudioMethod())
|
||||
.addMethod(getStopPlaybackMethod())
|
||||
.addMethod(getPausePlaybackMethod())
|
||||
.addMethod(getResumePlaybackMethod())
|
||||
|
||||
@ -24,25 +24,28 @@ public final class SpeakerServiceOuterClass {
|
||||
static {
|
||||
java.lang.String[] descriptorData = {
|
||||
"\n\036cmvr/api/speaker_service.proto\022\010cmvr.a" +
|
||||
"pi\032\036cmvr/api/speaker_command.proto2\260\005\n\016S" +
|
||||
"pi\032\036cmvr/api/speaker_command.proto2\234\006\n\016S" +
|
||||
"peakerService\022`\n\tGetStatus\022(.cmvr.api.Ge" +
|
||||
"tSpeakerStateCommand.Request\032).cmvr.api." +
|
||||
"GetSpeakerStateCommand.Feedback\022T\n\tPlayA" +
|
||||
"udio\022\".cmvr.api.PlayAudioCommand.Request" +
|
||||
"\032#.cmvr.api.PlayAudioCommand.Feedback\022[\n" +
|
||||
"\014StopPlayback\022$.cmvr.api.StopSpeakerComm" +
|
||||
"and.Request\032%.cmvr.api.StopSpeakerComman" +
|
||||
"d.Feedback\022^\n\rPausePlayback\022%.cmvr.api.P" +
|
||||
"auseSpeakerCommand.Request\032&.cmvr.api.Pa" +
|
||||
"useSpeakerCommand.Feedback\022a\n\016ResumePlay" +
|
||||
"back\022&.cmvr.api.ResumeSpeakerCommand.Req" +
|
||||
"uest\032\'.cmvr.api.ResumeSpeakerCommand.Fee" +
|
||||
"dback\022b\n\tSetVolume\022).cmvr.api.SetSpeaker" +
|
||||
"VolumeCommand.Request\032*.cmvr.api.SetSpea" +
|
||||
"kerVolumeCommand.Feedback\022b\n\tGetVolume\022)" +
|
||||
".cmvr.api.GetSpeakerVolumeCommand.Reques" +
|
||||
"t\032*.cmvr.api.GetSpeakerVolumeCommand.Fee" +
|
||||
"dbackb\006proto3"
|
||||
"\032#.cmvr.api.PlayAudioCommand.Feedback\022j\n" +
|
||||
"\013StreamAudio\022+.cmvr.api.StreamSpeakerAud" +
|
||||
"ioCommand.Request\032,.cmvr.api.StreamSpeak" +
|
||||
"erAudioCommand.Feedback(\001\022[\n\014StopPlaybac" +
|
||||
"k\022$.cmvr.api.StopSpeakerCommand.Request\032" +
|
||||
"%.cmvr.api.StopSpeakerCommand.Feedback\022^" +
|
||||
"\n\rPausePlayback\022%.cmvr.api.PauseSpeakerC" +
|
||||
"ommand.Request\032&.cmvr.api.PauseSpeakerCo" +
|
||||
"mmand.Feedback\022a\n\016ResumePlayback\022&.cmvr." +
|
||||
"api.ResumeSpeakerCommand.Request\032\'.cmvr." +
|
||||
"api.ResumeSpeakerCommand.Feedback\022b\n\tSet" +
|
||||
"Volume\022).cmvr.api.SetSpeakerVolumeComman" +
|
||||
"d.Request\032*.cmvr.api.SetSpeakerVolumeCom" +
|
||||
"mand.Feedback\022b\n\tGetVolume\022).cmvr.api.Ge" +
|
||||
"tSpeakerVolumeCommand.Request\032*.cmvr.api" +
|
||||
".GetSpeakerVolumeCommand.Feedbackb\006proto" +
|
||||
"3"
|
||||
};
|
||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||
.internalBuildGeneratedFileFrom(descriptorData,
|
||||
|
||||
@ -29,6 +29,22 @@ message CommandHeader {
|
||||
}
|
||||
}
|
||||
|
||||
message AudioData {
|
||||
enum AudioFormat {
|
||||
PCM = 0;
|
||||
MP3 = 1;
|
||||
AAC = 2;
|
||||
WAV = 3;
|
||||
}
|
||||
bytes data = 1;
|
||||
int32 sample_rate = 2;
|
||||
int32 channels = 3;
|
||||
AudioFormat format = 4;
|
||||
string codec = 5;
|
||||
int64 pts = 6;
|
||||
int32 nb_samples = 7;
|
||||
}
|
||||
|
||||
message ConfigParam {
|
||||
|
||||
string param_name = 1;
|
||||
|
||||
@ -11,6 +11,7 @@ message MicState {
|
||||
int32 volume = 4;
|
||||
string error_message = 5;
|
||||
}
|
||||
|
||||
message GetMicStateCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
@ -21,6 +22,7 @@ message GetMicStateCommand {
|
||||
MicState state = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message StartMicRecordingCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
@ -59,10 +61,20 @@ message ResumeMicRecordingCommand {
|
||||
}
|
||||
}
|
||||
|
||||
message StreamMicAudioCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
AudioData audio = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SetMicPhoneVolumeCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
int32 volume = 2; // 音量值(0 ~ 100)
|
||||
int32 volume = 2;
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
@ -75,7 +87,6 @@ message GetMicPhoneVolumeCommand {
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
int32 volume = 2; // 当前音量值
|
||||
int32 volume = 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,17 +4,14 @@ import "cmvr/api/microphone_command.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
|
||||
service MicPhoneService {
|
||||
// 基本控制
|
||||
rpc GetStatus(GetMicStateCommand.Request) returns (GetMicStateCommand.Feedback);
|
||||
rpc StartRecord(StartMicRecordingCommand.Request) returns (StartMicRecordingCommand.Feedback);
|
||||
rpc StopRecord(StopMicRecordingCommand.Request) returns (StopMicRecordingCommand.Feedback);
|
||||
rpc PauseRecord(PauseMicRecordingCommand.Request) returns (PauseMicRecordingCommand.Feedback);
|
||||
rpc ResumeRecord(ResumeMicRecordingCommand.Request) returns (ResumeMicRecordingCommand.Feedback);
|
||||
rpc StreamAudio(StreamMicAudioCommand.Request) returns (stream StreamMicAudioCommand.Feedback);
|
||||
|
||||
// 音量控制
|
||||
rpc SetVolume(SetMicPhoneVolumeCommand.Request) returns (SetMicPhoneVolumeCommand.Feedback);
|
||||
rpc GetVolume(GetMicPhoneVolumeCommand.Request) returns (GetMicPhoneVolumeCommand.Feedback);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "cmvr/api/common.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
// Real-time bidirectional robot audio service.
|
||||
// Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
service RobotAudioService {
|
||||
rpc AudioTalk(stream RobotAudioRequest) returns (stream RobotAudioResponse);
|
||||
}
|
||||
|
||||
// Fixed audio format: PCM_S16LE, 48 kHz, 16-bit, mono.
|
||||
// Both directions use 10 ms frames: 480 samples, 960 bytes.
|
||||
message RobotAudioFormat {
|
||||
int32 sample_rate = 1;
|
||||
int32 channels = 2;
|
||||
int32 bits_per_sample = 3;
|
||||
string encoding = 4;
|
||||
}
|
||||
|
||||
message RobotAudioStart {
|
||||
string session_id = 1;
|
||||
string operator_id = 2;
|
||||
RobotAudioFormat format = 3;
|
||||
}
|
||||
|
||||
message RobotAudioFrame {
|
||||
string session_id = 1;
|
||||
int64 sequence = 2;
|
||||
// Robot upload: capture timestamp. Browser downlink: backend send timestamp.
|
||||
int64 timestamp_ms = 3;
|
||||
// PCM_S16LE. Fixed 10 ms / 960 bytes at 48 kHz mono 16-bit.
|
||||
bytes pcm = 4;
|
||||
}
|
||||
|
||||
message RobotAudioHeartbeat {
|
||||
string session_id = 1;
|
||||
int64 timestamp_ms = 2;
|
||||
}
|
||||
|
||||
message RobotAudioStop {
|
||||
string session_id = 1;
|
||||
string reason = 2;
|
||||
}
|
||||
|
||||
message RobotAudioError {
|
||||
string session_id = 1;
|
||||
int32 code = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
message RobotAudioStartAck {
|
||||
string session_id = 1;
|
||||
bool accepted = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
message RobotAudioRequest {
|
||||
CommandHeader.Request header = 1;
|
||||
|
||||
oneof payload {
|
||||
RobotAudioStart start = 2;
|
||||
RobotAudioFrame audio_frame = 3;
|
||||
RobotAudioHeartbeat heartbeat = 4;
|
||||
RobotAudioStop stop = 5;
|
||||
RobotAudioError error = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message RobotAudioResponse {
|
||||
CommandHeader.Feedback header = 1;
|
||||
|
||||
oneof payload {
|
||||
RobotAudioStartAck start_ack = 2;
|
||||
RobotAudioFrame audio_frame = 3;
|
||||
RobotAudioHeartbeat heartbeat = 4;
|
||||
RobotAudioStop stop = 5;
|
||||
RobotAudioError error = 6;
|
||||
}
|
||||
}
|
||||
@ -4,33 +4,15 @@ import "cmvr/api/common.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
|
||||
// 音频数据格式(与麦克风类似,但用于输出)
|
||||
message AudioData {
|
||||
enum AudioFormat {
|
||||
PCM = 0;
|
||||
MP3 = 1;
|
||||
AAC = 2;
|
||||
WAV = 3;
|
||||
}
|
||||
bytes data = 1; // 音频二进制数据
|
||||
int32 sample_rate = 2; // 采样率(Hz)
|
||||
int32 channels = 3; // 声道数
|
||||
AudioFormat format = 4; // 音频格式
|
||||
string codec = 5; // 编码方式
|
||||
}
|
||||
|
||||
// 扬声器状态
|
||||
message SpeakerState {
|
||||
bool is_initialized = 1; // 是否已初始化
|
||||
bool is_running = 2; // 是否正在播放
|
||||
bool is_initialized = 1;
|
||||
bool is_running = 2;
|
||||
bool is_decoding = 3;
|
||||
bool is_paused = 5;
|
||||
int32 volume = 6; // 当前音量(0 ~ 100)
|
||||
string error_message = 7; // 错误信息
|
||||
int32 volume = 6;
|
||||
string error_message = 7;
|
||||
}
|
||||
|
||||
// 各种扬声器命令
|
||||
message GetSpeakerStateCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
@ -47,7 +29,19 @@ message PlayAudioCommand {
|
||||
CommandHeader.Request header = 1;
|
||||
string audio_path = 2;
|
||||
}
|
||||
message Feedback { CommandHeader.Feedback header = 1; }
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message StreamSpeakerAudioCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
AudioData audio = 2;
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message StopSpeakerCommand {
|
||||
@ -80,7 +74,7 @@ message ResumeSpeakerCommand {
|
||||
message SetSpeakerVolumeCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
int32 volume = 2; // 音量值(0 ~ 100)
|
||||
int32 volume = 2;
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
@ -93,7 +87,6 @@ message GetSpeakerVolumeCommand {
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
int32 volume = 2; // 当前音量值
|
||||
int32 volume = 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,17 +4,14 @@ import "cmvr/api/speaker_command.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
|
||||
service SpeakerService {
|
||||
// 基本控制
|
||||
rpc GetStatus(GetSpeakerStateCommand.Request) returns (GetSpeakerStateCommand.Feedback);
|
||||
rpc PlayAudio(PlayAudioCommand.Request) returns (PlayAudioCommand.Feedback);
|
||||
rpc StreamAudio(stream StreamSpeakerAudioCommand.Request) returns (StreamSpeakerAudioCommand.Feedback);
|
||||
rpc StopPlayback(StopSpeakerCommand.Request) returns (StopSpeakerCommand.Feedback);
|
||||
rpc PausePlayback(PauseSpeakerCommand.Request) returns (PauseSpeakerCommand.Feedback);
|
||||
rpc ResumePlayback(ResumeSpeakerCommand.Request) returns (ResumeSpeakerCommand.Feedback);
|
||||
|
||||
// 音量控制
|
||||
rpc SetVolume(SetSpeakerVolumeCommand.Request) returns (SetSpeakerVolumeCommand.Feedback);
|
||||
rpc GetVolume(GetSpeakerVolumeCommand.Request) returns (GetSpeakerVolumeCommand.Feedback);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,346 @@
|
||||
package com.cmvr.device.service;
|
||||
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.device.domain.DeDeviceTerminalConfig;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* 巡检报警监听状态服务。
|
||||
*
|
||||
* <p>监听条件仍然按边缘端IP和报警类型匹配,同时保存工作流执行上下文,
|
||||
* 使异步上报的PPE事件能够归属到具体巡检任务、检测项和节点。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InspectionAlertListenService
|
||||
{
|
||||
private static final Set<String> SUPPORTED_EVENT_TYPES;
|
||||
|
||||
static
|
||||
{
|
||||
Set<String> values = new LinkedHashSet<>();
|
||||
values.add("No-Glove");
|
||||
values.add("No-Helmet");
|
||||
SUPPORTED_EVENT_TYPES = Collections.unmodifiableSet(values);
|
||||
}
|
||||
|
||||
private final IDeDeviceTerminalConfigService terminalConfigService;
|
||||
|
||||
/** key=grpcIp,value=当前IP下的工作流监听订阅。 */
|
||||
private final ConcurrentMap<String, ConcurrentMap<String, ListenSubscription>> subscriptions =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/** 兼容原有调用方式,创建一个不绑定工作流的监听。 */
|
||||
public ListenState startListen(String terminalId, Collection<String> eventTypes)
|
||||
{
|
||||
return startListen(terminalId, eventTypes, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public ListenState startListen(String terminalId, Collection<String> eventTypes,
|
||||
String flowInstanceId, String taskId, String itemId,
|
||||
String nodeId, String listenerKey)
|
||||
{
|
||||
String grpcIp = resolveTerminalIp(terminalId);
|
||||
Set<String> normalizedTypes = normalizeEventTypes(eventTypes, true);
|
||||
ConcurrentMap<String, ListenSubscription> target =
|
||||
subscriptions.computeIfAbsent(grpcIp, key -> new ConcurrentHashMap<>());
|
||||
|
||||
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={}",
|
||||
terminalId, grpcIp, normalizedTypes);
|
||||
return new ListenState(terminalId, grpcIp, Collections.emptyList());
|
||||
}
|
||||
|
||||
for (String eventType : normalizedTypes)
|
||||
{
|
||||
int removed = removeMatching(current, eventType, flowInstanceId, itemId, listenerKey);
|
||||
if (removed == 0)
|
||||
{
|
||||
log.warn("结束监听巡检报警时未找到匹配订阅,terminalId={},grpcIp={},flowInstanceId={},itemId={},eventType={}",
|
||||
terminalId, grpcIp, flowInstanceId, itemId, eventType);
|
||||
}
|
||||
}
|
||||
if (current.isEmpty())
|
||||
{
|
||||
subscriptions.remove(grpcIp, current);
|
||||
}
|
||||
|
||||
List<String> remainingTypes = currentTypes(current);
|
||||
log.info("结束监听巡检报警,terminalId={},grpcIp={},flowInstanceId={},itemId={},remainingTypes={}",
|
||||
terminalId, grpcIp, flowInstanceId, itemId, remainingTypes);
|
||||
return new ListenState(terminalId, grpcIp, remainingTypes);
|
||||
}
|
||||
|
||||
/** 返回命中IP和任一标签的全部活动订阅。 */
|
||||
public List<ListenSubscription> matchSubscriptions(String grpcIp, Collection<String> labels)
|
||||
{
|
||||
String normalizedIp = StringUtils.trimToNull(grpcIp);
|
||||
if (normalizedIp == null)
|
||||
{
|
||||
log.warn("忽略巡检报警:grpc_ip为空,labels={}", labels);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
ConcurrentMap<String, ListenSubscription> current = subscriptions.get(normalizedIp);
|
||||
if (current == null || current.isEmpty())
|
||||
{
|
||||
log.info("忽略巡检报警:未开启该IP的报警监听,grpcIp={},labels={}", normalizedIp, labels);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Set<String> alertTypes = normalizeEventTypes(labels, false);
|
||||
List<ListenSubscription> matched = new ArrayList<>();
|
||||
for (ListenSubscription subscription : current.values())
|
||||
{
|
||||
if (alertTypes.contains(subscription.getEventType()))
|
||||
{
|
||||
matched.add(subscription.copy());
|
||||
}
|
||||
}
|
||||
if (matched.isEmpty())
|
||||
{
|
||||
log.info("忽略巡检报警:事件类型未命中监听条件,grpcIp={},labels={},listeningTypes={}",
|
||||
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)
|
||||
{
|
||||
String grpcIp = resolveTerminalIp(terminalId);
|
||||
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)
|
||||
{
|
||||
String normalizedTerminalId = StringUtils.trimToNull(terminalId);
|
||||
if (normalizedTerminalId == null)
|
||||
{
|
||||
throw new GlobalException("terminalId不能为空");
|
||||
}
|
||||
DeDeviceTerminalConfig terminalConfig =
|
||||
terminalConfigService.selectDeDeviceTerminalConfigById(normalizedTerminalId);
|
||||
if (terminalConfig == null)
|
||||
{
|
||||
throw new GlobalException("未找到终端配置,terminalId=" + normalizedTerminalId);
|
||||
}
|
||||
String grpcIp = StringUtils.trimToNull(terminalConfig.getHost());
|
||||
if (grpcIp == null)
|
||||
{
|
||||
throw new GlobalException("终端配置host不能为空,terminalId=" + normalizedTerminalId);
|
||||
}
|
||||
return grpcIp;
|
||||
}
|
||||
|
||||
private Set<String> normalizeEventTypes(Collection<String> eventTypes, boolean failOnEmpty)
|
||||
{
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
if (eventTypes != null)
|
||||
{
|
||||
for (String eventType : eventTypes)
|
||||
{
|
||||
String normalized = normalizeEventType(eventType);
|
||||
if (normalized != null)
|
||||
{
|
||||
result.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failOnEmpty && result.isEmpty())
|
||||
{
|
||||
throw new GlobalException("eventTypes不能为空,当前支持No-Glove、No-Helmet");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String normalizeEventType(String eventType)
|
||||
{
|
||||
String value = StringUtils.trimToNull(eventType);
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (String supported : SUPPORTED_EVENT_TYPES)
|
||||
{
|
||||
if (supported.equalsIgnoreCase(value))
|
||||
{
|
||||
return supported;
|
||||
}
|
||||
}
|
||||
log.warn("忽略不支持的巡检报警类型,eventType={},supported={}", value, SUPPORTED_EVENT_TYPES);
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> currentTypes(ConcurrentMap<String, ListenSubscription> current)
|
||||
{
|
||||
if (current == null || current.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Set<String> values = new LinkedHashSet<>();
|
||||
for (ListenSubscription subscription : current.values())
|
||||
{
|
||||
values.add(subscription.getEventType());
|
||||
}
|
||||
return new ArrayList<>(values);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ListenState
|
||||
{
|
||||
private final String terminalId;
|
||||
private final String grpcIp;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,5 +29,11 @@
|
||||
<artifactId>cmvr-iot-test</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 复用终端报警监听状态,按 grpc_ip + 事件类型过滤入库 -->
|
||||
<dependency>
|
||||
<groupId>com.cmvr</groupId>
|
||||
<artifactId>cmvr-iot-device</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -40,6 +40,18 @@ public class InspectionAlarm extends BaseEntity
|
||||
@ApiModelProperty("任务执行实例ID")
|
||||
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")
|
||||
@ApiModelProperty("任务ID")
|
||||
private String taskId;
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
package com.cmvr.inspection.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 边缘 AI 推送的 PPE 违规报警主表实体。
|
||||
*/
|
||||
@Data
|
||||
@TableName("inspection_detection_alert")
|
||||
public class InspectionDetectionAlert implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.INPUT)
|
||||
private String id;
|
||||
|
||||
private String eventId;
|
||||
private String idempotencyKey;
|
||||
@TableField("schema_version")
|
||||
private String schema;
|
||||
private String sourceId;
|
||||
private String grpcIp;
|
||||
@TableField("source_sequence")
|
||||
private Long sequence;
|
||||
private Long capturedAtNs;
|
||||
private Long receivedAtNs;
|
||||
private String traceId;
|
||||
private String sessionId;
|
||||
private String inputPort;
|
||||
private String ruleId;
|
||||
private String modelId;
|
||||
private String modelName;
|
||||
private String labelsJson;
|
||||
@TableField("detection_scope")
|
||||
private String scope;
|
||||
private String scopeId;
|
||||
private Integer hitCount;
|
||||
private Double windowMs;
|
||||
private Long firstSeenNs;
|
||||
private Long lastSeenNs;
|
||||
private Long triggeredAtNs;
|
||||
private Double maxConfidence;
|
||||
private String imagePath;
|
||||
private String rawJson;
|
||||
private Date createdAt;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 边缘 AI 推送消息的统一信封。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AlertEnvelope
|
||||
{
|
||||
/** 消息结构版本,当前固定为 DetectionAlert/v1。 */
|
||||
@JsonProperty("schema")
|
||||
private String schema;
|
||||
|
||||
/** 产生消息的视频源标识。 */
|
||||
@JsonProperty("source_id")
|
||||
private String sourceId;
|
||||
|
||||
/** 边缘端 gRPC 服务 IP,用于前端展示和问题定位。 */
|
||||
@JsonProperty("grpc_ip")
|
||||
private String grpcIp;
|
||||
|
||||
/** 视频源内递增序号。 */
|
||||
@JsonProperty("sequence")
|
||||
private Long sequence;
|
||||
|
||||
/** 图像采集时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("captured_at_ns")
|
||||
private Long capturedAtNs;
|
||||
|
||||
/** 边缘端接收时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("received_at_ns")
|
||||
private Long receivedAtNs;
|
||||
|
||||
/** 全链路追踪标识。 */
|
||||
@JsonProperty("trace_id")
|
||||
private String traceId;
|
||||
|
||||
/** 边缘推理会话标识。 */
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
/** 产生消息的输入端口。 */
|
||||
@JsonProperty("input_port")
|
||||
private String inputPort;
|
||||
|
||||
/** 扩展属性。 */
|
||||
@JsonProperty("attributes")
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
/** PPE 违规报警内容。 */
|
||||
@JsonProperty("payload")
|
||||
private DetectionAlert payload;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* 报警现场图片。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AlertImage
|
||||
{
|
||||
/** 图片媒体类型,例如 image/jpeg。 */
|
||||
@JsonProperty("media_type")
|
||||
private String mediaType;
|
||||
|
||||
@JsonProperty("width")
|
||||
private Integer width;
|
||||
|
||||
@JsonProperty("height")
|
||||
private Integer height;
|
||||
|
||||
/** 编码方式,当前协议固定为 base64。 */
|
||||
@JsonProperty("encoding")
|
||||
private String encoding;
|
||||
|
||||
/** Base64 编码后的图片内容。 */
|
||||
@JsonProperty("data")
|
||||
@ToString.Exclude
|
||||
private String data;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 检测目标的像素坐标框。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class BoundingBox
|
||||
{
|
||||
@JsonProperty("x_min")
|
||||
private Double xMin;
|
||||
|
||||
@JsonProperty("y_min")
|
||||
private Double yMin;
|
||||
|
||||
@JsonProperty("x_max")
|
||||
private Double xMax;
|
||||
|
||||
@JsonProperty("y_max")
|
||||
private Double yMax;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单个 PPE 检测结果。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Detection
|
||||
{
|
||||
/** 检测标签。 */
|
||||
@JsonProperty("label")
|
||||
private String label;
|
||||
|
||||
/** 检测置信度。 */
|
||||
@JsonProperty("confidence")
|
||||
private Double confidence;
|
||||
|
||||
/** 目标框坐标。 */
|
||||
@JsonProperty("box")
|
||||
private BoundingBox box;
|
||||
|
||||
/** 跟踪目标标识。 */
|
||||
@JsonProperty("track_id")
|
||||
private Long trackId;
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PPE 违规报警业务数据。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DetectionAlert
|
||||
{
|
||||
/** 边缘端生成的全局事件标识。 */
|
||||
@JsonProperty("event_id")
|
||||
private String eventId;
|
||||
|
||||
/** 命中的规则标识。 */
|
||||
@JsonProperty("rule_id")
|
||||
private String ruleId;
|
||||
|
||||
/** 模型版本标识。 */
|
||||
@JsonProperty("model_id")
|
||||
private String modelId;
|
||||
|
||||
/** 模型显示名称。 */
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
|
||||
/** 命中的标签集合。 */
|
||||
@JsonProperty("labels")
|
||||
private List<String> labels;
|
||||
|
||||
/** 规则统计范围。 */
|
||||
@JsonProperty("scope")
|
||||
private String scope;
|
||||
|
||||
/** 规则统计范围对应的对象标识。 */
|
||||
@JsonProperty("scope_id")
|
||||
private String scopeId;
|
||||
|
||||
/** 时间窗口内命中次数。 */
|
||||
@JsonProperty("hit_count")
|
||||
private Integer hitCount;
|
||||
|
||||
/** 规则统计窗口,单位毫秒。 */
|
||||
@JsonProperty("window_ms")
|
||||
private Double windowMs;
|
||||
|
||||
/** 首次命中时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("first_seen_ns")
|
||||
private Long firstSeenNs;
|
||||
|
||||
/** 最后命中时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("last_seen_ns")
|
||||
private Long lastSeenNs;
|
||||
|
||||
/** 触发报警时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("triggered_at_ns")
|
||||
private Long triggeredAtNs;
|
||||
|
||||
/** 本次报警中的最大置信度。 */
|
||||
@JsonProperty("max_confidence")
|
||||
private Double maxConfidence;
|
||||
|
||||
/** 报警关联的检测框。 */
|
||||
@JsonProperty("detections")
|
||||
private List<Detection> detections;
|
||||
|
||||
/** 报警现场图片,允许为空。 */
|
||||
@JsonProperty("image")
|
||||
private AlertImage image;
|
||||
}
|
||||
@ -30,6 +30,18 @@ public class InspectionAlarmVo extends BaseEntity
|
||||
@ApiModelProperty("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 = "告警编码")
|
||||
@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;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.cmvr.inspection.exception;
|
||||
|
||||
/**
|
||||
* 边缘检测报警请求参数不符合协议。
|
||||
*/
|
||||
public class DetectionAlertBadRequestException extends RuntimeException
|
||||
{
|
||||
public DetectionAlertBadRequestException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DetectionAlertBadRequestException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.cmvr.inspection.exception;
|
||||
|
||||
/**
|
||||
* 数据库或对象存储暂时不可用,边缘端可稍后重试。
|
||||
*/
|
||||
public class DetectionAlertTemporaryException extends RuntimeException
|
||||
{
|
||||
public DetectionAlertTemporaryException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DetectionAlertTemporaryException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,14 @@ package com.cmvr.inspection.listener;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.cmvr.common.utils.DateUtils;
|
||||
import com.cmvr.framework.websocket.service.MessagePushService;
|
||||
import com.cmvr.device.service.InspectionAlertListenService;
|
||||
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
||||
import com.cmvr.inspection.domain.InspectionTaskLog;
|
||||
import com.cmvr.inspection.enums.InspectionLogTypeEnum;
|
||||
import com.cmvr.inspection.enums.TaskStatusEnum;
|
||||
import com.cmvr.inspection.service.IInspectionTaskInstanceService;
|
||||
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.FlowExecutionListener;
|
||||
import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes;
|
||||
@ -24,18 +26,24 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
||||
private final IInspectionTaskInstanceService inspectionTaskInstanceService;
|
||||
private final IInspectionTaskLogService inspectionTaskLogService;
|
||||
private final MessagePushService messagePushService;
|
||||
private final IInspectionResultService inspectionResultService;
|
||||
private final InspectionAlertListenService inspectionAlertListenService;
|
||||
|
||||
/** key:itemId, value:已完成节点集合 */
|
||||
private final Map<String, Set<String>> itemNodeExecutMap = new HashMap<>();
|
||||
/** key:flowInstId|itemId, value:已完成节点集合 */
|
||||
private final Map<String, Set<String>> itemNodeExecutMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
/** 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,
|
||||
IInspectionTaskLogService inspectionTaskLogService,
|
||||
MessagePushService messagePushService) {
|
||||
MessagePushService messagePushService,
|
||||
IInspectionResultService inspectionResultService,
|
||||
InspectionAlertListenService inspectionAlertListenService) {
|
||||
this.inspectionTaskInstanceService = inspectionTaskInstanceService;
|
||||
this.inspectionTaskLogService = inspectionTaskLogService;
|
||||
this.messagePushService = messagePushService;
|
||||
this.inspectionResultService = inspectionResultService;
|
||||
this.inspectionAlertListenService = inspectionAlertListenService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -52,19 +60,27 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
||||
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
|
||||
if (StrUtil.isNotBlank(itemId) && StrUtil.isNotBlank(nodeId)
|
||||
&& 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)
|
||||
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()
|
||||
.eq(InspectionTaskInstance::getTaskInsId, instId)
|
||||
.one();
|
||||
InspectionTaskInstance taskInstance = findTaskInstance(instId);
|
||||
if (taskInstance == null) {
|
||||
log.warn("未查询到巡检实例,taskInsId:{}", instId);
|
||||
return;
|
||||
@ -77,13 +93,13 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
||||
case TASK_COMPLETED:
|
||||
targetStatus = TaskStatusEnum.SUCCESS.getCode();
|
||||
needUpdateDb = true;
|
||||
clearCache(instId);
|
||||
pushCompleteMsg(taskInstance.getId(), now, targetStatus);
|
||||
break;
|
||||
case TASK_FAILED:
|
||||
targetStatus = TaskStatusEnum.FAILED.getCode();
|
||||
needUpdateDb = true;
|
||||
clearCache(instId);
|
||||
break;
|
||||
case TASK_STOPPED:
|
||||
break;
|
||||
case NODE_COMPLETED:
|
||||
default:
|
||||
@ -96,6 +112,15 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
||||
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)) {
|
||||
int waypointCount = taskInstance.getWaypointCount() == null ? 0 : taskInstance.getWaypointCount();
|
||||
@ -107,7 +132,8 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
||||
int finishedItemNum = itemTotal - 1;
|
||||
|
||||
// 当前item已完成节点数
|
||||
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(itemId, new HashSet<>());
|
||||
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(
|
||||
itemExecutionKey(instId, itemId), new HashSet<>());
|
||||
int finishedNode = finishedNodeSet.size();
|
||||
int totalNode = event.getNodeCount() == 0 ? 1 : event.getNodeCount();
|
||||
|
||||
@ -159,10 +185,35 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
||||
/** 任务结束统一清理缓存 */
|
||||
private void clearCache(String taskInsId) {
|
||||
Set<String> itemList = taskItemExecutMap.getOrDefault(taskInsId, new HashSet<>());
|
||||
itemList.forEach(itemNodeExecutMap::remove);
|
||||
itemList.forEach(itemId -> itemNodeExecutMap.remove(itemExecutionKey(taskInsId, itemId)));
|
||||
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)
|
||||
*/
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package com.cmvr.inspection.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||
|
||||
/**
|
||||
* PPE 违规报警主表 Mapper。
|
||||
*/
|
||||
public interface InspectionDetectionAlertMapper extends BaseMapper<InspectionDetectionAlert>
|
||||
{
|
||||
}
|
||||
@ -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,18 @@
|
||||
package com.cmvr.inspection.service;
|
||||
|
||||
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||
|
||||
/**
|
||||
* 边缘 AI 检测报警接收服务。
|
||||
*/
|
||||
public interface IInspectionDetectionAlertService
|
||||
{
|
||||
/**
|
||||
* 接收并持久化一条 PPE 违规报警。
|
||||
*
|
||||
* @param envelope 报警信封
|
||||
* @param requestIdempotencyKey HTTP 请求头中的幂等键,仅记录,不用于重复过滤
|
||||
* @return true 表示本次新增,false 表示重复事件
|
||||
*/
|
||||
boolean receive(AlertEnvelope envelope, String requestIdempotencyKey);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -0,0 +1,376 @@
|
||||
package com.cmvr.inspection.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.cmvr.common.config.properties.MinioProperties;
|
||||
import com.cmvr.common.core.minio.MinioService;
|
||||
import com.cmvr.device.service.InspectionAlertListenService;
|
||||
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
||||
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||
import com.cmvr.inspection.domain.dto.alert.AlertImage;
|
||||
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
|
||||
import com.cmvr.inspection.exception.DetectionAlertTemporaryException;
|
||||
import com.cmvr.inspection.mapper.InspectionDetectionAlertMapper;
|
||||
import com.cmvr.inspection.mapper.InspectionTaskInstanceMapper;
|
||||
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
|
||||
import com.cmvr.inspection.service.IInspectionResultService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PPE 违规报警接收业务实现。
|
||||
*
|
||||
* 数据库唯一索引是并发幂等的最终保障;业务预查用于减少重复请求的无效写入。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InspectionDetectionAlertServiceImpl implements IInspectionDetectionAlertService
|
||||
{
|
||||
private static final String SUPPORTED_SCHEMA = "DetectionAlert/v1";
|
||||
private static final String IMAGE_ENCODING_BASE64 = "base64";
|
||||
private static final String IMAGE_OBJECT_PREFIX = "inspection/detection-alerts";
|
||||
private static final int MAX_EVENT_ID_LENGTH = 128;
|
||||
private static final int MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
||||
private static final int MAX_BASE64_LENGTH = (MAX_IMAGE_BYTES * 4 / 3) + 8;
|
||||
|
||||
private final InspectionDetectionAlertMapper alertMapper;
|
||||
private final MinioService minioService;
|
||||
private final MinioProperties minioProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final InspectionAlertListenService inspectionAlertListenService;
|
||||
private final InspectionTaskInstanceMapper taskInstanceMapper;
|
||||
private final IInspectionResultService inspectionResultService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean receive(AlertEnvelope envelope, String requestIdempotencyKey)
|
||||
{
|
||||
String eventId = validateAndResolveEventId(envelope);
|
||||
String idempotencyKey = trimToNull(requestIdempotencyKey);
|
||||
|
||||
// 工作流未开启对应 IP + 事件类型监听时,报警只确认接收,不做持久化。
|
||||
List<InspectionAlertListenService.ListenSubscription> matchedSubscriptions =
|
||||
inspectionAlertListenService.matchSubscriptions(
|
||||
envelope.getGrpcIp(), envelope.getPayload().getLabels());
|
||||
if (matchedSubscriptions.isEmpty())
|
||||
{
|
||||
log.info("忽略未监听的PPE报警,eventId={},grpcIp={},labels={}",
|
||||
eventId, envelope.getGrpcIp(), envelope.getPayload().getLabels());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exists(eventId))
|
||||
{
|
||||
log.info("忽略重复PPE报警,eventId={}", eventId);
|
||||
return false;
|
||||
}
|
||||
|
||||
InspectionDetectionAlert alert = buildAlert(envelope, idempotencyKey);
|
||||
StoredImage storedImage = null;
|
||||
try
|
||||
{
|
||||
alertMapper.insert(alert);
|
||||
|
||||
AlertImage image = envelope.getPayload().getImage();
|
||||
if (image != null)
|
||||
{
|
||||
storedImage = uploadImage(alert.getId(), image);
|
||||
alert.setImagePath(storedImage.url);
|
||||
if (alertMapper.updateById(alert) != 1)
|
||||
{
|
||||
throw new DetectionAlertTemporaryException("更新报警图片地址失败");
|
||||
}
|
||||
}
|
||||
|
||||
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={}",
|
||||
alert.getId(), eventId, envelope.getSourceId());
|
||||
return true;
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
// 数据库事务无法回滚 MinIO,对后续步骤失败时主动删除已上传对象。
|
||||
if (storedImage != null)
|
||||
{
|
||||
deleteUploadedImageQuietly(storedImage.objectName);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
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。
|
||||
*/
|
||||
private String validateAndResolveEventId(AlertEnvelope envelope)
|
||||
{
|
||||
if (envelope == null)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("请求体不能为空");
|
||||
}
|
||||
if (!SUPPORTED_SCHEMA.equals(envelope.getSchema()))
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("schema必须为" + SUPPORTED_SCHEMA);
|
||||
}
|
||||
if (envelope.getPayload() == null)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload不能为空");
|
||||
}
|
||||
|
||||
String eventId = trimToNull(envelope.getPayload().getEventId());
|
||||
if (eventId == null)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.event_id不能为空");
|
||||
}
|
||||
if (eventId.length() > MAX_EVENT_ID_LENGTH)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.event_id长度不能超过" + MAX_EVENT_ID_LENGTH);
|
||||
}
|
||||
|
||||
validateImage(envelope.getPayload().getImage());
|
||||
return eventId;
|
||||
}
|
||||
|
||||
private void validateImage(AlertImage image)
|
||||
{
|
||||
if (image == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!IMAGE_ENCODING_BASE64.equalsIgnoreCase(trimToEmpty(image.getEncoding())))
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.image.encoding必须为base64");
|
||||
}
|
||||
if (StringUtils.isBlank(image.getData()))
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.image.data不能为空");
|
||||
}
|
||||
if (image.getData().length() > MAX_BASE64_LENGTH)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("报警图片不能超过10MB");
|
||||
}
|
||||
String mediaType = trimToNull(image.getMediaType());
|
||||
if (mediaType == null || !mediaType.toLowerCase(Locale.ROOT).startsWith("image/"))
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.image.media_type必须是图片类型");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean exists(String eventId)
|
||||
{
|
||||
LambdaQueryWrapper<InspectionDetectionAlert> query = new LambdaQueryWrapper<>();
|
||||
query.eq(InspectionDetectionAlert::getEventId, eventId);
|
||||
return alertMapper.selectCount(query) > 0;
|
||||
}
|
||||
|
||||
private InspectionDetectionAlert buildAlert(AlertEnvelope envelope, String idempotencyKey)
|
||||
{
|
||||
com.cmvr.inspection.domain.dto.alert.DetectionAlert payload = envelope.getPayload();
|
||||
InspectionDetectionAlert alert = new InspectionDetectionAlert();
|
||||
alert.setId(newId());
|
||||
alert.setEventId(trimToNull(payload.getEventId()));
|
||||
alert.setIdempotencyKey(idempotencyKey);
|
||||
alert.setSchema(envelope.getSchema());
|
||||
alert.setSourceId(envelope.getSourceId());
|
||||
alert.setGrpcIp(envelope.getGrpcIp());
|
||||
alert.setSequence(envelope.getSequence());
|
||||
alert.setCapturedAtNs(envelope.getCapturedAtNs());
|
||||
alert.setReceivedAtNs(envelope.getReceivedAtNs());
|
||||
alert.setTraceId(envelope.getTraceId());
|
||||
alert.setSessionId(envelope.getSessionId());
|
||||
alert.setInputPort(envelope.getInputPort());
|
||||
alert.setRuleId(payload.getRuleId());
|
||||
alert.setModelId(payload.getModelId());
|
||||
alert.setModelName(payload.getModelName());
|
||||
alert.setLabelsJson(writeJson(payload.getLabels(), "序列化labels失败"));
|
||||
alert.setScope(payload.getScope());
|
||||
alert.setScopeId(payload.getScopeId());
|
||||
alert.setHitCount(payload.getHitCount());
|
||||
alert.setWindowMs(payload.getWindowMs());
|
||||
alert.setFirstSeenNs(payload.getFirstSeenNs());
|
||||
alert.setLastSeenNs(payload.getLastSeenNs());
|
||||
alert.setTriggeredAtNs(payload.getTriggeredAtNs());
|
||||
alert.setMaxConfidence(payload.getMaxConfidence());
|
||||
alert.setRawJson(buildSanitizedRawJson(envelope));
|
||||
alert.setCreatedAt(new Date());
|
||||
return alert;
|
||||
}
|
||||
|
||||
/**
|
||||
* raw_json保留协议元数据,但明确移除Base64图片,避免图片以另一种形式进入数据库。
|
||||
*/
|
||||
private String buildSanitizedRawJson(AlertEnvelope envelope)
|
||||
{
|
||||
AlertImage image = envelope.getPayload().getImage();
|
||||
String imageData = image == null ? null : image.getData();
|
||||
try
|
||||
{
|
||||
// 避免构建JSON树时复制一份大Base64字符串;finally中恢复仅供随后上传MinIO。
|
||||
if (image != null)
|
||||
{
|
||||
image.setData(null);
|
||||
}
|
||||
return objectMapper.writeValueAsString(envelope);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DetectionAlertTemporaryException("序列化报警原始数据失败", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (image != null)
|
||||
{
|
||||
image.setData(imageData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String writeJson(Object value, String errorMessage)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return objectMapper.writeValueAsString(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DetectionAlertTemporaryException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private StoredImage uploadImage(String alertId, AlertImage image)
|
||||
{
|
||||
final byte[] imageBytes;
|
||||
try
|
||||
{
|
||||
imageBytes = Base64.getDecoder().decode(image.getData().getBytes(StandardCharsets.US_ASCII));
|
||||
}
|
||||
catch (IllegalArgumentException ex)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.image.data不是有效的Base64", ex);
|
||||
}
|
||||
if (imageBytes.length > MAX_IMAGE_BYTES)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("报警图片不能超过10MB");
|
||||
}
|
||||
|
||||
String extension = extensionFor(image.getMediaType());
|
||||
String datePath = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
|
||||
String objectName = IMAGE_OBJECT_PREFIX + "/" + datePath + "/" + alertId + extension;
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes))
|
||||
{
|
||||
minioService.uploadStream(minioProperties.getBucketName(), objectName, inputStream,
|
||||
imageBytes.length, image.getMediaType());
|
||||
return new StoredImage(objectName, buildMinioUrl(objectName));
|
||||
}
|
||||
catch (DetectionAlertBadRequestException ex)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DetectionAlertTemporaryException("上传报警图片到MinIO失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildMinioUrl(String objectName)
|
||||
{
|
||||
String baseUrl = StringUtils.removeEnd(minioProperties.getUrl(), "/");
|
||||
return baseUrl + "/" + minioProperties.getBucketName() + "/" + objectName;
|
||||
}
|
||||
|
||||
private void deleteUploadedImageQuietly(String objectName)
|
||||
{
|
||||
try
|
||||
{
|
||||
minioService.deleteFile(minioProperties.getBucketName(), objectName);
|
||||
}
|
||||
catch (Exception cleanupEx)
|
||||
{
|
||||
log.error("回滚PPE报警时删除MinIO对象失败,objectName={}", objectName, cleanupEx);
|
||||
}
|
||||
}
|
||||
|
||||
private String extensionFor(String mediaType)
|
||||
{
|
||||
String normalized = trimToEmpty(mediaType).toLowerCase(Locale.ROOT);
|
||||
if ("image/jpeg".equals(normalized) || "image/jpg".equals(normalized))
|
||||
{
|
||||
return ".jpg";
|
||||
}
|
||||
if ("image/png".equals(normalized))
|
||||
{
|
||||
return ".png";
|
||||
}
|
||||
if ("image/webp".equals(normalized))
|
||||
{
|
||||
return ".webp";
|
||||
}
|
||||
// 未知图片类型使用无执行语义的后缀,实际Content-Type仍按协议上传。
|
||||
return ".img";
|
||||
}
|
||||
|
||||
private String trimToNull(String value)
|
||||
{
|
||||
return StringUtils.trimToNull(value);
|
||||
}
|
||||
|
||||
private String trimToEmpty(String value)
|
||||
{
|
||||
return StringUtils.trimToEmpty(value);
|
||||
}
|
||||
|
||||
private String newId()
|
||||
{
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
private static final class StoredImage
|
||||
{
|
||||
private final String objectName;
|
||||
private final String url;
|
||||
|
||||
private StoredImage(String objectName, String url)
|
||||
{
|
||||
this.objectName = objectName;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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="alarmCode" column="alarm_code" />
|
||||
<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="taskName" column="task_name" />
|
||||
<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="alarmCode" column="alarm_code" />
|
||||
<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="taskName" column="task_name" />
|
||||
<result property="robotId" column="robot_id" />
|
||||
@ -59,7 +67,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<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>
|
||||
|
||||
<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="alarmCode != null">alarm_code,</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="taskName != null">task_name,</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="alarmCode != null">#{alarmCode},</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="taskName != null">#{taskName},</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="alarmCode != null">alarm_code = #{alarmCode},</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="taskName != null">task_name = #{taskName},</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 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_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
|
||||
|
||||
@ -35,6 +35,12 @@
|
||||
<artifactId>cmvr-iot-grpc-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 设备终端配置,用于工作流报警监听组件按终端ID解析边缘端IP -->
|
||||
<dependency>
|
||||
<groupId>com.cmvr</groupId>
|
||||
<artifactId>cmvr-iot-device</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.graalvm.js</groupId>
|
||||
<artifactId>js</artifactId>
|
||||
@ -47,4 +53,4 @@
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@ -74,12 +74,18 @@ public enum ActionEnum {
|
||||
INTENT_RECOGNITION("LLM", "INTENT_RECOGNITION", "意图识别"),
|
||||
GENERATE_ADVANCED_AUDIO("LLM", "GENERATE_ADVANCED_AUDIO", "tts语音合成"),
|
||||
AI_AGENT_PLATFORM("LLM", "AI_AGENT_PLATFORM", "商道智能体"),
|
||||
INSPECTION_METER_RECOGNIZE("LLM", "INSPECTION_METER_RECOGNIZE", "巡检仪表读数识别"),
|
||||
AI_TTS("LLM", "AI_TTS", "tts语音播放"),
|
||||
GET_CURRENT_PAGE("LLM", "GET_CURRENT_PAGE", "获取当前页面名称"),
|
||||
|
||||
// 触控交互
|
||||
TI_PATH_SEARCH("EDGE", "TI_PATH_SEARCH", "路径搜索"),
|
||||
TI_TOUCH_COORDINATES("EDGE", "TI_TOUCH_COORDINATES", "获取触控二维坐标")
|
||||
TI_TOUCH_COORDINATES("EDGE", "TI_TOUCH_COORDINATES", "获取触控二维坐标"),
|
||||
|
||||
// 巡检报警
|
||||
INSPECTION_ALERT_LISTEN_START("EDGE", "INSPECTION_ALERT_LISTEN_START", "开始监听报警事件"),
|
||||
INSPECTION_ALERT_LISTEN_STOP("EDGE", "INSPECTION_ALERT_LISTEN_STOP", "结束监听报警事件"),
|
||||
INSPECTION_MANUAL_REVIEW_CREATE("EDGE", "INSPECTION_MANUAL_REVIEW_CREATE", "创建人工巡检判断任务")
|
||||
;
|
||||
|
||||
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
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 lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流程执行事件。
|
||||
* <p>
|
||||
@ -62,6 +66,26 @@ public class FlowExecutionEvent {
|
||||
*/
|
||||
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.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Slf4j
|
||||
@ -55,6 +56,11 @@ public class FlowAfterInterceptor extends AbstractFlowMsgPreInterceptor{
|
||||
.nodeId(message.getNodeId())
|
||||
.nodeCount(message.getGraph().allNodeIds().size())
|
||||
.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)
|
||||
.build();
|
||||
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
package com.cmvr.test.flow.runtime.operator.edge;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.cmvr.common.exception.GlobalException;
|
||||
import com.cmvr.device.service.InspectionAlertListenService;
|
||||
import com.cmvr.test.enums.ActionEnum;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 巡检报警监听工作流组件。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EdgeInspectionAlertOperateService implements EdgeOperateService
|
||||
{
|
||||
private final InspectionAlertListenService inspectionAlertListenService;
|
||||
|
||||
@Override
|
||||
public boolean supports(ActionEnum action)
|
||||
{
|
||||
return ActionEnum.INSPECTION_ALERT_LISTEN_START.equals(action)
|
||||
|| ActionEnum.INSPECTION_ALERT_LISTEN_STOP.equals(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message)
|
||||
{
|
||||
JSONObject inputParams = message.getInputParams() == null ? new JSONObject() : message.getInputParams();
|
||||
String terminalId = StringUtils.defaultIfBlank(inputParams.getString("terminalId"), message.getTerminalId());
|
||||
Collection<String> eventTypes = resolveEventTypes(inputParams);
|
||||
String listenerKey = inputParams.getString("listenerKey");
|
||||
|
||||
InspectionAlertListenService.ListenState state;
|
||||
switch (message.getAction())
|
||||
{
|
||||
case INSPECTION_ALERT_LISTEN_START:
|
||||
state = inspectionAlertListenService.startListen(terminalId, eventTypes,
|
||||
message.getInstId(), message.getTaskId(), message.getItemId(),
|
||||
message.getNodeId(), listenerKey);
|
||||
break;
|
||||
case INSPECTION_ALERT_LISTEN_STOP:
|
||||
state = inspectionAlertListenService.stopListen(terminalId, eventTypes,
|
||||
message.getInstId(), message.getItemId(), listenerKey);
|
||||
break;
|
||||
default:
|
||||
throw new GlobalException("不支持的巡检报警监听动作: " + message.getAction());
|
||||
}
|
||||
|
||||
JSONObject output = new JSONObject();
|
||||
output.put("terminalId", state.getTerminalId());
|
||||
output.put("grpcIp", state.getGrpcIp());
|
||||
output.put("eventTypes", state.getEventTypes());
|
||||
return TaskNodeExecuteResult.success(output);
|
||||
}
|
||||
|
||||
private Collection<String> resolveEventTypes(JSONObject inputParams)
|
||||
{
|
||||
List<String> eventTypes = new ArrayList<>();
|
||||
|
||||
Object rawEventTypes = inputParams.get("eventTypes");
|
||||
if (rawEventTypes instanceof JSONArray)
|
||||
{
|
||||
JSONArray array = (JSONArray) rawEventTypes;
|
||||
for (int i = 0; i < array.size(); i++)
|
||||
{
|
||||
eventTypes.add(array.getString(i));
|
||||
}
|
||||
}
|
||||
else if (rawEventTypes instanceof Collection)
|
||||
{
|
||||
for (Object item : (Collection<?>) rawEventTypes)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
eventTypes.add(String.valueOf(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (rawEventTypes != null)
|
||||
{
|
||||
addDelimited(eventTypes, String.valueOf(rawEventTypes));
|
||||
}
|
||||
|
||||
addDelimited(eventTypes, inputParams.getString("eventType"));
|
||||
return eventTypes;
|
||||
}
|
||||
|
||||
private void addDelimited(List<String> eventTypes, String value)
|
||||
{
|
||||
if (StringUtils.isBlank(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String[] split = value.split(",");
|
||||
for (String item : split)
|
||||
{
|
||||
eventTypes.add(StringUtils.trim(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.text.StrPool;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
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.utils.http.CallAPIUtil;
|
||||
import com.cmvr.llm.config.APIProperties;
|
||||
import com.cmvr.llm.config.AgentConfig;
|
||||
import com.cmvr.llm.util.LargeModelFileUploadUtil;
|
||||
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.flow.runtime.operator.edge.EdgeOperateService;
|
||||
import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
|
||||
import com.cmvr.test.service.ShangdaoFileService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -34,9 +29,8 @@ import java.util.Map;
|
||||
public class TiTouchOperateService implements EdgeOperateService {
|
||||
|
||||
private final ExTiVehicleFunctionService exTiVehicleFunctionService;
|
||||
private final MinioService minioService;
|
||||
private final MinioProperties minioProps;
|
||||
private final APIProperties apiProperties;
|
||||
private final ShangdaoFileService shangdaoFileService;
|
||||
|
||||
// 设置运动模式
|
||||
public static final List<String> DRIVE_MODE_PATH = Arrays.asList("主页", "设置", "驾驶模式", "运动模式");
|
||||
@ -81,11 +75,11 @@ public class TiTouchOperateService implements EdgeOperateService {
|
||||
case TI_TOUCH_COORDINATES: {
|
||||
// 页面图片(相机)
|
||||
String imageUrl = inputParams.getString("imageUrl");
|
||||
String pageImageUrl = uploadImageFromMinio(imageUrl);
|
||||
String pageImageUrl = shangdaoFileService.uploadFromMinio(imageUrl);
|
||||
|
||||
// icon图片
|
||||
String iconUrl = inputParams.getString("iconUrl");
|
||||
String iconImageUrl = uploadImageFromMinio(iconUrl);
|
||||
String iconImageUrl = shangdaoFileService.uploadFromMinio(iconUrl);
|
||||
|
||||
// 获取触控坐标
|
||||
String touchCoordinates = getTouchCoordinates(pageImageUrl, iconImageUrl);
|
||||
@ -109,10 +103,10 @@ public class TiTouchOperateService implements EdgeOperateService {
|
||||
}
|
||||
|
||||
public String test(String imageUrl,String iconUrl) {
|
||||
String pageImageUrl = uploadImageFromMinio(imageUrl);
|
||||
String pageImageUrl = shangdaoFileService.uploadFromMinio(imageUrl);
|
||||
|
||||
// icon图片
|
||||
String iconImageUrl = uploadImageFromMinio(iconUrl);
|
||||
String iconImageUrl = shangdaoFileService.uploadFromMinio(iconUrl);
|
||||
|
||||
// 获取触控坐标
|
||||
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) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
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;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 工作流节点执行请求
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "FlowActionRequestVO", description = "工作流单节点试执行请求")
|
||||
public class FlowActionRequestVO {
|
||||
|
||||
/**
|
||||
* 动作
|
||||
*/
|
||||
@ApiModelProperty(value = "动作编码。巡检新增动作:INSPECTION_METER_RECOGNIZE、INSPECTION_MANUAL_REVIEW_CREATE",
|
||||
required = true, example = "INSPECTION_METER_RECOGNIZE")
|
||||
private String action;
|
||||
|
||||
/**
|
||||
* 执行参数
|
||||
*/
|
||||
@ApiModelProperty(value = "节点参数。仪表识别参见InspectionMeterRecognizeConfigVO,人工判断参见InspectionManualReviewConfigVO",
|
||||
required = true)
|
||||
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;
|
||||
}
|
||||
@ -14,8 +14,13 @@ import com.cmvr.edge.client.service.EdgeHlcService;
|
||||
import com.cmvr.edge.client.service.EdgeMicrophoneService;
|
||||
import com.cmvr.edge.client.service.EdgeSpeakerService;
|
||||
import com.cmvr.edge.client.service.EdgeAgvService;
|
||||
import com.cmvr.edge.client.service.EdgeArmService;
|
||||
import com.cmvr.device.service.InspectionAlertListenService;
|
||||
import com.cmvr.llm.service.LLMAiAgentPlatformService;
|
||||
import com.cmvr.test.enums.ActionEnum;
|
||||
import com.cmvr.test.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 lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -32,6 +37,10 @@ public class FlowActionExecutorService {
|
||||
private final EdgeHlcService edgeHlcService;
|
||||
private final LLMAiAgentPlatformService llmAiAgentPlatformService;
|
||||
private final EdgeAgvService edgeAgvService;
|
||||
private final InspectionAlertListenService inspectionAlertListenService;
|
||||
private final EdgeArmService edgeArmService;
|
||||
private final EdgeManualInspectionOperateService edgeManualInspectionOperateService;
|
||||
private final InspectionMeterRecognizeOperateService inspectionMeterRecognizeOperateService;
|
||||
|
||||
public String actionExecute(FlowActionRequestVO req) {
|
||||
|
||||
@ -58,6 +67,16 @@ public class FlowActionExecutorService {
|
||||
String terminalId = payload.getString("terminalId");
|
||||
String deviceId = payload.getString("deviceId");
|
||||
|
||||
// 巡检报警监听组件只需要终端ID和事件类型,不需要设备ID。
|
||||
if (ActionEnum.INSPECTION_ALERT_LISTEN_START.equals(action)
|
||||
|| ActionEnum.INSPECTION_ALERT_LISTEN_STOP.equals(action)) {
|
||||
return executeInspectionAlertAction(action, payload);
|
||||
}
|
||||
if (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)) {
|
||||
throw new GlobalException("EDGE 类型动作必须提供 terminalId 和 deviceId");
|
||||
}
|
||||
@ -89,6 +108,10 @@ public class FlowActionExecutorService {
|
||||
EdgeTouchVO edgeTouchVO = payload.to(EdgeTouchVO.class);
|
||||
return edgeHlcService.touch(edgeTouchVO);
|
||||
|
||||
// ==== 机械臂 ====
|
||||
case ARM_MOVE_TO_POINT:
|
||||
return executeArmMoveToPoint(edgeCommonVO, payload);
|
||||
|
||||
// ==== 语料 ====
|
||||
case VI_PLAY_CORPUS:
|
||||
String audioPath = payload.getString("audioPath");
|
||||
@ -145,6 +168,83 @@ public class FlowActionExecutorService {
|
||||
}
|
||||
}
|
||||
|
||||
private String executeArmMoveToPoint(EdgeCommonVO edgeCommonVO, JSONObject payload) {
|
||||
Double x = payload.getDouble("x");
|
||||
Double y = payload.getDouble("y");
|
||||
Double z = payload.getDouble("z");
|
||||
Double rx = payload.getDouble("rx");
|
||||
Double ry = payload.getDouble("ry");
|
||||
Double rz = payload.getDouble("rz");
|
||||
String frame = payload.getString("frame");
|
||||
Double velocity = payload.getDouble("velocity");
|
||||
Double acceleration = payload.getDouble("acceleration");
|
||||
Double blendRadius = payload.getDouble("blendRadius");
|
||||
|
||||
if (x == null || y == null || z == null) {
|
||||
throw new GlobalException("X、Y、Z坐标不能为空");
|
||||
}
|
||||
if (rx == null || ry == null || rz == null) {
|
||||
throw new GlobalException("RX、RY、RZ旋转角度不能为空");
|
||||
}
|
||||
|
||||
// 单节点执行和正式工作流保持一致:先开启力矩,再下发笛卡尔直线运动。
|
||||
edgeArmService.torqueOn(edgeCommonVO);
|
||||
edgeArmService.moveL(
|
||||
edgeCommonVO,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
rx,
|
||||
ry,
|
||||
rz,
|
||||
frame,
|
||||
velocity,
|
||||
acceleration,
|
||||
blendRadius
|
||||
);
|
||||
return "机械臂末端运动任务下发成功";
|
||||
}
|
||||
|
||||
private String executeInspectionAlertAction(ActionEnum action, JSONObject payload) {
|
||||
String terminalId = payload.getString("terminalId");
|
||||
java.util.List<String> types = resolveInspectionAlertEventTypes(payload);
|
||||
|
||||
InspectionAlertListenService.ListenState state;
|
||||
if (ActionEnum.INSPECTION_ALERT_LISTEN_START.equals(action)) {
|
||||
state = inspectionAlertListenService.startListen(terminalId, types);
|
||||
} else {
|
||||
state = inspectionAlertListenService.stopListen(terminalId, types);
|
||||
}
|
||||
return JSONObject.toJSONString(state);
|
||||
}
|
||||
|
||||
private java.util.List<String> resolveInspectionAlertEventTypes(JSONObject payload) {
|
||||
java.util.List<String> types = new java.util.ArrayList<>();
|
||||
Object eventTypes = payload.get("eventTypes");
|
||||
|
||||
if (eventTypes instanceof Iterable) {
|
||||
for (Object item : (Iterable<?>) eventTypes) {
|
||||
if (item != null) {
|
||||
types.add(String.valueOf(item));
|
||||
}
|
||||
}
|
||||
} else if (eventTypes != null) {
|
||||
addInspectionAlertEventTypes(types, String.valueOf(eventTypes));
|
||||
}
|
||||
|
||||
addInspectionAlertEventTypes(types, payload.getString("eventType"));
|
||||
return types;
|
||||
}
|
||||
|
||||
private void addInspectionAlertEventTypes(java.util.List<String> types, String value) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return;
|
||||
}
|
||||
for (String item : value.split(",")) {
|
||||
types.add(item.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private String executeLLMAction(ActionEnum action, FlowActionRequestVO req) {
|
||||
log.info("LLM 执行动作: {}", action);
|
||||
|
||||
@ -157,9 +257,19 @@ public class FlowActionExecutorService {
|
||||
return "LLM 高级音频生成 OK";
|
||||
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();
|
||||
case INSPECTION_METER_RECOGNIZE:
|
||||
return inspectionMeterRecognizeOperateService.execute(
|
||||
buildSingleNodeMessage(action, req.getPayload())).getOutputParams().toJSONString();
|
||||
default:
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
74
sql/inspection_detection_alert.sql
Normal file
74
sql/inspection_detection_alert.sql
Normal file
@ -0,0 +1,74 @@
|
||||
-- PPE违规报警接收表。
|
||||
-- 本脚本用于已有数据库增量部署,不会删除现有巡检数据。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `inspection_detection_alert` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||
`event_id` varchar(128) NOT NULL COMMENT '边缘端事件ID,用于重复过滤',
|
||||
`idempotency_key` varchar(191) DEFAULT NULL COMMENT 'HTTP幂等键,仅记录',
|
||||
`schema_version` varchar(64) NOT NULL COMMENT '消息结构版本',
|
||||
`source_id` varchar(128) DEFAULT NULL COMMENT '视频源标识',
|
||||
`grpc_ip` varchar(64) DEFAULT NULL COMMENT '边缘端gRPC服务IP',
|
||||
`source_sequence` bigint DEFAULT NULL COMMENT '视频源内消息序号',
|
||||
`captured_at_ns` bigint DEFAULT NULL COMMENT '采集时间Unix纳秒',
|
||||
`received_at_ns` bigint DEFAULT NULL COMMENT '边缘端接收时间Unix纳秒',
|
||||
`trace_id` varchar(128) DEFAULT NULL COMMENT '链路追踪ID',
|
||||
`session_id` varchar(128) DEFAULT NULL COMMENT '边缘推理会话ID',
|
||||
`input_port` varchar(128) DEFAULT NULL COMMENT '消息输入端口',
|
||||
`rule_id` varchar(128) DEFAULT NULL COMMENT '命中的规则ID',
|
||||
`model_id` varchar(255) DEFAULT NULL COMMENT '模型版本ID',
|
||||
`model_name` varchar(255) DEFAULT NULL COMMENT '模型名称',
|
||||
`labels_json` text COMMENT '命中标签JSON',
|
||||
`detection_scope` varchar(64) DEFAULT NULL COMMENT '规则统计范围',
|
||||
`scope_id` varchar(128) DEFAULT NULL COMMENT '统计范围对象ID',
|
||||
`hit_count` int DEFAULT NULL COMMENT '窗口内命中次数',
|
||||
`window_ms` decimal(12,3) DEFAULT NULL COMMENT '统计窗口毫秒',
|
||||
`first_seen_ns` bigint DEFAULT NULL COMMENT '首次命中时间Unix纳秒',
|
||||
`last_seen_ns` bigint DEFAULT NULL COMMENT '最后命中时间Unix纳秒',
|
||||
`triggered_at_ns` bigint DEFAULT NULL COMMENT '报警触发时间Unix纳秒',
|
||||
`max_confidence` decimal(8,6) DEFAULT NULL COMMENT '最大置信度',
|
||||
`image_path` varchar(1000) DEFAULT NULL COMMENT 'MinIO图片URL',
|
||||
`raw_json` mediumtext COMMENT '已移除Base64图片数据的原始协议JSON',
|
||||
`created_at` datetime(3) NOT NULL COMMENT '平台接收时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_detection_alert_event_id` (`event_id`),
|
||||
KEY `idx_detection_alert_source_id` (`source_id`),
|
||||
KEY `idx_detection_alert_rule_id` (`rule_id`),
|
||||
KEY `idx_detection_alert_triggered_at` (`triggered_at_ns`),
|
||||
KEY `idx_detection_alert_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检PPE违规报警主表';
|
||||
|
||||
SET @exist_grpc_ip := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'inspection_detection_alert'
|
||||
AND column_name = 'grpc_ip'
|
||||
);
|
||||
SET @add_grpc_ip_sql := IF(
|
||||
@exist_grpc_ip = 0,
|
||||
'ALTER TABLE `inspection_detection_alert` ADD COLUMN `grpc_ip` varchar(64) DEFAULT NULL COMMENT ''边缘端gRPC服务IP'' AFTER `source_id`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE add_grpc_ip_stmt FROM @add_grpc_ip_sql;
|
||||
EXECUTE add_grpc_ip_stmt;
|
||||
DEALLOCATE PREPARE add_grpc_ip_stmt;
|
||||
|
||||
SET @exist_idempotency_unique := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'inspection_detection_alert'
|
||||
AND index_name = 'uk_detection_alert_idempotency_key'
|
||||
);
|
||||
SET @drop_idempotency_unique_sql := IF(
|
||||
@exist_idempotency_unique > 0,
|
||||
'ALTER TABLE `inspection_detection_alert` DROP INDEX `uk_detection_alert_idempotency_key`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE drop_idempotency_unique_stmt FROM @drop_idempotency_unique_sql;
|
||||
EXECUTE drop_idempotency_unique_stmt;
|
||||
DEALLOCATE PREPARE drop_idempotency_unique_stmt;
|
||||
|
||||
ALTER TABLE `inspection_detection_alert`
|
||||
MODIFY COLUMN `event_id` varchar(128) NOT NULL COMMENT '边缘端事件ID,用于重复过滤',
|
||||
MODIFY COLUMN `idempotency_key` varchar(191) DEFAULT NULL COMMENT 'HTTP幂等键,仅记录';
|
||||
@ -124,6 +124,10 @@ CREATE TABLE `inspection_alarm` (
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`alarm_code` varchar(64) DEFAULT NULL COMMENT '告警编码',
|
||||
`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_name` varchar(100) DEFAULT NULL COMMENT '任务名称',
|
||||
`robot_id` varchar(64) DEFAULT NULL COMMENT '机器人ID',
|
||||
@ -141,6 +145,7 @@ CREATE TABLE `inspection_alarm` (
|
||||
`evidence_image` varchar(500) DEFAULT NULL COMMENT '图片证据',
|
||||
PRIMARY KEY (`id`),
|
||||
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_id` (`task_id`),
|
||||
KEY `idx_robot_id` (`robot_id`),
|
||||
@ -177,3 +182,64 @@ CREATE TABLE `inspection_task_log` (
|
||||
KEY `idx_log_time` (`log_time`)
|
||||
) 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