diff --git a/wm-production/src/main/java/com/water/production/controller/VideoController.java b/wm-production/src/main/java/com/water/production/controller/VideoController.java new file mode 100644 index 00000000..2d517675 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/controller/VideoController.java @@ -0,0 +1,152 @@ +package com.water.production.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.common.core.result.R; +import com.water.production.entity.IntrusionEvent; +import com.water.production.entity.VideoCamera; +import com.water.production.entity.VideoRecording; +import com.water.production.service.IntrusionDetectionService; +import com.water.production.service.VideoMonitorService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@Tag(name = "视频监控与AI检测") +@RestController +@RequestMapping("/api/production/video") +@RequiredArgsConstructor +public class VideoController { + + private final VideoMonitorService videoMonitorService; + private final IntrusionDetectionService intrusionService; + + // === 摄像头管理 === + @GetMapping("/cameras") + public R> listCameras(@RequestParam(defaultValue = "1") int current, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String area, + @RequestParam(required = false) Integer status, + @RequestParam(required = false) String keyword) { + return R.ok(videoMonitorService.pageCameras(current, size, area, status, keyword)); + } + + @GetMapping("/cameras/all") + public R> listAllCameras() { + return R.ok(videoMonitorService.listAllCameras()); + } + + @GetMapping("/cameras/{id}") + public R getCamera(@PathVariable Long id) { + return R.ok(videoMonitorService.getCameraById(id)); + } + + @PostMapping("/cameras") + public R createCamera(@RequestBody VideoCamera camera) { + return R.ok(videoMonitorService.createCamera(camera)); + } + + @PutMapping("/cameras/{id}") + public R updateCamera(@PathVariable Long id, @RequestBody VideoCamera camera) { + camera.setId(id); + videoMonitorService.updateCamera(camera); + return R.ok("OK"); + } + + @DeleteMapping("/cameras/{id}") + public R deleteCamera(@PathVariable Long id) { + videoMonitorService.deleteCamera(id); + return R.ok("OK"); + } + + @PutMapping("/cameras/{id}/status") + public R updateCameraStatus(@PathVariable Long id, @RequestParam Integer status) { + videoMonitorService.updateCameraStatus(id, status); + return R.ok("OK"); + } + + // === 视频流 === + @GetMapping("/cameras/{id}/stream") + public R> getStreamUrl(@PathVariable Long id) { + return R.ok(videoMonitorService.getStreamUrls(id)); + } + + @PutMapping("/cameras/{id}/stream") + public R updateStreamUrls(@PathVariable Long id, + @RequestParam(required = false) String rtsp, + @RequestParam(required = false) String hls, + @RequestParam(required = false) String flv) { + videoMonitorService.updateStreamUrls(id, rtsp, hls, flv); + return R.ok("OK"); + } + + @PostMapping("/cameras/refresh-status") + public R> refreshAllStatus() { + return R.ok(videoMonitorService.refreshAllStatus()); + } + + // === 录像回放 === + @GetMapping("/recordings") + public R> getRecordings(@RequestParam(defaultValue = "1") int current, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) Long cameraId, + @RequestParam(required = false) String date) { + return R.ok(videoMonitorService.pageRecordings(current, size, cameraId, null, null, null)); + } + + @GetMapping("/recordings/{recordingId}/playback") + public R> getPlaybackUrl(@PathVariable Long recordingId) { + return R.ok(videoMonitorService.getPlaybackUrl(recordingId)); + } + + @PostMapping("/recordings") + public R createRecording(@RequestBody VideoRecording recording) { + return R.ok(videoMonitorService.createRecording(recording)); + } + + @DeleteMapping("/recordings/{id}") + public R deleteRecording(@PathVariable Long id) { + videoMonitorService.deleteRecording(id); + return R.ok("OK"); + } + + // === 监控统计 === + @GetMapping("/statistics/online") + public R> getOnlineStats() { + return R.ok(videoMonitorService.getDeviceOnlineStats()); + } + + @GetMapping("/statistics/area") + public R>> getAreaStats() { + return R.ok(videoMonitorService.getCameraStatsByArea()); + } + + // === AI 闯入检测 === + @PostMapping("/intrusion/detect") + public R detectIntrusion(@RequestParam String cameraId, + @RequestParam(required = false) String area) { + return R.ok(intrusionService.detect(cameraId, area)); + } + + @PostMapping("/intrusion/{id}/handle") + public R handleEvent(@PathVariable Long id, + @RequestParam String action, + @RequestParam String operator) { + return R.ok(intrusionService.handleEvent(id, action, operator)); + } + + @GetMapping("/intrusion/list") + public R> listEvents(@RequestParam(required = false) String status, + @RequestParam(required = false) String area, + @RequestParam(required = false) String alertLevel, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "20") int pageSize) { + return R.ok(intrusionService.listEvents(status, area, alertLevel, pageNum, pageSize)); + } + + @GetMapping("/intrusion/statistics") + public R> getIntrusionStatistics() { + return R.ok(intrusionService.getStatistics()); + } +} diff --git a/wm-production/src/main/java/com/water/production/entity/IntrusionEvent.java b/wm-production/src/main/java/com/water/production/entity/IntrusionEvent.java new file mode 100644 index 00000000..be8fe5c8 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/entity/IntrusionEvent.java @@ -0,0 +1,57 @@ +package com.water.production.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@TableName("prod_intrusion_event") +public class IntrusionEvent { + + @TableId(type = IdType.AUTO) + private Long id; + + private String eventNo; + + /** 关联摄像头ID */ + private String cameraId; + + /** 所属区域 */ + private String area; + + /** 检测时间 */ + private LocalDateTime detectedTime; + + /** AI识别置信度(0~1) */ + private Double confidence; + + /** 是否检测到闯入 */ + private Boolean detected; + + /** 事件状态: ACTIVE/CONFIRMED/DISMISSED/RESOLVED/FALSE_POSITIVE */ + private String status; + + /** 报警等级: 一般/重要/紧急 */ + private String alertLevel; + + /** 是否触发报警 */ + private Boolean alertTriggered; + + /** 抓拍图片URL */ + private String snapshotUrl; + + /** 描述 */ + private String description; + + /** 确认人 */ + private String confirmedBy; + + /** 确认时间 */ + private LocalDateTime confirmedTime; + + /** 解决时间 */ + private LocalDateTime resolvedTime; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdTime; +} diff --git a/wm-production/src/main/java/com/water/production/entity/VideoCamera.java b/wm-production/src/main/java/com/water/production/entity/VideoCamera.java index 864bc841..90679b41 100644 --- a/wm-production/src/main/java/com/water/production/entity/VideoCamera.java +++ b/wm-production/src/main/java/com/water/production/entity/VideoCamera.java @@ -1,10 +1,75 @@ package com.water.production.entity; + import com.baomidou.mybatisplus.annotation.*; import lombok.Data; -@Data @TableName("prod_video_camera") + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 视频监控摄像头实体(增强版) + */ +@Data +@TableName("prod_video_camera") public class VideoCamera { - @TableId(type = IdType.AUTO) private Long id; - private String cameraId, name, area; - private String streamUrl; private Integer status; // 0离线 1在线 - private Double lng, lat; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 摄像头唯一编号 */ + private String cameraId; + + /** 摄像头名称 */ + private String name; + + /** 所属区域 */ + private String area; + + /** RTSP 视频流地址 */ + private String streamUrlRtsp; + + /** HLS 视频流地址 */ + private String streamUrlHls; + + /** FLV 视频流地址 */ + private String streamUrlFlv; + + /** 状态: 0=离线, 1=在线, 2=故障 */ + private Integer status; + + /** 设备厂商 */ + private String manufacturer; + + /** 设备型号 */ + private String model; + + /** 经度 */ + private Double lng; + + /** 纬度 */ + private Double lat; + + /** 安装位置描述 */ + private String installLocation; + + /** 安装日期 */ + private LocalDate installDate; + + /** 最后在线时间 */ + private LocalDateTime lastOnlineTime; + + /** 是否启用AI检测: 0=未启用, 1=已启用 */ + private Integer aiEnabled; + + /** 备注 */ + private String remark; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedTime; + + @TableLogic + private Integer deleted; } diff --git a/wm-production/src/main/java/com/water/production/entity/VideoRecording.java b/wm-production/src/main/java/com/water/production/entity/VideoRecording.java new file mode 100644 index 00000000..11c07aba --- /dev/null +++ b/wm-production/src/main/java/com/water/production/entity/VideoRecording.java @@ -0,0 +1,63 @@ +package com.water.production.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 视频录像记录实体 + */ +@Data +@TableName("prod_video_recording") +public class VideoRecording { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 关联摄像头ID */ + private Long cameraId; + + /** 摄像头名称 */ + private String cameraName; + + /** 所属区域 */ + private String area; + + /** 录像开始时间 */ + private LocalDateTime startTime; + + /** 录像结束时间 */ + private LocalDateTime endTime; + + /** 时长(秒) */ + private Integer durationSec; + + /** 文件大小(MB) */ + private BigDecimal fileSizeMb; + + /** 存储路径 */ + private String storagePath; + + /** 回放地址 */ + private String playbackUrl; + + /** 录像类型: scheduled=计划录像, event_triggered=事件触发, manual=手动录像 */ + private String recordType; + + /** 关联闯入事件ID(事件触发时有值) */ + private Long eventId; + + /** 备注 */ + private String remark; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedTime; + + @TableLogic + private Integer deleted; +} diff --git a/wm-production/src/main/java/com/water/production/mapper/IntrusionEventMapper.java b/wm-production/src/main/java/com/water/production/mapper/IntrusionEventMapper.java new file mode 100644 index 00000000..34fc8649 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/mapper/IntrusionEventMapper.java @@ -0,0 +1,9 @@ +package com.water.production.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.production.entity.IntrusionEvent; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface IntrusionEventMapper extends BaseMapper { +} diff --git a/wm-production/src/main/java/com/water/production/mapper/VideoRecordingMapper.java b/wm-production/src/main/java/com/water/production/mapper/VideoRecordingMapper.java new file mode 100644 index 00000000..5c1b4243 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/mapper/VideoRecordingMapper.java @@ -0,0 +1,9 @@ +package com.water.production.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.production.entity.VideoRecording; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface VideoRecordingMapper extends BaseMapper { +} diff --git a/wm-production/src/main/java/com/water/production/service/IntrusionDetectionService.java b/wm-production/src/main/java/com/water/production/service/IntrusionDetectionService.java new file mode 100644 index 00000000..6f399904 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/service/IntrusionDetectionService.java @@ -0,0 +1,104 @@ +package com.water.production.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.production.entity.IntrusionEvent; +import com.water.production.mapper.IntrusionEventMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class IntrusionDetectionService { + + private final IntrusionEventMapper eventMapper; + + /** + * 模拟AI检测闯入事件 + */ + public IntrusionEvent detect(String cameraId, String area) { + double confidence = 0.7 + Math.random() * 0.3; + boolean detected = confidence > 0.85; + + IntrusionEvent event = new IntrusionEvent(); + event.setEventNo("INT-" + System.currentTimeMillis()); + event.setCameraId(cameraId); + event.setArea(area); + event.setDetectedTime(LocalDateTime.now()); + event.setConfidence(confidence); + event.setDetected(detected); + event.setStatus(detected ? "ACTIVE" : "FALSE_POSITIVE"); + event.setAlertLevel(detected ? (confidence > 0.95 ? "紧急" : "重要") : "一般"); + event.setSnapshotUrl("/snapshots/" + event.getEventNo() + ".jpg"); + + if (detected) { + event.setAlertTriggered(true); + event.setDescription(String.format("AI检测到人员闯入 (置信度: %.1f%%)", confidence * 100)); + } + + eventMapper.insert(event); + return event; + } + + /** + * 确认/处理闯入事件 + */ + public IntrusionEvent handleEvent(Long eventId, String action, String operator) { + IntrusionEvent event = eventMapper.selectById(eventId); + if (event == null) throw new RuntimeException("事件不存在"); + + switch (action) { + case "confirm" -> { event.setStatus("CONFIRMED"); event.setConfirmedBy(operator); event.setConfirmedTime(LocalDateTime.now()); } + case "dismiss" -> { event.setStatus("DISMISSED"); event.setConfirmedBy(operator); event.setConfirmedTime(LocalDateTime.now()); } + case "resolve" -> { event.setStatus("RESOLVED"); event.setResolvedTime(LocalDateTime.now()); } + default -> throw new RuntimeException("未知操作: " + action); + } + eventMapper.updateById(event); + return event; + } + + /** + * 分页查询闯入事件 + */ + public Page listEvents(String status, String area, String alertLevel, + int pageNum, int pageSize) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (status != null && !status.isBlank()) wrapper.eq(IntrusionEvent::getStatus, status); + if (area != null && !area.isBlank()) wrapper.like(IntrusionEvent::getArea, area); + if (alertLevel != null && !alertLevel.isBlank()) wrapper.eq(IntrusionEvent::getAlertLevel, alertLevel); + wrapper.orderByDesc(IntrusionEvent::getDetectedTime); + return eventMapper.selectPage(page, wrapper); + } + + /** + * 闯入事件统计 + */ + public Map getStatistics() { + long total = eventMapper.selectCount(null); + long active = eventMapper.selectCount(new LambdaQueryWrapper() + .eq(IntrusionEvent::getStatus, "ACTIVE")); + long confirmed = eventMapper.selectCount(new LambdaQueryWrapper() + .eq(IntrusionEvent::getStatus, "CONFIRMED")); + long todayEvents = eventMapper.selectCount(new LambdaQueryWrapper() + .ge(IntrusionEvent::getDetectedTime, LocalDateTime.now().toLocalDate().atStartOfDay())); + + double avgConfidence = eventMapper.selectList(null).stream() + .mapToDouble(e -> e.getConfidence() != null ? e.getConfidence() : 0) + .average().orElse(0); + + Map stats = new LinkedHashMap<>(); + stats.put("total", total); + stats.put("active", active); + stats.put("confirmed", confirmed); + stats.put("todayEvents", todayEvents); + stats.put("avgConfidence", avgConfidence); + stats.put("falsePositiveRate", total > 0 ? + (double) eventMapper.selectCount(new LambdaQueryWrapper() + .eq(IntrusionEvent::getStatus, "FALSE_POSITIVE")) / total : 0); + return stats; + } +} diff --git a/wm-production/src/main/java/com/water/production/service/VideoMonitorService.java b/wm-production/src/main/java/com/water/production/service/VideoMonitorService.java new file mode 100644 index 00000000..7f59d28b --- /dev/null +++ b/wm-production/src/main/java/com/water/production/service/VideoMonitorService.java @@ -0,0 +1,277 @@ +package com.water.production.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.production.entity.VideoCamera; +import com.water.production.entity.VideoRecording; +import com.water.production.mapper.VideoCameraMapper; +import com.water.production.mapper.VideoRecordingMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 视频监控管理服务 + * 提供摄像头CRUD、视频流地址管理、状态监控、录像查询、统计等能力 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VideoMonitorService { + + private final VideoCameraMapper cameraMapper; + private final VideoRecordingMapper recordingMapper; + + // ==================== 摄像头 CRUD ==================== + + /** + * 分页查询摄像头列表 + */ + public Page pageCameras(int current, int size, String area, Integer status, String keyword) { + Page page = new Page<>(current, size); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(area != null, VideoCamera::getArea, area) + .eq(status != null, VideoCamera::getStatus, status) + .and(StringUtils.hasText(keyword), w -> + w.like(VideoCamera::getName, keyword) + .or().like(VideoCamera::getCameraId, keyword)) + .orderByDesc(VideoCamera::getCreatedTime); + return cameraMapper.selectPage(page, wrapper); + } + + /** + * 获取所有摄像头 + */ + public List listAllCameras() { + return cameraMapper.selectList(new LambdaQueryWrapper() + .orderByDesc(VideoCamera::getCreatedTime)); + } + + /** + * 获取摄像头详情 + */ + public VideoCamera getCameraById(Long id) { + return cameraMapper.selectById(id); + } + + /** + * 创建摄像头 + */ + public VideoCamera createCamera(VideoCamera camera) { + camera.setStatus(camera.getStatus() != null ? camera.getStatus() : 0); + camera.setAiEnabled(camera.getAiEnabled() != null ? camera.getAiEnabled() : 0); + camera.setCreatedTime(LocalDateTime.now()); + camera.setUpdatedTime(LocalDateTime.now()); + camera.setDeleted(0); + cameraMapper.insert(camera); + log.info("创建摄像头: {} ({})", camera.getName(), camera.getCameraId()); + return camera; + } + + /** + * 更新摄像头 + */ + public boolean updateCamera(VideoCamera camera) { + camera.setUpdatedTime(LocalDateTime.now()); + int rows = cameraMapper.updateById(camera); + if (rows > 0) { + log.info("更新摄像头: id={}", camera.getId()); + } + return rows > 0; + } + + /** + * 删除摄像头 + */ + public boolean deleteCamera(Long id) { + int rows = cameraMapper.deleteById(id); + if (rows > 0) { + log.info("删除摄像头: id={}", id); + } + return rows > 0; + } + + // ==================== 视频流地址管理 ==================== + + /** + * 获取摄像头的所有视频流地址 + */ + public Map getStreamUrls(Long cameraId) { + VideoCamera camera = cameraMapper.selectById(cameraId); + if (camera == null) { + return Collections.emptyMap(); + } + Map urls = new LinkedHashMap<>(); + urls.put("cameraId", camera.getCameraId()); + urls.put("name", camera.getName()); + urls.put("rtsp", camera.getStreamUrlRtsp()); + urls.put("hls", camera.getStreamUrlHls()); + urls.put("flv", camera.getStreamUrlFlv()); + urls.put("status", camera.getStatus()); + return urls; + } + + /** + * 更新视频流地址 + */ + public boolean updateStreamUrls(Long cameraId, String rtsp, String hls, String flv) { + VideoCamera camera = new VideoCamera(); + camera.setId(cameraId); + camera.setStreamUrlRtsp(rtsp); + camera.setStreamUrlHls(hls); + camera.setStreamUrlFlv(flv); + camera.setUpdatedTime(LocalDateTime.now()); + return cameraMapper.updateById(camera) > 0; + } + + // ==================== 状态监控 ==================== + + /** + * 更新摄像头在线状态(模拟心跳检测) + */ + public boolean updateCameraStatus(Long cameraId, Integer status) { + VideoCamera camera = new VideoCamera(); + camera.setId(cameraId); + camera.setStatus(status); + if (status != null && status == 1) { + camera.setLastOnlineTime(LocalDateTime.now()); + } + camera.setUpdatedTime(LocalDateTime.now()); + return cameraMapper.updateById(camera) > 0; + } + + /** + * 批量刷新摄像头状态(模拟) + * 实际场景: 从流媒体服务器获取在线状态 + */ + public Map refreshAllStatus() { + List cameras = cameraMapper.selectList(null); + int online = 0, offline = 0, fault = 0; + for (VideoCamera cam : cameras) { + // 模拟: 根据最后在线时间判断状态 + if (cam.getLastOnlineTime() != null + && Duration.between(cam.getLastOnlineTime(), LocalDateTime.now()).toMinutes() < 5) { + online++; + } else if (cam.getStatus() != null && cam.getStatus() == 2) { + fault++; + } else { + offline++; + } + } + Map result = new LinkedHashMap<>(); + result.put("total", cameras.size()); + result.put("online", online); + result.put("offline", offline); + result.put("fault", fault); + result.put("refreshTime", LocalDateTime.now()); + return result; + } + + // ==================== 视频录像 ==================== + + /** + * 分页查询录像记录 + */ + public Page pageRecordings(int current, int size, Long cameraId, + String recordType, String startDate, String endDate) { + Page page = new Page<>(current, size); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(cameraId != null, VideoRecording::getCameraId, cameraId) + .eq(recordType != null, VideoRecording::getRecordType, recordType) + .ge(StringUtils.hasText(startDate), VideoRecording::getStartTime, + startDate != null ? startDate + " 00:00:00" : null) + .le(StringUtils.hasText(endDate), VideoRecording::getEndTime, + endDate != null ? endDate + " 23:59:59" : null) + .orderByDesc(VideoRecording::getStartTime); + return recordingMapper.selectPage(page, wrapper); + } + + /** + * 生成回放地址 + */ + public Map getPlaybackUrl(Long recordingId) { + VideoRecording recording = recordingMapper.selectById(recordingId); + if (recording == null) { + return Collections.emptyMap(); + } + Map result = new LinkedHashMap<>(); + result.put("recordingId", recording.getId()); + result.put("cameraId", recording.getCameraId()); + result.put("cameraName", recording.getCameraName()); + result.put("startTime", recording.getStartTime()); + result.put("endTime", recording.getEndTime()); + result.put("durationSec", recording.getDurationSec()); + result.put("playbackUrl", recording.getPlaybackUrl()); + result.put("fileSizeMb", recording.getFileSizeMb()); + return result; + } + + /** + * 创建录像记录 + */ + public VideoRecording createRecording(VideoRecording recording) { + recording.setCreatedTime(LocalDateTime.now()); + recording.setUpdatedTime(LocalDateTime.now()); + recording.setDeleted(0); + recordingMapper.insert(recording); + return recording; + } + + /** + * 删除录像记录 + */ + public boolean deleteRecording(Long id) { + return recordingMapper.deleteById(id) > 0; + } + + // ==================== 统计 ==================== + + /** + * 设备在线率统计 + */ + public Map getDeviceOnlineStats() { + List cameras = cameraMapper.selectList(null); + long total = cameras.size(); + long online = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 1).count(); + long offline = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 0).count(); + long fault = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 2).count(); + double onlineRate = total > 0 ? (double) online / total * 100 : 0; + + Map stats = new LinkedHashMap<>(); + stats.put("total", total); + stats.put("online", online); + stats.put("offline", offline); + stats.put("fault", fault); + stats.put("onlineRate", Math.round(onlineRate * 100.0) / 100.0); + return stats; + } + + /** + * 按区域统计摄像头分布 + */ + public List> getCameraStatsByArea() { + List cameras = cameraMapper.selectList(null); + Map> grouped = cameras.stream() + .filter(c -> c.getArea() != null) + .collect(Collectors.groupingBy(VideoCamera::getArea)); + + List> result = new ArrayList<>(); + for (Map.Entry> entry : grouped.entrySet()) { + Map item = new LinkedHashMap<>(); + item.put("area", entry.getKey()); + item.put("total", entry.getValue().size()); + long online = entry.getValue().stream().filter(c -> c.getStatus() != null && c.getStatus() == 1).count(); + item.put("online", online); + item.put("onlineRate", entry.getValue().isEmpty() ? 0 : + Math.round((double) online / entry.getValue().size() * 10000.0) / 100.0); + result.add(item); + } + return result; + } +} diff --git a/wm-production/src/main/resources/db/V2__video_intrusion.sql b/wm-production/src/main/resources/db/V2__video_intrusion.sql new file mode 100644 index 00000000..b43ce9d0 --- /dev/null +++ b/wm-production/src/main/resources/db/V2__video_intrusion.sql @@ -0,0 +1,50 @@ +-- Video Monitor & AI Intrusion Detection DDL + +-- Enhance prod_video_camera with more fields +ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS area VARCHAR(50); +ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS stream_url VARCHAR(500); +ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'online'; +ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS lng DOUBLE PRECISION; +ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS lat DOUBLE PRECISION; +ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS created_time TIMESTAMP DEFAULT NOW(); +ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS last_online_time TIMESTAMP; + +-- Video recordings +CREATE TABLE IF NOT EXISTS prod_video_recording ( + id BIGSERIAL PRIMARY KEY, + camera_id BIGINT, + recording_no VARCHAR(50), + start_time TIMESTAMP, + end_time TIMESTAMP, + duration_seconds INT, + file_size BIGINT, + file_path VARCHAR(500), + storage_type VARCHAR(20) DEFAULT 'local', + created_time TIMESTAMP DEFAULT NOW() +); + +-- Intrusion events +CREATE TABLE IF NOT EXISTS prod_intrusion_event ( + id BIGSERIAL PRIMARY KEY, + event_no VARCHAR(50) UNIQUE, + camera_id VARCHAR(50), + area VARCHAR(50), + detected_time TIMESTAMP, + confidence DOUBLE PRECISION, + detected BOOLEAN DEFAULT false, + status VARCHAR(20) DEFAULT 'ACTIVE', + alert_level VARCHAR(10), + alert_triggered BOOLEAN DEFAULT false, + snapshot_url VARCHAR(500), + description TEXT, + confirmed_by VARCHAR(50), + confirmed_time TIMESTAMP, + resolved_time TIMESTAMP, + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_vr_camera ON prod_video_recording(camera_id); +CREATE INDEX IF NOT EXISTS idx_vr_time ON prod_video_recording(start_time); +CREATE INDEX IF NOT EXISTS idx_ie_status ON prod_intrusion_event(status); +CREATE INDEX IF NOT EXISTS idx_ie_area ON prod_intrusion_event(area); +CREATE INDEX IF NOT EXISTS idx_ie_time ON prod_intrusion_event(detected_time);