Quellcode durchsuchen

feat(wm-production): #63 视频监控集成与AI人员闯入检测

- VideoMonitorService: 摄像头CRUD/视频流管理/状态监控/录像回放/统计
- IntrusionDetectionService: AI闯入检测(模拟)/事件处理/分页查询/统计
- VideoController: 20+ API端点 (/api/production/video/*)
- DDL: prod_video_recording + prod_intrusion_event + 索引
bot_dev2 vor 5 Tagen
Ursprung
Commit
dcb412c7e4

+ 152
- 0
wm-production/src/main/java/com/water/production/controller/VideoController.java Datei anzeigen

@@ -0,0 +1,152 @@
1
+package com.water.production.controller;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.common.core.result.R;
5
+import com.water.production.entity.IntrusionEvent;
6
+import com.water.production.entity.VideoCamera;
7
+import com.water.production.entity.VideoRecording;
8
+import com.water.production.service.IntrusionDetectionService;
9
+import com.water.production.service.VideoMonitorService;
10
+import io.swagger.v3.oas.annotations.tags.Tag;
11
+import lombok.RequiredArgsConstructor;
12
+import org.springframework.web.bind.annotation.*;
13
+
14
+import java.util.*;
15
+
16
+@Tag(name = "视频监控与AI检测")
17
+@RestController
18
+@RequestMapping("/api/production/video")
19
+@RequiredArgsConstructor
20
+public class VideoController {
21
+
22
+    private final VideoMonitorService videoMonitorService;
23
+    private final IntrusionDetectionService intrusionService;
24
+
25
+    // === 摄像头管理 ===
26
+    @GetMapping("/cameras")
27
+    public R<Page<VideoCamera>> listCameras(@RequestParam(defaultValue = "1") int current,
28
+                                             @RequestParam(defaultValue = "20") int size,
29
+                                             @RequestParam(required = false) String area,
30
+                                             @RequestParam(required = false) Integer status,
31
+                                             @RequestParam(required = false) String keyword) {
32
+        return R.ok(videoMonitorService.pageCameras(current, size, area, status, keyword));
33
+    }
34
+
35
+    @GetMapping("/cameras/all")
36
+    public R<List<VideoCamera>> listAllCameras() {
37
+        return R.ok(videoMonitorService.listAllCameras());
38
+    }
39
+
40
+    @GetMapping("/cameras/{id}")
41
+    public R<VideoCamera> getCamera(@PathVariable Long id) {
42
+        return R.ok(videoMonitorService.getCameraById(id));
43
+    }
44
+
45
+    @PostMapping("/cameras")
46
+    public R<VideoCamera> createCamera(@RequestBody VideoCamera camera) {
47
+        return R.ok(videoMonitorService.createCamera(camera));
48
+    }
49
+
50
+    @PutMapping("/cameras/{id}")
51
+    public R<String> updateCamera(@PathVariable Long id, @RequestBody VideoCamera camera) {
52
+        camera.setId(id);
53
+        videoMonitorService.updateCamera(camera);
54
+        return R.ok("OK");
55
+    }
56
+
57
+    @DeleteMapping("/cameras/{id}")
58
+    public R<String> deleteCamera(@PathVariable Long id) {
59
+        videoMonitorService.deleteCamera(id);
60
+        return R.ok("OK");
61
+    }
62
+
63
+    @PutMapping("/cameras/{id}/status")
64
+    public R<String> updateCameraStatus(@PathVariable Long id, @RequestParam Integer status) {
65
+        videoMonitorService.updateCameraStatus(id, status);
66
+        return R.ok("OK");
67
+    }
68
+
69
+    // === 视频流 ===
70
+    @GetMapping("/cameras/{id}/stream")
71
+    public R<Map<String, Object>> getStreamUrl(@PathVariable Long id) {
72
+        return R.ok(videoMonitorService.getStreamUrls(id));
73
+    }
74
+
75
+    @PutMapping("/cameras/{id}/stream")
76
+    public R<String> updateStreamUrls(@PathVariable Long id,
77
+                                       @RequestParam(required = false) String rtsp,
78
+                                       @RequestParam(required = false) String hls,
79
+                                       @RequestParam(required = false) String flv) {
80
+        videoMonitorService.updateStreamUrls(id, rtsp, hls, flv);
81
+        return R.ok("OK");
82
+    }
83
+
84
+    @PostMapping("/cameras/refresh-status")
85
+    public R<Map<String, Object>> refreshAllStatus() {
86
+        return R.ok(videoMonitorService.refreshAllStatus());
87
+    }
88
+
89
+    // === 录像回放 ===
90
+    @GetMapping("/recordings")
91
+    public R<Page<VideoRecording>> getRecordings(@RequestParam(defaultValue = "1") int current,
92
+                                                  @RequestParam(defaultValue = "20") int size,
93
+                                                  @RequestParam(required = false) Long cameraId,
94
+                                                  @RequestParam(required = false) String date) {
95
+        return R.ok(videoMonitorService.pageRecordings(current, size, cameraId, null, null, null));
96
+    }
97
+
98
+    @GetMapping("/recordings/{recordingId}/playback")
99
+    public R<Map<String, Object>> getPlaybackUrl(@PathVariable Long recordingId) {
100
+        return R.ok(videoMonitorService.getPlaybackUrl(recordingId));
101
+    }
102
+
103
+    @PostMapping("/recordings")
104
+    public R<VideoRecording> createRecording(@RequestBody VideoRecording recording) {
105
+        return R.ok(videoMonitorService.createRecording(recording));
106
+    }
107
+
108
+    @DeleteMapping("/recordings/{id}")
109
+    public R<String> deleteRecording(@PathVariable Long id) {
110
+        videoMonitorService.deleteRecording(id);
111
+        return R.ok("OK");
112
+    }
113
+
114
+    // === 监控统计 ===
115
+    @GetMapping("/statistics/online")
116
+    public R<Map<String, Object>> getOnlineStats() {
117
+        return R.ok(videoMonitorService.getDeviceOnlineStats());
118
+    }
119
+
120
+    @GetMapping("/statistics/area")
121
+    public R<List<Map<String, Object>>> getAreaStats() {
122
+        return R.ok(videoMonitorService.getCameraStatsByArea());
123
+    }
124
+
125
+    // === AI 闯入检测 ===
126
+    @PostMapping("/intrusion/detect")
127
+    public R<IntrusionEvent> detectIntrusion(@RequestParam String cameraId,
128
+                                              @RequestParam(required = false) String area) {
129
+        return R.ok(intrusionService.detect(cameraId, area));
130
+    }
131
+
132
+    @PostMapping("/intrusion/{id}/handle")
133
+    public R<IntrusionEvent> handleEvent(@PathVariable Long id,
134
+                                          @RequestParam String action,
135
+                                          @RequestParam String operator) {
136
+        return R.ok(intrusionService.handleEvent(id, action, operator));
137
+    }
138
+
139
+    @GetMapping("/intrusion/list")
140
+    public R<Page<IntrusionEvent>> listEvents(@RequestParam(required = false) String status,
141
+                                               @RequestParam(required = false) String area,
142
+                                               @RequestParam(required = false) String alertLevel,
143
+                                               @RequestParam(defaultValue = "1") int pageNum,
144
+                                               @RequestParam(defaultValue = "20") int pageSize) {
145
+        return R.ok(intrusionService.listEvents(status, area, alertLevel, pageNum, pageSize));
146
+    }
147
+
148
+    @GetMapping("/intrusion/statistics")
149
+    public R<Map<String, Object>> getIntrusionStatistics() {
150
+        return R.ok(intrusionService.getStatistics());
151
+    }
152
+}

+ 57
- 0
wm-production/src/main/java/com/water/production/entity/IntrusionEvent.java Datei anzeigen

@@ -0,0 +1,57 @@
1
+package com.water.production.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+import java.time.LocalDateTime;
6
+
7
+@Data
8
+@TableName("prod_intrusion_event")
9
+public class IntrusionEvent {
10
+
11
+    @TableId(type = IdType.AUTO)
12
+    private Long id;
13
+
14
+    private String eventNo;
15
+
16
+    /** 关联摄像头ID */
17
+    private String cameraId;
18
+
19
+    /** 所属区域 */
20
+    private String area;
21
+
22
+    /** 检测时间 */
23
+    private LocalDateTime detectedTime;
24
+
25
+    /** AI识别置信度(0~1) */
26
+    private Double confidence;
27
+
28
+    /** 是否检测到闯入 */
29
+    private Boolean detected;
30
+
31
+    /** 事件状态: ACTIVE/CONFIRMED/DISMISSED/RESOLVED/FALSE_POSITIVE */
32
+    private String status;
33
+
34
+    /** 报警等级: 一般/重要/紧急 */
35
+    private String alertLevel;
36
+
37
+    /** 是否触发报警 */
38
+    private Boolean alertTriggered;
39
+
40
+    /** 抓拍图片URL */
41
+    private String snapshotUrl;
42
+
43
+    /** 描述 */
44
+    private String description;
45
+
46
+    /** 确认人 */
47
+    private String confirmedBy;
48
+
49
+    /** 确认时间 */
50
+    private LocalDateTime confirmedTime;
51
+
52
+    /** 解决时间 */
53
+    private LocalDateTime resolvedTime;
54
+
55
+    @TableField(fill = FieldFill.INSERT)
56
+    private LocalDateTime createdTime;
57
+}

+ 70
- 5
wm-production/src/main/java/com/water/production/entity/VideoCamera.java Datei anzeigen

@@ -1,10 +1,75 @@
1 1
 package com.water.production.entity;
2
+
2 3
 import com.baomidou.mybatisplus.annotation.*;
3 4
 import lombok.Data;
4
-@Data @TableName("prod_video_camera")
5
+
6
+import java.time.LocalDate;
7
+import java.time.LocalDateTime;
8
+
9
+/**
10
+ * 视频监控摄像头实体(增强版)
11
+ */
12
+@Data
13
+@TableName("prod_video_camera")
5 14
 public class VideoCamera {
6
-    @TableId(type = IdType.AUTO) private Long id;
7
-    private String cameraId, name, area;
8
-    private String streamUrl; private Integer status; // 0离线 1在线
9
-    private Double lng, lat;
15
+
16
+    @TableId(type = IdType.AUTO)
17
+    private Long id;
18
+
19
+    /** 摄像头唯一编号 */
20
+    private String cameraId;
21
+
22
+    /** 摄像头名称 */
23
+    private String name;
24
+
25
+    /** 所属区域 */
26
+    private String area;
27
+
28
+    /** RTSP 视频流地址 */
29
+    private String streamUrlRtsp;
30
+
31
+    /** HLS 视频流地址 */
32
+    private String streamUrlHls;
33
+
34
+    /** FLV 视频流地址 */
35
+    private String streamUrlFlv;
36
+
37
+    /** 状态: 0=离线, 1=在线, 2=故障 */
38
+    private Integer status;
39
+
40
+    /** 设备厂商 */
41
+    private String manufacturer;
42
+
43
+    /** 设备型号 */
44
+    private String model;
45
+
46
+    /** 经度 */
47
+    private Double lng;
48
+
49
+    /** 纬度 */
50
+    private Double lat;
51
+
52
+    /** 安装位置描述 */
53
+    private String installLocation;
54
+
55
+    /** 安装日期 */
56
+    private LocalDate installDate;
57
+
58
+    /** 最后在线时间 */
59
+    private LocalDateTime lastOnlineTime;
60
+
61
+    /** 是否启用AI检测: 0=未启用, 1=已启用 */
62
+    private Integer aiEnabled;
63
+
64
+    /** 备注 */
65
+    private String remark;
66
+
67
+    @TableField(fill = FieldFill.INSERT)
68
+    private LocalDateTime createdTime;
69
+
70
+    @TableField(fill = FieldFill.INSERT_UPDATE)
71
+    private LocalDateTime updatedTime;
72
+
73
+    @TableLogic
74
+    private Integer deleted;
10 75
 }

+ 63
- 0
wm-production/src/main/java/com/water/production/entity/VideoRecording.java Datei anzeigen

@@ -0,0 +1,63 @@
1
+package com.water.production.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+
6
+import java.math.BigDecimal;
7
+import java.time.LocalDateTime;
8
+
9
+/**
10
+ * 视频录像记录实体
11
+ */
12
+@Data
13
+@TableName("prod_video_recording")
14
+public class VideoRecording {
15
+
16
+    @TableId(type = IdType.AUTO)
17
+    private Long id;
18
+
19
+    /** 关联摄像头ID */
20
+    private Long cameraId;
21
+
22
+    /** 摄像头名称 */
23
+    private String cameraName;
24
+
25
+    /** 所属区域 */
26
+    private String area;
27
+
28
+    /** 录像开始时间 */
29
+    private LocalDateTime startTime;
30
+
31
+    /** 录像结束时间 */
32
+    private LocalDateTime endTime;
33
+
34
+    /** 时长(秒) */
35
+    private Integer durationSec;
36
+
37
+    /** 文件大小(MB) */
38
+    private BigDecimal fileSizeMb;
39
+
40
+    /** 存储路径 */
41
+    private String storagePath;
42
+
43
+    /** 回放地址 */
44
+    private String playbackUrl;
45
+
46
+    /** 录像类型: scheduled=计划录像, event_triggered=事件触发, manual=手动录像 */
47
+    private String recordType;
48
+
49
+    /** 关联闯入事件ID(事件触发时有值) */
50
+    private Long eventId;
51
+
52
+    /** 备注 */
53
+    private String remark;
54
+
55
+    @TableField(fill = FieldFill.INSERT)
56
+    private LocalDateTime createdTime;
57
+
58
+    @TableField(fill = FieldFill.INSERT_UPDATE)
59
+    private LocalDateTime updatedTime;
60
+
61
+    @TableLogic
62
+    private Integer deleted;
63
+}

+ 9
- 0
wm-production/src/main/java/com/water/production/mapper/IntrusionEventMapper.java Datei anzeigen

@@ -0,0 +1,9 @@
1
+package com.water.production.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.production.entity.IntrusionEvent;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface IntrusionEventMapper extends BaseMapper<IntrusionEvent> {
9
+}

+ 9
- 0
wm-production/src/main/java/com/water/production/mapper/VideoRecordingMapper.java Datei anzeigen

@@ -0,0 +1,9 @@
1
+package com.water.production.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.production.entity.VideoRecording;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface VideoRecordingMapper extends BaseMapper<VideoRecording> {
9
+}

+ 104
- 0
wm-production/src/main/java/com/water/production/service/IntrusionDetectionService.java Datei anzeigen

@@ -0,0 +1,104 @@
1
+package com.water.production.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.production.entity.IntrusionEvent;
6
+import com.water.production.mapper.IntrusionEventMapper;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.stereotype.Service;
9
+
10
+import java.time.LocalDateTime;
11
+import java.util.*;
12
+
13
+@Service
14
+@RequiredArgsConstructor
15
+public class IntrusionDetectionService {
16
+
17
+    private final IntrusionEventMapper eventMapper;
18
+
19
+    /**
20
+     * 模拟AI检测闯入事件
21
+     */
22
+    public IntrusionEvent detect(String cameraId, String area) {
23
+        double confidence = 0.7 + Math.random() * 0.3;
24
+        boolean detected = confidence > 0.85;
25
+
26
+        IntrusionEvent event = new IntrusionEvent();
27
+        event.setEventNo("INT-" + System.currentTimeMillis());
28
+        event.setCameraId(cameraId);
29
+        event.setArea(area);
30
+        event.setDetectedTime(LocalDateTime.now());
31
+        event.setConfidence(confidence);
32
+        event.setDetected(detected);
33
+        event.setStatus(detected ? "ACTIVE" : "FALSE_POSITIVE");
34
+        event.setAlertLevel(detected ? (confidence > 0.95 ? "紧急" : "重要") : "一般");
35
+        event.setSnapshotUrl("/snapshots/" + event.getEventNo() + ".jpg");
36
+
37
+        if (detected) {
38
+            event.setAlertTriggered(true);
39
+            event.setDescription(String.format("AI检测到人员闯入 (置信度: %.1f%%)", confidence * 100));
40
+        }
41
+
42
+        eventMapper.insert(event);
43
+        return event;
44
+    }
45
+
46
+    /**
47
+     * 确认/处理闯入事件
48
+     */
49
+    public IntrusionEvent handleEvent(Long eventId, String action, String operator) {
50
+        IntrusionEvent event = eventMapper.selectById(eventId);
51
+        if (event == null) throw new RuntimeException("事件不存在");
52
+
53
+        switch (action) {
54
+            case "confirm" -> { event.setStatus("CONFIRMED"); event.setConfirmedBy(operator); event.setConfirmedTime(LocalDateTime.now()); }
55
+            case "dismiss" -> { event.setStatus("DISMISSED"); event.setConfirmedBy(operator); event.setConfirmedTime(LocalDateTime.now()); }
56
+            case "resolve" -> { event.setStatus("RESOLVED"); event.setResolvedTime(LocalDateTime.now()); }
57
+            default -> throw new RuntimeException("未知操作: " + action);
58
+        }
59
+        eventMapper.updateById(event);
60
+        return event;
61
+    }
62
+
63
+    /**
64
+     * 分页查询闯入事件
65
+     */
66
+    public Page<IntrusionEvent> listEvents(String status, String area, String alertLevel,
67
+                                            int pageNum, int pageSize) {
68
+        Page<IntrusionEvent> page = new Page<>(pageNum, pageSize);
69
+        LambdaQueryWrapper<IntrusionEvent> wrapper = new LambdaQueryWrapper<>();
70
+        if (status != null && !status.isBlank()) wrapper.eq(IntrusionEvent::getStatus, status);
71
+        if (area != null && !area.isBlank()) wrapper.like(IntrusionEvent::getArea, area);
72
+        if (alertLevel != null && !alertLevel.isBlank()) wrapper.eq(IntrusionEvent::getAlertLevel, alertLevel);
73
+        wrapper.orderByDesc(IntrusionEvent::getDetectedTime);
74
+        return eventMapper.selectPage(page, wrapper);
75
+    }
76
+
77
+    /**
78
+     * 闯入事件统计
79
+     */
80
+    public Map<String, Object> getStatistics() {
81
+        long total = eventMapper.selectCount(null);
82
+        long active = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
83
+            .eq(IntrusionEvent::getStatus, "ACTIVE"));
84
+        long confirmed = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
85
+            .eq(IntrusionEvent::getStatus, "CONFIRMED"));
86
+        long todayEvents = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
87
+            .ge(IntrusionEvent::getDetectedTime, LocalDateTime.now().toLocalDate().atStartOfDay()));
88
+
89
+        double avgConfidence = eventMapper.selectList(null).stream()
90
+            .mapToDouble(e -> e.getConfidence() != null ? e.getConfidence() : 0)
91
+            .average().orElse(0);
92
+
93
+        Map<String, Object> stats = new LinkedHashMap<>();
94
+        stats.put("total", total);
95
+        stats.put("active", active);
96
+        stats.put("confirmed", confirmed);
97
+        stats.put("todayEvents", todayEvents);
98
+        stats.put("avgConfidence", avgConfidence);
99
+        stats.put("falsePositiveRate", total > 0 ?
100
+            (double) eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
101
+                .eq(IntrusionEvent::getStatus, "FALSE_POSITIVE")) / total : 0);
102
+        return stats;
103
+    }
104
+}

+ 277
- 0
wm-production/src/main/java/com/water/production/service/VideoMonitorService.java Datei anzeigen

@@ -0,0 +1,277 @@
1
+package com.water.production.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.production.entity.VideoCamera;
6
+import com.water.production.entity.VideoRecording;
7
+import com.water.production.mapper.VideoCameraMapper;
8
+import com.water.production.mapper.VideoRecordingMapper;
9
+import lombok.RequiredArgsConstructor;
10
+import lombok.extern.slf4j.Slf4j;
11
+import org.springframework.stereotype.Service;
12
+import org.springframework.util.StringUtils;
13
+
14
+import java.time.Duration;
15
+import java.time.LocalDateTime;
16
+import java.util.*;
17
+import java.util.stream.Collectors;
18
+
19
+/**
20
+ * 视频监控管理服务
21
+ * 提供摄像头CRUD、视频流地址管理、状态监控、录像查询、统计等能力
22
+ */
23
+@Slf4j
24
+@Service
25
+@RequiredArgsConstructor
26
+public class VideoMonitorService {
27
+
28
+    private final VideoCameraMapper cameraMapper;
29
+    private final VideoRecordingMapper recordingMapper;
30
+
31
+    // ==================== 摄像头 CRUD ====================
32
+
33
+    /**
34
+     * 分页查询摄像头列表
35
+     */
36
+    public Page<VideoCamera> pageCameras(int current, int size, String area, Integer status, String keyword) {
37
+        Page<VideoCamera> page = new Page<>(current, size);
38
+        LambdaQueryWrapper<VideoCamera> wrapper = new LambdaQueryWrapper<>();
39
+        wrapper.eq(area != null, VideoCamera::getArea, area)
40
+               .eq(status != null, VideoCamera::getStatus, status)
41
+               .and(StringUtils.hasText(keyword), w ->
42
+                       w.like(VideoCamera::getName, keyword)
43
+                        .or().like(VideoCamera::getCameraId, keyword))
44
+               .orderByDesc(VideoCamera::getCreatedTime);
45
+        return cameraMapper.selectPage(page, wrapper);
46
+    }
47
+
48
+    /**
49
+     * 获取所有摄像头
50
+     */
51
+    public List<VideoCamera> listAllCameras() {
52
+        return cameraMapper.selectList(new LambdaQueryWrapper<VideoCamera>()
53
+                .orderByDesc(VideoCamera::getCreatedTime));
54
+    }
55
+
56
+    /**
57
+     * 获取摄像头详情
58
+     */
59
+    public VideoCamera getCameraById(Long id) {
60
+        return cameraMapper.selectById(id);
61
+    }
62
+
63
+    /**
64
+     * 创建摄像头
65
+     */
66
+    public VideoCamera createCamera(VideoCamera camera) {
67
+        camera.setStatus(camera.getStatus() != null ? camera.getStatus() : 0);
68
+        camera.setAiEnabled(camera.getAiEnabled() != null ? camera.getAiEnabled() : 0);
69
+        camera.setCreatedTime(LocalDateTime.now());
70
+        camera.setUpdatedTime(LocalDateTime.now());
71
+        camera.setDeleted(0);
72
+        cameraMapper.insert(camera);
73
+        log.info("创建摄像头: {} ({})", camera.getName(), camera.getCameraId());
74
+        return camera;
75
+    }
76
+
77
+    /**
78
+     * 更新摄像头
79
+     */
80
+    public boolean updateCamera(VideoCamera camera) {
81
+        camera.setUpdatedTime(LocalDateTime.now());
82
+        int rows = cameraMapper.updateById(camera);
83
+        if (rows > 0) {
84
+            log.info("更新摄像头: id={}", camera.getId());
85
+        }
86
+        return rows > 0;
87
+    }
88
+
89
+    /**
90
+     * 删除摄像头
91
+     */
92
+    public boolean deleteCamera(Long id) {
93
+        int rows = cameraMapper.deleteById(id);
94
+        if (rows > 0) {
95
+            log.info("删除摄像头: id={}", id);
96
+        }
97
+        return rows > 0;
98
+    }
99
+
100
+    // ==================== 视频流地址管理 ====================
101
+
102
+    /**
103
+     * 获取摄像头的所有视频流地址
104
+     */
105
+    public Map<String, Object> getStreamUrls(Long cameraId) {
106
+        VideoCamera camera = cameraMapper.selectById(cameraId);
107
+        if (camera == null) {
108
+            return Collections.emptyMap();
109
+        }
110
+        Map<String, Object> urls = new LinkedHashMap<>();
111
+        urls.put("cameraId", camera.getCameraId());
112
+        urls.put("name", camera.getName());
113
+        urls.put("rtsp", camera.getStreamUrlRtsp());
114
+        urls.put("hls", camera.getStreamUrlHls());
115
+        urls.put("flv", camera.getStreamUrlFlv());
116
+        urls.put("status", camera.getStatus());
117
+        return urls;
118
+    }
119
+
120
+    /**
121
+     * 更新视频流地址
122
+     */
123
+    public boolean updateStreamUrls(Long cameraId, String rtsp, String hls, String flv) {
124
+        VideoCamera camera = new VideoCamera();
125
+        camera.setId(cameraId);
126
+        camera.setStreamUrlRtsp(rtsp);
127
+        camera.setStreamUrlHls(hls);
128
+        camera.setStreamUrlFlv(flv);
129
+        camera.setUpdatedTime(LocalDateTime.now());
130
+        return cameraMapper.updateById(camera) > 0;
131
+    }
132
+
133
+    // ==================== 状态监控 ====================
134
+
135
+    /**
136
+     * 更新摄像头在线状态(模拟心跳检测)
137
+     */
138
+    public boolean updateCameraStatus(Long cameraId, Integer status) {
139
+        VideoCamera camera = new VideoCamera();
140
+        camera.setId(cameraId);
141
+        camera.setStatus(status);
142
+        if (status != null && status == 1) {
143
+            camera.setLastOnlineTime(LocalDateTime.now());
144
+        }
145
+        camera.setUpdatedTime(LocalDateTime.now());
146
+        return cameraMapper.updateById(camera) > 0;
147
+    }
148
+
149
+    /**
150
+     * 批量刷新摄像头状态(模拟)
151
+     * 实际场景: 从流媒体服务器获取在线状态
152
+     */
153
+    public Map<String, Object> refreshAllStatus() {
154
+        List<VideoCamera> cameras = cameraMapper.selectList(null);
155
+        int online = 0, offline = 0, fault = 0;
156
+        for (VideoCamera cam : cameras) {
157
+            // 模拟: 根据最后在线时间判断状态
158
+            if (cam.getLastOnlineTime() != null
159
+                    && Duration.between(cam.getLastOnlineTime(), LocalDateTime.now()).toMinutes() < 5) {
160
+                online++;
161
+            } else if (cam.getStatus() != null && cam.getStatus() == 2) {
162
+                fault++;
163
+            } else {
164
+                offline++;
165
+            }
166
+        }
167
+        Map<String, Object> result = new LinkedHashMap<>();
168
+        result.put("total", cameras.size());
169
+        result.put("online", online);
170
+        result.put("offline", offline);
171
+        result.put("fault", fault);
172
+        result.put("refreshTime", LocalDateTime.now());
173
+        return result;
174
+    }
175
+
176
+    // ==================== 视频录像 ====================
177
+
178
+    /**
179
+     * 分页查询录像记录
180
+     */
181
+    public Page<VideoRecording> pageRecordings(int current, int size, Long cameraId,
182
+                                                String recordType, String startDate, String endDate) {
183
+        Page<VideoRecording> page = new Page<>(current, size);
184
+        LambdaQueryWrapper<VideoRecording> wrapper = new LambdaQueryWrapper<>();
185
+        wrapper.eq(cameraId != null, VideoRecording::getCameraId, cameraId)
186
+               .eq(recordType != null, VideoRecording::getRecordType, recordType)
187
+               .ge(StringUtils.hasText(startDate), VideoRecording::getStartTime,
188
+                       startDate != null ? startDate + " 00:00:00" : null)
189
+               .le(StringUtils.hasText(endDate), VideoRecording::getEndTime,
190
+                       endDate != null ? endDate + " 23:59:59" : null)
191
+               .orderByDesc(VideoRecording::getStartTime);
192
+        return recordingMapper.selectPage(page, wrapper);
193
+    }
194
+
195
+    /**
196
+     * 生成回放地址
197
+     */
198
+    public Map<String, Object> getPlaybackUrl(Long recordingId) {
199
+        VideoRecording recording = recordingMapper.selectById(recordingId);
200
+        if (recording == null) {
201
+            return Collections.emptyMap();
202
+        }
203
+        Map<String, Object> result = new LinkedHashMap<>();
204
+        result.put("recordingId", recording.getId());
205
+        result.put("cameraId", recording.getCameraId());
206
+        result.put("cameraName", recording.getCameraName());
207
+        result.put("startTime", recording.getStartTime());
208
+        result.put("endTime", recording.getEndTime());
209
+        result.put("durationSec", recording.getDurationSec());
210
+        result.put("playbackUrl", recording.getPlaybackUrl());
211
+        result.put("fileSizeMb", recording.getFileSizeMb());
212
+        return result;
213
+    }
214
+
215
+    /**
216
+     * 创建录像记录
217
+     */
218
+    public VideoRecording createRecording(VideoRecording recording) {
219
+        recording.setCreatedTime(LocalDateTime.now());
220
+        recording.setUpdatedTime(LocalDateTime.now());
221
+        recording.setDeleted(0);
222
+        recordingMapper.insert(recording);
223
+        return recording;
224
+    }
225
+
226
+    /**
227
+     * 删除录像记录
228
+     */
229
+    public boolean deleteRecording(Long id) {
230
+        return recordingMapper.deleteById(id) > 0;
231
+    }
232
+
233
+    // ==================== 统计 ====================
234
+
235
+    /**
236
+     * 设备在线率统计
237
+     */
238
+    public Map<String, Object> getDeviceOnlineStats() {
239
+        List<VideoCamera> cameras = cameraMapper.selectList(null);
240
+        long total = cameras.size();
241
+        long online = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 1).count();
242
+        long offline = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 0).count();
243
+        long fault = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 2).count();
244
+        double onlineRate = total > 0 ? (double) online / total * 100 : 0;
245
+
246
+        Map<String, Object> stats = new LinkedHashMap<>();
247
+        stats.put("total", total);
248
+        stats.put("online", online);
249
+        stats.put("offline", offline);
250
+        stats.put("fault", fault);
251
+        stats.put("onlineRate", Math.round(onlineRate * 100.0) / 100.0);
252
+        return stats;
253
+    }
254
+
255
+    /**
256
+     * 按区域统计摄像头分布
257
+     */
258
+    public List<Map<String, Object>> getCameraStatsByArea() {
259
+        List<VideoCamera> cameras = cameraMapper.selectList(null);
260
+        Map<String, List<VideoCamera>> grouped = cameras.stream()
261
+                .filter(c -> c.getArea() != null)
262
+                .collect(Collectors.groupingBy(VideoCamera::getArea));
263
+
264
+        List<Map<String, Object>> result = new ArrayList<>();
265
+        for (Map.Entry<String, List<VideoCamera>> entry : grouped.entrySet()) {
266
+            Map<String, Object> item = new LinkedHashMap<>();
267
+            item.put("area", entry.getKey());
268
+            item.put("total", entry.getValue().size());
269
+            long online = entry.getValue().stream().filter(c -> c.getStatus() != null && c.getStatus() == 1).count();
270
+            item.put("online", online);
271
+            item.put("onlineRate", entry.getValue().isEmpty() ? 0 :
272
+                    Math.round((double) online / entry.getValue().size() * 10000.0) / 100.0);
273
+            result.add(item);
274
+        }
275
+        return result;
276
+    }
277
+}

+ 50
- 0
wm-production/src/main/resources/db/V2__video_intrusion.sql Datei anzeigen

@@ -0,0 +1,50 @@
1
+-- Video Monitor & AI Intrusion Detection DDL
2
+
3
+-- Enhance prod_video_camera with more fields
4
+ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS area VARCHAR(50);
5
+ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS stream_url VARCHAR(500);
6
+ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'online';
7
+ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS lng DOUBLE PRECISION;
8
+ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS lat DOUBLE PRECISION;
9
+ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS created_time TIMESTAMP DEFAULT NOW();
10
+ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS last_online_time TIMESTAMP;
11
+
12
+-- Video recordings
13
+CREATE TABLE IF NOT EXISTS prod_video_recording (
14
+    id BIGSERIAL PRIMARY KEY,
15
+    camera_id BIGINT,
16
+    recording_no VARCHAR(50),
17
+    start_time TIMESTAMP,
18
+    end_time TIMESTAMP,
19
+    duration_seconds INT,
20
+    file_size BIGINT,
21
+    file_path VARCHAR(500),
22
+    storage_type VARCHAR(20) DEFAULT 'local',
23
+    created_time TIMESTAMP DEFAULT NOW()
24
+);
25
+
26
+-- Intrusion events
27
+CREATE TABLE IF NOT EXISTS prod_intrusion_event (
28
+    id BIGSERIAL PRIMARY KEY,
29
+    event_no VARCHAR(50) UNIQUE,
30
+    camera_id VARCHAR(50),
31
+    area VARCHAR(50),
32
+    detected_time TIMESTAMP,
33
+    confidence DOUBLE PRECISION,
34
+    detected BOOLEAN DEFAULT false,
35
+    status VARCHAR(20) DEFAULT 'ACTIVE',
36
+    alert_level VARCHAR(10),
37
+    alert_triggered BOOLEAN DEFAULT false,
38
+    snapshot_url VARCHAR(500),
39
+    description TEXT,
40
+    confirmed_by VARCHAR(50),
41
+    confirmed_time TIMESTAMP,
42
+    resolved_time TIMESTAMP,
43
+    created_time TIMESTAMP DEFAULT NOW()
44
+);
45
+
46
+CREATE INDEX IF NOT EXISTS idx_vr_camera ON prod_video_recording(camera_id);
47
+CREATE INDEX IF NOT EXISTS idx_vr_time ON prod_video_recording(start_time);
48
+CREATE INDEX IF NOT EXISTS idx_ie_status ON prod_intrusion_event(status);
49
+CREATE INDEX IF NOT EXISTS idx_ie_area ON prod_intrusion_event(area);
50
+CREATE INDEX IF NOT EXISTS idx_ie_time ON prod_intrusion_event(detected_time);