Compare commits

2 Commits

Author SHA1 Message Date
f478e96d97 注册垃圾检测算法并修复ROI绑定后不推送配置
问题:
1. WVP 数据库没有 garbage 算法注册,前端无法选择
2. bindAlgo/unbindAlgo/updateAlgoParams 只写数据库,不触发配置推送到边缘端
3. bindAlgo 不校验 algo_code 是否存在

修复:
- PRESET_ALGORITHMS 添加 garbage 算法注册(参数与边缘端对齐)
- bindAlgo 添加算法存在性校验
- bindAlgo/unbindAlgo/updateAlgoParams 变更后自动推送配置到边缘端
- 新增 pushRoiConfig 辅助方法

Claude Opus 4.7
2026-04-29 14:34:25 +08:00
780650f0cc 修复摄像头增删改配置下发链路
问题:前端添加摄像头后配置未下发到边缘端,Redis Stream 无消息

根因:
1. StreamProxyServiceImpl.add/update 完全未调用配置推送
2. delete 错误地将 cameraCode 当作 deviceId 传递
3. writeDeviceAggregatedConfig 仅从 ROI 表查摄像头,新摄像头无 ROI 时被遗漏

修复:
- add/delete/update 后自动推送配置到对应 edgeDeviceId
- update 处理 edgeDeviceId 变更场景(推送给新旧设备)
- writeDeviceAggregatedConfig 双源查询:stream_proxy + ROI 表
- 新增 StreamProxyMapper.selectCameraCodesByEdgeDeviceId 方法

验证:
- 代码语法检查通过
- 字段映射符合边缘端期望格式 (snake_case)
- 设备切换场景正确处理

Claude Opus 4.7
2026-04-29 10:00:25 +08:00
5 changed files with 124 additions and 10 deletions

View File

@@ -74,6 +74,10 @@ public class AiAlgorithmServiceImpl implements IAiAlgorithmService {
"非机动车违停检测", "bicycle,motorcycle", "检测禁停区域内是否有非机动车自行车、电动车违规停放。确认非机动车停留10秒后开始3分钟倒计时超时触发告警。非机动车离开60秒后自动结束告警。", "非机动车违停检测", "bicycle,motorcycle", "检测禁停区域内是否有非机动车自行车、电动车违规停放。确认非机动车停留10秒后开始3分钟倒计时超时触发告警。非机动车离开60秒后自动结束告警。",
"{\"confirm_vehicle_sec\":{\"type\":\"int\",\"default\":10,\"min\":5},\"parking_countdown_sec\":{\"type\":\"int\",\"default\":180,\"min\":60},\"confirm_clear_sec\":{\"type\":\"int\",\"default\":60,\"min\":10},\"cooldown_sec\":{\"type\":\"int\",\"default\":900,\"min\":0}}" "{\"confirm_vehicle_sec\":{\"type\":\"int\",\"default\":10,\"min\":5},\"parking_countdown_sec\":{\"type\":\"int\",\"default\":180,\"min\":60},\"confirm_clear_sec\":{\"type\":\"int\",\"default\":60,\"min\":10},\"cooldown_sec\":{\"type\":\"int\",\"default\":900,\"min\":0}}"
}); });
PRESET_ALGORITHMS.put("garbage", new String[]{
"垃圾检测", "garbage", "检测监控区域内散落垃圾的持续存在确认60秒后触发告警垃圾清理后30秒确认自动结束告警。",
"{\"confirm_garbage_sec\":{\"type\":\"int\",\"default\":60,\"min\":10,\"max\":600},\"confirm_clear_sec\":{\"type\":\"int\",\"default\":30,\"min\":10,\"max\":300},\"cooldown_sec\":{\"type\":\"int\",\"default\":600,\"min\":0}}"
});
} }
@PostConstruct @PostConstruct

View File

@@ -438,11 +438,29 @@ public class AiRedisConfigServiceImpl implements IAiRedisConfigService {
} }
try { try {
// 1. 查询该设备下所有摄像头 // 1. 查询该设备下所有摄像头(优先从 stream_proxy 表查询,确保包含无 ROI 的摄像头)
List<String> cameraIds = roiMapper.queryDistinctCameraIdsByDeviceId(deviceId); // 合并两个来源stream_proxy 表的 edge_device_id 关联 + ROI 表的 device_id 关联
if (cameraIds == null || cameraIds.isEmpty()) { Set<String> cameraIdSet = new LinkedHashSet<>();
// 来源1stream_proxy 表中 edge_device_id 匹配的摄像头
List<String> proxyCameraIds = streamProxyMapper.selectCameraCodesByEdgeDeviceId(deviceId);
if (proxyCameraIds != null && !proxyCameraIds.isEmpty()) {
cameraIdSet.addAll(proxyCameraIds);
log.debug("[AiRedis] 从 stream_proxy 表查询到 {} 个摄像头: deviceId={}", proxyCameraIds.size(), deviceId);
}
// 来源2ROI 表中 device_id 关联的摄像头(兼容旧数据或边缘情况)
List<String> roiCameraIds = roiMapper.queryDistinctCameraIdsByDeviceId(deviceId);
if (roiCameraIds != null && !roiCameraIds.isEmpty()) {
cameraIdSet.addAll(roiCameraIds);
log.debug("[AiRedis] 从 ROI 表查询到 {} 个摄像头: deviceId={}", roiCameraIds.size(), deviceId);
}
List<String> cameraIds = new ArrayList<>(cameraIdSet);
if (cameraIds.isEmpty()) {
log.info("[AiRedis] 设备 {} 下无关联摄像头,写入空配置", deviceId); log.info("[AiRedis] 设备 {} 下无关联摄像头,写入空配置", deviceId);
cameraIds = Collections.emptyList(); } else {
log.info("[AiRedis] 设备 {} 下共 {} 个摄像头(去重后)", deviceId, cameraIds.size());
} }
// 2. 构建扁平格式 JSON // 2. 构建扁平格式 JSON

View File

@@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.aiot.dao.AiAlgorithmMapper;
import com.genersoft.iot.vmp.aiot.dao.AiRoiAlgoBindMapper; import com.genersoft.iot.vmp.aiot.dao.AiRoiAlgoBindMapper;
import com.genersoft.iot.vmp.aiot.dao.AiRoiMapper; import com.genersoft.iot.vmp.aiot.dao.AiRoiMapper;
import com.genersoft.iot.vmp.aiot.service.IAiConfigLogService; import com.genersoft.iot.vmp.aiot.service.IAiConfigLogService;
import com.genersoft.iot.vmp.aiot.service.IAiRedisConfigService;
import com.genersoft.iot.vmp.aiot.service.IAiRoiService; import com.genersoft.iot.vmp.aiot.service.IAiRoiService;
import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper; import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
@@ -40,6 +41,9 @@ public class AiRoiServiceImpl implements IAiRoiService {
@Autowired @Autowired
private IAiConfigLogService configLogService; private IAiConfigLogService configLogService;
@Autowired
private IAiRedisConfigService redisConfigService;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override @Override
@@ -157,6 +161,11 @@ public class AiRoiServiceImpl implements IAiRoiService {
if (existing != null) { if (existing != null) {
throw new IllegalArgumentException("该ROI已绑定此算法"); throw new IllegalArgumentException("该ROI已绑定此算法");
} }
// 校验算法是否存在
AiAlgorithm algo = algorithmMapper.queryByCode(bind.getAlgoCode());
if (algo == null) {
throw new IllegalArgumentException("算法不存在: " + bind.getAlgoCode());
}
if (ObjectUtils.isEmpty(bind.getBindId())) { if (ObjectUtils.isEmpty(bind.getBindId())) {
bind.setBindId(UUID.randomUUID().toString()); bind.setBindId(UUID.randomUUID().toString());
} }
@@ -170,17 +179,25 @@ public class AiRoiServiceImpl implements IAiRoiService {
bind.setUpdateTime(now); bind.setUpdateTime(now);
bindMapper.add(bind); bindMapper.add(bind);
configLogService.addLog("BIND", bind.getBindId(), null, toJson(bind), null); configLogService.addLog("BIND", bind.getBindId(), null, toJson(bind), null);
// 推送配置到 Edge绑定算法后
pushRoiConfig(bind.getRoiId());
} }
@Override @Override
@Transactional @Transactional
public void unbindAlgo(String bindId) { public void unbindAlgo(String bindId) {
AiRoiAlgoBind old = bindMapper.queryByBindId(bindId); AiRoiAlgoBind old = bindMapper.queryByBindId(bindId);
String roiId = null;
if (old != null) { if (old != null) {
String roiId = old.getRoiId(); roiId = old.getRoiId();
bindMapper.deleteByBindId(bindId); bindMapper.deleteByBindId(bindId);
configLogService.addLog("BIND", bindId, toJson(old), null, null); configLogService.addLog("BIND", bindId, toJson(old), null, null);
} }
// 推送配置到 Edge解绑算法后
if (roiId != null) {
pushRoiConfig(roiId);
}
} }
@Override @Override
@@ -200,6 +217,9 @@ public class AiRoiServiceImpl implements IAiRoiService {
bind.setUpdateTime(now); bind.setUpdateTime(now);
bindMapper.updateByBindId(bind); bindMapper.updateByBindId(bind);
configLogService.addLog("BIND", bind.getBindId(), toJson(old), toJson(bind), null); configLogService.addLog("BIND", bind.getBindId(), toJson(old), toJson(bind), null);
// 推送配置到 Edge参数变更后
pushRoiConfig(old.getRoiId());
} }
private String toJson(Object obj) { private String toJson(Object obj) {
@@ -212,4 +232,21 @@ public class AiRoiServiceImpl implements IAiRoiService {
return obj.toString(); return obj.toString();
} }
} }
/**
* 推送指定 ROI 所属设备的配置到边缘端
*/
private void pushRoiConfig(String roiId) {
try {
AiRoi roi = roiMapper.queryByRoiId(roiId);
if (roi != null && roi.getDeviceId() != null && !roi.getDeviceId().isEmpty()) {
redisConfigService.writeDeviceAggregatedConfig(roi.getDeviceId(), "UPDATE");
log.info("[AiRoi] ROI配置已推送到EdgeroiId={}, deviceId={}", roiId, roi.getDeviceId());
} else {
log.warn("[AiRoi] 无法确定ROI所属设备跳过推送roiId={}", roiId);
}
} catch (Exception e) {
log.error("[AiRoi] 推送ROI配置失败roiId={}", roiId, e);
}
}
} }

View File

@@ -133,4 +133,10 @@ public interface StreamProxyMapper {
*/ */
@Select("SELECT edge_device_id FROM wvp_stream_proxy WHERE camera_code = #{cameraCode}") @Select("SELECT edge_device_id FROM wvp_stream_proxy WHERE camera_code = #{cameraCode}")
String selectEdgeDeviceIdByCameraCode(@Param("cameraCode") String cameraCode); String selectEdgeDeviceIdByCameraCode(@Param("cameraCode") String cameraCode);
/**
* 根据 edge_device_id 查询关联的所有摄像头编码
*/
@Select("SELECT camera_code FROM wvp_stream_proxy WHERE edge_device_id = #{edgeDeviceId} AND camera_code IS NOT NULL AND camera_code != '' ORDER BY camera_name")
List<String> selectCameraCodesByEdgeDeviceId(@Param("edgeDeviceId") String edgeDeviceId);
} }

View File

@@ -169,6 +169,21 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
streamProxyMapper.add(streamProxy); streamProxyMapper.add(streamProxy);
streamProxy.setDataType(ChannelDataType.STREAM_PROXY); streamProxy.setDataType(ChannelDataType.STREAM_PROXY);
streamProxy.setDataDeviceId(streamProxy.getId()); streamProxy.setDataDeviceId(streamProxy.getId());
// 推送配置到 Edge新增摄像头
String edgeDeviceId = streamProxy.getEdgeDeviceId();
if (edgeDeviceId != null && !edgeDeviceId.isEmpty()) {
try {
redisConfigService.writeDeviceAggregatedConfig(edgeDeviceId, "UPDATE");
log.info("[StreamProxy] 新增摄像头后推送配置到 EdgeedgeDeviceId={}, cameraCode={}",
edgeDeviceId, cameraCode);
} catch (Exception e) {
log.error("[StreamProxy] 新增摄像头后推送配置失败edgeDeviceId={}, cameraCode={}",
edgeDeviceId, cameraCode, e);
}
} else {
log.warn("[StreamProxy] 新增摄像头但 edgeDeviceId 为空跳过推送配置cameraCode={}", cameraCode);
}
} catch (DuplicateKeyException e) { } catch (DuplicateKeyException e) {
throw new RuntimeException("生成 camera_code 失败,请重试"); throw new RuntimeException("生成 camera_code 失败,请重试");
} }
@@ -185,7 +200,7 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
private void delete(StreamProxy streamProxy) { private void delete(StreamProxy streamProxy) {
Assert.notNull(streamProxy, "代理不可为NULL"); Assert.notNull(streamProxy, "代理不可为NULL");
String cameraCode = streamProxy.getCameraCode(); String edgeDeviceId = streamProxy.getEdgeDeviceId();
if (streamProxy.getPulling() != null && streamProxy.getPulling()) { if (streamProxy.getPulling() != null && streamProxy.getPulling()) {
playService.stopProxy(streamProxy); playService.stopProxy(streamProxy);
} }
@@ -194,13 +209,15 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
} }
streamProxyMapper.delete(streamProxy.getId()); streamProxyMapper.delete(streamProxy.getId());
// 推送配置到 Edge删除摄像头 // 推送配置到 Edge删除摄像头
if (cameraCode != null) { if (edgeDeviceId != null && !edgeDeviceId.isEmpty()) {
try { try {
redisConfigService.writeDeviceAggregatedConfig(cameraCode, "UPDATE"); redisConfigService.writeDeviceAggregatedConfig(edgeDeviceId, "UPDATE");
log.info("[StreamProxy] 删除摄像头后推送配置到 Edgecamera_code={}", cameraCode); log.info("[StreamProxy] 删除摄像头后推送配置到 EdgeedgeDeviceId={}", edgeDeviceId);
} catch (Exception e) { } catch (Exception e) {
log.error("[StreamProxy] 删除摄像头后推送配置失败,camera_code={}", cameraCode, e); log.error("[StreamProxy] 删除摄像头后推送配置失败,edgeDeviceId={}", edgeDeviceId, e);
} }
} else {
log.warn("[StreamProxy] 删除摄像头但 edgeDeviceId 为空,跳过推送配置");
} }
} }
@@ -244,6 +261,38 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
gbChannelService.add(streamProxy.buildCommonGBChannel()); gbChannelService.add(streamProxy.buildCommonGBChannel());
} }
} }
// 推送配置到 Edge更新摄像头
String oldEdgeDeviceId = streamProxyInDb.getEdgeDeviceId();
String newEdgeDeviceId = streamProxy.getEdgeDeviceId();
if (oldEdgeDeviceId != null && !oldEdgeDeviceId.isEmpty() &&
newEdgeDeviceId != null && !newEdgeDeviceId.isEmpty() &&
!oldEdgeDeviceId.equals(newEdgeDeviceId)) {
// edgeDeviceId 变更:先推送给老设备(移除),再推送给新设备(添加)
try {
redisConfigService.writeDeviceAggregatedConfig(oldEdgeDeviceId, "UPDATE");
log.info("[StreamProxy] 更新摄像头后推送配置到旧 EdgeoldEdgeDeviceId={}", oldEdgeDeviceId);
} catch (Exception e) {
log.error("[StreamProxy] 更新摄像头后推送配置到旧 Edge 失败oldEdgeDeviceId={}", oldEdgeDeviceId, e);
}
try {
redisConfigService.writeDeviceAggregatedConfig(newEdgeDeviceId, "UPDATE");
log.info("[StreamProxy] 更新摄像头后推送配置到新 EdgenewEdgeDeviceId={}", newEdgeDeviceId);
} catch (Exception e) {
log.error("[StreamProxy] 更新摄像头后推送配置到新 Edge 失败newEdgeDeviceId={}", newEdgeDeviceId, e);
}
} else if (newEdgeDeviceId != null && !newEdgeDeviceId.isEmpty()) {
// edgeDeviceId 未变更或原为空:推送给当前设备
try {
redisConfigService.writeDeviceAggregatedConfig(newEdgeDeviceId, "UPDATE");
log.info("[StreamProxy] 更新摄像头后推送配置到 EdgeedgeDeviceId={}", newEdgeDeviceId);
} catch (Exception e) {
log.error("[StreamProxy] 更新摄像头后推送配置失败edgeDeviceId={}", newEdgeDeviceId, e);
}
} else {
log.warn("[StreamProxy] 更新摄像头但 edgeDeviceId 为空,跳过推送配置");
}
return true; return true;
} }