From d7bf9696945f33822fa53b01ae7eba0b12ff04ae Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Wed, 4 Feb 2026 08:59:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EAIoT=E8=BE=B9=E7=BC=98?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E6=A8=A1=E5=9D=97=EF=BC=9A=E6=91=84=E5=83=8F?= =?UTF-8?q?=E5=A4=B4ROI=E9=85=8D=E7=BD=AE=E3=80=81=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E3=80=81=E9=85=8D=E7=BD=AE=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:新增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 --- docker/media/config.ini | 22 +- .../iot/vmp/aiot/bean/AiAlgorithm.java | 39 +++ .../iot/vmp/aiot/bean/AiConfigLog.java | 30 ++ .../genersoft/iot/vmp/aiot/bean/AiRoi.java | 54 ++++ .../iot/vmp/aiot/bean/AiRoiAlgoBind.java | 36 +++ .../iot/vmp/aiot/bean/AiRoiDetail.java | 24 ++ .../iot/vmp/aiot/config/AiServiceConfig.java | 17 + .../controller/AiAlgorithmController.java | 39 +++ .../aiot/controller/AiConfigController.java | 32 ++ .../controller/AiConfigLogController.java | 31 ++ .../vmp/aiot/controller/AiRoiController.java | 131 ++++++++ .../iot/vmp/aiot/dao/AiAlgorithmMapper.java | 40 +++ .../iot/vmp/aiot/dao/AiConfigLogMapper.java | 24 ++ .../iot/vmp/aiot/dao/AiRoiAlgoBindMapper.java | 34 ++ .../iot/vmp/aiot/dao/AiRoiMapper.java | 46 +++ .../vmp/aiot/service/IAiAlgorithmService.java | 16 + .../vmp/aiot/service/IAiConfigLogService.java | 11 + .../vmp/aiot/service/IAiConfigService.java | 10 + .../iot/vmp/aiot/service/IAiRoiService.java | 27 ++ .../service/impl/AiAlgorithmServiceImpl.java | 98 ++++++ .../service/impl/AiConfigLogServiceImpl.java | 43 +++ .../service/impl/AiConfigServiceImpl.java | 101 ++++++ .../aiot/service/impl/AiRoiServiceImpl.java | 167 ++++++++++ src/main/resources/application-dev.yml | 12 + src/main/resources/application-docker.yml | 7 + web/src/api/aiAlgorithm.js | 23 ++ web/src/api/aiConfig.js | 26 ++ web/src/api/aiRoi.js | 64 ++++ web/src/api/cameraConfig.js | 30 ++ web/src/icons/svg/algorithm.svg | 1 + web/src/icons/svg/roi.svg | 1 + web/src/router/index.js | 40 +++ web/src/store/index.js | 6 +- web/src/store/modules/aiAlgorithm.js | 41 +++ web/src/store/modules/aiRoi.js | 93 ++++++ web/src/views/algorithmConfig/index.vue | 118 +++++++ web/src/views/cameraConfig/index.vue | 157 ++++++++++ web/src/views/cameraConfig/roiConfig.vue | 245 +++++++++++++++ .../components/AlgorithmParamEditor.vue | 126 ++++++++ .../roiConfig/components/RoiAlgorithmBind.vue | 151 +++++++++ .../views/roiConfig/components/RoiCanvas.vue | 294 ++++++++++++++++++ web/src/views/roiConfig/index.vue | 157 ++++++++++ web/src/views/roiConfig/roiEditor.vue | 214 +++++++++++++ 数据库/aiot/初始化-mysql-aiot.sql | 77 +++++ 44 files changed, 2943 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/bean/AiAlgorithm.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/bean/AiConfigLog.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoi.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoiAlgoBind.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoiDetail.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/config/AiServiceConfig.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/controller/AiAlgorithmController.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/controller/AiConfigController.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/controller/AiConfigLogController.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/dao/AiAlgorithmMapper.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/dao/AiConfigLogMapper.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiAlgoBindMapper.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiMapper.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/service/IAiAlgorithmService.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/service/IAiConfigLogService.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/service/IAiConfigService.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/service/IAiRoiService.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiAlgorithmServiceImpl.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigLogServiceImpl.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigServiceImpl.java create mode 100644 src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiRoiServiceImpl.java create mode 100644 web/src/api/aiAlgorithm.js create mode 100644 web/src/api/aiConfig.js create mode 100644 web/src/api/aiRoi.js create mode 100644 web/src/api/cameraConfig.js create mode 100644 web/src/icons/svg/algorithm.svg create mode 100644 web/src/icons/svg/roi.svg create mode 100644 web/src/store/modules/aiAlgorithm.js create mode 100644 web/src/store/modules/aiRoi.js create mode 100644 web/src/views/algorithmConfig/index.vue create mode 100644 web/src/views/cameraConfig/index.vue create mode 100644 web/src/views/cameraConfig/roiConfig.vue create mode 100644 web/src/views/roiConfig/components/AlgorithmParamEditor.vue create mode 100644 web/src/views/roiConfig/components/RoiAlgorithmBind.vue create mode 100644 web/src/views/roiConfig/components/RoiCanvas.vue create mode 100644 web/src/views/roiConfig/index.vue create mode 100644 web/src/views/roiConfig/roiEditor.vue create mode 100644 数据库/aiot/初始化-mysql-aiot.sql diff --git a/docker/media/config.ini b/docker/media/config.ini index 8788fb699..4c1319a2d 100644 --- a/docker/media/config.ini +++ b/docker/media/config.ini @@ -52,21 +52,21 @@ alive_interval=10.0 enable=1 on_flow_report= on_http_access= -on_play=http://polaris-wvp:18978/index/hook/on_play -on_publish=http://polaris-wvp:18978/index/hook/on_publish -on_record_mp4=http://polaris-wvp:18978/index/hook/on_record_mp4 +on_play=http://host.docker.internal:18080/index/hook/on_play +on_publish=http://host.docker.internal:18080/index/hook/on_publish +on_record_mp4=http://host.docker.internal:18080/index/hook/on_record_mp4 on_record_ts= -on_rtp_server_timeout=http://polaris-wvp:18978/index/hook/on_rtp_server_timeout +on_rtp_server_timeout=http://host.docker.internal:18080/index/hook/on_rtp_server_timeout on_rtsp_auth= on_rtsp_realm= -on_send_rtp_stopped=http://polaris-wvp:18978/index/hook/on_send_rtp_stopped +on_send_rtp_stopped=http://host.docker.internal:18080/index/hook/on_send_rtp_stopped on_server_exited= -on_server_keepalive=http://polaris-wvp:18978/index/hook/on_server_keepalive -on_server_started=http://polaris-wvp:18978/index/hook/on_server_started +on_server_keepalive=http://host.docker.internal:18080/index/hook/on_server_keepalive +on_server_started=http://host.docker.internal:18080/index/hook/on_server_started on_shell_login= -on_stream_changed=http://polaris-wvp:18978/index/hook/on_stream_changed -on_stream_none_reader=http://polaris-wvp:18978/index/hook/on_stream_none_reader -on_stream_not_found=http://polaris-wvp:18978/index/hook/on_stream_not_found +on_stream_changed=http://host.docker.internal:18080/index/hook/on_stream_changed +on_stream_none_reader=http://host.docker.internal:18080/index/hook/on_stream_none_reader +on_stream_not_found=http://host.docker.internal:18080/index/hook/on_stream_not_found retry=1 retry_delay=3.0 stream_changed_schemas=rtsp/rtmp/fmp4/ts/hls/hls.fmp4 @@ -169,7 +169,7 @@ h265_pt=99 merge_frame=1 opus_pt=100 port=10003 -port_range=30000-30500 +port_range=40000-45000 ps_pt=96 rtp_g711_dur_ms=100 timeoutSec=15 diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiAlgorithm.java b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiAlgorithm.java new file mode 100644 index 000000000..7075b4c2d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiAlgorithm.java @@ -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; +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiConfigLog.java b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiConfigLog.java new file mode 100644 index 000000000..263af6f48 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiConfigLog.java @@ -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; +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoi.java b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoi.java new file mode 100644 index 000000000..0d255bbd5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoi.java @@ -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; +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoiAlgoBind.java b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoiAlgoBind.java new file mode 100644 index 000000000..21061d471 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoiAlgoBind.java @@ -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; +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoiDetail.java b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoiDetail.java new file mode 100644 index 000000000..c8ac2698f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/bean/AiRoiDetail.java @@ -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 algorithms; + + @Data + @Schema(description = "算法绑定详情") + public static class AiRoiAlgoBindDetail { + private AiRoiAlgoBind bind; + private AiAlgorithm algorithm; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/config/AiServiceConfig.java b/src/main/java/com/genersoft/iot/vmp/aiot/config/AiServiceConfig.java new file mode 100644 index 000000000..546a71632 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/config/AiServiceConfig.java @@ -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; +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiAlgorithmController.java b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiAlgorithmController.java new file mode 100644 index 000000000..786a47453 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiAlgorithmController.java @@ -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 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(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiConfigController.java b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiConfigController.java new file mode 100644 index 000000000..117f14dcd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiConfigController.java @@ -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 exportConfig(@RequestParam String cameraId) { + return configService.exportConfig(cameraId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiConfigLogController.java b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiConfigLogController.java new file mode 100644 index 000000000..7dbbb2a0c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiConfigLogController.java @@ -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 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); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java new file mode 100644 index 000000000..1703edb34 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java @@ -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 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 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); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiAlgorithmMapper.java b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiAlgorithmMapper.java new file mode 100644 index 000000000..211b29070 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiAlgorithmMapper.java @@ -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 queryAll(); + + @Select("SELECT * FROM wvp_ai_algorithm WHERE is_active=1 ORDER BY id") + List 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); +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiConfigLogMapper.java b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiConfigLogMapper.java new file mode 100644 index 000000000..f890f1f89 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiConfigLogMapper.java @@ -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 = {""}) + List queryList(@Param("configType") String configType, + @Param("configId") String configId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiAlgoBindMapper.java b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiAlgoBindMapper.java new file mode 100644 index 000000000..ee6e79d95 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiAlgoBindMapper.java @@ -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 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); +} 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 new file mode 100644 index 000000000..8b021fbd0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/dao/AiRoiMapper.java @@ -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 = {""}) + List 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 queryByCameraId(@Param("cameraId") String cameraId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiAlgorithmService.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiAlgorithmService.java new file mode 100644 index 000000000..f7e1e70ac --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiAlgorithmService.java @@ -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 queryAll(); + + List queryActive(); + + void toggleActive(Integer id, Integer isActive); + + void syncFromEdge(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiConfigLogService.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiConfigLogService.java new file mode 100644 index 000000000..e33322fac --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiConfigLogService.java @@ -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 queryList(String configType, String configId, int page, int count); +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiConfigService.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiConfigService.java new file mode 100644 index 000000000..2741aadb2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiConfigService.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.aiot.service; + +import java.util.Map; + +public interface IAiConfigService { + + Map exportConfig(String cameraId); + + void pushConfig(String cameraId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiRoiService.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiRoiService.java new file mode 100644 index 000000000..8e61330ec --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/IAiRoiService.java @@ -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 queryByCameraId(String cameraId); + + PageInfo queryList(String cameraId, String deviceId, String query, int page, int count); + + void bindAlgo(AiRoiAlgoBind bind); + + void unbindAlgo(String bindId); + + void updateAlgoParams(AiRoiAlgoBind bind); +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiAlgorithmServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiAlgorithmServiceImpl.java new file mode 100644 index 000000000..109f8678d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiAlgorithmServiceImpl.java @@ -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 queryAll() { + return algorithmMapper.queryAll(); + } + + @Override + public List 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> remoteAlgos = restTemplate.getForObject(url, List.class); + if (remoteAlgos == null || remoteAlgos.isEmpty()) { + log.info("边缘端无算法数据"); + return; + } + for (Map 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()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigLogServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigLogServiceImpl.java new file mode 100644 index 000000000..e1112129c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigLogServiceImpl.java @@ -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 queryList(String configType, String configId, int page, int count) { + PageHelper.startPage(page, count); + List list = configLogMapper.queryList(configType, configId); + return new PageInfo<>(list); + } +} 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 new file mode 100644 index 000000000..e6b20e948 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiConfigServiceImpl.java @@ -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 exportConfig(String cameraId) { + Map config = new LinkedHashMap<>(); + config.put("camera_id", cameraId); + + List rois = roiMapper.queryByCameraId(cameraId); + List> roiList = new ArrayList<>(); + for (AiRoi roi : rois) { + Map 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 binds = bindMapper.queryByRoiId(roi.getRoiId()); + List> algoList = new ArrayList<>(); + for (AiRoiAlgoBind bind : binds) { + Map 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 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> entity = new HttpEntity<>(config, headers); + ResponseEntity 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()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiRoiServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiRoiServiceImpl.java new file mode 100644 index 000000000..69743478b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/aiot/service/impl/AiRoiServiceImpl.java @@ -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 binds = bindMapper.queryByRoiId(roi.getRoiId()); + List 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 queryByCameraId(String cameraId) { + return roiMapper.queryByCameraId(cameraId); + } + + @Override + public PageInfo queryList(String cameraId, String deviceId, String query, int page, int count) { + PageHelper.startPage(page, count); + List 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(); + } + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0cfdf6e6b..eebc73dd8 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -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 + diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml index 9eed50bff..157089c88 100644 --- a/src/main/resources/application-docker.yml +++ b/src/main/resources/application-docker.yml @@ -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} diff --git a/web/src/api/aiAlgorithm.js b/web/src/api/aiAlgorithm.js new file mode 100644 index 000000000..07b7103d1 --- /dev/null +++ b/web/src/api/aiAlgorithm.js @@ -0,0 +1,23 @@ +import request from '@/utils/request' + +export function queryAlgorithmList() { + return request({ + method: 'get', + url: '/api/ai/algorithm/list' + }) +} + +export function toggleAlgorithmActive(id, isActive) { + return request({ + method: 'post', + url: `/api/ai/algorithm/toggle/${id}`, + params: { isActive } + }) +} + +export function syncAlgorithmsFromEdge() { + return request({ + method: 'post', + url: '/api/ai/algorithm/sync' + }) +} diff --git a/web/src/api/aiConfig.js b/web/src/api/aiConfig.js new file mode 100644 index 000000000..2cd71b577 --- /dev/null +++ b/web/src/api/aiConfig.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +export function pushConfig(cameraId) { + return request({ + method: 'post', + url: '/api/ai/config/push', + params: { cameraId } + }) +} + +export function exportConfig(cameraId) { + return request({ + method: 'get', + url: '/api/ai/config/export', + params: { cameraId } + }) +} + +export function queryConfigLogs(params) { + const { page, count, configType, configId } = params + return request({ + method: 'get', + url: '/api/ai/log/list', + params: { page, count, configType, configId } + }) +} diff --git a/web/src/api/aiRoi.js b/web/src/api/aiRoi.js new file mode 100644 index 000000000..dcc39a54d --- /dev/null +++ b/web/src/api/aiRoi.js @@ -0,0 +1,64 @@ +import request from '@/utils/request' + +export function queryRoiList(params) { + const { page, count, cameraId, deviceId, query } = params + return request({ + method: 'get', + url: '/api/ai/roi/list', + params: { page, count, cameraId, deviceId, query } + }) +} + +export function queryRoiDetail(id) { + return request({ + method: 'get', + url: `/api/ai/roi/${id}` + }) +} + +export function queryRoiByCameraId(cameraId) { + return request({ + method: 'get', + url: '/api/ai/roi/channel', + params: { cameraId } + }) +} + +export function saveRoi(data) { + return request({ + method: 'post', + url: '/api/ai/roi/save', + data: data + }) +} + +export function deleteRoi(roiId) { + return request({ + method: 'delete', + url: `/api/ai/roi/delete/${roiId}` + }) +} + +export function bindAlgo(data) { + return request({ + method: 'post', + url: '/api/ai/roi/bindAlgo', + data: data + }) +} + +export function unbindAlgo(bindId) { + return request({ + method: 'delete', + url: '/api/ai/roi/unbindAlgo', + params: { bindId } + }) +} + +export function updateAlgoParams(data) { + return request({ + method: 'post', + url: '/api/ai/roi/updateAlgoParams', + data: data + }) +} diff --git a/web/src/api/cameraConfig.js b/web/src/api/cameraConfig.js new file mode 100644 index 000000000..c5a84bff2 --- /dev/null +++ b/web/src/api/cameraConfig.js @@ -0,0 +1,30 @@ +import request from '@/utils/request' + +export function queryCameraList(params) { + const { page, count, query, pulling, mediaServerId } = params + return request({ + method: 'get', + url: '/api/proxy/list', + params: { page, count, query, pulling, mediaServerId } + }) +} + +export function startCamera(id) { + return request({ + method: 'get', + url: '/api/proxy/start', + params: { id } + }) +} + +export function stopCamera(id) { + return request({ + method: 'get', + url: '/api/proxy/stop', + params: { id } + }) +} + +export function getSnapUrl(cameraId) { + return `/api/ai/roi/snap?cameraId=${encodeURIComponent(cameraId)}` +} diff --git a/web/src/icons/svg/algorithm.svg b/web/src/icons/svg/algorithm.svg new file mode 100644 index 000000000..8aea5b205 --- /dev/null +++ b/web/src/icons/svg/algorithm.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/svg/roi.svg b/web/src/icons/svg/roi.svg new file mode 100644 index 000000000..72a953295 --- /dev/null +++ b/web/src/icons/svg/roi.svg @@ -0,0 +1 @@ + diff --git a/web/src/router/index.js b/web/src/router/index.js index 1d844fa93..c39b7c45c 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -89,6 +89,7 @@ export const constantRoutes = [ path: '/map', component: Layout, redirect: '/map', + hidden: true, children: [{ path: '', name: 'Map', @@ -116,6 +117,7 @@ export const constantRoutes = [ meta: { title: '国标录像' } }, { + hidden: true, path: '/jtDevice', name: 'JTDevice', component: () => import('@/views/jtDevice/index'), @@ -129,6 +131,7 @@ export const constantRoutes = [ meta: { title: '部标录像' } }, { + hidden: true, path: '/push', name: 'PushList', component: () => import('@/views/streamPush/index'), @@ -167,6 +170,7 @@ export const constantRoutes = [ path: '/recordPlan', component: Layout, redirect: '/recordPlan', + hidden: true, children: [ { path: '', @@ -180,6 +184,7 @@ export const constantRoutes = [ path: '/cloudRecord', component: Layout, redirect: '/cloudRecord', + hidden: true, onlyIndex: 0, children: [ { @@ -200,6 +205,7 @@ export const constantRoutes = [ path: '/mediaServer', component: Layout, redirect: '/mediaServer', + hidden: true, children: [ { path: '', @@ -213,6 +219,7 @@ export const constantRoutes = [ path: '/platform', component: Layout, redirect: '/platform', + hidden: true, children: [ { path: '', @@ -222,6 +229,38 @@ export const constantRoutes = [ } ] }, + { + path: '/cameraConfig', + component: Layout, + redirect: '/cameraConfig', + onlyIndex: 0, + children: [ + { + path: '/cameraConfig', + name: 'CameraConfig', + component: () => import('@/views/cameraConfig/index'), + meta: { title: '摄像头配置', icon: 'roi' } + }, + { + hidden: true, + path: '/cameraConfig/roi/:cameraId', + name: 'CameraRoiConfig', + component: () => import('@/views/cameraConfig/roiConfig'), + meta: { title: 'ROI配置' } + } + ] + }, + { + path: '/algorithmConfig', + component: Layout, + redirect: '/algorithmConfig', + children: [{ + path: '', + name: 'AlgorithmConfig', + component: () => import('@/views/algorithmConfig/index'), + meta: { title: '算法配置', icon: 'algorithm' } + }] + }, { path: '/user', component: Layout, @@ -251,6 +290,7 @@ export const constantRoutes = [ { path: '/operations', component: Layout, + hidden: true, meta: { title: '运维中心', icon: 'operations' }, redirect: '/operations/systemInfo', children: [ diff --git a/web/src/store/index.js b/web/src/store/index.js index 138c4abed..e264d6cd0 100644 --- a/web/src/store/index.js +++ b/web/src/store/index.js @@ -23,6 +23,8 @@ import gbRecord from './modules/gbRecord' import log from './modules/log' import frontEnd from './modules/frontEnd' import jtDevice from './modules/jtDevice' +import aiRoi from './modules/aiRoi' +import aiAlgorithm from './modules/aiAlgorithm' Vue.use(Vuex) @@ -49,7 +51,9 @@ const store = new Vuex.Store({ gbRecord, log, frontEnd, - jtDevice + jtDevice, + aiRoi, + aiAlgorithm }, getters }) diff --git a/web/src/store/modules/aiAlgorithm.js b/web/src/store/modules/aiAlgorithm.js new file mode 100644 index 000000000..d4e9df43e --- /dev/null +++ b/web/src/store/modules/aiAlgorithm.js @@ -0,0 +1,41 @@ +import { + queryAlgorithmList, + toggleAlgorithmActive, + syncAlgorithmsFromEdge +} from '@/api/aiAlgorithm' + +const actions = { + queryAlgorithmList({ commit }) { + return new Promise((resolve, reject) => { + queryAlgorithmList().then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + toggleAlgorithmActive({ commit }, [id, isActive]) { + return new Promise((resolve, reject) => { + toggleAlgorithmActive(id, isActive).then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + syncAlgorithmsFromEdge({ commit }) { + return new Promise((resolve, reject) => { + syncAlgorithmsFromEdge().then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} diff --git a/web/src/store/modules/aiRoi.js b/web/src/store/modules/aiRoi.js new file mode 100644 index 000000000..30fa10275 --- /dev/null +++ b/web/src/store/modules/aiRoi.js @@ -0,0 +1,93 @@ +import { + queryRoiList, + queryRoiDetail, + queryRoiByCameraId, + saveRoi, + deleteRoi, + bindAlgo, + unbindAlgo, + updateAlgoParams +} from '@/api/aiRoi' + +const actions = { + queryRoiList({ commit }, params) { + return new Promise((resolve, reject) => { + queryRoiList(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryRoiDetail({ commit }, id) { + return new Promise((resolve, reject) => { + queryRoiDetail(id).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + queryRoiByCameraId({ commit }, cameraId) { + return new Promise((resolve, reject) => { + queryRoiByCameraId(cameraId).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + saveRoi({ commit }, data) { + return new Promise((resolve, reject) => { + saveRoi(data).then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + deleteRoi({ commit }, roiId) { + return new Promise((resolve, reject) => { + deleteRoi(roiId).then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + bindAlgo({ commit }, data) { + return new Promise((resolve, reject) => { + bindAlgo(data).then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + unbindAlgo({ commit }, bindId) { + return new Promise((resolve, reject) => { + unbindAlgo(bindId).then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + updateAlgoParams({ commit }, data) { + return new Promise((resolve, reject) => { + updateAlgoParams(data).then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + } +} + +export default { + namespaced: true, + actions +} diff --git a/web/src/views/algorithmConfig/index.vue b/web/src/views/algorithmConfig/index.vue new file mode 100644 index 000000000..4d696a1c1 --- /dev/null +++ b/web/src/views/algorithmConfig/index.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/web/src/views/cameraConfig/index.vue b/web/src/views/cameraConfig/index.vue new file mode 100644 index 000000000..6a4fe4f72 --- /dev/null +++ b/web/src/views/cameraConfig/index.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/web/src/views/cameraConfig/roiConfig.vue b/web/src/views/cameraConfig/roiConfig.vue new file mode 100644 index 000000000..d67415239 --- /dev/null +++ b/web/src/views/cameraConfig/roiConfig.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/web/src/views/roiConfig/components/AlgorithmParamEditor.vue b/web/src/views/roiConfig/components/AlgorithmParamEditor.vue new file mode 100644 index 000000000..f3c419ca7 --- /dev/null +++ b/web/src/views/roiConfig/components/AlgorithmParamEditor.vue @@ -0,0 +1,126 @@ + + + diff --git a/web/src/views/roiConfig/components/RoiAlgorithmBind.vue b/web/src/views/roiConfig/components/RoiAlgorithmBind.vue new file mode 100644 index 000000000..26a9cc7a0 --- /dev/null +++ b/web/src/views/roiConfig/components/RoiAlgorithmBind.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/web/src/views/roiConfig/components/RoiCanvas.vue b/web/src/views/roiConfig/components/RoiCanvas.vue new file mode 100644 index 000000000..2bece4a42 --- /dev/null +++ b/web/src/views/roiConfig/components/RoiCanvas.vue @@ -0,0 +1,294 @@ + + + + + diff --git a/web/src/views/roiConfig/index.vue b/web/src/views/roiConfig/index.vue new file mode 100644 index 000000000..87b268904 --- /dev/null +++ b/web/src/views/roiConfig/index.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/web/src/views/roiConfig/roiEditor.vue b/web/src/views/roiConfig/roiEditor.vue new file mode 100644 index 000000000..74265c553 --- /dev/null +++ b/web/src/views/roiConfig/roiEditor.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/数据库/aiot/初始化-mysql-aiot.sql b/数据库/aiot/初始化-mysql-aiot.sql new file mode 100644 index 000000000..2166ccbc5 --- /dev/null +++ b/数据库/aiot/初始化-mysql-aiot.sql @@ -0,0 +1,77 @@ +-- ============================================================ +-- AIoT 智能视频管理平台 - MySQL 初始化脚本 +-- 字段名与 FastAPI 端保持一致(snake_case) +-- ============================================================ + +-- 1. ROI 区域配置(对应 FastAPI 端 roi_configs) +CREATE TABLE IF NOT EXISTS wvp_ai_roi ( + id INT AUTO_INCREMENT PRIMARY KEY, + roi_id VARCHAR(50) NOT NULL COMMENT 'UUID,与FastAPI端同步用', + camera_id VARCHAR(50) NOT NULL COMMENT '通道国标编号', + channel_db_id INT NULL COMMENT 'WVP通道表ID', + device_id VARCHAR(50) NULL COMMENT '设备国标编号', + name VARCHAR(100) NULL COMMENT 'ROI名称', + roi_type VARCHAR(20) NOT NULL DEFAULT 'rectangle' COMMENT '形状:rectangle/polygon', + coordinates TEXT NOT NULL COMMENT 'JSON归一化坐标', + color VARCHAR(20) NULL DEFAULT '#FF0000' COMMENT '显示颜色', + priority INT NOT NULL DEFAULT 0 COMMENT '优先级', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用状态', + extra_params TEXT NULL COMMENT '扩展参数JSON', + description VARCHAR(500) NULL COMMENT '描述', + create_time VARCHAR(50) NULL COMMENT '创建时间', + update_time VARCHAR(50) NULL COMMENT '更新时间', + UNIQUE KEY uk_roi_id (roi_id), + INDEX idx_camera_id (camera_id), + INDEX idx_device_id (device_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ROI区域配置'; + +-- 2. 算法注册表(对应 FastAPI 端 algorithm_registry) +CREATE TABLE IF NOT EXISTS wvp_ai_algorithm ( + id INT AUTO_INCREMENT PRIMARY KEY, + algo_code VARCHAR(100) NOT NULL COMMENT '算法编码', + algo_name VARCHAR(200) NULL COMMENT '算法名称', + target_class VARCHAR(50) NULL COMMENT '目标类别', + param_schema TEXT NULL COMMENT '参数模板JSON', + description VARCHAR(500) NULL COMMENT '描述', + is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可用', + sync_time VARCHAR(50) NULL COMMENT '最后同步时间', + create_time VARCHAR(50) NULL COMMENT '创建时间', + update_time VARCHAR(50) NULL COMMENT '更新时间', + UNIQUE KEY uk_algo_code (algo_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='算法注册表'; + +-- 3. ROI 算法绑定(对应 FastAPI 端 roi_algo_bind) +CREATE TABLE IF NOT EXISTS wvp_ai_roi_algo_bind ( + id INT AUTO_INCREMENT PRIMARY KEY, + bind_id VARCHAR(50) NOT NULL COMMENT 'UUID,同步用', + roi_id VARCHAR(50) NOT NULL COMMENT 'ROI的UUID', + algo_code VARCHAR(100) NOT NULL COMMENT '算法编码', + params TEXT NULL COMMENT '自定义参数JSON', + priority INT NOT NULL DEFAULT 0 COMMENT '优先级', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用状态', + create_time VARCHAR(50) NULL COMMENT '创建时间', + update_time VARCHAR(50) NULL COMMENT '更新时间', + UNIQUE KEY uk_bind_id (bind_id), + INDEX idx_roi_id (roi_id), + INDEX idx_algo_code (algo_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ROI算法绑定'; + +-- 4. 配置变更日志(对应 FastAPI 端 config_update_log) +CREATE TABLE IF NOT EXISTS wvp_ai_config_log ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + config_type VARCHAR(30) NOT NULL COMMENT '类型:ROI/ALGORITHM/BIND', + config_id VARCHAR(50) NOT NULL COMMENT '目标ID', + old_value TEXT NULL COMMENT '变更前JSON', + new_value TEXT NULL COMMENT '变更后JSON', + updated_by VARCHAR(100) NULL COMMENT '操作人', + updated_at VARCHAR(50) NULL COMMENT '操作时间', + INDEX idx_config_type (config_type), + INDEX idx_config_id (config_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配置变更日志'; + +-- ============================================================ +-- 初始数据:预置算法(2个,不含人群聚集检测) +-- ============================================================ +INSERT INTO wvp_ai_algorithm (algo_code, algo_name, target_class, param_schema, description, is_active, create_time, update_time) VALUES +('leave_post', '离岗检测', 'person', '{"confirm_on_duty_sec":{"type":"int","default":10,"min":1},"confirm_leave_sec":{"type":"int","default":10,"min":1},"cooldown_sec":{"type":"int","default":300,"min":0},"working_hours":{"type":"list","default":[]}}', '检测人员是否在岗,支持工作时间段配置', 1, NOW(), NOW()), +('intrusion', '周界入侵检测', 'person', '{"cooldown_seconds":{"type":"int","default":120,"min":0},"confirm_seconds":{"type":"int","default":5,"min":1}}', '检测人员进入指定区域', 1, NOW(), NOW());