feat(aiot): 启动时自动回填 camera_code 并修复 ROI 旧格式引用

解决 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 20:16:03 +08:00
parent 6f61b0c9bf
commit 2a8e9c7b82
3 changed files with 99 additions and 2 deletions

View File

@@ -61,4 +61,16 @@ public interface AiRoiMapper {
@Update("UPDATE wvp_ai_roi SET device_id=#{deviceId}") @Update("UPDATE wvp_ai_roi SET device_id=#{deviceId}")
int updateAllDeviceId(@Param("deviceId") String 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<AiRoi> queryWithLegacyCameraId();
} }

View File

@@ -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.aiot.service.IAiRedisConfigService;
import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy;
import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper; import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@@ -67,10 +68,14 @@ public class AiConfigServiceImpl implements IAiConfigService {
private final ObjectMapper objectMapper = new ObjectMapper(); 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 @PostConstruct
public void normalizeDeviceIds() { public void normalizeOnStartup() {
// 1. 统一 device_id
try { try {
String defaultDeviceId = getDefaultDeviceId(); String defaultDeviceId = getDefaultDeviceId();
int updated = roiMapper.updateAllDeviceId(defaultDeviceId); int updated = roiMapper.updateAllDeviceId(defaultDeviceId);
@@ -80,6 +85,74 @@ public class AiConfigServiceImpl implements IAiConfigService {
} catch (Exception e) { } catch (Exception e) {
log.warn("[AiConfig] 启动时修复 device_id 失败: {}", e.getMessage()); log.warn("[AiConfig] 启动时修复 device_id 失败: {}", e.getMessage());
} }
// 2. 回填缺失的 camera_code
backfillCameraCode();
// 3. 修复 ROI 表中旧格式的 camera_idapp/stream → camera_code
fixLegacyRoiCameraId();
}
/**
* 为 stream_proxy 表中 camera_code 为 NULL 的记录自动生成 camera_code
*/
private void backfillCameraCode() {
try {
List<StreamProxy> 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<AiRoi> 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_idapp/stream → camera_code", fixed);
}
} catch (Exception e) {
log.warn("[AiConfig] 修复 ROI camera_id 失败: {}", e.getMessage());
}
} }
@Override @Override

View File

@@ -100,4 +100,16 @@ public interface StreamProxyMapper {
*/ */
@Select("SELECT * FROM wvp_stream_proxy WHERE camera_code = #{cameraCode}") @Select("SELECT * FROM wvp_stream_proxy WHERE camera_code = #{cameraCode}")
StreamProxy selectByCameraCode(@Param("cameraCode") String 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<StreamProxy> 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);
} }