From 2a8e9c7b82c6280137afb651ed156c52da070f28 Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Sun, 1 Mar 2026 20:16:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(aiot):=20=E5=90=AF=E5=8A=A8=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=9B=9E=E5=A1=AB=20camera=5Fcode=20?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A4=8D=20ROI=20=E6=97=A7=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解决 stream_proxy 表 camera_code 为 NULL 导致配置推送时跳过摄像头的问题: - @PostConstruct 启动时自动为 camera_code 为空的记录生成 cam_xxx 编码 - 自动将 ROI 表中 app/stream 格式的 camera_id 替换为对应的 camera_code - StreamProxyMapper 新增 selectWithNullCameraCode/updateCameraCode 方法 - AiRoiMapper 新增 updateCameraId/queryWithLegacyCameraId 方法 Co-Authored-By: Claude Opus 4.6 --- .../iot/vmp/aiot/dao/AiRoiMapper.java | 12 +++ .../service/impl/AiConfigServiceImpl.java | 77 ++++++++++++++++++- .../streamProxy/dao/StreamProxyMapper.java | 12 +++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiMapper.java b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiMapper.java index 86b01a559..b7289c785 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiMapper.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiMapper.java @@ -61,4 +61,16 @@ public interface AiRoiMapper { @Update("UPDATE wvp_ai_roi SET device_id=#{deviceId}") int updateAllDeviceId(@Param("deviceId") String deviceId); + + /** + * 将 ROI 表中 camera_id 从 app/stream 格式更新为 camera_code + */ + @Update("UPDATE wvp_ai_roi SET camera_id = #{cameraCode} WHERE camera_id = #{oldCameraId}") + int updateCameraId(@Param("oldCameraId") String oldCameraId, @Param("cameraCode") String cameraCode); + + /** + * 查询使用非 camera_code 格式的 ROI(即 camera_id 不以 cam_ 开头的记录) + */ + @Select("SELECT * FROM wvp_ai_roi WHERE camera_id NOT LIKE 'cam_%'") + List queryWithLegacyCameraId(); } diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigServiceImpl.java index e2926b55f..134f64bff 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigServiceImpl.java @@ -17,6 +17,7 @@ import com.genersoft.iot.vmp.aiot.service.IAiConfigSnapshotService; import com.genersoft.iot.vmp.aiot.service.IAiRedisConfigService; import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -67,10 +68,14 @@ public class AiConfigServiceImpl implements IAiConfigService { private final ObjectMapper objectMapper = new ObjectMapper(); /** - * 启动时统一 ROI 表中的 device_id,确保与 Edge 端 EDGE_DEVICE_ID 一致 + * 启动时自动修复数据一致性: + * 1. 统一 ROI 表 device_id + * 2. 回填 stream_proxy 缺失的 camera_code + * 3. 修复 ROI 表中仍使用 app/stream 格式的 camera_id */ @PostConstruct - public void normalizeDeviceIds() { + public void normalizeOnStartup() { + // 1. 统一 device_id try { String defaultDeviceId = getDefaultDeviceId(); int updated = roiMapper.updateAllDeviceId(defaultDeviceId); @@ -80,6 +85,74 @@ public class AiConfigServiceImpl implements IAiConfigService { } catch (Exception e) { log.warn("[AiConfig] 启动时修复 device_id 失败: {}", e.getMessage()); } + + // 2. 回填缺失的 camera_code + backfillCameraCode(); + + // 3. 修复 ROI 表中旧格式的 camera_id(app/stream → camera_code) + fixLegacyRoiCameraId(); + } + + /** + * 为 stream_proxy 表中 camera_code 为 NULL 的记录自动生成 camera_code + */ + private void backfillCameraCode() { + try { + List nullCodeProxies = streamProxyMapper.selectWithNullCameraCode(); + if (nullCodeProxies == null || nullCodeProxies.isEmpty()) { + return; + } + int backfilled = 0; + for (StreamProxy proxy : nullCodeProxies) { + String cameraCode = "cam_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12); + streamProxyMapper.updateCameraCode(proxy.getId(), cameraCode); + backfilled++; + log.info("[AiConfig] 回填 camera_code: id={}, app={}, stream={} → {}", + proxy.getId(), proxy.getApp(), proxy.getStream(), cameraCode); + } + log.info("[AiConfig] 启动时回填 {} 条 stream_proxy 的 camera_code", backfilled); + } catch (Exception e) { + log.warn("[AiConfig] 回填 camera_code 失败: {}", e.getMessage()); + } + } + + /** + * 修复 ROI 表中仍使用 app/stream 格式的 camera_id,替换为对应的 camera_code + * 例如: camera_id="live/camera01" → camera_id="cam_a1b2c3d4e5f6" + */ + private void fixLegacyRoiCameraId() { + try { + List legacyRois = roiMapper.queryWithLegacyCameraId(); + if (legacyRois == null || legacyRois.isEmpty()) { + return; + } + int fixed = 0; + for (AiRoi roi : legacyRois) { + String oldCameraId = roi.getCameraId(); + if (oldCameraId == null || !oldCameraId.contains("/")) { + continue; + } + // 尝试通过 app/stream 查找对应的 stream_proxy + String[] parts = oldCameraId.split("/", 2); + if (parts.length != 2) { + continue; + } + StreamProxy proxy = streamProxyMapper.selectOneByAppAndStream(parts[0], parts[1]); + if (proxy != null && proxy.getCameraCode() != null && !proxy.getCameraCode().isEmpty()) { + roiMapper.updateCameraId(oldCameraId, proxy.getCameraCode()); + fixed++; + log.info("[AiConfig] 修复 ROI camera_id: {} → {} (roi={})", + oldCameraId, proxy.getCameraCode(), roi.getRoiId()); + } else { + log.warn("[AiConfig] ROI camera_id={} 无法找到对应的 stream_proxy 记录", oldCameraId); + } + } + if (fixed > 0) { + log.info("[AiConfig] 启动时修复 {} 条 ROI 的 camera_id(app/stream → camera_code)", fixed); + } + } catch (Exception e) { + log.warn("[AiConfig] 修复 ROI camera_id 失败: {}", e.getMessage()); + } } @Override diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java index 648319cb0..98167b754 100755 --- a/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java @@ -100,4 +100,16 @@ public interface StreamProxyMapper { */ @Select("SELECT * FROM wvp_stream_proxy WHERE camera_code = #{cameraCode}") StreamProxy selectByCameraCode(@Param("cameraCode") String cameraCode); + + /** + * 查询 camera_code 为 NULL 或空的记录(需要回填) + */ + @Select("SELECT * FROM wvp_stream_proxy WHERE camera_code IS NULL OR camera_code = ''") + List selectWithNullCameraCode(); + + /** + * 更新指定记录的 camera_code + */ + @Update("UPDATE wvp_stream_proxy SET camera_code = #{cameraCode} WHERE id = #{id}") + int updateCameraCode(@Param("id") int id, @Param("cameraCode") String cameraCode); }