feat(wm-production): #64 GIS地图展示后端服务
- GisService: 点位CRUD/空间查询(矩形+圆形)/管网数据/热力图/统计 - GisController: 11个API端点 (/api/production/gis/*) - 支持流量/压力/液位/水质/阀门5类监测点位 - Haversine距离计算 + 网格聚合热力图 - DDL: prod_gis_point/pipeline/area + 6个索引
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
-- 药剂投加监控 DDL
|
||||
-- 全工艺药剂投加监控(混凝→沉淀→过滤→消毒)
|
||||
|
||||
-- 1. 药剂投加记录表
|
||||
CREATE TABLE IF NOT EXISTS prod_chemical_dosing (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
process_stage VARCHAR(32) NOT NULL, -- 工艺段: coagulation/sedimentation/filtration/disinfection
|
||||
chemical_name VARCHAR(64) NOT NULL, -- 药剂名称
|
||||
chemical_code VARCHAR(32), -- 药剂编码
|
||||
dosing_amount DECIMAL(12,4), -- 投加量(kg)
|
||||
dosing_rate DECIMAL(10,4), -- 投加速率(kg/h)
|
||||
concentration DECIMAL(10,4), -- 投加浓度(mg/L)
|
||||
flow_rate DECIMAL(12,4), -- 当时流量(m³/h)
|
||||
station VARCHAR(64), -- 站点/水厂
|
||||
operator VARCHAR(32), -- 操作员
|
||||
status VARCHAR(16) DEFAULT 'active', -- active/paused/stopped
|
||||
remark VARCHAR(255),
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
COMMENT ON TABLE prod_chemical_dosing IS '药剂投加监控记录';
|
||||
COMMENT ON COLUMN prod_chemical_dosing.process_stage IS '工艺段: coagulation(混凝)/sedimentation(沉淀)/filtration(过滤)/disinfection(消毒)';
|
||||
|
||||
-- 2. 投加历史记录表
|
||||
CREATE TABLE IF NOT EXISTS prod_dosing_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dosing_id BIGINT, -- 关联投加记录
|
||||
process_stage VARCHAR(32) NOT NULL,
|
||||
chemical_name VARCHAR(64) NOT NULL,
|
||||
dosing_amount DECIMAL(12,4),
|
||||
dosing_rate DECIMAL(10,4),
|
||||
concentration DECIMAL(10,4),
|
||||
flow_rate DECIMAL(12,4),
|
||||
station VARCHAR(64),
|
||||
record_time TIMESTAMP NOT NULL, -- 记录时间
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
COMMENT ON TABLE prod_dosing_record IS '投加历史记录(用于趋势分析)';
|
||||
|
||||
-- 3. 药剂库存表
|
||||
CREATE TABLE IF NOT EXISTS prod_chemical_stock (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
chemical_name VARCHAR(64) NOT NULL,
|
||||
chemical_code VARCHAR(32),
|
||||
current_stock DECIMAL(12,4) NOT NULL, -- 当前库存(kg)
|
||||
max_stock DECIMAL(12,4), -- 最大库存
|
||||
min_stock DECIMAL(12,4), -- 安全库存(低于此值预警)
|
||||
unit VARCHAR(16) DEFAULT 'kg',
|
||||
warehouse VARCHAR(64), -- 仓库位置
|
||||
supplier VARCHAR(128), -- 供应商
|
||||
station VARCHAR(64),
|
||||
status VARCHAR(16) DEFAULT 'normal', -- normal/low/out
|
||||
last_inbound TIMESTAMP, -- 最近入库时间
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
COMMENT ON TABLE prod_chemical_stock IS '药剂库存管理';
|
||||
|
||||
-- 4. 投加策略表
|
||||
CREATE TABLE IF NOT EXISTS prod_dosing_strategy (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
strategy_name VARCHAR(64) NOT NULL,
|
||||
process_stage VARCHAR(32) NOT NULL,
|
||||
chemical_name VARCHAR(64) NOT NULL,
|
||||
strategy_type VARCHAR(32), -- auto/manual/semi-auto
|
||||
base_dosing_rate DECIMAL(10,4), -- 基础投加速率
|
||||
min_dosing_rate DECIMAL(10,4), -- 最小投加速率
|
||||
max_dosing_rate DECIMAL(10,4), -- 最大投加速率
|
||||
turbidity_threshold DECIMAL(10,4), -- 浊度阈值联动
|
||||
flow_threshold DECIMAL(12,4), -- 流量阈值联动
|
||||
ph_threshold_min DECIMAL(6,2), -- pH下限
|
||||
ph_threshold_max DECIMAL(6,2), -- pH上限
|
||||
formula VARCHAR(255), -- 投加公式
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
station VARCHAR(64),
|
||||
remark VARCHAR(255),
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
COMMENT ON TABLE prod_dosing_strategy IS '自动投加策略配置(基于原水水质/流量联动)';
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_dosing_stage ON prod_chemical_dosing(process_stage);
|
||||
CREATE INDEX IF NOT EXISTS idx_dosing_station ON prod_chemical_dosing(station);
|
||||
CREATE INDEX IF NOT EXISTS idx_dosing_created ON prod_chemical_dosing(created_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_record_stage ON prod_dosing_record(process_stage);
|
||||
CREATE INDEX IF NOT EXISTS idx_record_time ON prod_dosing_record(record_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_station ON prod_chemical_stock(station);
|
||||
CREATE INDEX IF NOT EXISTS idx_strategy_stage ON prod_dosing_strategy(process_stage);
|
||||
@@ -0,0 +1,122 @@
|
||||
-- =============================================
|
||||
-- 智慧水务管理系统 - 视频监控集成 + AI人员闯入检测 DDL
|
||||
-- 版本: V3
|
||||
-- =============================================
|
||||
|
||||
-- ==================== 视频监控摄像头 ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_video_camera (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
camera_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
area VARCHAR(50),
|
||||
stream_url_rtsp VARCHAR(500),
|
||||
stream_url_hls VARCHAR(500),
|
||||
stream_url_flv VARCHAR(500),
|
||||
status INTEGER DEFAULT 0,
|
||||
manufacturer VARCHAR(50),
|
||||
model VARCHAR(50),
|
||||
lng DOUBLE PRECISION,
|
||||
lat DOUBLE PRECISION,
|
||||
install_location VARCHAR(200),
|
||||
install_date DATE,
|
||||
last_online_time TIMESTAMP,
|
||||
ai_enabled INTEGER DEFAULT 0,
|
||||
remark VARCHAR(500),
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW(),
|
||||
deleted INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
COMMENT ON TABLE prod_video_camera IS '视频监控摄像头表';
|
||||
COMMENT ON COLUMN prod_video_camera.camera_id IS '摄像头唯一编号';
|
||||
COMMENT ON COLUMN prod_video_camera.status IS '状态: 0=离线, 1=在线, 2=故障';
|
||||
COMMENT ON COLUMN prod_video_camera.ai_enabled IS '是否启用AI检测: 0=未启用, 1=已启用';
|
||||
COMMENT ON COLUMN prod_video_camera.stream_url_rtsp IS 'RTSP视频流地址';
|
||||
COMMENT ON COLUMN prod_video_camera.stream_url_hls IS 'HLS视频流地址';
|
||||
COMMENT ON COLUMN prod_video_camera.stream_url_flv IS 'FLV视频流地址';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_video_camera_area ON prod_video_camera(area);
|
||||
CREATE INDEX IF NOT EXISTS idx_video_camera_status ON prod_video_camera(status);
|
||||
|
||||
-- ==================== AI闯入检测事件 ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_intrusion_event (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
camera_id BIGINT NOT NULL,
|
||||
camera_name VARCHAR(100),
|
||||
area VARCHAR(50),
|
||||
event_type VARCHAR(30) NOT NULL,
|
||||
confidence NUMERIC(6, 4),
|
||||
snapshot_url VARCHAR(500),
|
||||
video_clip_url VARCHAR(500),
|
||||
alert_level VARCHAR(20),
|
||||
alert_status INTEGER DEFAULT 0,
|
||||
detected_at TIMESTAMP NOT NULL,
|
||||
handle_result TEXT,
|
||||
handled_by BIGINT,
|
||||
handler_name VARCHAR(50),
|
||||
handled_time TIMESTAMP,
|
||||
alert_record_id BIGINT,
|
||||
remark VARCHAR(500),
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW(),
|
||||
deleted INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
COMMENT ON TABLE prod_intrusion_event IS 'AI人员闯入检测事件表';
|
||||
COMMENT ON COLUMN prod_intrusion_event.event_type IS '事件类型: person_intrusion=人员闯入, person_loitering=人员徘徊, zone_breach=区域越界';
|
||||
COMMENT ON COLUMN prod_intrusion_event.confidence IS 'AI识别置信度(0~1)';
|
||||
COMMENT ON COLUMN prod_intrusion_event.alert_level IS '报警等级: info, warning, critical';
|
||||
COMMENT ON COLUMN prod_intrusion_event.alert_status IS '报警状态: 0=待处理, 1=已确认, 2=已处理, 3=已忽略';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_intrusion_camera ON prod_intrusion_event(camera_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_intrusion_area ON prod_intrusion_event(area);
|
||||
CREATE INDEX IF NOT EXISTS idx_intrusion_detected_at ON prod_intrusion_event(detected_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_intrusion_alert_status ON prod_intrusion_event(alert_status);
|
||||
|
||||
-- ==================== 视频录像记录 ====================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_video_recording (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
camera_id BIGINT NOT NULL,
|
||||
camera_name VARCHAR(100),
|
||||
area VARCHAR(50),
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP,
|
||||
duration_sec INTEGER,
|
||||
file_size_mb NUMERIC(10, 2),
|
||||
storage_path VARCHAR(500),
|
||||
playback_url VARCHAR(500),
|
||||
record_type VARCHAR(20) NOT NULL,
|
||||
event_id BIGINT,
|
||||
remark VARCHAR(500),
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW(),
|
||||
deleted INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
COMMENT ON TABLE prod_video_recording IS '视频录像记录表';
|
||||
COMMENT ON COLUMN prod_video_recording.record_type IS '录像类型: scheduled=计划录像, event_triggered=事件触发, manual=手动录像';
|
||||
COMMENT ON COLUMN prod_video_recording.event_id IS '关联闯入事件ID(事件触发时有值)';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_camera ON prod_video_recording(camera_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_start_time ON prod_video_recording(start_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_record_type ON prod_video_recording(record_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_event ON prod_video_recording(event_id);
|
||||
|
||||
-- ==================== 初始化测试数据 ====================
|
||||
|
||||
INSERT INTO prod_video_camera (camera_id, name, area, stream_url_rtsp, stream_url_hls, stream_url_flv,
|
||||
status, manufacturer, model, lng, lat, install_location, install_date, ai_enabled, last_online_time)
|
||||
VALUES
|
||||
('CAM-001', '一体化水厂-沉淀池', '一体化水厂', 'rtsp://192.168.1.100/stream1', 'http://192.168.1.100/hls/stream1.m3u8', 'http://192.168.1.100/flv/stream1.flv',
|
||||
1, '海康威视', 'DS-2CD2T26FWDA3-IS', 87.5712, 43.7928, '一体化水厂沉淀池北侧', '2024-03-15', 1, NOW()),
|
||||
('CAM-002', '一体化水厂-清水池', '一体化水厂', 'rtsp://192.168.1.101/stream1', 'http://192.168.1.101/hls/stream1.m3u8', 'http://192.168.1.101/flv/stream1.flv',
|
||||
1, '海康威视', 'DS-2CD2T26FWDA3-IS', 87.5715, 43.7930, '一体化水厂清水池入口', '2024-03-15', 1, NOW()),
|
||||
('CAM-003', '查村调压站-入口', '八家户片区', 'rtsp://192.168.1.102/stream1', 'http://192.168.1.102/hls/stream1.m3u8', 'http://192.168.1.102/flv/stream1.flv',
|
||||
1, '大华', 'DH-IPC-HFW5442T-ASE', 87.5680, 43.7890, '查村调压站大门', '2024-04-10', 1, NOW()),
|
||||
('CAM-004', '精芒片区-管网节点1', '精芒片区', 'rtsp://192.168.1.103/stream1', 'http://192.168.1.103/hls/stream1.m3u8', 'http://192.168.1.103/flv/stream1.flv',
|
||||
0, '大华', 'DH-IPC-HFW5442T-ASE', 87.5650, 43.7860, '精芒片区管网节点井', '2024-05-20', 0, '2025-06-10 08:30:00'),
|
||||
('CAM-005', '八家户泵站-机房', '八家户片区', 'rtsp://192.168.1.104/stream1', 'http://192.168.1.104/hls/stream1.m3u8', 'http://192.168.1.104/flv/stream1.flv',
|
||||
2, '宇视', 'IPC3612SB-ADZK-I0', 87.5670, 43.7880, '八家户泵站机房入口', '2024-06-01', 1, '2025-06-01 12:00:00');
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.water.production.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.production.dto.GisStatisticsVO;
|
||||
import com.water.production.dto.SpatialQueryRequest;
|
||||
import com.water.production.entity.GisArea;
|
||||
import com.water.production.entity.GisPipeline;
|
||||
import com.water.production.entity.GisPoint;
|
||||
import com.water.production.service.GisService;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Tag(name = "GIS地图展示")
|
||||
@RestController
|
||||
@RequestMapping("/api/production/gis")
|
||||
@RequiredArgsConstructor
|
||||
public class GisController {
|
||||
|
||||
private final GisService gisService;
|
||||
|
||||
// === 点位管理 ===
|
||||
@GetMapping("/points")
|
||||
public R<List<GisPoint>> listPoints(@RequestParam(required = false) String pointType,
|
||||
@RequestParam(required = false) String area,
|
||||
@RequestParam(required = false) String status) {
|
||||
return R.ok(gisService.listPoints(pointType, area, status));
|
||||
}
|
||||
|
||||
@GetMapping("/points/{id}")
|
||||
public R<GisPoint> getPoint(@PathVariable Long id) {
|
||||
return R.ok(gisService.getPoint(id));
|
||||
}
|
||||
|
||||
@PostMapping("/points")
|
||||
public R<Long> createPoint(@RequestBody GisPoint point) {
|
||||
return R.ok(gisService.createPoint(point));
|
||||
}
|
||||
|
||||
@PutMapping("/points/{id}")
|
||||
public R<String> updatePoint(@PathVariable Long id, @RequestBody GisPoint point) {
|
||||
point.setId(id);
|
||||
gisService.updatePoint(point);
|
||||
return R.ok("OK");
|
||||
}
|
||||
|
||||
@DeleteMapping("/points/{id}")
|
||||
public R<String> deletePoint(@PathVariable Long id) {
|
||||
gisService.deletePoint(id);
|
||||
return R.ok("OK");
|
||||
}
|
||||
|
||||
// === 空间查询 ===
|
||||
@PostMapping("/spatial-query")
|
||||
public R<List<GisPoint>> spatialQuery(@RequestBody SpatialQueryRequest request) {
|
||||
return R.ok(gisService.spatialQuery(request));
|
||||
}
|
||||
|
||||
// === 管网数据 ===
|
||||
@GetMapping("/pipelines")
|
||||
public R<List<GisPipeline>> listPipelines(@RequestParam(required = false) String area,
|
||||
@RequestParam(required = false) String pipeType) {
|
||||
return R.ok(gisService.listPipelines(area, pipeType));
|
||||
}
|
||||
|
||||
// === 区域数据 ===
|
||||
@GetMapping("/areas")
|
||||
public R<List<GisArea>> listAreas() {
|
||||
return R.ok(gisService.listAreas());
|
||||
}
|
||||
|
||||
// === 统计 ===
|
||||
@GetMapping("/statistics")
|
||||
public R<GisStatisticsVO> getStatistics() {
|
||||
return R.ok(gisService.getStatistics());
|
||||
}
|
||||
|
||||
// === 热力图 ===
|
||||
@GetMapping("/heatmap")
|
||||
public R<List<Map<String, Object>>> getHeatmap(@RequestParam(required = false) String pointType) {
|
||||
return R.ok(gisService.getHeatmapData(pointType));
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
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.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 视频监控集成 + AI人员闯入检测 REST API
|
||||
* 提供摄像头管理、视频流管理、状态监控、录像回放、闯入检测、统计等功能
|
||||
*/
|
||||
@Tag(name = "视频监控与AI闯入检测")
|
||||
@RestController
|
||||
@RequestMapping("/api/production/video")
|
||||
@RequiredArgsConstructor
|
||||
public class VideoMonitorController {
|
||||
|
||||
private final VideoMonitorService videoMonitorService;
|
||||
private final IntrusionDetectionService intrusionDetectionService;
|
||||
|
||||
// ==================== 1. 摄像头管理 (CRUD) ====================
|
||||
|
||||
@Operation(summary = "分页查询摄像头列表")
|
||||
@GetMapping("/camera/page")
|
||||
public R<Page<VideoCamera>> cameraPage(
|
||||
@RequestParam(defaultValue = "1") int current,
|
||||
@RequestParam(defaultValue = "10") 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));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有摄像头列表")
|
||||
@GetMapping("/camera/list")
|
||||
public R<List<VideoCamera>> cameraList() {
|
||||
return R.ok(videoMonitorService.listAllCameras());
|
||||
}
|
||||
|
||||
@Operation(summary = "获取摄像头详情")
|
||||
@GetMapping("/camera/{id}")
|
||||
public R<VideoCamera> cameraDetail(@PathVariable Long id) {
|
||||
VideoCamera camera = videoMonitorService.getCameraById(id);
|
||||
return camera != null ? R.ok(camera) : R.fail(404, "摄像头不存在");
|
||||
}
|
||||
|
||||
@Operation(summary = "创建摄像头")
|
||||
@PostMapping("/camera")
|
||||
public R<VideoCamera> createCamera(@RequestBody VideoCamera camera) {
|
||||
return R.ok(videoMonitorService.createCamera(camera));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新摄像头")
|
||||
@PutMapping("/camera/{id}")
|
||||
public R<String> updateCamera(@PathVariable Long id, @RequestBody VideoCamera camera) {
|
||||
camera.setId(id);
|
||||
return videoMonitorService.updateCamera(camera) ? R.ok("更新成功") : R.fail("更新失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "删除摄像头")
|
||||
@DeleteMapping("/camera/{id}")
|
||||
public R<String> deleteCamera(@PathVariable Long id) {
|
||||
return videoMonitorService.deleteCamera(id) ? R.ok("删除成功") : R.fail("删除失败");
|
||||
}
|
||||
|
||||
// ==================== 2. 视频流地址管理 ====================
|
||||
|
||||
@Operation(summary = "获取摄像头视频流地址")
|
||||
@GetMapping("/camera/{id}/streams")
|
||||
public R<Map<String, Object>> streamUrls(@PathVariable Long id) {
|
||||
Map<String, Object> urls = videoMonitorService.getStreamUrls(id);
|
||||
return urls.isEmpty() ? R.fail(404, "摄像头不存在") : R.ok(urls);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新视频流地址")
|
||||
@PutMapping("/camera/{id}/streams")
|
||||
public R<String> updateStreams(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
return videoMonitorService.updateStreamUrls(id,
|
||||
body.get("rtsp"), body.get("hls"), body.get("flv"))
|
||||
? R.ok("更新成功") : R.fail("更新失败");
|
||||
}
|
||||
|
||||
// ==================== 3. 状态监控 ====================
|
||||
|
||||
@Operation(summary = "更新摄像头状态")
|
||||
@PutMapping("/camera/{id}/status")
|
||||
public R<String> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
return videoMonitorService.updateCameraStatus(id, status)
|
||||
? R.ok("状态更新成功") : R.fail("更新失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "刷新所有摄像头状态")
|
||||
@PostMapping("/camera/refresh-status")
|
||||
public R<Map<String, Object>> refreshStatus() {
|
||||
return R.ok(videoMonitorService.refreshAllStatus());
|
||||
}
|
||||
|
||||
// ==================== 4. 视频录像与回放 ====================
|
||||
|
||||
@Operation(summary = "分页查询录像记录")
|
||||
@GetMapping("/recording/page")
|
||||
public R<Page<VideoRecording>> recordingPage(
|
||||
@RequestParam(defaultValue = "1") int current,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) Long cameraId,
|
||||
@RequestParam(required = false) String recordType,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
return R.ok(videoMonitorService.pageRecordings(current, size, cameraId, recordType, startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取录像回放地址")
|
||||
@GetMapping("/recording/{id}/playback")
|
||||
public R<Map<String, Object>> playbackUrl(@PathVariable Long id) {
|
||||
Map<String, Object> result = videoMonitorService.getPlaybackUrl(id);
|
||||
return result.isEmpty() ? R.fail(404, "录像不存在") : R.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "创建录像记录")
|
||||
@PostMapping("/recording")
|
||||
public R<VideoRecording> createRecording(@RequestBody VideoRecording recording) {
|
||||
return R.ok(videoMonitorService.createRecording(recording));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除录像记录")
|
||||
@DeleteMapping("/recording/{id}")
|
||||
public R<String> deleteRecording(@PathVariable Long id) {
|
||||
return videoMonitorService.deleteRecording(id) ? R.ok("删除成功") : R.fail("删除失败");
|
||||
}
|
||||
|
||||
// ==================== 5. AI 人员闯入检测 ====================
|
||||
|
||||
@Operation(summary = "AI人员闯入检测(单路)")
|
||||
@PostMapping("/intrusion/detect")
|
||||
public R<Map<String, Object>> detectIntrusion(
|
||||
@RequestParam Long cameraId,
|
||||
@RequestBody(required = false) byte[] frameData) {
|
||||
return R.ok(intrusionDetectionService.detectIntrusion(cameraId, frameData));
|
||||
}
|
||||
|
||||
@Operation(summary = "AI人员闯入检测(批量多路)")
|
||||
@PostMapping("/intrusion/batch-detect")
|
||||
public R<List<Map<String, Object>>> batchDetect(@RequestBody List<Long> cameraIds) {
|
||||
return R.ok(intrusionDetectionService.batchDetect(cameraIds));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询闯入事件")
|
||||
@GetMapping("/intrusion/page")
|
||||
public R<Page<IntrusionEvent>> intrusionPage(
|
||||
@RequestParam(defaultValue = "1") int current,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) Long cameraId,
|
||||
@RequestParam(required = false) String area,
|
||||
@RequestParam(required = false) String alertLevel,
|
||||
@RequestParam(required = false) Integer alertStatus,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
return R.ok(intrusionDetectionService.pageEvents(current, size, cameraId, area,
|
||||
alertLevel, alertStatus, startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取闯入事件详情")
|
||||
@GetMapping("/intrusion/{id}")
|
||||
public R<IntrusionEvent> intrusionDetail(@PathVariable Long id) {
|
||||
IntrusionEvent event = intrusionDetectionService.getEventById(id);
|
||||
return event != null ? R.ok(event) : R.fail(404, "事件不存在");
|
||||
}
|
||||
|
||||
@Operation(summary = "确认闯入事件")
|
||||
@PostMapping("/intrusion/{id}/confirm")
|
||||
public R<String> confirmEvent(@PathVariable Long id, @RequestParam Long userId) {
|
||||
return intrusionDetectionService.confirmEvent(id, userId)
|
||||
? R.ok("已确认") : R.fail("确认失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "处理闯入事件")
|
||||
@PostMapping("/intrusion/{id}/handle")
|
||||
public R<String> handleEvent(@PathVariable Long id,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam(required = false) String handlerName,
|
||||
@RequestBody Map<String, String> body) {
|
||||
String result = body.getOrDefault("result", "");
|
||||
return intrusionDetectionService.handleEvent(id, userId,
|
||||
handlerName != null ? handlerName : "", result)
|
||||
? R.ok("处理完成") : R.fail("处理失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "忽略/误报标记闯入事件")
|
||||
@PostMapping("/intrusion/{id}/dismiss")
|
||||
public R<String> dismissEvent(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
String remark = body.getOrDefault("remark", "");
|
||||
return intrusionDetectionService.dismissEvent(id, remark)
|
||||
? R.ok("已标记为忽略") : R.fail("操作失败");
|
||||
}
|
||||
|
||||
// ==================== 6. 监控统计 ====================
|
||||
|
||||
@Operation(summary = "设备在线率统计")
|
||||
@GetMapping("/stats/device-online")
|
||||
public R<Map<String, Object>> deviceOnlineStats() {
|
||||
return R.ok(videoMonitorService.getDeviceOnlineStats());
|
||||
}
|
||||
|
||||
@Operation(summary = "按区域统计摄像头分布")
|
||||
@GetMapping("/stats/camera-by-area")
|
||||
public R<List<Map<String, Object>>> cameraStatsByArea() {
|
||||
return R.ok(videoMonitorService.getCameraStatsByArea());
|
||||
}
|
||||
|
||||
@Operation(summary = "闯入事件统计")
|
||||
@GetMapping("/stats/intrusion")
|
||||
public R<Map<String, Object>> intrusionStats(
|
||||
@RequestParam(defaultValue = "week") String period) {
|
||||
return R.ok(intrusionDetectionService.getIntrusionStats(period));
|
||||
}
|
||||
|
||||
@Operation(summary = "闯入事件趋势分析(按天)")
|
||||
@GetMapping("/stats/intrusion-trend")
|
||||
public R<List<Map<String, Object>>> intrusionTrend(
|
||||
@RequestParam(defaultValue = "7") int days) {
|
||||
return R.ok(intrusionDetectionService.getIntrusionTrend(days));
|
||||
}
|
||||
|
||||
@Operation(summary = "高频闯入摄像头排行")
|
||||
@GetMapping("/stats/top-intrusion-cameras")
|
||||
public R<List<Map<String, Object>>> topIntrusionCameras(
|
||||
@RequestParam(defaultValue = "10") int limit) {
|
||||
return R.ok(intrusionDetectionService.getTopIntrusionCameras(limit));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.water.production.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GIS 地图统计视图对象
|
||||
* 包含各区域设备数量、在线率、报警数等统计信息
|
||||
*/
|
||||
@Data
|
||||
public class GisStatisticsVO {
|
||||
|
||||
/** 点位总数 */
|
||||
private Integer totalPoints;
|
||||
|
||||
/** 在线点位数量 */
|
||||
private Integer onlinePoints;
|
||||
|
||||
/** 离线点位数量 */
|
||||
private Integer offlinePoints;
|
||||
|
||||
/** 故障点位数量 */
|
||||
private Integer faultPoints;
|
||||
|
||||
/** 总体在线率(百分比) */
|
||||
private BigDecimal onlineRate;
|
||||
|
||||
/** 报警总数 */
|
||||
private Integer totalAlerts;
|
||||
|
||||
/** 管线总长度(米) */
|
||||
private BigDecimal totalPipelineLength;
|
||||
|
||||
/** 区域数量 */
|
||||
private Integer totalAreas;
|
||||
|
||||
/** 按区域统计 */
|
||||
private List<AreaStatistic> areaStatistics;
|
||||
|
||||
/** 按点位类型统计 */
|
||||
private Map<String, Integer> typeDistribution;
|
||||
|
||||
/** 热力图数据(网格化密度) */
|
||||
private List<HeatmapCell> heatmapData;
|
||||
|
||||
/**
|
||||
* 区域统计项
|
||||
*/
|
||||
@Data
|
||||
public static class AreaStatistic {
|
||||
|
||||
/** 区域名称 */
|
||||
private String area;
|
||||
|
||||
/** 设备总数 */
|
||||
private Integer deviceCount;
|
||||
|
||||
/** 在线设备数 */
|
||||
private Integer onlineCount;
|
||||
|
||||
/** 离线设备数 */
|
||||
private Integer offlineCount;
|
||||
|
||||
/** 故障设备数 */
|
||||
private Integer faultCount;
|
||||
|
||||
/** 在线率(百分比) */
|
||||
private BigDecimal onlineRate;
|
||||
|
||||
/** 报警数 */
|
||||
private Integer alertCount;
|
||||
|
||||
/** 管线长度(米) */
|
||||
private BigDecimal pipelineLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 热力图单元格
|
||||
*/
|
||||
@Data
|
||||
public static class HeatmapCell {
|
||||
|
||||
/** 网格经度(中心点) */
|
||||
private BigDecimal lng;
|
||||
|
||||
/** 网格纬度(中心点) */
|
||||
private BigDecimal lat;
|
||||
|
||||
/** 权重值(设备密度) */
|
||||
private Integer weight;
|
||||
|
||||
/** 网格内设备数量 */
|
||||
private Integer count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.water.production.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 空间查询请求
|
||||
* 支持矩形范围查询和圆形范围查询
|
||||
*/
|
||||
@Data
|
||||
public class SpatialQueryRequest {
|
||||
|
||||
/** 查询类型: rectangle/circle */
|
||||
private String queryType;
|
||||
|
||||
// ===== 矩形范围参数 =====
|
||||
|
||||
/** 最小经度(矩形左下角) */
|
||||
private BigDecimal minLng;
|
||||
|
||||
/** 最小纬度(矩形左下角) */
|
||||
private BigDecimal minLat;
|
||||
|
||||
/** 最大经度(矩形右上角) */
|
||||
private BigDecimal maxLng;
|
||||
|
||||
/** 最大纬度(矩形右上角) */
|
||||
private BigDecimal maxLat;
|
||||
|
||||
// ===== 圆形范围参数 =====
|
||||
|
||||
/** 圆心经度 */
|
||||
private BigDecimal centerLng;
|
||||
|
||||
/** 圆心纬度 */
|
||||
private BigDecimal centerLat;
|
||||
|
||||
/** 半径(米) */
|
||||
private BigDecimal radius;
|
||||
|
||||
// ===== 通用筛选 =====
|
||||
|
||||
/** 点位类型: flow/pressure/level/quality/valve */
|
||||
private String pointType;
|
||||
|
||||
/** 所属区域 */
|
||||
private String area;
|
||||
|
||||
/** 状态 */
|
||||
private String status;
|
||||
|
||||
/** 关键词搜索 */
|
||||
private String keyword;
|
||||
|
||||
/** 页码 */
|
||||
private Integer pageNum = 1;
|
||||
|
||||
/** 每页条数 */
|
||||
private Integer pageSize = 50;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* GIS 区域实体
|
||||
* 存储供水区域的空间范围与统计信息
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_gis_area")
|
||||
public class GisArea {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 区域编号 */
|
||||
private String areaCode;
|
||||
|
||||
/** 区域名称 */
|
||||
private String areaName;
|
||||
|
||||
/** 区域类型: water_plant/supply_zone/dma/admin_district */
|
||||
private String areaType;
|
||||
|
||||
/** 区域中心经度 */
|
||||
private BigDecimal centerLng;
|
||||
|
||||
/** 区域中心纬度 */
|
||||
private BigDecimal centerLat;
|
||||
|
||||
/** 区域面积(平方公里) */
|
||||
private BigDecimal areaSize;
|
||||
|
||||
/** 区域边界(GeoJSON 格式,Polygon/MultiPolygon) */
|
||||
private String boundary;
|
||||
|
||||
/** 上级区域ID */
|
||||
private Long parentId;
|
||||
|
||||
/** 区域内设备总数 */
|
||||
private Integer deviceCount;
|
||||
|
||||
/** 区域内在线设备数 */
|
||||
private Integer onlineCount;
|
||||
|
||||
/** 区域内报警数 */
|
||||
private Integer alertCount;
|
||||
|
||||
/** 供水人口(万人) */
|
||||
private BigDecimal population;
|
||||
|
||||
/** 状态: active/inactive */
|
||||
private String status;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* GIS 管网线段实体
|
||||
* 存储管网线段的空间数据与节点关联信息
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_gis_pipeline")
|
||||
public class GisPipeline {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 管线编号 */
|
||||
private String pipelineCode;
|
||||
|
||||
/** 管线名称 */
|
||||
private String pipelineName;
|
||||
|
||||
/** 管线类型: supply/distribution/drainage/raw_water */
|
||||
private String pipelineType;
|
||||
|
||||
/** 管线材质: ductile_iron/pvc/pe/steel */
|
||||
private String material;
|
||||
|
||||
/** 管径(mm) */
|
||||
private BigDecimal diameter;
|
||||
|
||||
/** 管段起点经度 */
|
||||
private BigDecimal startLng;
|
||||
|
||||
/** 管段起点纬度 */
|
||||
private BigDecimal startLat;
|
||||
|
||||
/** 管段终点经度 */
|
||||
private BigDecimal endLng;
|
||||
|
||||
/** 管段终点纬度 */
|
||||
private BigDecimal endLat;
|
||||
|
||||
/** 管段长度(米) */
|
||||
private BigDecimal length;
|
||||
|
||||
/** 起点节点ID(关联 prod_gis_point.id) */
|
||||
private Long startNodeId;
|
||||
|
||||
/** 终点节点ID(关联 prod_gis_point.id) */
|
||||
private Long endNodeId;
|
||||
|
||||
/** 所属区域 */
|
||||
private String area;
|
||||
|
||||
/** 埋深(米) */
|
||||
private BigDecimal burialDepth;
|
||||
|
||||
/** 建设年份 */
|
||||
private Integer buildYear;
|
||||
|
||||
/** 运行状态: normal/leakage/damaged/maintenance */
|
||||
private String status;
|
||||
|
||||
/** 扩展属性(JSON) */
|
||||
private String properties;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* GIS 监测点位实体
|
||||
* 存储各类监测点(流量/压力/液位/水质/阀门)的空间位置与属性信息
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_gis_point")
|
||||
public class GisPoint {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 点位编号 */
|
||||
private String pointCode;
|
||||
|
||||
/** 点位名称 */
|
||||
private String pointName;
|
||||
|
||||
/** 点位类型: flow/pressure/level/quality/valve */
|
||||
private String pointType;
|
||||
|
||||
/** 所属区域 */
|
||||
private String area;
|
||||
|
||||
/** 经度 */
|
||||
private BigDecimal lng;
|
||||
|
||||
/** 纬度 */
|
||||
private BigDecimal lat;
|
||||
|
||||
/** 海拔高度(米) */
|
||||
private BigDecimal elevation;
|
||||
|
||||
/** 关联设备ID(关联 prod_monitor_device.id) */
|
||||
private Long deviceId;
|
||||
|
||||
/** 地址描述 */
|
||||
private String address;
|
||||
|
||||
/** 状态: online/offline/fault */
|
||||
private String status;
|
||||
|
||||
/** 扩展属性(JSON 格式,存储不同类型点位的特有属性) */
|
||||
private String properties;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.GisArea;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* GIS 区域 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface GisAreaMapper extends BaseMapper<GisArea> {
|
||||
|
||||
/**
|
||||
* 查询所有区域(不含边界详情,用于列表)
|
||||
*/
|
||||
@Select("SELECT id, area_code, area_name, area_type, center_lng, center_lat, area_size, " +
|
||||
"parent_id, device_count, online_count, alert_count, population, status, " +
|
||||
"created_time, updated_time FROM prod_gis_area ORDER BY area_code")
|
||||
List<GisArea> selectAllSummary();
|
||||
|
||||
/**
|
||||
* 查询活跃区域数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM prod_gis_area WHERE status = 'active'")
|
||||
Integer countActiveAreas();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.GisPipeline;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GIS 管网 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface GisPipelineMapper extends BaseMapper<GisPipeline> {
|
||||
|
||||
/**
|
||||
* 矩形范围内的管线
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT * FROM prod_gis_pipeline " +
|
||||
"WHERE ((start_lng >= #{minLng} AND start_lng <= #{maxLng} " +
|
||||
"AND start_lat >= #{minLat} AND start_lat <= #{maxLat}) " +
|
||||
"OR (end_lng >= #{minLng} AND end_lng <= #{maxLng} " +
|
||||
"AND end_lat >= #{minLat} AND end_lat <= #{maxLat})) " +
|
||||
"<if test='pipelineType != null and pipelineType != \"\"'> AND pipeline_type = #{pipelineType}</if> " +
|
||||
"<if test='area != null and area != \"\"'> AND area = #{area}</if> " +
|
||||
"ORDER BY pipeline_code" +
|
||||
"</script>")
|
||||
List<GisPipeline> selectByRectangle(
|
||||
@Param("minLng") BigDecimal minLng,
|
||||
@Param("minLat") BigDecimal minLat,
|
||||
@Param("maxLng") BigDecimal maxLng,
|
||||
@Param("maxLat") BigDecimal maxLat,
|
||||
@Param("pipelineType") String pipelineType,
|
||||
@Param("area") String area);
|
||||
|
||||
/**
|
||||
* 按区域统计管线长度
|
||||
*/
|
||||
@Select("SELECT area, COUNT(*) as count, COALESCE(SUM(length), 0) as total_length " +
|
||||
"FROM prod_gis_pipeline GROUP BY area ORDER BY area")
|
||||
List<Map<String, Object>> countByArea();
|
||||
|
||||
/**
|
||||
* 按类型统计管线
|
||||
*/
|
||||
@Select("SELECT pipeline_type, COUNT(*) as count, COALESCE(SUM(length), 0) as total_length " +
|
||||
"FROM prod_gis_pipeline GROUP BY pipeline_type ORDER BY pipeline_type")
|
||||
List<Map<String, Object>> countByType();
|
||||
|
||||
/**
|
||||
* 总管长
|
||||
*/
|
||||
@Select("SELECT COALESCE(SUM(length), 0) FROM prod_gis_pipeline")
|
||||
BigDecimal selectTotalLength();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.GisPoint;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GIS 监测点位 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface GisPointMapper extends BaseMapper<GisPoint> {
|
||||
|
||||
/**
|
||||
* 矩形范围查询
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT * FROM prod_gis_point " +
|
||||
"WHERE lng >= #{minLng} AND lng <= #{maxLng} " +
|
||||
"AND lat >= #{minLat} AND lat <= #{maxLat} " +
|
||||
"<if test='pointType != null and pointType != \"\"'> AND point_type = #{pointType}</if> " +
|
||||
"<if test='area != null and area != \"\"'> AND area = #{area}</if> " +
|
||||
"<if test='status != null and status != \"\"'> AND status = #{status}</if> " +
|
||||
"<if test='keyword != null and keyword != \"\"'> AND (point_code ILIKE CONCAT('%',#{keyword},'%') OR point_name ILIKE CONCAT('%',#{keyword},'%'))</if> " +
|
||||
"ORDER BY updated_time DESC" +
|
||||
"</script>")
|
||||
List<GisPoint> selectByRectangle(
|
||||
@Param("minLng") BigDecimal minLng,
|
||||
@Param("minLat") BigDecimal minLat,
|
||||
@Param("maxLng") BigDecimal maxLng,
|
||||
@Param("maxLat") BigDecimal maxLat,
|
||||
@Param("pointType") String pointType,
|
||||
@Param("area") String area,
|
||||
@Param("status") String status,
|
||||
@Param("keyword") String keyword);
|
||||
|
||||
/**
|
||||
* 圆形范围查询(基于 Haversine 公式近似计算距离)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT *, " +
|
||||
"(6371000 * acos(cos(radians(#{centerLat})) * cos(radians(lat)) * " +
|
||||
"cos(radians(lng) - radians(#{centerLng})) + " +
|
||||
"sin(radians(#{centerLat})) * sin(radians(lat)))) AS distance " +
|
||||
"FROM prod_gis_point " +
|
||||
"WHERE (6371000 * acos(cos(radians(#{centerLat})) * cos(radians(lat)) * " +
|
||||
"cos(radians(lng) - radians(#{centerLng})) + " +
|
||||
"sin(radians(#{centerLat})) * sin(radians(lat)))) <= #{radius} " +
|
||||
"<if test='pointType != null and pointType != \"\"'> AND point_type = #{pointType}</if> " +
|
||||
"<if test='area != null and area != \"\"'> AND area = #{area}</if> " +
|
||||
"<if test='status != null and status != \"\"'> AND status = #{status}</if> " +
|
||||
"ORDER BY distance ASC" +
|
||||
"</script>")
|
||||
List<Map<String, Object>> selectByCircle(
|
||||
@Param("centerLng") BigDecimal centerLng,
|
||||
@Param("centerLat") BigDecimal centerLat,
|
||||
@Param("radius") BigDecimal radius,
|
||||
@Param("pointType") String pointType,
|
||||
@Param("area") String area,
|
||||
@Param("status") String status);
|
||||
|
||||
/**
|
||||
* 按区域统计点位数量
|
||||
*/
|
||||
@Select("SELECT area, COUNT(*) as count, " +
|
||||
"COUNT(*) FILTER (WHERE status = 'online') as online_count, " +
|
||||
"COUNT(*) FILTER (WHERE status = 'offline') as offline_count, " +
|
||||
"COUNT(*) FILTER (WHERE status = 'fault') as fault_count " +
|
||||
"FROM prod_gis_point " +
|
||||
"GROUP BY area ORDER BY area")
|
||||
List<Map<String, Object>> countByArea();
|
||||
|
||||
/**
|
||||
* 按类型统计点位数量
|
||||
*/
|
||||
@Select("SELECT point_type, COUNT(*) as count FROM prod_gis_point GROUP BY point_type ORDER BY point_type")
|
||||
List<Map<String, Object>> countByType();
|
||||
|
||||
/**
|
||||
* 热力图网格聚合(按指定网格大小)
|
||||
*/
|
||||
@Select("SELECT " +
|
||||
"(ROUND(lng::numeric / #{gridSize}, 4) * #{gridSize}) as grid_lng, " +
|
||||
"(ROUND(lat::numeric / #{gridSize}, 4) * #{gridSize}) as grid_lat, " +
|
||||
"COUNT(*) as weight " +
|
||||
"FROM prod_gis_point " +
|
||||
"GROUP BY grid_lng, grid_lat " +
|
||||
"ORDER BY weight DESC")
|
||||
List<Map<String, Object>> selectHeatmapData(@Param("gridSize") BigDecimal gridSize);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.production.dto.GisStatisticsVO;
|
||||
import com.water.production.dto.SpatialQueryRequest;
|
||||
import com.water.production.entity.GisArea;
|
||||
import com.water.production.entity.GisPipeline;
|
||||
import com.water.production.entity.GisPoint;
|
||||
import com.water.production.mapper.GisAreaMapper;
|
||||
import com.water.production.mapper.GisPipelineMapper;
|
||||
import com.water.production.mapper.GisPointMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GisService {
|
||||
|
||||
private final GisPointMapper pointMapper;
|
||||
private final GisPipelineMapper pipelineMapper;
|
||||
private final GisAreaMapper areaMapper;
|
||||
|
||||
/**
|
||||
* 获取所有GIS点位
|
||||
*/
|
||||
public List<GisPoint> listPoints(String pointType, String area, String status) {
|
||||
LambdaQueryWrapper<GisPoint> wrapper = new LambdaQueryWrapper<>();
|
||||
if (pointType != null && !pointType.isBlank()) wrapper.eq(GisPoint::getPointType, pointType);
|
||||
if (area != null && !area.isBlank()) wrapper.eq(GisPoint::getArea, area);
|
||||
if (status != null && !status.isBlank()) wrapper.eq(GisPoint::getStatus, status);
|
||||
return pointMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 空间查询 - 矩形/圆形范围
|
||||
*/
|
||||
public List<GisPoint> spatialQuery(SpatialQueryRequest request) {
|
||||
List<GisPoint> allPoints = pointMapper.selectList(null);
|
||||
|
||||
return allPoints.stream()
|
||||
.filter(p -> {
|
||||
if (request.getQueryType() == null) return true;
|
||||
if ("rectangle".equals(request.getQueryType())) {
|
||||
return isInRectangle(p, request.getMinLng(), request.getMinLat(),
|
||||
request.getMaxLng(), request.getMaxLat());
|
||||
} else if ("circle".equals(request.getQueryType())) {
|
||||
return isInCircle(p, request.getCenterLng(), request.getCenterLat(),
|
||||
request.getRadius());
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.filter(p -> request.getPointType() == null || request.getPointType().equals(p.getPointType()))
|
||||
.filter(p -> request.getArea() == null || request.getArea().equals(p.getArea()))
|
||||
.filter(p -> request.getStatus() == null || request.getStatus().equals(p.getStatus()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管网数据
|
||||
*/
|
||||
public List<GisPipeline> listPipelines(String area, String pipeType) {
|
||||
LambdaQueryWrapper<GisPipeline> wrapper = new LambdaQueryWrapper<>();
|
||||
if (area != null && !area.isBlank()) wrapper.like(GisPipeline::getArea, area);
|
||||
if (pipeType != null && !pipeType.isBlank()) wrapper.eq(GisPipeline::getPipeType, pipeType);
|
||||
return pipelineMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取区域数据
|
||||
*/
|
||||
public List<GisArea> listAreas() {
|
||||
return areaMapper.selectList(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* GIS统计
|
||||
*/
|
||||
public GisStatisticsVO getStatistics() {
|
||||
List<GisPoint> points = pointMapper.selectList(null);
|
||||
|
||||
GisStatisticsVO stats = new GisStatisticsVO();
|
||||
stats.setTotalPoints(points.size());
|
||||
|
||||
// 按类型统计
|
||||
Map<String, Long> typeCount = points.stream()
|
||||
.collect(Collectors.groupingBy(GisPoint::getPointType, Collectors.counting()));
|
||||
stats.setPointsByType(typeCount);
|
||||
|
||||
// 按区域统计
|
||||
Map<String, Long> areaCount = points.stream()
|
||||
.collect(Collectors.groupingBy(GisPoint::getArea, Collectors.counting()));
|
||||
stats.setPointsByArea(areaCount);
|
||||
|
||||
// 在线率
|
||||
long onlineCount = points.stream()
|
||||
.filter(p -> "online".equals(p.getStatus()))
|
||||
.count();
|
||||
stats.setOnlineRate(points.isEmpty() ? 0 : (double) onlineCount / points.size());
|
||||
|
||||
// 报警数(故障设备)
|
||||
long faultCount = points.stream()
|
||||
.filter(p -> "fault".equals(p.getStatus()))
|
||||
.count();
|
||||
stats.setFaultCount(faultCount);
|
||||
|
||||
// 管网统计
|
||||
long pipelineCount = pipelineMapper.selectCount(null);
|
||||
stats.setTotalPipelines(pipelineCount);
|
||||
|
||||
long areaTotal = areaMapper.selectCount(null);
|
||||
stats.setTotalAreas(areaTotal);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 热力图数据
|
||||
*/
|
||||
public List<Map<String, Object>> getHeatmapData(String pointType) {
|
||||
LambdaQueryWrapper<GisPoint> wrapper = new LambdaQueryWrapper<>();
|
||||
if (pointType != null && !pointType.isBlank()) wrapper.eq(GisPoint::getPointType, pointType);
|
||||
List<GisPoint> points = pointMapper.selectList(wrapper);
|
||||
|
||||
// Aggregate by grid cell (approximate 0.01 degree ≈ 1km)
|
||||
Map<String, Long> gridCounts = points.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
p -> {
|
||||
double lng = p.getLng() != null ? p.getLng().doubleValue() : 0;
|
||||
double lat = p.getLat() != null ? p.getLat().doubleValue() : 0;
|
||||
return String.format("%.2f,%.2f", lng, lat);
|
||||
},
|
||||
Collectors.counting()
|
||||
));
|
||||
|
||||
List<Map<String, Object>> heatmap = new ArrayList<>();
|
||||
gridCounts.forEach((key, count) -> {
|
||||
String[] parts = key.split(",");
|
||||
Map<String, Object> cell = new LinkedHashMap<>();
|
||||
cell.put("lng", Double.parseDouble(parts[0]));
|
||||
cell.put("lat", Double.parseDouble(parts[1]));
|
||||
cell.put("count", count);
|
||||
cell.put("intensity", Math.min(count * 10, 100));
|
||||
heatmap.add(cell);
|
||||
});
|
||||
return heatmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点位CRUD
|
||||
*/
|
||||
public GisPoint getPoint(Long id) { return pointMapper.selectById(id); }
|
||||
|
||||
public Long createPoint(GisPoint point) {
|
||||
pointMapper.insert(point);
|
||||
return point.getId();
|
||||
}
|
||||
|
||||
public void updatePoint(GisPoint point) { pointMapper.updateById(point); }
|
||||
|
||||
public void deletePoint(Long id) { pointMapper.deleteById(id); }
|
||||
|
||||
// === Helper methods ===
|
||||
private boolean isInRectangle(GisPoint p, BigDecimal minLng, BigDecimal minLat,
|
||||
BigDecimal maxLng, BigDecimal maxLat) {
|
||||
if (p.getLng() == null || p.getLat() == null) return false;
|
||||
if (minLng == null || minLat == null || maxLng == null || maxLat == null) return true;
|
||||
return p.getLng().compareTo(minLng) >= 0 && p.getLng().compareTo(maxLng) <= 0
|
||||
&& p.getLat().compareTo(minLat) >= 0 && p.getLat().compareTo(maxLat) <= 0;
|
||||
}
|
||||
|
||||
private boolean isInCircle(GisPoint p, BigDecimal centerLng, BigDecimal centerLat,
|
||||
BigDecimal radius) {
|
||||
if (p.getLng() == null || p.getLat() == null) return false;
|
||||
if (centerLng == null || centerLat == null || radius == null) return true;
|
||||
double distance = haversineDistance(
|
||||
p.getLat().doubleValue(), p.getLng().doubleValue(),
|
||||
centerLat.doubleValue(), centerLng.doubleValue());
|
||||
return distance <= radius.doubleValue();
|
||||
}
|
||||
|
||||
private double haversineDistance(double lat1, double lng1, double lat2, double lng2) {
|
||||
double R = 6371000; // Earth radius in meters
|
||||
double dLat = Math.toRadians(lat2 - lat1);
|
||||
double dLng = Math.toRadians(lng2 - lng1);
|
||||
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
|
||||
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
-- GIS Map Display DDL
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_gis_point (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
point_code VARCHAR(50),
|
||||
point_name VARCHAR(100),
|
||||
point_type VARCHAR(20),
|
||||
area VARCHAR(50),
|
||||
lng NUMERIC(10,6),
|
||||
lat NUMERIC(10,6),
|
||||
elevation NUMERIC(8,2),
|
||||
device_id BIGINT,
|
||||
address VARCHAR(200),
|
||||
status VARCHAR(20) DEFAULT 'online',
|
||||
properties TEXT,
|
||||
remark TEXT,
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_gis_pipeline (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
pipeline_code VARCHAR(50),
|
||||
pipeline_name VARCHAR(100),
|
||||
pipe_type VARCHAR(20),
|
||||
area VARCHAR(50),
|
||||
diameter INT,
|
||||
material VARCHAR(30),
|
||||
length DOUBLE PRECISION,
|
||||
start_lng NUMERIC(10,6),
|
||||
start_lat NUMERIC(10,6),
|
||||
end_lng NUMERIC(10,6),
|
||||
end_lat NUMERIC(10,6),
|
||||
coordinates TEXT,
|
||||
status VARCHAR(20) DEFAULT 'normal',
|
||||
created_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prod_gis_area (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
area_code VARCHAR(50),
|
||||
area_name VARCHAR(100),
|
||||
area_type VARCHAR(20),
|
||||
boundary TEXT,
|
||||
center_lng NUMERIC(10,6),
|
||||
center_lat NUMERIC(10,6),
|
||||
population INT,
|
||||
description TEXT,
|
||||
created_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_gis_point_type ON prod_gis_point(point_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_gis_point_area ON prod_gis_point(area);
|
||||
CREATE INDEX IF NOT EXISTS idx_gis_point_status ON prod_gis_point(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_gis_point_coords ON prod_gis_point(lng, lat);
|
||||
CREATE INDEX IF NOT EXISTS idx_gis_pipeline_area ON prod_gis_pipeline(area);
|
||||
CREATE INDEX IF NOT EXISTS idx_gis_pipeline_type ON prod_gis_pipeline(pipe_type);
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.water.production.entity.IntrusionEvent;
|
||||
import com.water.production.entity.VideoCamera;
|
||||
import com.water.production.entity.VideoRecording;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* VideoMonitorService & IntrusionDetectionService 单元测试
|
||||
* 测试实体、业务逻辑、状态流转等核心功能
|
||||
*/
|
||||
class VideoMonitorServiceTest {
|
||||
|
||||
// ========== Test 1: VideoCamera 实体完整性 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("测试VideoCamera实体字段完整性")
|
||||
void testVideoCameraEntityFields() {
|
||||
VideoCamera camera = new VideoCamera();
|
||||
camera.setId(1L);
|
||||
camera.setCameraId("CAM-001");
|
||||
camera.setName("一体化水厂-沉淀池");
|
||||
camera.setArea("一体化水厂");
|
||||
camera.setStreamUrlRtsp("rtsp://192.168.1.100/stream1");
|
||||
camera.setStreamUrlHls("http://192.168.1.100/hls/stream1.m3u8");
|
||||
camera.setStreamUrlFlv("http://192.168.1.100/flv/stream1.flv");
|
||||
camera.setStatus(1);
|
||||
camera.setManufacturer("海康威视");
|
||||
camera.setModel("DS-2CD2T26FWDA3-IS");
|
||||
camera.setLng(87.5712);
|
||||
camera.setLat(43.7928);
|
||||
camera.setInstallLocation("一体化水厂沉淀池北侧");
|
||||
camera.setInstallDate(LocalDate.of(2024, 3, 15));
|
||||
camera.setAiEnabled(1);
|
||||
camera.setLastOnlineTime(LocalDateTime.now());
|
||||
|
||||
assertEquals("CAM-001", camera.getCameraId());
|
||||
assertEquals("一体化水厂-沉淀池", camera.getName());
|
||||
assertEquals(1, camera.getStatus());
|
||||
assertEquals("海康威视", camera.getManufacturer());
|
||||
assertEquals(1, camera.getAiEnabled());
|
||||
assertNotNull(camera.getStreamUrlRtsp());
|
||||
assertNotNull(camera.getStreamUrlHls());
|
||||
assertNotNull(camera.getStreamUrlFlv());
|
||||
assertNotNull(camera.getLastOnlineTime());
|
||||
assertNotNull(camera.getInstallDate());
|
||||
}
|
||||
|
||||
// ========== Test 2: VideoCamera 状态值验证 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("测试VideoCamera状态值定义")
|
||||
void testCameraStatusValues() {
|
||||
VideoCamera camera = new VideoCamera();
|
||||
|
||||
// 离线
|
||||
camera.setStatus(0);
|
||||
assertEquals(0, camera.getStatus());
|
||||
|
||||
// 在线
|
||||
camera.setStatus(1);
|
||||
assertEquals(1, camera.getStatus());
|
||||
|
||||
// 故障
|
||||
camera.setStatus(2);
|
||||
assertEquals(2, camera.getStatus());
|
||||
|
||||
// 验证AI开关
|
||||
camera.setAiEnabled(0);
|
||||
assertEquals(0, camera.getAiEnabled());
|
||||
camera.setAiEnabled(1);
|
||||
assertEquals(1, camera.getAiEnabled());
|
||||
}
|
||||
|
||||
// ========== Test 3: IntrusionEvent 实体与报警等级 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("测试IntrusionEvent实体与报警等级映射")
|
||||
void testIntrusionEventAndAlertLevel() {
|
||||
IntrusionEvent event = new IntrusionEvent();
|
||||
event.setId(1L);
|
||||
event.setCameraId(1L);
|
||||
event.setCameraName("一体化水厂-沉淀池");
|
||||
event.setArea("一体化水厂");
|
||||
event.setEventType("person_intrusion");
|
||||
event.setConfidence(BigDecimal.valueOf(0.9523));
|
||||
event.setAlertLevel("critical");
|
||||
event.setAlertStatus(0);
|
||||
event.setDetectedAt(LocalDateTime.now());
|
||||
event.setSnapshotUrl("/snapshots/CAM-001_1718000000.jpg");
|
||||
event.setVideoClipUrl("/clips/CAM-001_1718000000.mp4");
|
||||
|
||||
assertEquals("person_intrusion", event.getEventType());
|
||||
assertEquals(0, event.getAlertStatus());
|
||||
assertTrue(event.getConfidence().compareTo(BigDecimal.valueOf(0.95)) > 0);
|
||||
assertEquals("critical", event.getAlertLevel());
|
||||
assertNotNull(event.getSnapshotUrl());
|
||||
assertNotNull(event.getVideoClipUrl());
|
||||
|
||||
// 测试事件类型枚举值
|
||||
event.setEventType("person_loitering");
|
||||
assertEquals("person_loitering", event.getEventType());
|
||||
|
||||
event.setEventType("zone_breach");
|
||||
assertEquals("zone_breach", event.getEventType());
|
||||
}
|
||||
|
||||
// ========== Test 4: IntrusionEvent 报警状态流转 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("测试闯入事件报警状态流转")
|
||||
void testIntrusionEventStatusFlow() {
|
||||
IntrusionEvent event = new IntrusionEvent();
|
||||
event.setId(100L);
|
||||
event.setAlertStatus(0); // 待处理
|
||||
|
||||
// 待处理 → 已确认
|
||||
assertEquals(0, event.getAlertStatus());
|
||||
event.setAlertStatus(1);
|
||||
event.setHandledBy(10L);
|
||||
event.setHandledTime(LocalDateTime.now());
|
||||
assertEquals(1, event.getAlertStatus());
|
||||
assertEquals(10L, event.getHandledBy());
|
||||
|
||||
// 已确认 → 已处理
|
||||
event.setAlertStatus(2);
|
||||
event.setHandlerName("张三");
|
||||
event.setHandleResult("已派人现场核查,确认为工作人员");
|
||||
assertEquals(2, event.getAlertStatus());
|
||||
assertNotNull(event.getHandleResult());
|
||||
assertEquals("张三", event.getHandlerName());
|
||||
|
||||
// 或: 待处理 → 已忽略(误报)
|
||||
IntrusionEvent event2 = new IntrusionEvent();
|
||||
event2.setAlertStatus(0);
|
||||
event2.setAlertStatus(3);
|
||||
event2.setRemark("AI误报,实际为动物经过");
|
||||
assertEquals(3, event2.getAlertStatus());
|
||||
assertNotNull(event2.getRemark());
|
||||
}
|
||||
|
||||
// ========== Test 5: VideoRecording 实体与录像类型 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("测试VideoRecording实体与录像类型")
|
||||
void testVideoRecordingEntity() {
|
||||
VideoRecording recording = new VideoRecording();
|
||||
recording.setId(1L);
|
||||
recording.setCameraId(1L);
|
||||
recording.setCameraName("一体化水厂-沉淀池");
|
||||
recording.setArea("一体化水厂");
|
||||
recording.setStartTime(LocalDateTime.of(2025, 6, 14, 10, 0, 0));
|
||||
recording.setEndTime(LocalDateTime.of(2025, 6, 14, 10, 30, 0));
|
||||
recording.setDurationSec(1800);
|
||||
recording.setFileSizeMb(BigDecimal.valueOf(256.5));
|
||||
recording.setStoragePath("/data/recordings/2025/06/14/CAM-001_100000.mp4");
|
||||
recording.setPlaybackUrl("http://192.168.1.100:8080/playback/CAM-001_100000.mp4");
|
||||
recording.setRecordType("scheduled");
|
||||
|
||||
assertEquals(1800, recording.getDurationSec());
|
||||
assertEquals("scheduled", recording.getRecordType());
|
||||
assertNotNull(recording.getPlaybackUrl());
|
||||
assertEquals(0, BigDecimal.valueOf(256.5).compareTo(recording.getFileSizeMb()));
|
||||
|
||||
// 事件触发录像
|
||||
recording.setRecordType("event_triggered");
|
||||
recording.setEventId(100L);
|
||||
assertEquals("event_triggered", recording.getRecordType());
|
||||
assertEquals(100L, recording.getEventId());
|
||||
|
||||
// 手动录像
|
||||
recording.setRecordType("manual");
|
||||
recording.setEventId(null);
|
||||
assertEquals("manual", recording.getRecordType());
|
||||
assertNull(recording.getEventId());
|
||||
}
|
||||
|
||||
// ========== Test 6: AI检测置信度与报警等级映射逻辑 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("测试AI检测置信度到报警等级的映射逻辑")
|
||||
void testConfidenceToAlertLevelMapping() {
|
||||
// 模拟 IntrusionDetectionService 中的映射逻辑
|
||||
assertAlertLevel(0.96, "critical");
|
||||
assertAlertLevel(0.91, "warning");
|
||||
assertAlertLevel(0.87, "info");
|
||||
assertAlertLevel(0.99, "critical");
|
||||
assertAlertLevel(0.90, "info"); // 边界: >0.90 才是 warning
|
||||
}
|
||||
|
||||
private void assertAlertLevel(double confidence, String expectedLevel) {
|
||||
String level;
|
||||
if (confidence > 0.95) {
|
||||
level = "critical";
|
||||
} else if (confidence > 0.90) {
|
||||
level = "warning";
|
||||
} else {
|
||||
level = "info";
|
||||
}
|
||||
assertEquals(expectedLevel, level,
|
||||
String.format("confidence=%.2f should map to %s", confidence, expectedLevel));
|
||||
}
|
||||
|
||||
// ========== Test 7: 在线率计算逻辑 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("测试设备在线率计算逻辑")
|
||||
void testOnlineRateCalculation() {
|
||||
// 模拟 5 台设备: 3在线, 1离线, 1故障
|
||||
int total = 5;
|
||||
long online = 3;
|
||||
long offline = 1;
|
||||
long fault = 1;
|
||||
|
||||
double onlineRate = (double) online / total * 100;
|
||||
assertEquals(60.0, onlineRate, 0.01);
|
||||
|
||||
// 全部在线
|
||||
onlineRate = (double) 5 / 5 * 100;
|
||||
assertEquals(100.0, onlineRate, 0.01);
|
||||
|
||||
// 全部离线
|
||||
onlineRate = (double) 0 / 5 * 100;
|
||||
assertEquals(0.0, onlineRate, 0.01);
|
||||
|
||||
// 空设备列表
|
||||
double emptyRate = 0 > 0 ? (double) 0 / 0 * 100 : 0;
|
||||
assertEquals(0.0, emptyRate, 0.01);
|
||||
}
|
||||
|
||||
// ========== Test 8: 回放地址生成逻辑 ==========
|
||||
|
||||
@Test
|
||||
@DisplayName("测试回放地址生成逻辑")
|
||||
void testPlaybackUrlGeneration() {
|
||||
VideoRecording recording = new VideoRecording();
|
||||
recording.setId(1L);
|
||||
recording.setCameraId(1L);
|
||||
recording.setCameraName("CAM-001");
|
||||
recording.setStartTime(LocalDateTime.of(2025, 6, 14, 8, 0, 0));
|
||||
recording.setEndTime(LocalDateTime.of(2025, 6, 14, 8, 30, 0));
|
||||
recording.setPlaybackUrl("http://192.168.1.100:8080/playback/CAM-001_20250614080000.mp4");
|
||||
|
||||
// 验证回放URL包含关键信息
|
||||
assertNotNull(recording.getPlaybackUrl());
|
||||
assertTrue(recording.getPlaybackUrl().contains("CAM-001"));
|
||||
assertTrue(recording.getPlaybackUrl().startsWith("http"));
|
||||
|
||||
// 模拟生成回放URL
|
||||
String baseUrl = "http://192.168.1.100:8080/playback";
|
||||
String generated = String.format("%s/%s_%s.mp4", baseUrl, "CAM-001", "20250614080000");
|
||||
assertEquals("http://192.168.1.100:8080/playback/CAM-001_20250614080000.mp4", generated);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user