Compare commits
125 Commits
1ae7d35efc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f064c2e3e | ||
| 98c2aeaa9b | |||
| 98042b237c | |||
|
|
bf0996d750 | ||
|
|
3abac5ff1b | ||
| 99a8a943bc | |||
| f0b4c5a8bf | |||
|
|
f40d6ffcb6 | ||
|
|
524c8343e6 | ||
| aa32f9e9ac | |||
| 5f6058c024 | |||
| e3701991ef | |||
| 7096359434 | |||
|
|
4cec966613 | ||
| 1a7ecafc7d | |||
| 735704d585 | |||
|
|
c32385e87d | ||
|
|
3cf64fd02f | ||
| 4a34892ea9 | |||
| 9b8cf2509d | |||
| 53d60c1016 | |||
|
|
44182e6a74 | ||
|
|
f80035d32a | ||
| c7345ccfca | |||
| e657aab730 | |||
|
|
9fd1832ce9 | ||
|
|
7e0e55944e | ||
| eb3a50f89d | |||
| 018a050e52 | |||
| 0b71f7018a | |||
| 501a376f98 | |||
|
|
0e4f543434 | ||
| f4418b8860 | |||
| c1e116f65d | |||
| c63d6ffb3d | |||
| dfc0baff58 | |||
| 7a31bf7c2a | |||
| 1deafab5b3 | |||
| 89535c9a5c | |||
| b43cf4bc5e | |||
| 204908b580 | |||
| 8976a70a21 | |||
| 4bbbff266d | |||
| 2a689d6e5c | |||
| 89a8526e5d | |||
| 03397d1c4a | |||
| 694ea5b1af | |||
| a3e0a45b9c | |||
| c730334f8d | |||
| 182f045517 | |||
| e462a99645 | |||
| cfd0489d3d | |||
| ca021cdcce | |||
| e6941d5ae0 | |||
| 3050496a83 | |||
| 65701395b9 | |||
| 384084ba36 | |||
| 144fa7b423 | |||
| fc88a11af3 | |||
| 20f0481f3a | |||
| dcd905bfde | |||
| befad83e13 | |||
| 863c191521 | |||
| 34fce9e552 | |||
| a917bbc936 | |||
| 5e54e4ae62 | |||
| b4e8cf5c33 | |||
| 9e36a84354 | |||
| 6dba13713f | |||
| 1f22d777e9 | |||
| e77a2f7cff | |||
| 8f4e25e895 | |||
| 5e5d1acaaa | |||
| 8c3431cf2c | |||
| 95ab923d1a | |||
| 3150e504b7 | |||
| 1323c1f717 | |||
| 0f7dc58a50 | |||
| 7e7818b7d3 | |||
| 730f508ff8 | |||
| 34a4c7912c | |||
| 197ff9b888 | |||
| e82d919ebf | |||
| 62d9c0ffb5 | |||
| d32e3c4040 | |||
| bdf33e28e6 | |||
| e0f2b8bf35 | |||
| 39d39a7a24 | |||
|
|
049c683814 | ||
|
|
73fc27f91f | ||
| 3fe5f8083d | |||
| c15b9a06c9 | |||
|
|
214f8d1f65 | ||
|
|
0e5a153e4e | ||
| c24c5f5f21 | |||
| fe422241c8 | |||
| c63e130729 | |||
| 9508468265 | |||
| 4711883466 | |||
| 02c2a25c36 | |||
| f09d814a70 | |||
|
|
fa1568e3dc | ||
|
|
3cc63e031e | ||
|
|
427bf937ef | ||
| bd92a0b317 | |||
| 6d60be06aa | |||
|
|
79f0ab66c3 | ||
|
|
89be8981ff | ||
| 9a682f4ff2 | |||
| bb325bcfbf | |||
| 7ca2f82ebe | |||
|
|
94e23b8f04 | ||
|
|
457eceb647 | ||
| e62b89a290 | |||
| af815e00ee | |||
| e4f0c65478 | |||
| 1e5166c403 | |||
| c822d03e98 | |||
| f3e072352b | |||
| 516f4e94c9 | |||
| 6507bc6646 | |||
| 8b3e41f60f | |||
| e5275fdc5d | |||
|
|
792d97ee9a | ||
|
|
a233e4358e |
54
.dockerignore
Normal 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
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTIBJBDZGZJSFPAUBTBXFSIUFTCMBHGCBKAGHFAMFSGYEIJPGPGXGJEREEBYAJFHFGESCXBJIGGHDBISEQAMAGGWGCADDVENHCIYITAVBUDYDWJGIRIRIKFHBAABGUHEDHFODQGUATGIGSEPBFBMBEDNCUGSEBCGCMCJGTEMFTCGBQBEBCFOCGDOIGEGFDAIEYEUEXGMDWJAFHCEHBGLJHAIIGGQANAKFBCOBGIUHYEWCSGNANCICCCOGNIVITFDHPHQCBANJIBTAQEMJFJLAJFGELHOHPGPAIGCEJDVHOIZHXECIIAMEPDJGBCXGSHCHHCVGVIJCKHKBXCBHKFRISAIDTFWGOGRBHBMFAHYACAQFRHAGCGVBWFWFGBCIGEHEHJBHZHABMGCHJEFCUEDFIBSABDTDYGXBMFRIZAHINEUBJFHICIJHRANJCBXBBBMFEJDJPDQCLIOFLDKDEGTICCMCWIUDWBYGFAPJOAUCCFUEMBEBOGDIFJMHJDQGWBFACBKEREXGPBXDJBPEZCUDTCRFVBWHACNFACDFDFEBIJKJNGWAPAMGQFDDKIJBCJNHLBBDOFMEBGUHDDYISIBEAJMEAIQEDCJFQBEIUHFJTCAIVBZGMCAFBDTHFDCARBRFWBFGRCJDIHGIZABDJFQGLCTHCJUJCJGGNHXHOBHHRIIBQIGJFACDRDBAHFMEMFQAZFXBAIJBCGCHZFRHNBFEPCFBIHFDDAWGPFBECITGIASAIDXHMAIHVHKINFVDHDDGFJHHWECASFHITFQIJACDACVAIDAFVISGAALHFCQACEUHOBOGFDEBHFSBUEQEXESEWIOJUGXHHIZDTEZJGDFIOAJGTDCBUBABPEVBTJJGNGGHOFCCHIWGFCPBDCEANIIACBFBEDSCMIWISCXAZFUBFCSGSENHOHKGLGDGEBTAIBNIOIRGMFBIMHVEXFGFICECPAQCZEFFZERETCTBEHLEEJPIFDVHHGNIZAOINGOCKBPDRCIBQJUBYDCEGFWBECSDDJHGCIUCUCCGKIHDRIRIPJBENGAAPJSDIBECTHAAXIVHZFFDCHDHWJTIQAIIPBYGMEMCYCPIRGOJHINFZASGVDVBWHZCNEFJI
|
||||
567
COMPLETE-SUMMARY.md
Normal 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
@@ -0,0 +1 @@
|
||||
|
||||
329
DEPLOYMENT-NOTES.md
Normal 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
|
||||
- MySQL:3306
|
||||
- Redis:6379
|
||||
|
||||
## 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
@@ -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
@@ -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依赖
|
||||
|
||||
🎉 **祝部署顺利!**
|
||||
525
INSPECTION-FEATURE-SUMMARY.md
Normal 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-50MB,30秒)
|
||||
- 处理后视频(~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
@@ -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
@@ -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><> 许可证
|
||||
|
||||
[根据项目实际许可证填写]
|
||||
43
README.md
@@ -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-flv,ws-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
@@ -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
@@ -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
@@ -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设备ID,CPU使用'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
@@ -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
@@ -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
@@ -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:
|
||||
5
pom.xml
@@ -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>
|
||||
|
||||
47
python-inference-service/.dockerignore
Normal 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/
|
||||
49
python-inference-service/Dockerfile
Normal 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"]
|
||||
252
python-inference-service/README.md
Normal 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模型包装类
|
||||
|
||||
## 许可证
|
||||
|
||||
[根据项目实际许可证填写]
|
||||
1
python-inference-service/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Python Inference Service package
|
||||
330
python-inference-service/app/detector.py
Normal 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()
|
||||
191
python-inference-service/app/main.py
Normal 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)
|
||||
40
python-inference-service/app/models.py
Normal 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]
|
||||
18
python-inference-service/models.json
Normal 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"
|
||||
}
|
||||
]
|
||||
BIN
python-inference-service/models/garbage.pt
Normal file
1
python-inference-service/models/garbage.txt
Normal file
@@ -0,0 +1 @@
|
||||
垃圾
|
||||
BIN
python-inference-service/models/smoke.pt
Normal file
1
python-inference-service/models/smoke.txt
Normal file
@@ -0,0 +1 @@
|
||||
垃圾
|
||||
169
python-inference-service/models/universal_yolo_model.py
Normal 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}模型已关闭")
|
||||
|
||||
10
python-inference-service/requirements.txt
Normal 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
|
||||
5
python-inference-service/start_service.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
echo Starting Python Inference Service...
|
||||
cd /d %~dp0
|
||||
python -m app.main
|
||||
pause
|
||||
4
python-inference-service/start_service.sh
Normal 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
@@ -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
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 若依管理系统
|
||||
VITE_APP_TITLE = 福安德视频监控分析
|
||||
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 若依管理系统
|
||||
VITE_APP_TITLE = 福安德视频监控分析
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 若依管理系统
|
||||
VITE_APP_TITLE = 福安德视频监控分析
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'staging'
|
||||
|
||||
41
rtsp-vue/Dockerfile
Normal 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;'"
|
||||
@@ -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
@@ -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-FLV,backend使用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;
|
||||
}
|
||||
@@ -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
|
After Width: | Height: | Size: 9.9 KiB |
4
rtsp-vue/src/api/system/oss.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 根据文件名获取文件
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
44
rtsp-vue/src/api/video/insRecord.js
Normal 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'
|
||||
})
|
||||
}
|
||||
53
rtsp-vue/src/api/video/model.js
Normal 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 }
|
||||
})
|
||||
}
|
||||
BIN
rtsp-vue/src/assets/images/avatar.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
rtsp-vue/src/assets/images/camera.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
rtsp-vue/src/assets/images/device.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
rtsp-vue/src/assets/images/login-back.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 9.9 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
rtsp-vue/src/assets/styles/element/layout-compact.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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--;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">¥免费开源</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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -308,7 +308,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
jobName: undefined,
|
||||
jobGroup: undefined,
|
||||
status: undefined
|
||||
|
||||
@@ -191,7 +191,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
dictName: undefined,
|
||||
dictType: undefined,
|
||||
status: undefined
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -219,7 +219,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
operIp: undefined,
|
||||
title: undefined,
|
||||
operName: undefined,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -185,7 +185,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
configName: undefined,
|
||||
configKey: undefined,
|
||||
configType: undefined
|
||||
|
||||
@@ -209,7 +209,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
dictType: undefined,
|
||||
dictLabel: undefined,
|
||||
status: undefined
|
||||
|
||||
@@ -193,7 +193,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
dictName: undefined,
|
||||
dictType: undefined,
|
||||
status: undefined
|
||||
|
||||
@@ -178,7 +178,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
noticeTitle: undefined,
|
||||
createBy: undefined,
|
||||
status: undefined
|
||||
|
||||
@@ -164,7 +164,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
postCode: undefined,
|
||||
postName: undefined,
|
||||
status: undefined
|
||||
|
||||
@@ -108,7 +108,7 @@ const userIds = ref([]);
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
roleId: route.params.roleId,
|
||||
userName: undefined,
|
||||
phonenumber: undefined,
|
||||
|
||||
@@ -282,7 +282,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
roleName: undefined,
|
||||
roleKey: undefined,
|
||||
status: undefined
|
||||
|
||||
@@ -79,7 +79,7 @@ const userIds = ref([]);
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
roleId: undefined,
|
||||
userName: undefined,
|
||||
phonenumber: undefined
|
||||
|
||||
@@ -330,7 +330,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
userName: undefined,
|
||||
phonenumber: undefined,
|
||||
status: undefined,
|
||||
|
||||
@@ -61,7 +61,7 @@ const { proxy } = getCurrentInstance();
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
tableName: undefined,
|
||||
tableComment: undefined
|
||||
});
|
||||
|
||||
@@ -183,7 +183,7 @@ const uniqueId = ref("");
|
||||
const data = reactive({
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
tableName: undefined,
|
||||
tableComment: undefined
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
175
rtsp-vue/src/views/video/device/component/jpegPlayer.vue
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -126,7 +126,7 @@ const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
imageName: null,
|
||||
imageData: null,
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
368
rtsp-vue/src/views/video/inspection/record.vue
Normal 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>
|
||||
323
rtsp-vue/src/views/video/model/dict.vue
Normal 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>
|
||||
309
rtsp-vue/src/views/video/model/index.vue
Normal 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>
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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:
|
||||
# 从数据源开关/默认关闭
|
||||
|
||||
@@ -7,7 +7,7 @@ ruoyi:
|
||||
# 版权年份
|
||||
copyrightYear: 2025
|
||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
trash
|
||||