feat(inspection): 添加边缘端gRPC服务IP字段并优化报警去重逻辑

- 在AlertEnvelope中添加grpcIp字段用于前端展示和问题定位
- 将数据库表inspection_detection_alert的幂等键改为仅记录模式,使用event_id进行重复过滤
- 移除检测框明细表及相关处理逻辑,简化数据结构
- 更新麦克风和扬声器控制器的参数传递方式,统一使用RequestBody
- 新增EdgeSpeakerPlayAudioVO类用于扬声器播放音频接口参数传递
This commit is contained in:
lixiaolong 2026-07-22 14:46:32 +08:00
parent ecda2b0ebc
commit 922d30632b
10 changed files with 108 additions and 176 deletions

View File

@ -6,12 +6,10 @@ import com.cmvr.edge.client.model.microphone.EdgeMicrophoneVolumeVO;
import com.cmvr.edge.client.service.EdgeMicrophoneService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
/**
* 麦克风管理
@ -27,14 +25,17 @@ public class EdgeMicrophoneController {
@ApiOperation("获取麦克风状态")
@PreAuthorize("@ss.hasPermi('system:microphone:query')")
@GetMapping("/status")
public AjaxResult getStatus(EdgeCommonVO edgeCommonVO) {
public AjaxResult getStatus(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
edgeCommonVO.setTerminalId(terminalId);
edgeCommonVO.setDeviceId(deviceId);
return AjaxResult.success(microphoneService.getStatus(edgeCommonVO));
}
@ApiOperation("开始录音")
@PreAuthorize("@ss.hasPermi('system:microphone:record')")
@PostMapping("/start")
public AjaxResult startRecord(EdgeCommonVO edgeCommonVO) {
public AjaxResult startRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
String filePath = microphoneService.startRecord(edgeCommonVO);
return AjaxResult.success(filePath);
}
@ -42,7 +43,7 @@ public class EdgeMicrophoneController {
@ApiOperation("停止录音")
@PreAuthorize("@ss.hasPermi('system:microphone:stop')")
@PostMapping("/stop")
public AjaxResult stopRecord(EdgeCommonVO edgeCommonVO) {
public AjaxResult stopRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
microphoneService.stopRecord(edgeCommonVO);
return AjaxResult.success();
}
@ -50,7 +51,7 @@ public class EdgeMicrophoneController {
@ApiOperation("暂停录音")
@PreAuthorize("@ss.hasPermi('system:microphone:pause')")
@PostMapping("/pause")
public AjaxResult pauseRecord(EdgeCommonVO edgeCommonVO) {
public AjaxResult pauseRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
microphoneService.pauseRecord(edgeCommonVO);
return AjaxResult.success();
}
@ -58,7 +59,7 @@ public class EdgeMicrophoneController {
@ApiOperation("恢复录音")
@PreAuthorize("@ss.hasPermi('system:microphone:resume')")
@PostMapping("/resume")
public AjaxResult resumeRecord(EdgeCommonVO edgeCommonVO) {
public AjaxResult resumeRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
microphoneService.resumeRecord(edgeCommonVO);
return AjaxResult.success();
}
@ -66,7 +67,7 @@ public class EdgeMicrophoneController {
@ApiOperation("设置音量")
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
@PostMapping("/volume")
public AjaxResult setVolume(EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
microphoneService.setVolume(edgeMicrophoneVolumeVO);
return AjaxResult.success();
}
@ -74,7 +75,10 @@ public class EdgeMicrophoneController {
@ApiOperation("获取音量")
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
@GetMapping("/volume")
public AjaxResult getVolume(EdgeCommonVO edgeCommonVO) {
public AjaxResult getVolume(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
edgeCommonVO.setTerminalId(terminalId);
edgeCommonVO.setDeviceId(deviceId);
return AjaxResult.success(microphoneService.getVolume(edgeCommonVO));
}
}

View File

@ -1,6 +1,9 @@
package com.cmvr.web.controller.api;
import cmvr.api.SpeakerCommand;
import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.model.microphone.EdgeMicrophoneVolumeVO;
import com.cmvr.edge.client.model.speaker.EdgeSpeakerPlayAudioVO;
import com.cmvr.edge.client.service.EdgeSpeakerService;
import com.cmvr.common.core.domain.AjaxResult;
import io.swagger.annotations.Api;
@ -69,14 +72,8 @@ public class EdgeSpeakerController {
@ApiOperation("播放音频")
@PreAuthorize("@ss.hasPermi('system:speaker:play')")
@PostMapping("/play")
public AjaxResult playAudio(
@ApiParam(value = "终端设备ID", required = true)
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId,
@ApiParam(value = "音频文件路径", required = true)
@RequestParam String audioPath) {
speakerService.playAudio(terminalId, deviceId, audioPath);
public AjaxResult playAudio(@RequestBody EdgeSpeakerPlayAudioVO edgeSpeakerPlayAudioVO) {
speakerService.playAudio(edgeSpeakerPlayAudioVO.getTerminalId(), edgeSpeakerPlayAudioVO.getDeviceId(), edgeSpeakerPlayAudioVO.getAudioPath());
return AjaxResult.success();
}
@ -86,12 +83,8 @@ public class EdgeSpeakerController {
@ApiOperation("停止播放")
@PreAuthorize("@ss.hasPermi('system:speaker:stop')")
@PostMapping("/stop")
public AjaxResult stopPlayback(
@ApiParam(value = "终端设备ID", required = true)
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId) {
speakerService.stopPlayback(terminalId, deviceId);
public AjaxResult stopPlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
speakerService.stopPlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
return AjaxResult.success();
}
@ -101,12 +94,8 @@ public class EdgeSpeakerController {
@ApiOperation("暂停播放")
@PreAuthorize("@ss.hasPermi('system:speaker:pause')")
@PostMapping("/pause")
public AjaxResult pausePlayback(
@ApiParam(value = "终端设备ID", required = true)
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId) {
speakerService.pausePlayback(terminalId, deviceId);
public AjaxResult pausePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
speakerService.pausePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
return AjaxResult.success();
}
@ -116,12 +105,8 @@ public class EdgeSpeakerController {
@ApiOperation("恢复播放")
@PreAuthorize("@ss.hasPermi('system:speaker:resume')")
@PostMapping("/resume")
public AjaxResult resumePlayback(
@ApiParam(value = "终端设备ID", required = true)
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId) {
speakerService.resumePlayback(terminalId, deviceId);
public AjaxResult resumePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
speakerService.resumePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
return AjaxResult.success();
}
@ -131,14 +116,8 @@ public class EdgeSpeakerController {
@ApiOperation("设置音量")
@PreAuthorize("@ss.hasPermi('system:speaker:volume')")
@PostMapping("/volume")
public AjaxResult setVolume(
@ApiParam(value = "终端设备ID", required = true)
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId,
@ApiParam(value = "音量大小0-100", required = true)
@RequestParam int volume) {
speakerService.setVolume(terminalId, deviceId, volume);
public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
speakerService.setVolume(edgeMicrophoneVolumeVO.getTerminalId(), edgeMicrophoneVolumeVO.getDeviceId(), edgeMicrophoneVolumeVO.getVolume());
return AjaxResult.success();
}

View File

@ -0,0 +1,20 @@
package com.cmvr.edge.client.model.speaker;
import com.cmvr.edge.client.model.EdgeCommonVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
/**
* 扬声器播放音频请求参数
*/
@Data
@ApiModel("扬声器播放音频VO")
public class EdgeSpeakerPlayAudioVO extends EdgeCommonVO {
@ApiModelProperty("音频文件路径")
@NotEmpty(message = "音频文件路径不能为空")
private String audioPath;
}

View File

@ -26,6 +26,7 @@ public class InspectionDetectionAlert implements Serializable
@TableField("schema_version")
private String schema;
private String sourceId;
private String grpcIp;
@TableField("source_sequence")
private Long sequence;
private Long capturedAtNs;

View File

@ -1,30 +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 lombok.Data;
import java.io.Serializable;
/**
* PPE 报警检测框明细实体
*/
@Data
@TableName("inspection_detection_detail")
public class InspectionDetectionDetail implements Serializable
{
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private String id;
private String alertId;
private String label;
private Double confidence;
private Double xMin;
private Double yMin;
private Double xMax;
private Double yMax;
private Long trackId;
}

View File

@ -21,6 +21,10 @@ public class AlertEnvelope
@JsonProperty("source_id")
private String sourceId;
/** 边缘端 gRPC 服务 IP用于前端展示和问题定位。 */
@JsonProperty("grpc_ip")
private String grpcIp;
/** 视频源内递增序号。 */
@JsonProperty("sequence")
private Long sequence;

View File

@ -1,11 +0,0 @@
package com.cmvr.inspection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmvr.inspection.domain.InspectionDetectionDetail;
/**
* PPE 报警检测框明细 Mapper
*/
public interface InspectionDetectionDetailMapper extends BaseMapper<InspectionDetectionDetail>
{
}

View File

@ -11,7 +11,7 @@ public interface IInspectionDetectionAlertService
* 接收并持久化一条 PPE 违规报警
*
* @param envelope 报警信封
* @param requestIdempotencyKey HTTP 请求头中的幂等键允许为空
* @param requestIdempotencyKey HTTP 请求头中的幂等键仅记录不用于重复过滤
* @return true 表示本次新增false 表示重复事件
*/
boolean receive(AlertEnvelope envelope, String requestIdempotencyKey);

View File

@ -4,15 +4,11 @@ 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.inspection.domain.InspectionDetectionAlert;
import com.cmvr.inspection.domain.InspectionDetectionDetail;
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
import com.cmvr.inspection.domain.dto.alert.AlertImage;
import com.cmvr.inspection.domain.dto.alert.BoundingBox;
import com.cmvr.inspection.domain.dto.alert.Detection;
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
import com.cmvr.inspection.exception.DetectionAlertTemporaryException;
import com.cmvr.inspection.mapper.InspectionDetectionAlertMapper;
import com.cmvr.inspection.mapper.InspectionDetectionDetailMapper;
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
@ -27,7 +23,6 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
@ -44,13 +39,11 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
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_IDEMPOTENCY_KEY_LENGTH = 191;
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 InspectionDetectionDetailMapper detailMapper;
private final MinioService minioService;
private final MinioProperties minioProperties;
private final ObjectMapper objectMapper;
@ -59,12 +52,12 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
@Transactional(rollbackFor = Exception.class)
public boolean receive(AlertEnvelope envelope, String requestIdempotencyKey)
{
String idempotencyKey = validateAndResolveIdempotencyKey(envelope, requestIdempotencyKey);
String eventId = trimToNull(envelope.getPayload().getEventId());
String eventId = validateAndResolveEventId(envelope);
String idempotencyKey = trimToNull(requestIdempotencyKey);
if (exists(idempotencyKey, eventId))
if (exists(eventId))
{
log.info("忽略重复PPE报警idempotencyKey={}eventId={}", idempotencyKey, eventId);
log.info("忽略重复PPE报警eventId={}", eventId);
return false;
}
@ -85,7 +78,6 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
}
}
saveDetectionDetails(alert.getId(), envelope.getPayload().getDetections());
log.info("PPE报警接收成功alertId={}eventId={}sourceId={}",
alert.getId(), eventId, envelope.getSourceId());
return true;
@ -102,9 +94,9 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
}
/**
* 校验协议关键字段按请求头event_idtrace_id 的顺序生成幂等键
* 校验协议关键字段返回用于重复过滤的 event_id
*/
private String validateAndResolveIdempotencyKey(AlertEnvelope envelope, String requestIdempotencyKey)
private String validateAndResolveEventId(AlertEnvelope envelope)
{
if (envelope == null)
{
@ -120,31 +112,17 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
}
String eventId = trimToNull(envelope.getPayload().getEventId());
if (eventId != null && eventId.length() > MAX_EVENT_ID_LENGTH)
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);
}
String idempotencyKey = trimToNull(requestIdempotencyKey);
if (idempotencyKey == null)
{
idempotencyKey = eventId;
}
if (idempotencyKey == null)
{
idempotencyKey = trimToNull(envelope.getTraceId());
}
if (idempotencyKey == null)
{
throw new DetectionAlertBadRequestException("Idempotency-Key、payload.event_id和trace_id不能同时为空");
}
if (idempotencyKey.length() > MAX_IDEMPOTENCY_KEY_LENGTH)
{
throw new DetectionAlertBadRequestException("幂等键长度不能超过" + MAX_IDEMPOTENCY_KEY_LENGTH);
}
validateImage(envelope.getPayload().getImage());
return idempotencyKey;
return eventId;
}
private void validateImage(AlertImage image)
@ -172,14 +150,10 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
}
}
private boolean exists(String idempotencyKey, String eventId)
private boolean exists(String eventId)
{
LambdaQueryWrapper<InspectionDetectionAlert> query = new LambdaQueryWrapper<>();
query.eq(InspectionDetectionAlert::getIdempotencyKey, idempotencyKey);
if (eventId != null)
{
query.or().eq(InspectionDetectionAlert::getEventId, eventId);
}
query.eq(InspectionDetectionAlert::getEventId, eventId);
return alertMapper.selectCount(query) > 0;
}
@ -192,6 +166,7 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
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());
@ -295,36 +270,6 @@ public class InspectionDetectionAlertServiceImpl implements IInspectionDetection
}
}
private void saveDetectionDetails(String alertId, List<Detection> detections)
{
if (detections == null || detections.isEmpty())
{
return;
}
for (Detection detection : detections)
{
if (detection == null)
{
throw new DetectionAlertBadRequestException("payload.detections不能包含null元素");
}
InspectionDetectionDetail detail = new InspectionDetectionDetail();
detail.setId(newId());
detail.setAlertId(alertId);
detail.setLabel(detection.getLabel());
detail.setConfidence(detection.getConfidence());
detail.setTrackId(detection.getTrackId());
BoundingBox box = detection.getBox();
if (box != null)
{
detail.setXMin(box.getXMin());
detail.setYMin(box.getYMin());
detail.setXMax(box.getXMax());
detail.setYMax(box.getYMax());
}
detailMapper.insert(detail);
}
}
private String buildMinioUrl(String objectName)
{
String baseUrl = StringUtils.removeEnd(minioProperties.getUrl(), "/");

View File

@ -3,10 +3,11 @@
CREATE TABLE IF NOT EXISTS `inspection_detection_alert` (
`id` varchar(32) NOT NULL COMMENT '主键ID',
`event_id` varchar(128) DEFAULT NULL COMMENT '边缘端事件ID',
`idempotency_key` varchar(191) NOT NULL COMMENT 'HTTP幂等键',
`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纳秒',
@ -30,25 +31,44 @@ CREATE TABLE IF NOT EXISTS `inspection_detection_alert` (
`created_at` datetime(3) NOT NULL COMMENT '平台接收时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_detection_alert_event_id` (`event_id`),
UNIQUE KEY `uk_detection_alert_idempotency_key` (`idempotency_key`),
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违规报警主表';
CREATE TABLE IF NOT EXISTS `inspection_detection_detail` (
`id` varchar(32) NOT NULL COMMENT '主键ID',
`alert_id` varchar(32) NOT NULL COMMENT '报警主表ID',
`label` varchar(128) DEFAULT NULL COMMENT '检测标签',
`confidence` decimal(8,6) DEFAULT NULL COMMENT '检测置信度',
`x_min` decimal(12,4) DEFAULT NULL COMMENT '目标框左上X坐标',
`y_min` decimal(12,4) DEFAULT NULL COMMENT '目标框左上Y坐标',
`x_max` decimal(12,4) DEFAULT NULL COMMENT '目标框右下X坐标',
`y_max` decimal(12,4) DEFAULT NULL COMMENT '目标框右下Y坐标',
`track_id` bigint DEFAULT NULL COMMENT '目标跟踪ID',
PRIMARY KEY (`id`),
KEY `idx_detection_detail_alert_id` (`alert_id`),
CONSTRAINT `fk_detection_detail_alert` FOREIGN KEY (`alert_id`)
REFERENCES `inspection_detection_alert` (`id`) ON DELETE CASCADE
) 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幂等键仅记录';