Compare commits

..

No commits in common. "521e3e4e243f4840f532051d3c6ddb70ea49e35f" and "152cae03231196de019a0d9b9fb61053c7d35575" have entirely different histories.

74 changed files with 12765 additions and 9854 deletions

View File

@ -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));
} }
} }

View File

@ -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();
} }

View File

@ -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;
}
}

View File

@ -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必须传alarmLevelreviewVersion用于并发控制")
@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, "巡检结果数据");
}
}

View File

@ -156,13 +156,8 @@ public class TeFlowController extends BaseController {
} }
@PostMapping("/action") @PostMapping("/action")
@ApiOperation(value = "单个节点执行", @ApiOperation("单个节点执行")
notes = "巡检仪表节点action=INSPECTION_METER_RECOGNIZEpayload使用InspectionMeterRecognizeConfigVO" public AjaxResult actionExecute(@RequestBody FlowActionRequestVO flowActionRequestVO) {
+ "人工判断节点action=INSPECTION_MANUAL_REVIEW_CREATEpayload使用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));
} }

View File

@ -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())

View File

@ -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:

View File

@ -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));
} }

View File

@ -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;
}

View File

@ -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;
}
}
} }

View File

@ -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();

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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())

View File

@ -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,

View File

@ -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;

View File

@ -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; //
} }
} }

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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; //
} }
} }

View File

@ -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);
}
}

View File

@ -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=grpcIpvalue=当前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;
}
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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("告警编码")

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
} }
// 任意事件当前任务绑定itemitem一启动就入Map所以统计已完成item要-1 // 任意事件当前任务绑定itemitem一启动就入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
*/ */

View File

@ -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>
{
}

View File

@ -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>
{
}

View File

@ -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>
{
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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("-", "");
}
}

View File

@ -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

View File

@ -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>

View File

@ -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", "创建人工巡检判断任务")
; ;

View File

@ -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;
/** /**
* 当前任务状态 * 当前任务状态
*/ */

View File

@ -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();

View File

@ -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));
}
}
}

View File

@ -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);
}
/**
* 合并mediaListimageUrl和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";
}
}

View File

@ -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());

View File

@ -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>节点输出包含valueunitresultStatus和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;
}
}
}

View File

@ -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;
} }

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
} }

View File

@ -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);
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -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幂等键仅记录';

View File

@ -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='巡检结果媒体证据表';

View File

@ -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;