Compare commits
No commits in common. "521e3e4e243f4840f532051d3c6ddb70ea49e35f" and "152cae03231196de019a0d9b9fb61053c7d35575" have entirely different histories.
521e3e4e24
...
152cae0323
@ -6,10 +6,12 @@ import com.cmvr.edge.client.model.microphone.EdgeMicrophoneVolumeVO;
|
|||||||
import com.cmvr.edge.client.service.EdgeMicrophoneService;
|
import com.cmvr.edge.client.service.EdgeMicrophoneService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import io.swagger.annotations.ApiParam;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 麦克风管理
|
* 麦克风管理
|
||||||
@ -25,17 +27,14 @@ public class EdgeMicrophoneController {
|
|||||||
@ApiOperation("获取麦克风状态")
|
@ApiOperation("获取麦克风状态")
|
||||||
@PreAuthorize("@ss.hasPermi('system:microphone:query')")
|
@PreAuthorize("@ss.hasPermi('system:microphone:query')")
|
||||||
@GetMapping("/status")
|
@GetMapping("/status")
|
||||||
public AjaxResult getStatus(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
|
public AjaxResult getStatus(EdgeCommonVO edgeCommonVO) {
|
||||||
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
|
|
||||||
edgeCommonVO.setTerminalId(terminalId);
|
|
||||||
edgeCommonVO.setDeviceId(deviceId);
|
|
||||||
return AjaxResult.success(microphoneService.getStatus(edgeCommonVO));
|
return AjaxResult.success(microphoneService.getStatus(edgeCommonVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation("开始录音")
|
@ApiOperation("开始录音")
|
||||||
@PreAuthorize("@ss.hasPermi('system:microphone:record')")
|
@PreAuthorize("@ss.hasPermi('system:microphone:record')")
|
||||||
@PostMapping("/start")
|
@PostMapping("/start")
|
||||||
public AjaxResult startRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
|
public AjaxResult startRecord(EdgeCommonVO edgeCommonVO) {
|
||||||
String filePath = microphoneService.startRecord(edgeCommonVO);
|
String filePath = microphoneService.startRecord(edgeCommonVO);
|
||||||
return AjaxResult.success(filePath);
|
return AjaxResult.success(filePath);
|
||||||
}
|
}
|
||||||
@ -43,7 +42,7 @@ public class EdgeMicrophoneController {
|
|||||||
@ApiOperation("停止录音")
|
@ApiOperation("停止录音")
|
||||||
@PreAuthorize("@ss.hasPermi('system:microphone:stop')")
|
@PreAuthorize("@ss.hasPermi('system:microphone:stop')")
|
||||||
@PostMapping("/stop")
|
@PostMapping("/stop")
|
||||||
public AjaxResult stopRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
|
public AjaxResult stopRecord(EdgeCommonVO edgeCommonVO) {
|
||||||
microphoneService.stopRecord(edgeCommonVO);
|
microphoneService.stopRecord(edgeCommonVO);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
@ -51,7 +50,7 @@ public class EdgeMicrophoneController {
|
|||||||
@ApiOperation("暂停录音")
|
@ApiOperation("暂停录音")
|
||||||
@PreAuthorize("@ss.hasPermi('system:microphone:pause')")
|
@PreAuthorize("@ss.hasPermi('system:microphone:pause')")
|
||||||
@PostMapping("/pause")
|
@PostMapping("/pause")
|
||||||
public AjaxResult pauseRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
|
public AjaxResult pauseRecord(EdgeCommonVO edgeCommonVO) {
|
||||||
microphoneService.pauseRecord(edgeCommonVO);
|
microphoneService.pauseRecord(edgeCommonVO);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
@ -59,7 +58,7 @@ public class EdgeMicrophoneController {
|
|||||||
@ApiOperation("恢复录音")
|
@ApiOperation("恢复录音")
|
||||||
@PreAuthorize("@ss.hasPermi('system:microphone:resume')")
|
@PreAuthorize("@ss.hasPermi('system:microphone:resume')")
|
||||||
@PostMapping("/resume")
|
@PostMapping("/resume")
|
||||||
public AjaxResult resumeRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
|
public AjaxResult resumeRecord(EdgeCommonVO edgeCommonVO) {
|
||||||
microphoneService.resumeRecord(edgeCommonVO);
|
microphoneService.resumeRecord(edgeCommonVO);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
@ -67,7 +66,7 @@ public class EdgeMicrophoneController {
|
|||||||
@ApiOperation("设置音量")
|
@ApiOperation("设置音量")
|
||||||
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
|
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
|
||||||
@PostMapping("/volume")
|
@PostMapping("/volume")
|
||||||
public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
|
public AjaxResult setVolume(EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
|
||||||
microphoneService.setVolume(edgeMicrophoneVolumeVO);
|
microphoneService.setVolume(edgeMicrophoneVolumeVO);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
@ -75,10 +74,7 @@ public class EdgeMicrophoneController {
|
|||||||
@ApiOperation("获取音量")
|
@ApiOperation("获取音量")
|
||||||
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
|
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
|
||||||
@GetMapping("/volume")
|
@GetMapping("/volume")
|
||||||
public AjaxResult getVolume(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
|
public AjaxResult getVolume(EdgeCommonVO edgeCommonVO) {
|
||||||
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
|
|
||||||
edgeCommonVO.setTerminalId(terminalId);
|
|
||||||
edgeCommonVO.setDeviceId(deviceId);
|
|
||||||
return AjaxResult.success(microphoneService.getVolume(edgeCommonVO));
|
return AjaxResult.success(microphoneService.getVolume(edgeCommonVO));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,6 @@
|
|||||||
package com.cmvr.web.controller.api;
|
package com.cmvr.web.controller.api;
|
||||||
|
|
||||||
import cmvr.api.SpeakerCommand;
|
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.edge.client.service.EdgeSpeakerService;
|
||||||
import com.cmvr.common.core.domain.AjaxResult;
|
import com.cmvr.common.core.domain.AjaxResult;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
@ -72,8 +69,14 @@ public class EdgeSpeakerController {
|
|||||||
@ApiOperation("播放音频")
|
@ApiOperation("播放音频")
|
||||||
@PreAuthorize("@ss.hasPermi('system:speaker:play')")
|
@PreAuthorize("@ss.hasPermi('system:speaker:play')")
|
||||||
@PostMapping("/play")
|
@PostMapping("/play")
|
||||||
public AjaxResult playAudio(@RequestBody EdgeSpeakerPlayAudioVO edgeSpeakerPlayAudioVO) {
|
public AjaxResult playAudio(
|
||||||
speakerService.playAudio(edgeSpeakerPlayAudioVO.getTerminalId(), edgeSpeakerPlayAudioVO.getDeviceId(), edgeSpeakerPlayAudioVO.getAudioPath());
|
@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);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,8 +86,12 @@ public class EdgeSpeakerController {
|
|||||||
@ApiOperation("停止播放")
|
@ApiOperation("停止播放")
|
||||||
@PreAuthorize("@ss.hasPermi('system:speaker:stop')")
|
@PreAuthorize("@ss.hasPermi('system:speaker:stop')")
|
||||||
@PostMapping("/stop")
|
@PostMapping("/stop")
|
||||||
public AjaxResult stopPlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
|
public AjaxResult stopPlayback(
|
||||||
speakerService.stopPlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
|
@ApiParam(value = "终端设备ID", required = true)
|
||||||
|
@RequestParam String terminalId,
|
||||||
|
@ApiParam(value = "设备ID", required = true)
|
||||||
|
@RequestParam String deviceId) {
|
||||||
|
speakerService.stopPlayback(terminalId, deviceId);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,8 +101,12 @@ public class EdgeSpeakerController {
|
|||||||
@ApiOperation("暂停播放")
|
@ApiOperation("暂停播放")
|
||||||
@PreAuthorize("@ss.hasPermi('system:speaker:pause')")
|
@PreAuthorize("@ss.hasPermi('system:speaker:pause')")
|
||||||
@PostMapping("/pause")
|
@PostMapping("/pause")
|
||||||
public AjaxResult pausePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
|
public AjaxResult pausePlayback(
|
||||||
speakerService.pausePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
|
@ApiParam(value = "终端设备ID", required = true)
|
||||||
|
@RequestParam String terminalId,
|
||||||
|
@ApiParam(value = "设备ID", required = true)
|
||||||
|
@RequestParam String deviceId) {
|
||||||
|
speakerService.pausePlayback(terminalId, deviceId);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,8 +116,12 @@ public class EdgeSpeakerController {
|
|||||||
@ApiOperation("恢复播放")
|
@ApiOperation("恢复播放")
|
||||||
@PreAuthorize("@ss.hasPermi('system:speaker:resume')")
|
@PreAuthorize("@ss.hasPermi('system:speaker:resume')")
|
||||||
@PostMapping("/resume")
|
@PostMapping("/resume")
|
||||||
public AjaxResult resumePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
|
public AjaxResult resumePlayback(
|
||||||
speakerService.resumePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
|
@ApiParam(value = "终端设备ID", required = true)
|
||||||
|
@RequestParam String terminalId,
|
||||||
|
@ApiParam(value = "设备ID", required = true)
|
||||||
|
@RequestParam String deviceId) {
|
||||||
|
speakerService.resumePlayback(terminalId, deviceId);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,8 +131,14 @@ public class EdgeSpeakerController {
|
|||||||
@ApiOperation("设置音量")
|
@ApiOperation("设置音量")
|
||||||
@PreAuthorize("@ss.hasPermi('system:speaker:volume')")
|
@PreAuthorize("@ss.hasPermi('system:speaker:volume')")
|
||||||
@PostMapping("/volume")
|
@PostMapping("/volume")
|
||||||
public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
|
public AjaxResult setVolume(
|
||||||
speakerService.setVolume(edgeMicrophoneVolumeVO.getTerminalId(), edgeMicrophoneVolumeVO.getDeviceId(), edgeMicrophoneVolumeVO.getVolume());
|
@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);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,123 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
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,13 +156,8 @@ public class TeFlowController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/action")
|
@PostMapping("/action")
|
||||||
@ApiOperation(value = "单个节点执行",
|
@ApiOperation("单个节点执行")
|
||||||
notes = "巡检仪表节点action=INSPECTION_METER_RECOGNIZE,payload使用InspectionMeterRecognizeConfigVO;"
|
public AjaxResult actionExecute(@RequestBody FlowActionRequestVO flowActionRequestVO) {
|
||||||
+ "人工判断节点action=INSPECTION_MANUAL_REVIEW_CREATE,payload使用InspectionManualReviewConfigVO。"
|
|
||||||
+ "该接口用于试调节点,不会生成正式巡检任务结果")
|
|
||||||
public AjaxResult actionExecute(
|
|
||||||
@io.swagger.annotations.ApiParam(value = "单节点动作和参数", required = true)
|
|
||||||
@RequestBody FlowActionRequestVO flowActionRequestVO) {
|
|
||||||
return AjaxResult.ok(flowActionExecutorService.actionExecute(flowActionRequestVO));
|
return AjaxResult.ok(flowActionExecutorService.actionExecute(flowActionRequestVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,6 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import com.cmvr.common.config.CmvrIotConfig;
|
import com.cmvr.common.config.CmvrIotConfig;
|
||||||
import com.cmvr.test.model.vo.inspection.InspectionAlarmRuleVO;
|
|
||||||
import com.cmvr.test.model.vo.inspection.InspectionManualReviewConfigVO;
|
|
||||||
import com.cmvr.test.model.vo.inspection.InspectionMediaVO;
|
|
||||||
import com.cmvr.test.model.vo.inspection.InspectionMeterRecognizeConfigVO;
|
|
||||||
import com.fasterxml.classmate.TypeResolver;
|
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import io.swagger.models.auth.In;
|
import io.swagger.models.auth.In;
|
||||||
import springfox.documentation.builders.ApiInfoBuilder;
|
import springfox.documentation.builders.ApiInfoBuilder;
|
||||||
@ -39,10 +34,6 @@ public class SwaggerConfig
|
|||||||
@Autowired
|
@Autowired
|
||||||
private CmvrIotConfig cmvrIotConfig;
|
private CmvrIotConfig cmvrIotConfig;
|
||||||
|
|
||||||
/** 用于将动态工作流payload对应的参数模型显式加入Swagger文档。 */
|
|
||||||
@Autowired
|
|
||||||
private TypeResolver typeResolver;
|
|
||||||
|
|
||||||
/** 是否开启swagger */
|
/** 是否开启swagger */
|
||||||
@Value("${swagger.enabled}")
|
@Value("${swagger.enabled}")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
@ -71,12 +62,6 @@ public class SwaggerConfig
|
|||||||
// 扫描所有 .apis(RequestHandlerSelectors.any())
|
// 扫描所有 .apis(RequestHandlerSelectors.any())
|
||||||
.paths(PathSelectors.any())
|
.paths(PathSelectors.any())
|
||||||
.build()
|
.build()
|
||||||
// 工作流单节点接口使用JSONObject作为payload,需显式注册巡检节点参数模型。
|
|
||||||
.additionalModels(
|
|
||||||
typeResolver.resolve(InspectionMeterRecognizeConfigVO.class),
|
|
||||||
typeResolver.resolve(InspectionAlarmRuleVO.class),
|
|
||||||
typeResolver.resolve(InspectionManualReviewConfigVO.class),
|
|
||||||
typeResolver.resolve(InspectionMediaVO.class))
|
|
||||||
/* 设置安全模式,swagger可以设置访问token */
|
/* 设置安全模式,swagger可以设置访问token */
|
||||||
.securitySchemes(securitySchemes())
|
.securitySchemes(securitySchemes())
|
||||||
.securityContexts(securityContexts())
|
.securityContexts(securityContexts())
|
||||||
|
|||||||
@ -21,9 +21,6 @@ server:
|
|||||||
tomcat:
|
tomcat:
|
||||||
# tomcat的URI编码
|
# tomcat的URI编码
|
||||||
uri-encoding: UTF-8
|
uri-encoding: UTF-8
|
||||||
# PPE报警包含Base64图片,允许读取较大的JSON请求,同时限制异常请求的吞入大小
|
|
||||||
max-http-form-post-size: 20MB
|
|
||||||
max-swallow-size: 20MB
|
|
||||||
# 连接数满后的排队数,默认为100
|
# 连接数满后的排队数,默认为100
|
||||||
accept-count: 1000
|
accept-count: 1000
|
||||||
threads:
|
threads:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -47,10 +47,8 @@ public class GrpcServiceManager {
|
|||||||
clientFactories.put(SystemServiceGrpc.SystemServiceBlockingStub.class, new GrpcClientFactory<>(SystemServiceGrpc::newBlockingStub));
|
clientFactories.put(SystemServiceGrpc.SystemServiceBlockingStub.class, new GrpcClientFactory<>(SystemServiceGrpc::newBlockingStub));
|
||||||
// 注册扬声器服务的 Stub
|
// 注册扬声器服务的 Stub
|
||||||
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceBlockingStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newBlockingStub));
|
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceBlockingStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newBlockingStub));
|
||||||
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newStub));
|
|
||||||
// 注册麦克风服务的 Stub
|
// 注册麦克风服务的 Stub
|
||||||
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newBlockingStub));
|
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newBlockingStub));
|
||||||
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newStub));
|
|
||||||
// 注册机器人头部服务的 Stub
|
// 注册机器人头部服务的 Stub
|
||||||
clientFactories.put(BioHeadServiceGrpc.BioHeadServiceBlockingStub.class, new GrpcClientFactory<>(BioHeadServiceGrpc::newBlockingStub));
|
clientFactories.put(BioHeadServiceGrpc.BioHeadServiceBlockingStub.class, new GrpcClientFactory<>(BioHeadServiceGrpc::newBlockingStub));
|
||||||
// 注册机器人灵巧手服务的 Stub
|
// 注册机器人灵巧手服务的 Stub
|
||||||
@ -62,6 +60,7 @@ public class GrpcServiceManager {
|
|||||||
// 注册agv服务的stub
|
// 注册agv服务的stub
|
||||||
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub));
|
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub));
|
||||||
// 注册机器人实时语音双向流服务的异步Stub
|
// 注册机器人实时语音双向流服务的异步Stub
|
||||||
|
clientFactories.put(RobotAudioServiceGrpc.RobotAudioServiceStub.class, new GrpcClientFactory<>(RobotAudioServiceGrpc::newStub));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
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,12 +21,6 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
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;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,12 +40,6 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
|
|
||||||
/** 机器人心跳超时时间:机器人每 1 秒上报一次,超过 5 秒判定离线。 */
|
/** 机器人心跳超时时间:机器人每 1 秒上报一次,超过 5 秒判定离线。 */
|
||||||
private static final long ROBOT_HEARTBEAT_TIMEOUT_MS = 5000L;
|
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;
|
private final RobotAudioGrpcAdapter robotAudioGrpcAdapter;
|
||||||
|
|
||||||
@ -178,25 +166,13 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
private void handleJoin(WebSocketSession webSocketSession, JsonNode jsonNode) {
|
private void handleJoin(WebSocketSession webSocketSession, JsonNode jsonNode) {
|
||||||
String terminalId = getText(jsonNode, "terminalId");
|
String terminalId = getText(jsonNode, "terminalId");
|
||||||
String deviceId = getText(jsonNode, "deviceId");
|
String deviceId = getText(jsonNode, "deviceId");
|
||||||
String speakerDeviceId = getText(jsonNode, "speakerDeviceId");
|
|
||||||
String micDeviceId = getText(jsonNode, "micDeviceId");
|
|
||||||
String operatorId = getText(jsonNode, "operatorId");
|
String operatorId = getText(jsonNode, "operatorId");
|
||||||
String sessionId = getText(jsonNode, "sessionId");
|
String sessionId = getText(jsonNode, "sessionId");
|
||||||
|
|
||||||
if (speakerDeviceId == null) {
|
if (terminalId == null || deviceId == null) {
|
||||||
speakerDeviceId = deviceId;
|
sendError(webSocketSession, sessionId, "terminalId和deviceId不能为空");
|
||||||
}
|
|
||||||
if (micDeviceId == null) {
|
|
||||||
micDeviceId = deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (terminalId == null || speakerDeviceId == null || micDeviceId == null) {
|
|
||||||
sendError(webSocketSession, sessionId, "terminalId、speakerDeviceId、micDeviceId不能为空");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (deviceId == null) {
|
|
||||||
deviceId = speakerDeviceId;
|
|
||||||
}
|
|
||||||
if (sessionId == null) {
|
if (sessionId == null) {
|
||||||
sessionId = IdUtils.fastSimpleUUID();
|
sessionId = IdUtils.fastSimpleUUID();
|
||||||
}
|
}
|
||||||
@ -215,28 +191,24 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
session.sessionId = sessionId;
|
session.sessionId = sessionId;
|
||||||
session.terminalId = terminalId;
|
session.terminalId = terminalId;
|
||||||
session.deviceId = deviceId;
|
session.deviceId = deviceId;
|
||||||
session.speakerDeviceId = speakerDeviceId;
|
|
||||||
session.micDeviceId = micDeviceId;
|
|
||||||
session.operatorId = operatorId;
|
session.operatorId = operatorId;
|
||||||
session.webSocketSession = webSocketSession;
|
session.webSocketSession = webSocketSession;
|
||||||
session.state = AudioSessionState.CONNECTING;
|
session.state = AudioSessionState.CONNECTING;
|
||||||
session.createdAt = System.currentTimeMillis();
|
session.createdAt = System.currentTimeMillis();
|
||||||
session.lastBrowserSignalAt = session.createdAt;
|
session.lastBrowserSignalAt = session.createdAt;
|
||||||
session.lastRobotHeartbeatAt = session.createdAt;
|
session.lastRobotHeartbeatAt = session.createdAt;
|
||||||
startBrowserAudioPusher(session);
|
|
||||||
|
|
||||||
sessionMap.put(sessionId, session);
|
sessionMap.put(sessionId, session);
|
||||||
webSocketSessionMap.put(webSocketSession.getId(), sessionId);
|
webSocketSessionMap.put(webSocketSession.getId(), sessionId);
|
||||||
try {
|
try {
|
||||||
session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, speakerDeviceId, micDeviceId, sessionId, operatorId, this);
|
session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, deviceId, sessionId, operatorId, this);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
sessionMap.remove(sessionId);
|
sessionMap.remove(sessionId);
|
||||||
webSocketSessionMap.remove(webSocketSession.getId());
|
webSocketSessionMap.remove(webSocketSession.getId());
|
||||||
stopBrowserAudioPusher(session);
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
sendState(session, "joined");
|
sendState(session, "joined");
|
||||||
log.info("实时语音会话创建成功,sessionId={},terminalId={},speakerDeviceId={},micDeviceId={}", sessionId, terminalId, speakerDeviceId, micDeviceId);
|
log.info("实时语音会话创建成功,sessionId={},terminalId={},deviceId={}", sessionId, terminalId, deviceId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("创建实时语音会话失败,sessionId={}", sessionId, e);
|
log.error("创建实时语音会话失败,sessionId={}", sessionId, e);
|
||||||
sendError(webSocketSession, sessionId, "创建实时语音会话失败:" + e.getMessage());
|
sendError(webSocketSession, sessionId, "创建实时语音会话失败:" + e.getMessage());
|
||||||
@ -322,7 +294,6 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
try {
|
try {
|
||||||
session.state = AudioSessionState.CLOSED;
|
session.state = AudioSessionState.CLOSED;
|
||||||
removeWebSocketMapping(session);
|
removeWebSocketMapping(session);
|
||||||
stopBrowserAudioPusher(session);
|
|
||||||
if (session.robotStream != null) {
|
if (session.robotStream != null) {
|
||||||
session.robotStream.sendStop(reason);
|
session.robotStream.sendStop(reason);
|
||||||
}
|
}
|
||||||
@ -347,13 +318,12 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (now - session.lastRobotHeartbeatAt > ROBOT_HEARTBEAT_TIMEOUT_MS) {
|
if (now - session.lastRobotHeartbeatAt > ROBOT_HEARTBEAT_TIMEOUT_MS) {
|
||||||
log.warn("机器人心跳超时,sessionId={},terminalId={},speakerDeviceId={},micDeviceId={}",
|
log.warn("机器人心跳超时,sessionId={},terminalId={},deviceId={}",
|
||||||
session.sessionId, session.terminalId, session.speakerDeviceId, session.micDeviceId);
|
session.sessionId, session.terminalId, session.deviceId);
|
||||||
// 先从业务会话表摘除,避免 cancel/close 触发的异步回调再次进入同一个会话并重复关闭 native 对象。
|
// 先从业务会话表摘除,避免 cancel/close 触发的异步回调再次进入同一个会话并重复关闭 native 对象。
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
session.state = AudioSessionState.ROBOT_OFFLINE;
|
session.state = AudioSessionState.ROBOT_OFFLINE;
|
||||||
sendState(session, "robotOffline");
|
sendState(session, "robotOffline");
|
||||||
stopBrowserAudioPusher(session);
|
|
||||||
if (session.robotStream != null) {
|
if (session.robotStream != null) {
|
||||||
session.robotStream.cancel("robot heartbeat timeout");
|
session.robotStream.cancel("robot heartbeat timeout");
|
||||||
}
|
}
|
||||||
@ -389,7 +359,7 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
}
|
}
|
||||||
session.lastRobotAudioAt = System.currentTimeMillis();
|
session.lastRobotAudioAt = System.currentTimeMillis();
|
||||||
session.lastRobotHeartbeatAt = session.lastRobotAudioAt;
|
session.lastRobotHeartbeatAt = session.lastRobotAudioAt;
|
||||||
enqueueRobotAudioForBrowser(session, pcm);
|
sendBinary(session.webSocketSession, pcm);
|
||||||
Consumer<byte[]> consumer = session.robotAudioConsumer;
|
Consumer<byte[]> consumer = session.robotAudioConsumer;
|
||||||
if (consumer != null) {
|
if (consumer != null) {
|
||||||
try {
|
try {
|
||||||
@ -442,14 +412,12 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
sendState(session, "grpcCompleted");
|
sendState(session, "grpcCompleted");
|
||||||
}
|
}
|
||||||
sessionMap.remove(sessionId);
|
sessionMap.remove(sessionId);
|
||||||
stopBrowserAudioPusher(session);
|
|
||||||
removeWebSocketMapping(session);
|
removeWebSocketMapping(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void failSession(AudioSession session, String reason, boolean cancelRobotStream) {
|
private void failSession(AudioSession session, String reason, boolean cancelRobotStream) {
|
||||||
sessionMap.remove(session.sessionId, session);
|
sessionMap.remove(session.sessionId, session);
|
||||||
session.state = AudioSessionState.FAILED;
|
session.state = AudioSessionState.FAILED;
|
||||||
stopBrowserAudioPusher(session);
|
|
||||||
ObjectNode event = baseMessage("error", session.sessionId);
|
ObjectNode event = baseMessage("error", session.sessionId);
|
||||||
event.put("message", reason == null ? "未知错误" : reason);
|
event.put("message", reason == null ? "未知错误" : reason);
|
||||||
event.put("state", session.state.name());
|
event.put("state", session.state.name());
|
||||||
@ -468,8 +436,6 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
ObjectNode event = baseMessage(type, session.sessionId);
|
ObjectNode event = baseMessage(type, session.sessionId);
|
||||||
event.put("terminalId", session.terminalId);
|
event.put("terminalId", session.terminalId);
|
||||||
event.put("deviceId", session.deviceId);
|
event.put("deviceId", session.deviceId);
|
||||||
event.put("speakerDeviceId", session.speakerDeviceId);
|
|
||||||
event.put("micDeviceId", session.micDeviceId);
|
|
||||||
event.put("state", session.state.name());
|
event.put("state", session.state.name());
|
||||||
event.put("createdAt", session.createdAt);
|
event.put("createdAt", session.createdAt);
|
||||||
event.put("lastRobotHeartbeatAt", session.lastRobotHeartbeatAt);
|
event.put("lastRobotHeartbeatAt", session.lastRobotHeartbeatAt);
|
||||||
@ -496,108 +462,6 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
sendText(session, toJson(event));
|
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) {
|
private void sendBinary(WebSocketSession session, byte[] pcm) {
|
||||||
if (session == null || !session.isOpen() || pcm == null || pcm.length == 0) {
|
if (session == null || !session.isOpen() || pcm == null || pcm.length == 0) {
|
||||||
return;
|
return;
|
||||||
@ -707,21 +571,10 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
private String sessionId;
|
private String sessionId;
|
||||||
private String terminalId;
|
private String terminalId;
|
||||||
private String deviceId;
|
private String deviceId;
|
||||||
private String speakerDeviceId;
|
|
||||||
private String micDeviceId;
|
|
||||||
private String operatorId;
|
private String operatorId;
|
||||||
private volatile WebSocketSession webSocketSession;
|
private volatile WebSocketSession webSocketSession;
|
||||||
private volatile RobotAudioStream robotStream;
|
private volatile RobotAudioStream robotStream;
|
||||||
private volatile Consumer<byte[]> robotAudioConsumer;
|
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 AudioSessionState state;
|
||||||
private volatile long createdAt;
|
private volatile long createdAt;
|
||||||
private volatile long lastBrowserSignalAt;
|
private volatile long lastBrowserSignalAt;
|
||||||
@ -735,19 +588,4 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
private String sdpMid;
|
private String sdpMid;
|
||||||
private int sdpMLineIndex;
|
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,37 +170,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
return getResumeRecordMethod;
|
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,
|
private static volatile io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request,
|
||||||
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> getSetVolumeMethod;
|
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> getSetVolumeMethod;
|
||||||
|
|
||||||
@ -312,6 +281,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
public static abstract class MicPhoneServiceImplBase implements io.grpc.BindableService {
|
public static abstract class MicPhoneServiceImplBase implements io.grpc.BindableService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 基本控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
||||||
@ -347,13 +319,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
* <pre>
|
||||||
public void streamAudio(cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request,
|
* 音量控制
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> responseObserver) {
|
* </pre>
|
||||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getStreamAudioMethod(), responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
*/
|
||||||
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
||||||
@ -404,13 +372,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request,
|
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request,
|
||||||
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>(
|
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>(
|
||||||
this, METHODID_RESUME_RECORD)))
|
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(
|
.addMethod(
|
||||||
getSetVolumeMethod(),
|
getSetVolumeMethod(),
|
||||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||||
@ -444,6 +405,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 基本控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
||||||
@ -484,14 +448,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
* <pre>
|
||||||
public void streamAudio(cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request,
|
* 音量控制
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> responseObserver) {
|
* </pre>
|
||||||
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
|
|
||||||
getChannel().newCall(getStreamAudioMethod(), getCallOptions()), request, responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
*/
|
||||||
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
||||||
@ -523,6 +482,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 基本控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
public cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||||
@ -558,14 +520,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
* <pre>
|
||||||
public java.util.Iterator<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> streamAudio(
|
* 音量控制
|
||||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request) {
|
* </pre>
|
||||||
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
|
|
||||||
getChannel(), getStreamAudioMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
*/
|
||||||
public cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
public cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||||
@ -595,6 +552,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 基本控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> getStatus(
|
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> getStatus(
|
||||||
cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
||||||
@ -635,6 +595,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 音量控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> setVolume(
|
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> setVolume(
|
||||||
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
||||||
@ -656,9 +619,8 @@ public final class MicPhoneServiceGrpc {
|
|||||||
private static final int METHODID_STOP_RECORD = 2;
|
private static final int METHODID_STOP_RECORD = 2;
|
||||||
private static final int METHODID_PAUSE_RECORD = 3;
|
private static final int METHODID_PAUSE_RECORD = 3;
|
||||||
private static final int METHODID_RESUME_RECORD = 4;
|
private static final int METHODID_RESUME_RECORD = 4;
|
||||||
private static final int METHODID_STREAM_AUDIO = 5;
|
private static final int METHODID_SET_VOLUME = 5;
|
||||||
private static final int METHODID_SET_VOLUME = 6;
|
private static final int METHODID_GET_VOLUME = 6;
|
||||||
private static final int METHODID_GET_VOLUME = 7;
|
|
||||||
|
|
||||||
private static final class MethodHandlers<Req, Resp> implements
|
private static final class MethodHandlers<Req, Resp> implements
|
||||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||||
@ -697,10 +659,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
serviceImpl.resumeRecord((cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request) request,
|
serviceImpl.resumeRecord((cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request) request,
|
||||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>) responseObserver);
|
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>) responseObserver);
|
||||||
break;
|
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:
|
case METHODID_SET_VOLUME:
|
||||||
serviceImpl.setVolume((cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request) request,
|
serviceImpl.setVolume((cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request) request,
|
||||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback>) responseObserver);
|
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback>) responseObserver);
|
||||||
@ -775,7 +733,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
.addMethod(getStopRecordMethod())
|
.addMethod(getStopRecordMethod())
|
||||||
.addMethod(getPauseRecordMethod())
|
.addMethod(getPauseRecordMethod())
|
||||||
.addMethod(getResumeRecordMethod())
|
.addMethod(getResumeRecordMethod())
|
||||||
.addMethod(getStreamAudioMethod())
|
|
||||||
.addMethod(getSetVolumeMethod())
|
.addMethod(getSetVolumeMethod())
|
||||||
.addMethod(getGetVolumeMethod())
|
.addMethod(getGetVolumeMethod())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ public final class MicrophoneService {
|
|||||||
java.lang.String[] descriptorData = {
|
java.lang.String[] descriptorData = {
|
||||||
"\n!cmvr/api/microphone_service.proto\022\010cmv" +
|
"\n!cmvr/api/microphone_service.proto\022\010cmv" +
|
||||||
"r.api\032!cmvr/api/microphone_command.proto" +
|
"r.api\032!cmvr/api/microphone_command.proto" +
|
||||||
"2\273\006\n\017MicPhoneService\022X\n\tGetStatus\022$.cmvr" +
|
"2\327\005\n\017MicPhoneService\022X\n\tGetStatus\022$.cmvr" +
|
||||||
".api.GetMicStateCommand.Request\032%.cmvr.a" +
|
".api.GetMicStateCommand.Request\032%.cmvr.a" +
|
||||||
"pi.GetMicStateCommand.Feedback\022f\n\013StartR" +
|
"pi.GetMicStateCommand.Feedback\022f\n\013StartR" +
|
||||||
"ecord\022*.cmvr.api.StartMicRecordingComman" +
|
"ecord\022*.cmvr.api.StartMicRecordingComman" +
|
||||||
@ -38,14 +38,12 @@ public final class MicrophoneService {
|
|||||||
"ingCommand.Feedback\022i\n\014ResumeRecord\022+.cm" +
|
"ingCommand.Feedback\022i\n\014ResumeRecord\022+.cm" +
|
||||||
"vr.api.ResumeMicRecordingCommand.Request" +
|
"vr.api.ResumeMicRecordingCommand.Request" +
|
||||||
"\032,.cmvr.api.ResumeMicRecordingCommand.Fe" +
|
"\032,.cmvr.api.ResumeMicRecordingCommand.Fe" +
|
||||||
"edback\022b\n\013StreamAudio\022\'.cmvr.api.StreamM" +
|
"edback\022d\n\tSetVolume\022*.cmvr.api.SetMicPho" +
|
||||||
"icAudioCommand.Request\032(.cmvr.api.Stream" +
|
"neVolumeCommand.Request\032+.cmvr.api.SetMi" +
|
||||||
"MicAudioCommand.Feedback0\001\022d\n\tSetVolume\022" +
|
"cPhoneVolumeCommand.Feedback\022d\n\tGetVolum" +
|
||||||
"*.cmvr.api.SetMicPhoneVolumeCommand.Requ" +
|
"e\022*.cmvr.api.GetMicPhoneVolumeCommand.Re" +
|
||||||
"est\032+.cmvr.api.SetMicPhoneVolumeCommand." +
|
"quest\032+.cmvr.api.GetMicPhoneVolumeComman" +
|
||||||
"Feedback\022d\n\tGetVolume\022*.cmvr.api.GetMicP" +
|
"d.Feedbackb\006proto3"
|
||||||
"honeVolumeCommand.Request\032+.cmvr.api.Get" +
|
|
||||||
"MicPhoneVolumeCommand.Feedbackb\006proto3"
|
|
||||||
};
|
};
|
||||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||||
.internalBuildGeneratedFileFrom(descriptorData,
|
.internalBuildGeneratedFileFrom(descriptorData,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,280 @@
|
|||||||
|
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,37 +77,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
return getPlayAudioMethod;
|
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,
|
private static volatile io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StopSpeakerCommand.Request,
|
||||||
cmvr.api.SpeakerCommand.StopSpeakerCommand.Feedback> getStopPlaybackMethod;
|
cmvr.api.SpeakerCommand.StopSpeakerCommand.Feedback> getStopPlaybackMethod;
|
||||||
|
|
||||||
@ -312,6 +281,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
public static abstract class SpeakerServiceImplBase implements io.grpc.BindableService {
|
public static abstract class SpeakerServiceImplBase implements io.grpc.BindableService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 基本控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
||||||
@ -325,13 +297,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getPlayAudioMethod(), responseObserver);
|
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,
|
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
||||||
@ -354,6 +319,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 音量控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
||||||
@ -383,13 +351,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
cmvr.api.SpeakerCommand.PlayAudioCommand.Request,
|
cmvr.api.SpeakerCommand.PlayAudioCommand.Request,
|
||||||
cmvr.api.SpeakerCommand.PlayAudioCommand.Feedback>(
|
cmvr.api.SpeakerCommand.PlayAudioCommand.Feedback>(
|
||||||
this, METHODID_PLAY_AUDIO)))
|
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(
|
.addMethod(
|
||||||
getStopPlaybackMethod(),
|
getStopPlaybackMethod(),
|
||||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||||
@ -444,6 +405,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 基本控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
||||||
@ -459,14 +423,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
getChannel().newCall(getPlayAudioMethod(), getCallOptions()), request, responseObserver);
|
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,
|
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
||||||
@ -492,6 +448,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 音量控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
||||||
@ -523,6 +482,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 基本控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
public cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||||
@ -558,6 +520,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 音量控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
public cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||||
@ -587,6 +552,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 基本控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> getStatus(
|
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> getStatus(
|
||||||
cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
||||||
@ -627,6 +595,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <pre>
|
||||||
|
* 音量控制
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> setVolume(
|
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> setVolume(
|
||||||
cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
||||||
@ -650,7 +621,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
private static final int METHODID_RESUME_PLAYBACK = 4;
|
private static final int METHODID_RESUME_PLAYBACK = 4;
|
||||||
private static final int METHODID_SET_VOLUME = 5;
|
private static final int METHODID_SET_VOLUME = 5;
|
||||||
private static final int METHODID_GET_VOLUME = 6;
|
private static final int METHODID_GET_VOLUME = 6;
|
||||||
private static final int METHODID_STREAM_AUDIO = 7;
|
|
||||||
|
|
||||||
private static final class MethodHandlers<Req, Resp> implements
|
private static final class MethodHandlers<Req, Resp> implements
|
||||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||||
@ -707,9 +677,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
public io.grpc.stub.StreamObserver<Req> invoke(
|
||||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||||
switch (methodId) {
|
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:
|
default:
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
@ -763,7 +730,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
.setSchemaDescriptor(new SpeakerServiceFileDescriptorSupplier())
|
.setSchemaDescriptor(new SpeakerServiceFileDescriptorSupplier())
|
||||||
.addMethod(getGetStatusMethod())
|
.addMethod(getGetStatusMethod())
|
||||||
.addMethod(getPlayAudioMethod())
|
.addMethod(getPlayAudioMethod())
|
||||||
.addMethod(getStreamAudioMethod())
|
|
||||||
.addMethod(getStopPlaybackMethod())
|
.addMethod(getStopPlaybackMethod())
|
||||||
.addMethod(getPausePlaybackMethod())
|
.addMethod(getPausePlaybackMethod())
|
||||||
.addMethod(getResumePlaybackMethod())
|
.addMethod(getResumePlaybackMethod())
|
||||||
|
|||||||
@ -24,28 +24,25 @@ public final class SpeakerServiceOuterClass {
|
|||||||
static {
|
static {
|
||||||
java.lang.String[] descriptorData = {
|
java.lang.String[] descriptorData = {
|
||||||
"\n\036cmvr/api/speaker_service.proto\022\010cmvr.a" +
|
"\n\036cmvr/api/speaker_service.proto\022\010cmvr.a" +
|
||||||
"pi\032\036cmvr/api/speaker_command.proto2\234\006\n\016S" +
|
"pi\032\036cmvr/api/speaker_command.proto2\260\005\n\016S" +
|
||||||
"peakerService\022`\n\tGetStatus\022(.cmvr.api.Ge" +
|
"peakerService\022`\n\tGetStatus\022(.cmvr.api.Ge" +
|
||||||
"tSpeakerStateCommand.Request\032).cmvr.api." +
|
"tSpeakerStateCommand.Request\032).cmvr.api." +
|
||||||
"GetSpeakerStateCommand.Feedback\022T\n\tPlayA" +
|
"GetSpeakerStateCommand.Feedback\022T\n\tPlayA" +
|
||||||
"udio\022\".cmvr.api.PlayAudioCommand.Request" +
|
"udio\022\".cmvr.api.PlayAudioCommand.Request" +
|
||||||
"\032#.cmvr.api.PlayAudioCommand.Feedback\022j\n" +
|
"\032#.cmvr.api.PlayAudioCommand.Feedback\022[\n" +
|
||||||
"\013StreamAudio\022+.cmvr.api.StreamSpeakerAud" +
|
"\014StopPlayback\022$.cmvr.api.StopSpeakerComm" +
|
||||||
"ioCommand.Request\032,.cmvr.api.StreamSpeak" +
|
"and.Request\032%.cmvr.api.StopSpeakerComman" +
|
||||||
"erAudioCommand.Feedback(\001\022[\n\014StopPlaybac" +
|
"d.Feedback\022^\n\rPausePlayback\022%.cmvr.api.P" +
|
||||||
"k\022$.cmvr.api.StopSpeakerCommand.Request\032" +
|
"auseSpeakerCommand.Request\032&.cmvr.api.Pa" +
|
||||||
"%.cmvr.api.StopSpeakerCommand.Feedback\022^" +
|
"useSpeakerCommand.Feedback\022a\n\016ResumePlay" +
|
||||||
"\n\rPausePlayback\022%.cmvr.api.PauseSpeakerC" +
|
"back\022&.cmvr.api.ResumeSpeakerCommand.Req" +
|
||||||
"ommand.Request\032&.cmvr.api.PauseSpeakerCo" +
|
"uest\032\'.cmvr.api.ResumeSpeakerCommand.Fee" +
|
||||||
"mmand.Feedback\022a\n\016ResumePlayback\022&.cmvr." +
|
"dback\022b\n\tSetVolume\022).cmvr.api.SetSpeaker" +
|
||||||
"api.ResumeSpeakerCommand.Request\032\'.cmvr." +
|
"VolumeCommand.Request\032*.cmvr.api.SetSpea" +
|
||||||
"api.ResumeSpeakerCommand.Feedback\022b\n\tSet" +
|
"kerVolumeCommand.Feedback\022b\n\tGetVolume\022)" +
|
||||||
"Volume\022).cmvr.api.SetSpeakerVolumeComman" +
|
".cmvr.api.GetSpeakerVolumeCommand.Reques" +
|
||||||
"d.Request\032*.cmvr.api.SetSpeakerVolumeCom" +
|
"t\032*.cmvr.api.GetSpeakerVolumeCommand.Fee" +
|
||||||
"mand.Feedback\022b\n\tGetVolume\022).cmvr.api.Ge" +
|
"dbackb\006proto3"
|
||||||
"tSpeakerVolumeCommand.Request\032*.cmvr.api" +
|
|
||||||
".GetSpeakerVolumeCommand.Feedbackb\006proto" +
|
|
||||||
"3"
|
|
||||||
};
|
};
|
||||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||||
.internalBuildGeneratedFileFrom(descriptorData,
|
.internalBuildGeneratedFileFrom(descriptorData,
|
||||||
|
|||||||
@ -29,22 +29,6 @@ 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 {
|
message ConfigParam {
|
||||||
|
|
||||||
string param_name = 1;
|
string param_name = 1;
|
||||||
|
|||||||
@ -11,7 +11,6 @@ message MicState {
|
|||||||
int32 volume = 4;
|
int32 volume = 4;
|
||||||
string error_message = 5;
|
string error_message = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetMicStateCommand {
|
message GetMicStateCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
@ -22,7 +21,6 @@ message GetMicStateCommand {
|
|||||||
MicState state = 2;
|
MicState state = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message StartMicRecordingCommand {
|
message StartMicRecordingCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
@ -61,20 +59,10 @@ message ResumeMicRecordingCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message StreamMicAudioCommand {
|
|
||||||
message Request {
|
|
||||||
CommandHeader.Request header = 1;
|
|
||||||
}
|
|
||||||
message Feedback {
|
|
||||||
CommandHeader.Feedback header = 1;
|
|
||||||
AudioData audio = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message SetMicPhoneVolumeCommand {
|
message SetMicPhoneVolumeCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
int32 volume = 2;
|
int32 volume = 2; // 音量值(0 ~ 100)
|
||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
CommandHeader.Feedback header = 1;
|
||||||
@ -87,6 +75,7 @@ message GetMicPhoneVolumeCommand {
|
|||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
CommandHeader.Feedback header = 1;
|
||||||
int32 volume = 2;
|
int32 volume = 2; // 当前音量值
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,17 @@ import "cmvr/api/microphone_command.proto";
|
|||||||
|
|
||||||
package cmvr.api;
|
package cmvr.api;
|
||||||
|
|
||||||
|
|
||||||
service MicPhoneService {
|
service MicPhoneService {
|
||||||
|
// 基本控制
|
||||||
rpc GetStatus(GetMicStateCommand.Request) returns (GetMicStateCommand.Feedback);
|
rpc GetStatus(GetMicStateCommand.Request) returns (GetMicStateCommand.Feedback);
|
||||||
rpc StartRecord(StartMicRecordingCommand.Request) returns (StartMicRecordingCommand.Feedback);
|
rpc StartRecord(StartMicRecordingCommand.Request) returns (StartMicRecordingCommand.Feedback);
|
||||||
rpc StopRecord(StopMicRecordingCommand.Request) returns (StopMicRecordingCommand.Feedback);
|
rpc StopRecord(StopMicRecordingCommand.Request) returns (StopMicRecordingCommand.Feedback);
|
||||||
rpc PauseRecord(PauseMicRecordingCommand.Request) returns (PauseMicRecordingCommand.Feedback);
|
rpc PauseRecord(PauseMicRecordingCommand.Request) returns (PauseMicRecordingCommand.Feedback);
|
||||||
rpc ResumeRecord(ResumeMicRecordingCommand.Request) returns (ResumeMicRecordingCommand.Feedback);
|
rpc ResumeRecord(ResumeMicRecordingCommand.Request) returns (ResumeMicRecordingCommand.Feedback);
|
||||||
rpc StreamAudio(StreamMicAudioCommand.Request) returns (stream StreamMicAudioCommand.Feedback);
|
|
||||||
|
|
||||||
|
// 音量控制
|
||||||
rpc SetVolume(SetMicPhoneVolumeCommand.Request) returns (SetMicPhoneVolumeCommand.Feedback);
|
rpc SetVolume(SetMicPhoneVolumeCommand.Request) returns (SetMicPhoneVolumeCommand.Feedback);
|
||||||
rpc GetVolume(GetMicPhoneVolumeCommand.Request) returns (GetMicPhoneVolumeCommand.Feedback);
|
rpc GetVolume(GetMicPhoneVolumeCommand.Request) returns (GetMicPhoneVolumeCommand.Feedback);
|
||||||
}
|
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
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,15 +4,33 @@ import "cmvr/api/common.proto";
|
|||||||
|
|
||||||
package cmvr.api;
|
package cmvr.api;
|
||||||
|
|
||||||
message SpeakerState {
|
|
||||||
bool is_initialized = 1;
|
// 音频数据格式(与麦克风类似,但用于输出)
|
||||||
bool is_running = 2;
|
message AudioData {
|
||||||
bool is_decoding = 3;
|
enum AudioFormat {
|
||||||
bool is_paused = 5;
|
PCM = 0;
|
||||||
int32 volume = 6;
|
MP3 = 1;
|
||||||
string error_message = 7;
|
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_decoding = 3;
|
||||||
|
bool is_paused = 5;
|
||||||
|
int32 volume = 6; // 当前音量(0 ~ 100)
|
||||||
|
string error_message = 7; // 错误信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 各种扬声器命令
|
||||||
message GetSpeakerStateCommand {
|
message GetSpeakerStateCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
@ -29,19 +47,7 @@ message PlayAudioCommand {
|
|||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
string audio_path = 2;
|
string audio_path = 2;
|
||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback { CommandHeader.Feedback header = 1; }
|
||||||
CommandHeader.Feedback header = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message StreamSpeakerAudioCommand {
|
|
||||||
message Request {
|
|
||||||
CommandHeader.Request header = 1;
|
|
||||||
AudioData audio = 2;
|
|
||||||
}
|
|
||||||
message Feedback {
|
|
||||||
CommandHeader.Feedback header = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message StopSpeakerCommand {
|
message StopSpeakerCommand {
|
||||||
@ -74,7 +80,7 @@ message ResumeSpeakerCommand {
|
|||||||
message SetSpeakerVolumeCommand {
|
message SetSpeakerVolumeCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
int32 volume = 2;
|
int32 volume = 2; // 音量值(0 ~ 100)
|
||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
CommandHeader.Feedback header = 1;
|
||||||
@ -87,6 +93,7 @@ message GetSpeakerVolumeCommand {
|
|||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
CommandHeader.Feedback header = 1;
|
||||||
int32 volume = 2;
|
int32 volume = 2; // 当前音量值
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,17 @@ import "cmvr/api/speaker_command.proto";
|
|||||||
|
|
||||||
package cmvr.api;
|
package cmvr.api;
|
||||||
|
|
||||||
|
|
||||||
service SpeakerService {
|
service SpeakerService {
|
||||||
|
// 基本控制
|
||||||
rpc GetStatus(GetSpeakerStateCommand.Request) returns (GetSpeakerStateCommand.Feedback);
|
rpc GetStatus(GetSpeakerStateCommand.Request) returns (GetSpeakerStateCommand.Feedback);
|
||||||
rpc PlayAudio(PlayAudioCommand.Request) returns (PlayAudioCommand.Feedback);
|
rpc PlayAudio(PlayAudioCommand.Request) returns (PlayAudioCommand.Feedback);
|
||||||
rpc StreamAudio(stream StreamSpeakerAudioCommand.Request) returns (StreamSpeakerAudioCommand.Feedback);
|
|
||||||
rpc StopPlayback(StopSpeakerCommand.Request) returns (StopSpeakerCommand.Feedback);
|
rpc StopPlayback(StopSpeakerCommand.Request) returns (StopSpeakerCommand.Feedback);
|
||||||
rpc PausePlayback(PauseSpeakerCommand.Request) returns (PauseSpeakerCommand.Feedback);
|
rpc PausePlayback(PauseSpeakerCommand.Request) returns (PauseSpeakerCommand.Feedback);
|
||||||
rpc ResumePlayback(ResumeSpeakerCommand.Request) returns (ResumeSpeakerCommand.Feedback);
|
rpc ResumePlayback(ResumeSpeakerCommand.Request) returns (ResumeSpeakerCommand.Feedback);
|
||||||
|
|
||||||
|
// 音量控制
|
||||||
rpc SetVolume(SetSpeakerVolumeCommand.Request) returns (SetSpeakerVolumeCommand.Feedback);
|
rpc SetVolume(SetSpeakerVolumeCommand.Request) returns (SetSpeakerVolumeCommand.Feedback);
|
||||||
rpc GetVolume(GetSpeakerVolumeCommand.Request) returns (GetSpeakerVolumeCommand.Feedback);
|
rpc GetVolume(GetSpeakerVolumeCommand.Request) returns (GetSpeakerVolumeCommand.Feedback);
|
||||||
}
|
|
||||||
|
}
|
||||||
@ -1,346 +0,0 @@
|
|||||||
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,11 +29,5 @@
|
|||||||
<artifactId>cmvr-iot-test</artifactId>
|
<artifactId>cmvr-iot-test</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 复用终端报警监听状态,按 grpc_ip + 事件类型过滤入库 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.cmvr</groupId>
|
|
||||||
<artifactId>cmvr-iot-device</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@ -40,18 +40,6 @@ public class InspectionAlarm extends BaseEntity
|
|||||||
@ApiModelProperty("任务执行实例ID")
|
@ApiModelProperty("任务执行实例ID")
|
||||||
private String taskInstanceId;
|
private String taskInstanceId;
|
||||||
|
|
||||||
@ApiModelProperty("关联巡检结果ID")
|
|
||||||
private String resultId;
|
|
||||||
|
|
||||||
@ApiModelProperty("告警来源:PPE、METER、MANUAL")
|
|
||||||
private String alarmSource;
|
|
||||||
|
|
||||||
@ApiModelProperty("检测项ID")
|
|
||||||
private String itemId;
|
|
||||||
|
|
||||||
@ApiModelProperty("工作流节点ID")
|
|
||||||
private String nodeId;
|
|
||||||
|
|
||||||
@Excel(name = "任务ID")
|
@Excel(name = "任务ID")
|
||||||
@ApiModelProperty("任务ID")
|
@ApiModelProperty("任务ID")
|
||||||
private String taskId;
|
private String taskId;
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
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,18 +30,6 @@ public class InspectionAlarmVo extends BaseEntity
|
|||||||
@ApiModelProperty("id")
|
@ApiModelProperty("id")
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
@ApiModelProperty("关联巡检结果ID")
|
|
||||||
private String resultId;
|
|
||||||
|
|
||||||
@ApiModelProperty("告警来源:PPE、METER、MANUAL")
|
|
||||||
private String alarmSource;
|
|
||||||
|
|
||||||
@ApiModelProperty("检测项ID")
|
|
||||||
private String itemId;
|
|
||||||
|
|
||||||
@ApiModelProperty("工作流节点ID")
|
|
||||||
private String nodeId;
|
|
||||||
|
|
||||||
/** 告警编码 */
|
/** 告警编码 */
|
||||||
@Excel(name = "告警编码")
|
@Excel(name = "告警编码")
|
||||||
@ApiModelProperty("告警编码")
|
@ApiModelProperty("告警编码")
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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,14 +4,12 @@ package com.cmvr.inspection.listener;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.cmvr.common.utils.DateUtils;
|
import com.cmvr.common.utils.DateUtils;
|
||||||
import com.cmvr.framework.websocket.service.MessagePushService;
|
import com.cmvr.framework.websocket.service.MessagePushService;
|
||||||
import com.cmvr.device.service.InspectionAlertListenService;
|
|
||||||
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
import com.cmvr.inspection.domain.InspectionTaskInstance;
|
||||||
import com.cmvr.inspection.domain.InspectionTaskLog;
|
import com.cmvr.inspection.domain.InspectionTaskLog;
|
||||||
import com.cmvr.inspection.enums.InspectionLogTypeEnum;
|
import com.cmvr.inspection.enums.InspectionLogTypeEnum;
|
||||||
import com.cmvr.inspection.enums.TaskStatusEnum;
|
import com.cmvr.inspection.enums.TaskStatusEnum;
|
||||||
import com.cmvr.inspection.service.IInspectionTaskInstanceService;
|
import com.cmvr.inspection.service.IInspectionTaskInstanceService;
|
||||||
import com.cmvr.inspection.service.IInspectionTaskLogService;
|
import com.cmvr.inspection.service.IInspectionTaskLogService;
|
||||||
import com.cmvr.inspection.service.IInspectionResultService;
|
|
||||||
import com.cmvr.test.flow.runtime.event.FlowExecutionEvent;
|
import com.cmvr.test.flow.runtime.event.FlowExecutionEvent;
|
||||||
import com.cmvr.test.flow.runtime.event.FlowExecutionListener;
|
import com.cmvr.test.flow.runtime.event.FlowExecutionListener;
|
||||||
import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes;
|
import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes;
|
||||||
@ -26,24 +24,18 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
private final IInspectionTaskInstanceService inspectionTaskInstanceService;
|
private final IInspectionTaskInstanceService inspectionTaskInstanceService;
|
||||||
private final IInspectionTaskLogService inspectionTaskLogService;
|
private final IInspectionTaskLogService inspectionTaskLogService;
|
||||||
private final MessagePushService messagePushService;
|
private final MessagePushService messagePushService;
|
||||||
private final IInspectionResultService inspectionResultService;
|
|
||||||
private final InspectionAlertListenService inspectionAlertListenService;
|
|
||||||
|
|
||||||
/** key:flowInstId|itemId, value:已完成节点集合 */
|
/** key:itemId, value:已完成节点集合 */
|
||||||
private final Map<String, Set<String>> itemNodeExecutMap = new java.util.concurrent.ConcurrentHashMap<>();
|
private final Map<String, Set<String>> itemNodeExecutMap = new HashMap<>();
|
||||||
/** key:taskInsId(流程实例id=taskInsId), value:已执行item集合 */
|
/** key:taskInsId(流程实例id=taskInsId), value:已执行item集合 */
|
||||||
private final Map<String, Set<String>> taskItemExecutMap = new java.util.concurrent.ConcurrentHashMap<>();
|
private final Map<String, Set<String>> taskItemExecutMap = new HashMap<>();
|
||||||
|
|
||||||
public InspectionFlowExecutionListener(IInspectionTaskInstanceService inspectionTaskInstanceService,
|
public InspectionFlowExecutionListener(IInspectionTaskInstanceService inspectionTaskInstanceService,
|
||||||
IInspectionTaskLogService inspectionTaskLogService,
|
IInspectionTaskLogService inspectionTaskLogService,
|
||||||
MessagePushService messagePushService,
|
MessagePushService messagePushService) {
|
||||||
IInspectionResultService inspectionResultService,
|
|
||||||
InspectionAlertListenService inspectionAlertListenService) {
|
|
||||||
this.inspectionTaskInstanceService = inspectionTaskInstanceService;
|
this.inspectionTaskInstanceService = inspectionTaskInstanceService;
|
||||||
this.inspectionTaskLogService = inspectionTaskLogService;
|
this.inspectionTaskLogService = inspectionTaskLogService;
|
||||||
this.messagePushService = messagePushService;
|
this.messagePushService = messagePushService;
|
||||||
this.inspectionResultService = inspectionResultService;
|
|
||||||
this.inspectionAlertListenService = inspectionAlertListenService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -60,27 +52,19 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无论数据库是否可用,任务结束事件都必须先清理内存监听和进度状态。
|
|
||||||
if (FlowExecutionEvent.EventType.TASK_COMPLETED == event.getEventType()
|
|
||||||
|| FlowExecutionEvent.EventType.TASK_FAILED == event.getEventType()
|
|
||||||
|| FlowExecutionEvent.EventType.TASK_STOPPED == event.getEventType()) {
|
|
||||||
clearCache(instId);
|
|
||||||
inspectionAlertListenService.clearByFlowInstanceId(instId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.节点完成才记录当前item下node
|
// 1.节点完成才记录当前item下node
|
||||||
if (StrUtil.isNotBlank(itemId) && StrUtil.isNotBlank(nodeId)
|
if (StrUtil.isNotBlank(itemId) && StrUtil.isNotBlank(nodeId)
|
||||||
&& FlowExecutionEvent.EventType.NODE_COMPLETED == event.getEventType()) {
|
&& FlowExecutionEvent.EventType.NODE_COMPLETED == event.getEventType()) {
|
||||||
itemNodeExecutMap.computeIfAbsent(itemExecutionKey(instId, itemId),
|
itemNodeExecutMap.computeIfAbsent(itemId, k -> new HashSet<>()).add(nodeId);
|
||||||
k -> java.util.concurrent.ConcurrentHashMap.newKeySet()).add(nodeId);
|
|
||||||
}
|
}
|
||||||
// 任意事件:当前任务绑定item(item一启动就入Map,所以统计已完成item要-1)
|
// 任意事件:当前任务绑定item(item一启动就入Map,所以统计已完成item要-1)
|
||||||
if (StrUtil.isNotBlank(instId) && StrUtil.isNotBlank(itemId)) {
|
if (StrUtil.isNotBlank(instId) && StrUtil.isNotBlank(itemId)) {
|
||||||
taskItemExecutMap.computeIfAbsent(instId,
|
taskItemExecutMap.computeIfAbsent(instId, k -> new HashSet<>()).add(itemId);
|
||||||
k -> java.util.concurrent.ConcurrentHashMap.newKeySet()).add(itemId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InspectionTaskInstance taskInstance = findTaskInstance(instId);
|
InspectionTaskInstance taskInstance = inspectionTaskInstanceService.lambdaQuery()
|
||||||
|
.eq(InspectionTaskInstance::getTaskInsId, instId)
|
||||||
|
.one();
|
||||||
if (taskInstance == null) {
|
if (taskInstance == null) {
|
||||||
log.warn("未查询到巡检实例,taskInsId:{}", instId);
|
log.warn("未查询到巡检实例,taskInsId:{}", instId);
|
||||||
return;
|
return;
|
||||||
@ -93,13 +77,13 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
case TASK_COMPLETED:
|
case TASK_COMPLETED:
|
||||||
targetStatus = TaskStatusEnum.SUCCESS.getCode();
|
targetStatus = TaskStatusEnum.SUCCESS.getCode();
|
||||||
needUpdateDb = true;
|
needUpdateDb = true;
|
||||||
|
clearCache(instId);
|
||||||
pushCompleteMsg(taskInstance.getId(), now, targetStatus);
|
pushCompleteMsg(taskInstance.getId(), now, targetStatus);
|
||||||
break;
|
break;
|
||||||
case TASK_FAILED:
|
case TASK_FAILED:
|
||||||
targetStatus = TaskStatusEnum.FAILED.getCode();
|
targetStatus = TaskStatusEnum.FAILED.getCode();
|
||||||
needUpdateDb = true;
|
needUpdateDb = true;
|
||||||
break;
|
clearCache(instId);
|
||||||
case TASK_STOPPED:
|
|
||||||
break;
|
break;
|
||||||
case NODE_COMPLETED:
|
case NODE_COMPLETED:
|
||||||
default:
|
default:
|
||||||
@ -112,15 +96,6 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
inspectionTaskInstanceService.updateById(taskInstance);
|
inspectionTaskInstanceService.updateById(taskInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FlowExecutionEvent.EventType.NODE_COMPLETED == event.getEventType()) {
|
|
||||||
try {
|
|
||||||
inspectionResultService.recordWorkflowResult(event, taskInstance);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("保存工作流巡检结果失败,taskInstanceId:{},nodeId:{}",
|
|
||||||
taskInstance.getId(), nodeId, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 节点推送
|
// 节点推送
|
||||||
if (StrUtil.isNotBlank(nodeId)) {
|
if (StrUtil.isNotBlank(nodeId)) {
|
||||||
int waypointCount = taskInstance.getWaypointCount() == null ? 0 : taskInstance.getWaypointCount();
|
int waypointCount = taskInstance.getWaypointCount() == null ? 0 : taskInstance.getWaypointCount();
|
||||||
@ -132,8 +107,7 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
int finishedItemNum = itemTotal - 1;
|
int finishedItemNum = itemTotal - 1;
|
||||||
|
|
||||||
// 当前item已完成节点数
|
// 当前item已完成节点数
|
||||||
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(
|
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(itemId, new HashSet<>());
|
||||||
itemExecutionKey(instId, itemId), new HashSet<>());
|
|
||||||
int finishedNode = finishedNodeSet.size();
|
int finishedNode = finishedNodeSet.size();
|
||||||
int totalNode = event.getNodeCount() == 0 ? 1 : event.getNodeCount();
|
int totalNode = event.getNodeCount() == 0 ? 1 : event.getNodeCount();
|
||||||
|
|
||||||
@ -185,35 +159,10 @@ public class InspectionFlowExecutionListener implements FlowExecutionListener {
|
|||||||
/** 任务结束统一清理缓存 */
|
/** 任务结束统一清理缓存 */
|
||||||
private void clearCache(String taskInsId) {
|
private void clearCache(String taskInsId) {
|
||||||
Set<String> itemList = taskItemExecutMap.getOrDefault(taskInsId, new HashSet<>());
|
Set<String> itemList = taskItemExecutMap.getOrDefault(taskInsId, new HashSet<>());
|
||||||
itemList.forEach(itemId -> itemNodeExecutMap.remove(itemExecutionKey(taskInsId, itemId)));
|
itemList.forEach(itemNodeExecutMap::remove);
|
||||||
taskItemExecutMap.remove(taskInsId);
|
taskItemExecutMap.remove(taskInsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String itemExecutionKey(String taskInsId, String itemId) {
|
|
||||||
return taskInsId + "|" + itemId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流是异步启动的,首个节点事件可能略早于巡检实例写入task_ins_id,短暂重试消除该竞态。
|
|
||||||
*/
|
|
||||||
private InspectionTaskInstance findTaskInstance(String taskInsId) {
|
|
||||||
for (int attempt = 0; attempt < 5; attempt++) {
|
|
||||||
InspectionTaskInstance instance = inspectionTaskInstanceService.lambdaQuery()
|
|
||||||
.eq(InspectionTaskInstance::getTaskInsId, taskInsId)
|
|
||||||
.one();
|
|
||||||
if (instance != null) {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Thread.sleep(50L);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推送完成任务信息(参数dbInsId为数据库主键ID)
|
* 推送完成任务信息(参数dbInsId为数据库主键ID)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
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>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,376 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,739 +0,0 @@
|
|||||||
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,10 +13,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="remark" column="remark" />
|
<result property="remark" column="remark" />
|
||||||
<result property="alarmCode" column="alarm_code" />
|
<result property="alarmCode" column="alarm_code" />
|
||||||
<result property="taskInstanceId" column="task_instance_id" />
|
<result property="taskInstanceId" column="task_instance_id" />
|
||||||
<result property="resultId" column="result_id" />
|
|
||||||
<result property="alarmSource" column="alarm_source" />
|
|
||||||
<result property="itemId" column="item_id" />
|
|
||||||
<result property="nodeId" column="node_id" />
|
|
||||||
<result property="taskId" column="task_id" />
|
<result property="taskId" column="task_id" />
|
||||||
<result property="taskName" column="task_name" />
|
<result property="taskName" column="task_name" />
|
||||||
<result property="robotId" column="robot_id" />
|
<result property="robotId" column="robot_id" />
|
||||||
@ -43,10 +39,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="remark" column="remark" />
|
<result property="remark" column="remark" />
|
||||||
<result property="alarmCode" column="alarm_code" />
|
<result property="alarmCode" column="alarm_code" />
|
||||||
<result property="taskInstanceId" column="task_instance_id" />
|
<result property="taskInstanceId" column="task_instance_id" />
|
||||||
<result property="resultId" column="result_id" />
|
|
||||||
<result property="alarmSource" column="alarm_source" />
|
|
||||||
<result property="itemId" column="item_id" />
|
|
||||||
<result property="nodeId" column="node_id" />
|
|
||||||
<result property="taskId" column="task_id" />
|
<result property="taskId" column="task_id" />
|
||||||
<result property="taskName" column="task_name" />
|
<result property="taskName" column="task_name" />
|
||||||
<result property="robotId" column="robot_id" />
|
<result property="robotId" column="robot_id" />
|
||||||
@ -67,11 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectInspectionAlarmVo">
|
<sql id="selectInspectionAlarmVo">
|
||||||
select id, create_by, create_time, update_by, update_time, remark, alarm_code,
|
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
|
||||||
task_instance_id, result_id, alarm_source, item_id, node_id,
|
|
||||||
task_id, task_name, robot_id, robot_name, alarm_level, alarm_type,
|
|
||||||
alarm_title, alarm_content, alarm_location, alarm_time, handle_status,
|
|
||||||
handler, handle_time, handle_remark, evidence_image from inspection_alarm
|
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectInspectionAlarmList" parameterType="InspectionAlarm" resultMap="InspectionAlarmResult">
|
<select id="selectInspectionAlarmList" parameterType="InspectionAlarm" resultMap="InspectionAlarmResult">
|
||||||
@ -102,10 +90,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">remark,</if>
|
<if test="remark != null">remark,</if>
|
||||||
<if test="alarmCode != null">alarm_code,</if>
|
<if test="alarmCode != null">alarm_code,</if>
|
||||||
<if test="taskInstanceId != null">task_instance_id,</if>
|
<if test="taskInstanceId != null">task_instance_id,</if>
|
||||||
<if test="resultId != null">result_id,</if>
|
|
||||||
<if test="alarmSource != null">alarm_source,</if>
|
|
||||||
<if test="itemId != null">item_id,</if>
|
|
||||||
<if test="nodeId != null">node_id,</if>
|
|
||||||
<if test="taskId != null">task_id,</if>
|
<if test="taskId != null">task_id,</if>
|
||||||
<if test="taskName != null">task_name,</if>
|
<if test="taskName != null">task_name,</if>
|
||||||
<if test="robotId != null">robot_id,</if>
|
<if test="robotId != null">robot_id,</if>
|
||||||
@ -131,10 +115,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">#{remark},</if>
|
<if test="remark != null">#{remark},</if>
|
||||||
<if test="alarmCode != null">#{alarmCode},</if>
|
<if test="alarmCode != null">#{alarmCode},</if>
|
||||||
<if test="taskInstanceId != null">#{taskInstanceId},</if>
|
<if test="taskInstanceId != null">#{taskInstanceId},</if>
|
||||||
<if test="resultId != null">#{resultId},</if>
|
|
||||||
<if test="alarmSource != null">#{alarmSource},</if>
|
|
||||||
<if test="itemId != null">#{itemId},</if>
|
|
||||||
<if test="nodeId != null">#{nodeId},</if>
|
|
||||||
<if test="taskId != null">#{taskId},</if>
|
<if test="taskId != null">#{taskId},</if>
|
||||||
<if test="taskName != null">#{taskName},</if>
|
<if test="taskName != null">#{taskName},</if>
|
||||||
<if test="robotId != null">#{robotId},</if>
|
<if test="robotId != null">#{robotId},</if>
|
||||||
@ -163,10 +143,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">remark = #{remark},</if>
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
<if test="alarmCode != null">alarm_code = #{alarmCode},</if>
|
<if test="alarmCode != null">alarm_code = #{alarmCode},</if>
|
||||||
<if test="taskInstanceId != null">task_instance_id = #{taskInstanceId},</if>
|
<if test="taskInstanceId != null">task_instance_id = #{taskInstanceId},</if>
|
||||||
<if test="resultId != null">result_id = #{resultId},</if>
|
|
||||||
<if test="alarmSource != null">alarm_source = #{alarmSource},</if>
|
|
||||||
<if test="itemId != null">item_id = #{itemId},</if>
|
|
||||||
<if test="nodeId != null">node_id = #{nodeId},</if>
|
|
||||||
<if test="taskId != null">task_id = #{taskId},</if>
|
<if test="taskId != null">task_id = #{taskId},</if>
|
||||||
<if test="taskName != null">task_name = #{taskName},</if>
|
<if test="taskName != null">task_name = #{taskName},</if>
|
||||||
<if test="robotId != null">robot_id = #{robotId},</if>
|
<if test="robotId != null">robot_id = #{robotId},</if>
|
||||||
@ -199,8 +175,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<select id="selectInspectionAlarmVoList" parameterType="InspectionAlarm" resultMap="InspectionAlarmVoResult">
|
<select id="selectInspectionAlarmVoList" parameterType="InspectionAlarm" resultMap="InspectionAlarmVoResult">
|
||||||
select a.id, a.create_by, a.create_time, a.update_by, a.update_time, a.remark,
|
select a.id, a.create_by, a.create_time, a.update_by, a.update_time, a.remark,
|
||||||
a.alarm_code, a.task_instance_id, a.result_id, a.alarm_source, a.item_id, a.node_id,
|
a.alarm_code, a.task_instance_id, a.task_id, t.task_name, a.robot_id, r.robot_name,
|
||||||
a.task_id, t.task_name, a.robot_id, r.robot_name,
|
|
||||||
a.alarm_level, a.alarm_type, a.alarm_title, a.alarm_content, a.alarm_location,
|
a.alarm_level, a.alarm_type, a.alarm_title, a.alarm_content, a.alarm_location,
|
||||||
a.alarm_time, a.handle_status, a.handler, a.handle_time, a.handle_remark, a.evidence_image,
|
a.alarm_time, a.handle_status, a.handler, a.handle_time, a.handle_remark, a.evidence_image,
|
||||||
u1.nick_name as create_by_name, u2.nick_name as update_by_name
|
u1.nick_name as create_by_name, u2.nick_name as update_by_name
|
||||||
|
|||||||
@ -35,12 +35,6 @@
|
|||||||
<artifactId>cmvr-iot-grpc-client</artifactId>
|
<artifactId>cmvr-iot-grpc-client</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 设备终端配置,用于工作流报警监听组件按终端ID解析边缘端IP -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.cmvr</groupId>
|
|
||||||
<artifactId>cmvr-iot-device</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.graalvm.js</groupId>
|
<groupId>org.graalvm.js</groupId>
|
||||||
<artifactId>js</artifactId>
|
<artifactId>js</artifactId>
|
||||||
@ -53,4 +47,4 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@ -74,18 +74,12 @@ public enum ActionEnum {
|
|||||||
INTENT_RECOGNITION("LLM", "INTENT_RECOGNITION", "意图识别"),
|
INTENT_RECOGNITION("LLM", "INTENT_RECOGNITION", "意图识别"),
|
||||||
GENERATE_ADVANCED_AUDIO("LLM", "GENERATE_ADVANCED_AUDIO", "tts语音合成"),
|
GENERATE_ADVANCED_AUDIO("LLM", "GENERATE_ADVANCED_AUDIO", "tts语音合成"),
|
||||||
AI_AGENT_PLATFORM("LLM", "AI_AGENT_PLATFORM", "商道智能体"),
|
AI_AGENT_PLATFORM("LLM", "AI_AGENT_PLATFORM", "商道智能体"),
|
||||||
INSPECTION_METER_RECOGNIZE("LLM", "INSPECTION_METER_RECOGNIZE", "巡检仪表读数识别"),
|
|
||||||
AI_TTS("LLM", "AI_TTS", "tts语音播放"),
|
AI_TTS("LLM", "AI_TTS", "tts语音播放"),
|
||||||
GET_CURRENT_PAGE("LLM", "GET_CURRENT_PAGE", "获取当前页面名称"),
|
GET_CURRENT_PAGE("LLM", "GET_CURRENT_PAGE", "获取当前页面名称"),
|
||||||
|
|
||||||
// 触控交互
|
// 触控交互
|
||||||
TI_PATH_SEARCH("EDGE", "TI_PATH_SEARCH", "路径搜索"),
|
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,15 +1,11 @@
|
|||||||
package com.cmvr.test.flow.runtime.event;
|
package com.cmvr.test.flow.runtime.event;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
|
||||||
import com.cmvr.test.enums.ActionEnum;
|
|
||||||
import com.cmvr.test.enums.TaskStatusEnum;
|
import com.cmvr.test.enums.TaskStatusEnum;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程执行事件。
|
* 流程执行事件。
|
||||||
* <p>
|
* <p>
|
||||||
@ -66,26 +62,6 @@ public class FlowExecutionEvent {
|
|||||||
*/
|
*/
|
||||||
private String nodeName;
|
private String nodeName;
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前节点动作,用于业务监听器识别标准化节点输出。
|
|
||||||
*/
|
|
||||||
private ActionEnum action;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点成功执行后的输出参数。
|
|
||||||
*/
|
|
||||||
private JSONObject outputParams;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前工作流绑定的终端ID。
|
|
||||||
*/
|
|
||||||
private String terminalId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 循环节点迭代路径,用于区分同一节点的多次执行结果。
|
|
||||||
*/
|
|
||||||
private List<Integer> iterations;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前任务状态。
|
* 当前任务状态。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -56,11 +55,6 @@ public class FlowAfterInterceptor extends AbstractFlowMsgPreInterceptor{
|
|||||||
.nodeId(message.getNodeId())
|
.nodeId(message.getNodeId())
|
||||||
.nodeCount(message.getGraph().allNodeIds().size())
|
.nodeCount(message.getGraph().allNodeIds().size())
|
||||||
.nodeType(message.getNodeType())
|
.nodeType(message.getNodeType())
|
||||||
.action(message.getAction())
|
|
||||||
.outputParams(result != null ? result.getOutputParams() : null)
|
|
||||||
.terminalId(message.getTerminalId())
|
|
||||||
.iterations(message.getIterations() == null
|
|
||||||
? new ArrayList<>() : new ArrayList<>(message.getIterations()))
|
|
||||||
.errorMessage(errorMessage)
|
.errorMessage(errorMessage)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|||||||
@ -1,109 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
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,23 +1,28 @@
|
|||||||
package com.cmvr.test.flow.runtime.operator.edge.ti;
|
package com.cmvr.test.flow.runtime.operator.edge.ti;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.text.StrPool;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.common.config.properties.MinioProperties;
|
||||||
|
import com.cmvr.common.core.minio.MinioService;
|
||||||
import com.cmvr.common.exception.GlobalException;
|
import com.cmvr.common.exception.GlobalException;
|
||||||
import com.cmvr.common.utils.http.CallAPIUtil;
|
import com.cmvr.common.utils.http.CallAPIUtil;
|
||||||
import com.cmvr.llm.config.APIProperties;
|
import com.cmvr.llm.config.APIProperties;
|
||||||
import com.cmvr.llm.config.AgentConfig;
|
import com.cmvr.llm.config.AgentConfig;
|
||||||
|
import com.cmvr.llm.util.LargeModelFileUploadUtil;
|
||||||
import com.cmvr.test.enums.ActionEnum;
|
import com.cmvr.test.enums.ActionEnum;
|
||||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||||
import com.cmvr.test.flow.runtime.operator.edge.EdgeOperateService;
|
import com.cmvr.test.flow.runtime.operator.edge.EdgeOperateService;
|
||||||
import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
|
import com.cmvr.test.service.ex.ExTiVehicleFunctionService;
|
||||||
import com.cmvr.test.service.ShangdaoFileService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -29,8 +34,9 @@ import java.util.Map;
|
|||||||
public class TiTouchOperateService implements EdgeOperateService {
|
public class TiTouchOperateService implements EdgeOperateService {
|
||||||
|
|
||||||
private final ExTiVehicleFunctionService exTiVehicleFunctionService;
|
private final ExTiVehicleFunctionService exTiVehicleFunctionService;
|
||||||
|
private final MinioService minioService;
|
||||||
|
private final MinioProperties minioProps;
|
||||||
private final APIProperties apiProperties;
|
private final APIProperties apiProperties;
|
||||||
private final ShangdaoFileService shangdaoFileService;
|
|
||||||
|
|
||||||
// 设置运动模式
|
// 设置运动模式
|
||||||
public static final List<String> DRIVE_MODE_PATH = Arrays.asList("主页", "设置", "驾驶模式", "运动模式");
|
public static final List<String> DRIVE_MODE_PATH = Arrays.asList("主页", "设置", "驾驶模式", "运动模式");
|
||||||
@ -75,11 +81,11 @@ public class TiTouchOperateService implements EdgeOperateService {
|
|||||||
case TI_TOUCH_COORDINATES: {
|
case TI_TOUCH_COORDINATES: {
|
||||||
// 页面图片(相机)
|
// 页面图片(相机)
|
||||||
String imageUrl = inputParams.getString("imageUrl");
|
String imageUrl = inputParams.getString("imageUrl");
|
||||||
String pageImageUrl = shangdaoFileService.uploadFromMinio(imageUrl);
|
String pageImageUrl = uploadImageFromMinio(imageUrl);
|
||||||
|
|
||||||
// icon图片
|
// icon图片
|
||||||
String iconUrl = inputParams.getString("iconUrl");
|
String iconUrl = inputParams.getString("iconUrl");
|
||||||
String iconImageUrl = shangdaoFileService.uploadFromMinio(iconUrl);
|
String iconImageUrl = uploadImageFromMinio(iconUrl);
|
||||||
|
|
||||||
// 获取触控坐标
|
// 获取触控坐标
|
||||||
String touchCoordinates = getTouchCoordinates(pageImageUrl, iconImageUrl);
|
String touchCoordinates = getTouchCoordinates(pageImageUrl, iconImageUrl);
|
||||||
@ -103,10 +109,10 @@ public class TiTouchOperateService implements EdgeOperateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String test(String imageUrl,String iconUrl) {
|
public String test(String imageUrl,String iconUrl) {
|
||||||
String pageImageUrl = shangdaoFileService.uploadFromMinio(imageUrl);
|
String pageImageUrl = uploadImageFromMinio(imageUrl);
|
||||||
|
|
||||||
// icon图片
|
// icon图片
|
||||||
String iconImageUrl = shangdaoFileService.uploadFromMinio(iconUrl);
|
String iconImageUrl = uploadImageFromMinio(iconUrl);
|
||||||
|
|
||||||
// 获取触控坐标
|
// 获取触控坐标
|
||||||
return getTouchCoordinates(pageImageUrl, iconImageUrl);
|
return getTouchCoordinates(pageImageUrl, iconImageUrl);
|
||||||
@ -136,6 +142,29 @@ public class TiTouchOperateService implements EdgeOperateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 MinIO 文件访问地址上传到商道,返回商道文件地址
|
||||||
|
*/
|
||||||
|
private String uploadImageFromMinio(String url) {
|
||||||
|
try {
|
||||||
|
String bucketName = minioProps.getBucketName();
|
||||||
|
String prefix = StrPool.SLASH + bucketName + StrPool.SLASH;
|
||||||
|
|
||||||
|
String objectName = StrUtil.removePrefix(
|
||||||
|
url,
|
||||||
|
StrUtil.subBefore(url, prefix, true) + prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
File imageFile = minioService.getFile(bucketName, objectName);
|
||||||
|
byte[] imageBytes = FileUtil.readBytes(imageFile);
|
||||||
|
|
||||||
|
return LargeModelFileUploadUtil.uploadFile(imageBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("上传 MinIO 文件到商道失败,url={}", url, e);
|
||||||
|
throw new GlobalException("图片上传失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String queryResultByRunId(String processId, AgentConfig agentConfig) {
|
private String queryResultByRunId(String processId, AgentConfig agentConfig) {
|
||||||
Map<String, String> headers = new HashMap<>();
|
Map<String, String> headers = new HashMap<>();
|
||||||
headers.put("Apikey", agentConfig.getAppKey());
|
headers.put("Apikey", agentConfig.getAppKey());
|
||||||
|
|||||||
@ -1,310 +0,0 @@
|
|||||||
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,28 +1,21 @@
|
|||||||
package com.cmvr.test.model.vo;
|
package com.cmvr.test.model.vo;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import io.swagger.annotations.ApiModel;
|
|
||||||
import io.swagger.annotations.ApiModelProperty;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流节点执行请求
|
* 工作流节点执行请求
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ApiModel(value = "FlowActionRequestVO", description = "工作流单节点试执行请求")
|
|
||||||
public class FlowActionRequestVO {
|
public class FlowActionRequestVO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动作
|
* 动作
|
||||||
*/
|
*/
|
||||||
@ApiModelProperty(value = "动作编码。巡检新增动作:INSPECTION_METER_RECOGNIZE、INSPECTION_MANUAL_REVIEW_CREATE",
|
|
||||||
required = true, example = "INSPECTION_METER_RECOGNIZE")
|
|
||||||
private String action;
|
private String action;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行参数
|
* 执行参数
|
||||||
*/
|
*/
|
||||||
@ApiModelProperty(value = "节点参数。仪表识别参见InspectionMeterRecognizeConfigVO,人工判断参见InspectionManualReviewConfigVO",
|
|
||||||
required = true)
|
|
||||||
private JSONObject payload;
|
private JSONObject payload;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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,13 +14,8 @@ import com.cmvr.edge.client.service.EdgeHlcService;
|
|||||||
import com.cmvr.edge.client.service.EdgeMicrophoneService;
|
import com.cmvr.edge.client.service.EdgeMicrophoneService;
|
||||||
import com.cmvr.edge.client.service.EdgeSpeakerService;
|
import com.cmvr.edge.client.service.EdgeSpeakerService;
|
||||||
import com.cmvr.edge.client.service.EdgeAgvService;
|
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.llm.service.LLMAiAgentPlatformService;
|
||||||
import com.cmvr.test.enums.ActionEnum;
|
import com.cmvr.test.enums.ActionEnum;
|
||||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
|
||||||
import com.cmvr.test.flow.runtime.operator.edge.EdgeManualInspectionOperateService;
|
|
||||||
import com.cmvr.test.flow.runtime.operator.llm.InspectionMeterRecognizeOperateService;
|
|
||||||
import com.cmvr.test.model.vo.FlowActionRequestVO;
|
import com.cmvr.test.model.vo.FlowActionRequestVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -37,10 +32,6 @@ public class FlowActionExecutorService {
|
|||||||
private final EdgeHlcService edgeHlcService;
|
private final EdgeHlcService edgeHlcService;
|
||||||
private final LLMAiAgentPlatformService llmAiAgentPlatformService;
|
private final LLMAiAgentPlatformService llmAiAgentPlatformService;
|
||||||
private final EdgeAgvService edgeAgvService;
|
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) {
|
public String actionExecute(FlowActionRequestVO req) {
|
||||||
|
|
||||||
@ -67,16 +58,6 @@ public class FlowActionExecutorService {
|
|||||||
String terminalId = payload.getString("terminalId");
|
String terminalId = payload.getString("terminalId");
|
||||||
String deviceId = payload.getString("deviceId");
|
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)) {
|
if (StrUtil.isEmpty(terminalId) || StrUtil.isEmpty(deviceId)) {
|
||||||
throw new GlobalException("EDGE 类型动作必须提供 terminalId 和 deviceId");
|
throw new GlobalException("EDGE 类型动作必须提供 terminalId 和 deviceId");
|
||||||
}
|
}
|
||||||
@ -108,10 +89,6 @@ public class FlowActionExecutorService {
|
|||||||
EdgeTouchVO edgeTouchVO = payload.to(EdgeTouchVO.class);
|
EdgeTouchVO edgeTouchVO = payload.to(EdgeTouchVO.class);
|
||||||
return edgeHlcService.touch(edgeTouchVO);
|
return edgeHlcService.touch(edgeTouchVO);
|
||||||
|
|
||||||
// ==== 机械臂 ====
|
|
||||||
case ARM_MOVE_TO_POINT:
|
|
||||||
return executeArmMoveToPoint(edgeCommonVO, payload);
|
|
||||||
|
|
||||||
// ==== 语料 ====
|
// ==== 语料 ====
|
||||||
case VI_PLAY_CORPUS:
|
case VI_PLAY_CORPUS:
|
||||||
String audioPath = payload.getString("audioPath");
|
String audioPath = payload.getString("audioPath");
|
||||||
@ -168,83 +145,6 @@ 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) {
|
private String executeLLMAction(ActionEnum action, FlowActionRequestVO req) {
|
||||||
log.info("LLM 执行动作: {}", action);
|
log.info("LLM 执行动作: {}", action);
|
||||||
|
|
||||||
@ -257,19 +157,9 @@ public class FlowActionExecutorService {
|
|||||||
return "LLM 高级音频生成 OK";
|
return "LLM 高级音频生成 OK";
|
||||||
case AI_AGENT_PLATFORM:
|
case AI_AGENT_PLATFORM:
|
||||||
return llmAiAgentPlatformService.query(action.getAction(), req.getPayload().getJSONObject("config").getString("text"), req.getPayload().getJSONObject("config").getString("apiKey"), req.getPayload().getBoolean("invokeTts"), req.getPayload().getJSONObject("tts")).toString();
|
return llmAiAgentPlatformService.query(action.getAction(), req.getPayload().getJSONObject("config").getString("text"), req.getPayload().getJSONObject("config").getString("apiKey"), req.getPayload().getBoolean("invokeTts"), req.getPayload().getJSONObject("tts")).toString();
|
||||||
case INSPECTION_METER_RECOGNIZE:
|
|
||||||
return inspectionMeterRecognizeOperateService.execute(
|
|
||||||
buildSingleNodeMessage(action, req.getPayload())).getOutputParams().toJSONString();
|
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedOperationException("未实现的 LLM Action: " + action);
|
throw new UnsupportedOperationException("未实现的 LLM Action: " + action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TaskNodeExecuteMessage buildSingleNodeMessage(ActionEnum action, JSONObject payload) {
|
|
||||||
TaskNodeExecuteMessage message = new TaskNodeExecuteMessage();
|
|
||||||
message.setAction(action);
|
|
||||||
message.setInputParams(payload == null ? new JSONObject() : payload);
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
BIN
logs/audio-dump/robot-to-backend-1784086438864.wav
Normal file
BIN
logs/audio-dump/robot-to-backend-1784086438864.wav
Normal file
Binary file not shown.
BIN
logs/audio-dump/robot-to-backend-1784086792085.wav
Normal file
BIN
logs/audio-dump/robot-to-backend-1784086792085.wav
Normal file
Binary file not shown.
@ -1,74 +0,0 @@
|
|||||||
-- 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,10 +124,6 @@ CREATE TABLE `inspection_alarm` (
|
|||||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||||
`alarm_code` varchar(64) DEFAULT NULL COMMENT '告警编码',
|
`alarm_code` varchar(64) DEFAULT NULL COMMENT '告警编码',
|
||||||
`task_instance_id` varchar(64) DEFAULT NULL COMMENT '任务执行实例ID',
|
`task_instance_id` varchar(64) DEFAULT NULL COMMENT '任务执行实例ID',
|
||||||
`result_id` varchar(64) DEFAULT NULL COMMENT '关联巡检结果ID',
|
|
||||||
`alarm_source` varchar(20) DEFAULT NULL COMMENT '告警来源(PPE/METER/MANUAL)',
|
|
||||||
`item_id` varchar(64) DEFAULT NULL COMMENT '检测项ID',
|
|
||||||
`node_id` varchar(64) DEFAULT NULL COMMENT '工作流节点ID',
|
|
||||||
`task_id` varchar(64) DEFAULT NULL COMMENT '任务ID',
|
`task_id` varchar(64) DEFAULT NULL COMMENT '任务ID',
|
||||||
`task_name` varchar(100) DEFAULT NULL COMMENT '任务名称',
|
`task_name` varchar(100) DEFAULT NULL COMMENT '任务名称',
|
||||||
`robot_id` varchar(64) DEFAULT NULL COMMENT '机器人ID',
|
`robot_id` varchar(64) DEFAULT NULL COMMENT '机器人ID',
|
||||||
@ -145,7 +141,6 @@ CREATE TABLE `inspection_alarm` (
|
|||||||
`evidence_image` varchar(500) DEFAULT NULL COMMENT '图片证据',
|
`evidence_image` varchar(500) DEFAULT NULL COMMENT '图片证据',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_alarm_code` (`alarm_code`),
|
UNIQUE KEY `uk_alarm_code` (`alarm_code`),
|
||||||
UNIQUE KEY `uk_alarm_result_id` (`result_id`),
|
|
||||||
KEY `idx_task_instance_id` (`task_instance_id`),
|
KEY `idx_task_instance_id` (`task_instance_id`),
|
||||||
KEY `idx_task_id` (`task_id`),
|
KEY `idx_task_id` (`task_id`),
|
||||||
KEY `idx_robot_id` (`robot_id`),
|
KEY `idx_robot_id` (`robot_id`),
|
||||||
@ -182,64 +177,3 @@ CREATE TABLE `inspection_task_log` (
|
|||||||
KEY `idx_log_time` (`log_time`)
|
KEY `idx_log_time` (`log_time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检任务执行日志表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检任务执行日志表';
|
||||||
|
|
||||||
-- 6. 统一巡检结果表
|
|
||||||
DROP TABLE IF EXISTS `inspection_result_media`;
|
|
||||||
DROP TABLE IF EXISTS `inspection_result`;
|
|
||||||
CREATE TABLE `inspection_result` (
|
|
||||||
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
|
||||||
`result_code` varchar(64) NOT NULL COMMENT '结果编码',
|
|
||||||
`dedupe_key` varchar(255) NOT NULL COMMENT '结果幂等键',
|
|
||||||
`task_instance_id` varchar(64) DEFAULT NULL COMMENT '巡检任务实例数据库ID',
|
|
||||||
`flow_instance_id` varchar(64) DEFAULT NULL COMMENT '工作流实例ID',
|
|
||||||
`task_id` varchar(64) DEFAULT NULL COMMENT '巡检任务ID',
|
|
||||||
`item_id` varchar(64) DEFAULT NULL COMMENT '检测项ID',
|
|
||||||
`node_id` varchar(64) DEFAULT NULL COMMENT '工作流节点ID',
|
|
||||||
`node_name` varchar(255) DEFAULT NULL COMMENT '工作流节点名称',
|
|
||||||
`iteration_path` varchar(255) DEFAULT NULL COMMENT '循环迭代路径JSON',
|
|
||||||
`result_type` varchar(20) NOT NULL COMMENT '结果类型(PPE/METER/MANUAL)',
|
|
||||||
`result_status` varchar(32) NOT NULL COMMENT 'PENDING/NORMAL/ABNORMAL/RECOGNIZE_FAILED',
|
|
||||||
`result_name` varchar(255) NOT NULL COMMENT '检查名称',
|
|
||||||
`value_number` decimal(20,8) DEFAULT NULL COMMENT '结构化数值',
|
|
||||||
`value_text` varchar(1000) DEFAULT NULL COMMENT '文本结果',
|
|
||||||
`unit` varchar(32) DEFAULT NULL COMMENT '单位',
|
|
||||||
`alarm_level` tinyint DEFAULT NULL COMMENT '告警级别(1提示 2警告 3严重)',
|
|
||||||
`alarm_message` varchar(1000) DEFAULT NULL COMMENT '告警描述',
|
|
||||||
`confidence` decimal(10,8) DEFAULT NULL COMMENT '模型置信度',
|
|
||||||
`source_event_id` varchar(128) DEFAULT NULL COMMENT '外部PPE事件ID',
|
|
||||||
`evidence_url` varchar(1000) DEFAULT NULL COMMENT '主要MinIO证据地址',
|
|
||||||
`evidence_type` varchar(20) DEFAULT NULL COMMENT '主要证据类型(IMAGE/VIDEO)',
|
|
||||||
`raw_result` mediumtext COMMENT '模型或边缘端原始返回',
|
|
||||||
`matched_rule_json` text COMMENT '最终命中的报警规则快照',
|
|
||||||
`rule_snapshot_json` mediumtext COMMENT '本次执行使用的完整规则快照',
|
|
||||||
`reviewer` varchar(64) DEFAULT NULL COMMENT '人工复核人',
|
|
||||||
`review_time` datetime DEFAULT NULL COMMENT '人工复核时间',
|
|
||||||
`review_remark` varchar(1000) DEFAULT NULL COMMENT '人工复核说明',
|
|
||||||
`review_version` int NOT NULL DEFAULT 0 COMMENT '人工复核乐观锁版本',
|
|
||||||
`alarm_id` varchar(64) DEFAULT NULL COMMENT '关联告警ID',
|
|
||||||
`occurred_time` datetime NOT NULL COMMENT '结果发生时间',
|
|
||||||
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
|
||||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
|
||||||
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
|
||||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
|
||||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uk_inspection_result_code` (`result_code`),
|
|
||||||
UNIQUE KEY `uk_inspection_result_dedupe` (`dedupe_key`),
|
|
||||||
KEY `idx_result_task_instance` (`task_instance_id`),
|
|
||||||
KEY `idx_result_flow_instance` (`flow_instance_id`),
|
|
||||||
KEY `idx_result_status` (`result_status`),
|
|
||||||
KEY `idx_result_type` (`result_type`),
|
|
||||||
KEY `idx_result_occurred_time` (`occurred_time`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='统一巡检结果表';
|
|
||||||
|
|
||||||
-- 7. 巡检结果媒体证据表
|
|
||||||
CREATE TABLE `inspection_result_media` (
|
|
||||||
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
|
||||||
`result_id` varchar(32) NOT NULL COMMENT '巡检结果ID',
|
|
||||||
`media_type` varchar(20) NOT NULL COMMENT 'IMAGE或VIDEO',
|
|
||||||
`media_url` varchar(1000) NOT NULL COMMENT 'MinIO媒体地址',
|
|
||||||
`sort_order` int NOT NULL DEFAULT 0 COMMENT '排序号',
|
|
||||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_result_media_result_id` (`result_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检结果媒体证据表';
|
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
-- 统一巡检结果和三级告警增量脚本。
|
|
||||||
-- 执行前请先备份数据库;本脚本不删除现有巡检数据。
|
|
||||||
|
|
||||||
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