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