diff --git a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeMicrophoneController.java b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeMicrophoneController.java index 8289e9e..4a86698 100644 --- a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeMicrophoneController.java +++ b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeMicrophoneController.java @@ -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)); } -} \ No newline at end of file +} diff --git a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeSpeakerController.java b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeSpeakerController.java index 7f17c87..8aa368f 100644 --- a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeSpeakerController.java +++ b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeSpeakerController.java @@ -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(); } diff --git a/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/model/speaker/EdgeSpeakerPlayAudioVO.java b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/model/speaker/EdgeSpeakerPlayAudioVO.java new file mode 100644 index 0000000..1f7e9ff --- /dev/null +++ b/cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/model/speaker/EdgeSpeakerPlayAudioVO.java @@ -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; +} diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionDetectionAlert.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionDetectionAlert.java index 0409c92..48741cd 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionDetectionAlert.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionDetectionAlert.java @@ -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; diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionDetectionDetail.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionDetectionDetail.java deleted file mode 100644 index d184433..0000000 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/InspectionDetectionDetail.java +++ /dev/null @@ -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; -} diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/dto/alert/AlertEnvelope.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/dto/alert/AlertEnvelope.java index c4d9a06..8150a8d 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/dto/alert/AlertEnvelope.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/domain/dto/alert/AlertEnvelope.java @@ -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; diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/mapper/InspectionDetectionDetailMapper.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/mapper/InspectionDetectionDetailMapper.java deleted file mode 100644 index 3d52e32..0000000 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/mapper/InspectionDetectionDetailMapper.java +++ /dev/null @@ -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 -{ -} diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/IInspectionDetectionAlertService.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/IInspectionDetectionAlertService.java index 5d78e84..9c0d5f7 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/IInspectionDetectionAlertService.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/IInspectionDetectionAlertService.java @@ -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); diff --git a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionDetectionAlertServiceImpl.java b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionDetectionAlertServiceImpl.java index 570dc21..23acf7c 100644 --- a/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionDetectionAlertServiceImpl.java +++ b/cmvr-iot-inspection/src/main/java/com/cmvr/inspection/service/impl/InspectionDetectionAlertServiceImpl.java @@ -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_id、trace_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 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 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(), "/"); diff --git a/sql/inspection_detection_alert.sql b/sql/inspection_detection_alert.sql index 696e5ef..4f1fc69 100644 --- a/sql/inspection_detection_alert.sql +++ b/sql/inspection_detection_alert.sql @@ -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幂等键,仅记录';