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

- VideoMonitorService: 摄像头CRUD/视频流管理/状态监控/录像回放/统计
- IntrusionDetectionService: AI闯入检测(模拟)/事件处理/分页查询/统计
- VideoController: 20+ API端点 (/api/production/video/*)
- DDL: prod_video_recording + prod_intrusion_event + 索引
This commit is contained in:
2026-06-14 16:15:50 +08:00
parent 21cf0e97af
commit dcb412c7e4
9 changed files with 791 additions and 5 deletions
@@ -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<Page<VideoCamera>> 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<List<VideoCamera>> listAllCameras() {
return R.ok(videoMonitorService.listAllCameras());
}
@GetMapping("/cameras/{id}")
public R<VideoCamera> getCamera(@PathVariable Long id) {
return R.ok(videoMonitorService.getCameraById(id));
}
@PostMapping("/cameras")
public R<VideoCamera> createCamera(@RequestBody VideoCamera camera) {
return R.ok(videoMonitorService.createCamera(camera));
}
@PutMapping("/cameras/{id}")
public R<String> updateCamera(@PathVariable Long id, @RequestBody VideoCamera camera) {
camera.setId(id);
videoMonitorService.updateCamera(camera);
return R.ok("OK");
}
@DeleteMapping("/cameras/{id}")
public R<String> deleteCamera(@PathVariable Long id) {
videoMonitorService.deleteCamera(id);
return R.ok("OK");
}
@PutMapping("/cameras/{id}/status")
public R<String> updateCameraStatus(@PathVariable Long id, @RequestParam Integer status) {
videoMonitorService.updateCameraStatus(id, status);
return R.ok("OK");
}
// === 视频流 ===
@GetMapping("/cameras/{id}/stream")
public R<Map<String, Object>> getStreamUrl(@PathVariable Long id) {
return R.ok(videoMonitorService.getStreamUrls(id));
}
@PutMapping("/cameras/{id}/stream")
public R<String> 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<Map<String, Object>> refreshAllStatus() {
return R.ok(videoMonitorService.refreshAllStatus());
}
// === 录像回放 ===
@GetMapping("/recordings")
public R<Page<VideoRecording>> 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<Map<String, Object>> getPlaybackUrl(@PathVariable Long recordingId) {
return R.ok(videoMonitorService.getPlaybackUrl(recordingId));
}
@PostMapping("/recordings")
public R<VideoRecording> createRecording(@RequestBody VideoRecording recording) {
return R.ok(videoMonitorService.createRecording(recording));
}
@DeleteMapping("/recordings/{id}")
public R<String> deleteRecording(@PathVariable Long id) {
videoMonitorService.deleteRecording(id);
return R.ok("OK");
}
// === 监控统计 ===
@GetMapping("/statistics/online")
public R<Map<String, Object>> getOnlineStats() {
return R.ok(videoMonitorService.getDeviceOnlineStats());
}
@GetMapping("/statistics/area")
public R<List<Map<String, Object>>> getAreaStats() {
return R.ok(videoMonitorService.getCameraStatsByArea());
}
// === AI 闯入检测 ===
@PostMapping("/intrusion/detect")
public R<IntrusionEvent> detectIntrusion(@RequestParam String cameraId,
@RequestParam(required = false) String area) {
return R.ok(intrusionService.detect(cameraId, area));
}
@PostMapping("/intrusion/{id}/handle")
public R<IntrusionEvent> handleEvent(@PathVariable Long id,
@RequestParam String action,
@RequestParam String operator) {
return R.ok(intrusionService.handleEvent(id, action, operator));
}
@GetMapping("/intrusion/list")
public R<Page<IntrusionEvent>> 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<Map<String, Object>> getIntrusionStatistics() {
return R.ok(intrusionService.getStatistics());
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<IntrusionEvent> {
}
@@ -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<VideoRecording> {
}
@@ -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<IntrusionEvent> listEvents(String status, String area, String alertLevel,
int pageNum, int pageSize) {
Page<IntrusionEvent> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<IntrusionEvent> 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<String, Object> getStatistics() {
long total = eventMapper.selectCount(null);
long active = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
.eq(IntrusionEvent::getStatus, "ACTIVE"));
long confirmed = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
.eq(IntrusionEvent::getStatus, "CONFIRMED"));
long todayEvents = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
.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<String, Object> 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<IntrusionEvent>()
.eq(IntrusionEvent::getStatus, "FALSE_POSITIVE")) / total : 0);
return stats;
}
}
@@ -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<VideoCamera> pageCameras(int current, int size, String area, Integer status, String keyword) {
Page<VideoCamera> page = new Page<>(current, size);
LambdaQueryWrapper<VideoCamera> 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<VideoCamera> listAllCameras() {
return cameraMapper.selectList(new LambdaQueryWrapper<VideoCamera>()
.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<String, Object> getStreamUrls(Long cameraId) {
VideoCamera camera = cameraMapper.selectById(cameraId);
if (camera == null) {
return Collections.emptyMap();
}
Map<String, Object> 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<String, Object> refreshAllStatus() {
List<VideoCamera> 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<String, Object> 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<VideoRecording> pageRecordings(int current, int size, Long cameraId,
String recordType, String startDate, String endDate) {
Page<VideoRecording> page = new Page<>(current, size);
LambdaQueryWrapper<VideoRecording> 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<String, Object> getPlaybackUrl(Long recordingId) {
VideoRecording recording = recordingMapper.selectById(recordingId);
if (recording == null) {
return Collections.emptyMap();
}
Map<String, Object> 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<String, Object> getDeviceOnlineStats() {
List<VideoCamera> 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<String, Object> 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<Map<String, Object>> getCameraStatsByArea() {
List<VideoCamera> cameras = cameraMapper.selectList(null);
Map<String, List<VideoCamera>> grouped = cameras.stream()
.filter(c -> c.getArea() != null)
.collect(Collectors.groupingBy(VideoCamera::getArea));
List<Map<String, Object>> result = new ArrayList<>();
for (Map.Entry<String, List<VideoCamera>> entry : grouped.entrySet()) {
Map<String, Object> 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;
}
}
@@ -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);