新增AIoT边缘智能模块:摄像头ROI配置、算法管理、配置推送

- 后端:新增aiot模块(bean/dao/service/controller),支持ROI区域CRUD、
  算法注册表管理、ROI-算法绑定、配置推送到FastAPI边缘端、变更日志
- 前端:新增摄像头配置页(列表+ROI子页面)、算法配置页、Canvas绘图组件
  (矩形/多边形)、动态算法参数编辑器、ZLM截图作为ROI编辑背景
- 数据库:新建4张表(wvp_ai_roi/algorithm/roi_algo_bind/config_log)
  字段与FastAPI端SQLite兼容,含2个预置算法
- 路由裁剪:隐藏无关菜单(地图/部标/推流/录制计划等)
- 修复cameraId含/导致REST路径解析错误(改用query参数)
- 新增ai.service配置项(边缘端地址/超时/开关)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 08:59:20 +08:00
parent 6c471cdfd7
commit d7bf969694
44 changed files with 2943 additions and 12 deletions

View File

@@ -0,0 +1,39 @@
package com.genersoft.iot.vmp.aiot.bean;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "算法注册表")
public class AiAlgorithm {
@Schema(description = "数据库自增ID")
private Integer id;
@Schema(description = "算法编码")
private String algoCode;
@Schema(description = "算法名称")
private String algoName;
@Schema(description = "目标类别")
private String targetClass;
@Schema(description = "参数模板JSON")
private String paramSchema;
@Schema(description = "描述")
private String description;
@Schema(description = "是否可用")
private Integer isActive;
@Schema(description = "最后同步时间")
private String syncTime;
@Schema(description = "创建时间")
private String createTime;
@Schema(description = "更新时间")
private String updateTime;
}

View File

@@ -0,0 +1,30 @@
package com.genersoft.iot.vmp.aiot.bean;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "配置变更日志")
public class AiConfigLog {
@Schema(description = "数据库自增ID")
private Long id;
@Schema(description = "类型ROI/ALGORITHM/BIND")
private String configType;
@Schema(description = "目标ID")
private String configId;
@Schema(description = "变更前JSON")
private String oldValue;
@Schema(description = "变更后JSON")
private String newValue;
@Schema(description = "操作人")
private String updatedBy;
@Schema(description = "操作时间")
private String updatedAt;
}

View File

@@ -0,0 +1,54 @@
package com.genersoft.iot.vmp.aiot.bean;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "ROI区域配置")
public class AiRoi {
@Schema(description = "数据库自增ID")
private Integer id;
@Schema(description = "UUID与FastAPI端同步用")
private String roiId;
@Schema(description = "通道国标编号")
private String cameraId;
@Schema(description = "WVP通道表ID")
private Integer channelDbId;
@Schema(description = "设备国标编号")
private String deviceId;
@Schema(description = "ROI名称")
private String name;
@Schema(description = "形状rectangle/polygon")
private String roiType;
@Schema(description = "JSON归一化坐标")
private String coordinates;
@Schema(description = "显示颜色")
private String color;
@Schema(description = "优先级")
private Integer priority;
@Schema(description = "启用状态")
private Integer enabled;
@Schema(description = "扩展参数JSON")
private String extraParams;
@Schema(description = "描述")
private String description;
@Schema(description = "创建时间")
private String createTime;
@Schema(description = "更新时间")
private String updateTime;
}

View File

@@ -0,0 +1,36 @@
package com.genersoft.iot.vmp.aiot.bean;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "ROI算法绑定")
public class AiRoiAlgoBind {
@Schema(description = "数据库自增ID")
private Integer id;
@Schema(description = "UUID同步用")
private String bindId;
@Schema(description = "ROI的UUID")
private String roiId;
@Schema(description = "算法编码")
private String algoCode;
@Schema(description = "自定义参数JSON")
private String params;
@Schema(description = "优先级")
private Integer priority;
@Schema(description = "启用状态")
private Integer enabled;
@Schema(description = "创建时间")
private String createTime;
@Schema(description = "更新时间")
private String updateTime;
}

View File

@@ -0,0 +1,24 @@
package com.genersoft.iot.vmp.aiot.bean;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "ROI详情含关联算法")
public class AiRoiDetail {
@Schema(description = "ROI基本信息")
private AiRoi roi;
@Schema(description = "绑定的算法列表")
private List<AiRoiAlgoBindDetail> algorithms;
@Data
@Schema(description = "算法绑定详情")
public static class AiRoiAlgoBindDetail {
private AiRoiAlgoBind bind;
private AiAlgorithm algorithm;
}
}

View File

@@ -0,0 +1,17 @@
package com.genersoft.iot.vmp.aiot.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "ai.service")
public class AiServiceConfig {
private String url = "http://localhost:8090";
private int pushTimeout = 10000;
private boolean enabled = false;
}

View File

@@ -0,0 +1,39 @@
package com.genersoft.iot.vmp.aiot.controller;
import com.genersoft.iot.vmp.aiot.bean.AiAlgorithm;
import com.genersoft.iot.vmp.aiot.service.IAiAlgorithmService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/ai/algorithm")
@Tag(name = "AI-算法管理")
public class AiAlgorithmController {
@Autowired
private IAiAlgorithmService algorithmService;
@Operation(summary = "查询算法列表")
@GetMapping("/list")
public List<AiAlgorithm> queryList() {
return algorithmService.queryAll();
}
@Operation(summary = "启用/禁用算法")
@PostMapping("/toggle/{id}")
public void toggleActive(@PathVariable Integer id, @RequestParam Integer isActive) {
algorithmService.toggleActive(id, isActive);
}
@Operation(summary = "从边缘端同步算法")
@PostMapping("/sync")
public void syncFromEdge() {
algorithmService.syncFromEdge();
}
}

View File

@@ -0,0 +1,32 @@
package com.genersoft.iot.vmp.aiot.controller;
import com.genersoft.iot.vmp.aiot.service.IAiConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/ai/config")
@Tag(name = "AI-配置推送")
public class AiConfigController {
@Autowired
private IAiConfigService configService;
@Operation(summary = "推送配置到边缘端")
@PostMapping("/push")
public void pushConfig(@RequestParam String cameraId) {
configService.pushConfig(cameraId);
}
@Operation(summary = "导出通道完整配置JSON")
@GetMapping("/export")
public Map<String, Object> exportConfig(@RequestParam String cameraId) {
return configService.exportConfig(cameraId);
}
}

View File

@@ -0,0 +1,31 @@
package com.genersoft.iot.vmp.aiot.controller;
import com.genersoft.iot.vmp.aiot.bean.AiConfigLog;
import com.genersoft.iot.vmp.aiot.service.IAiConfigLogService;
import com.github.pagehelper.PageInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/ai/log")
@Tag(name = "AI-变更日志")
public class AiConfigLogController {
@Autowired
private IAiConfigLogService configLogService;
@Operation(summary = "分页查询变更日志")
@GetMapping("/list")
public PageInfo<AiConfigLog> queryList(
@Parameter(description = "配置类型") @RequestParam(required = false) String configType,
@Parameter(description = "配置ID") @RequestParam(required = false) String configId,
@RequestParam int page,
@RequestParam int count) {
return configLogService.queryList(configType, configId, page, count);
}
}

View File

@@ -0,0 +1,131 @@
package com.genersoft.iot.vmp.aiot.controller;
import com.genersoft.iot.vmp.aiot.bean.AiRoi;
import com.genersoft.iot.vmp.aiot.bean.AiRoiAlgoBind;
import com.genersoft.iot.vmp.aiot.bean.AiRoiDetail;
import com.genersoft.iot.vmp.aiot.service.IAiRoiService;
import com.genersoft.iot.vmp.media.bean.MediaServer;
import com.genersoft.iot.vmp.media.service.IMediaServerService;
import com.github.pagehelper.PageInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/ai/roi")
@Tag(name = "AI-ROI区域管理")
public class AiRoiController {
@Autowired
private IAiRoiService roiService;
@Autowired
private IMediaServerService mediaServerService;
@Operation(summary = "分页查询ROI列表")
@GetMapping("/list")
public PageInfo<AiRoi> queryList(
@Parameter(description = "通道国标编号") @RequestParam(required = false) String cameraId,
@Parameter(description = "设备国标编号") @RequestParam(required = false) String deviceId,
@Parameter(description = "关键字") @RequestParam(required = false) String query,
@RequestParam int page,
@RequestParam int count) {
return roiService.queryList(cameraId, deviceId, query, page, count);
}
@Operation(summary = "ROI详情含关联算法")
@GetMapping("/{id}")
public AiRoiDetail queryDetail(@PathVariable Integer id) {
return roiService.queryDetail(id);
}
@Operation(summary = "通道的所有ROI")
@GetMapping("/channel")
public List<AiRoi> queryByCameraId(@RequestParam String cameraId) {
return roiService.queryByCameraId(cameraId);
}
@Operation(summary = "新增或更新ROI")
@PostMapping("/save")
public void save(@RequestBody AiRoi roi) {
roiService.save(roi);
}
@Operation(summary = "删除ROI")
@DeleteMapping("/delete/{roiId}")
public void delete(@PathVariable String roiId) {
roiService.delete(roiId);
}
@Operation(summary = "绑定算法")
@PostMapping("/bindAlgo")
public void bindAlgo(@RequestBody AiRoiAlgoBind bind) {
roiService.bindAlgo(bind);
}
@Operation(summary = "解绑算法")
@DeleteMapping("/unbindAlgo")
public void unbindAlgo(@RequestParam String bindId) {
roiService.unbindAlgo(bindId);
}
@Operation(summary = "更新绑定参数")
@PostMapping("/updateAlgoParams")
public void updateAlgoParams(@RequestBody AiRoiAlgoBind bind) {
roiService.updateAlgoParams(bind);
}
@Operation(summary = "获取摄像头截图")
@GetMapping("/snap")
public void getSnap(HttpServletResponse resp,
@RequestParam String app,
@RequestParam String stream) {
MediaServer mediaServer = mediaServerService.getDefaultMediaServer();
if (mediaServer == null) {
resp.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return;
}
String snapDir = "snap" + File.separator + "ai";
String fileName = app + "_" + stream + ".jpg";
// 调用ZLM截图
mediaServerService.getSnap(mediaServer, app, stream, 10, 1, snapDir, fileName);
// 等待截图生成
File snapFile = new File(snapDir + File.separator + fileName);
int retries = 0;
while (!snapFile.exists() && retries < 50) {
try {
Thread.sleep(200);
} catch (InterruptedException ignored) {}
retries++;
}
if (!snapFile.exists()) {
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
try {
InputStream in = Files.newInputStream(snapFile.toPath());
resp.setContentType(MediaType.IMAGE_JPEG_VALUE);
ServletOutputStream outputStream = resp.getOutputStream();
IOUtils.copy(in, outputStream);
in.close();
outputStream.close();
} catch (IOException e) {
log.warn("[AI截图] 读取截图失败: {}", e.getMessage());
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
}
}

View File

@@ -0,0 +1,40 @@
package com.genersoft.iot.vmp.aiot.dao;
import com.genersoft.iot.vmp.aiot.bean.AiAlgorithm;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface AiAlgorithmMapper {
@Select("SELECT * FROM wvp_ai_algorithm ORDER BY id")
List<AiAlgorithm> queryAll();
@Select("SELECT * FROM wvp_ai_algorithm WHERE is_active=1 ORDER BY id")
List<AiAlgorithm> queryActive();
@Select("SELECT * FROM wvp_ai_algorithm WHERE algo_code=#{algoCode}")
AiAlgorithm queryByCode(@Param("algoCode") String algoCode);
@Select("SELECT * FROM wvp_ai_algorithm WHERE id=#{id}")
AiAlgorithm queryById(@Param("id") Integer id);
@Insert("INSERT INTO wvp_ai_algorithm (algo_code, algo_name, target_class, param_schema, " +
"description, is_active, sync_time, create_time, update_time) " +
"VALUES (#{algoCode}, #{algoName}, #{targetClass}, #{paramSchema}, " +
"#{description}, #{isActive}, #{syncTime}, #{createTime}, #{updateTime})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int add(AiAlgorithm algorithm);
@Update("UPDATE wvp_ai_algorithm SET algo_name=#{algoName}, target_class=#{targetClass}, " +
"param_schema=#{paramSchema}, description=#{description}, is_active=#{isActive}, " +
"sync_time=#{syncTime}, update_time=#{updateTime} WHERE algo_code=#{algoCode}")
int updateByCode(AiAlgorithm algorithm);
@Update("UPDATE wvp_ai_algorithm SET is_active=#{isActive}, update_time=#{updateTime} WHERE id=#{id}")
int updateActive(@Param("id") Integer id, @Param("isActive") Integer isActive, @Param("updateTime") String updateTime);
@Delete("DELETE FROM wvp_ai_algorithm WHERE id=#{id}")
int delete(@Param("id") Integer id);
}

View File

@@ -0,0 +1,24 @@
package com.genersoft.iot.vmp.aiot.dao;
import com.genersoft.iot.vmp.aiot.bean.AiConfigLog;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface AiConfigLogMapper {
@Insert("INSERT INTO wvp_ai_config_log (config_type, config_id, old_value, new_value, updated_by, updated_at) " +
"VALUES (#{configType}, #{configId}, #{oldValue}, #{newValue}, #{updatedBy}, #{updatedAt})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int add(AiConfigLog log);
@Select(value = {"<script>" +
"SELECT * FROM wvp_ai_config_log WHERE 1=1 " +
"<if test='configType != null'> AND config_type=#{configType}</if> " +
"<if test='configId != null'> AND config_id=#{configId}</if> " +
"ORDER BY id DESC" +
"</script>"})
List<AiConfigLog> queryList(@Param("configType") String configType,
@Param("configId") String configId);
}

View File

@@ -0,0 +1,34 @@
package com.genersoft.iot.vmp.aiot.dao;
import com.genersoft.iot.vmp.aiot.bean.AiRoiAlgoBind;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface AiRoiAlgoBindMapper {
@Insert("INSERT INTO wvp_ai_roi_algo_bind (bind_id, roi_id, algo_code, params, priority, enabled, create_time, update_time) " +
"VALUES (#{bindId}, #{roiId}, #{algoCode}, #{params}, #{priority}, #{enabled}, #{createTime}, #{updateTime})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int add(AiRoiAlgoBind bind);
@Update("UPDATE wvp_ai_roi_algo_bind SET params=#{params}, priority=#{priority}, " +
"enabled=#{enabled}, update_time=#{updateTime} WHERE bind_id=#{bindId}")
int updateByBindId(AiRoiAlgoBind bind);
@Delete("DELETE FROM wvp_ai_roi_algo_bind WHERE bind_id=#{bindId}")
int deleteByBindId(@Param("bindId") String bindId);
@Delete("DELETE FROM wvp_ai_roi_algo_bind WHERE roi_id=#{roiId}")
int deleteByRoiId(@Param("roiId") String roiId);
@Select("SELECT * FROM wvp_ai_roi_algo_bind WHERE roi_id=#{roiId} ORDER BY priority DESC, id")
List<AiRoiAlgoBind> queryByRoiId(@Param("roiId") String roiId);
@Select("SELECT * FROM wvp_ai_roi_algo_bind WHERE bind_id=#{bindId}")
AiRoiAlgoBind queryByBindId(@Param("bindId") String bindId);
@Select("SELECT * FROM wvp_ai_roi_algo_bind WHERE roi_id=#{roiId} AND algo_code=#{algoCode}")
AiRoiAlgoBind queryByRoiIdAndAlgoCode(@Param("roiId") String roiId, @Param("algoCode") String algoCode);
}

View File

@@ -0,0 +1,46 @@
package com.genersoft.iot.vmp.aiot.dao;
import com.genersoft.iot.vmp.aiot.bean.AiRoi;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface AiRoiMapper {
@Insert("INSERT INTO wvp_ai_roi (roi_id, camera_id, channel_db_id, device_id, name, roi_type, " +
"coordinates, color, priority, enabled, extra_params, description, create_time, update_time) " +
"VALUES (#{roiId}, #{cameraId}, #{channelDbId}, #{deviceId}, #{name}, #{roiType}, " +
"#{coordinates}, #{color}, #{priority}, #{enabled}, #{extraParams}, #{description}, #{createTime}, #{updateTime})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int add(AiRoi roi);
@Update("UPDATE wvp_ai_roi SET camera_id=#{cameraId}, channel_db_id=#{channelDbId}, device_id=#{deviceId}, " +
"name=#{name}, roi_type=#{roiType}, coordinates=#{coordinates}, color=#{color}, " +
"priority=#{priority}, enabled=#{enabled}, extra_params=#{extraParams}, " +
"description=#{description}, update_time=#{updateTime} WHERE id=#{id}")
int update(AiRoi roi);
@Delete("DELETE FROM wvp_ai_roi WHERE roi_id=#{roiId}")
int deleteByRoiId(@Param("roiId") String roiId);
@Select("SELECT * FROM wvp_ai_roi WHERE id=#{id}")
AiRoi queryById(@Param("id") Integer id);
@Select("SELECT * FROM wvp_ai_roi WHERE roi_id=#{roiId}")
AiRoi queryByRoiId(@Param("roiId") String roiId);
@Select(value = {"<script>" +
"SELECT * FROM wvp_ai_roi WHERE 1=1 " +
"<if test='cameraId != null'> AND camera_id=#{cameraId}</if> " +
"<if test='deviceId != null'> AND device_id=#{deviceId}</if> " +
"<if test='query != null'> AND (name LIKE concat('%',#{query},'%') OR camera_id LIKE concat('%',#{query},'%'))</if> " +
"ORDER BY priority DESC, id DESC" +
"</script>"})
List<AiRoi> queryList(@Param("cameraId") String cameraId,
@Param("deviceId") String deviceId,
@Param("query") String query);
@Select("SELECT * FROM wvp_ai_roi WHERE camera_id=#{cameraId} AND enabled=1 ORDER BY priority DESC")
List<AiRoi> queryByCameraId(@Param("cameraId") String cameraId);
}

View File

@@ -0,0 +1,16 @@
package com.genersoft.iot.vmp.aiot.service;
import com.genersoft.iot.vmp.aiot.bean.AiAlgorithm;
import java.util.List;
public interface IAiAlgorithmService {
List<AiAlgorithm> queryAll();
List<AiAlgorithm> queryActive();
void toggleActive(Integer id, Integer isActive);
void syncFromEdge();
}

View File

@@ -0,0 +1,11 @@
package com.genersoft.iot.vmp.aiot.service;
import com.genersoft.iot.vmp.aiot.bean.AiConfigLog;
import com.github.pagehelper.PageInfo;
public interface IAiConfigLogService {
void addLog(String configType, String configId, String oldValue, String newValue, String updatedBy);
PageInfo<AiConfigLog> queryList(String configType, String configId, int page, int count);
}

View File

@@ -0,0 +1,10 @@
package com.genersoft.iot.vmp.aiot.service;
import java.util.Map;
public interface IAiConfigService {
Map<String, Object> exportConfig(String cameraId);
void pushConfig(String cameraId);
}

View File

@@ -0,0 +1,27 @@
package com.genersoft.iot.vmp.aiot.service;
import com.genersoft.iot.vmp.aiot.bean.AiRoi;
import com.genersoft.iot.vmp.aiot.bean.AiRoiAlgoBind;
import com.genersoft.iot.vmp.aiot.bean.AiRoiDetail;
import com.github.pagehelper.PageInfo;
import java.util.List;
public interface IAiRoiService {
void save(AiRoi roi);
void delete(String roiId);
AiRoiDetail queryDetail(Integer id);
List<AiRoi> queryByCameraId(String cameraId);
PageInfo<AiRoi> queryList(String cameraId, String deviceId, String query, int page, int count);
void bindAlgo(AiRoiAlgoBind bind);
void unbindAlgo(String bindId);
void updateAlgoParams(AiRoiAlgoBind bind);
}

View File

@@ -0,0 +1,98 @@
package com.genersoft.iot.vmp.aiot.service.impl;
import com.genersoft.iot.vmp.aiot.bean.AiAlgorithm;
import com.genersoft.iot.vmp.aiot.config.AiServiceConfig;
import com.genersoft.iot.vmp.aiot.dao.AiAlgorithmMapper;
import com.genersoft.iot.vmp.aiot.service.IAiAlgorithmService;
import com.genersoft.iot.vmp.aiot.service.IAiConfigLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class AiAlgorithmServiceImpl implements IAiAlgorithmService {
@Autowired
private AiAlgorithmMapper algorithmMapper;
@Autowired
private AiServiceConfig aiServiceConfig;
@Autowired
private IAiConfigLogService configLogService;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public List<AiAlgorithm> queryAll() {
return algorithmMapper.queryAll();
}
@Override
public List<AiAlgorithm> queryActive() {
return algorithmMapper.queryActive();
}
@Override
public void toggleActive(Integer id, Integer isActive) {
String now = LocalDateTime.now().format(FORMATTER);
algorithmMapper.updateActive(id, isActive, now);
configLogService.addLog("ALGORITHM", String.valueOf(id),
null, "{\"is_active\":" + isActive + "}", null);
}
@Override
@SuppressWarnings("unchecked")
public void syncFromEdge() {
if (!aiServiceConfig.isEnabled()) {
log.warn("AI服务未启用跳过同步");
return;
}
String now = LocalDateTime.now().format(FORMATTER);
try {
RestTemplate restTemplate = new RestTemplate();
String url = aiServiceConfig.getUrl() + "/api/algorithms";
List<Map<String, Object>> remoteAlgos = restTemplate.getForObject(url, List.class);
if (remoteAlgos == null || remoteAlgos.isEmpty()) {
log.info("边缘端无算法数据");
return;
}
for (Map<String, Object> remote : remoteAlgos) {
String algoCode = (String) remote.get("algo_code");
AiAlgorithm existing = algorithmMapper.queryByCode(algoCode);
if (existing != null) {
existing.setAlgoName((String) remote.get("algo_name"));
existing.setTargetClass((String) remote.get("target_class"));
existing.setParamSchema(remote.get("param_schema") != null ? remote.get("param_schema").toString() : null);
existing.setDescription((String) remote.get("description"));
existing.setSyncTime(now);
existing.setUpdateTime(now);
algorithmMapper.updateByCode(existing);
} else {
AiAlgorithm algo = new AiAlgorithm();
algo.setAlgoCode(algoCode);
algo.setAlgoName((String) remote.get("algo_name"));
algo.setTargetClass((String) remote.get("target_class"));
algo.setParamSchema(remote.get("param_schema") != null ? remote.get("param_schema").toString() : null);
algo.setDescription((String) remote.get("description"));
algo.setIsActive(1);
algo.setSyncTime(now);
algo.setCreateTime(now);
algo.setUpdateTime(now);
algorithmMapper.add(algo);
}
}
log.info("从边缘端同步算法完成,共{}个", remoteAlgos.size());
} catch (Exception e) {
log.error("从边缘端同步算法失败", e);
throw new RuntimeException("同步失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,43 @@
package com.genersoft.iot.vmp.aiot.service.impl;
import com.genersoft.iot.vmp.aiot.bean.AiConfigLog;
import com.genersoft.iot.vmp.aiot.dao.AiConfigLogMapper;
import com.genersoft.iot.vmp.aiot.service.IAiConfigLogService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Slf4j
@Service
public class AiConfigLogServiceImpl implements IAiConfigLogService {
@Autowired
private AiConfigLogMapper configLogMapper;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void addLog(String configType, String configId, String oldValue, String newValue, String updatedBy) {
AiConfigLog logEntry = new AiConfigLog();
logEntry.setConfigType(configType);
logEntry.setConfigId(configId);
logEntry.setOldValue(oldValue);
logEntry.setNewValue(newValue);
logEntry.setUpdatedBy(updatedBy);
logEntry.setUpdatedAt(LocalDateTime.now().format(FORMATTER));
configLogMapper.add(logEntry);
}
@Override
public PageInfo<AiConfigLog> queryList(String configType, String configId, int page, int count) {
PageHelper.startPage(page, count);
List<AiConfigLog> list = configLogMapper.queryList(configType, configId);
return new PageInfo<>(list);
}
}

View File

@@ -0,0 +1,101 @@
package com.genersoft.iot.vmp.aiot.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.genersoft.iot.vmp.aiot.bean.AiAlgorithm;
import com.genersoft.iot.vmp.aiot.bean.AiRoi;
import com.genersoft.iot.vmp.aiot.bean.AiRoiAlgoBind;
import com.genersoft.iot.vmp.aiot.config.AiServiceConfig;
import com.genersoft.iot.vmp.aiot.dao.AiAlgorithmMapper;
import com.genersoft.iot.vmp.aiot.dao.AiRoiAlgoBindMapper;
import com.genersoft.iot.vmp.aiot.dao.AiRoiMapper;
import com.genersoft.iot.vmp.aiot.service.IAiConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.*;
@Slf4j
@Service
public class AiConfigServiceImpl implements IAiConfigService {
@Autowired
private AiRoiMapper roiMapper;
@Autowired
private AiRoiAlgoBindMapper bindMapper;
@Autowired
private AiAlgorithmMapper algorithmMapper;
@Autowired
private AiServiceConfig aiServiceConfig;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Map<String, Object> exportConfig(String cameraId) {
Map<String, Object> config = new LinkedHashMap<>();
config.put("camera_id", cameraId);
List<AiRoi> rois = roiMapper.queryByCameraId(cameraId);
List<Map<String, Object>> roiList = new ArrayList<>();
for (AiRoi roi : rois) {
Map<String, Object> roiMap = new LinkedHashMap<>();
roiMap.put("roi_id", roi.getRoiId());
roiMap.put("roi_type", roi.getRoiType());
roiMap.put("name", roi.getName());
roiMap.put("enabled", roi.getEnabled() == 1);
try {
roiMap.put("coordinates", objectMapper.readValue(roi.getCoordinates(), Object.class));
} catch (Exception e) {
roiMap.put("coordinates", roi.getCoordinates());
}
List<AiRoiAlgoBind> binds = bindMapper.queryByRoiId(roi.getRoiId());
List<Map<String, Object>> algoList = new ArrayList<>();
for (AiRoiAlgoBind bind : binds) {
Map<String, Object> algoMap = new LinkedHashMap<>();
algoMap.put("algo_code", bind.getAlgoCode());
algoMap.put("enabled", bind.getEnabled() == 1);
try {
algoMap.put("params", objectMapper.readValue(bind.getParams(), Object.class));
} catch (Exception e) {
algoMap.put("params", bind.getParams());
}
algoList.add(algoMap);
}
roiMap.put("algorithms", algoList);
roiList.add(roiMap);
}
config.put("rois", roiList);
return config;
}
@Override
public void pushConfig(String cameraId) {
if (!aiServiceConfig.isEnabled()) {
log.warn("AI服务未启用跳过推送");
throw new RuntimeException("AI服务未启用请在配置中设置ai.service.enabled=true");
}
Map<String, Object> config = exportConfig(cameraId);
try {
RestTemplate restTemplate = new RestTemplate();
String url = aiServiceConfig.getUrl() + "/api/config/receive";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(config, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("推送失败,边缘端返回: " + response.getStatusCode());
}
log.info("配置推送成功: cameraId={}", cameraId);
} catch (Exception e) {
log.error("配置推送失败: cameraId={}", cameraId, e);
throw new RuntimeException("推送失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,167 @@
package com.genersoft.iot.vmp.aiot.service.impl;
import com.genersoft.iot.vmp.aiot.bean.*;
import com.genersoft.iot.vmp.aiot.dao.AiAlgorithmMapper;
import com.genersoft.iot.vmp.aiot.dao.AiRoiAlgoBindMapper;
import com.genersoft.iot.vmp.aiot.dao.AiRoiMapper;
import com.genersoft.iot.vmp.aiot.service.IAiConfigLogService;
import com.genersoft.iot.vmp.aiot.service.IAiRoiService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Slf4j
@Service
public class AiRoiServiceImpl implements IAiRoiService {
@Autowired
private AiRoiMapper roiMapper;
@Autowired
private AiRoiAlgoBindMapper bindMapper;
@Autowired
private AiAlgorithmMapper algorithmMapper;
@Autowired
private IAiConfigLogService configLogService;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
@Transactional
public void save(AiRoi roi) {
String now = LocalDateTime.now().format(FORMATTER);
roi.setUpdateTime(now);
if (roi.getId() != null && roi.getId() > 0) {
AiRoi old = roiMapper.queryById(roi.getId());
roiMapper.update(roi);
configLogService.addLog("ROI", roi.getRoiId(), toJson(old), toJson(roi), null);
} else {
if (ObjectUtils.isEmpty(roi.getRoiId())) {
roi.setRoiId(UUID.randomUUID().toString());
}
roi.setCreateTime(now);
if (roi.getEnabled() == null) {
roi.setEnabled(1);
}
if (roi.getPriority() == null) {
roi.setPriority(0);
}
roiMapper.add(roi);
configLogService.addLog("ROI", roi.getRoiId(), null, toJson(roi), null);
}
}
@Override
@Transactional
public void delete(String roiId) {
AiRoi old = roiMapper.queryByRoiId(roiId);
if (old != null) {
bindMapper.deleteByRoiId(roiId);
roiMapper.deleteByRoiId(roiId);
configLogService.addLog("ROI", roiId, toJson(old), null, null);
}
}
@Override
public AiRoiDetail queryDetail(Integer id) {
AiRoi roi = roiMapper.queryById(id);
if (roi == null) {
return null;
}
AiRoiDetail detail = new AiRoiDetail();
detail.setRoi(roi);
List<AiRoiAlgoBind> binds = bindMapper.queryByRoiId(roi.getRoiId());
List<AiRoiDetail.AiRoiAlgoBindDetail> algoDetails = new ArrayList<>();
for (AiRoiAlgoBind bind : binds) {
AiRoiDetail.AiRoiAlgoBindDetail ad = new AiRoiDetail.AiRoiAlgoBindDetail();
ad.setBind(bind);
AiAlgorithm algo = algorithmMapper.queryByCode(bind.getAlgoCode());
ad.setAlgorithm(algo);
algoDetails.add(ad);
}
detail.setAlgorithms(algoDetails);
return detail;
}
@Override
public List<AiRoi> queryByCameraId(String cameraId) {
return roiMapper.queryByCameraId(cameraId);
}
@Override
public PageInfo<AiRoi> queryList(String cameraId, String deviceId, String query, int page, int count) {
PageHelper.startPage(page, count);
List<AiRoi> list = roiMapper.queryList(cameraId, deviceId, query);
return new PageInfo<>(list);
}
@Override
@Transactional
public void bindAlgo(AiRoiAlgoBind bind) {
String now = LocalDateTime.now().format(FORMATTER);
AiRoiAlgoBind existing = bindMapper.queryByRoiIdAndAlgoCode(bind.getRoiId(), bind.getAlgoCode());
if (existing != null) {
throw new IllegalArgumentException("该ROI已绑定此算法");
}
if (ObjectUtils.isEmpty(bind.getBindId())) {
bind.setBindId(UUID.randomUUID().toString());
}
if (bind.getEnabled() == null) {
bind.setEnabled(1);
}
if (bind.getPriority() == null) {
bind.setPriority(0);
}
bind.setCreateTime(now);
bind.setUpdateTime(now);
bindMapper.add(bind);
configLogService.addLog("BIND", bind.getBindId(), null, toJson(bind), null);
}
@Override
@Transactional
public void unbindAlgo(String bindId) {
AiRoiAlgoBind old = bindMapper.queryByBindId(bindId);
if (old != null) {
bindMapper.deleteByBindId(bindId);
configLogService.addLog("BIND", bindId, toJson(old), null, null);
}
}
@Override
@Transactional
public void updateAlgoParams(AiRoiAlgoBind bind) {
String now = LocalDateTime.now().format(FORMATTER);
AiRoiAlgoBind old = bindMapper.queryByBindId(bind.getBindId());
if (old == null) {
throw new IllegalArgumentException("绑定关系不存在");
}
bind.setUpdateTime(now);
bindMapper.updateByBindId(bind);
configLogService.addLog("BIND", bind.getBindId(), toJson(old), toJson(bind), null);
}
private String toJson(Object obj) {
if (obj == null) return null;
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
return mapper.writeValueAsString(obj);
} catch (Exception e) {
log.warn("序列化失败", e);
return obj.toString();
}
}
}

View File

@@ -87,6 +87,8 @@ media:
http-port: 6080
# [必选选] zlm服务器的hook.admin_params=secret
secret: su6TiedN2rVAmBbIDX0aa0QTiBJLBdcf
# [重要] ZLM在Docker内运行时hook回调需用host.docker.internal才能访问宿主机
hook-ip: host.docker.internal
# 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分 点播超时建议使用多端口测试
rtp:
# [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输
@@ -110,3 +112,13 @@ user-settings:
# 是否返回Date属性true不返回避免摄像头通过该参数自动校时false返回摄像头可能会根据该时间校时
disable-date-header: false
# AI边缘端服务配置
ai:
service:
# FastAPI边缘端地址
url: http://localhost:8090
# 推送超时ms
push-timeout: 10000
# 暂未对接时设为false
enabled: false

View File

@@ -77,3 +77,10 @@ user-settings:
# 推流直播是否录制
record-push-live: true
auto-apply-play: true
# AI边缘端服务配置
ai:
service:
url: ${AI_SERVICE_URL:http://localhost:8090}
push-timeout: ${AI_PUSH_TIMEOUT:10000}
enabled: ${AI_SERVICE_ENABLED:false}