Compare commits

..

125 Commits

Author SHA1 Message Date
砂糖
4f064c2e3e feat: 增加设备名称 2025-10-09 10:00:09 +08:00
98c2aeaa9b Merge remote-tracking branch 'origin/master' 2025-10-09 09:54:53 +08:00
98042b237c feat(video): 添加设备名称字段并优化巡检任务关联查询
- 在 Device 实体类中新增 deviceName 字段及其 getter/setter 方法
- 更新 DeviceMapper.xml,支持 device_name 字段的查询、插入和更新操作
- 修改 InspectionTask 实体类,增加 deviceIp 字段用于联查返回设备 IP
- 调整 InspectionTaskMapper.xml,通过左连接获取设备名称与 IP 信息- 移除冗余的 device_name 插入与更新逻辑
- 注释掉旧有的设备信息设置代码,避免重复赋值
- 更新 toString 方法以包含新的 deviceName 属性
2025-10-09 09:54:40 +08:00
砂糖
bf0996d750 修改首页设备列表 2025-10-08 16:16:41 +08:00
砂糖
3abac5ff1b feat: 报警页面完善 2025-10-08 13:53:54 +08:00
99a8a943bc feat(video): 新增报警批量处理功能并优化任务执行逻辑
- 新增 alarmBatchBo 类用于批量处理报警记录
- 移除报警记录控制器中的权限注解
- 批量处理接口改为接收 alarmBatchBo 对象
- 引入 ruoyi-quartz 依赖用于定时任务处理- 恢复并优化 InspectionTaskServiceImpl 中设备信息设置
- 更新任务执行时更新最后执行时间与下次执行时间- 视频分析服务中增加报警记录时更新任务报警次数
2025-10-08 13:40:04 +08:00
f0b4c5a8bf 提高视频帧率 2025-10-08 11:51:28 +08:00
砂糖
f40d6ffcb6 1 2025-10-08 10:09:48 +08:00
砂糖
524c8343e6 feat: 增加打印信息 2025-10-08 10:00:36 +08:00
aa32f9e9ac fix(models): 解决 PyTorch 2.6+ 兼容性问题
- 在 garbage_model.py 和 smoke_model.py 中添加 weights_only=False 参数以允许加载模型类结构
- 修复 HTTP YOLO 检测器中的文件上传和响应解析逻辑- 移除不必要的导入并优化代码结构
- 添加自定义字节数组资源类以支持 RestTemplate 文件上传- 改进错误处理和日志记录机制
2025-10-07 17:53:34 +08:00
5f6058c024 refactor(detector):重构HTTP YOLO检测器实现
- 使用ByteArrayResource替代自定义资源类
- 将model_name参数移至URL查询参数
-优化响应解析逻辑,增强类型检查
- 改进错误处理和空值判断
- 清理无用的导入和代码格式化- 修复潜在的编码异常处理问题
2025-10-07 16:57:03 +08:00
e3701991ef fix(video): 修改Python服务URL为本地地址
- 将检测器API地址从容器名改为localhost- 更新PYTHON_API_URL常量值为http://localhost:8000/api/detect/file
2025-10-07 16:35:26 +08:00
7096359434 feat(video): 添加模型名称字段以支持动态模型选择
- 在 InspectionTask 实体类中新增 modelName 字段及其 getter/setter 方法
- 更新 MyBatis 映射文件,增加对 model_name 字段的映射和支持
- 修改 SQL 查询语句,在查询条件和插入、更新操作中加入 modelName 字段处理
- 调整 VideoAnalysisService 中的模型选择逻辑,优先使用任务配置的模型名称
- 记录日志输出所使用的模型名称及对应的任务ID,便于追踪分析过程
2025-10-07 16:07:23 +08:00
砂糖
4cec966613 feat: python模型管理 2025-10-07 15:49:58 +08:00
1a7ecafc7d Merge remote-tracking branch 'origin/master' 2025-10-07 14:29:44 +08:00
735704d585 refactor(scheduler): 优化巡检任务调度逻辑
- 移除任务状态自动规范化逻辑
- 不再修改任务状态字段
- 仅根据任务状态 0=启用、1=停用 控制触发
- 移除运行状态缓存机制- 注释掉任务状态更新相关代码
- 调整任务记录失败状态值为2
2025-10-07 14:29:33 +08:00
砂糖
c32385e87d Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-10-07 14:28:17 +08:00
砂糖
3cf64fd02f feat: 将部分系统菜单改到右上角 2025-10-07 14:28:14 +08:00
4a34892ea9 fix(video): 更新Python服务端口配置
- 将视频分析服务中的Python API URL端口从8000更改为10083
- 确保与容器化环境中的实际服务端口保持一致
2025-10-07 13:37:18 +08:00
9b8cf2509d Merge remote-tracking branch 'origin/master' 2025-10-07 13:15:03 +08:00
53d60c1016 feat(video): 添加内部巡检任务调度器当点击启动的时候应该执行定时任务,此处不借用若依自带的定时任务 因为需要在系统监控里面加入定时任务才能使用多了一个步骤 除了status为1(停止)的其余都需要加入轮询
- 引入 javax.annotation-api 依赖以支持注解生命周期管理- 实现 InspectionCronScheduler 调度器组件
- 使用 ScheduledExecutorService 每10秒轮询一次巡检任务- 支持解析 Cron 表达式并计算下次执行时间
- 自动更新任务的下次执行时间和状态
- 添加触发去抖机制防止重复执行
- 异步调用 inspectionTaskService 执行具体任务
-任务执行后自动规范化状态为启用- 增加详细的日志记录和异常处理机制
2025-10-07 13:14:55 +08:00
砂糖
44182e6a74 Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-10-07 13:13:10 +08:00
砂糖
f80035d32a 完善首页 2025-10-07 13:13:06 +08:00
c7345ccfca Merge remote-tracking branch 'origin/master' 2025-10-07 11:36:01 +08:00
e657aab730 fix(video): 修改视频附件分隔符
- 将附件URL拼接分隔符从分号改为逗号
- 更新 InspectionTaskRecord 的附件字段更新逻辑- 确保附件列表格式统一,便于后续解析处理
2025-10-07 11:35:53 +08:00
砂糖
9fd1832ce9 Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-10-07 11:10:56 +08:00
砂糖
7e0e55944e feat: 任务状态字典展示 2025-10-07 11:10:54 +08:00
eb3a50f89d feat(video): 添加MinIO对象更新和批量删除功能- 新增updateVMinioObject方法支持按ID更新对象信息- 新增selectVMinioObjectList方法用于查询所有对象
- 新增deleteVMinioObjectByIds方法支持批量删除对象
- 完善XML映射文件中的动态SQL配置- 优化删除逻辑以提高执行效率和安全性
2025-10-07 11:08:28 +08:00
018a050e52 fix(video):修正巡检任务查询映射配置
- 将 selectInspectionTaskById 查询的 resultType 调整为 resultMap
- 确保返回结果正确映射到 InspectionTask 对象- 修复可能因映射不匹配导致的数据读取问题
2025-10-07 10:37:42 +08:00
0b71f7018a Merge remote-tracking branch 'origin/master' 2025-10-07 10:18:43 +08:00
501a376f98 feat(video): 新增巡检任务查询功能
- 添加根据任务ID查询巡检任务的SQL映射- 修改删除单个巡检任务的方法名为deleteInspectionTaskById
- 修改批量删除巡检任务的方法名为deleteInspectionTaskByIds
- 优化XML文件结构,提升可读性
2025-10-07 10:18:33 +08:00
砂糖
0e4f543434 视频巡检详情页面 2025-10-07 10:02:26 +08:00
f4418b8860 提高视频帧率 2025-10-02 15:51:00 +08:00
c1e116f65d 提高视频帧率 2025-10-02 15:35:03 +08:00
c63d6ffb3d 修复工作 2025-10-02 15:24:58 +08:00
dfc0baff58 修复工作 2025-10-02 15:20:41 +08:00
7a31bf7c2a 修复工作 2025-10-02 15:14:37 +08:00
1deafab5b3 修复工作 2025-10-02 15:06:35 +08:00
89535c9a5c 修复工作 2025-10-02 14:55:58 +08:00
b43cf4bc5e 修复工作 2025-10-02 14:52:17 +08:00
204908b580 修复工作 2025-10-02 14:48:28 +08:00
8976a70a21 修复工作 2025-10-02 14:41:47 +08:00
4bbbff266d 修复工作 2025-10-02 14:32:42 +08:00
2a689d6e5c 修复工作 2025-10-02 14:16:13 +08:00
89a8526e5d 修复工作 2025-10-01 23:59:52 +08:00
03397d1c4a 修复工作 2025-10-01 23:54:14 +08:00
694ea5b1af 修复工作 2025-10-01 23:42:52 +08:00
a3e0a45b9c 修复工作 2025-10-01 23:37:51 +08:00
c730334f8d 修复工作 2025-10-01 23:32:29 +08:00
182f045517 修复工作 2025-10-01 23:27:08 +08:00
e462a99645 修复工作 2025-10-01 23:21:09 +08:00
cfd0489d3d 修复工作 2025-10-01 23:12:52 +08:00
ca021cdcce 修复工作 2025-10-01 23:06:28 +08:00
e6941d5ae0 修复工作 2025-10-01 23:02:42 +08:00
3050496a83 修复工作 2025-10-01 22:56:33 +08:00
65701395b9 修复工作 2025-10-01 22:50:16 +08:00
384084ba36 修复工作 2025-10-01 22:49:08 +08:00
144fa7b423 修复工作 2025-10-01 22:38:48 +08:00
fc88a11af3 修复工作 2025-10-01 22:30:06 +08:00
20f0481f3a 修复工作 2025-10-01 22:19:45 +08:00
dcd905bfde 修复工作 2025-10-01 22:05:03 +08:00
befad83e13 修改 2025-10-01 21:42:39 +08:00
863c191521 修复工作 2025-10-01 21:31:30 +08:00
34fce9e552 修改 2025-10-01 21:25:24 +08:00
a917bbc936 修改 2025-10-01 21:23:09 +08:00
5e54e4ae62 修复工作 2025-10-01 17:48:57 +08:00
b4e8cf5c33 修复工作 2025-09-30 21:12:52 +08:00
9e36a84354 修复工作 2025-09-30 20:56:55 +08:00
6dba13713f 修复工作 2025-09-30 20:48:14 +08:00
1f22d777e9 修复工作 2025-09-30 20:27:56 +08:00
e77a2f7cff 修复工作 2025-09-30 20:10:34 +08:00
8f4e25e895 修复工作 2025-09-30 19:51:37 +08:00
5e5d1acaaa 修复工作 2025-09-30 19:50:10 +08:00
8c3431cf2c 修复工作 2025-09-30 19:45:11 +08:00
95ab923d1a 修复工作 2025-09-30 19:36:48 +08:00
3150e504b7 修复工作 2025-09-30 19:31:01 +08:00
1323c1f717 修复工作 2025-09-30 19:23:42 +08:00
0f7dc58a50 修复工作 2025-09-30 19:14:28 +08:00
7e7818b7d3 修复工作 2025-09-30 18:19:31 +08:00
730f508ff8 修复工作 2025-09-30 18:07:39 +08:00
34a4c7912c 修复工作 2025-09-30 17:47:13 +08:00
197ff9b888 将检测任务迁移python 2025-09-30 17:34:54 +08:00
e82d919ebf 1 2025-09-30 17:34:03 +08:00
62d9c0ffb5 将检测任务迁移python 2025-09-30 17:09:37 +08:00
d32e3c4040 将检测任务迁移python 2025-09-30 17:03:41 +08:00
bdf33e28e6 update dockerfile 2025-09-30 16:55:42 +08:00
e0f2b8bf35 Merge remote-tracking branch 'origin/master' 2025-09-30 14:24:59 +08:00
39d39a7a24 将检测任务迁移python 2025-09-30 14:23:33 +08:00
砂糖
049c683814 Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-09-30 14:23:20 +08:00
砂糖
73fc27f91f feat: 登录页改为loading页面 2025-09-30 14:23:17 +08:00
3fe5f8083d Merge remote-tracking branch 'origin/master' 2025-09-30 13:42:56 +08:00
c15b9a06c9 fix(video):修复自动巡检时模型加载错误
- 确保自动巡检功能使用正确的检测模型-修复模型管理器获取错误的模型键值问题
2025-09-30 13:42:48 +08:00
砂糖
214f8d1f65 Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-09-30 13:24:56 +08:00
砂糖
0e5a153e4e feat: 巡检记录 2025-09-30 13:24:53 +08:00
c24c5f5f21 feat(sql): 删除任务表中的记录数据id字段
- 移除了 `record_id` 字段以简化任务表结构- 更新了相关注释和字段描述信息- 调整了字段顺序以保持逻辑一致性
2025-09-30 13:00:55 +08:00
fe422241c8 feat(video): 添加巡检任务记录关联任务ID功能
- 在巡检任务记录表中新增 task_id 字段并建立关联关系
- 移除巡检任务实体中的冗余 recordId 字段及相关映射配置- 更新数据库查询语句以适配新的字段结构
- 为 InspectionTaskRecord 实体类增加 taskId 属性及对应 getter/setter 方法- 新增根据任务ID查询记录列表的接口方法
- 修改 MyBatis 映射文件支持 task_id 的增删改查操作
- 调整 toString 方法输出内容以包含 taskId 信息
2025-09-30 11:55:55 +08:00
c63e130729 feat(video): 添加巡检任务记录功能及相关接口
- 在 InspectionTask 实体中新增 recordId 字段及其 getter/setter 方法
- 更新 InspectionTask 的 toString 方法以包含 recordId
- 修改 InspectionTaskMapper.xml,增加 record_id 的映射和查询字段
- 新增巡检任务记录实体类 InspectionTaskRecord 及其相关属性与方法
- 创建巡检任务记录的控制器、服务层和数据访问层(Controller, Service, Mapper)- 实现巡检任务记录的增删改查接口,并支持导出 Excel 功能
- 配置 MyBatis XML 映射文件,完成数据库操作语句的编写
2025-09-30 11:27:04 +08:00
9508468265 feat(sql): 移除巡检任务记录表中的任务ID字段
- 删除 `v_inspection_task_record` 表中的 `task_id` 字段
- 简化表结构,减少冗余关联字段- 调整表字段注释以适应新的数据模型
2025-09-30 10:46:57 +08:00
4711883466 feat(sql): 初始化巡检系统数据库表结构和测试数据
- 创建巡检任务表(v_inspection_task)和报警记录表(v_alarm_record)
- 添加巡检任务和报警记录的菜单权限配置
- 插入测试任务数据和权限菜单项
- 配置巡检任务和报警记录的详细权限控制
- 设置默认的巡检任务执行计划和报警处理流程
2025-09-30 10:43:57 +08:00
02c2a25c36 Merge remote-tracking branch 'origin/master' 2025-09-29 18:26:47 +08:00
f09d814a70 修改 2025-09-29 18:26:41 +08:00
砂糖
fa1568e3dc feat: 替换logo 2025-09-29 14:12:39 +08:00
砂糖
3cc63e031e Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-09-29 14:08:00 +08:00
砂糖
427bf937ef feat: 去除登录页 2025-09-29 14:07:58 +08:00
bd92a0b317 Merge remote-tracking branch 'origin/master' 2025-09-29 13:51:29 +08:00
6d60be06aa 修改 2025-09-29 13:45:55 +08:00
砂糖
79f0ab66c3 Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-09-29 13:38:13 +08:00
砂糖
89be8981ff feat: 模型的文件上传 2025-09-29 13:38:10 +08:00
9a682f4ff2 feat(video): 实现报警记录详情查看与处理功能
- 新增查询报警记录详细接口- 修改处理报警记录接口为 PUT 方法- 新增导出报警记录接口
- 前端页面增加报警视频播放功能
-优化报警记录处理状态显示逻辑- 完善报警详情展示内容,支持图片和视频预览
- 后端实现会话聚合逻辑,支持截图和视频证据保存
- 新增模型修改接口
- 调整权限注解配置
- 完善 MinIO 文件上传和回填逻辑
2025-09-29 13:25:54 +08:00
bb325bcfbf Merge remote-tracking branch 'origin/master' 2025-09-29 11:24:42 +08:00
7ca2f82ebe refactor(video): 移除设备和巡检任务接口权限注解并调整模型控制器包结构
- 移除了 DeviceController 中所有方法的 @PreAuthorize 权限注解
- 移除了 InspectionTaskController 中所有方法的 @PreAuthorize 权限注解
- 将 ModelController 从 ruoyi-admin 模块迁移至 ruoyi-video 模块- 调整 ModelController 包路径并继承 BaseController-为 ModelController 的各个接口添加了相应的权限注解- 修改 list 方法返回类型为 TableDataInfo 并支持分页查询- 引入缺失的类依赖和安全注解支持
2025-09-29 11:22:53 +08:00
砂糖
94e23b8f04 Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-09-29 11:13:23 +08:00
砂糖
457eceb647 feat: 紧凑布局 2025-09-29 11:13:20 +08:00
e62b89a290 refactor(video): 使用@Autowired注解简化依赖注入
- 移除了构造函数注入方式
- 添加了@Autowired注解实现字段注入
- 简化了VModelMapper的依赖管理
- 提高了代码可读性和维护性
2025-09-29 10:38:44 +08:00
af815e00ee feat(video): 新增模型管理与MinIO对象存储功能
- 新增算法模型实体类VModel及对应CRUD接口和实现
- 新增MinIO对象记录实体类VMinioObject及对应CRUD接口和实现
- 实现模型下载重定向功能
- 扩展MinioService支持指定文件名上传和删除对象
- 在CommonController中增加上传后持久化MinIO对象记录逻辑
- 新增ModelController用于模型管理RESTful接口- 新增VMinioObjectController用于MinIO对象记录管理接口
- 添加相关Mapper XML配置和DAO接口
- 更新pom.xml引入必要依赖
2025-09-29 10:37:12 +08:00
e4f0c65478 Merge remote-tracking branch 'origin/master' 2025-09-28 22:07:00 +08:00
1e5166c403 修复问题 2025-09-28 18:30:39 +08:00
c822d03e98 Merge remote-tracking branch 'origin/master' 2025-09-28 18:23:29 +08:00
f3e072352b feat(storage): 集成MinIO对象存储支持
- 添加MinIO配置属性类MinioProperties
- 实现MinIO文件上传服务MinioService
- 在CommonController中增加MinIO上传逻辑分支
- 支持单文件和多文件的MinIO上传处理
- 保留原有本地文件上传作为备选方案- 添加MinIO Java SDK依赖到框架模块
- 移除重复的Spring Context依赖声明
2025-09-28 18:23:21 +08:00
516f4e94c9 1 2025-09-28 18:09:55 +08:00
6507bc6646 1 2025-09-28 17:21:20 +08:00
8b3e41f60f Merge remote-tracking branch 'origin/master' 2025-09-28 16:56:00 +08:00
e5275fdc5d 修改为openvivn 2025-09-28 16:55:52 +08:00
砂糖
792d97ee9a Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system 2025-09-28 15:22:23 +08:00
砂糖
a233e4358e 标题登录页首页等静态资源替换 2025-09-28 15:19:59 +08:00
166 changed files with 12338 additions and 2697 deletions

54
.dockerignore Normal file
View File

@@ -0,0 +1,54 @@
# Git
.git
.gitignore
.gitattributes
# CI/CD
.github
.gitlab-ci.yml
# IDE
.idea
.vscode
*.iml
*.ipr
*.iws
# Maven
**/target/
.mvn/
mvnw
mvnw.cmd
# Node
**/node_modules/
**/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Documentation
*.md
README*
LICENSE
docs/
# Scripts
bin/
*.bat
*.sh
# Logs
*.log
**/logs/
# Temporary files
*.tmp
*.temp
.DS_Store
Thumbs.db

39
.env Normal file
View File

@@ -0,0 +1,39 @@
# MySQL数据库配置远程MySQL
MYSQL_HOST=49.232.154.205
MYSQL_PORT=10080
MYSQL_DATABASE=fad_watch
MYSQL_USER=root
MYSQL_PASSWORD=Fuande@666
# Redis配置
REDIS_HOST=rtsp-redis
REDIS_PORT=6379
REDIS_PASSWORD=
# 服务器配置
SERVER_HOST=49.232.154.205
# Backend对外端口重要
BACKEND_EXTERNAL_PORT=10082
MEDIA_SERVER_EXTERNAL_PORT=10083
# 后端服务配置
BACKEND_HOST=rtsp-backend
BACKEND_PORT=8080
# 前端服务配置
FRONTEND_PORT=10081
# Python推理服务配置
PYTHON_SERVICE_HOST=rtsp-python-service
PYTHON_SERVICE_PORT=8000
# MinIO配置外部服务
MINIO_ENABLED=true
MINIO_ENDPOINT=http://49.232.154.205:10900
MINIO_ACCESS_KEY=4EsLD9g9OM09DT0HaBKj
MINIO_SECRET_KEY=05SFC5fleqTnaLRYBrxHiphMFYbGX5nYicj0WCHA
MINIO_BUCKET=rtsp
# 时区配置
TZ=Asia/Shanghai

View File

@@ -1 +0,0 @@
EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTIBJBDZGZJSFPAUBTBXFSIUFTCMBHGCBKAGHFAMFSGYEIJPGPGXGJEREEBYAJFHFGESCXBJIGGHDBISEQAMAGGWGCADDVENHCIYITAVBUDYDWJGIRIRIKFHBAABGUHEDHFODQGUATGIGSEPBFBMBEDNCUGSEBCGCMCJGTEMFTCGBQBEBCFOCGDOIGEGFDAIEYEUEXGMDWJAFHCEHBGLJHAIIGGQANAKFBCOBGIUHYEWCSGNANCICCCOGNIVITFDHPHQCBANJIBTAQEMJFJLAJFGELHOHPGPAIGCEJDVHOIZHXECIIAMEPDJGBCXGSHCHHCVGVIJCKHKBXCBHKFRISAIDTFWGOGRBHBMFAHYACAQFRHAGCGVBWFWFGBCIGEHEHJBHZHABMGCHJEFCUEDFIBSABDTDYGXBMFRIZAHINEUBJFHICIJHRANJCBXBBBMFEJDJPDQCLIOFLDKDEGTICCMCWIUDWBYGFAPJOAUCCFUEMBEBOGDIFJMHJDQGWBFACBKEREXGPBXDJBPEZCUDTCRFVBWHACNFACDFDFEBIJKJNGWAPAMGQFDDKIJBCJNHLBBDOFMEBGUHDDYISIBEAJMEAIQEDCJFQBEIUHFJTCAIVBZGMCAFBDTHFDCARBRFWBFGRCJDIHGIZABDJFQGLCTHCJUJCJGGNHXHOBHHRIIBQIGJFACDRDBAHFMEMFQAZFXBAIJBCGCHZFRHNBFEPCFBIHFDDAWGPFBECITGIASAIDXHMAIHVHKINFVDHDDGFJHHWECASFHITFQIJACDACVAIDAFVISGAALHFCQACEUHOBOGFDEBHFSBUEQEXESEWIOJUGXHHIZDTEZJGDFIOAJGTDCBUBABPEVBTJJGNGGHOFCCHIWGFCPBDCEANIIACBFBEDSCMIWISCXAZFUBFCSGSENHOHKGLGDGEBTAIBNIOIRGMFBIMHVEXFGFICECPAQCZEFFZERETCTBEHLEEJPIFDVHHGNIZAOINGOCKBPDRCIBQJUBYDCEGFWBECSDDJHGCIUCUCCGKIHDRIRIPJBENGAAPJSDIBECTHAAXIVHZFFDCHDHWJTIQAIIPBYGMEMCYCPIRGOJHINFZASGVDVBWHZCNEFJI

567
COMPLETE-SUMMARY.md Normal file
View File

@@ -0,0 +1,567 @@
# 🎉 RTSP视频分析系统 - 完整更新总结
## 📋 项目概述
本文档总结了RTSP视频分析系统的所有Docker部署和巡检任务记录功能的实现。
## ✅ 完成的工作
### 一、Docker部署方案
#### 1.1 核心配置文件
-`.env` - 环境变量配置前端端口10080
-`docker-compose.yml` - 5个服务编排无MinIO使用外部
-`.dockerignore` - Docker构建优化
#### 1.2 Dockerfile文件
-`ruoyi-admin/Dockerfile` - Java后端多阶段构建
-`rtsp-vue/Dockerfile` - Vue前端多阶段构建
-`rtsp-vue/nginx.conf` - Nginx配置容器名代理
-`python-inference-service/Dockerfile` - Python服务CPU模式
#### 1.3 YOLOv8支持
-`python-inference-service/requirements.txt` - ultralytics>=8.0.0
-`python-inference-service/models/yolov8_model.py` - YOLOv8包装类
-`python-inference-service/models/models.json` - yolov8_detector配置
#### 1.4 部署脚本
-`deploy.bat` - Windows一键部署
-`deploy.sh` - Linux/Mac一键部署
#### 1.5 部署文档
-`README-DOCKER.md` - 完整部署文档
-`DOCKER-QUICK-START.md` - 快速开始指南
-`YOLOV8-SETUP.md` - YOLOv8配置指南
-`DEPLOYMENT-NOTES.md` - 部署配置说明
-`FINAL-SUMMARY.md` - 最终配置总结
### 二、巡检任务记录功能
#### 2.1 新增Service层
-`IInspectionTaskRecordService.java` - 服务接口
-`IInspectionTaskRecordServiceImpl.java` - 服务实现
#### 2.2 修改核心服务
-`InspectionTaskServiceImpl.java`
- 添加依赖注入InspectionTaskRecordMapper等
- 修改`executeInspectionTask()` - 创建记录
- 新增`performVideoAnalysisWithRecord()` - 录制+分析
- 新增`analyzeVideoAndUpdateRecord()` - 调用分析
- 新增`updateRecordFailed()` - 失败处理
-`VideoAnalysisService.java`
- 更新Python服务URL为容器名
- 更新模型名称为yolov8_detector
- 新增`analyzeVideoWithRecord()` - 带记录的分析
- 新增`processVideoWithRecord()` - 处理并记录
- 新增`createAlarmRecordForRecord()` - 去重告警
- 新增`uploadProcessedVideoForRecord()` - 上传视频
#### 2.3 功能文档
-`INSPECTION-WORKFLOW.md` - 详细工作流程
-`INSPECTION-FEATURE-SUMMARY.md` - 功能总结
## 🎯 关键特性
### Docker部署特性
| 特性 | 说明 |
|------|------|
| YOLOv8 | CPU模式无需GPU/CUDA |
| MinIO | 使用外部服务49.232.154.205:10900 |
| 端口 | 只暴露前端10080端口 |
| 网络 | 容器间使用容器名通信 |
| 服务 | MySQL, Redis, Python(CPU), Backend, Frontend |
### 巡检任务特性
| 特性 | 说明 |
|------|------|
| 自动记录 | 每次执行自动创建InspectionTaskRecord |
| 视频保存 | 自动录制并上传到MinIO |
| AI识别 | 调用Python服务(YOLOv8)识别 |
| 结果更新 | 自动更新record.accessory和result |
| 告警去重 | 相同对象只创建一次告警 |
## 🔄 完整工作流程
```
用户启动巡检任务
创建InspectionTaskRecord
├── recordId: [auto]
├── taskId: 1001
├── executeTime: now
└── status: 1 (执行中)
录制RTSP视频流
├── 抓取视频流
├── 录制30秒
└── 保存临时文件
上传原始视频
├── 上传到MinIO
├── 获取URL
└── 更新record.accessory = "video1.mp4"
AI识别处理
├── 逐帧分析
├── 每10帧调用Python API (YOLOv8)
├── 检测结果去重
└── 创建AlarmRecord
├── 提取检测区域图片
├── 上传告警图片
└── 保存告警记录
生成处理后视频
├── 绘制检测框
├── 上传到MinIO
└── 更新record.accessory += ";video2.mp4"
更新记录
├── record.result = "检测结果摘要"
├── record.duration = 32秒
└── record.status = 0 (成功)
```
## 📊 数据表关系
```
InspectionTask (1)
InspectionTaskRecord (N)
├── accessory: 原始视频URL;处理后视频URL
├── result: 共检测到X个问题详情...
└── status: 0=成功, 1=失败, 2=部分成功
AlarmRecord (N)
├── 告警类型、内容、置信度
├── 告警图片URL
├── 视频帧位置
└── 自动去重(相同对象只记录一次)
```
## 🚀 快速开始
### 1. Docker部署
```bash
# 1. 准备YOLOv8模型
# 将best.pt放到: python-inference-service/models/best.pt
# 2. 一键部署
deploy.bat # Windows
# 3. 访问系统
# http://localhost:10080
```
### 2. 创建巡检任务
```java
// 创建任务
InspectionTask task = new InspectionTask();
task.setDeviceId(deviceId);
task.setDuration(30); // 录制30秒
task.setStatus(0); // 待执行
inspectionTaskService.insertInspectionTask(task);
// 启动任务
inspectionTaskService.executeInspectionTask(task.getTaskId());
```
### 3. 查看结果
```sql
-- 查看执行记录
SELECT * FROM v_inspection_task_record
WHERE task_id = ?
ORDER BY execute_time DESC;
-- 查看告警
SELECT * FROM v_alarm_record
WHERE task_id = ?
ORDER BY create_time DESC;
```
## 📦 文件清单
### Docker部署文件15个
```
.env # 环境变量
docker-compose.yml # 服务编排
.dockerignore # 构建优化
ruoyi-admin/Dockerfile # 后端镜像
rtsp-vue/Dockerfile # 前端镜像
rtsp-vue/nginx.conf # Nginx配置
rtsp-vue/.dockerignore # 前端构建优化
python-inference-service/Dockerfile # Python镜像
python-inference-service/.dockerignore # Python构建优化
deploy.bat # Windows部署脚本
deploy.sh # Linux部署脚本
README-DOCKER.md # 完整文档
DOCKER-QUICK-START.md # 快速开始
DEPLOYMENT-NOTES.md # 配置说明
FINAL-SUMMARY.md # 配置总结
```
### YOLOv8文件4个
```
python-inference-service/requirements.txt # ultralytics依赖
python-inference-service/models/yolov8_model.py # YOLOv8包装类
python-inference-service/models/models.json # 模型配置
YOLOV8-SETUP.md # 配置指南
```
### 巡检任务文件5个
```
ruoyi-video/src/main/java/com/ruoyi/video/service/
├── IInspectionTaskRecordService.java # 记录服务接口
└── impl/
└── IInspectionTaskRecordServiceImpl.java # 记录服务实现
ruoyi-video/src/main/java/com/ruoyi/video/service/impl/
├── InspectionTaskServiceImpl.java (修改) # 添加记录创建
└── VideoAnalysisService.java (修改) # 添加记录更新
INSPECTION-WORKFLOW.md # 工作流程文档
INSPECTION-FEATURE-SUMMARY.md # 功能总结
```
### 后端配置修改2个
```
ruoyi-admin/pom.xml # 添加actuator依赖
ruoyi-admin/src/main/resources/application.yml # 添加actuator配置
```
## 🎯 配置要点
### 1. 环境变量(.env
```bash
FRONTEND_PORT=10080 # 前端对外端口
MYSQL_HOST=rtsp-mysql # 数据库容器名
REDIS_HOST=rtsp-redis # Redis容器名
BACKEND_HOST=rtsp-backend # 后端容器名
PYTHON_SERVICE_HOST=rtsp-python-service # Python服务容器名
# MinIO配置在application.yml中
```
### 2. MinIO配置application.yml
```yaml
minio:
endpoint: http://49.232.154.205:10900
access-key: 4EsLD9g9OM09DT0HaBKj
secret-key: 05SFC5fleqTnaLRYBrxHiphMFYbGX5nYicj0WCHA
bucket: rtsp
```
### 3. Python服务URL
```java
// VideoAnalysisService.java
private static final String PYTHON_API_URL = "http://rtsp-python-service:8000/api/detect/file";
private static final String MODEL_NAME = "yolov8_detector";
```
## 📖 文档导航
### 快速开始
1. **FINAL-SUMMARY.md** - Docker配置总结
2. **DOCKER-QUICK-START.md** - 快速启动指南
### 部署相关
3. **README-DOCKER.md** - 完整部署文档
4. **DEPLOYMENT-NOTES.md** - 详细配置说明
5. **YOLOV8-SETUP.md** - YOLOv8模型配置
### 功能相关
6. **INSPECTION-FEATURE-SUMMARY.md** - 巡检功能总结
7. **INSPECTION-WORKFLOW.md** - 详细工作流程
### 更新记录
8. **UPDATE-SUMMARY.md** - 更新变更记录
9. **DEPLOYMENT-FILES.md** - 文件清单
10. **COMPLETE-SUMMARY.md** - 本文档
## 🔍 验证清单
### Docker部署验证
- [ ] 所有容器运行正常
```bash
docker-compose ps
```
- [ ] 前端可访问
```
http://localhost:10080
```
- [ ] Python服务健康
```bash
docker-compose logs python-service | grep "YOLOv8模型加载完成"
```
- [ ] 后端连接正常
```bash
docker-compose logs backend | grep "Started RuoYiApplication"
```
### 巡检功能验证
- [ ] 创建测试任务
- [ ] 执行任务
- [ ] 检查Record创建
```sql
SELECT * FROM v_inspection_task_record ORDER BY create_time DESC LIMIT 1;
```
- [ ] 检查视频保存
```sql
SELECT accessory FROM v_inspection_task_record WHERE record_id = ?;
```
- [ ] 检查识别结果
```sql
SELECT result FROM v_inspection_task_record WHERE record_id = ?;
```
- [ ] 检查告警创建
```sql
SELECT * FROM v_alarm_record WHERE task_id = ? ORDER BY create_time DESC;
```
## ⚙️ 系统架构
```
┌──────────────┐
│ 浏览器 │
└──────┬───────┘
│ :10080 ← 唯一对外端口
┌──────▼───────┐
│ Frontend │
│ (Nginx) │
└──────┬───────┘
┌───┴──────────────┐
│ │
┌──▼────┐ ┌──────▼──────┐
│Backend│ │ Python │
│ :8080 │──────│ Service │
│ │ │ :8000(CPU) │
└─┬──┬──┘ └─────────────┘
│ │
│ │ ┌──────────────┐
│ └──│ MinIO(外部) │
│ │ 49.232... │
│ └──────────────┘
┌─▼────────┐
│ MySQL │
│ Redis │
└──────────┘
```
## 🎯 工作流程
### 巡检任务执行流程
```
1. 启动任务
└── 创建InspectionTaskRecord (status=1执行中)
2. 录制视频
├── 从RTSP流录制30秒
├── 上传到MinIO
└── 更新record.accessory = "原始视频URL"
3. AI识别
├── 逐帧分析视频
├── 调用Python服务(YOLOv8)
├── 检测结果去重
└── 创建AlarmRecord不重复
4. 保存结果
├── 绘制检测框
├── 上传处理后视频
├── 更新record.accessory += ";处理后URL"
└── 更新record.result = "检测结果摘要"
5. 完成
├── record.status = 0 (成功)
├── record.duration = 实际时长
└── task.status = 2 (已完成)
```
## 📝 配置摘要
### Docker服务配置
| 服务 | 容器名 | 端口 | 暴露 |
|------|--------|------|------|
| MySQL | rtsp-mysql | 3306 | ❌ |
| Redis | rtsp-redis | 6379 | ❌ |
| Python | rtsp-python-service | 8000 | ❌ |
| Backend | rtsp-backend | 8080 | ❌ |
| Frontend | rtsp-frontend | 80→10080 | ✅ |
| MinIO | 外部服务 | 10900 | - |
### 巡检任务配置
| 配置项 | 值 | 位置 |
|--------|-----|------|
| Python服务URL | http://rtsp-python-service:8000 | VideoAnalysisService.java |
| 模型名称 | yolov8_detector | VideoAnalysisService.java |
| 检测频率 | 每10帧 | processVideoWithRecord() |
| 去重容差 | 10像素 | generateDetectionKey() |
| 去重时间窗口 | 60秒 | processVideoWithRecord() |
## 💡 使用建议
### 性能优化CPU模式
1. **使用最小模型**
- yolov8n.pt推荐
- 而不是yolov8l.pt或yolov8x.pt
2. **降低检测频率**
```java
if (frameCount % 30 == 0) { // 从10改为30
```
3. **缩短录制时长**
```java
task.setDuration(15); // 从30秒改为15秒
```
### MinIO Bucket准备
需要在外部MinIO服务中创建以下bucket
- `inspection-videos` - 巡检视频
- `alarm-images` - 告警图片
### 数据库表确认
确保以下表存在:
- `v_inspection_task` - 巡检任务
- `v_inspection_task_record` - 巡检记录
- `v_alarm_record` - 告警记录
- `v_minio_object` - MinIO对象
- `v_device` - 设备信息
## 🐛 常见问题
### Q1: 视频未保存
**检查**
```bash
docker-compose logs backend | grep "MinIO"
curl http://49.232.154.205:10900/minio/health/live
```
### Q2: Python识别失败
**检查**
```bash
docker-compose logs python-service
docker exec -it rtsp-python-service ls -lh /app/models/best.pt
```
### Q3: 告警重复
**调整**
```java
// generateDetectionKey中增大容差
int x = rect.x() / 20 * 20; // 从10改为20
```
### Q4: 执行时间过长
**优化**
- 降低检测频率每30帧而不是10帧
- 缩短录制时长
- 使用更小的YOLOv8模型
## 📞 获取帮助
### 查看日志
```bash
# 所有服务
docker-compose logs -f
# 后端
docker-compose logs -f backend
# Python服务
docker-compose logs -f python-service
```
### 查看数据
```sql
-- 最新的执行记录
SELECT * FROM v_inspection_task_record ORDER BY create_time DESC LIMIT 10;
-- 最新的告警
SELECT * FROM v_alarm_record ORDER BY create_time DESC LIMIT 10;
-- 统计信息
SELECT
t.task_id,
COUNT(DISTINCT r.record_id) as execution_count,
COUNT(DISTINCT a.alarm_id) as alarm_count
FROM v_inspection_task t
LEFT JOIN v_inspection_task_record r ON t.task_id = r.task_id
LEFT JOIN v_alarm_record a ON t.task_id = a.task_id
GROUP BY t.task_id;
```
## 🎓 学习资源
- [Ultralytics YOLOv8文档](https://docs.ultralytics.com/)
- [Docker Compose文档](https://docs.docker.com/compose/)
- [FFmpeg JavaCV文档](https://github.com/bytedeco/javacv)
- [MinIO文档](https://min.io/docs/minio/linux/index.html)
## ✨ 总结
### 部署方面
✅ 完整的Docker部署方案
✅ YOLOv8 CPU模式支持
✅ 外部MinIO集成
✅ 容器间使用容器名通信
✅ 只暴露前端端口10080
✅ 完整的健康检查和依赖管理
### 功能方面
✅ 自动创建巡检记录
✅ 自动录制和保存视频
✅ 调用Python服务(YOLOv8)识别
✅ 自动更新识别结果
✅ 创建不重复的告警记录
✅ 完整的异常处理和日志记录
### 文档方面
✅ 10个详细的文档文件
✅ 工作流程图示
✅ 配置说明
✅ 故障排查指南
✅ SQL查询示例
---
**项目状态**: ✅ 完成
**部署状态**: 待部署
**测试状态**: 待测试
**文档版本**: 1.0
**最后更新**: 2025-09-30
🎊 **所有功能已完整实现,可以开始部署和测试!**

1
DEPLOYMENT-FILES.md Normal file
View File

@@ -0,0 +1 @@

329
DEPLOYMENT-NOTES.md Normal file
View File

@@ -0,0 +1,329 @@
# 📌 部署配置说明
## 重要配置
### 1. MinIO配置使用外部服务
本系统使用**外部已部署的MinIO服务**不在Docker中部署MinIO。
**配置位置**: `ruoyi-admin/src/main/resources/application.yml`
```yaml
minio:
enabled: true
endpoint: http://49.232.154.205:10900
access-key: 4EsLD9g9OM09DT0HaBKj
secret-key: 05SFC5fleqTnaLRYBrxHiphMFYbGX5nYicj0WCHA
bucket: rtsp
```
**如需更换MinIO服务器**
1. 编辑上述配置文件
2. 重新构建后端镜像:`docker-compose build backend`
3. 重启服务:`docker-compose up -d`
### 2. Python推理服务CPU模式
本系统**不使用GPU**Python推理服务运行在CPU模式。
**配置特点**
- ✅ 无需NVIDIA Docker Runtime
- ✅ 无需GPU驱动
- ✅ 适合CPU服务器部署
- ⚠️ 推理速度比GPU慢
**如需提升性能**
1. 使用更小的YOLOv8模型如yolov8n.pt
2. 减小输入图像尺寸
3. 调整置信度阈值
### 3. 服务端口配置
**对外暴露端口**
- 前端10080可在.env中修改`FRONTEND_PORT`
**内部端口**(不对外暴露):
- 后端8080
- Python服务8000
- MySQL3306
- Redis6379
## Docker Compose配置要点
### 服务列表
```yaml
services:
mysql # MySQL数据库
redis # Redis缓存
python-service # Python推理服务CPU
backend # Java后端
frontend # Vue前端唯一对外暴露
```
**注意**没有MinIO服务使用外部MinIO。
### 服务依赖关系
```
frontend 依赖 backend (健康检查)
backend 依赖 mysql, redis (健康检查) + python-service (启动)
```
### 环境变量(.env
```bash
# 数据库
MYSQL_HOST=rtsp-mysql
MYSQL_PORT=3306
MYSQL_PASSWORD=ruoyi123
# Redis
REDIS_HOST=rtsp-redis
REDIS_PORT=6379
# 后端
BACKEND_HOST=rtsp-backend
BACKEND_PORT=8080
# 前端(对外端口)
FRONTEND_PORT=10080
# Python服务
PYTHON_SERVICE_HOST=rtsp-python-service
PYTHON_SERVICE_PORT=8000
```
**不包含MinIO配置**在application.yml中
## 网络架构
### 内部网络rtsp-network
所有服务运行在同一个Docker网络中
- 容器间通过容器名通信
- 外部只能访问前端10080端口
- 安全性高
### 对外访问
```
浏览器 → 前端:10080 → Nginx → 后端:8080
→ Python服务:8000
后端 → MySQL:3306
→ Redis:6379
→ MinIO:49.232.154.205:10900 (外部)
```
## YOLOv8模型配置
### 模型要求
- **模型格式**: PyTorch (.pt)
- **训练框架**: Ultralytics YOLOv8
- **模型位置**: `python-inference-service/models/best.pt`
- **类别文件**: `python-inference-service/models/classes.txt`(可选)
### 模型配置
**models.json**:
```json
[
{
"name": "yolov8_detector",
"path": "models/yolov8_model.py",
"size": [640, 640]
}
]
```
### 性能调优CPU模式
编辑 `yolov8_model.py`
```python
# 降低置信度阈值(检测更多目标)
self.conf_threshold = 0.25
# 使用更小的输入尺寸(提升速度)
self.img_size = 320 # 或 480
# 建议使用 yolov8n.pt最快的模型
```
## 数据持久化
### Docker卷
```yaml
volumes:
mysql-data # MySQL数据
redis-data # Redis数据
backend-logs # 后端日志
backend-upload # 上传文件
```
**注意**MinIO数据在外部服务器不在本地。
### 数据备份
```bash
# 备份MySQL
docker exec rtsp-mysql mysqldump -u root -pruoyi123 ry-vue > backup.sql
# 备份Redis
docker exec rtsp-redis redis-cli SAVE
docker cp rtsp-redis:/data/dump.rdb ./redis-backup.rdb
```
## 部署检查清单
### 部署前检查
- [ ] YOLOv8模型文件best.pt已放置
- [ ] 类别文件classes.txt已创建可选
- [ ] .env配置已检查
- [ ] MinIO外部服务可访问
- [ ] Docker和Docker Compose已安装
- [ ] 端口10080未被占用
### 部署后检查
- [ ] 所有容器运行正常:`docker-compose ps`
- [ ] 前端可访问http://localhost:10080
- [ ] 后端健康检查通过
- [ ] Python服务加载模型成功
- [ ] MySQL连接正常
- [ ] Redis连接正常
- [ ] MinIO外部服务连接正常
## 常见问题
### Q1: MinIO连接失败
**检查**
1. 外部MinIO服务是否可访问
2. application.yml中的配置是否正确
3. 网络是否畅通
```bash
# 测试连接
curl http://49.232.154.205:10900/minio/health/live
```
### Q2: Python推理慢
**原因**CPU模式比GPU慢
**优化**
1. 使用yolov8n.pt最小模型
2. 减小输入尺寸320或480
3. 提高置信度阈值(减少检测框)
### Q3: 容器启动失败
**排查步骤**
```bash
# 查看日志
docker-compose logs [服务名]
# 检查端口占用
netstat -an | grep 10080
# 检查资源
docker stats
```
### Q4: 模型加载失败
**检查**
1. best.pt文件是否存在
2. 文件是否是YOLOv8格式
3. Python依赖是否安装完整
```bash
# 进入容器检查
docker exec -it rtsp-python-service bash
ls -lh /app/models/
```
## 安全建议
1. **修改默认密码**
- MySQL: ruoyi123
- MinIO密钥在application.yml中
2. **网络隔离**
- 只暴露必要端口10080
- 使用防火墙限制访问
3. **MinIO安全**
- 使用HTTPS连接
- 定期更新密钥
- 限制bucket访问权限
4. **定期备份**
- MySQL数据
- Redis数据
- 应用日志
## 维护操作
### 更新服务
```bash
# 停止服务
docker-compose down
# 更新代码
git pull
# 重新构建
docker-compose build
# 启动服务
docker-compose up -d
```
### 查看日志
```bash
# 所有服务
docker-compose logs -f
# 特定服务
docker-compose logs -f backend
docker-compose logs -f python-service
```
### 清理资源
```bash
# 清理未使用的镜像
docker image prune -a
# 清理未使用的卷
docker volume prune
# 完全清理(包括数据)
docker-compose down -v
```
## 性能监控
```bash
# 查看资源使用
docker stats
# 查看容器状态
docker-compose ps
# 查看网络连接
docker network inspect rtsp-network
```
---
**最后更新**: 2025-09-30
**配置版本**: Docker Compose 3.8, YOLOv8 CPU模式

202
DOCKER-QUICK-START.md Normal file
View File

@@ -0,0 +1,202 @@
# 🚀 Docker快速启动指南
## 一键部署
### Windows
```bash
deploy.bat
```
### Linux/Mac
```bash
chmod +x deploy.sh
./deploy.sh
```
## 手动部署
```bash
# 1. 启动所有服务
docker-compose up -d
# 2. 查看服务状态
docker-compose ps
# 3. 查看日志
docker-compose logs -f
```
## 常用命令
### 服务管理
```bash
# 启动服务
docker-compose up -d
# 停止服务
docker-compose stop
# 重启服务
docker-compose restart
# 停止并删除容器
docker-compose down
# 停止并删除容器和数据卷
docker-compose down -v
```
### 日志查看
```bash
# 查看所有日志
docker-compose logs
# 查看特定服务日志
docker-compose logs backend
docker-compose logs frontend
docker-compose logs python-service
# 实时查看日志
docker-compose logs -f
# 查看最近100行日志
docker-compose logs --tail=100
```
### 服务重建
```bash
# 重新构建所有镜像
docker-compose build
# 重新构建特定服务
docker-compose build backend
# 强制重建并启动
docker-compose up -d --build --force-recreate
```
### 进入容器
```bash
# 进入后端容器
docker exec -it rtsp-backend sh
# 进入前端容器
docker exec -it rtsp-frontend sh
# 进入Python服务容器
docker exec -it rtsp-python-service bash
# 进入MySQL容器
docker exec -it rtsp-mysql bash
# 进入Redis容器
docker exec -it rtsp-redis sh
```
## 端口配置
| 服务 | 默认端口 | 对外暴露 | 说明 |
|------|---------|---------|------|
| 前端 | 10080 | ✅ | 唯一对外暴露的端口 |
| 后端 | 8080 | ❌ | 仅容器内部访问 |
| Python服务 | 8000 | ❌ | 仅容器内部访问CPU模式 |
| MySQL | 3306 | ❌ | 仅容器内部访问 |
| Redis | 6379 | ❌ | 仅容器内部访问 |
| MinIO | 外部服务 | - | 使用已部署的外部服务 |
要修改前端端口,编辑 `.env` 文件中的 `FRONTEND_PORT` 变量。
## 环境变量配置
所有配置都在 `.env` 文件中:
```bash
# 修改前端端口
FRONTEND_PORT=8080
# 修改数据库密码
MYSQL_ROOT_PASSWORD=your_secure_password
MYSQL_PASSWORD=your_secure_password
# 修改MinIO密码
MINIO_ROOT_PASSWORD=your_secure_password
```
修改后需要重新启动服务:
```bash
docker-compose down
docker-compose up -d
```
## 数据备份
### 备份MySQL数据
```bash
docker exec rtsp-mysql mysqldump -u root -pruoyi123 ry-vue > backup.sql
```
### 备份MinIO数据
```bash
docker cp rtsp-minio:/data ./minio-backup
```
### 恢复MySQL数据
```bash
docker exec -i rtsp-mysql mysql -u root -pruoyi123 ry-vue < backup.sql
```
## 故障排查
### 查看服务健康状态
```bash
docker-compose ps
```
### 服务启动失败
```bash
# 查看详细日志
docker-compose logs [服务名]
# 常见问题:
# 1. 端口被占用 -> 修改.env中的端口配置
# 2. 数据库连接失败 -> 等待MySQL完全启动约30秒
# 3. 内存不足 -> 增加Docker内存限制或减少服务
```
### 清理并重新开始
```bash
# 完全清理(会删除所有数据)
docker-compose down -v
docker system prune -a
# 重新部署
docker-compose up -d
```
## 访问地址
部署成功后:
- **前端界面**: http://localhost:10080
- **后端API**: 通过前端代理 `/prod-api/`
- **Python API**: 通过前端代理 `/python-api/`
- **API文档**: http://localhost:10080/prod-api/swagger-ui.html
## 首次使用
1. **准备YOLOv8模型**:将训练好的`best.pt`文件放到`python-inference-service/models/`目录
2. 访问 http://localhost:10080
3. 默认账号密码请参考主文档
4. 首次启动可能需要等待1-2分钟让所有服务完全启动
## YOLOv8模型说明
本系统使用**YOLOv8**Ultralytics进行目标检测
- 将训练好的YOLOv8模型`best.pt`)放到`python-inference-service/models/`目录
- (可选)创建`classes.txt`文件,每行一个类别名称
- 模型会自动加载并提供推理服务
## 更多帮助
详细文档请查看:`README-DOCKER.md`

279
FINAL-SUMMARY.md Normal file
View File

@@ -0,0 +1,279 @@
# ✅ 最终配置总结
## 🎯 您的部署配置
根据您的需求,已完成以下配置:
### 1. ✅ YOLOv8 (CPU模式)
- **框架**: Ultralytics YOLOv8
- **运行模式**: CPU无需GPU
- **模型文件**: `python-inference-service/models/best.pt`
### 2. ✅ MinIO (外部服务)
- **地址**: http://49.232.154.205:10900
- **配置位置**: `ruoyi-admin/src/main/resources/application.yml`
- **不在Docker中部署**
### 3. ✅ 前端端口
- **对外端口**: 10080
- **访问地址**: http://localhost:10080
## 📦 Docker服务列表
| 服务 | 说明 | 备注 |
|------|------|------|
| MySQL | 数据库 | 内部部署 |
| Redis | 缓存 | 内部部署 |
| Python服务 | YOLOv8推理 | CPU模式不需要GPU |
| Java后端 | 业务逻辑 | 连接外部MinIO |
| Vue前端 | 用户界面 | 唯一对外暴露 :10080 |
| MinIO | 对象存储 | **使用外部服务** |
## 🚀 快速部署
### 1. 准备模型文件
```bash
# 将YOLOv8训练的模型放到这里
python-inference-service/models/best.pt
```
### 2. 启动服务
```bash
# Windows
deploy.bat
# Linux/Mac
chmod +x deploy.sh
./deploy.sh
# 或手动启动
docker-compose up -d
```
### 3. 访问系统
```
前端: http://localhost:10080
```
## 📋 关键配置文件
### docker-compose.yml
**包含的服务**
```yaml
services:
mysql # ✅ 内部部署
redis # ✅ 内部部署
python-service # ✅ CPU模式无GPU配置
backend # ✅ 连接外部MinIO
frontend # ✅ 对外端口10080
# minio # ❌ 不部署,使用外部
```
### .env
**环境变量**
```bash
MYSQL_HOST=rtsp-mysql
REDIS_HOST=rtsp-redis
BACKEND_HOST=rtsp-backend
FRONTEND_PORT=10080 # 前端对外端口
PYTHON_SERVICE_HOST=rtsp-python-service
# MinIO配置在application.yml中不在.env
```
### application.yml
**MinIO外部服务配置**
```yaml
minio:
enabled: true
endpoint: http://49.232.154.205:10900
access-key: 4EsLD9g9OM09DT0HaBKj
secret-key: 05SFC5fleqTnaLRYBrxHiphMFYbGX5nYicj0WCHA
bucket: rtsp
```
## 🎯 系统架构
```
┌────────────────┐
│ 浏览器 │
│ │
└───────┬────────┘
│ :10080 ← 唯一对外端口
┌───────▼────────┐
│ Frontend │
│ (Nginx) │
└───────┬────────┘
┌───┴───────────────┐
│ │
┌───▼─────┐ ┌──────▼──────┐
│ Backend │ │ Python │
│ :8080 │ │ Service │
│ │ │ :8000(CPU) │
└─┬──┬──┬─┘ └─────────────┘
│ │ │
│ │ └────────────┐
│ │ │
│ │ ┌────▼─────┐
│ │ │ MinIO │
│ │ │ 外部服务 │
│ │ │ 49.232.. │
│ │ └──────────┘
│ │
┌─▼──▼───┐
│ MySQL │
│ Redis │
└────────┘
```
## ⚙️ 配置特点
### ✅ 优点
1. **无需GPU**
- 不需要NVIDIA驱动
- 不需要NVIDIA Docker Runtime
- 适合普通CPU服务器
2. **使用外部MinIO**
- 不占用本地存储
- 配置灵活
- 易于扩展
3. **网络安全**
- 只暴露前端端口10080
- 其他服务内部通信
- 提高安全性
### ⚠️ 注意事项
1. **推理速度**
- CPU模式比GPU慢
- 建议使用yolov8n.pt最小模型
- 可适当降低图像尺寸
2. **MinIO依赖**
- 需要外部MinIO服务可用
- 修改配置需重新构建后端
3. **首次启动**
- 等待1-2分钟让服务完全启动
- Python服务需要加载模型
## 📖 重要文档
| 文档 | 说明 |
|------|------|
| `DEPLOYMENT-NOTES.md` | **最重要** - 部署配置说明 |
| `DOCKER-QUICK-START.md` | 快速启动和常用命令 |
| `YOLOV8-SETUP.md` | YOLOv8模型配置 |
| `README-DOCKER.md` | 完整部署文档 |
| `UPDATE-SUMMARY.md` | 更新变更记录 |
## 🔍 部署检查清单
### 部署前
- [ ] Docker和Docker Compose已安装
- [ ] YOLOv8模型文件best.pt已准备
- [ ] 外部MinIO服务可访问http://49.232.154.205:10900
- [ ] 端口10080未被占用
- [ ] 服务器有足够CPU资源
### 部署后
- [ ] 所有容器正常运行:`docker-compose ps`
- [ ] 前端可访问http://localhost:10080
- [ ] Python服务加载模型成功
- [ ] 后端连接MySQL成功
- [ ] 后端连接Redis成功
- [ ] 后端连接外部MinIO成功
## 🛠️ 常用命令
```bash
# 启动服务
docker-compose up -d
# 查看状态
docker-compose ps
# 查看日志
docker-compose logs -f
# 查看特定服务日志
docker-compose logs -f python-service
docker-compose logs -f backend
# 重启服务
docker-compose restart
# 停止服务
docker-compose down
# 完全清理(包括数据)
docker-compose down -v
```
## 🔧 性能优化建议
### CPU模式优化
1. **使用最小模型**
```bash
# 使用yolov8n.pt而不是yolov8l.pt或yolov8x.pt
```
2. **调整输入尺寸**
```python
# 在yolov8_model.py中
self.img_size = 320 # 从640改为320
```
3. **提高置信度阈值**
```python
# 减少检测框数量
self.conf_threshold = 0.4 # 从0.25提高到0.4
```
## 📞 获取帮助
1. **查看日志**
```bash
docker-compose logs [服务名]
```
2. **检查配置**
- `.env` - 环境变量
- `docker-compose.yml` - 服务配置
- `application.yml` - MinIO配置
3. **阅读文档**
- `DEPLOYMENT-NOTES.md` - 详细配置说明
- `DOCKER-QUICK-START.md` - 快速参考
## ✨ 部署完成
所有配置已完成,您可以:
1. 运行 `deploy.bat` 或 `./deploy.sh` 启动系统
2. 访问 http://localhost:10080 使用系统
3. 查看日志确认服务正常运行
---
**配置日期**: 2025-09-30
**配置特点**:
- ✅ YOLOv8 (CPU模式)
- ✅ 外部MinIO (49.232.154.205:10900)
- ✅ 前端端口 10080
- ✅ 无GPU依赖
🎉 **祝部署顺利!**

View File

@@ -0,0 +1,525 @@
# ✅ 巡检任务功能更新总结
## 🎯 功能实现
根据需求已实现完整的巡检任务记录和AI识别流程。
## 📋 新增功能
### 1. ✅ 自动创建巡检记录
**何时创建**:巡检任务启动时自动创建
**InspectionTaskRecord字段**
- `recordId`: 自动生成的记录ID
- `taskId`: 关联的巡检任务ID
- `executeTime`: 执行开始时间
- `duration`: 执行时长(秒)
- `accessory`: 视频URL原始;处理后)
- `result`: AI识别结果摘要
- `status`: 0=成功, 1=失败, 2=部分成功
### 2. ✅ 自动保存视频
**视频保存流程**
1. 从RTSP流录制视频按task.duration时长
2. 上传原始视频到MinIO
3. 保存URL到`record.accessory`
4. 继续分析处理
### 3. ✅ 调用Python服务识别
**识别流程**
1. 创建`HttpYoloDetector`连接Python服务
2. 逐帧提取并调用YOLOv8检测
3. 每10帧检测一次可调整
4. 绘制检测框到视频上
5. 生成带标注的处理后视频
### 4. ✅ 更新识别结果
**result字段内容**
```
共检测到 5 个问题,详情:垃圾(3) 烟雾(2)
```
### 5. ✅ 创建不重复告警
**去重机制**
- 使用位置+类别生成唯一键
- 相同对象只创建一次告警
- 允许10像素的位置波动
- 60秒未检测到自动清除
**AlarmRecord包含**
- 告警类型、内容、置信度
- 关联的任务ID和设备ID
- 告警图片MinIO存储
- 视频帧位置
- 未处理状态
## 🔄 完整执行流程
```
1. 用户启动巡检任务
2. InspectionTaskServiceImpl.executeInspectionTask()
├── 创建InspectionTaskRecord (status=1执行中)
├── 更新InspectionTask (status=1执行中)
└── 调用performVideoAnalysisWithRecord()
3. performVideoAnalysisWithRecord()
├── 录制RTSP视频流30秒
├── 上传原始视频到MinIO
├── 更新record.accessory = "原始视频URL"
└── 调用analyzeVideoAndUpdateRecord()
4. VideoAnalysisService.analyzeVideoWithRecord()
├── 逐帧分析视频
├── 每10帧调用Python API检测
├── 发现新对象 → createAlarmRecordForRecord()
│ ├── 提取检测区域图片
│ ├── 上传告警图片到MinIO
│ └── 创建AlarmRecord去重
├── 绘制检测框
├── 保存处理后视频
├── 上传处理后视频到MinIO
├── 更新record.accessory += ";处理后视频URL"
└── 更新record.result = "检测结果摘要"
5. 完成
├── 更新record.status = 0 (成功)
├── 更新record.duration = 实际执行时长
└── 更新task.status = 2 (已完成)
```
## 📦 新增/修改的文件
### 新增文件
1. **IInspectionTaskRecordService.java**
- 巡检记录服务接口
2. **IInspectionTaskRecordServiceImpl.java**
- 巡检记录服务实现
3. **INSPECTION-WORKFLOW.md**
- 详细工作流程文档
4. **INSPECTION-FEATURE-SUMMARY.md**
- 本文档
### 修改文件
1. **InspectionTaskServiceImpl.java**
- 添加依赖注入InspectionTaskRecordMapper, MinioService, VMinioObjectService
- 修改`executeInspectionTask()` - 创建记录
- 新增`performVideoAnalysisWithRecord()` - 录制视频并分析
- 新增`analyzeVideoAndUpdateRecord()` - 调用分析服务
- 新增`updateRecordFailed()` - 更新失败状态
2. **VideoAnalysisService.java**
- 添加InspectionTaskRecordMapper依赖
- 更新Python服务URL为容器名
- 更新模型名称为yolov8_detector
- 新增`analyzeVideoWithRecord()` - 带记录的视频分析
- 新增`processVideoWithRecord()` - 处理视频并记录结果
- 新增`createAlarmRecordForRecord()` - 创建去重告警
- 新增`uploadProcessedVideoForRecord()` - 上传处理后视频
## 🎯 数据流转
### InspectionTaskRecord字段变化
```
创建时:
recordId: [auto]
taskId: 1001
executeTime: 2025-09-30 14:30:00
status: 1 (执行中)
accessory: null
result: null
duration: null
录制视频后:
accessory: "http://.../inspection_1001_xxx.mp4"
分析完成后:
accessory: "http://.../inspection_1001_xxx.mp4;http://.../processed_xxx.mp4"
result: "共检测到 3 个问题,详情:垃圾(2) 烟雾(1)"
duration: 32
status: 0 (成功)
```
### AlarmRecord创建条件
仅当满足以下条件时创建告警:
1. ✅ 检测到新对象(不在缓存中)
2. ✅ 位置和类别不重复
3. ✅ 置信度超过阈值Python服务的conf参数
## 💾 数据库查询示例
### 查看任务执行历史
```sql
-- 查看任务的所有执行记录
SELECT
r.record_id,
r.execute_time,
r.duration,
r.status,
r.result,
(SELECT COUNT(*) FROM v_alarm_record a WHERE a.task_id = r.task_id
AND a.create_time >= r.execute_time) as alarm_count
FROM v_inspection_task_record r
WHERE r.task_id = 1001
ORDER BY r.execute_time DESC;
```
### 查看记录详情
```sql
-- 查看单条记录的完整信息
SELECT
r.*,
t.device_id,
d.ip as device_ip
FROM v_inspection_task_record r
JOIN v_inspection_task t ON r.task_id = t.task_id
JOIN v_device d ON t.device_id = d.device_id
WHERE r.record_id = 2001;
```
### 查看记录的所有告警
```sql
-- 查看某次执行产生的告警
SELECT
a.alarm_id,
a.alarm_type,
a.alarm_content,
a.confidence,
a.frame_position,
m.object_url as alarm_image_url
FROM v_alarm_record a
LEFT JOIN v_minio_object m ON a.image_oss_id = m.object_id
WHERE a.task_id = 1001
AND a.create_time >= (SELECT execute_time FROM v_inspection_task_record WHERE record_id = 2001)
AND a.create_time <= DATE_ADD((SELECT execute_time FROM v_inspection_task_record WHERE record_id = 2001),
INTERVAL (SELECT duration FROM v_inspection_task_record WHERE record_id = 2001) SECOND)
ORDER BY a.create_time;
```
## 🔧 配置参数
### 关键配置位置
**VideoAnalysisService.java**:
```java
// Python服务地址使用容器名
private static final String PYTHON_API_URL = "http://rtsp-python-service:8000/api/detect/file";
// 模型名称
private static final String MODEL_NAME = "yolov8_detector";
// 检测频率每N帧
if (frameCount % 10 == 0) { ... }
// 去重时间窗口60秒
(currentId - entry.getValue()) > grabber.getFrameRate() * 60
```
### 可调整参数
| 参数 | 位置 | 默认值 | 说明 |
|------|------|--------|------|
| 检测频率 | processVideoWithRecord | 10帧 | 降低可提升性能 |
| 去重容差 | generateDetectionKey | 10像素 | 提高容差减少告警 |
| 去重时间窗口 | processVideoWithRecord | 60秒 | 缩短窗口增加告警 |
| 模型名称 | MODEL_NAME | yolov8_detector | 与Python配置对应 |
## 🚀 API接口
### Python服务接口
**请求**
```http
POST http://rtsp-python-service:8000/api/detect/file
Content-Type: multipart/form-data
model_name: yolov8_detector
file: [图像文件]
```
**响应**
```json
{
"model_name": "yolov8_detector",
"detections": [
{
"label": "[yolov8_detector] 垃圾",
"confidence": 0.95,
"x": 100,
"y": 200,
"width": 150,
"height": 180,
"color": 65280
}
],
"inference_time": 45.6
}
```
## 🎬 使用示例
### 示例1: 创建并执行巡检任务
```java
// 1. 创建任务
InspectionTask task = new InspectionTask();
task.setDeviceId(5001L);
task.setDuration(30); // 30秒
task.setStatus(0); // 待执行
inspectionTaskService.insertInspectionTask(task);
// 2. 启动任务(异步)
inspectionTaskService.executeInspectionTask(task.getTaskId());
// 3. 查询执行记录
List<InspectionTaskRecord> records =
inspectionTaskRecordService.selectInspectionTaskRecordList(
new InspectionTaskRecord().setTaskId(task.getTaskId())
);
```
### 示例2: 查询告警
```java
// 查询某任务的所有告警
AlarmRecord query = new AlarmRecord();
query.setTaskId(1001L);
query.setStatus(0); // 未处理
List<AlarmRecord> alarms = alarmRecordService.selectAlarmRecordList(query);
```
## ⚠️ 注意事项
### 1. 执行时间
- 录制视频需要时间与duration设置一致
- AI分析需要额外时间取决于视频长度和CPU性能
- 总执行时间 ≈ duration + 分析时间
### 2. 存储空间
每次执行会产生:
- 原始视频(~10-50MB30秒
- 处理后视频(~10-50MB
- 告警图片(每个~100-500KB
建议定期清理历史数据。
### 3. Python服务调用
- 使用HTTP调用Python服务
- 每帧调用可能较慢已优化为每10帧
- CPU模式下建议降低检测频率
### 4. MinIO存储
- 确保bucket已创建
- `inspection-videos`(巡检视频)
- `alarm-images`(告警图片)
- 确保外部MinIO服务可访问
## 🔍 测试清单
### 部署后测试
- [ ] Python服务可访问
```bash
curl http://rtsp-python-service:8000/health
curl http://rtsp-python-service:8000/api/models
```
- [ ] MinIO bucket已创建
```bash
# 登录MinIO管理界面创建bucket
# 或使用mc命令创建
```
- [ ] 创建测试任务
```sql
INSERT INTO v_inspection_task (device_id, duration, status) VALUES (1, 30, 0);
```
- [ ] 执行任务并查看记录
```sql
SELECT * FROM v_inspection_task_record ORDER BY create_time DESC LIMIT 1;
```
- [ ] 查看生成的告警
```sql
SELECT * FROM v_alarm_record ORDER BY create_time DESC LIMIT 10;
```
- [ ] 验证视频URL可访问
```
访问record.accessory中的URL
```
## 📊 预期结果
### 成功执行的记录
```json
{
"recordId": 2001,
"taskId": 1001,
"executeTime": "2025-09-30 14:30:00",
"duration": 32,
"accessory": "http://49.232.154.205:10900/inspection-videos/inspection_1001_1696056600000.mp4;http://49.232.154.205:10900/inspection-videos/processed_1696056632000.mp4",
"result": "共检测到 5 个问题,详情:垃圾(3) 烟雾(2)",
"status": 0
}
```
### 生成的告警
```json
{
"alarmId": 3001,
"deviceId": 5001,
"taskId": 1001,
"alarmType": "detection",
"alarmContent": "垃圾 - 置信度: 0.95",
"imageOssId": 4001,
"framePosition": 150,
"confidence": 0.95,
"status": 0
}
```
## 🛠️ 维护和优化
### 1. 清理历史数据
```sql
-- 删除30天前的记录
DELETE FROM v_inspection_task_record
WHERE execute_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- 删除已处理的告警
DELETE FROM v_alarm_record
WHERE status = 1 AND create_time < DATE_SUB(NOW(), INTERVAL 7 DAY);
```
### 2. 性能监控
```sql
-- 查看最近的执行统计
SELECT
DATE(execute_time) as date,
COUNT(*) as total_executions,
SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) as success_count,
AVG(duration) as avg_duration
FROM v_inspection_task_record
WHERE execute_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(execute_time)
ORDER BY date DESC;
```
### 3. 告警统计
```sql
-- 查看最近的告警统计
SELECT
DATE(create_time) as date,
alarm_type,
COUNT(*) as count,
AVG(confidence) as avg_confidence
FROM v_alarm_record
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(create_time), alarm_type
ORDER BY date DESC, count DESC;
```
## 📞 故障排查
### 问题1: Record未创建
**症状**:执行任务但`v_inspection_task_record`表无数据
**检查**
```sql
SELECT * FROM v_inspection_task WHERE task_id = ?;
```
**解决**
- 确认任务状态为0待执行
- 查看后端日志
- 检查Mapper XML配置
### 问题2: Accessory为空
**症状**Record创建了但accessory字段为空
**检查**
```bash
# 查看MinIO上传日志
docker-compose logs backend | grep "MinIO"
# 测试MinIO连接
curl http://49.232.154.205:10900/minio/health/live
```
**解决**
- 确认MinIO服务可访问
- 检查application.yml中的MinIO配置
- 确认bucket已创建
### 问题3: Result为空
**症状**视频已保存但result字段为空
**检查**
```bash
# 查看Python服务日志
docker-compose logs python-service
# 测试Python服务
curl http://rtsp-python-service:8000/api/models
```
**解决**
- 确认Python服务运行正常
- 确认best.pt模型文件存在
- 检查容器间网络通信
### 问题4: 告警重复
**症状**:相同对象产生多个告警
**调整去重参数**
```java
// 在generateDetectionKey中增大容差
int x = rect.x() / 20 * 20; // 从10改为20
int y = rect.y() / 20 * 20;
```
## 📖 相关文档
- `INSPECTION-WORKFLOW.md` - 详细工作流程
- `YOLOV8-SETUP.md` - YOLOv8模型配置
- `DEPLOYMENT-NOTES.md` - 部署配置说明
- `DOCKER-QUICK-START.md` - Docker快速开始
---
**功能状态**: ✅ 已实现
**测试状态**: 待测试
**文档版本**: 1.0
**最后更新**: 2025-09-30
<EFBFBD><EFBFBD> **巡检任务记录功能已完整实现!**

544
INSPECTION-WORKFLOW.md Normal file
View File

@@ -0,0 +1,544 @@
# 巡检任务工作流程说明
## 📋 功能概述
本文档说明巡检任务的完整执行流程包括视频录制、保存、AI识别和告警创建。
## 🔄 完整工作流程
### 1. 任务启动
当巡检任务启动时:
```
InspectionTaskServiceImpl.executeInspectionTask(taskId)
├── 创建 InspectionTaskRecord记录ID
├── 更新任务状态为"执行中"
└── 调用 performVideoAnalysisWithRecord()
```
### 2. 视频录制和保存
```
performVideoAnalysisWithRecord()
├── 从RTSP流抓取视频
├── 录制指定时长的视频
├── 保存为临时文件
├── 上传视频到MinIO
├── 更新InspectionTaskRecord.accessory视频URL
└── 调用Python服务进行分析
```
### 3. AI识别处理
```
VideoAnalysisService.analyzeVideoWithRecord()
├── 创建HttpYoloDetector连接Python服务
├── 逐帧分析视频
├── 每10帧调用一次Python API检测
├── 绘制检测框
├── 去重检测结果(避免重复告警)
├── 创建告警记录
├── 上传处理后的视频
├── 生成检测结果摘要
└── 更新InspectionTaskRecord.result识别结果
```
### 4. 告警创建(去重)
```
createAlarmRecordForRecord()
├── 提取检测区域图像
├── 上传告警图片到MinIO
├── 创建AlarmRecord
│ ├── 设备ID
│ ├── 告警类型
│ ├── 告警内容(检测类别+置信度)
│ ├── 关联的任务ID
│ ├── 图片URL
│ └── 帧位置
└── 保存到数据库(仅新检测的对象)
```
## 📊 数据表关系
```
InspectionTask (巡检任务)
↓ 1:N
InspectionTaskRecord (巡检记录)
├── accessory: 原始视频URL + 处理后视频URL
├── result: AI识别结果摘要
├── duration: 执行时长
└── status: 0=成功, 1=失败, 2=部分成功
InspectionTaskRecord → AlarmRecord (1:N)
├── 同一个record可以有多个告警
└── 告警自动去重(相同位置的相同对象只记录一次)
```
## 🎯 关键字段说明
### InspectionTaskRecord
| 字段 | 说明 | 示例 |
|------|------|------|
| recordId | 记录ID | 自增主键 |
| taskId | 关联的任务ID | 1001 |
| executeTime | 执行时间 | 2025-09-30 14:30:00 |
| duration | 执行时长(秒) | 30 |
| accessory | 附件URL | video1.mp4;video2.mp4 |
| result | 识别结果 | 共检测到3个问题详情垃圾(2) 烟雾(1) |
| status | 执行状态 | 0=成功, 1=失败, 2=部分成功 |
### AlarmRecord
| 字段 | 说明 | 示例 |
|------|------|------|
| alarmId | 告警ID | 自增主键 |
| deviceId | 设备ID | 1001 |
| taskId | 任务ID | 1001 |
| alarmType | 告警类型 | detection |
| alarmContent | 告警内容 | 垃圾 - 置信度: 0.95 |
| imageOssId | 告警图片ID | MinIO对象ID |
| framePosition | 视频帧位置 | 150 |
| confidence | 置信度 | 0.95 |
| status | 处理状态 | 0=未处理, 1=已处理 |
## 🔧 关键实现细节
### 1. 去重机制
使用`generateDetectionKey`生成唯一键:
```java
private String generateDetectionKey(Detection detection) {
Rect rect = detection.getRect();
// 取10的倍数允许小范围波动
int x = rect.x() / 10 * 10;
int y = rect.y() / 10 * 10;
int w = rect.width() / 10 * 10;
int h = rect.height() / 10 * 10;
return String.format("%s_%d_%d_%d_%d", detection.getLabel(), x, y, w, h);
}
```
**原理**
- 相同类别 + 相似位置 → 认为是同一个对象
- 允许10像素的波动
- 超过60秒未检测到自动清除
### 2. Python服务调用
使用容器名调用:
```java
private static final String PYTHON_API_URL = "http://rtsp-python-service:8000/api/detect/file";
private static final String MODEL_NAME = "yolov8_detector";
```
### 3. 视频处理流程
```
RTSP流 → FFmpegFrameGrabber → 录制 → 临时文件
→ 上传MinIO → 保存URL到record.accessory
→ 逐帧分析 → 调用Python API → 绘制检测框
→ 保存处理后视频 → 追加URL到record.accessory
→ 更新record.result
```
### 4. 附件字段格式
`accessory`字段使用分号分隔多个URL
```
原始视频URL;处理后视频URL
```
示例:
```
http://minio.com/inspection-videos/inspection_1001_1234567890.mp4;http://minio.com/inspection-videos/processed_1234567891.mp4
```
## 🚀 使用方法
### 1. 创建巡检任务
```java
InspectionTask task = new InspectionTask();
task.setDeviceId(deviceId);
task.setDuration(30); // 录制30秒
task.setStatus(0); // 待执行
inspectionTaskService.insertInspectionTask(task);
```
### 2. 启动任务
```java
// 异步执行
inspectionTaskService.executeInspectionTask(taskId);
```
### 3. 查看执行记录
```sql
-- 查询某任务的所有执行记录
SELECT * FROM v_inspection_task_record WHERE task_id = 1001 ORDER BY execute_time DESC;
-- 查询成功的记录
SELECT * FROM v_inspection_task_record WHERE status = 0;
-- 查询某记录的所有告警
SELECT * FROM v_alarm_record WHERE task_id = 1001;
```
### 4. 查看告警
```sql
-- 查询某任务的所有告警
SELECT * FROM v_alarm_record WHERE task_id = 1001 ORDER BY create_time DESC;
-- 查询未处理的告警
SELECT * FROM v_alarm_record WHERE status = 0;
```
## 📝 执行示例
### 执行流程
1. **任务创建**
```
Task ID: 1001
Device ID: 5001
Duration: 30秒
```
2. **记录创建**
```
Record ID: 2001
Task ID: 1001
Execute Time: 2025-09-30 14:30:00
Status: 1 (执行中)
```
3. **视频录制**
```
录制30秒视频
保存到MinIO: inspection_1001_1234567890.mp4
更新Record.accessory: http://minio.com/.../inspection_1001_1234567890.mp4
```
4. **AI识别**
```
调用Python服务
检测到: 垃圾(2个), 烟雾(1个)
```
5. **告警创建**(去重)
```
Alarm 1: 垃圾 - 位置(100,200) - 置信度0.95
Alarm 2: 垃圾 - 位置(300,400) - 置信度0.87
Alarm 3: 烟雾 - 位置(500,100) - 置信度0.92
```
6. **处理后视频**
```
带检测框的视频上传
保存到MinIO: processed_1234567891.mp4
更新Record.accessory: 原始URL;处理后URL
```
7. **更新记录**
```
Record.result: "共检测到 3 个问题,详情:垃圾(2) 烟雾(1)"
Record.status: 0 (成功)
Record.duration: 32秒
```
## ⚙️ 配置说明
### Python服务配置
在Docker环境中Python服务地址为
```
http://rtsp-python-service:8000
```
### 模型配置
确保Python服务使用正确的模型名称
```json
{
"name": "yolov8_detector",
"path": "models/yolov8_model.py",
"size": [640, 640]
}
```
### 检测参数
在`VideoAnalysisService`中可调整:
```java
// 检测频率每N帧检测一次
if (frameCount % 10 == 0) { ... }
// 去重时间窗口(秒)
detectedGarbageCache.entrySet().removeIf(entry ->
(currentId - entry.getValue()) > grabber.getFrameRate() * 60);
```
## 🐛 故障排查
### 问题1: 视频未保存
**检查**
```sql
-- 查看record的accessory字段
SELECT record_id, accessory FROM v_inspection_task_record WHERE task_id = ?;
-- 查看MinIO对象
SELECT * FROM v_minio_object WHERE bucket_name = 'inspection-videos' ORDER BY create_time DESC;
```
**解决**
- 检查MinIO服务是否可用
- 检查网络连接
- 查看后端日志
### 问题2: Python识别未执行
**检查**
```bash
# 查看Python服务日志
docker-compose logs python-service
# 测试Python服务
curl http://rtsp-python-service:8000/health
curl http://rtsp-python-service:8000/api/models
```
**解决**
- 确认Python服务运行正常
- 确认模型文件存在
- 检查网络连通性
### 问题3: 告警未创建
**检查**
```sql
-- 查看告警记录
SELECT * FROM v_alarm_record WHERE task_id = ? ORDER BY create_time DESC;
-- 查看检测结果
SELECT result FROM v_inspection_task_record WHERE record_id = ?;
```
**解决**
- 检查检测置信度阈值
- 查看视频内容是否有检测对象
- 检查Python服务返回结果
### 问题4: 重复告警
**检查**
- 去重机制是否正常工作
- `generateDetectionKey`逻辑是否合理
**调整**
```java
// 调整去重的位置容差
int x = rect.x() / 20 * 20; // 从10改为20更宽松的去重
```
## 📊 性能优化
### 1. 检测频率
```java
// 降低检测频率以提升性能CPU模式
if (frameCount % 30 == 0) { // 从10改为30
// 每30帧检测一次
}
```
### 2. 视频质量
```java
// 降低视频比特率节省存储
recorder.setVideoBitrate(500000); // 降低比特率
```
### 3. 去重时间窗口
```java
// 缩短去重时间窗口
(currentId - entry.getValue()) > grabber.getFrameRate() * 30 // 从60秒改为30秒
```
## 🔍 调试方法
### 查看执行日志
```bash
# 查看后端日志
docker-compose logs -f backend | grep "inspection"
# 查看Python服务日志
docker-compose logs -f python-service
# 查看特定记录的处理过程
docker-compose logs backend | grep "recordId=2001"
```
### 数据库查询
```sql
-- 查看最新的执行记录
SELECT
r.record_id,
r.task_id,
r.execute_time,
r.duration,
r.status,
r.result,
LENGTH(r.accessory) as accessory_length
FROM v_inspection_task_record r
ORDER BY r.create_time DESC
LIMIT 10;
-- 查看记录对应的告警
SELECT
a.alarm_id,
a.alarm_content,
a.confidence,
a.frame_position,
a.create_time
FROM v_alarm_record a
WHERE a.task_id = ?
ORDER BY a.create_time DESC;
-- 统计告警数量
SELECT
r.record_id,
r.execute_time,
COUNT(a.alarm_id) as alarm_count
FROM v_inspection_task_record r
LEFT JOIN v_alarm_record a ON r.task_id = a.task_id
AND a.create_time >= r.execute_time
AND a.create_time <= DATE_ADD(r.execute_time, INTERVAL r.duration SECOND)
GROUP BY r.record_id
ORDER BY r.create_time DESC;
```
## 💡 扩展建议
### 1. 添加检测类型过滤
在`createAlarmRecordForRecord`中:
```java
// 只对特定类型创建告警
List<String> alarmTypes = Arrays.asList("垃圾", "烟雾", "火焰");
if (!alarmTypes.contains(detection.getLabel())) {
return; // 忽略其他类型
}
```
### 2. 添加置信度阈值
```java
// 只对高置信度的检测创建告警
if (detection.getConfidence() < 0.7) {
return; // 忽略低置信度检测
}
```
### 3. 添加区域过滤
```java
// 只对特定区域的检测创建告警
Rect rect = detection.getRect();
if (!isInMonitorArea(rect, task)) {
return; // 忽略监控区域外的检测
}
```
### 4. 添加告警级别
```java
// 根据检测类型设置告警级别
String alarmLevel = "medium";
if (detection.getLabel().contains("火焰")) {
alarmLevel = "high";
} else if (detection.getLabel().contains("垃圾")) {
alarmLevel = "low";
}
alarmRecord.setAlarmLevel(alarmLevel);
```
## 🔒 安全考虑
### 1. 异常处理
所有方法都包含完整的异常处理:
- 视频录制失败 → 更新record状态为失败
- Python服务调用失败 → 记录错误但不影响整体流程
- MinIO上传失败 → 记录错误并回滚
### 2. 资源清理
使用try-finally确保资源释放
- FFmpegFrameGrabber自动关闭
- FFmpegFrameRecorder自动关闭
- 临时文件自动删除
### 3. 并发控制
使用`@Async`异步执行,避免阻塞:
- 任务执行不阻塞API响应
- 多个任务可并发执行
- 通过runningTasks避免重复执行
## 📈 监控指标
### 建议监控的指标
1. **执行成功率**
```sql
SELECT
COUNT(CASE WHEN status = 0 THEN 1 END) * 100.0 / COUNT(*) as success_rate
FROM v_inspection_task_record
WHERE execute_time >= DATE_SUB(NOW(), INTERVAL 1 DAY);
```
2. **平均执行时长**
```sql
SELECT AVG(duration) as avg_duration
FROM v_inspection_task_record
WHERE status = 0 AND execute_time >= DATE_SUB(NOW(), INTERVAL 1 DAY);
```
3. **告警统计**
```sql
SELECT
alarm_type,
COUNT(*) as count
FROM v_alarm_record
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 1 DAY)
GROUP BY alarm_type;
```
## 📞 技术支持
如有问题,请查看:
1. 后端日志:`docker-compose logs backend`
2. Python服务日志`docker-compose logs python-service`
3. 数据库记录:查询`v_inspection_task_record`和`v_alarm_record`表
4. MinIO对象查询`v_minio_object`表
---
**文档版本**: 1.0
**最后更新**: 2025-09-30
**适用版本**: YOLOv8, Docker Compose部署

304
README-DOCKER.md Normal file
View File

@@ -0,0 +1,304 @@
# RTSP视频分析系统 Docker部署方案
## 📋 目录结构
```
rtsp-video-analysis-system/
├── .env # 环境变量配置文件
├── docker-compose.yml # Docker Compose编排文件
├── ruoyi-admin/
│ └── Dockerfile # Java后端Dockerfile
├── rtsp-vue/
│ ├── Dockerfile # 前端Dockerfile
│ └── nginx.conf # Nginx配置文件
├── python-inference-service/
│ └── Dockerfile # Python推理服务Dockerfile
└── sql/ # 数据库初始化脚本
```
## 🚀 快速开始
### 1. 前置要求
- Docker 20.10+
- Docker Compose 2.0+
- 可选NVIDIA Docker Runtime如需GPU支持
### 2. 准备工作
#### 2.1 配置环境变量
编辑 `.env` 文件,根据需要调整配置:
```bash
# MySQL数据库配置
MYSQL_ROOT_PASSWORD=ruoyi123
MYSQL_DATABASE=ry-vue
MYSQL_USER=ruoyi
MYSQL_PASSWORD=ruoyi123
# MinIO对象存储配置
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=minio123
# 其他配置...
```
#### 2.2 准备数据库初始化脚本
将数据库SQL脚本放到 `sql/` 目录:
```bash
# 示例
cp sql/fad_watch.sql sql/
cp sql/ry_face.sql sql/
```
### 3. 启动服务
#### 3.1 构建并启动所有服务
```bash
# 启动所有服务
docker-compose up -d
# 查看服务状态
docker-compose ps
# 查看日志
docker-compose logs -f
```
#### 3.2 查看特定服务日志
```bash
# 查看后端日志
docker-compose logs -f backend
# 查看前端日志
docker-compose logs -f frontend
# 查看Python服务日志
docker-compose logs -f python-service
```
### 4. 访问服务
- **前端界面**: http://localhost:10080 或配置的FRONTEND_PORT
- **后端API**: 通过前端的 `/prod-api/` 路径访问
- **Python推理API**: 通过前端的 `/python-api/` 路径访问
- **MinIO控制台**: 仅容器内部访问如需外部访问需修改docker-compose.yml
> 注意:除前端外,其他服务端口均不对外暴露,仅容器间通信使用。
## 🔧 服务说明
### 服务列表
| 服务名称 | 容器名称 | 端口(内部) | 端口(外部) | 说明 |
|---------|---------|------------|------------|------|
| mysql | rtsp-mysql | 3306 | - | MySQL数据库 |
| redis | rtsp-redis | 6379 | - | Redis缓存 |
| python-service | rtsp-python-service | 8000 | - | Python推理服务CPU |
| backend | rtsp-backend | 8080 | - | Java后端服务 |
| frontend | rtsp-frontend | 80 | 10080 | Vue前端服务 |
**注意**
- MinIO使用外部已部署的服务http://49.232.154.205:10900
- Python推理服务使用CPU模式不需要GPU
### 服务依赖关系
```
frontend
└── backend
├── mysql
├── redis
├── python-service
└── minio (外部服务)
```
## ⚙️ 高级配置
### 1. 修改前端暴露端口
编辑 `.env` 文件:
```bash
FRONTEND_PORT=8080 # 修改为你想要的端口
```
### 2. 如需外部访问其他服务
编辑 `docker-compose.yml`,在对应服务下添加 `ports` 配置:
```yaml
mysql:
# ... 其他配置
ports:
- "3306:3306" # 添加端口映射
```
### 3. 调整Java后端内存
编辑 `ruoyi-admin/Dockerfile`
```dockerfile
ENV JAVA_OPTS="-Xms1g -Xmx2g -Djava.security.egd=file:/dev/./urandom"
```
或在 `docker-compose.yml` 中添加环境变量:
```yaml
backend:
environment:
JAVA_OPTS: "-Xms1g -Xmx2g"
```
### 4. MinIO配置
本系统使用**外部已部署的MinIO服务**,配置在 `ruoyi-admin/src/main/resources/application.yml`
```yaml
minio:
enabled: true
endpoint: http://49.232.154.205:10900
access-key: 4EsLD9g9OM09DT0HaBKj
secret-key: 05SFC5fleqTnaLRYBrxHiphMFYbGX5nYicj0WCHA
bucket: rtsp
```
如需修改MinIO配置请编辑上述文件。
## 🔍 故障排查
### 1. 查看服务健康状态
```bash
docker-compose ps
```
健康的服务会显示 `healthy` 状态。
### 2. 查看容器日志
```bash
# 查看所有服务日志
docker-compose logs
# 查看特定服务的最新日志
docker-compose logs --tail=100 -f backend
```
### 3. 进入容器调试
```bash
# 进入后端容器
docker exec -it rtsp-backend sh
# 进入前端容器
docker exec -it rtsp-frontend sh
# 进入Python服务容器
docker exec -it rtsp-python-service bash
```
### 4. 重启服务
```bash
# 重启单个服务
docker-compose restart backend
# 重启所有服务
docker-compose restart
```
### 5. 完全重建服务
```bash
# 停止并删除所有容器
docker-compose down
# 重新构建并启动
docker-compose up -d --build
```
### 6. 清理数据重新开始
```bash
# 停止并删除所有容器和数据卷
docker-compose down -v
# 重新启动
docker-compose up -d
```
## 📦 数据持久化
以下数据会持久化存储:
- `mysql-data`: MySQL数据库数据
- `redis-data`: Redis持久化数据
- `backend-logs`: 后端服务日志
- `backend-upload`: 后端上传文件
**注意**MinIO数据存储在外部服务器不在本地
## 🔒 安全建议
1. **修改默认密码**:部署到生产环境前,请修改 `.env` 中的所有默认密码
2. **限制网络访问**:使用防火墙规则限制对前端端口的访问
3. **HTTPS配置**在生产环境中建议配置HTTPS可使用Nginx反向代理+Let's Encrypt
4. **定期备份**:定期备份数据卷内容
## 🔄 更新部署
```bash
# 1. 拉取最新代码
git pull
# 2. 重新构建镜像
docker-compose build
# 3. 重启服务
docker-compose up -d
```
## 📝 常见问题
### Q1: 后端启动失败,提示连接不到数据库
**A**: 检查MySQL是否已完全启动。使用 `docker-compose logs mysql` 查看MySQL日志。
### Q2: 前端无法连接后端API
**A**: 确认nginx配置中的后端地址是否正确应该使用容器名 `rtsp-backend` 而不是 `localhost`
### Q3: Python推理服务启动慢
**A**:
1. 首次启动需要下载Ultralytics YOLOv8依赖这可能需要一些时间
2. 确保`python-inference-service/models/best.pt`文件存在
3. 使用 `docker-compose logs python-service` 查看进度
### Q4: 如何更换MinIO服务
**A**: 修改 `ruoyi-admin/src/main/resources/application.yml` 中的MinIO配置
```yaml
minio:
endpoint: http://your-minio-server:port
access-key: your-access-key
secret-key: your-secret-key
bucket: your-bucket
```
## 📞 支持
如有问题,请查看:
- 项目GitHub Issues
- 查看服务日志进行调试
- 检查环境变量配置是否正确
## <20><> 许可证
[根据项目实际许可证填写]

View File

@@ -1,40 +1,3 @@
<p align="center">
<img alt="logo" src="https://gdhxkj.oss-cn-guangzhou.aliyuncs.com/file/2025/01/16/蒜头王八_20250116174410A005.png" style="width: 80px;">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">rtsp视频分析 v1.0.0</h1>
<h4 align="center">基于SpringBoot+Vue前后端分离的rtsp视频分析系统</h4>
## 平台简介
这是一个基于ruoyi-vue框架改进而来的RTSP视频分析平台继承ruoyi框架的基本功能。项目集成了虹软SDK实现了人脸识别、活体检测、3D角度分析、年龄及性别识别等功能同时利用JavaCV进行高效的视频处理将rtsp视频转成http-flv和ws-flv。后端采用SpringBoot框架前端则运用了Vue3框架确保系统的稳定与用户体验的流畅。
* 前端采用Vue3、Element Plus、XgpLayer。
* 后端采用Spring Boot、Spring Security、Redis 、 javaCv & Jwt。
加微信联系: chenbai0511
## 内置功能
1. 若依全功能。
2. rtsp视频转http-flvws-flv在线播放。
3. rtsp视频人脸识别活体检测3D角度分析年龄及性别识别。
## 演示图
<table>
<tr>
<td><img src="https://gdhxkj.oss-cn-guangzhou.aliyuncs.com/file/2025/01/16/g1_20250116174656A007.png"/></td>
</tr>
<tr>
<td><img src="https://gdhxkj.oss-cn-guangzhou.aliyuncs.com/file/2025/01/16/g2_20250116174755A009.png"/></td>
</tr>
<tr>
<td><img src="https://gdhxkj.oss-cn-guangzhou.aliyuncs.com/file/2025/01/16/g3_20250116174816A011.png"/></td>
</tr>
</table>
## 联系我吧
<img src="https://gdhxkj.oss-cn-guangzhou.aliyuncs.com/file/2025/01/16/17c8bc62cbd5b58c27775e0c2ff83bd_20250116174058A003.jpg" style="width: 500px; display: block; margin: auto;"/>
qwq
java 17
node16 .20
opencv推测4.10

135
START-HERE.md Normal file
View File

@@ -0,0 +1,135 @@
# 🚀 快速开始 - 从这里开始!
## 📌 您需要的一切
### 第一步:部署系统
```bash
# 1. 准备YOLOv8模型文件
# 将训练好的 best.pt 放到:
python-inference-service/models/best.pt
# 2. 运行部署脚本
deploy.bat # Windows用户
# 3. 访问系统
# 打开浏览器http://localhost:10080
```
**就这么简单!** 🎊
---
## 📖 详细文档
### 🐳 Docker部署
| 文档 | 用途 | 优先级 |
|------|------|--------|
| `FINAL-SUMMARY.md` | Docker配置总结 | ⭐⭐⭐ |
| `DOCKER-QUICK-START.md` | 常用命令 | ⭐⭐⭐ |
| `README-DOCKER.md` | 完整部署文档 | ⭐⭐ |
| `DEPLOYMENT-NOTES.md` | 配置细节 | ⭐ |
### 🤖 YOLOv8模型
| 文档 | 用途 | 优先级 |
|------|------|--------|
| `YOLOV8-SETUP.md` | 模型配置指南 | ⭐⭐⭐ |
| `python-inference-service/README.md` | Python服务文档 | ⭐⭐ |
### 📹 巡检任务
| 文档 | 用途 | 优先级 |
|------|------|--------|
| `INSPECTION-FEATURE-SUMMARY.md` | 功能总结 | ⭐⭐⭐ |
| `INSPECTION-WORKFLOW.md` | 详细流程 | ⭐⭐ |
### 📝 更新记录
| 文档 | 用途 |
|------|------|
| `COMPLETE-SUMMARY.md` | 完整更新总结 |
| `UPDATE-SUMMARY.md` | 变更记录 |
---
## 🎯 核心配置
### 系统架构
```
浏览器 :10080 (唯一对外端口)
前端 (Nginx)
后端 + Python服务 (内部)
MySQL + Redis + MinIO(外部)
```
### 关键特性
-**YOLOv8**: CPU模式无需GPU
-**MinIO**: 使用外部服务
-**端口**: 只暴露10080
-**记录**: 自动创建巡检记录
-**视频**: 自动保存到MinIO
-**识别**: 调用YOLOv8分析
-**告警**: 自动去重
---
## 🔧 常用命令
```bash
# 启动服务
docker-compose up -d
# 查看状态
docker-compose ps
# 查看日志
docker-compose logs -f backend
docker-compose logs -f python-service
# 重启服务
docker-compose restart
# 停止服务
docker-compose down
```
---
## 📞 遇到问题?
### 部署问题
→ 查看 `DOCKER-QUICK-START.md`
### 模型问题
→ 查看 `YOLOV8-SETUP.md`
### 功能问题
→ 查看 `INSPECTION-FEATURE-SUMMARY.md`
### 其他问题
→ 查看日志:`docker-compose logs [服务名]`
---
## ✨ 下一步
1. ✅ 部署系统 - `deploy.bat`
2. ✅ 访问前端 - http://localhost:10080
3. ✅ 创建巡检任务
4. ✅ 查看执行记录和告警
---
**快速帮助**
- 📘 完整功能:`COMPLETE-SUMMARY.md`
- 🚀 快速参考:`DOCKER-QUICK-START.md`
- 🤖 模型配置:`YOLOV8-SETUP.md`
**祝使用愉快!** 🎉

295
UPDATE-SUMMARY.md Normal file
View File

@@ -0,0 +1,295 @@
# 🎉 Docker部署方案更新总结
根据您的需求,已完成以下更新:
## ✅ 主要变更
### 1. YOLOv5 → YOLOv8 (Ultralytics)
| 变更项 | 之前 | 现在 |
|-------|------|------|
| Python包 | `yolov5>=7.0.0` | `ultralytics>=8.0.0` |
| 模型文件 | `garbage_model.py` | `yolov8_model.py` |
| 模型名称 | `garbage_detector` | `yolov8_detector` |
| API框架 | yolov5 | Ultralytics YOLO |
**优势:**
- ✨ 更高的检测精度
- ⚡ 更快的推理速度
- 🎯 更简单的API接口
- 📚 更好的官方文档支持
### 2. 前端端口调整
| 变更项 | 之前 | 现在 |
|-------|------|------|
| 对外端口 | 80 | 10080 |
| 容器内部端口 | 80 | 80 |
| 访问地址 | http://localhost | http://localhost:10080 |
**修改文件:**
- `.env` - `FRONTEND_PORT=10080`
- 所有文档中的访问地址
## 📦 新增文件
### 1. YOLOv8模型支持
- `python-inference-service/models/yolov8_model.py` - YOLOv8模型包装类
- `YOLOV8-SETUP.md` - YOLOv8配置和使用指南
- 更新 `python-inference-service/README.md` - 详细的YOLOv8文档
### 2. 配置文件更新
- 更新 `python-inference-service/requirements.txt` - ultralytics依赖
- 更新 `python-inference-service/models/models.json` - 指向yolov8_model
### 3. 文档更新
- `README-DOCKER.md` - 更新端口和YOLOv8说明
- `DOCKER-QUICK-START.md` - 更新访问地址和YOLOv8说明
- `deploy.bat` / `deploy.sh` - 更新访问地址
- `UPDATE-SUMMARY.md` - 本文档
## 🚀 使用指南
### 准备工作
1. **准备YOLOv8模型文件**
```bash
# 将训练好的模型放到指定位置
python-inference-service/models/best.pt
```
2. **创建类别文件**(可选)
```bash
# 创建 classes.txt每行一个类别名
python-inference-service/models/classes.txt
```
3. **检查环境变量**
```bash
# 查看 .env 文件,确认端口配置
FRONTEND_PORT=10080
```
### 快速部署
#### Windows
```batch
deploy.bat
```
#### Linux/Mac
```bash
chmod +x deploy.sh
./deploy.sh
```
#### 手动部署
```bash
# 1. 启动所有服务
docker-compose up -d
# 2. 查看服务状态
docker-compose ps
# 3. 查看日志
docker-compose logs -f python-service
```
### 访问系统
部署成功后访问:
- **前端界面**: http://localhost:10080
- **后端API**: http://localhost:10080/prod-api/
- **Python API**: http://localhost:10080/python-api/
- **API文档**: http://localhost:10080/prod-api/swagger-ui.html
## 📋 YOLOv8模型要求
### 模型训练
如果需要训练新模型:
```python
from ultralytics import YOLO
# 加载预训练模型
model = YOLO('yolov8n.pt') # n/s/m/l/x
# 训练
results = model.train(
data='data.yaml',
epochs=100,
imgsz=640,
batch=16
)
# 模型保存在 runs/detect/train/weights/best.pt
```
### 模型部署
```bash
# 1. 复制模型到项目
cp runs/detect/train/weights/best.pt python-inference-service/models/
# 2. 创建类别文件
cat > python-inference-service/models/classes.txt << EOF
class1
class2
class3
EOF
# 3. 重启服务
docker-compose restart python-service
```
## 🔧 配置说明
### 环境变量(.env
所有配置集中在一个文件:
```bash
# 前端端口
FRONTEND_PORT=10080
# MySQL
MYSQL_HOST=rtsp-mysql
MYSQL_PORT=3306
MYSQL_PASSWORD=ruoyi123
# Redis
REDIS_HOST=rtsp-redis
# MinIO
MINIO_HOST=rtsp-minio
MINIO_ACCESS_KEY=minio
MINIO_SECRET_KEY=minio123
# Python服务
PYTHON_SERVICE_HOST=rtsp-python-service
```
### 模型配置models.json
```json
[
{
"name": "yolov8_detector",
"path": "models/yolov8_model.py",
"size": [640, 640]
}
]
```
### 性能调优
编辑 `yolov8_model.py`
```python
# 置信度阈值
self.conf_threshold = 0.25 # 默认0.25
# 输入尺寸
self.img_size = 640 # 320/640/1280
```
## 🎯 服务架构
```
┌─────────────┐
│ Browser │
│ :10080 │ ← 唯一对外暴露
└──────┬──────┘
┌──────▼──────┐
│ Frontend │
│ Nginx │
└──────┬──────┘
┌───┴────────────────┐
│ │
┌──▼───────┐ ┌──────▼──────┐
│ Backend │ │ Python │
│ :8080 │ │ Service │
│ │ │ :8000(CPU) │
└───┬──┬───┘ └─────────────┘
│ │
┌───▼──▼───┐ ┌─────────────┐
│ MySQL │ │ MinIO(外部) │
│ Redis │ │ 49.232... │
└──────────┘ └─────────────┘
```
## ⚠️ 重要提示
### 必须操作
1. ✅ 将 `best.pt` 文件放到 `python-inference-service/models/` 目录
2. ✅ 确保模型是 YOLOv8 训练的不支持YOLOv5直接使用
3. ✅ 首次启动等待1-2分钟
### 可选操作
1. 📝 创建 `classes.txt` 类别文件
2. 🎨 调整置信度阈值
3. 🔧 修改MinIO配置如需更换服务器
### 常见问题
#### Q1: 模型加载失败
```bash
# 检查模型文件
ls -lh python-inference-service/models/best.pt
# 查看日志
docker-compose logs python-service
```
#### Q2: 端口被占用
```bash
# 修改 .env 文件
FRONTEND_PORT=8080 # 改为其他端口
# 重启服务
docker-compose down
docker-compose up -d
```
#### Q3: 如何更换MinIO服务
```bash
# 编辑 ruoyi-admin/src/main/resources/application.yml
minio:
endpoint: http://your-server:port
access-key: your-key
secret-key: your-secret
bucket: your-bucket
```
## 📖 文档导航
- 📘 **完整部署文档**: `README-DOCKER.md`
- 🚀 **快速开始**: `DOCKER-QUICK-START.md`
- 🤖 **YOLOv8配置**: `YOLOV8-SETUP.md`
- 📋 **文件清单**: `DEPLOYMENT-FILES.md`
- 🐍 **Python服务**: `python-inference-service/README.md`
## 🎓 学习资源
- [Ultralytics YOLOv8官方文档](https://docs.ultralytics.com/)
- [YOLOv8训练教程](https://docs.ultralytics.com/modes/train/)
- [Docker部署最佳实践](https://docs.docker.com/develop/dev-best-practices/)
## 💡 技术支持
遇到问题?
1. 查看日志:`docker-compose logs [服务名]`
2. 查看服务状态:`docker-compose ps`
3. 查看相关文档
4. 检查模型文件和配置
---
**更新日期**: 2025-09-30
**适用版本**: Docker Compose 3.8+, YOLOv8 8.0+
✅ 所有更新已完成,可以直接使用!

314
YOLOV8-SETUP.md Normal file
View File

@@ -0,0 +1,314 @@
# YOLOv8模型配置指南
本系统使用**YOLOv8**Ultralytics进行目标检测推理。
## 快速开始
### 1. 准备模型文件
将YOLOv8训练好的模型放到指定位置
```bash
# 模型文件路径
python-inference-service/models/best.pt
```
### 2. 准备类别文件(可选)
创建 `classes.txt` 文件,每行一个类别名称:
```bash
# 文件路径
python-inference-service/models/classes.txt
# 内容示例
person
car
truck
bicycle
```
### 3. 启动服务
```bash
# 使用部署脚本
deploy.bat # Windows
./deploy.sh # Linux/Mac
# 或手动启动
docker-compose up -d
```
## 训练YOLOv8模型
如果还没有训练好的模型,可以按以下步骤训练:
### 1. 安装Ultralytics
```bash
pip install ultralytics
```
### 2. 准备数据集
创建数据集配置文件 `data.yaml`
```yaml
# 数据集路径
path: /path/to/dataset
train: images/train
val: images/val
# 类别
nc: 4 # 类别数量
names: ['person', 'car', 'truck', 'bicycle'] # 类别名称
```
### 3. 训练模型
```python
from ultralytics import YOLO
# 加载预训练模型
model = YOLO('yolov8n.pt') # n, s, m, l, x 可选
# 训练模型
results = model.train(
data='data.yaml',
epochs=100,
imgsz=640,
batch=16,
device=0 # GPU设备IDCPU使用'cpu'
)
# 训练完成后,最佳模型保存在 runs/detect/train/weights/best.pt
```
### 4. 导出模型
训练完成后,将最佳模型复制到项目中:
```bash
# 复制模型文件
cp runs/detect/train/weights/best.pt python-inference-service/models/best.pt
# 创建类别文件
echo "person
car
truck
bicycle" > python-inference-service/models/classes.txt
```
## 模型配置
### 修改模型参数
编辑 `python-inference-service/models/yolov8_model.py`
```python
# 置信度阈值
self.conf_threshold = 0.25 # 默认0.25,降低可检测更多目标
# 输入图像尺寸
self.img_size = 640 # 默认640可改为320、1280等
```
### 配置文件
`python-inference-service/models/models.json`
```json
[
{
"name": "yolov8_detector",
"path": "models/yolov8_model.py",
"size": [640, 640],
"comment": "YOLOv8检测模型"
}
]
```
参数说明:
- `name`: 模型名称API调用时使用
- `path`: 模型包装类路径
- `size`: 输入图像尺寸 [宽度, 高度]
## 多模型配置
如果有多个模型,可以配置多个:
```json
[
{
"name": "person_detector",
"path": "models/person_model.py",
"size": [640, 640]
},
{
"name": "vehicle_detector",
"path": "models/vehicle_model.py",
"size": [640, 640]
}
]
```
然后为每个模型创建对应的模型文件,参考 `yolov8_model.py`
## 测试模型
### 本地测试
```python
from ultralytics import YOLO
# 加载模型
model = YOLO('python-inference-service/models/best.pt')
# 测试图像
results = model('test.jpg')
# 显示结果
results[0].show()
# 打印检测结果
for r in results:
print(r.boxes)
```
### API测试
启动服务后使用curl测试
```bash
# 1. 检查服务健康
curl http://localhost:10080/python-api/health
# 2. 查看可用模型
curl http://localhost:10080/python-api/api/models
# 3. 测试检测(文件上传)
curl -X POST "http://localhost:10080/python-api/api/detect/file" \
-F "model_name=yolov8_detector" \
-F "file=@test.jpg"
```
## 性能优化
### 1. 使用GPU
确保Docker配置了GPU支持
```yaml
# docker-compose.yml
python-service:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
```
### 2. 选择合适的模型大小
YOLOv8提供多种尺寸
| 模型 | 参数量 | 速度 | 精度 |
|------|--------|------|------|
| YOLOv8n | 3.2M | 最快 | 较低 |
| YOLOv8s | 11.2M | 快 | 中等 |
| YOLOv8m | 25.9M | 中等 | 较高 |
| YOLOv8l | 43.7M | 慢 | 高 |
| YOLOv8x | 68.2M | 最慢 | 最高 |
根据需求选择:
- 实时处理 → 使用 `yolov8n.pt``yolov8s.pt`
- 高精度 → 使用 `yolov8l.pt``yolov8x.pt`
### 3. 调整图像尺寸
```python
# 在 yolov8_model.py 中
self.img_size = 320 # 更快但精度降低
# 或
self.img_size = 1280 # 更慢但精度提高
```
## 常见问题
### Q1: 模型加载失败
```
错误FileNotFoundError: best.pt not found
解决:确保模型文件在 python-inference-service/models/best.pt
```
### Q2: GPU不可用
```
错误CUDA not available
解决:
1. 检查NVIDIA驱动安装
2. 检查Docker GPU支持
3. 使用 docker run --gpus all 测试GPU
```
### Q3: 检测结果为空
```
原因:
1. 置信度阈值太高
2. 模型未正确训练
3. 输入图像与训练数据差异大
解决:
1. 降低 conf_threshold
2. 检查模型训练情况
3. 检查输入图像质量
```
### Q4: 推理速度慢
```
解决:
1. 使用GPU加速
2. 使用更小的模型如yolov8n
3. 减小输入图像尺寸
4. 批量处理多张图像
```
## 模型版本差异
### YOLOv5 vs YOLOv8
本系统已从YOLOv5升级到YOLOv8
| 特性 | YOLOv5 | YOLOv8 |
|------|--------|--------|
| 精度 | 较高 | 更高 |
| 速度 | 快 | 更快 |
| API | yolov5 | ultralytics |
| 训练 | 复杂 | 简单 |
### 迁移说明
如果之前使用YOLOv5模型
1. 使用YOLOv8重新训练推荐
2. 或使用 `garbage_model.py` 作为模板支持YOLOv5
## 参考资料
- [Ultralytics YOLOv8文档](https://docs.ultralytics.com/)
- [YOLOv8 GitHub](https://github.com/ultralytics/ultralytics)
- [模型训练教程](https://docs.ultralytics.com/modes/train/)
- [推理示例](https://docs.ultralytics.com/modes/predict/)
## 技术支持
如遇问题:
1. 查看服务日志:`docker-compose logs python-service`
2. 查看模型加载日志
3. 测试模型是否可以本地加载
4. 检查环境配置是否正确

54
deploy.bat Normal file
View File

@@ -0,0 +1,54 @@
@echo off
chcp 65001 >nul
echo ============================================
echo RTSP视频分析系统 Docker部署
echo ============================================
echo.
REM 检查Docker是否运行
docker info >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] Docker未运行或未安装
echo 请先启动Docker Desktop
pause
exit /b 1
)
echo [1/5] 检查环境文件...
if not exist .env (
echo [错误] .env文件不存在
echo 请先配置.env文件
pause
exit /b 1
)
echo [2/5] 停止现有容器...
docker-compose down
echo [3/5] 构建Docker镜像...
docker-compose build
echo [4/5] 启动所有服务...
docker-compose up -d
echo [5/5] 等待服务启动...
timeout /t 10 /nobreak >nul
echo.
echo ============================================
echo 部署完成!
echo ============================================
echo.
echo 查看服务状态: docker-compose ps
echo 查看日志: docker-compose logs -f
echo.
echo 访问地址:
echo 前端界面: http://localhost:10080
echo.
echo 按任意键查看服务状态...
pause >nul
docker-compose ps
echo.
pause

46
deploy.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
echo "============================================"
echo "RTSP视频分析系统 Docker部署"
echo "============================================"
echo ""
# 检查Docker是否运行
if ! docker info > /dev/null 2>&1; then
echo "[错误] Docker未运行或未安装"
echo "请先安装并启动Docker"
exit 1
fi
echo "[1/5] 检查环境文件..."
if [ ! -f .env ]; then
echo "[错误] .env文件不存在"
echo "请先配置.env文件"
exit 1
fi
echo "[2/5] 停止现有容器..."
docker-compose down
echo "[3/5] 构建Docker镜像..."
docker-compose build
echo "[4/5] 启动所有服务..."
docker-compose up -d
echo "[5/5] 等待服务启动..."
sleep 10
echo ""
echo "============================================"
echo "部署完成!"
echo "============================================"
echo ""
echo "查看服务状态: docker-compose ps"
echo "查看日志: docker-compose logs -f"
echo ""
echo "访问地址:"
echo " 前端界面: http://localhost:10080"
echo ""
echo "服务状态:"
docker-compose ps

91
docker-compose.yml Normal file
View File

@@ -0,0 +1,91 @@
version: '3.8'
services:
redis:
image: redis:7-alpine
container_name: ${REDIS_HOST}
restart: always
command: redis-server --appendonly yes --bind 127.0.0.1 ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}}
network_mode: "host"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
python-service:
build:
context: ./python-inference-service
dockerfile: Dockerfile
container_name: ${PYTHON_SERVICE_HOST}
restart: always
network_mode: "host"
environment:
TZ: ${TZ}
MODEL_DIR: /app/models
volumes:
- ./python-inference-service/models:/app/models
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
backend:
build:
context: .
dockerfile: ruoyi-admin/Dockerfile
container_name: ${BACKEND_HOST}
restart: always
network_mode: "host"
depends_on:
redis:
condition: service_healthy
python-service:
condition: service_started
environment:
TZ: ${TZ}
SERVER_HOST: ${SERVER_HOST:-localhost}
SERVER_PORT: ${BACKEND_EXTERNAL_PORT}
SERVER_ADDRESS: 0.0.0.0
MEDIA_SERVER_PORT: ${MEDIA_SERVER_EXTERNAL_PORT}
MEDIA_SERVER_ADDRESS: 0.0.0.0
SPRING_DATASOURCE_URL: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: ${MYSQL_USER}
SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD}
SPRING_DATA_REDIS_HOST: localhost
SPRING_DATA_REDIS_PORT: ${REDIS_PORT}
SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD}
PYTHON_API_URL: http://localhost:${PYTHON_SERVICE_PORT}
volumes:
- backend-logs:/app/logs
- backend-upload:/app/upload
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${BACKEND_EXTERNAL_PORT}/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
frontend:
build:
context: ./rtsp-vue
dockerfile: Dockerfile
container_name: rtsp-frontend
restart: always
network_mode: "host"
environment:
TZ: ${TZ}
NGINX_PORT: ${FRONTEND_PORT}
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://localhost:${FRONTEND_PORT}"]
interval: 30s
timeout: 10s
retries: 3
volumes:
redis-data:
backend-logs:
backend-upload:

View File

@@ -42,7 +42,7 @@
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.10</version> <!-- 版本号需与项目兼容 -->
<version>1.5.12</version> <!-- 版本号需与项目兼容 -->
</dependency>
<!-- SpringBoot的依赖配置-->
<dependency>
@@ -220,6 +220,9 @@
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>

View File

@@ -0,0 +1,47 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
*.egg-info/
dist/
build/
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Git
.git
.gitignore
# CI/CD
.github
.gitlab-ci.yml
# Documentation
*.md
README*
# Scripts
*.bat
*.sh
# Large model files (will be mounted as volume)
models/*.pt
models/*.onnx
models/*.pth
# Logs
*.log
logs/

View File

@@ -0,0 +1,49 @@
# 使用支持CUDA的PyTorch基础镜像
FROM python:3.9-slim
# 设置工作目录
WORKDIR /app
# 设置pip镜像源
# 安装系统依赖
RUN apt-get update && apt-get install -y \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender-dev \
libgomp1 \
libgl1 \
libglib2.0-dev \
curl \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# 复制requirements.txt
COPY requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.mirrors.ustc.edu.cn/simple
# 复制应用代码
COPY app/ /app/app/
# 创建models目录
RUN mkdir -p /app/models
# 设置环境变量
ENV PYTHONPATH=/app
ENV MODEL_DIR=/app/models
ENV PYTHONUNBUFFERED=1
# 暴露端口(仅内部使用)
EXPOSE 8000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# 启动应用(只监听本地,外网无法访问)
CMD ["uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", "8000", "--workers", "1"]

View File

@@ -0,0 +1,252 @@
# Python推理服务
基于FastAPI的YOLOv8目标检测推理服务。
## 功能特性
- 支持YOLOv8模型推理
- RESTful API接口
- 支持Base64图像和文件上传
- 支持GPU加速可选
- Docker部署支持
## 模型要求
本服务使用**YOLOv8**Ultralytics进行目标检测。
### 模型文件准备
1. **模型文件**: 将YOLOv8训练好的模型文件命名为`best.pt`,放在`models/`目录下
2. **类别文件**: (可选)创建`classes.txt`文件,每行一个类别名称
3. **配置文件**: `models.json`配置模型参数
### 目录结构
```
python-inference-service/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI应用
│ ├── detector.py # 检测器封装
│ └── models.py # 数据模型
├── models/
│ ├── best.pt # YOLOv8模型文件必需
│ ├── classes.txt # 类别名称(可选)
│ ├── yolov8_model.py # YOLOv8模型包装类
│ └── models.json # 模型配置
├── requirements.txt
└── Dockerfile
```
## 安装依赖
```bash
pip install -r requirements.txt
```
主要依赖:
- `ultralytics>=8.0.0` - YOLOv8框架
- `fastapi` - Web框架
- `uvicorn` - ASGI服务器
- `opencv-python` - 图像处理
- `torch` - PyTorch
## 配置模型
编辑`models/models.json`
```json
[
{
"name": "yolov8_detector",
"path": "models/yolov8_model.py",
"size": [640, 640],
"comment": "YOLOv8检测模型"
}
]
```
参数说明:
- `name`: 模型名称API调用时使用
- `path`: 模型包装类的路径
- `size`: 输入图像尺寸 [宽度, 高度]
## 启动服务
### 本地启动
```bash
# 启动服务默认端口8000
uvicorn app.main:app --host 0.0.0.0 --port 8000
# 或使用启动脚本
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
```
### Docker启动
```bash
# 构建镜像
docker build -t python-inference-service .
# 运行容器
docker run -p 8000:8000 \
-v $(pwd)/models:/app/models \
python-inference-service
```
### 使用GPU
```bash
# 确保安装了NVIDIA Docker Runtime
docker run --gpus all -p 8000:8000 \
-v $(pwd)/models:/app/models \
python-inference-service
```
## API接口
服务启动后访问http://localhost:8000/docs 查看API文档
### 1. 健康检查
```bash
GET /health
```
### 2. 获取可用模型列表
```bash
GET /api/models
```
### 3. Base64图像检测
```bash
POST /api/detect
Content-Type: application/json
{
"model_name": "yolov8_detector",
"image_data": "base64_encoded_image_string"
}
```
### 4. 文件上传检测
```bash
POST /api/detect/file
Content-Type: multipart/form-data
model_name: yolov8_detector
file: <image_file>
```
## 响应格式
```json
{
"model_name": "yolov8_detector",
"detections": [
{
"label": "[yolov8_detector] 类别名",
"confidence": 0.95,
"x": 100,
"y": 150,
"width": 200,
"height": 180,
"color": 65280
}
],
"inference_time": 45.6
}
```
## 自定义模型
要使用自己训练的YOLOv8模型
1. **训练模型**使用Ultralytics YOLOv8训练您的模型
```python
from ultralytics import YOLO
model = YOLO('yolov8n.yaml')
model.train(data='your_data.yaml', epochs=100)
```
2. **导出模型**:训练完成后会生成`best.pt`文件
3. **准备类别文件**:创建`classes.txt`
```
class1
class2
class3
```
4. **放置文件**:将`best.pt`和`classes.txt`放到`models/`目录
5. **更新配置**:确保`models.json`配置正确
6. **重启服务**
## 环境变量
- `MODEL_DIR`: 模型目录路径(默认:`/app/models`
- `MODELS_JSON`: 模型配置文件路径(默认:`models/models.json`
## 性能优化
### GPU加速
服务会自动检测GPU并使用。如果有多张GPU可以指定
```bash
CUDA_VISIBLE_DEVICES=0 uvicorn app.main:app --host 0.0.0.0 --port 8000
```
### 置信度阈值
在`yolov8_model.py`中调整:
```python
self.conf_threshold = 0.25 # 降低阈值检测更多目标
```
## 故障排查
### 模型加载失败
```
错误:找不到 best.pt
解决:确保模型文件在 models/ 目录下
```
### GPU不可用
```
错误CUDA not available
解决:
1. 检查NVIDIA驱动
2. 检查PyTorch GPU版本
3. 检查CUDA版本兼容性
```
### 推理速度慢
```
解决:
1. 使用GPU加速
2. 使用更小的模型如yolov8n.pt
3. 减小输入图像尺寸
```
## 开发者
如需修改或扩展功能,请参考:
- `app/main.py` - API路由定义
- `app/detector.py` - 检测器基类
- `models/yolov8_model.py` - YOLOv8模型包装类
## 许可证
[根据项目实际许可证填写]

View File

@@ -0,0 +1 @@
# Python Inference Service package

View File

@@ -0,0 +1,330 @@
import os
import cv2
import numpy as np
import time
from typing import List, Dict, Tuple, Optional
import importlib.util
import sys
from app.models import Detection
class PythonModelDetector:
"""Object detector using native Python models"""
def __init__(self, model_name: str, model_path: str, input_width: int, input_height: int, color: int = 0x00FF00, model_config: dict = None):
"""
Initialize detector with Python model
Args:
model_name: Name of the model
model_path: Path to the Python model file (.py)
input_width: Input width for the model
input_height: Input height for the model
color: RGB color for detection boxes (default: green)
model_config: Additional configuration to pass to the model
"""
self.model_name = model_name
self.input_width = input_width
self.input_height = input_height
self.color = color
self.model_config = model_config or {}
# Convert color from RGB to BGR (OpenCV uses BGR)
self.color_bgr = ((color & 0xFF) << 16) | (color & 0xFF00) | ((color >> 16) & 0xFF)
# Default confidence thresholds
self.conf_threshold = 0.25
self.nms_threshold = 0.45
# Load the Python model dynamically
self._load_python_model(model_path)
# Load class names if available
self.classes = []
model_dir = os.path.dirname(model_path)
classes_path = os.path.join(model_dir, "classes.txt")
if os.path.exists(classes_path):
with open(classes_path, 'r') as f:
self.classes = [line.strip() for line in f.readlines() if line.strip()]
def _load_python_model(self, model_path: str):
"""Load Python model dynamically"""
if not os.path.exists(model_path):
raise FileNotFoundError(f"Model file not found: {model_path}")
# Get model directory and file name
model_dir = os.path.dirname(model_path)
model_file = os.path.basename(model_path)
model_name = os.path.splitext(model_file)[0]
# Add model directory to system path
if model_dir not in sys.path:
sys.path.append(model_dir)
# Import the model module
spec = importlib.util.spec_from_file_location(model_name, model_path)
if spec is None:
raise ImportError(f"Failed to load model specification: {model_path}")
model_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(model_module)
# Check if the module has the required interface
if not hasattr(model_module, "Model"):
raise AttributeError(f"Model module must define a 'Model' class: {model_path}")
# Create model instance with config
# Try to pass config to model constructor if it accepts parameters
import inspect
model_class = model_module.Model
sig = inspect.signature(model_class.__init__)
if len(sig.parameters) > 1: # Has parameters beyond 'self'
# Pass all config as keyword arguments
self.model = model_class(**self.model_config)
else:
# No parameters, create without arguments
self.model = model_class()
# Check if model has the required methods
if not hasattr(self.model, "predict"):
raise AttributeError(f"Model must implement 'predict' method: {model_path}")
def preprocess(self, img: np.ndarray) -> np.ndarray:
"""Preprocess image for model input"""
# Ensure BGR image
if len(img.shape) == 2: # Grayscale
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
elif img.shape[2] == 4: # BGRA
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
# Resize to model input size
resized = cv2.resize(img, (self.input_width, self.input_height))
# Use model's preprocess method if available
if hasattr(self.model, "preprocess"):
return self.model.preprocess(resized)
# Default preprocessing: normalize to [0, 1]
return resized / 255.0
def detect(self, img: np.ndarray) -> Tuple[List[Detection], float]:
"""
Detect objects in an image
Args:
img: Input image in BGR format (OpenCV)
Returns:
List of Detection objects and inference time in milliseconds
"""
if img is None or img.size == 0:
return [], 0.0
# Original image dimensions
img_height, img_width = img.shape[:2]
# Measure inference time
start_time = time.time()
try:
# Run inference using model's predict method
# Note: Pass original image to model, let it handle preprocessing
# Expected return format from model's predict:
# List of dicts with keys: 'bbox', 'class_id', 'confidence'
# bbox: (x, y, w, h) normalized [0-1]
model_results = self.model.predict(img)
# Calculate inference time in milliseconds
inference_time = (time.time() - start_time) * 1000
# Convert model results to Detection objects
detections = []
for result in model_results:
# Skip low confidence detections
confidence = result.get('confidence', 0)
if confidence < self.conf_threshold:
continue
# Get bounding box (normalized coordinates)
bbox = result.get('bbox', [0, 0, 0, 0])
# Denormalize bbox to image coordinates
x = int(bbox[0] * img_width)
y = int(bbox[1] * img_height)
w = int(bbox[2] * img_width)
h = int(bbox[3] * img_height)
# Skip invalid boxes
if w <= 0 or h <= 0:
continue
# Get class ID and name
class_id = result.get('class_id', 0)
class_name = f"cls{class_id}"
if 0 <= class_id < len(self.classes):
class_name = self.classes[class_id]
# Create Detection object
label = f"[{self.model_name}] {class_name}"
detection = Detection(
label=label,
confidence=confidence,
x=x,
y=y,
width=w,
height=h,
color=self.color
)
detections.append(detection)
# Apply NMS if model doesn't do it internally
if hasattr(self.model, "applies_nms") and self.model.applies_nms:
return detections, inference_time
else:
# Convert detections to boxes and scores
boxes = [(d.x, d.y, d.width, d.height) for d in detections]
scores = [d.confidence for d in detections]
if boxes:
# Apply NMS
indices = self._non_max_suppression(boxes, scores, self.nms_threshold)
detections = [detections[i] for i in indices]
return detections, inference_time
except Exception as e:
print(f"Error during detection: {str(e)}")
return [], (time.time() - start_time) * 1000
def _non_max_suppression(self, boxes: List[Tuple], scores: List[float], threshold: float) -> List[int]:
"""Apply Non-Maximum Suppression to remove overlapping boxes"""
# Sort by score in descending order
indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
keep = []
while indices:
# Get index with highest score
current = indices.pop(0)
keep.append(current)
# No more indices to process
if not indices:
break
# Get current box
x1, y1, w1, h1 = boxes[current]
x2_1 = x1 + w1
y2_1 = y1 + h1
area1 = w1 * h1
# Check remaining boxes
i = 0
while i < len(indices):
# Get box to compare
idx = indices[i]
x2, y2, w2, h2 = boxes[idx]
x2_2 = x2 + w2
y2_2 = y2 + h2
area2 = w2 * h2
# Calculate intersection
xx1 = max(x1, x2)
yy1 = max(y1, y2)
xx2 = min(x2_1, x2_2)
yy2 = min(y2_1, y2_2)
# Calculate intersection area
w = max(0, xx2 - xx1)
h = max(0, yy2 - yy1)
intersection = w * h
# Calculate IoU
union = area1 + area2 - intersection + 1e-9 # Avoid division by zero
iou = intersection / union
# Remove box if IoU is above threshold
if iou > threshold:
indices.pop(i)
else:
i += 1
return keep
def close(self):
"""Close the model resources"""
if hasattr(self.model, "close"):
self.model.close()
self.model = None
class ModelManager:
"""Model manager for detectors"""
def __init__(self):
self.models = {}
def load(self, models_config: List[Dict]):
"""
Load models from configuration
Args:
models_config: List of model configurations
"""
# Basic color palette for different models
palette = [0x00FF00, 0xFF8000, 0x00A0FF, 0xFF00FF, 0x00FFFF, 0xFF0000, 0x80FF00]
for i, model_config in enumerate(models_config):
name = model_config.get("name")
path = model_config.get("path")
size = model_config.get("size", [640, 640])
if not name or not path or not os.path.exists(path):
print(f"Skipping model: {name} - Invalid configuration")
continue
try:
# Use color from palette
color = palette[i % len(palette)]
# Extract model-specific config (model_file, model_name, etc.)
# These will be passed to the Model class __init__
model_init_config = {}
if "model_file" in model_config:
model_init_config["model_file"] = model_config["model_file"]
if "display_name" in model_config:
model_init_config["model_name"] = model_config["display_name"]
# Create detector for Python model
detector = PythonModelDetector(
model_name=name,
model_path=path,
input_width=size[0],
input_height=size[1],
color=color,
model_config=model_init_config
)
self.models[name] = detector
print(f"Model loaded: {name} ({path})")
except Exception as e:
print(f"Failed to load model {name}: {str(e)}")
def get(self, name: str) -> Optional[PythonModelDetector]:
"""Get detector by name"""
return self.models.get(name)
def all(self) -> List[PythonModelDetector]:
"""Get all detectors"""
return list(self.models.values())
def close(self):
"""Close all detectors"""
for detector in self.models.values():
try:
detector.close()
except:
pass
self.models.clear()

View File

@@ -0,0 +1,191 @@
import os
import base64
import cv2
import json
import numpy as np
from typing import Dict, List
from fastapi import FastAPI, HTTPException, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from app.models import Detection, DetectionRequest, DetectionResponse, ModelInfo, ModelsResponse
from app.detector import ModelManager
# Initialize FastAPI app
app = FastAPI(
title="Python Model Inference Service",
description="API for object detection using Python models",
version="1.0.0"
)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize model manager
model_manager = None
# Load models from configuration
@app.on_event("startup")
async def startup_event():
global model_manager
model_manager = ModelManager()
# Look for models.json configuration file
models_json_path = os.getenv("MODELS_JSON", os.path.join(os.path.dirname(__file__), "..", "models.json"))
if os.path.exists(models_json_path):
try:
with open(models_json_path, "r") as f:
models_config = json.load(f)
model_manager.load(models_config)
print(f"Loaded model configuration from {models_json_path}")
except Exception as e:
print(f"Failed to load models from {models_json_path}: {str(e)}")
else:
print(f"Models configuration not found: {models_json_path}")
@app.on_event("shutdown")
async def shutdown_event():
global model_manager
if model_manager:
model_manager.close()
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "ok"}
@app.get("/api/models", response_model=ModelsResponse)
async def get_models():
"""Get available models"""
global model_manager
if not model_manager:
raise HTTPException(status_code=500, detail="Model manager not initialized")
detectors = model_manager.all()
models = []
for detector in detectors:
model_info = ModelInfo(
name=detector.model_name,
path=getattr(detector, 'model_path', ''),
size=[detector.input_width, detector.input_height],
backend="Python",
loaded=True
)
models.append(model_info)
return ModelsResponse(models=models)
@app.post("/api/detect", response_model=DetectionResponse)
async def detect(request: DetectionRequest):
"""Detect objects in an image"""
global model_manager
if not model_manager:
raise HTTPException(status_code=500, detail="Model manager not initialized")
# Get detector for requested model
detector = model_manager.get(request.model_name)
if not detector:
raise HTTPException(status_code=404, detail=f"Model not found: {request.model_name}")
# Decode base64 image
try:
# Remove data URL prefix if present
if "base64," in request.image_data:
image_data = request.image_data.split("base64,")[1]
else:
image_data = request.image_data
# Decode base64 image
image_bytes = base64.b64decode(image_data)
nparr = np.frombuffer(image_bytes, np.uint8)
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if image is None:
raise HTTPException(status_code=400, detail="Invalid image data")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Failed to decode image: {str(e)}")
# Run detection
detections, inference_time = detector.detect(image)
return DetectionResponse(
model_name=request.model_name,
detections=detections,
inference_time=inference_time
)
@app.post("/api/detect/file", response_model=DetectionResponse)
async def detect_file(
model_name: str,
file: UploadFile = File(...)
):
"""Detect objects in an uploaded image file"""
print(f"接收到的 model_name: {model_name}")
print(f"文件名: {file.filename}")
print(f"文件内容类型: {file.content_type}")
global model_manager
if not model_manager:
raise HTTPException(status_code=500, detail="Model manager not initialized")
# Get detector for requested model
detector = model_manager.get(model_name)
if not detector:
raise HTTPException(status_code=404, detail=f"Model not found: {model_name}")
# Read uploaded file
try:
contents = await file.read()
print(f"文件大小: {len(contents)} 字节")
if len(contents) == 0:
raise HTTPException(status_code=400, detail="Empty file")
nparr = np.frombuffer(contents, np.uint8)
print(f"numpy数组形状: {nparr.shape}, dtype: {nparr.dtype}")
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if image is None:
print("错误: cv2.imdecode 返回 None")
raise HTTPException(status_code=400, detail="Invalid image data - failed to decode")
print(f"解码后图像形状: {image.shape}, dtype: {image.dtype}")
except HTTPException:
raise
except Exception as e:
print(f"处理图像时出错: {str(e)}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=400, detail=f"Failed to process image: {str(e)}")
# Run detection
try:
detections, inference_time = detector.detect(image)
print(f"检测完成: 找到 {len(detections)} 个目标, 耗时 {inference_time:.2f}ms")
except Exception as e:
print(f"推理过程中出错: {str(e)}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Detection failed: {str(e)}")
return DetectionResponse(
model_name=model_name,
detections=detections,
inference_time=inference_time
)
if __name__ == "__main__":
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)

View File

@@ -0,0 +1,40 @@
from pydantic import BaseModel
from typing import List, Optional
class Detection(BaseModel):
"""Object detection result"""
label: str
confidence: float
x: int
y: int
width: int
height: int
color: int = 0x00FF00 # Default green color
class DetectionRequest(BaseModel):
"""Request for model inference on image data"""
model_name: str
image_data: str # Base64 encoded image
class DetectionResponse(BaseModel):
"""Response with detection results"""
model_name: str
detections: List[Detection]
inference_time: float # Time in milliseconds
class ModelInfo(BaseModel):
"""Model information"""
name: str
path: str
size: List[int] # [width, height]
backend: str = "ONNX"
loaded: bool = False
class ModelsResponse(BaseModel):
"""Response with available models"""
models: List[ModelInfo]

View File

@@ -0,0 +1,18 @@
[
{
"name": "smoke",
"path": "models/universal_yolo_model.py",
"model_file": "smoke.pt",
"display_name": "吸烟检测",
"size": [640, 640],
"comment": "吸烟检测模型 - YOLOv11"
},
{
"name": "garbage",
"path": "models/universal_yolo_model.py",
"model_file": "garbage.pt",
"display_name": "垃圾识别",
"size": [640, 640],
"comment": "垃圾检测模型 - YOLOv8"
}
]

Binary file not shown.

View File

@@ -0,0 +1 @@
垃圾

Binary file not shown.

View File

@@ -0,0 +1 @@
垃圾

View File

@@ -0,0 +1,169 @@
import os
import numpy as np
import cv2
from typing import List, Dict, Any
import torch
class Model:
"""
通用 YOLO 模型 - 支持 YOLOv8/YOLOv11 等基于 Ultralytics 的模型
"""
def __init__(self, model_file: str = None, model_name: str = "YOLO"):
"""
初始化模型
Args:
model_file: 模型文件名(如 smoke.pt, best.pt
model_name: 模型显示名称(用于日志)
"""
# 获取当前文件所在目录路径
model_dir = os.path.dirname(os.path.abspath(__file__))
# 如果没有指定模型文件,尝试常见的文件名
if model_file is None:
for possible_file in ['garbage.pt', 'smoke.pt', 'best.pt', 'yolov8.pt', 'model.pt']:
test_path = os.path.join(model_dir, possible_file)
if os.path.exists(test_path):
model_file = possible_file
break
if model_file is None:
raise FileNotFoundError(f"未找到模型文件,请在初始化时指定 model_file 参数")
# 模型文件路径
model_path = os.path.join(model_dir, model_file)
if not os.path.exists(model_path):
raise FileNotFoundError(f"模型文件不存在: {model_path}")
self.model_name = model_name
print(f"正在加载{model_name}模型: {model_path}")
# 检查设备
self.device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"使用设备: {self.device}")
# 使用 Ultralytics YOLO 加载模型
try:
from ultralytics import YOLO
self.model = YOLO(model_path)
print(f"使用 Ultralytics YOLO 加载模型成功")
except ImportError:
raise ImportError("请安装 ultralytics: pip install ultralytics>=8.0.0")
except Exception as e:
raise Exception(f"加载{model_name}模型失败: {str(e)}")
# 加载类别名称
self.classes = []
# 1. 首先尝试加载与模型文件同名的类别文件(如 smoke.txt
model_base_name = os.path.splitext(model_file)[0]
classes_path_specific = os.path.join(model_dir, f"{model_base_name}.txt")
# 2. 然后尝试加载通用的 classes.txt
classes_path_generic = os.path.join(model_dir, "classes.txt")
if os.path.exists(classes_path_specific):
with open(classes_path_specific, 'r', encoding='utf-8') as f:
self.classes = [line.strip() for line in f.readlines() if line.strip()]
print(f"已加载类别文件: {model_base_name}.txt ({len(self.classes)} 个类别)")
elif os.path.exists(classes_path_generic):
with open(classes_path_generic, 'r', encoding='utf-8') as f:
self.classes = [line.strip() for line in f.readlines() if line.strip()]
print(f"已加载类别文件: classes.txt ({len(self.classes)} 个类别)")
else:
# 使用模型自带的类别信息
if hasattr(self.model, 'names') and self.model.names:
self.classes = list(self.model.names.values()) if isinstance(self.model.names, dict) else self.model.names
print(f"使用模型自带类别,共 {len(self.classes)} 个类别")
else:
print("未找到类别文件,将使用数字索引作为类别名")
# 设置识别参数
self.conf_threshold = 0.25 # 置信度阈值
self.img_size = 640 # 默认输入图像大小
print(f"{model_name}模型加载完成")
def preprocess(self, image: np.ndarray) -> np.ndarray:
"""预处理图像 - Ultralytics YOLO 会自动处理,这里直接返回"""
return image
def predict(self, image: np.ndarray) -> List[Dict[str, Any]]:
"""模型推理"""
original_height, original_width = image.shape[:2]
try:
# YOLO 推理
results = self.model(
image,
conf=self.conf_threshold,
device=self.device,
verbose=False
)
detections = []
# 解析结果
for result in results:
# 获取检测框
boxes = result.boxes
if boxes is None or len(boxes) == 0:
continue
# 遍历每个检测框
for box in boxes:
# 获取坐标 (xyxy格式)
xyxy = box.xyxy[0].cpu().numpy()
x1, y1, x2, y2 = xyxy
# 转换为归一化坐标 (x, y, w, h)
x = x1 / original_width
y = y1 / original_height
w = (x2 - x1) / original_width
h = (y2 - y1) / original_height
# 获取置信度
conf = float(box.conf[0].cpu().numpy())
# 获取类别ID
cls_id = int(box.cls[0].cpu().numpy())
# 获取类别名称
class_name = f"cls{cls_id}"
if 0 <= cls_id < len(self.classes):
class_name = self.classes[cls_id]
# 添加检测结果
if conf >= self.conf_threshold:
detections.append({
'bbox': (x, y, w, h),
'class_id': cls_id,
'confidence': conf
})
return detections
except Exception as e:
print(f"推理过程中出错: {str(e)}")
import traceback
traceback.print_exc()
return []
@property
def applies_nms(self) -> bool:
"""模型是否内部应用了 NMS"""
# Ultralytics YOLO 会自动应用 NMS
return True
def close(self):
"""释放资源"""
if hasattr(self, 'model'):
# 删除模型以释放 GPU 内存
del self.model
if torch.cuda.is_available():
torch.cuda.empty_cache()
print(f"{self.model_name}模型已关闭")

View File

@@ -0,0 +1,10 @@
fastapi==0.103.1
uvicorn==0.23.2
opencv-python==4.8.0.76
numpy==1.25.2
pydantic==2.3.0
python-multipart==0.0.6
minio>=7.2,<8
torch>=1.7.0
torchvision>=0.8.1
ultralytics>=8.0.0

View File

@@ -0,0 +1,5 @@
@echo off
echo Starting Python Inference Service...
cd /d %~dp0
python -m app.main
pause

View File

@@ -0,0 +1,4 @@
#!/bin/bash
echo "Starting Python Inference Service..."
cd "$(dirname "$0")"
python -m app.main

41
rtsp-vue/.dockerignore Normal file
View File

@@ -0,0 +1,41 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# Build output
dist/
build/
# IDE
.vscode
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS
.DS_Store
Thumbs.db
# Git
.git
.gitignore
# CI/CD
.github
.gitlab-ci.yml
# Documentation
*.md
README*
# Scripts
bin/
*.bat
*.sh

View File

@@ -1,5 +1,5 @@
# 页面标题
VITE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 福安德视频监控分析
# 开发环境配置
VITE_APP_ENV = 'development'

View File

@@ -1,5 +1,5 @@
# 页面标题
VITE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 福安德视频监控分析
# 生产环境配置
VITE_APP_ENV = 'production'

View File

@@ -1,5 +1,5 @@
# 页面标题
VITE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 福安德视频监控分析
# 生产环境配置
VITE_APP_ENV = 'staging'

41
rtsp-vue/Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
# 构建阶段
FROM node:18-alpine AS builder
# 设置工作目录
WORKDIR /app
# 设置npm镜像源
RUN npm config set registry https://registry.npmmirror.com
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm install
# 复制源代码
COPY . .
# 构建生产环境项目
RUN npm run build:prod
# 运行阶段
FROM nginx:1.25-alpine
# 安装curl和envsubst用于健康检查和环境变量替换
RUN apk add --no-cache curl gettext
# 删除默认nginx配置
RUN rm -rf /etc/nginx/conf.d/*
# 复制nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 暴露端口
EXPOSE 80
# 使用envsubst替换环境变量然后启动Nginx
CMD sh -c "envsubst '\$NGINX_PORT' < /etc/nginx/conf.d/default.conf > /tmp/nginx.conf && mv /tmp/nginx.conf /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"

View File

@@ -6,8 +6,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="/favicon.ico">
<title>若依管理系统</title>
<link rel="icon" href="/favicon.png">
<title>福安德视频监控分析</title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style>
html,
@@ -201,7 +201,7 @@
</head>
<body>
<div id="app">
<div id="app" class="compact">
<div id="loader-wrapper">
<div id="loader"></div>
<div class="loader-section section-left"></div>

123
rtsp-vue/nginx.conf Normal file
View File

@@ -0,0 +1,123 @@
server {
listen ${NGINX_PORT} default_server;
listen [::]:${NGINX_PORT} default_server;
server_name _;
# 前端资源
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 后端API代理backend使用host网络暴露10082端口
location /prod-api/ {
proxy_pass http://127.0.0.1:10082/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
}
# WebSocket支持用于视频流
location /websocket/ {
proxy_pass http://127.0.0.1:10082/websocket/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
# Python推理服务代理映射到宿主机端口
location /python-api/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 视频流代理HTTP-FLVbackend使用host网络暴露10083端口
location /live {
proxy_pass http://127.0.0.1:10083/live;
proxy_http_version 1.1;
# 【关键】禁用所有缓冲,立即转发每一个字节
proxy_buffering off;
proxy_cache off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
# 设置超时时间(视频流长连接)
proxy_connect_timeout 120s;
proxy_send_timeout 7200s;
proxy_read_timeout 7200s;
# 请求头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection "";
# 【关键】响应头 - 禁用缓冲和缓存
add_header X-Accel-Buffering no always;
add_header Cache-Control 'no-cache, no-store, must-revalidate' always;
add_header Pragma no-cache always;
add_header Expires 0 always;
# CORS支持
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers '*' always;
# 【关键】TCP 优化
tcp_nopush off;
tcp_nodelay on;
keepalive_timeout 0;
}
# 视频流代理HLS
location /hls {
proxy_pass http://127.0.0.1:10083;
proxy_http_version 1.1;
# HLS也需要禁用缓冲
proxy_buffering off;
proxy_cache off;
proxy_request_buffering off;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# CORS支持
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers '*';
}
# MinIO使用外部服务不需要代理
# 错误页面配置
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# 日志配置
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}

View File

@@ -1,8 +1,8 @@
{
"name": "ruoyi",
"version": "3.8.9",
"description": "若依管理系统",
"author": "若依",
"description": "福安德视频监控分析",
"author": "KLP",
"license": "MIT",
"type": "module",
"scripts": {

BIN
rtsp-vue/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
// 根据文件名获取文件

View File

@@ -9,6 +9,14 @@ export function listAlarm(query) {
})
}
// 查询报警记录详细
export function getAlarm(alarmId) {
return request({
url: '/video/alarm/' + alarmId,
method: 'get'
})
}
// 处理报警记录
export function handleAlarm(data) {
return request({
@@ -23,6 +31,15 @@ export function batchHandleAlarm(data) {
return request({
url: '/video/alarm/batchHandle',
method: 'post',
params: data
data: data
})
}
// 导出报警记录
export function exportAlarm(query) {
return request({
url: '/video/alarm/export',
method: 'post',
params: query
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询巡检任务记录列表
export function listInspectionRecord(query) {
return request({
url: '/video/inspectionRecord/list',
method: 'get',
params: query
})
}
// 查询巡检任务记录详细
export function getInspectionRecord(recordId) {
return request({
url: '/video/inspectionRecord/' + recordId,
method: 'get'
})
}
// 新增巡检任务记录
export function addInspectionRecord(data) {
return request({
url: '/video/inspectionRecord',
method: 'post',
data: data
})
}
// 修改巡检任务记录
export function updateInspectionRecord(data) {
return request({
url: '/video/inspectionRecord',
method: 'put',
data: data
})
}
// 删除巡检任务记录
export function delInspectionRecord(recordId) {
return request({
url: '/video/inspectionRecord/' + recordId,
method: 'delete'
})
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询算法模型列表
export function listModel(query) {
return request({
url: '/video/model/list',
method: 'get',
params: query
})
}
// 查询算法模型详细
export function getModel(modelId) {
return request({
url: '/video/model/' + modelId,
method: 'get'
})
}
// 新增算法模型
export function addModel(data) {
return request({
url: '/video/model',
method: 'post',
data: data
})
}
// 修改算法模型
export function updateModel(data) {
return request({
url: '/video/model',
method: 'put',
data: data
})
}
// 删除算法模型
export function delModel(modelId) {
return request({
url: '/video/model/' + modelId,
method: 'delete'
})
}
// 启用或禁用算法模型
export function enableModel(modelId, enabled) {
return request({
url: '/video/model/' + modelId + '/enable',
method: 'put',
params: { enabled }
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -1,4 +1,5 @@
// cover some element-ui styles
@import './element/layout-compact.scss';
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
@@ -54,6 +55,10 @@
margin: 0 auto;
}
.el-select {
width: 180px;
}
// refine element ui upload
.upload-container {
.el-upload {
@@ -93,4 +98,5 @@
.el-dropdown .el-dropdown-link{
color: var(--el-color-primary) !important;
}
}

View File

@@ -0,0 +1,77 @@
// 定制element-ui组件的尺寸, 紧凑布局
:root {
--el-component-size: 25px !important;
--el-pagination-button-width: 25px !important;
--el-pagination-button-height: 25px !important;
}
.el-table {
.el-table__cell {
padding: 0 !important;
}
.el-table__header-wrapper th.el-table__cell {
padding: 0 !important;
height: 30px !important;
}
}
.el-button {
height: 25px !important;
font-size: 12px !important;
border-radius: 0;
&.is-circle {
width: 25px !important;
border-radius: 8px;
}
}
.el-form {
&--inline {
.el-form-item {
margin-right: 10px !important;
}
}
.el-form-item {
margin-bottom: 10px !important;
font-size: 12px !important;
}
}
.el-select {
.el-select__wrapper {
min-height: 25px !important;
}
}
.el-menu {
.el-menu-item {
height: 40px !important;
}
.el-sub-menu__title {
height: 40px !important;
}
}
.el-pagination {
.el-pager {
li {
height: 25px !important;
width: 25px !important;
min-height: 25px !important;
min-width: 25px !important;
padding: 0 !important;
}
}
.btn-prev, .btn-next {
height: 25px !important;
width: 25px !important;
min-height: 25px !important;
min-width: 25px !important;
padding: 0 !important;
}
}

View File

@@ -123,7 +123,7 @@ aside {
//main-container全局样式
.app-container {
padding: 20px;
padding: 15px;
}
.components-container {
@@ -132,7 +132,7 @@ aside {
}
.pagination-container {
margin-top: 30px;
margin-top: 10px;
}
.text-center {

View File

@@ -10,7 +10,7 @@ $panGreen: #30B08F;
// 默认主题变量
$menuText: #bfcbd9;
$menuActiveText: #409eff;
$menuActiveText: #2bf;
$menuBg: #304156;
$menuHover: #263445;
@@ -18,7 +18,7 @@ $menuHover: #263445;
$menuLightBg: #ffffff;
$menuLightHover: #f0f1f5;
$menuLightText: #303133;
$menuLightActiveText: #409EFF;
$menuLightActiveText: #2bf;
// 基础变量
$base-sidebar-width: 200px;
@@ -32,7 +32,7 @@ $base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
// 组件变量
$--color-primary: #409EFF;
$--color-primary: #2bf;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;

View File

@@ -62,6 +62,11 @@ const props = defineProps({
isShowTip: {
type: Boolean,
default: true
},
// 存储字段
storeField: {
type: String,
default: 'url'
}
});
@@ -139,7 +144,7 @@ function handleUploadError(err) {
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName });
uploadList.value.push({ name: res[props.storeField], url: res[props.storeField] });
uploadedSuccessfully();
} else {
number.value--;

View File

@@ -120,7 +120,7 @@ function checkboxChange(event, label) {
<style lang='scss' scoped>
:deep(.el-transfer__button) {
border-radius: 50%;
border-radius: 8px;
display: block;
margin-left: 0px;
}

View File

@@ -8,13 +8,17 @@
<template v-if="appStore.device !== 'mobile'">
<header-search id="header-search" class="right-menu-item" />
<el-tooltip content="源码地址" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip>
<div @click="router.push('/system/menu')" title="菜单管理" class="right-menu-item hover-effect">
<svg-icon icon-class="tree" />
</div>
<el-tooltip content="文档地址" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<div @click="router.push('/system/dict')" title="字典管理" class="right-menu-item hover-effect">
<svg-icon icon-class="dict" />
</div>
<div @click="router.push('/tool/gen')" title="代码生成" class="right-menu-item hover-effect">
<svg-icon icon-class="code" />
</div>
<screenfull id="screenfull" class="right-menu-item hover-effect" />
@@ -24,12 +28,8 @@
<svg-icon v-if="!settingsStore.isDark" icon-class="moon" />
</div>
</el-tooltip>
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<div class="avatar-container">
<!-- <div class="avatar-container">
<el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="userStore.avatar" class="user-avatar" />
@@ -49,7 +49,7 @@
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div> -->
</div>
</div>
</template>
@@ -60,13 +60,11 @@ import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
import router from '@/router'
const appStore = useAppStore()
const userStore = useUserStore()

View File

@@ -1,7 +1,7 @@
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg'
import defAva from '@/assets/images/avatar.png'
const useUserStore = defineStore(
'user',

View File

@@ -1,5 +1,5 @@
import axios from 'axios'
import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import { ElNotification, ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from '@/utils/ruoyi'
@@ -67,46 +67,53 @@ service.interceptors.request.use(config => {
}
return config
}, error => {
console.log(error)
Promise.reject(error)
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
// isRelogin.show = true;
// ElMessageBox.confirm(
// '登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示',
// { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }
// ).then(() => {
// isRelogin.show = false;
// useUserStore().logOut().then(() => {
// location.href = '/index';
// })
// }).catch(() => {
// isRelogin.show = false;
// });
// 直接重新登录
useUserStore().login({ username: 'admin', password: 'admin123' }).then(() => {
location.href = '/index';
})
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
useUserStore().logOut().then(() => {
location.href = '/index';
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
} else if (code !== 200) {
ElNotification.error({ title: msg })
return Promise.reject('error')
} else {
return Promise.resolve(res.data)
}
},
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
} else if (code !== 200) {
ElNotification.error({ title: msg })
return Promise.reject('error')
} else {
return Promise.resolve(res.data)
}
},
error => {
console.log('err' + error)
let { message } = error;

View File

@@ -1,197 +1,251 @@
<template>
<div class="app-container home">
<el-row :gutter="20">
<el-col :sm="24" :lg="12" style="padding-left: 20px">
<h2>RTSP视频分析平台</h2>
<p>
这是一个基于ruoyi-vue框架改进而来的RTSP视频分析平台项目集成了虹软SDK实现了人脸识别活体检测3D角度分析年龄及性别识别等功能同时利用JavaCV进行高效的视频处理后端采用SpringBoot框架前端则运用了Vue3框架确保系统的稳定与用户体验的流畅<br>
特别感谢ruoyi和EasyMedia两位开源项目的大佬他们的无私奉献为本项目奠定了坚实的基础
</p>
<p>
<b>当前版本:</b> <span>v1.0.0</span>
</p>
<p>
<el-tag type="danger">&yen;免费开源</el-tag>
</p>
<p>
<el-button
type="primary"
icon="Cloudy"
plain
@click="goTarget('https://gitee.com/xiaochemgzi')"
>访问码云</el-button
>
<el-button
icon="HomeFilled"
plain
@click="goTarget('https://gitee.com/xiaochemgzi')"
>访问码云主页</el-button
>
</p>
</el-col>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="设备类型" prop="type">
<el-radio-group type="button" v-model="queryParams.type" @change="handleQuery">
<el-radio-button :label="undefined">全部</el-radio-button>
<el-radio-button v-for="dict in device_type" :key="dict.value" :label="dict.value">{{ dict.label
}}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item style="float: right;">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-form-item>
</el-form>
<el-col :sm="24" :lg="12" style="padding-left: 50px">
<div style="display: flex; gap: 20px">
<div class="card" @click="handleVideoCameraFilled(item)" v-for="item in deviceList" :key="item.deviceId">
<el-row>
<el-col :span="12">
<h2>技术选型</h2>
<el-col :span="16">
<h3 style="display: flex; align-items: center;"><img :src="DeviceImage" alt="" style="width: 20px; height: 20px; margin-right: 10px;"> {{ item.deviceName }}</h3>
<div class="li" style="display: flex; align-items: center;">设备分类<dict-tag :options="device_type" :value="item.type" /></div>
<div class="li" style="display: flex; align-items: center;">设备IP{{ item.ip }}</div>
<div class="li" style="display: flex; align-items: center;">用户名{{ item.username ?? '-' }}</div>
</el-col>
<el-col :span="8">
<img :src="CameraImage" alt="设备图片" style="width: 100%;" />
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<h4>后端技术</h4>
<ul>
<li>SpringBoot</li>
<li>Spring Security</li>
<li>JWT</li>
<li>MyBatis</li>
<li>Druid</li>
<li>JavaCv</li>
<li>...</li>
</ul>
</el-col>
<el-col :span="6">
<h4>前端技术</h4>
<ul>
<li>Vue</li>
<li>Vite</li>
<li>Element-Plus</li>
<li>Axios</li>
<li>Sass</li>
<li>Xgplayer</li>
<li>...</li>
</ul>
</el-col>
</el-row>
</el-col>
</el-row>
<el-divider />
<!-- <div class="card-image">
<img :src="DeviceImage" :alt="item.url" style="width: 100%; height: 100%;" />
</div>
<div class="category"><dict-tag :options="device_type" :value="item.type" /></div>
<div class="heading">
{{ item.ip }}
</div> -->
</div>
</div>
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="12" :lg="8">
<el-card class="update-log">
<template v-slot:header>
<div class="clearfix">
<span>联系信息</span>
</div>
</template>
<div class="body">
<p>
<i class="el-icon-chat-dot-round"></i> 微信<a
href="javascript:;"
>chenbai0511</a
>
</p>
<el-col :span="12">
<div style="width: 100%; height: 100%;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3>告警记录 {{ alarmRecordList.length }}</h3>
<el-button type="primary" @click="handleAlarmRecord">查看更多</el-button>
</div>
</el-card>
<el-table :data="alarmRecordList" style="width: 100%">
<el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="报警类型" align="center" prop="alarmType" />
<!-- <el-table-column label="报警级别" align="center" prop="alarmLevel">
<template #default="scope">
<el-tag v-if="scope.row.alarmLevel === '1'" type="info"></el-tag>
<el-tag v-else-if="scope.row.alarmLevel === '2'" type="warning"></el-tag>
<el-tag v-else-if="scope.row.alarmLevel === '3'" type="danger"></el-tag>
</template>
</el-table-column> -->
<el-table-column label="置信度" align="center" prop="confidence">
<template #default="scope">
{{ (scope.row.confidence * 100).toFixed(1) }}%
</template>
</el-table-column>
</el-table>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="8">
<el-card class="update-log">
<template v-slot:header>
<div class="clearfix">
<span>更新日志</span>
</div>
</template>
<el-collapse accordion>
<el-collapse-item title="v1.0.0 - 2025-01-16">
<ol>
<li>RTSP视频分析平台正式发布</li>
<li>rtsp转http-flv和ws-flv</li>
<li>javaCV抽帧转换</li>
</ol>
</el-collapse-item>
</el-collapse>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="8">
<el-card class="update-log">
<template v-slot:header>
<div class="clearfix">
<span>捐赠支持</span>
</div>
</template>
<div class="body">
<img
src="@/assets/images/pay.png"
alt="donate"
style="width:100%"
/>
<span style="display: inline-block; height: 30px; line-height: 30px"
>你可以请作者喝杯咖啡表示鼓励</span
>
<el-col :span="12">
<div style="width: 100%; height: 100%;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3>巡检记录 {{ inspectionRecordList.length }}</h3>
<el-button type="primary" @click="handleInspectionRecord">查看更多</el-button>
</div>
</el-card>
<el-table :data="inspectionRecordList" style="width: 100%">
<el-table-column label="巡检任务" prop="taskName">
<template #default="scope">
{{ findTaskName(scope.row.taskId) }}
</template>
</el-table-column>
<el-table-column label="执行时间" prop="executeTime" />
<el-table-column label="执行时长" prop="duration" />
<el-table-column label="执行状态" prop="status">
<template #default="scope">
<dict-tag :options="ins_record_status" :value="scope.row.status" />
</template>
</el-table-column>
</el-table>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup name="Index">
const version = ref('3.8.9')
function goTarget(url) {
window.open(url, '__blank')
<script setup name="Device">
import router from '@/router'
import { listDevice } from "@/api/video/device";
import { listInspectionRecord } from "@/api/video/insRecord";
import { listInspection } from "@/api/video/inspection";
import { listAlarm } from "@/api/video/alarm";
import DeviceImage from "@/assets/images/device.png";
import CameraImage from "@/assets/images/camera.png";
const { proxy } = getCurrentInstance();
const { device_on_status, device_type, ins_record_status } = proxy.useDict('device_on_status', 'device_type', 'ins_record_status');
const inspectionRecordList = ref([]);
const inspectionList = ref([]);
const findTaskName = (taskId) => {
return inspectionList.value.find(item => item.taskId === taskId)?.taskName;
}
function getInspectionList() {
listInspection({
pageNum: 1,
pageSize: 10,
}).then(response => {
inspectionList.value = response.rows;
});
}
function getInspectionRecordList() {
listInspectionRecord({
pageNum: 1,
pageSize: 10,
}).then(response => {
inspectionRecordList.value = response.rows;
});
}
function handleInspectionRecord() {
router.push({ path: "/insrecord" });
}
getInspectionList();
getInspectionRecordList();
function handleAlarmRecord() {
router.push({ path: "/alarm" });
}
const alarmRecordList = ref([]);
function getAlarmRecordList() {
listAlarm({
pageNum: 1,
pageSize: 10,
}).then(response => {
alarmRecordList.value = response.rows;
});
}
getAlarmRecordList();
const deviceList = ref([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 20,
type: undefined,
},
});
const { queryParams } = toRefs(data);
const handleVideoCameraFilled = (row) => {
router.push({ path: "/video/flv", query: { deviceId: row.deviceId } });
}
/** 查询设备列表 */
function getList() {
loading.value = true;
listDevice(queryParams.value).then(response => {
deviceList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.deviceId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
getList();
</script>
<style scoped lang="scss">
.home {
blockquote {
padding: 10px 20px;
margin: 0 0 20px;
font-size: 17.5px;
border-left: 5px solid #eee;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.col-item {
margin-bottom: 20px;
}
<style scoped>
/* From Uiverse.io by alexmaracinaru */
.card {
width: 300px;
padding: .4em;
border-radius: 0;
border: 1px solid #e0e0e0;
}
ul {
padding: 0;
margin: 0;
}
.card-image {
width: 100%;
height: 130px;
text-align: center;
word-break: break-all;
}
font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
color: #676a6c;
overflow-x: hidden;
.card-image:hover {
transform: scale(0.98);
}
ul {
list-style-type: none;
}
.category {
text-transform: uppercase;
font-size: 0.7em;
font-weight: 600;
color: rgb(63, 121, 230);
padding: 10px 7px 0;
}
h4 {
margin-top: 0px;
}
.category:hover {
cursor: pointer;
}
h2 {
margin-top: 10px;
font-size: 26px;
font-weight: 100;
}
.heading {
font-weight: 600;
color: rgb(88, 87, 87);
padding: 7px;
}
p {
margin-top: 10px;
.heading:hover {
cursor: pointer;
}
b {
font-weight: 700;
}
}
.author {
color: gray;
font-weight: 400;
font-size: 11px;
padding-top: 20px;
}
.update-log {
ol {
display: block;
list-style-type: decimal;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 40px;
}
}
.name {
font-weight: 600;
}
.name:hover {
cursor: pointer;
}
</style>

View File

@@ -1,100 +1,15 @@
<template>
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">若依后台管理系统</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
size="large"
auto-complete="off"
placeholder="账号"
>
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
size="large"
auto-complete="off"
placeholder="密码"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="large"
type="primary"
style="width:100%;"
@click.prevent="handleLogin"
>
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span>
</div>
</div>
</template>
<script setup>
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
const loginForm = ref({
username: "admin",
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
});
const loginRules = {
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
};
const codeUrl = ref("");
const loading = ref(false);
// 验证码开关
const captchaEnabled = ref(true);
// 注册开关
const register = ref(false);
const redirect = ref(undefined);
watch(route, (newRoute) => {
@@ -102,126 +17,107 @@ watch(route, (newRoute) => {
}, { immediate: true });
function handleLogin() {
proxy.$refs.loginRef.validate(valid => {
if (valid) {
loading.value = true;
// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 });
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 });
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 });
} else {
// 否则移除
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
// 调用action的登录方法
userStore.login(loginForm.value).then(() => {
const query = route.query;
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== "redirect") {
acc[cur] = query[cur];
}
// 调用action的登录方法
userStore.login(loginForm.value).then(() => {
const query = route.query;
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== "redirect") {
acc[cur] = query[cur];
}
return acc;
}, {});
router.push({ path: redirect.value || "/", query: otherQueryParams });
}).catch(() => {
loading.value = false;
// 重新获取验证码
if (captchaEnabled.value) {
getCode();
}
});
}
});
return acc;
}, {});
router.push({ path: redirect.value || "/", query: otherQueryParams });
})
}
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
loginForm.value.uuid = res.uuid;
}
});
}
function getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
loginForm.value = {
username: username === undefined ? loginForm.value.username : username,
password: password === undefined ? loginForm.value.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};
}
getCode();
getCookie();
handleLogin();
</script>
<style lang='scss' scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
<template>
<!-- From Uiverse.io by Donewenfu -->
<div class="loader">
<div class="justify-content-center jimu-primary-loading"></div>
</div>
</template>
<style lang="scss" scoped>
/* From Uiverse.io by Donewenfu */
.loader {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
.jimu-primary-loading:before,
.jimu-primary-loading:after {
position: absolute;
top: 0;
content: '';
}
.jimu-primary-loading:before {
left: -19.992px;
}
.jimu-primary-loading:after {
left: 19.992px;
-webkit-animation-delay: 0.32s !important;
animation-delay: 0.32s !important;
}
.jimu-primary-loading:before,
.jimu-primary-loading:after,
.jimu-primary-loading {
background: #076fe5;
-webkit-animation: loading-keys-app-loading 0.8s infinite ease-in-out;
animation: loading-keys-app-loading 0.8s infinite ease-in-out;
width: 13.6px;
height: 32px;
}
.jimu-primary-loading {
text-indent: -9999em;
margin: auto;
position: absolute;
right: calc(50% - 6.8px);
top: calc(50% - 16px);
-webkit-animation-delay: 0.16s !important;
animation-delay: 0.16s !important;
}
@-webkit-keyframes loading-keys-app-loading {
0%,
80%,
100% {
opacity: .75;
box-shadow: 0 0 #076fe5;
height: 32px;
}
40% {
opacity: 1;
box-shadow: 0 -8px #076fe5;
height: 40px;
input {
height: 40px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 0px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 40px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
@keyframes loading-keys-app-loading {
0%,
80%,
100% {
opacity: .75;
box-shadow: 0 0 #076fe5;
height: 32px;
}
40% {
opacity: 1;
box-shadow: 0 -8px #076fe5;
height: 40px;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
.login-code-img {
height: 40px;
padding-left: 12px;
}
</style>

View File

@@ -308,7 +308,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
jobName: undefined,
jobGroup: undefined,
status: undefined

View File

@@ -191,7 +191,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
dictName: undefined,
dictType: undefined,
status: undefined

View File

@@ -144,7 +144,7 @@ const defaultSort = ref({ prop: "loginTime", order: "descending" });
// 查询参数
const queryParams = ref({
pageNum: 1,
pageSize: 10,
pageSize: 20,
ipaddr: undefined,
userName: undefined,
status: undefined,

View File

@@ -219,7 +219,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
operIp: undefined,
title: undefined,
operName: undefined,

View File

@@ -1,7 +1,7 @@
<template>
<div class="register">
<el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">若依后台管理系统</h3>
<h3 class="title">福安德视频监控分析</h3>
<el-form-item prop="username">
<el-input
v-model="registerForm.username"

View File

@@ -185,7 +185,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
configName: undefined,
configKey: undefined,
configType: undefined

View File

@@ -209,7 +209,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
dictType: undefined,
dictLabel: undefined,
status: undefined

View File

@@ -193,7 +193,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
dictName: undefined,
dictType: undefined,
status: undefined

View File

@@ -178,7 +178,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
noticeTitle: undefined,
createBy: undefined,
status: undefined

View File

@@ -164,7 +164,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
postCode: undefined,
postName: undefined,
status: undefined

View File

@@ -108,7 +108,7 @@ const userIds = ref([]);
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
pageSize: 20,
roleId: route.params.roleId,
userName: undefined,
phonenumber: undefined,

View File

@@ -282,7 +282,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
roleName: undefined,
roleKey: undefined,
status: undefined

View File

@@ -79,7 +79,7 @@ const userIds = ref([]);
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
pageSize: 20,
roleId: undefined,
userName: undefined,
phonenumber: undefined

View File

@@ -330,7 +330,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
userName: undefined,
phonenumber: undefined,
status: undefined,

View File

@@ -61,7 +61,7 @@ const { proxy } = getCurrentInstance();
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
pageSize: 20,
tableName: undefined,
tableComment: undefined
});

View File

@@ -183,7 +183,7 @@ const uniqueId = ref("");
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
tableName: undefined,
tableComment: undefined
},

View File

@@ -79,15 +79,14 @@
v-hasPermi="['video:alarm:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
<right-toolbar v-model="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="alarmList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="报警ID" align="center" prop="alarmId" />
<el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="报警类型" align="center" prop="alarmType" />
<!-- <el-table-column label="报警类型" align="center" prop="alarmType" /> -->
<el-table-column label="报警级别" align="center" prop="alarmLevel">
<template #default="scope">
<el-tag v-if="scope.row.alarmLevel === '1'" type="info"></el-tag>
@@ -109,6 +108,7 @@
:src="scope.row.imagePath"
:preview-src-list="[scope.row.imagePath]"
fit="cover"
preview-teleported
/>
</template>
</el-table-column>
@@ -132,26 +132,26 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="success"
icon="Check"
@click="handleProcess(scope.row, '1')"
v-hasPermi="['video:alarm:handle']"
<el-button
v-if="scope.row.handleStatus === '0'"
link
type="primary"
icon="Check"
@click="handleProcess(scope.row, '1')"
v-hasPermi="['video:alarm:handle']"
>处理</el-button>
<el-button
link
type="info"
icon="Close"
@click="handleProcess(scope.row, '2')"
v-hasPermi="['video:alarm:handle']"
<el-button
v-if="scope.row.handleStatus === '0'"
link
type="info"
icon="Close"
@click="handleProcess(scope.row, '2')"
v-hasPermi="['video:alarm:handle']"
>忽略</el-button>
<el-button
link
type="primary"
icon="View"
<el-button
link
type="primary"
icon="View"
@click="handleView(scope.row)"
>查看</el-button>
</template>
@@ -159,21 +159,17 @@
</el-table>
<pagination
v-show="total>0"
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:page="queryParams.pageNum"
:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 处理报警对话框 -->
<el-dialog :title="processTitle" v-model="processOpen" width="500px" append-to-body>
<el-form ref="processRef" :model="processForm" label-width="80px">
<el-form-item label="处理状态">
<el-tag v-if="processForm.handleStatus === '1'" type="success">已处理</el-tag>
<el-tag v-else-if="processForm.handleStatus === '2'" type="info">已忽略</el-tag>
</el-form-item>
<el-form-item label="处理备注">
<el-form-item label="处理备注" prop="handleRemark">
<el-input
v-model="processForm.handleRemark"
type="textarea"
@@ -190,29 +186,32 @@
</template>
</el-dialog>
<!-- 查看报警详情对话框 -->
<!-- 查看详情对话框 -->
<el-dialog title="报警详情" v-model="viewOpen" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="报警ID">{{ viewForm.alarmId }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ viewForm.taskName }}</el-descriptions-item>
<el-descriptions-item label="设备名称">{{ viewForm.deviceName }}</el-descriptions-item>
<el-descriptions-item label="报警类型">{{ viewForm.alarmType }}</el-descriptions-item>
<!-- <el-descriptions-item label="报警类型">{{ viewForm.alarmType }}</el-descriptions-item> -->
<el-descriptions-item label="报警级别">
<el-tag v-if="viewForm.alarmLevel === '1'" type="info"></el-tag>
<el-tag v-else-if="viewForm.alarmLevel === '2'" type="warning"></el-tag>
<el-tag v-else-if="viewForm.alarmLevel === '3'" type="danger"></el-tag>
</el-descriptions-item>
<el-descriptions-item label="置信度">{{ (viewForm.confidence * 100).toFixed(1) }}%</el-descriptions-item>
<el-descriptions-item label="报警时间" :span="2">{{ parseTime(viewForm.alarmTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="报警时间" :span="2">
{{ parseTime(viewForm.alarmTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</el-descriptions-item>
<el-descriptions-item label="报警描述" :span="2">{{ viewForm.alarmDesc }}</el-descriptions-item>
<el-descriptions-item label="处理状态">
<el-tag v-if="viewForm.handleStatus === '0'" type="danger">未处理</el-tag>
<el-tag v-else-if="viewForm.handleStatus === '1'" type="success">已处理</el-tag>
<el-tag v-else-if="viewForm.handleStatus === '2'" type="info">已忽略</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处理人">{{ viewForm.handleBy || '' }}</el-descriptions-item>
<el-descriptions-item label="处理时间" :span="2">{{ parseTime(viewForm.handleTime, '{y}-{m}-{d} {h}:{i}:{s}') || '无' }}</el-descriptions-item>
<el-descriptions-item label="处理备注" :span="2">{{ viewForm.handleRemark || '' }}</el-descriptions-item>
<el-descriptions-item label="处理人">{{ viewForm.handleBy || '-' }}</el-descriptions-item>
<el-descriptions-item label="处理时间" :span="2">
{{ viewForm.handleTime ? parseTime(viewForm.handleTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}
</el-descriptions-item>
<el-descriptions-item label="处理备注" :span="2">{{ viewForm.handleRemark || '-' }}</el-descriptions-item>
</el-descriptions>
<div v-if="viewForm.imagePath" style="margin-top: 20px;">
@@ -222,6 +221,7 @@
:src="viewForm.imagePath"
:preview-src-list="[viewForm.imagePath]"
fit="contain"
preview-teleported
/>
</div>
</el-dialog>
@@ -251,7 +251,7 @@ const viewForm = ref({});
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
taskName: null,
deviceName: null,
alarmType: null,

View File

@@ -35,97 +35,331 @@ export default {
methods: {
createPlayer(url, hasCloseBtn, index) {
if (!url) {
console.error('播放地址为空');
return;
}
if (this.player) {
console.log('播放器已存在,切换视频源');
this.changeVideo(url);
return;
}
console.log('创建播放器URL:', url, 'Element ID:', this.elId);
this.isPlay = true;
this.player = new FlvJsPlayer({
id: this.elId,
url: url,
// fitVideoSize: 'auto',
fluid: true,
autoplay: true,
isLive: true,
playsinline: false,
screenShot: true,
whitelist: [''],
ignores: ['time'],
closeVideoClick: true,
// errorTips: '<span class="app-error">无视频源</span>',
customConfig: {
isClickPlayBack: false
},
flvOptionalConfig: {
enableWorker: true,
enableStashBuffer: false, //关闭缓存
stashInitialSize: 2048, //缓存大小2m
lazyLoad: false,
lazyLoadMaxDuration: 40 * 60,
autoCleanupSourceBuffer: true,
autoCleanupMaxBackwardDuration: 35 * 60,
autoCleanupMinBackwardDuration: 30 * 60
} //flv.js可选配置项 [flv.js配置](https://github.com/bilibili/flv.js/blob/master/docs/api.md#config)
});
// 自定义播放器按钮
// let divStr =
// '<i class="btn-hover el-icon-camera button-screen-shot" style="font-size: 23px;margin-right: 10px;pointer-events: auto;"></i>' +
// '<i class="btn-hover el-icon-s-tools button-set" style="font-size: 23px;margin-right: 10px;pointer-events: auto;"></i>' +
// '<i class="btn-hover el-icon-video-camera-solid button-history" style="font-size: 23px;margin-right: 10px;pointer-events: auto;"></i>';
let divStr = '<i class="btn-hover el-icon-d-arrow-left button-playback" style="font-size: 23px;pointer-events: auto;"></i>';
let divClose = '<i @click="closePlayer" class="btn-hover el-icon-close app-close-btn-c"></i>';
let util = Player.util;
// let customBtn = util.createDom('div', divStr, {}, 'flex align-center justify-center app-player-button'); //'div', divStr, {}, 'class'
let customBtn = util.createDom(
'div',
divStr,
{ style: 'width: 40px;heigth:40px;position: absolute;right: 155px;top: 7px;' },
'flex align-center justify-center app-player-button'
); //'div', divStr, {}, 'class'
let closeBtn = util.createDom('div', divClose, {}, 'app-close-btn');
let xgControls = this.player.root.querySelector('xg-controls');
let xgError = this.player.root.querySelector('xg-error');
xgControls.appendChild(customBtn);
this.player.root.appendChild(closeBtn);
// let shot = customBtn.querySelector('.button-screen-shot');
// let set = customBtn.querySelector('.button-set');
// let history = customBtn.querySelector('.button-history');
let closeBtnc = closeBtn.querySelector('.app-close-btn-c');
let playback = customBtn.querySelector('.button-playback');
this.player.on('play', () => {});
this.player.on('focus', () => {
if (hasCloseBtn) {
closeBtn.style.display = 'block';
// 等待 DOM 元素渲染完成
this.$nextTick(() => {
const videoElement = document.getElementById(this.elId);
console.log('📺 DOM 元素:', videoElement);
if (!videoElement) {
console.error('❌ 找不到视频容器元素:', this.elId);
return;
}
});
this.player.on('ended', () => {});
this.player.on('blur', () => {
closeBtn.style.display = 'none';
});
this.player.on('error', () => {});
if (closeBtnc) {
closeBtnc.addEventListener('click', () => {
this.closePlayer();
this.player = new FlvJsPlayer({
id: this.elId,
url: url,
// fitVideoSize: 'auto',
fluid: true,
autoplay: true,
isLive: true,
playsinline: true,
screenShot: true,
whitelist: [''],
ignores: ['time'],
closeVideoClick: true,
// errorTips: '<span class="app-error">无视频源</span>',
customConfig: {
isClickPlayBack: false
},
flvOptionalConfig: {
enableWorker: false, // 禁用 Worker便于调试
enableStashBuffer: true, // ⚠️ 必须启用缓存,否则无法播放
stashInitialSize: 128, // 初始缓存大小 (KB)
isLive: true, // 明确标记为直播流
lazyLoad: false, // 禁用懒加载
lazyLoadMaxDuration: 3 * 60,
autoCleanupSourceBuffer: true, // 自动清理缓冲区
autoCleanupMaxBackwardDuration: 3 * 60,
autoCleanupMinBackwardDuration: 2 * 60,
fixAudioTimestampGap: true, // 修复音频时间戳间隙
// 关键:启用详细日志
enableLogging: true,
logLevel: 'debug'
} //flv.js可选配置项 [flv.js配置](https://github.com/bilibili/flv.js/blob/master/docs/api.md#config)
});
}
console.log('✅ 播放器对象已创建:', this.player);
console.log('🎬 播放器底层视频元素:', this.player.video);
console.log('🔍 播放器完整对象键:', Object.keys(this.player));
// 等待一下再尝试访问 flv 实例
setTimeout(() => {
// 尝试不同的方式访问 flv.js 实例
let flvInstance = null;
if (this.player.__flv__) {
flvInstance = this.player.__flv__;
console.log('📊 找到 flv.js 实例 (this.player.__flv__):', flvInstance);
} else if (this.player.flv) {
flvInstance = this.player.flv;
console.log('📊 找到 flv.js 实例 (this.player.flv):', flvInstance);
} else if (this.player._flv) {
flvInstance = this.player._flv;
console.log('📊 找到 flv.js 实例 (this.player._flv):', flvInstance);
} else if (this.player.video && this.player.video.flvjs) {
flvInstance = this.player.video.flvjs;
console.log('📊 找到 flv.js 实例 (this.player.video.flvjs):', flvInstance);
} else {
console.warn('⚠️ 未找到 flv.js 实例,尝试其他属性...');
console.log('播放器属性:', Object.keys(this.player).filter(k => k.toLowerCase().includes('flv')));
}
// 如果找到了 flv 实例,绑定事件
if (flvInstance && typeof flvInstance.on === 'function') {
console.log('✅ flv.js 实例已找到,开始绑定事件');
console.log('🔍 flv.js 配置:', flvInstance._config);
// 监听 flv.js 的统计信息
let statsCount = 0;
flvInstance.on('statistics_info', (stats) => {
statsCount++;
// 只输出前几次和有变化的统计
if (statsCount <= 3 || stats.decodedFrames > 0 || statsCount % 20 === 0) {
console.log('📈 flv.js 统计 #' + statsCount + ':', {
speed: stats.speed.toFixed(2) + ' KB/s',
videoBufferSize: stats.videoBufferSize,
audioBufferSize: stats.audioBufferSize,
decodedFrames: stats.decodedFrames,
droppedFrames: stats.droppedFrames
});
}
// 如果收到了很多数据但没有解码任何帧,说明格式有问题
if (statsCount > 10 && stats.decodedFrames === 0) {
console.error('❌ 已接收数据但无法解码!可能的原因:');
console.error(' 1. FLV 流缺少元数据onMetaData');
console.error(' 2. 视频编码格式不支持(需要 H.264');
console.error(' 3. FLV 封装格式错误');
console.error(' 建议:检查后端 FFmpeg 日志是否有错误');
}
});
// 监听 flv.js 的错误
flvInstance.on('error', (errorType, errorDetail, errorInfo) => {
console.error('💥 flv.js 错误:');
console.error(' 类型:', errorType);
console.error(' 详情:', errorDetail);
console.error(' 信息:', errorInfo);
// 常见错误处理提示
if (errorType === 'NetworkError') {
console.error(' => 网络错误,检查后端服务是否正常');
} else if (errorType === 'MediaError') {
console.error(' => 媒体解码错误,可能是视频格式不支持');
console.error(' => 请确认后端 FFmpeg 使用 H.264 (baseline) + AAC 编码');
}
});
// 监听元数据解析
flvInstance.on('metadata_arrived', (metadata) => {
console.log('📦 FLV 元数据:', metadata);
});
// 监听视频数据解析
flvInstance.on('scriptdata_arrived', (data) => {
console.log('📜 FLV 脚本数据:', data);
});
} else {
console.error('❌ flv.js 实例不可用或不支持事件监听');
}
}, 500); // 等待 500ms 让播放器完全初始化
// 监听播放器错误
this.player.on('error', (error) => {
console.error('❌ 播放器错误:', error);
console.error('错误详情:', JSON.stringify(error, null, 2));
console.error('播放器状态:', {
paused: this.player.video.paused,
currentTime: this.player.video.currentTime,
duration: this.player.video.duration,
readyState: this.player.video.readyState,
networkState: this.player.video.networkState,
buffered: this.player.video.buffered.length > 0 ?
`${this.player.video.buffered.start(0)} - ${this.player.video.buffered.end(0)}` : 'none'
});
});
// 监听播放器就绪
this.player.on('ready', () => {
console.log('✅ 播放器就绪');
});
// 监听播放开始
this.player.on('play', () => {
console.log('▶️ 开始播放');
});
// 监听播放暂停
this.player.on('pause', () => {
console.log('⏸️ 播放暂停');
});
// 监听视频加载开始
this.player.on('loadstart', () => {
console.log('🔄 开始加载视频数据');
});
// 监听视频元数据加载
this.player.on('loadedmetadata', () => {
console.log('📊 视频元数据已加载');
console.log('视频信息:', {
width: this.player.video.videoWidth,
height: this.player.video.videoHeight,
duration: this.player.video.duration
});
});
// 直接监听原生 video 元素事件
if (this.player.video) {
// 监听数据加载
this.player.video.addEventListener('loadeddata', () => {
console.log('✅ Video: 数据已加载(第一帧)');
});
// 监听可以播放
this.player.video.addEventListener('canplay', () => {
console.log('✅ Video: 可以播放');
});
// 监听可以流畅播放
this.player.video.addEventListener('canplaythrough', () => {
console.log('✅ Video: 可以流畅播放');
});
// 监听播放中
this.player.video.addEventListener('playing', () => {
console.log('▶️ Video: 正在播放');
});
// 监听暂停
this.player.video.addEventListener('pause', () => {
console.log('⏸️ Video: 已暂停');
});
// 监听停止
this.player.video.addEventListener('stalled', () => {
console.warn('⚠️ Video: 数据停滞');
});
// 监听缓冲
this.player.video.addEventListener('waiting', () => {
console.log('⏳ Video: 等待数据...');
});
// 监听时间更新(每秒触发多次)
let lastLog = 0;
this.player.video.addEventListener('timeupdate', () => {
const now = Date.now();
if (now - lastLog > 3000) { // 每3秒输出一次
console.log('⏱️ Video: 播放时间更新', this.player.video.currentTime.toFixed(2) + 's');
lastLog = now;
}
});
// 监听解码错误
this.player.video.addEventListener('error', (e) => {
console.error('❌ Video 元素错误:', e);
if (this.player.video.error) {
console.error('错误代码:', this.player.video.error.code);
console.error('错误信息:', this.player.video.error.message);
const errorCodes = {
1: 'MEDIA_ERR_ABORTED - 加载被中止',
2: 'MEDIA_ERR_NETWORK - 网络错误',
3: 'MEDIA_ERR_DECODE - 解码错误(视频格式不支持)',
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED - 源不支持'
};
console.error('错误说明:', errorCodes[this.player.video.error.code]);
}
});
}
// 监听视频可以播放
this.player.on('canplay', () => {
console.log('✅ 视频可以播放,尝试播放...');
// 确保播放
this.player.play().then(() => {
console.log('▶️ 播放已启动');
}).catch(err => {
console.error('播放失败:', err);
});
});
// 监听等待数据
this.player.on('waiting', () => {
console.log('⏳ 等待视频数据...');
});
// 监听数据接收
this.player.on('progress', () => {
console.log('📥 正在接收数据...');
});
// 监听播放失败
this.player.on('ended', () => {
console.log('⏹️ 播放结束');
});
// 输出播放器完整配置
console.log('🔧 播放器配置:', {
url: url,
elId: this.elId,
isLive: true,
autoplay: true
});
// 自定义播放器按钮(移到 $nextTick 内部,确保 player 已创建)
let divStr = '<i class="btn-hover el-icon-d-arrow-left button-playback" style="font-size: 23px;pointer-events: auto;"></i>';
let divClose = '<i class="btn-hover el-icon-close app-close-btn-c"></i>';
// 点击视频时间设置selectIndex
this.player.video.addEventListener('click', () => {
// this.selectIndex = index;
// this.$parent.setSelectIndex(index);
this.$emit('clickPlayer', index);
let util = Player.util;
let customBtn = util.createDom(
'div',
divStr,
{ style: 'width: 40px;heigth:40px;position: absolute;right: 155px;top: 7px;' },
'flex align-center justify-center app-player-button'
);
let closeBtn = util.createDom('div', divClose, {}, 'app-close-btn');
let xgControls = this.player.root.querySelector('xg-controls');
let xgError = this.player.root.querySelector('xg-error');
xgControls.appendChild(customBtn);
this.player.root.appendChild(closeBtn);
let closeBtnc = closeBtn.querySelector('.app-close-btn-c');
let playback = customBtn.querySelector('.button-playback');
this.player.on('focus', () => {
if (hasCloseBtn) {
closeBtn.style.display = 'block';
}
});
this.player.on('blur', () => {
closeBtn.style.display = 'none';
});
if (closeBtnc) {
closeBtnc.addEventListener('click', () => {
this.closePlayer();
});
}
// 点击视频时间设置selectIndex
this.player.video.addEventListener('click', () => {
this.$emit('clickPlayer', index);
});
});
// if (shot) {

View File

@@ -30,7 +30,7 @@
<el-row :gutter="10" >
<el-col :span="24">
<div @click="clickVideo(0)" class="selectVideo">
<CusPlayer @clickPlayer="clickPlayer" ref="video"></CusPlayer>
<JpegPlayer @clickPlayer="clickPlayer" ref="video"></JpegPlayer>
</div>
</el-col>
</el-row>
@@ -42,14 +42,15 @@
<script setup name="Flv">
import {useRoute} from 'vue-router'
import {ref, onMounted} from 'vue';
import {ref, onMounted, getCurrentInstance} from 'vue';
import 'xgplayer';
import FlvJsPlayer from 'xgplayer-flv.js';
import CusPlayer from './cusPlayer.vue';
import JpegPlayer from './jpegPlayer.vue';
import {getDevice} from "@/api/video/device";
const {proxy} = getCurrentInstance();
const {device_type} = proxy.useDict('device_type');
const { proxy } = getCurrentInstance();
const { device_type } = proxy.useDict('device_type');
const route = useRoute();
@@ -68,7 +69,11 @@ onMounted(() => {
const getDetails = async (deviceId) => {
const res = await getDevice(deviceId);
tableData.value = res.data;
playUrl.value = `http://localhost:8866/live?url=${tableData.value.url}`;
// 🔧 测试:使用 JavaCV 方案 + WebSocket推送 JPEG 帧)
// WebSocket 地址格式: ws://host:port/live?url=rtsp://...
const videoServerUrl = 'ws://49.232.154.205:10083';
playUrl.value = `${videoServerUrl}/live?url=${tableData.value.url}`;
console.log('📺 播放地址JavaCV WebSocket 方案):', playUrl.value);
handlePlay();
}

View File

@@ -0,0 +1,175 @@
<template>
<div style="position: absolute;height: 100%;width: 100%;display: flex;align-items: center;justify-content: center;">
<div v-show="isPlay" style="width: 100%; height: 100%; position: relative;">
<canvas ref="canvas" style="width: 100%; height: 100%; object-fit: contain;"></canvas>
<div class="stats" v-if="showStats">
FPS: {{ fps }} | 延迟: {{ latency }}ms | 帧数: {{ frameCount }}
</div>
</div>
<div v-show="!isPlay" style="color: #08979C;font-size: 25px;">暂无视频源</div>
</div>
</template>
<script>
export default {
name: 'JpegPlayer',
data() {
return {
isPlay: false,
ws: null,
canvas: null,
ctx: null,
frameCount: 0,
fps: 0,
latency: 0,
showStats: true,
lastFrameTime: 0,
fpsCounter: 0,
fpsTimer: null
};
},
mounted() {
this.canvas = this.$refs.canvas;
this.ctx = this.canvas.getContext('2d');
// 启动 FPS 计算定时器
this.fpsTimer = setInterval(() => {
this.fps = this.fpsCounter;
this.fpsCounter = 0;
}, 1000);
},
methods: {
createPlayer(url) {
if (!url) {
return;
}
// 检查是否是 WebSocket URL
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
return;
}
// 关闭旧连接
if (this.ws) {
this.ws.close();
}
this.isPlay = true;
this.frameCount = 0;
// 创建 WebSocket 连接
this.ws = new WebSocket(url);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
this.lastFrameTime = Date.now();
};
this.ws.onmessage = (event) => {
this.handleFrame(event.data);
};
this.ws.onerror = (error) => {
// 静默处理错误
};
this.ws.onclose = () => {
this.isPlay = false;
};
},
handleFrame(data) {
try {
this.frameCount++;
this.fpsCounter++;
// 计算延迟
const now = Date.now();
this.latency = now - this.lastFrameTime;
this.lastFrameTime = now;
// 使用 createImageBitmap 实现更快的解码(如果浏览器支持)
if (window.createImageBitmap) {
const blob = new Blob([data], { type: 'image/jpeg' });
createImageBitmap(blob).then(imageBitmap => {
// 只在尺寸变化时调整 canvas
if (this.canvas.width !== imageBitmap.width || this.canvas.height !== imageBitmap.height) {
this.canvas.width = imageBitmap.width;
this.canvas.height = imageBitmap.height;
}
// 直接绘制 imageBitmap比 Image 对象更快)
this.ctx.drawImage(imageBitmap, 0, 0);
imageBitmap.close(); // 释放内存
}).catch(() => {
// 静默处理错误
});
} else {
// 降级方案:使用传统的 Image 对象
const blob = new Blob([data], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
if (this.canvas.width !== img.width || this.canvas.height !== img.height) {
this.canvas.width = img.width;
this.canvas.height = img.height;
}
this.ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
};
img.onerror = () => {
URL.revokeObjectURL(url);
};
img.src = url;
}
} catch (error) {
// 静默处理错误
}
},
changeVideo(url) {
this.createPlayer(url);
},
closePlayer() {
this.isPlay = false;
if (this.ws) {
this.ws.close();
this.ws = null;
}
if (this.fpsTimer) {
clearInterval(this.fpsTimer);
this.fpsTimer = null;
}
if (this.ctx && this.canvas) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
},
beforeDestroy() {
this.closePlayer();
}
};
</script>
<style scoped>
.stats {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
color: #00ff00;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
z-index: 100;
}
</style>

View File

@@ -63,6 +63,7 @@
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="设备ID" align="center" prop="deviceId" />-->
<el-table-column label="IP地址" align="center" prop="ip" width="150" fixed />
<el-table-column label="设备名称" align="center" prop="deviceName" width="150" />
<el-table-column label="设备类型" align="center" prop="type" width="80">
<template #default="scope">
<dict-tag :options="device_type" :value="scope.row.type"/>
@@ -103,6 +104,9 @@
<el-form-item label="IP地址" prop="ip">
<el-input v-model="form.ip" placeholder="请输入IP地址" />
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="form.deviceName" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item label="设备类型" prop="type">
<el-select v-model="form.type" placeholder="请选择设备类型">
<el-option
@@ -172,7 +176,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
type: null,
},
rules: {
@@ -218,6 +222,7 @@ function reset() {
form.value = {
deviceId: null,
ip: null,
deviceName: null,
type: null,
userName: null,
password: null,

View File

@@ -126,7 +126,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
imageName: null,
imageData: null,
},

View File

@@ -2,25 +2,19 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="任务名称" prop="taskName">
<el-input
v-model="queryParams.taskName"
placeholder="请输入任务名称"
clearable
@keyup.enter="handleQuery"
/>
<el-input v-model="queryParams.taskName" placeholder="请输入任务名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input
v-model="queryParams.deviceName"
placeholder="请输入设备名称"
clearable
@keyup.enter="handleQuery"
/>
<el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="任务状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable>
<el-option label="启用" value="0" />
<el-option label="停用" value="1" />
<el-option v-for="dict in task_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="模型" prop="modelName">
<el-select v-model="queryParams.modelName" placeholder="请选择模型" clearable>
<el-option v-for="dict in py_model_manager" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
@@ -31,61 +25,45 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['video:inspection:add']"
>新增</el-button>
<el-button type="primary" plain icon="Plus" @click="handleAdd"
v-hasPermi="['video:inspection:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['video:inspection:edit']"
>修改</el-button>
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate"
v-hasPermi="['video:inspection:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['video:inspection:remove']"
>删除</el-button>
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete"
v-hasPermi="['video:inspection:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['video:inspection:export']"
>导出</el-button>
<el-button type="warning" plain icon="Download" @click="handleExport"
v-hasPermi="['video:inspection:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="inspectionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="任务ID" align="center" prop="taskId" />
<!-- <el-table-column label="任务ID" align="center" prop="taskId" /> -->
<el-table-column label="模型" align="center" prop="modelName">
<template #default="scope">
<dict-tag :options="py_model_manager" :value="scope.row.modelName" />
</template>
</el-table-column>
<el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="设备IP" align="center" prop="deviceIp" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="Cron表达式" align="center" prop="cronExpression" />
<el-table-column label="巡检时长(秒)" align="center" prop="duration" />
<el-table-column label="任务状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status"/>
<dict-tag :options="task_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="启用检测" align="center" prop="enableDetection">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.enableDetection"/>
<dict-tag :options="sys_normal_disable" :value="scope.row.enableDetection" />
</template>
</el-table-column>
<el-table-column label="检测阈值" align="center" prop="threshold" />
@@ -98,22 +76,24 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['video:inspection:edit']">修改</el-button>
<el-button link type="primary" icon="VideoPlay" @click="handleStart(scope.row)" v-hasPermi="['video:inspection:start']" v-if="scope.row.status === '1'">启动</el-button>
<el-button link type="warning" icon="VideoPause" @click="handleStop(scope.row)" v-hasPermi="['video:inspection:stop']" v-if="scope.row.status === '0'">停止</el-button>
<el-button link type="success" icon="CaretRight" @click="handleExecute(scope.row)" v-hasPermi="['video:inspection:execute']">执行</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['video:inspection:remove']">删除</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
v-hasPermi="['video:inspection:edit']">修改</el-button>
<el-button link type="primary" icon="VideoPlay" @click="handleStart(scope.row)"
v-hasPermi="['video:inspection:start']" v-if="scope.row.status === '1'">启动</el-button>
<el-button link type="warning" icon="VideoPause" @click="handleStop(scope.row)"
v-hasPermi="['video:inspection:stop']" v-if="scope.row.status === '0'">停止</el-button>
<el-button link type="success" icon="CaretRight" @click="handleExecute(scope.row)"
v-hasPermi="['video:inspection:execute']">执行</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
v-hasPermi="['video:inspection:remove']">删除</el-button>
<el-button link type="primary" icon="Document" @click="handleDetail(scope.row)"
v-hasPermi="['video:inspection:video']">详情</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
@pagination="getList" />
<!-- 添加或修改巡检任务对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
@@ -121,40 +101,43 @@
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="form.taskName" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="设备" prop="deviceId">
<el-select v-model="form.deviceId" placeholder="请选择设备" style="width: 100%">
<el-option
v-for="device in deviceList"
:key="device.deviceId"
:label="device.ip"
:value="device.deviceId"
/>
<el-form-item label="模型" prop="modelName">
<el-select v-model="form.modelName" placeholder="请选择模型" style="width: 100%">
<el-option v-for="dict in py_model_manager" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="Cron表达式" prop="cronExpression">
<el-input v-model="form.cronExpression" placeholder="请输入Cron表达式" />
<div class="el-form-item__tip">
例如0 0 8 * * ? 表示每天8点执行<br/>
0 0/30 * * * ? 表示每30分钟执行一次
</div>
<el-form-item label="设备" prop="deviceId">
<el-select v-model="form.deviceId" placeholder="请选择设备" style="width: 100%">
<el-option v-for="device in deviceList" :key="device.deviceId" :label="device.ip"
:value="device.deviceId" />
</el-select>
</el-form-item>
<el-form-item label="cron" prop="cronExpression">
<el-input v-model="form.cronExpression" placeholder="请输入cron执行表达式">
<template #append>
<el-button type="primary" @click="handleShowCron">
生成表达式
<i class="el-icon-time el-icon--right"></i>
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="巡检时长" prop="duration">
<el-input-number v-model="form.duration" :min="10" :max="3600" placeholder="巡检时长(秒)" />
</el-form-item>
<el-form-item label="任务状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="0">启用</el-radio>
<el-radio label="1">停用</el-radio>
<el-radio v-for="dict in task_status" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="启用检测" prop="enableDetection">
<el-radio-group v-model="form.enableDetection">
<el-radio label="0">启用</el-radio>
<el-radio label="1">停用</el-radio>
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label
}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="检测阈值" prop="threshold">
<el-input-number v-model="form.threshold" :min="0.1" :max="1.0" :step="0.1" placeholder="检测阈值" />
<el-input-number v-model="form.threshold" :min="0.01" :max="1.0" :step="0.01" placeholder="检测阈值" />
</el-form-item>
</el-form>
<template #footer>
@@ -164,15 +147,22 @@
</div>
</template>
</el-dialog>
<el-dialog title="Cron表达式生成器" v-model="openCron" append-to-body destroy-on-close>
<crontab ref="crontabRef" @hide="openCron = false" @fill="crontabFill" :expression="expression"></crontab>
</el-dialog>
</div>
</template>
<script setup name="Inspection">
import { listInspection, getInspection, delInspection, addInspection, updateInspection, startTask, stopTask, executeTask } from "@/api/video/inspection";
import { listDevice } from "@/api/video/device";
import Crontab from '@/components/Crontab'
import router from '@/router'
const { proxy } = getCurrentInstance();
const { sys_normal_disable } = proxy.useDict('sys_normal_disable');
const { sys_normal_disable, task_status, py_model_manager } = proxy.useDict('sys_normal_disable', 'task_status', 'py_model_manager');
const inspectionList = ref([]);
const deviceList = ref([]);
@@ -189,7 +179,7 @@ const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 20,
taskName: null,
deviceName: null,
status: null
@@ -290,6 +280,10 @@ function handleUpdate(row) {
});
}
function handleDetail(row) {
router.push({ path: "/insrecord", query: { taskId: row.taskId } });
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["inspectionRef"].validate(valid => {
@@ -314,41 +308,42 @@ function submitForm() {
/** 删除按钮操作 */
function handleDelete(row) {
const taskIds = row.taskId || ids.value;
proxy.$modal.confirm('是否确认删除巡检任务编号为"' + taskIds + '"的数据项?').then(function() {
proxy.$modal.confirm('是否确认删除巡检任务编号为"' + taskIds + '"的数据项?').then(function () {
return delInspection(taskIds);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}).catch(() => { });
}
/** 启动任务 */
function handleStart(row) {
proxy.$modal.confirm('是否确认启动任务"' + row.taskName + '"').then(function() {
proxy.$modal.confirm('是否确认启动任务"' + row.taskName + '"').then(function () {
console.log(row.taskId);
return startTask(row.taskId);
}).then(() => {
getList();
proxy.$modal.msgSuccess("启动成功");
}).catch(() => {});
}).catch(() => { });
}
/** 停止任务 */
function handleStop(row) {
proxy.$modal.confirm('是否确认停止任务"' + row.taskName + '"').then(function() {
proxy.$modal.confirm('是否确认停止任务"' + row.taskName + '"').then(function () {
return stopTask(row.taskId);
}).then(() => {
getList();
proxy.$modal.msgSuccess("停止成功");
}).catch(() => {});
}).catch(() => { });
}
/** 执行任务 */
function handleExecute(row) {
proxy.$modal.confirm('是否确认立即执行任务"' + row.taskName + '"').then(function() {
proxy.$modal.confirm('是否确认立即执行任务"' + row.taskName + '"').then(function () {
return executeTask(row.taskId);
}).then(() => {
proxy.$modal.msgSuccess("任务已提交执行");
}).catch(() => {});
}).catch(() => { });
}
/** 导出按钮操作 */
@@ -358,6 +353,20 @@ function handleExport() {
}, `inspection_${new Date().getTime()}.xlsx`)
}
/** cron表达式按钮操作 */
function handleShowCron() {
expression.value = form.value.cronExpression;
openCron.value = true;
}
/** 确定后回传值 */
function crontabFill(value) {
form.value.cronExpression = value;
}
const openCron = ref(false);
const expression = ref("");
onMounted(() => {
getList();
getDeviceList();

View File

@@ -0,0 +1,368 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="巡检任务" prop="taskId">
<el-select v-model="queryParams.taskId" placeholder="请选择巡检任务" clearable>
<el-option
v-for="item in inspectionList"
:key="item.taskId"
:label="item.taskName"
:value="item.taskId"
/>
</el-select>
</el-form-item>
<el-form-item label="执行状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择执行状态" clearable>
<el-option
v-for="dict in ins_record_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['video:inspectionRecord:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['video:inspectionRecord:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['video:inspectionRecord:remove']"
>删除</el-button>
</el-col> -->
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['video:inspectionRecord:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="inspectionRecordList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="巡检任务" align="center" prop="taskId">
<template #default="scope">
{{ findTaskName(scope.row.taskId) }}
</template>
</el-table-column>
<el-table-column label="执行时间" align="center" prop="executeTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.executeTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="执行时长" align="center" prop="duration" />
<el-table-column label="巡检结果" align="center" prop="result" :show-overflow-tooltip="true"></el-table-column>
<el-table-column label="执行状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="ins_record_status" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link icon="Document" @click="handleDetail(scope.row)">详情</el-button>
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['video:inspectionRecord:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['video:inspectionRecord:remove']">删除</el-button> -->
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改巡检任务记录对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="inspectionRecordRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="巡检任务" prop="taskId">
<el-select v-model="form.taskId" placeholder="请选择巡检任务">
<el-option
v-for="item in inspectionList"
:key="item.taskId"
:label="item.taskName"
:value="item.taskId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="执行时间" prop="executeTime">
<el-date-picker clearable
v-model="form.executeTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择执行时间">
</el-date-picker>
</el-form-item>
<el-form-item label="执行时长" prop="duration">
<el-input v-model="form.duration" placeholder="请输入执行时长" />
</el-form-item>
<el-form-item label="附件" prop="accessory">
<file-upload v-model="form.accessory" />
</el-form-item>
<el-form-item label="巡检结果" prop="result">
<el-input v-model="form.result" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="执行状态" prop="status">
<el-select v-model="form.status" placeholder="请选择执行状态">
<el-option
v-for="dict in ins_record_status"
:key="dict.value"
:label="dict.label"
:value="parseInt(dict.value)"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog :title="title" v-model="detailShow" width="600px" append-to-body>
<el-row>
<el-descriptions :column="3" border>
<el-descriptions-item label="巡检任务" prop="taskId">
{{ findTaskName(form.taskId) }}
</el-descriptions-item>
<el-descriptions-item label="执行时间" prop="executeTime">
{{ parseTime(form.executeTime, '{y}-{m}-{d}') }}
</el-descriptions-item>
<el-descriptions-item label="执行时长" prop="duration">
{{ form.duration }}
</el-descriptions-item>
</el-descriptions>
</el-row>
<el-row>
<div style="width: 100%; height: 100%;">
<h3>巡检结果</h3>
{{ form.result }}
</div>
</el-row>
<el-row>
<div style="width: 100%; height: 100%;">
<h3>附件</h3>
<video v-for="item in form.accessory.split(',')" :key="item" :src="item" controls style="width: 100%; height: 300px;"></video>
</div>
</el-row>
</el-dialog>
</div>
</template>
<script setup name="InspectionRecord">
import { listInspectionRecord, getInspectionRecord, delInspectionRecord, addInspectionRecord, updateInspectionRecord } from "@/api/video/insRecord";
import { listInspection } from "@/api/video/inspection";
const { proxy } = getCurrentInstance();
const { ins_record_status } = proxy.useDict('ins_record_status');
const route = useRoute();
const taskId = route.query.taskId;
const inspectionList = ref([]);
const inspectionRecordList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const detailShow = ref(false);
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 20,
taskId: taskId ? parseInt(taskId) : null,
executeTime: null,
duration: null,
accessory: null,
result: null,
status: null,
},
rules: {
}
});
const findTaskName = (taskId) => {
return inspectionList.value.find(item => item.taskId === taskId)?.taskName;
}
watch(() => taskId, (newVal) => {
if (newVal) {
queryParams.value.taskId = parseInt(newVal);
getList();
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询巡检任务记录列表 */
function getList() {
loading.value = true;
listInspectionRecord(queryParams.value).then(response => {
inspectionRecordList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
// 表单重置
function reset() {
form.value = {
recordId: null,
taskId: null,
executeTime: null,
duration: null,
accessory: null,
result: null,
status: null,
createBy: null,
updateBy: null,
createTime: null,
updateTime: null,
delFlag: null,
remark: null
};
proxy.resetForm("inspectionRecordRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
function getInspectionList() {
listInspection({ pageNum: 1, pageSize: 1000 }).then(response => {
inspectionList.value = response.rows;
});
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.recordId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加巡检任务记录";
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const _recordId = row.recordId || ids.value
getInspectionRecord(_recordId).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改巡检任务记录";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["inspectionRecordRef"].validate(valid => {
if (valid) {
if (form.value.recordId != null) {
updateInspectionRecord(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addInspectionRecord(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _recordIds = row.recordId || ids.value;
proxy.$modal.confirm('是否确认删除巡检任务记录编号为"' + _recordIds + '"的数据项?').then(function() {
return delInspectionRecord(_recordIds);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('video/inspectionRecord/export', {
...queryParams.value
}, `inspectionRecord_${new Date().getTime()}.xlsx`)
}
function handleDetail(row) {
form.value = row;
detailShow.value = true;
}
getInspectionList();
getList();
</script>

View File

@@ -0,0 +1,323 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-form-item label="模型简称" prop="dictLabel">
<el-input
v-model="queryParams.dictLabel"
placeholder="请输入模型简称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="数据状态" clearable style="width: 200px">
<el-option
v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['system:dict:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['system:dict:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:dict:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['system:dict:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="模型简称" align="center" prop="dictLabel">
<template #default="scope">
<span v-if="(scope.row.listClass == '' || scope.row.listClass == 'default') && (scope.row.cssClass == '' || scope.row.cssClass == null)">{{ scope.row.dictLabel }}</span>
<el-tag v-else :type="scope.row.listClass == 'primary' ? '' : scope.row.listClass" :class="scope.row.cssClass">{{ scope.row.dictLabel }}</el-tag>
</template>
</el-table-column>
<el-table-column label="模型名称" align="center" prop="dictValue" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="dataRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="模型简称" prop="dictLabel">
<el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
</el-form-item>
<el-form-item label="模型名称" prop="dictValue">
<el-input v-model="form.dictValue" placeholder="请输入模型名称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Data">
import useDictStore from '@/store/modules/dict'
import { optionselect as getDictOptionselect, getType } from "@/api/system/dict/type";
import { listData, getData, delData, addData, updateData } from "@/api/system/dict/data";
const { proxy } = getCurrentInstance();
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const dataList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const defaultDictType = ref("");
const typeOptions = ref([]);
const route = useRoute();
// 数据标签回显样式
const listClassOptions = ref([
{ value: "default", label: "默认" },
{ value: "primary", label: "主要" },
{ value: "success", label: "成功" },
{ value: "info", label: "信息" },
{ value: "warning", label: "警告" },
{ value: "danger", label: "危险" }
]);
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 20,
dictType: undefined,
dictLabel: undefined,
status: undefined
},
rules: {
dictLabel: [{ required: true, message: "数据标签不能为空", trigger: "blur" }],
dictValue: [{ required: true, message: "数据键值不能为空", trigger: "blur" }],
dictSort: [{ required: true, message: "数据顺序不能为空", trigger: "blur" }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询字典类型详细 */
function getTypes(dictId) {
getType(dictId).then(response => {
queryParams.value.dictType = response.data.dictType;
defaultDictType.value = response.data.dictType;
getList();
});
}
/** 查询字典类型列表 */
function getTypeList() {
getDictOptionselect().then(response => {
typeOptions.value = response.data;
});
}
/** 查询字典数据列表 */
function getList() {
loading.value = true;
listData(queryParams.value).then(response => {
dataList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
/** 取消按钮 */
function cancel() {
open.value = false;
reset();
}
/** 表单重置 */
function reset() {
form.value = {
dictCode: undefined,
dictLabel: undefined,
dictValue: undefined,
cssClass: undefined,
listClass: "default",
dictSort: 0,
status: "0",
remark: undefined
};
proxy.resetForm("dataRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 返回按钮操作 */
function handleClose() {
const obj = { path: "/system/dict" };
proxy.$tab.closeOpenPage(obj);
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
queryParams.value.dictType = defaultDictType.value;
handleQuery();
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加字典数据";
form.value.dictType = queryParams.value.dictType;
}
/** 多选框选中数据 */
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.dictCode);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const dictCode = row.dictCode || ids.value;
getData(dictCode).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改字典数据";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["dataRef"].validate(valid => {
if (valid) {
if (form.value.dictCode != undefined) {
updateData(form.value).then(response => {
useDictStore().removeDict(queryParams.value.dictType);
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addData(form.value).then(response => {
useDictStore().removeDict(queryParams.value.dictType);
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const dictCodes = row.dictCode || ids.value;
proxy.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?').then(function() {
return delData(dictCodes);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
useDictStore().removeDict(queryParams.value.dictType);
}).catch(() => {});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download("system/dict/data/export", {
...queryParams.value
}, `dict_data_${new Date().getTime()}.xlsx`);
}
getTypes(105);
getTypeList();
</script>

View File

@@ -0,0 +1,309 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="75px">
<el-form-item label="模型名称" prop="modelName">
<el-input
v-model="queryParams.modelName"
placeholder="请输入模型名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="框架/格式" prop="framework">
<el-input
v-model="queryParams.framework"
placeholder="请输入框架/格式,如 onnx"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['video:model:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['video:model:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['video:model:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['video:model:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="modelList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="modelId" />
<el-table-column label="模型名称" align="center" prop="modelName" />
<el-table-column label="模型版本" align="center" prop="version" />
<el-table-column label="框架/格式" align="center" prop="framework" />
<el-table-column label="OSS模型" align="center" prop="url" />
<el-table-column label="文件大小" align="center" prop="fileSize" />
<el-table-column label="文件校验" align="center" prop="checksum" />
<el-table-column label="是否可用" align="center" prop="enabled">
<template #default="scope">
<dict-tag :options="common_switch_1" :value="scope.row.enabled"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Check" @click="handleEnable(scope.row)" v-if="scope.row.enabled == 0">启用</el-button>
<el-button link type="primary" icon="Close" @click="handleEnable(scope.row)" v-if="scope.row.enabled == 1">禁用</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改算法模型对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="modelRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="模型名称" prop="modelName">
<el-input v-model="form.modelName" placeholder="请输入模型名称" />
</el-form-item>
<el-form-item label="模型版本" prop="version">
<el-input v-model="form.version" placeholder="请输入模型版本" />
</el-form-item>
<el-form-item label="框架/格式" prop="framework">
<el-input v-model="form.framework" placeholder="请输入框架/格式,如 onnx" />
</el-form-item>
<el-form-item label="OSS模型" prop="url">
<!-- <el-input v-model="form.url" type="textarea" placeholder="请输入内容" /> -->
<file-upload v-model="form.url" />
</el-form-item>
<!-- <el-form-item label="文件大小" prop="fileSize">
<el-input v-model="form.fileSize" placeholder="请输入文件大小" />
</el-form-item>
<el-form-item label="文件校验" prop="checksum">
<el-input v-model="form.checksum" placeholder="请输入文件校验" />
</el-form-item> -->
<!-- <el-form-item label="是否可用" prop="canuse">
<el-radio-group v-model="form.canuse">
<el-radio
v-for="dict in common_switch_1"
:key="dict.value"
:label="parseInt(dict.value)"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Model">
import { listModel, getModel, delModel, addModel, updateModel, enableModel } from "@/api/video/model";
const { proxy } = getCurrentInstance();
const { common_switch_1 } = proxy.useDict('common_switch_1');
const modelList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 20,
modelName: null,
framework: null,
},
rules: {
modelName: [
{ required: true, message: "模型名称不能为空", trigger: "blur" }
],
url: [
{ required: true, message: "OSS模型URL不能为空", trigger: "blur" }
],
canuse: [
{ required: true, message: "是否可用 1是 0否不能为空", trigger: "change" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询算法模型列表 */
function getList() {
loading.value = true;
listModel(queryParams.value).then(response => {
modelList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
function handleEnable(row) {
enableModel(row.modelId, row.enabled == 0 ? 1 : 0).then(response => {
proxy.$modal.msgSuccess(row.enabled == 0 ? "启用成功" : "禁用成功");
getList();
});
}
// 表单重置
function reset() {
form.value = {
modelId: null,
modelName: null,
version: null,
framework: null,
url: null,
fileSize: null,
checksum: null,
canuse: '1',
createBy: null,
createTime: null,
updateBy: null,
updateTime: null,
remark: null,
delFlag: null
};
proxy.resetForm("modelRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.modelId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加算法模型";
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const _modelId = row.modelId || ids.value
getModel(_modelId).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改算法模型";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["modelRef"].validate(valid => {
if (valid) {
const payload = {
...form.value,
enabled: form.value.canuse
}
if (form.value.modelId != null) {
updateModel(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addModel(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _modelIds = row.modelId || ids.value;
proxy.$modal.confirm('是否确认删除算法模型编号为"' + _modelIds + '"的数据项?').then(function() {
return delModel(_modelIds);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('video/model/export', {
...queryParams.value
}, `model_${new Date().getTime()}.xlsx`)
}
getList();
</script>

View File

@@ -25,15 +25,32 @@ export default defineConfig(({ mode, command }) => {
},
// vite 相关配置
server: {
port: 1024,
port: 80,
host: true,
open: true,
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
target: 'http://localhost:8080',
target: 'http://49.232.154.205:10082',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
},
// 视频流代理HTTP-FLV
'/live': {
target: 'http://localhost:10083/live',
changeOrigin: true,
// 关键:禁用所有缓冲,实时转发
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('🎬 代理视频流请求:', req.url);
});
proxy.on('proxyRes', (proxyRes, req, res) => {
console.log('📺 收到视频流响应,开始转发...');
// 禁用缓冲
proxyRes.headers['x-accel-buffering'] = 'no';
proxyRes.headers['cache-control'] = 'no-cache';
});
}
}
}
},

69
ruoyi-admin/Dockerfile Normal file
View File

@@ -0,0 +1,69 @@
# 构建阶段
FROM maven:3.8-eclipse-temurin-17 AS builder
# 设置工作目录
WORKDIR /build
# 复制pom文件和源代码
COPY pom.xml .
COPY ruoyi-admin/pom.xml ./ruoyi-admin/
COPY ruoyi-common/pom.xml ./ruoyi-common/
COPY ruoyi-framework/pom.xml ./ruoyi-framework/
COPY ruoyi-generator/pom.xml ./ruoyi-generator/
COPY ruoyi-quartz/pom.xml ./ruoyi-quartz/
COPY ruoyi-system/pom.xml ./ruoyi-system/
COPY ruoyi-video/pom.xml ./ruoyi-video/
COPY ruoyi-admin/src ./ruoyi-admin/src
COPY ruoyi-common/src ./ruoyi-common/src
COPY ruoyi-framework/src ./ruoyi-framework/src
COPY ruoyi-generator/src ./ruoyi-generator/src
COPY ruoyi-quartz/src ./ruoyi-quartz/src
COPY ruoyi-system/src ./ruoyi-system/src
COPY ruoyi-video/src ./ruoyi-video/src
# 构建项目跳过测试和enforcer检查以避免依赖冲突
RUN mvn clean package -DskipTests -Denforcer.skip=true -pl ruoyi-admin -am
# 运行阶段 - 使用Debian而不是Alpine以支持JavaCV/FFmpeg
FROM eclipse-temurin:17-jre
# 安装必要的工具和JavaCV/FFmpeg完整依赖包含 libx264
RUN apt-get update && apt-get install -y \
curl \
libgomp1 \
libva-drm2 \
libva2 \
libxcb-shm0 \
libxcb-shape0 \
libxcb1 \
libx11-6 \
libx11-xcb1 \
libasound2-dev \
ffmpeg \
libx264-dev \
libavcodec-extra \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 创建日志和上传目录
RUN mkdir -p /app/logs /app/upload
# 复制jar包和配置文件
COPY --from=builder /build/ruoyi-admin/target/ruoyi-admin.jar /app/app.jar
# 设置环境变量
ENV JAVA_OPTS="-Xms512m -Xmx1024m -Djava.security.egd=file:/dev/./urandom"
# 暴露端口(仅内部使用)
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=60s \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 启动应用
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app/app.jar"]

View File

@@ -30,6 +30,12 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- spring-boot-actuator for health check -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Mysql驱动包 -->
<dependency>
<groupId>com.mysql</groupId>
@@ -64,7 +70,11 @@
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>

View File

@@ -20,6 +20,11 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.framework.config.ServerConfig;
import com.ruoyi.framework.config.MinioProperties;
import com.ruoyi.framework.service.MinioService;
import com.ruoyi.video.service.IVMinioObjectService;
import com.ruoyi.video.domain.VMinioObject;
import com.ruoyi.common.utils.SecurityUtils;
/**
* 通用请求处理
@@ -35,6 +40,15 @@ public class CommonController
@Autowired
private ServerConfig serverConfig;
@Autowired(required = false)
private MinioService minioService;
@Autowired(required = false)
private MinioProperties minioProperties;
@Autowired
private IVMinioObjectService vMinioObjectService;
private static final String FILE_DELIMETER = ",";
/**
@@ -77,9 +91,31 @@ public class CommonController
{
try
{
// 上传文件路径
if (minioProperties != null && minioProperties.isEnabled())
{
// 先生成唯一文件名(含扩展名),再用该名称上传
String uniqueObjectName = FileUploadUtils.extractFilename(file);
MinioService.UploadResult result = minioService.uploadWithName(file, uniqueObjectName);
String fileName = result.getObjectName(); // 即 uniqueObjectName
String url = result.getUrl();
// persist to v_minio_object
VMinioObject record = new VMinioObject();
record.setObjectName(fileName);
record.setUrl(url);
record.setOriginalName(file.getOriginalFilename());
try { record.setCreateBy(SecurityUtils.getUsername()); } catch (Exception ignored) {}
vMinioObjectService.insert(record);
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
// 本地上传路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称
// 本地上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
AjaxResult ajax = AjaxResult.success();
@@ -103,21 +139,45 @@ public class CommonController
{
try
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
List<String> urls = new ArrayList<String>();
List<String> fileNames = new ArrayList<String>();
List<String> newFileNames = new ArrayList<String>();
List<String> originalFilenames = new ArrayList<String>();
for (MultipartFile file : files)
if (minioProperties != null && minioProperties.isEnabled())
{
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
urls.add(url);
fileNames.add(fileName);
newFileNames.add(FileUtils.getName(fileName));
originalFilenames.add(file.getOriginalFilename());
for (MultipartFile file : files)
{
String uniqueObjectName = FileUploadUtils.extractFilename(file);
MinioService.UploadResult result = minioService.uploadWithName(file, uniqueObjectName);
String fileName = result.getObjectName(); // 即 uniqueObjectName
String url = result.getUrl();
urls.add(url);
fileNames.add(fileName);
newFileNames.add(FileUtils.getName(fileName));
originalFilenames.add(file.getOriginalFilename());
// persist each to v_minio_object
VMinioObject record = new VMinioObject();
record.setObjectName(fileName);
record.setUrl(url);
record.setOriginalName(file.getOriginalFilename());
try { record.setCreateBy(SecurityUtils.getUsername()); } catch (Exception ignored) {}
vMinioObjectService.insert(record);
}
}
else
{
// 本地上传路径
String filePath = RuoYiConfig.getUploadPath();
for (MultipartFile file : files)
{
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
urls.add(url);
fileNames.add(fileName);
newFileNames.add(FileUtils.getName(fileName));
originalFilenames.add(file.getOriginalFilename());
}
}
AjaxResult ajax = AjaxResult.success();
ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));

View File

@@ -3,6 +3,7 @@ package com.ruoyi.web.controller.video;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -33,11 +34,13 @@ import com.ruoyi.common.core.page.TableDataInfo;
public class DeviceController extends BaseController {
@Autowired
private IDeviceService deviceService;
@Value("${server.port:8080}")
private String serverPort;
/**
* 查询设备列表
*/
@PreAuthorize("@ss.hasPermi('video:device:list')")
@GetMapping("/list")
public TableDataInfo list(Device device) {
startPage();
@@ -48,7 +51,6 @@ public class DeviceController extends BaseController {
/**
* 导出设备列表
*/
@PreAuthorize("@ss.hasPermi('video:device:export')")
@Log(title = "设备", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, Device device) {
@@ -60,7 +62,6 @@ public class DeviceController extends BaseController {
/**
* 获取设备详细信息
*/
@PreAuthorize("@ss.hasPermi('video:device:query')")
@GetMapping(value = "/{deviceId}")
public AjaxResult getInfo(@PathVariable("deviceId") Long deviceId) {
return success(deviceService.selectDeviceByDeviceId(deviceId));
@@ -69,7 +70,6 @@ public class DeviceController extends BaseController {
/**
* 新增设备
*/
@PreAuthorize("@ss.hasPermi('video:device:add')")
@Log(title = "设备", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody Device device) {
@@ -79,7 +79,6 @@ public class DeviceController extends BaseController {
/**
* 修改设备
*/
@PreAuthorize("@ss.hasPermi('video:device:edit')")
@Log(title = "设备", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody Device device) {
@@ -89,7 +88,6 @@ public class DeviceController extends BaseController {
/**
* 删除设备
*/
@PreAuthorize("@ss.hasPermi('video:device:remove')")
@Log(title = "设备", businessType = BusinessType.DELETE)
@DeleteMapping("/{deviceIds}")
public AjaxResult remove(@PathVariable Long[] deviceIds) {

View File

@@ -4,11 +4,11 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
# 主库数据源 - 支持环境变量配置
master:
url: jdbc:mysql://140.143.206.120:3306/fad_watch?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: klp
password: KeLunPu123@
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true}
username: ${SPRING_DATASOURCE_USERNAME:root}
password: ${SPRING_DATASOURCE_PASSWORD:password}
# 从库数据源
slave:
# 从数据源开关/默认关闭

View File

@@ -7,7 +7,7 @@ ruoyi:
# 版权年份
copyrightYear: 2025
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: /home/wangyu/uploadPath
profile: D:\temp
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
@@ -15,8 +15,10 @@ ruoyi:
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
# 服务器的HTTP端口支持环境变量配置
port: ${SERVER_PORT:8080}
# 监听地址,支持环境变量配置
address: ${SERVER_ADDRESS:0.0.0.0}
servlet:
# 应用的访问路径
context-path: /
@@ -68,14 +70,14 @@ spring:
data:
# redis 配置
redis:
# 地址
host: localhost
# 地址 - 支持环境变量Docker环境使用容器名
host: ${SPRING_DATA_REDIS_HOST:localhost}
# 端口默认为6379
port: 6379
port: ${SPRING_DATA_REDIS_PORT:6379}
# 数据库索引
database: 0
# 密码
password:
password: ${SPRING_DATA_REDIS_PASSWORD:}
# 连接超时时间
timeout: 10s
lettuce:
@@ -127,6 +129,14 @@ springdoc:
paths-to-match: '/**'
packages-to-scan: com.ruoyi.web.controller.tool
# MinIO配置 - 支持环境变量配置
minio:
enabled: ${MINIO_ENABLED:true}
endpoint: ${MINIO_ENDPOINT:http://49.232.154.205:10900}
access-key: ${MINIO_ACCESS_KEY:4EsLD9g9OM09DT0HaBKj}
secret-key: ${MINIO_SECRET_KEY:05SFC5fleqTnaLRYBrxHiphMFYbGX5nYicj0WCHA}
bucket: ${MINIO_BUCKET:rtsp}
# 防止XSS攻击
xss:
# 过滤开关
@@ -138,7 +148,9 @@ xss:
# 流媒体服务端口
mediasServer:
port: 8866
port: ${MEDIA_SERVER_PORT:10083}
# 监听地址支持环境变量配置0.0.0.0 允许外网直接访问)
address: ${MEDIA_SERVER_ADDRESS:0.0.0.0}
# 网络超时15秒
netTimeout: 15000000
# 读写超时15秒
@@ -148,10 +160,6 @@ mediasServer:
# 无人拉流观看持续多久自动关闭1分钟
noClientsDuration: 60000
# 虹软sdk
arcFace:
appId: '替换成你的appId'
sdkKey: '替换成你的sdkKey'
# 视频分析配置
video:
@@ -166,3 +174,17 @@ video:
transport: tcp
# 重试次数
retryCount: 3
# Spring Boot Actuator配置
management:
endpoints:
web:
exposure:
include: health,info
base-path: /actuator
endpoint:
health:
show-details: when-authorized
health:
defaults:
enabled: true

View File

@@ -0,0 +1 @@
trash

Some files were not shown because too many files have changed in this diff Show More