Compare commits
4
Commits
08649f027e
...
dcb412c7e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcb412c7e4 | ||
|
|
21cf0e97af | ||
|
|
4a82e90fcb | ||
|
|
f240059d1e |
+136
@@ -0,0 +1,136 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- DMA分区计量与漏损分析 DDL
|
||||||
|
-- 数据库: PostgreSQL
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- DMA分区表
|
||||||
|
CREATE TABLE IF NOT EXISTS dma_zone (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
zone_name VARCHAR(100) NOT NULL,
|
||||||
|
zone_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
parent_id BIGINT REFERENCES dma_zone(id),
|
||||||
|
zone_level INTEGER NOT NULL DEFAULT 1,
|
||||||
|
area VARCHAR(100),
|
||||||
|
area_size NUMERIC(10, 2),
|
||||||
|
population INTEGER,
|
||||||
|
pipe_length NUMERIC(10, 2),
|
||||||
|
status VARCHAR(20) DEFAULT 'active',
|
||||||
|
remark VARCHAR(500),
|
||||||
|
deleted INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE dma_zone IS 'DMA分区表';
|
||||||
|
COMMENT ON COLUMN dma_zone.zone_level IS '分区层级: 1=一级/2=二级/3=三级';
|
||||||
|
COMMENT ON COLUMN dma_zone.status IS '状态: active/inactive';
|
||||||
|
|
||||||
|
-- DMA计量表
|
||||||
|
CREATE TABLE IF NOT EXISTS dma_meter (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
zone_id BIGINT REFERENCES dma_zone(id),
|
||||||
|
meter_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
meter_name VARCHAR(100),
|
||||||
|
meter_type VARCHAR(20) NOT NULL,
|
||||||
|
location VARCHAR(200),
|
||||||
|
longitude NUMERIC(12, 8),
|
||||||
|
latitude NUMERIC(12, 8),
|
||||||
|
caliber INTEGER,
|
||||||
|
brand VARCHAR(100),
|
||||||
|
status VARCHAR(20) DEFAULT 'online',
|
||||||
|
remark VARCHAR(500),
|
||||||
|
deleted INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE dma_meter IS 'DMA计量表';
|
||||||
|
COMMENT ON COLUMN dma_meter.meter_type IS '表计类型: inlet=进水表/outlet=出水表/boundary=边界表';
|
||||||
|
COMMENT ON COLUMN dma_meter.status IS '状态: online/offline/fault';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_meter_zone ON dma_meter(zone_id);
|
||||||
|
|
||||||
|
-- DMA流量记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS dma_flow_record (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
zone_id BIGINT NOT NULL REFERENCES dma_zone(id),
|
||||||
|
meter_id BIGINT NOT NULL REFERENCES dma_meter(id),
|
||||||
|
instant_flow NUMERIC(12, 4),
|
||||||
|
total_flow NUMERIC(14, 4),
|
||||||
|
pressure NUMERIC(8, 4),
|
||||||
|
collect_time TIMESTAMP NOT NULL,
|
||||||
|
data_quality VARCHAR(20) DEFAULT 'good',
|
||||||
|
deleted INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE dma_flow_record IS 'DMA流量记录表';
|
||||||
|
COMMENT ON COLUMN dma_flow_record.instant_flow IS '瞬时流量(m³/h)';
|
||||||
|
COMMENT ON COLUMN dma_flow_record.total_flow IS '累计流量(m³)';
|
||||||
|
COMMENT ON COLUMN dma_flow_record.pressure IS '压力(MPa)';
|
||||||
|
COMMENT ON COLUMN dma_flow_record.data_quality IS '数据质量: good/bad/missing';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_flow_zone_time ON dma_flow_record(zone_id, collect_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_flow_meter_time ON dma_flow_record(meter_id, collect_time);
|
||||||
|
|
||||||
|
-- DMA漏损分析表
|
||||||
|
CREATE TABLE IF NOT EXISTS dma_leakage_analysis (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
zone_id BIGINT NOT NULL REFERENCES dma_zone(id),
|
||||||
|
analysis_date DATE NOT NULL,
|
||||||
|
supply_volume NUMERIC(14, 4),
|
||||||
|
sale_volume NUMERIC(14, 4),
|
||||||
|
leakage_volume NUMERIC(14, 4),
|
||||||
|
nrw_rate NUMERIC(8, 2),
|
||||||
|
leakage_rate NUMERIC(8, 2),
|
||||||
|
mnf NUMERIC(10, 4),
|
||||||
|
mnf_time VARCHAR(20),
|
||||||
|
background_leakage NUMERIC(10, 4),
|
||||||
|
burst_leakage NUMERIC(10, 4),
|
||||||
|
alarm_level VARCHAR(20) DEFAULT 'normal',
|
||||||
|
remark VARCHAR(500),
|
||||||
|
deleted INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE dma_leakage_analysis IS 'DMA漏损分析表';
|
||||||
|
COMMENT ON COLUMN dma_leakage_analysis.nrw_rate IS '产销差率(%)';
|
||||||
|
COMMENT ON COLUMN dma_leakage_analysis.leakage_rate IS '漏损率(%)';
|
||||||
|
COMMENT ON COLUMN dma_leakage_analysis.mnf IS '最小夜间流量(m³/h)';
|
||||||
|
COMMENT ON COLUMN dma_leakage_analysis.alarm_level IS '报警级别: normal/warning/critical';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_leakage_zone_date ON dma_leakage_analysis(zone_id, analysis_date);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_leakage_zone_date ON dma_leakage_analysis(zone_id, analysis_date) WHERE deleted = 0;
|
||||||
|
|
||||||
|
-- 水平衡表
|
||||||
|
CREATE TABLE IF NOT EXISTS dma_water_balance (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
zone_id BIGINT NOT NULL REFERENCES dma_zone(id),
|
||||||
|
period VARCHAR(20) NOT NULL,
|
||||||
|
stat_date DATE NOT NULL,
|
||||||
|
total_supply NUMERIC(14, 4),
|
||||||
|
total_sale NUMERIC(14, 4),
|
||||||
|
billing_sale NUMERIC(14, 4),
|
||||||
|
free_supply NUMERIC(14, 4),
|
||||||
|
apparent_loss NUMERIC(14, 4),
|
||||||
|
real_loss NUMERIC(14, 4),
|
||||||
|
background_loss NUMERIC(14, 4),
|
||||||
|
burst_loss NUMERIC(14, 4),
|
||||||
|
total_loss NUMERIC(14, 4),
|
||||||
|
nrw_rate NUMERIC(8, 2),
|
||||||
|
leakage_rate NUMERIC(8, 2),
|
||||||
|
remark VARCHAR(500),
|
||||||
|
deleted INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE dma_water_balance IS '水平衡表';
|
||||||
|
COMMENT ON COLUMN dma_water_balance.period IS '统计周期: daily/monthly/yearly';
|
||||||
|
COMMENT ON COLUMN dma_water_balance.apparent_loss IS '表观漏损(m³) - 计量误差+偷水';
|
||||||
|
COMMENT ON COLUMN dma_water_balance.real_loss IS '实际漏损(m³) - 物理漏损';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_balance_zone_date ON dma_water_balance(zone_id, stat_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_balance_period ON dma_water_balance(period);
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
-- =============================================
|
||||||
|
-- 智慧水务管理系统 - 在线监测列表 DDL
|
||||||
|
-- 版本: V3
|
||||||
|
-- 功能: 在线监测设备 + 实时数据 + 多维筛选
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- ==================== 在线监测设备 ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS prod_monitor_device (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
device_code VARCHAR(50) NOT NULL UNIQUE, -- 设备编号
|
||||||
|
device_name VARCHAR(200) NOT NULL, -- 设备名称
|
||||||
|
device_type VARCHAR(30) NOT NULL, -- 设备类型: flow/pressure/level/quality
|
||||||
|
area VARCHAR(50) NOT NULL, -- 所属区域
|
||||||
|
location VARCHAR(300), -- 安装位置描述
|
||||||
|
lng DECIMAL(12, 8), -- 经度
|
||||||
|
lat DECIMAL(12, 8), -- 纬度
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'offline', -- 设备状态: online/offline/fault/abnormal
|
||||||
|
last_report_time TIMESTAMP, -- 最后上报时间
|
||||||
|
brand VARCHAR(100), -- 品牌/型号
|
||||||
|
install_time TIMESTAMP, -- 安装时间
|
||||||
|
remark VARCHAR(500), -- 备注
|
||||||
|
created_time TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_time TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE prod_monitor_device IS '在线监测设备表';
|
||||||
|
COMMENT ON COLUMN prod_monitor_device.device_type IS '设备类型: flow(流量计)/pressure(压力计)/level(液位计)/quality(水质仪)';
|
||||||
|
COMMENT ON COLUMN prod_monitor_device.status IS '设备状态: online(在线)/offline(离线)/fault(故障)/abnormal(数据异常)';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_monitor_device_area ON prod_monitor_device(area);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_monitor_device_type ON prod_monitor_device(device_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_monitor_device_status ON prod_monitor_device(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_monitor_device_report ON prod_monitor_device(last_report_time DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_monitor_device_code_name ON prod_monitor_device(device_code, device_name);
|
||||||
|
|
||||||
|
-- ==================== 监测实时数据 ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS prod_monitor_realtime_data (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
device_id BIGINT NOT NULL REFERENCES prod_monitor_device(id), -- 关联设备
|
||||||
|
device_code VARCHAR(50) NOT NULL, -- 设备编号(冗余)
|
||||||
|
metric_key VARCHAR(50) NOT NULL, -- 参数类型: flow/pressure/level/turbidity/ph/residual_chlorine/temperature
|
||||||
|
metric_value DECIMAL(14, 4) NOT NULL, -- 实时值
|
||||||
|
unit VARCHAR(20), -- 单位
|
||||||
|
threshold_high DECIMAL(14, 4), -- 阈值上限
|
||||||
|
threshold_low DECIMAL(14, 4), -- 阈值下限
|
||||||
|
is_abnormal SMALLINT DEFAULT 0, -- 是否异常: 0正常 1异常
|
||||||
|
collect_time TIMESTAMP NOT NULL, -- 采集时间
|
||||||
|
created_time TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE prod_monitor_realtime_data IS '监测实时数据表';
|
||||||
|
COMMENT ON COLUMN prod_monitor_realtime_data.metric_key IS '参数类型: flow(流量)/pressure(压力)/level(液位)/turbidity(浊度)/ph(pH值)/residual_chlorine(余氯)/temperature(温度)';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_realtime_device_id ON prod_monitor_realtime_data(device_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_realtime_device_code ON prod_monitor_realtime_data(device_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_realtime_metric_key ON prod_monitor_realtime_data(metric_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_realtime_collect_time ON prod_monitor_realtime_data(collect_time DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_realtime_device_metric ON prod_monitor_realtime_data(device_id, metric_key, collect_time DESC);
|
||||||
|
|
||||||
|
-- ==================== 初始化数据(示例) ====================
|
||||||
|
|
||||||
|
INSERT INTO prod_monitor_device (device_code, device_name, device_type, area, location, lng, lat, status, last_report_time, brand)
|
||||||
|
VALUES
|
||||||
|
('MON-FLOW-001', '一号泵站出口流量计', 'flow', '一体化水厂', '一号泵站出口', 82.07123456, 44.84567890, 'online', NOW(), 'E+H Promag 50'),
|
||||||
|
('MON-FLOW-002', '二号泵站出口流量计', 'flow', '一体化水厂', '二号泵站出口', 82.07234567, 44.84678901, 'online', NOW(), 'E+H Promag 50'),
|
||||||
|
('MON-PRES-001', '管网压力监测点A', 'pressure', '管网一区', '人民路DN300', 82.06890123, 44.84234567, 'online', NOW(), 'WIKA S-20'),
|
||||||
|
('MON-PRES-002', '管网压力监测点B', 'pressure', '管网一区', '建设路DN200', 82.06901234, 44.84345678, 'offline', NOW() - INTERVAL '2 hours', 'WIKA S-20'),
|
||||||
|
('MON-LEV-001', '清水池液位计', 'level', '一体化水厂', '清水池', 82.07156789, 44.84501234, 'online', NOW(), 'VEGA VEGAPULS 64'),
|
||||||
|
('MON-LEV-002', '沉淀池液位计', 'level', '一体化水厂', '沉淀池', 82.07167890, 44.84512345, 'fault', NOW() - INTERVAL '30 minutes', 'VEGA VEGAPULS 64'),
|
||||||
|
('MON-QUAL-001', '出厂水质监测仪', 'quality', '一体化水厂', '出厂水口', 82.07178901, 44.84523456, 'online', NOW(), 'HACH sc200'),
|
||||||
|
('MON-QUAL-002', '管网末梢水质仪', 'quality', '管网二区', '末梢检测点', 82.06543210, 44.83987654, 'abnormal',NOW(), 'HACH sc200'),
|
||||||
|
('MON-FLOW-003', '三号泵站流量计', 'flow', '管网二区', '三号泵站', 82.06654321, 44.84098765, 'online', NOW(), 'E+H Promag 10'),
|
||||||
|
('MON-PRES-003', '高位水池压力计', 'pressure', '管网三区', '高位水池出口', 82.07345678, 44.84789012, 'online', NOW(), 'WIKA S-20')
|
||||||
|
ON CONFLICT (device_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO prod_monitor_realtime_data (device_id, device_code, metric_key, metric_value, unit, threshold_high, threshold_low, is_abnormal, collect_time)
|
||||||
|
SELECT d.id, d.device_code, m.metric_key, m.metric_value, m.unit, m.threshold_high, m.threshold_low, m.is_abnormal, NOW()
|
||||||
|
FROM prod_monitor_device d
|
||||||
|
CROSS JOIN (VALUES
|
||||||
|
('flow', 125.50, 'm³/h', 200.0, 10.0, 0),
|
||||||
|
('pressure', 0.35, 'MPa', 0.6, 0.15, 0),
|
||||||
|
('level', 3.80, 'm', 5.0, 0.5, 0),
|
||||||
|
('turbidity', 0.45, 'NTU', 1.0, NULL, 0),
|
||||||
|
('ph', 7.20, '', 8.5, 6.5, 0),
|
||||||
|
('residual_chlorine', 0.35, 'mg/L', 0.8, 0.05, 0)
|
||||||
|
) AS m(metric_key, metric_value, unit, threshold_high, threshold_low, is_abnormal)
|
||||||
|
WHERE d.status = 'online'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
<module>wm-system</module>
|
<module>wm-system</module>
|
||||||
<module>wm-mobile-app</module>
|
<module>wm-mobile-app</module>
|
||||||
<module>wm-config</module>
|
<module>wm-config</module>
|
||||||
|
<module>wm-dma</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.water</groupId>
|
||||||
|
<artifactId>wm-parent</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>wm-dma</artifactId>
|
||||||
|
<name>wm-dma</name>
|
||||||
|
<description>DMA分区计量与漏损分析模块</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- 公共模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.water</groupId>
|
||||||
|
<artifactId>wm-common</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Web -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Nacos 服务发现 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- PostgreSQL -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MyBatis-Plus -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Hutool -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Knife4j OpenAPI3 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
|
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.water.dma;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
@SpringBootApplication(scanBasePackages = "com.water")
|
||||||
|
@MapperScan("com.water.dma.mapper")
|
||||||
|
@EnableScheduling
|
||||||
|
public class DmaApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(DmaApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.water.dma.config;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.DbType;
|
||||||
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class MyBatisPlusConfig implements MetaObjectHandler {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||||
|
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||||
|
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
|
||||||
|
return interceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
this.strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
|
||||||
|
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFill(MetaObject metaObject) {
|
||||||
|
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.water.dma.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.dma.entity.DmaFlowRecord;
|
||||||
|
import com.water.dma.service.DmaFlowService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA流量计量控制器
|
||||||
|
*/
|
||||||
|
@Tag(name = "DMA流量计量")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/dma/flow")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DmaFlowController {
|
||||||
|
|
||||||
|
private final DmaFlowService flowService;
|
||||||
|
|
||||||
|
@Operation(summary = "分页查询流量记录")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public R<Page<DmaFlowRecord>> page(
|
||||||
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
|
@RequestParam(required = false) Long zoneId,
|
||||||
|
@RequestParam(required = false) Long meterId,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
|
||||||
|
return R.ok(flowService.page(pageNum, pageSize, zoneId, meterId, startTime, endTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "创建流量记录")
|
||||||
|
@PostMapping
|
||||||
|
public R<DmaFlowRecord> create(@RequestBody DmaFlowRecord record) {
|
||||||
|
return R.ok(flowService.create(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "批量创建流量记录")
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public R<String> batchCreate(@RequestBody List<DmaFlowRecord> records) {
|
||||||
|
flowService.batchCreate(records);
|
||||||
|
return R.ok("批量创建成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取分区进出水量汇总")
|
||||||
|
@GetMapping("/summary/{zoneId}")
|
||||||
|
public R<Map<String, Object>> getZoneFlowSummary(
|
||||||
|
@PathVariable Long zoneId,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
|
||||||
|
return R.ok(flowService.getZoneFlowSummary(zoneId, startTime, endTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "最小夜间流量(MNF)分析")
|
||||||
|
@GetMapping("/mnf/{zoneId}")
|
||||||
|
public R<Map<String, Object>> getMNFAnalysis(
|
||||||
|
@PathVariable Long zoneId,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
||||||
|
return R.ok(flowService.getMNFAnalysis(zoneId, date));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取流量趋势")
|
||||||
|
@GetMapping("/trend/{zoneId}")
|
||||||
|
public R<List<Map<String, Object>>> getFlowTrend(
|
||||||
|
@PathVariable Long zoneId,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
|
||||||
|
return R.ok(flowService.getFlowTrend(zoneId, startTime, endTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.water.dma.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.dma.entity.DmaLeakageAnalysis;
|
||||||
|
import com.water.dma.service.DmaLeakageService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA漏损分析控制器
|
||||||
|
*/
|
||||||
|
@Tag(name = "DMA漏损分析")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/dma/leakage")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DmaLeakageController {
|
||||||
|
|
||||||
|
private final DmaLeakageService leakageService;
|
||||||
|
|
||||||
|
@Operation(summary = "分页查询漏损分析")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public R<Page<DmaLeakageAnalysis>> page(
|
||||||
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize,
|
||||||
|
@RequestParam(required = false) Long zoneId,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||||
|
return R.ok(leakageService.page(pageNum, pageSize, zoneId, startDate, endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "执行漏损分析")
|
||||||
|
@PostMapping("/analyze")
|
||||||
|
public R<DmaLeakageAnalysis> analyze(
|
||||||
|
@RequestParam Long zoneId,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
|
||||||
|
@RequestParam BigDecimal supplyVolume,
|
||||||
|
@RequestParam BigDecimal saleVolume) {
|
||||||
|
return R.ok(leakageService.analyze(zoneId, date, supplyVolume, saleVolume));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取漏损趋势")
|
||||||
|
@GetMapping("/trend/{zoneId}")
|
||||||
|
public R<List<Map<String, Object>>> getTrend(
|
||||||
|
@PathVariable Long zoneId,
|
||||||
|
@RequestParam(defaultValue = "30") int days) {
|
||||||
|
return R.ok(leakageService.getTrend(zoneId, days));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取报警列表")
|
||||||
|
@GetMapping("/alarms")
|
||||||
|
public R<List<DmaLeakageAnalysis>> getAlarms(
|
||||||
|
@RequestParam(required = false) String alarmLevel) {
|
||||||
|
return R.ok(leakageService.getAlarms(alarmLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取分区漏损汇总")
|
||||||
|
@GetMapping("/summary/{zoneId}")
|
||||||
|
public R<Map<String, Object>> getZoneSummary(@PathVariable Long zoneId) {
|
||||||
|
return R.ok(leakageService.getZoneSummary(zoneId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.water.dma.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.dma.entity.DmaMeter;
|
||||||
|
import com.water.dma.service.DmaMeterService;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA计量表管理控制器
|
||||||
|
*/
|
||||||
|
@Tag(name = "DMA计量表管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/dma/meter")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DmaMeterController {
|
||||||
|
|
||||||
|
private final DmaMeterService meterService;
|
||||||
|
|
||||||
|
@Operation(summary = "分页查询计量表")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public R<Page<DmaMeter>> page(
|
||||||
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize,
|
||||||
|
@RequestParam(required = false) Long zoneId,
|
||||||
|
@RequestParam(required = false) String meterType) {
|
||||||
|
return R.ok(meterService.page(pageNum, pageSize, zoneId, meterType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取计量表详情")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public R<DmaMeter> getById(@PathVariable Long id) {
|
||||||
|
return R.ok(meterService.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "创建计量表")
|
||||||
|
@PostMapping
|
||||||
|
public R<DmaMeter> create(@RequestBody DmaMeter meter) {
|
||||||
|
return R.ok(meterService.create(meter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新计量表")
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public R<String> update(@PathVariable Long id, @RequestBody DmaMeter meter) {
|
||||||
|
meter.setId(id);
|
||||||
|
meterService.update(meter);
|
||||||
|
return R.ok("更新成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "删除计量表")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public R<String> delete(@PathVariable Long id) {
|
||||||
|
meterService.delete(id);
|
||||||
|
return R.ok("删除成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取分区下的计量表")
|
||||||
|
@GetMapping("/zone/{zoneId}")
|
||||||
|
public R<List<DmaMeter>> listByZoneId(@PathVariable Long zoneId) {
|
||||||
|
return R.ok(meterService.listByZoneId(zoneId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "绑定计量表到分区")
|
||||||
|
@PostMapping("/{meterId}/bind/{zoneId}")
|
||||||
|
public R<String> bindToZone(@PathVariable Long meterId, @PathVariable Long zoneId) {
|
||||||
|
meterService.bindToZone(meterId, zoneId);
|
||||||
|
return R.ok("绑定成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.water.dma.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.dma.entity.DmaZone;
|
||||||
|
import com.water.dma.service.DmaZoneService;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA分区管理控制器
|
||||||
|
*/
|
||||||
|
@Tag(name = "DMA分区管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/dma/zone")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DmaZoneController {
|
||||||
|
|
||||||
|
private final DmaZoneService zoneService;
|
||||||
|
|
||||||
|
@Operation(summary = "分页查询分区")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public R<Page<DmaZone>> page(
|
||||||
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize,
|
||||||
|
@RequestParam(required = false) String zoneName) {
|
||||||
|
return R.ok(zoneService.page(pageNum, pageSize, zoneName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取分区详情")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public R<DmaZone> getById(@PathVariable Long id) {
|
||||||
|
return R.ok(zoneService.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "创建分区")
|
||||||
|
@PostMapping
|
||||||
|
public R<DmaZone> create(@RequestBody DmaZone zone) {
|
||||||
|
return R.ok(zoneService.create(zone));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新分区")
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public R<String> update(@PathVariable Long id, @RequestBody DmaZone zone) {
|
||||||
|
zone.setId(id);
|
||||||
|
zoneService.update(zone);
|
||||||
|
return R.ok("更新成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "删除分区")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public R<String> delete(@PathVariable Long id) {
|
||||||
|
zoneService.delete(id);
|
||||||
|
return R.ok("删除成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取分区树")
|
||||||
|
@GetMapping("/tree")
|
||||||
|
public R<List<Map<String, Object>>> getZoneTree() {
|
||||||
|
return R.ok(zoneService.getZoneTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取所有分区")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public R<List<DmaZone>> listAll() {
|
||||||
|
return R.ok(zoneService.listAll());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.water.dma.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.dma.entity.WaterBalance;
|
||||||
|
import com.water.dma.service.WaterBalanceService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 水平衡分析控制器
|
||||||
|
*/
|
||||||
|
@Tag(name = "水平衡分析")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/dma/balance")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WaterBalanceController {
|
||||||
|
|
||||||
|
private final WaterBalanceService balanceService;
|
||||||
|
|
||||||
|
@Operation(summary = "分页查询水平衡")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public R<Page<WaterBalance>> page(
|
||||||
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize,
|
||||||
|
@RequestParam(required = false) Long zoneId,
|
||||||
|
@RequestParam(required = false) String period,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||||
|
return R.ok(balanceService.page(pageNum, pageSize, zoneId, period, startDate, endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取水平衡详情")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public R<WaterBalance> getById(@PathVariable Long id) {
|
||||||
|
return R.ok(balanceService.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "创建水平衡记录")
|
||||||
|
@PostMapping
|
||||||
|
public R<WaterBalance> create(@RequestBody WaterBalance balance) {
|
||||||
|
return R.ok(balanceService.create(balance));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新水平衡记录")
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public R<String> update(@PathVariable Long id, @RequestBody WaterBalance balance) {
|
||||||
|
balance.setId(id);
|
||||||
|
balanceService.update(balance);
|
||||||
|
return R.ok("更新成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "删除水平衡记录")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public R<String> delete(@PathVariable Long id) {
|
||||||
|
balanceService.delete(id);
|
||||||
|
return R.ok("删除成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "生成水平衡分析报告")
|
||||||
|
@GetMapping("/report/{zoneId}")
|
||||||
|
public R<Map<String, Object>> generateReport(
|
||||||
|
@PathVariable Long zoneId,
|
||||||
|
@RequestParam(defaultValue = "monthly") String period,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||||
|
return R.ok(balanceService.generateReport(zoneId, period, startDate, endDate));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.water.dma.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import com.water.common.core.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA流量记录实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("dma_flow_record")
|
||||||
|
public class DmaFlowRecord extends BaseEntity {
|
||||||
|
|
||||||
|
/** 所属分区ID */
|
||||||
|
private Long zoneId;
|
||||||
|
|
||||||
|
/** 表计ID */
|
||||||
|
private Long meterId;
|
||||||
|
|
||||||
|
/** 瞬时流量(m³/h) */
|
||||||
|
private BigDecimal instantFlow;
|
||||||
|
|
||||||
|
/** 累计流量(m³) */
|
||||||
|
private BigDecimal totalFlow;
|
||||||
|
|
||||||
|
/** 压力(MPa) */
|
||||||
|
private BigDecimal pressure;
|
||||||
|
|
||||||
|
/** 采集时间 */
|
||||||
|
private LocalDateTime collectTime;
|
||||||
|
|
||||||
|
/** 数据质量: good/bad/missing */
|
||||||
|
private String dataQuality;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.water.dma.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import com.water.common.core.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA漏损分析实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("dma_leakage_analysis")
|
||||||
|
public class DmaLeakageAnalysis extends BaseEntity {
|
||||||
|
|
||||||
|
/** 所属分区ID */
|
||||||
|
private Long zoneId;
|
||||||
|
|
||||||
|
/** 分析日期 */
|
||||||
|
private LocalDate analysisDate;
|
||||||
|
|
||||||
|
/** 供水量(m³) */
|
||||||
|
private BigDecimal supplyVolume;
|
||||||
|
|
||||||
|
/** 售水量(m³) */
|
||||||
|
private BigDecimal saleVolume;
|
||||||
|
|
||||||
|
/** 漏损量(m³) */
|
||||||
|
private BigDecimal leakageVolume;
|
||||||
|
|
||||||
|
/** 产销差率(%) */
|
||||||
|
private BigDecimal nrwRate;
|
||||||
|
|
||||||
|
/** 漏损率(%) */
|
||||||
|
private BigDecimal leakageRate;
|
||||||
|
|
||||||
|
/** 最小夜间流量(m³/h) */
|
||||||
|
private BigDecimal mnf;
|
||||||
|
|
||||||
|
/** MNF发生时间 */
|
||||||
|
private String mnfTime;
|
||||||
|
|
||||||
|
/** 背景漏损(m³/h) */
|
||||||
|
private BigDecimal backgroundLeakage;
|
||||||
|
|
||||||
|
/** 爆管漏损(m³/h) */
|
||||||
|
private BigDecimal burstLeakage;
|
||||||
|
|
||||||
|
/** 报警级别: normal/warning/critical */
|
||||||
|
private String alarmLevel;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.water.dma.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import com.water.common.core.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA分区计量表实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("dma_meter")
|
||||||
|
public class DmaMeter extends BaseEntity {
|
||||||
|
|
||||||
|
/** 所属分区ID */
|
||||||
|
private Long zoneId;
|
||||||
|
|
||||||
|
/** 表计编号 */
|
||||||
|
private String meterCode;
|
||||||
|
|
||||||
|
/** 表计名称 */
|
||||||
|
private String meterName;
|
||||||
|
|
||||||
|
/** 表计类型: inlet=进水表/outlet=出水表/boundary=边界表 */
|
||||||
|
private String meterType;
|
||||||
|
|
||||||
|
/** 安装位置 */
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
/** 经度 */
|
||||||
|
private BigDecimal longitude;
|
||||||
|
|
||||||
|
/** 纬度 */
|
||||||
|
private BigDecimal latitude;
|
||||||
|
|
||||||
|
/** 口径(mm) */
|
||||||
|
private Integer caliber;
|
||||||
|
|
||||||
|
/** 品牌/型号 */
|
||||||
|
private String brand;
|
||||||
|
|
||||||
|
/** 状态: online/offline/fault */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.water.dma.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import com.water.common.core.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA分区实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("dma_zone")
|
||||||
|
public class DmaZone extends BaseEntity {
|
||||||
|
|
||||||
|
/** 分区名称 */
|
||||||
|
private String zoneName;
|
||||||
|
|
||||||
|
/** 分区编码(唯一) */
|
||||||
|
private String zoneCode;
|
||||||
|
|
||||||
|
/** 父级分区ID(顶级为null) */
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
/** 分区层级: 1=一级/2=二级/3=三级 */
|
||||||
|
private Integer zoneLevel;
|
||||||
|
|
||||||
|
/** 所属区域 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
/** 分区面积(km²) */
|
||||||
|
private BigDecimal areaSize;
|
||||||
|
|
||||||
|
/** 服务人口数 */
|
||||||
|
private Integer population;
|
||||||
|
|
||||||
|
/** 管网长度(km) */
|
||||||
|
private BigDecimal pipeLength;
|
||||||
|
|
||||||
|
/** 状态: active/inactive */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.water.dma.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import com.water.common.core.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 水平衡表实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("dma_water_balance")
|
||||||
|
public class WaterBalance extends BaseEntity {
|
||||||
|
|
||||||
|
/** 所属分区ID */
|
||||||
|
private Long zoneId;
|
||||||
|
|
||||||
|
/** 统计周期: daily/monthly/yearly */
|
||||||
|
private String period;
|
||||||
|
|
||||||
|
/** 统计日期 */
|
||||||
|
private LocalDate statDate;
|
||||||
|
|
||||||
|
/** 总供水量(m³) */
|
||||||
|
private BigDecimal totalSupply;
|
||||||
|
|
||||||
|
/** 总售水量(m³) */
|
||||||
|
private BigDecimal totalSale;
|
||||||
|
|
||||||
|
/** 计费售水量(m³) */
|
||||||
|
private BigDecimal billingSale;
|
||||||
|
|
||||||
|
/** 免费供水量(m³) */
|
||||||
|
private BigDecimal freeSupply;
|
||||||
|
|
||||||
|
/** 表观漏损(m³) - 计量误差+偷水 */
|
||||||
|
private BigDecimal apparentLoss;
|
||||||
|
|
||||||
|
/** 实际漏损(m³) - 物理漏损 */
|
||||||
|
private BigDecimal realLoss;
|
||||||
|
|
||||||
|
/** 背景漏损(m³) */
|
||||||
|
private BigDecimal backgroundLoss;
|
||||||
|
|
||||||
|
/** 爆管漏损(m³) */
|
||||||
|
private BigDecimal burstLoss;
|
||||||
|
|
||||||
|
/** 总漏损量(m³) */
|
||||||
|
private BigDecimal totalLoss;
|
||||||
|
|
||||||
|
/** 产销差率(%) */
|
||||||
|
private BigDecimal nrwRate;
|
||||||
|
|
||||||
|
/** 漏损率(%) */
|
||||||
|
private BigDecimal leakageRate;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.water.dma.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.dma.entity.DmaFlowRecord;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA流量记录Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DmaFlowRecordMapper extends BaseMapper<DmaFlowRecord> {
|
||||||
|
|
||||||
|
@Select("SELECT meter_id, SUM(instant_flow) as total_flow FROM dma_flow_record " +
|
||||||
|
"WHERE zone_id = #{zoneId} AND collect_time BETWEEN #{startTime} AND #{endTime} " +
|
||||||
|
"GROUP BY meter_id")
|
||||||
|
List<Map<String, Object>> sumFlowByMeter(Long zoneId, LocalDateTime startTime, LocalDateTime endTime);
|
||||||
|
|
||||||
|
@Select("SELECT MIN(instant_flow) as mnf FROM dma_flow_record " +
|
||||||
|
"WHERE zone_id = #{zoneId} AND collect_time::time BETWEEN '02:00:00' AND '04:00:00' " +
|
||||||
|
"AND collect_time::date = #{date}")
|
||||||
|
BigDecimal getMNF(Long zoneId, java.time.LocalDate date);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.water.dma.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.dma.entity.DmaLeakageAnalysis;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA漏损分析Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DmaLeakageAnalysisMapper extends BaseMapper<DmaLeakageAnalysis> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.water.dma.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.dma.entity.DmaMeter;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA计量表Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DmaMeterMapper extends BaseMapper<DmaMeter> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.water.dma.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.dma.entity.DmaZone;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA分区Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DmaZoneMapper extends BaseMapper<DmaZone> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.water.dma.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.dma.entity.WaterBalance;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 水平衡表Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface WaterBalanceMapper extends BaseMapper<WaterBalance> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.dma.entity.DmaFlowRecord;
|
||||||
|
import com.water.dma.mapper.DmaFlowRecordMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA流量记录服务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DmaFlowService {
|
||||||
|
|
||||||
|
private final DmaFlowRecordMapper flowRecordMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询流量记录
|
||||||
|
*/
|
||||||
|
public Page<DmaFlowRecord> page(int pageNum, int pageSize, Long zoneId, Long meterId,
|
||||||
|
LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
LambdaQueryWrapper<DmaFlowRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (zoneId != null) {
|
||||||
|
wrapper.eq(DmaFlowRecord::getZoneId, zoneId);
|
||||||
|
}
|
||||||
|
if (meterId != null) {
|
||||||
|
wrapper.eq(DmaFlowRecord::getMeterId, meterId);
|
||||||
|
}
|
||||||
|
if (startTime != null) {
|
||||||
|
wrapper.ge(DmaFlowRecord::getCollectTime, startTime);
|
||||||
|
}
|
||||||
|
if (endTime != null) {
|
||||||
|
wrapper.le(DmaFlowRecord::getCollectTime, endTime);
|
||||||
|
}
|
||||||
|
wrapper.orderByDesc(DmaFlowRecord::getCollectTime);
|
||||||
|
return flowRecordMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建流量记录
|
||||||
|
*/
|
||||||
|
public void batchCreate(List<DmaFlowRecord> records) {
|
||||||
|
for (DmaFlowRecord record : records) {
|
||||||
|
if (record.getDataQuality() == null) {
|
||||||
|
record.setDataQuality("good");
|
||||||
|
}
|
||||||
|
flowRecordMapper.insert(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建单条流量记录
|
||||||
|
*/
|
||||||
|
public DmaFlowRecord create(DmaFlowRecord record) {
|
||||||
|
if (record.getDataQuality() == null) {
|
||||||
|
record.setDataQuality("good");
|
||||||
|
}
|
||||||
|
flowRecordMapper.insert(record);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分区进出水量汇总
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getZoneFlowSummary(Long zoneId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
List<Map<String, Object>> flowSums = flowRecordMapper.sumFlowByMeter(zoneId, startTime, endTime);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
BigDecimal totalInflow = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalOutflow = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (Map<String, Object> row : flowSums) {
|
||||||
|
Object flowObj = row.get("total_flow");
|
||||||
|
BigDecimal flow = flowObj != null ? new BigDecimal(flowObj.toString()) : BigDecimal.ZERO;
|
||||||
|
totalInflow = totalInflow.add(flow);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("zoneId", zoneId);
|
||||||
|
result.put("startTime", startTime);
|
||||||
|
result.put("endTime", endTime);
|
||||||
|
result.put("totalInflow", totalInflow);
|
||||||
|
result.put("totalOutflow", totalOutflow);
|
||||||
|
result.put("netFlow", totalInflow.subtract(totalOutflow));
|
||||||
|
result.put("meterCount", flowSums.size());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小夜间流量(MNF)分析
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMNFAnalysis(Long zoneId, LocalDate date) {
|
||||||
|
BigDecimal mnf = flowRecordMapper.getMNF(zoneId, date);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("zoneId", zoneId);
|
||||||
|
result.put("date", date);
|
||||||
|
result.put("mnf", mnf != null ? mnf : BigDecimal.ZERO);
|
||||||
|
result.put("mnfTime", "02:00-04:00");
|
||||||
|
result.put("analysisResult", mnf != null && mnf.compareTo(new BigDecimal("5")) > 0 ? "异常" : "正常");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取流量趋势
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getFlowTrend(Long zoneId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
LambdaQueryWrapper<DmaFlowRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(DmaFlowRecord::getZoneId, zoneId);
|
||||||
|
wrapper.ge(DmaFlowRecord::getCollectTime, startTime);
|
||||||
|
wrapper.le(DmaFlowRecord::getCollectTime, endTime);
|
||||||
|
wrapper.orderByAsc(DmaFlowRecord::getCollectTime);
|
||||||
|
|
||||||
|
List<DmaFlowRecord> records = flowRecordMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
List<Map<String, Object>> trend = new ArrayList<>();
|
||||||
|
for (DmaFlowRecord record : records) {
|
||||||
|
Map<String, Object> point = new LinkedHashMap<>();
|
||||||
|
point.put("time", record.getCollectTime());
|
||||||
|
point.put("instantFlow", record.getInstantFlow());
|
||||||
|
point.put("totalFlow", record.getTotalFlow());
|
||||||
|
point.put("pressure", record.getPressure());
|
||||||
|
trend.add(point);
|
||||||
|
}
|
||||||
|
return trend;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.dma.entity.DmaLeakageAnalysis;
|
||||||
|
import com.water.dma.entity.DmaZone;
|
||||||
|
import com.water.dma.mapper.DmaLeakageAnalysisMapper;
|
||||||
|
import com.water.dma.mapper.DmaZoneMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA漏损分析服务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DmaLeakageService {
|
||||||
|
|
||||||
|
private final DmaLeakageAnalysisMapper leakageMapper;
|
||||||
|
private final DmaZoneMapper zoneMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询漏损分析
|
||||||
|
*/
|
||||||
|
public Page<DmaLeakageAnalysis> page(int pageNum, int pageSize, Long zoneId,
|
||||||
|
LocalDate startDate, LocalDate endDate) {
|
||||||
|
LambdaQueryWrapper<DmaLeakageAnalysis> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (zoneId != null) {
|
||||||
|
wrapper.eq(DmaLeakageAnalysis::getZoneId, zoneId);
|
||||||
|
}
|
||||||
|
if (startDate != null) {
|
||||||
|
wrapper.ge(DmaLeakageAnalysis::getAnalysisDate, startDate);
|
||||||
|
}
|
||||||
|
if (endDate != null) {
|
||||||
|
wrapper.le(DmaLeakageAnalysis::getAnalysisDate, endDate);
|
||||||
|
}
|
||||||
|
wrapper.orderByDesc(DmaLeakageAnalysis::getAnalysisDate);
|
||||||
|
return leakageMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行漏损分析
|
||||||
|
*/
|
||||||
|
public DmaLeakageAnalysis analyze(Long zoneId, LocalDate date, BigDecimal supplyVolume, BigDecimal saleVolume) {
|
||||||
|
DmaLeakageAnalysis analysis = new DmaLeakageAnalysis();
|
||||||
|
analysis.setZoneId(zoneId);
|
||||||
|
analysis.setAnalysisDate(date);
|
||||||
|
analysis.setSupplyVolume(supplyVolume);
|
||||||
|
analysis.setSaleVolume(saleVolume);
|
||||||
|
|
||||||
|
// 计算漏损量
|
||||||
|
BigDecimal leakageVolume = supplyVolume.subtract(saleVolume);
|
||||||
|
analysis.setLeakageVolume(leakageVolume);
|
||||||
|
|
||||||
|
// 计算产销差率
|
||||||
|
if (supplyVolume.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
BigDecimal nrwRate = leakageVolume.multiply(new BigDecimal("100"))
|
||||||
|
.divide(supplyVolume, 2, RoundingMode.HALF_UP);
|
||||||
|
analysis.setNrwRate(nrwRate);
|
||||||
|
analysis.setLeakageRate(nrwRate);
|
||||||
|
} else {
|
||||||
|
analysis.setNrwRate(BigDecimal.ZERO);
|
||||||
|
analysis.setLeakageRate(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置报警级别
|
||||||
|
String alarmLevel = determineAlarmLevel(analysis.getNrwRate());
|
||||||
|
analysis.setAlarmLevel(alarmLevel);
|
||||||
|
|
||||||
|
leakageMapper.insert(analysis);
|
||||||
|
return analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定报警级别
|
||||||
|
*/
|
||||||
|
private String determineAlarmLevel(BigDecimal nrwRate) {
|
||||||
|
if (nrwRate == null) return "normal";
|
||||||
|
if (nrwRate.compareTo(new BigDecimal("20")) > 0) {
|
||||||
|
return "critical";
|
||||||
|
} else if (nrwRate.compareTo(new BigDecimal("12")) > 0) {
|
||||||
|
return "warning";
|
||||||
|
}
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取漏损趋势分析
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getTrend(Long zoneId, int days) {
|
||||||
|
LocalDate endDate = LocalDate.now();
|
||||||
|
LocalDate startDate = endDate.minusDays(days);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<DmaLeakageAnalysis> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(DmaLeakageAnalysis::getZoneId, zoneId);
|
||||||
|
wrapper.ge(DmaLeakageAnalysis::getAnalysisDate, startDate);
|
||||||
|
wrapper.le(DmaLeakageAnalysis::getAnalysisDate, endDate);
|
||||||
|
wrapper.orderByAsc(DmaLeakageAnalysis::getAnalysisDate);
|
||||||
|
|
||||||
|
List<DmaLeakageAnalysis> analyses = leakageMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
List<Map<String, Object>> trend = new ArrayList<>();
|
||||||
|
for (DmaLeakageAnalysis a : analyses) {
|
||||||
|
Map<String, Object> point = new LinkedHashMap<>();
|
||||||
|
point.put("date", a.getAnalysisDate());
|
||||||
|
point.put("supplyVolume", a.getSupplyVolume());
|
||||||
|
point.put("saleVolume", a.getSaleVolume());
|
||||||
|
point.put("leakageVolume", a.getLeakageVolume());
|
||||||
|
point.put("nrwRate", a.getNrwRate());
|
||||||
|
point.put("alarmLevel", a.getAlarmLevel());
|
||||||
|
trend.add(point);
|
||||||
|
}
|
||||||
|
return trend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取报警列表
|
||||||
|
*/
|
||||||
|
public List<DmaLeakageAnalysis> getAlarms(String alarmLevel) {
|
||||||
|
LambdaQueryWrapper<DmaLeakageAnalysis> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (alarmLevel != null && !alarmLevel.isEmpty()) {
|
||||||
|
wrapper.eq(DmaLeakageAnalysis::getAlarmLevel, alarmLevel);
|
||||||
|
} else {
|
||||||
|
wrapper.in(DmaLeakageAnalysis::getAlarmLevel, "warning", "critical");
|
||||||
|
}
|
||||||
|
wrapper.orderByDesc(DmaLeakageAnalysis::getAnalysisDate);
|
||||||
|
return leakageMapper.selectList(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分区漏损汇总
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getZoneSummary(Long zoneId) {
|
||||||
|
LambdaQueryWrapper<DmaLeakageAnalysis> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(DmaLeakageAnalysis::getZoneId, zoneId);
|
||||||
|
wrapper.orderByDesc(DmaLeakageAnalysis::getAnalysisDate);
|
||||||
|
wrapper.last("LIMIT 30");
|
||||||
|
|
||||||
|
List<DmaLeakageAnalysis> recent = leakageMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
Map<String, Object> summary = new LinkedHashMap<>();
|
||||||
|
if (recent.isEmpty()) {
|
||||||
|
summary.put("zoneId", zoneId);
|
||||||
|
summary.put("avgNrwRate", BigDecimal.ZERO);
|
||||||
|
summary.put("totalLeakage", BigDecimal.ZERO);
|
||||||
|
summary.put("alarmCount", 0);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal totalNrw = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalLeakage = BigDecimal.ZERO;
|
||||||
|
int alarmCount = 0;
|
||||||
|
|
||||||
|
for (DmaLeakageAnalysis a : recent) {
|
||||||
|
if (a.getNrwRate() != null) totalNrw = totalNrw.add(a.getNrwRate());
|
||||||
|
if (a.getLeakageVolume() != null) totalLeakage = totalLeakage.add(a.getLeakageVolume());
|
||||||
|
if (!"normal".equals(a.getAlarmLevel())) alarmCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal avgNrw = totalNrw.divide(new BigDecimal(recent.size()), 2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
DmaZone zone = zoneMapper.selectById(zoneId);
|
||||||
|
summary.put("zoneId", zoneId);
|
||||||
|
summary.put("zoneName", zone != null ? zone.getZoneName() : "");
|
||||||
|
summary.put("dataDays", recent.size());
|
||||||
|
summary.put("avgNrwRate", avgNrw);
|
||||||
|
summary.put("totalLeakage", totalLeakage);
|
||||||
|
summary.put("alarmCount", alarmCount);
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.dma.entity.DmaMeter;
|
||||||
|
import com.water.dma.mapper.DmaMeterMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA计量表管理服务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DmaMeterService {
|
||||||
|
|
||||||
|
private final DmaMeterMapper meterMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询计量表
|
||||||
|
*/
|
||||||
|
public Page<DmaMeter> page(int pageNum, int pageSize, Long zoneId, String meterType) {
|
||||||
|
LambdaQueryWrapper<DmaMeter> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (zoneId != null) {
|
||||||
|
wrapper.eq(DmaMeter::getZoneId, zoneId);
|
||||||
|
}
|
||||||
|
if (meterType != null && !meterType.isEmpty()) {
|
||||||
|
wrapper.eq(DmaMeter::getMeterType, meterType);
|
||||||
|
}
|
||||||
|
wrapper.orderByAsc(DmaMeter::getMeterCode);
|
||||||
|
return meterMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取计量表详情
|
||||||
|
*/
|
||||||
|
public DmaMeter getById(Long id) {
|
||||||
|
return meterMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建计量表
|
||||||
|
*/
|
||||||
|
public DmaMeter create(DmaMeter meter) {
|
||||||
|
if (meter.getStatus() == null) {
|
||||||
|
meter.setStatus("online");
|
||||||
|
}
|
||||||
|
meterMapper.insert(meter);
|
||||||
|
return meter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新计量表
|
||||||
|
*/
|
||||||
|
public void update(DmaMeter meter) {
|
||||||
|
meterMapper.updateById(meter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除计量表
|
||||||
|
*/
|
||||||
|
public void delete(Long id) {
|
||||||
|
meterMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分区下的所有计量表
|
||||||
|
*/
|
||||||
|
public List<DmaMeter> listByZoneId(Long zoneId) {
|
||||||
|
LambdaQueryWrapper<DmaMeter> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(DmaMeter::getZoneId, zoneId);
|
||||||
|
return meterMapper.selectList(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计分区表计数量
|
||||||
|
*/
|
||||||
|
public Long countByZoneId(Long zoneId) {
|
||||||
|
LambdaQueryWrapper<DmaMeter> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(DmaMeter::getZoneId, zoneId);
|
||||||
|
return meterMapper.selectCount(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定计量表到分区
|
||||||
|
*/
|
||||||
|
public void bindToZone(Long meterId, Long zoneId) {
|
||||||
|
DmaMeter meter = meterMapper.selectById(meterId);
|
||||||
|
if (meter == null) {
|
||||||
|
throw new RuntimeException("计量表不存在");
|
||||||
|
}
|
||||||
|
meter.setZoneId(zoneId);
|
||||||
|
meterMapper.updateById(meter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.dma.entity.DmaZone;
|
||||||
|
import com.water.dma.mapper.DmaZoneMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA分区管理服务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DmaZoneService {
|
||||||
|
|
||||||
|
private final DmaZoneMapper zoneMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询分区
|
||||||
|
*/
|
||||||
|
public Page<DmaZone> page(int pageNum, int pageSize, String zoneName) {
|
||||||
|
LambdaQueryWrapper<DmaZone> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (zoneName != null && !zoneName.isEmpty()) {
|
||||||
|
wrapper.like(DmaZone::getZoneName, zoneName);
|
||||||
|
}
|
||||||
|
wrapper.orderByAsc(DmaZone::getZoneLevel, DmaZone::getZoneCode);
|
||||||
|
return zoneMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分区详情
|
||||||
|
*/
|
||||||
|
public DmaZone getById(Long id) {
|
||||||
|
return zoneMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分区
|
||||||
|
*/
|
||||||
|
public DmaZone create(DmaZone zone) {
|
||||||
|
if (zone.getStatus() == null) {
|
||||||
|
zone.setStatus("active");
|
||||||
|
}
|
||||||
|
zoneMapper.insert(zone);
|
||||||
|
return zone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分区
|
||||||
|
*/
|
||||||
|
public void update(DmaZone zone) {
|
||||||
|
zoneMapper.updateById(zone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分区
|
||||||
|
*/
|
||||||
|
public void delete(Long id) {
|
||||||
|
// 检查是否有子分区
|
||||||
|
LambdaQueryWrapper<DmaZone> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(DmaZone::getParentId, id);
|
||||||
|
Long count = zoneMapper.selectCount(wrapper);
|
||||||
|
if (count > 0) {
|
||||||
|
throw new RuntimeException("该分区存在子分区,无法删除");
|
||||||
|
}
|
||||||
|
zoneMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分区树形结构
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getZoneTree() {
|
||||||
|
LambdaQueryWrapper<DmaZone> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.orderByAsc(DmaZone::getZoneLevel, DmaZone::getZoneCode);
|
||||||
|
List<DmaZone> allZones = zoneMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
Map<Long, Map<String, Object>> zoneMap = new LinkedHashMap<>();
|
||||||
|
List<Map<String, Object>> roots = new ArrayList<>();
|
||||||
|
|
||||||
|
for (DmaZone zone : allZones) {
|
||||||
|
Map<String, Object> node = new LinkedHashMap<>();
|
||||||
|
node.put("id", zone.getId());
|
||||||
|
node.put("zoneName", zone.getZoneName());
|
||||||
|
node.put("zoneCode", zone.getZoneCode());
|
||||||
|
node.put("zoneLevel", zone.getZoneLevel());
|
||||||
|
node.put("parentId", zone.getParentId());
|
||||||
|
node.put("area", zone.getArea());
|
||||||
|
node.put("status", zone.getStatus());
|
||||||
|
node.put("children", new ArrayList<>());
|
||||||
|
zoneMap.put(zone.getId(), node);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<Long, Map<String, Object>> entry : zoneMap.entrySet()) {
|
||||||
|
Map<String, Object> node = entry.getValue();
|
||||||
|
Long parentId = (Long) node.get("parentId");
|
||||||
|
if (parentId == null) {
|
||||||
|
roots.add(node);
|
||||||
|
} else {
|
||||||
|
Map<String, Object> parent = zoneMap.get(parentId);
|
||||||
|
if (parent != null) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> children = (List<Map<String, Object>>) parent.get("children");
|
||||||
|
children.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有分区列表
|
||||||
|
*/
|
||||||
|
public List<DmaZone> listAll() {
|
||||||
|
return zoneMapper.selectList(new LambdaQueryWrapper<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.dma.entity.WaterBalance;
|
||||||
|
import com.water.dma.mapper.WaterBalanceMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 水平衡分析服务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WaterBalanceService {
|
||||||
|
|
||||||
|
private final WaterBalanceMapper balanceMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询水平衡数据
|
||||||
|
*/
|
||||||
|
public Page<WaterBalance> page(int pageNum, int pageSize, Long zoneId, String period,
|
||||||
|
LocalDate startDate, LocalDate endDate) {
|
||||||
|
LambdaQueryWrapper<WaterBalance> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (zoneId != null) {
|
||||||
|
wrapper.eq(WaterBalance::getZoneId, zoneId);
|
||||||
|
}
|
||||||
|
if (period != null && !period.isEmpty()) {
|
||||||
|
wrapper.eq(WaterBalance::getPeriod, period);
|
||||||
|
}
|
||||||
|
if (startDate != null) {
|
||||||
|
wrapper.ge(WaterBalance::getStatDate, startDate);
|
||||||
|
}
|
||||||
|
if (endDate != null) {
|
||||||
|
wrapper.le(WaterBalance::getStatDate, endDate);
|
||||||
|
}
|
||||||
|
wrapper.orderByDesc(WaterBalance::getStatDate);
|
||||||
|
return balanceMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建水平衡记录
|
||||||
|
*/
|
||||||
|
public WaterBalance create(WaterBalance balance) {
|
||||||
|
// 自动计算总漏损
|
||||||
|
if (balance.getTotalLoss() == null && balance.getTotalSupply() != null && balance.getTotalSale() != null) {
|
||||||
|
balance.setTotalLoss(balance.getTotalSupply().subtract(balance.getTotalSale()));
|
||||||
|
}
|
||||||
|
// 计算产销差率
|
||||||
|
calculateRates(balance);
|
||||||
|
balanceMapper.insert(balance);
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新水平衡记录
|
||||||
|
*/
|
||||||
|
public void update(WaterBalance balance) {
|
||||||
|
calculateRates(balance);
|
||||||
|
balanceMapper.updateById(balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除水平衡记录
|
||||||
|
*/
|
||||||
|
public void delete(Long id) {
|
||||||
|
balanceMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算产销差率和漏损率
|
||||||
|
*/
|
||||||
|
private void calculateRates(WaterBalance balance) {
|
||||||
|
if (balance.getTotalSupply() != null && balance.getTotalSupply().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
BigDecimal totalLoss = balance.getTotalLoss() != null ? balance.getTotalLoss() : BigDecimal.ZERO;
|
||||||
|
BigDecimal rate = totalLoss.multiply(new BigDecimal("100"))
|
||||||
|
.divide(balance.getTotalSupply(), 2, RoundingMode.HALF_UP);
|
||||||
|
balance.setNrwRate(rate);
|
||||||
|
balance.setLeakageRate(rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成水平衡分析报告
|
||||||
|
*/
|
||||||
|
public Map<String, Object> generateReport(Long zoneId, String period, LocalDate startDate, LocalDate endDate) {
|
||||||
|
LambdaQueryWrapper<WaterBalance> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(WaterBalance::getZoneId, zoneId);
|
||||||
|
wrapper.eq(WaterBalance::getPeriod, period);
|
||||||
|
wrapper.ge(WaterBalance::getStatDate, startDate);
|
||||||
|
wrapper.le(WaterBalance::getStatDate, endDate);
|
||||||
|
|
||||||
|
List<WaterBalance> records = balanceMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
Map<String, Object> report = new LinkedHashMap<>();
|
||||||
|
report.put("zoneId", zoneId);
|
||||||
|
report.put("period", period);
|
||||||
|
report.put("startDate", startDate);
|
||||||
|
report.put("endDate", endDate);
|
||||||
|
|
||||||
|
if (records.isEmpty()) {
|
||||||
|
report.put("totalSupply", BigDecimal.ZERO);
|
||||||
|
report.put("totalSale", BigDecimal.ZERO);
|
||||||
|
report.put("totalLoss", BigDecimal.ZERO);
|
||||||
|
report.put("avgNrwRate", BigDecimal.ZERO);
|
||||||
|
report.put("recordCount", 0);
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal totalSupply = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalSale = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalLoss = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalApparentLoss = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalRealLoss = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (WaterBalance r : records) {
|
||||||
|
if (r.getTotalSupply() != null) totalSupply = totalSupply.add(r.getTotalSupply());
|
||||||
|
if (r.getTotalSale() != null) totalSale = totalSale.add(r.getTotalSale());
|
||||||
|
if (r.getTotalLoss() != null) totalLoss = totalLoss.add(r.getTotalLoss());
|
||||||
|
if (r.getApparentLoss() != null) totalApparentLoss = totalApparentLoss.add(r.getApparentLoss());
|
||||||
|
if (r.getRealLoss() != null) totalRealLoss = totalRealLoss.add(r.getRealLoss());
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal avgNrwRate = BigDecimal.ZERO;
|
||||||
|
if (totalSupply.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
avgNrwRate = totalLoss.multiply(new BigDecimal("100"))
|
||||||
|
.divide(totalSupply, 2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
report.put("totalSupply", totalSupply);
|
||||||
|
report.put("totalSale", totalSale);
|
||||||
|
report.put("totalLoss", totalLoss);
|
||||||
|
report.put("apparentLoss", totalApparentLoss);
|
||||||
|
report.put("realLoss", totalRealLoss);
|
||||||
|
report.put("avgNrwRate", avgNrwRate);
|
||||||
|
report.put("recordCount", records.size());
|
||||||
|
|
||||||
|
// IWA水平衡组成
|
||||||
|
Map<String, Object> iwa = new LinkedHashMap<>();
|
||||||
|
iwa.put("billingSale", records.stream()
|
||||||
|
.map(WaterBalance::getBillingSale)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||||
|
iwa.put("freeSupply", records.stream()
|
||||||
|
.map(WaterBalance::getFreeSupply)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||||
|
iwa.put("apparentLoss", totalApparentLoss);
|
||||||
|
iwa.put("realLoss", totalRealLoss);
|
||||||
|
report.put("iwaComponents", iwa);
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取水平衡详情
|
||||||
|
*/
|
||||||
|
public WaterBalance getById(Long id) {
|
||||||
|
return balanceMapper.selectById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
server:
|
||||||
|
port: 8090
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: wm-dma
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/water_management
|
||||||
|
username: water
|
||||||
|
password: water123
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
cloud:
|
||||||
|
nacos:
|
||||||
|
discovery:
|
||||||
|
server-addr: localhost:8848
|
||||||
|
|
||||||
|
mybatis-plus:
|
||||||
|
global-config:
|
||||||
|
db-config:
|
||||||
|
logic-delete-field: deleted
|
||||||
|
logic-delete-value: 1
|
||||||
|
logic-not-delete-value: 0
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.water.dma: DEBUG
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.water.dma.entity.DmaFlowRecord;
|
||||||
|
import com.water.dma.mapper.DmaFlowRecordMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA流量服务测试
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DmaFlowServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DmaFlowRecordMapper flowRecordMapper;
|
||||||
|
|
||||||
|
private DmaFlowService flowService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
flowService = new DmaFlowService(flowRecordMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("创建流量记录")
|
||||||
|
void testCreate() {
|
||||||
|
DmaFlowRecord record = new DmaFlowRecord();
|
||||||
|
record.setZoneId(1L);
|
||||||
|
record.setMeterId(1L);
|
||||||
|
record.setInstantFlow(new BigDecimal("12.5"));
|
||||||
|
record.setTotalFlow(new BigDecimal("1000"));
|
||||||
|
record.setCollectTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
when(flowRecordMapper.insert(any(DmaFlowRecord.class))).thenReturn(1);
|
||||||
|
|
||||||
|
DmaFlowRecord created = flowService.create(record);
|
||||||
|
assertNotNull(created);
|
||||||
|
assertEquals("good", created.getDataQuality());
|
||||||
|
verify(flowRecordMapper).insert(any(DmaFlowRecord.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("批量创建流量记录")
|
||||||
|
void testBatchCreate() {
|
||||||
|
DmaFlowRecord r1 = new DmaFlowRecord();
|
||||||
|
r1.setZoneId(1L);
|
||||||
|
r1.setMeterId(1L);
|
||||||
|
r1.setInstantFlow(new BigDecimal("10"));
|
||||||
|
r1.setCollectTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
DmaFlowRecord r2 = new DmaFlowRecord();
|
||||||
|
r2.setZoneId(1L);
|
||||||
|
r2.setMeterId(2L);
|
||||||
|
r2.setInstantFlow(new BigDecimal("15"));
|
||||||
|
r2.setCollectTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
when(flowRecordMapper.insert(any(DmaFlowRecord.class))).thenReturn(1);
|
||||||
|
|
||||||
|
flowService.batchCreate(Arrays.asList(r1, r2));
|
||||||
|
verify(flowRecordMapper, times(2)).insert(any(DmaFlowRecord.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("获取分区进出水量汇总")
|
||||||
|
void testGetZoneFlowSummary() {
|
||||||
|
Map<String, Object> row = new HashMap<>();
|
||||||
|
row.put("meter_id", 1L);
|
||||||
|
row.put("total_flow", new BigDecimal("100"));
|
||||||
|
|
||||||
|
when(flowRecordMapper.sumFlowByMeter(eq(1L), any(), any())).thenReturn(List.of(row));
|
||||||
|
|
||||||
|
Map<String, Object> summary = flowService.getZoneFlowSummary(1L,
|
||||||
|
LocalDateTime.now().minusHours(24), LocalDateTime.now());
|
||||||
|
|
||||||
|
assertNotNull(summary);
|
||||||
|
assertEquals(1L, summary.get("zoneId"));
|
||||||
|
assertEquals(1, summary.get("meterCount"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("MNF分析-正常")
|
||||||
|
void testGetMNFAnalysis() {
|
||||||
|
when(flowRecordMapper.getMNF(eq(1L), any())).thenReturn(new BigDecimal("3.5"));
|
||||||
|
|
||||||
|
Map<String, Object> result = flowService.getMNFAnalysis(1L, LocalDate.now());
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(new BigDecimal("3.5"), result.get("mnf"));
|
||||||
|
assertEquals("正常", result.get("analysisResult"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("MNF分析-无数据")
|
||||||
|
void testGetMNFAnalysisNoData() {
|
||||||
|
when(flowRecordMapper.getMNF(eq(1L), any())).thenReturn(null);
|
||||||
|
|
||||||
|
Map<String, Object> result = flowService.getMNFAnalysis(1L, LocalDate.now());
|
||||||
|
|
||||||
|
assertEquals(BigDecimal.ZERO, result.get("mnf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("获取流量趋势")
|
||||||
|
void testGetFlowTrend() {
|
||||||
|
DmaFlowRecord record = new DmaFlowRecord();
|
||||||
|
record.setCollectTime(LocalDateTime.now());
|
||||||
|
record.setInstantFlow(new BigDecimal("12.5"));
|
||||||
|
|
||||||
|
when(flowRecordMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(record));
|
||||||
|
|
||||||
|
List<Map<String, Object>> trend = flowService.getFlowTrend(1L,
|
||||||
|
LocalDateTime.now().minusHours(24), LocalDateTime.now());
|
||||||
|
|
||||||
|
assertEquals(1, trend.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.water.dma.entity.DmaLeakageAnalysis;
|
||||||
|
import com.water.dma.entity.DmaZone;
|
||||||
|
import com.water.dma.mapper.DmaLeakageAnalysisMapper;
|
||||||
|
import com.water.dma.mapper.DmaZoneMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA漏损分析服务测试
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DmaLeakageServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DmaLeakageAnalysisMapper leakageMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DmaZoneMapper zoneMapper;
|
||||||
|
|
||||||
|
private DmaLeakageService leakageService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
leakageService = new DmaLeakageService(leakageMapper, zoneMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("执行漏损分析-正常")
|
||||||
|
void testAnalyze() {
|
||||||
|
BigDecimal supply = new BigDecimal("1000");
|
||||||
|
BigDecimal sale = new BigDecimal("850");
|
||||||
|
|
||||||
|
when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
|
||||||
|
|
||||||
|
DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(new BigDecimal("150"), result.getLeakageVolume());
|
||||||
|
assertEquals(0, new BigDecimal("15.00").compareTo(result.getNrwRate()));
|
||||||
|
assertEquals("warning", result.getAlarmLevel());
|
||||||
|
verify(leakageMapper).insert(any(DmaLeakageAnalysis.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("执行漏损分析-高漏损报警")
|
||||||
|
void testAnalyzeHighAlarm() {
|
||||||
|
BigDecimal supply = new BigDecimal("1000");
|
||||||
|
BigDecimal sale = new BigDecimal("700");
|
||||||
|
|
||||||
|
when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
|
||||||
|
|
||||||
|
DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
|
||||||
|
|
||||||
|
assertEquals("critical", result.getAlarmLevel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("执行漏损分析-零供水")
|
||||||
|
void testAnalyzeZeroSupply() {
|
||||||
|
BigDecimal supply = BigDecimal.ZERO;
|
||||||
|
BigDecimal sale = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
|
||||||
|
|
||||||
|
DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
|
||||||
|
|
||||||
|
assertEquals(0, BigDecimal.ZERO.compareTo(result.getNrwRate()));
|
||||||
|
assertEquals("normal", result.getAlarmLevel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("获取漏损趋势")
|
||||||
|
void testGetTrend() {
|
||||||
|
DmaLeakageAnalysis analysis = new DmaLeakageAnalysis();
|
||||||
|
analysis.setZoneId(1L);
|
||||||
|
analysis.setAnalysisDate(LocalDate.now());
|
||||||
|
analysis.setNrwRate(new BigDecimal("12.5"));
|
||||||
|
|
||||||
|
when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(analysis));
|
||||||
|
|
||||||
|
List<Map<String, Object>> trend = leakageService.getTrend(1L, 30);
|
||||||
|
assertEquals(1, trend.size());
|
||||||
|
assertEquals(new BigDecimal("12.5"), trend.get(0).get("nrwRate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("获取分区漏损汇总-无数据")
|
||||||
|
void testGetZoneSummaryEmpty() {
|
||||||
|
when(zoneMapper.selectById(1L)).thenReturn(null);
|
||||||
|
when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of());
|
||||||
|
|
||||||
|
Map<String, Object> summary = leakageService.getZoneSummary(1L);
|
||||||
|
assertEquals(0, ((BigDecimal) summary.get("avgNrwRate")).compareTo(BigDecimal.ZERO));
|
||||||
|
assertEquals(0, summary.get("alarmCount"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("获取报警列表")
|
||||||
|
void testGetAlarms() {
|
||||||
|
DmaLeakageAnalysis alarm = new DmaLeakageAnalysis();
|
||||||
|
alarm.setAlarmLevel("critical");
|
||||||
|
|
||||||
|
when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(alarm));
|
||||||
|
|
||||||
|
List<DmaLeakageAnalysis> alarms = leakageService.getAlarms(null);
|
||||||
|
assertFalse(alarms.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.water.dma.entity.DmaMeter;
|
||||||
|
import com.water.dma.mapper.DmaMeterMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA计量表服务测试
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DmaMeterServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DmaMeterMapper meterMapper;
|
||||||
|
|
||||||
|
private DmaMeterService meterService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
meterService = new DmaMeterService(meterMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("创建计量表")
|
||||||
|
void testCreateMeter() {
|
||||||
|
DmaMeter meter = new DmaMeter();
|
||||||
|
meter.setZoneId(1L);
|
||||||
|
meter.setMeterCode("M-001");
|
||||||
|
meter.setMeterName("进水表1");
|
||||||
|
meter.setMeterType("inlet");
|
||||||
|
|
||||||
|
when(meterMapper.insert(any(DmaMeter.class))).thenReturn(1);
|
||||||
|
|
||||||
|
DmaMeter created = meterService.create(meter);
|
||||||
|
assertNotNull(created);
|
||||||
|
assertEquals("online", created.getStatus());
|
||||||
|
verify(meterMapper).insert(any(DmaMeter.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("绑定计量表到分区")
|
||||||
|
void testBindToZone() {
|
||||||
|
DmaMeter meter = new DmaMeter();
|
||||||
|
meter.setId(1L);
|
||||||
|
meter.setMeterCode("M-001");
|
||||||
|
meter.setZoneId(null);
|
||||||
|
|
||||||
|
when(meterMapper.selectById(1L)).thenReturn(meter);
|
||||||
|
when(meterMapper.updateById(any(DmaMeter.class))).thenReturn(1);
|
||||||
|
|
||||||
|
meterService.bindToZone(1L, 2L);
|
||||||
|
assertEquals(2L, meter.getZoneId());
|
||||||
|
verify(meterMapper).updateById(any(DmaMeter.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("绑定不存在的计量表-失败")
|
||||||
|
void testBindToZoneNotFound() {
|
||||||
|
when(meterMapper.selectById(999L)).thenReturn(null);
|
||||||
|
|
||||||
|
assertThrows(RuntimeException.class, () -> {
|
||||||
|
meterService.bindToZone(999L, 1L);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("统计分区表计数量")
|
||||||
|
void testCountByZoneId() {
|
||||||
|
when(meterMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(5L);
|
||||||
|
|
||||||
|
Long count = meterService.countByZoneId(1L);
|
||||||
|
assertEquals(5L, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("获取分区下的所有计量表")
|
||||||
|
void testListByZoneId() {
|
||||||
|
DmaMeter meter = new DmaMeter();
|
||||||
|
meter.setId(1L);
|
||||||
|
meter.setZoneId(1L);
|
||||||
|
|
||||||
|
when(meterMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(meter));
|
||||||
|
|
||||||
|
List<DmaMeter> meters = meterService.listByZoneId(1L);
|
||||||
|
assertEquals(1, meters.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.water.dma.entity.DmaZone;
|
||||||
|
import com.water.dma.mapper.DmaZoneMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMA分区服务测试
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DmaZoneServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DmaZoneMapper zoneMapper;
|
||||||
|
|
||||||
|
private DmaZoneService zoneService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
zoneService = new DmaZoneService(zoneMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("创建DMA分区")
|
||||||
|
void testCreateZone() {
|
||||||
|
DmaZone zone = new DmaZone();
|
||||||
|
zone.setZoneName("测试分区A");
|
||||||
|
zone.setZoneCode("DMA-001");
|
||||||
|
zone.setZoneLevel(1);
|
||||||
|
zone.setArea("城北区");
|
||||||
|
|
||||||
|
when(zoneMapper.insert(any(DmaZone.class))).thenReturn(1);
|
||||||
|
|
||||||
|
DmaZone created = zoneService.create(zone);
|
||||||
|
assertNotNull(created);
|
||||||
|
assertEquals("active", created.getStatus());
|
||||||
|
verify(zoneMapper).insert(any(DmaZone.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("获取分区树形结构")
|
||||||
|
void testGetZoneTree() {
|
||||||
|
DmaZone parent = new DmaZone();
|
||||||
|
parent.setId(1L);
|
||||||
|
parent.setZoneName("总区");
|
||||||
|
parent.setZoneCode("ROOT");
|
||||||
|
parent.setZoneLevel(1);
|
||||||
|
parent.setParentId(null);
|
||||||
|
|
||||||
|
DmaZone child = new DmaZone();
|
||||||
|
child.setId(2L);
|
||||||
|
child.setZoneName("子区A");
|
||||||
|
child.setZoneCode("A");
|
||||||
|
child.setZoneLevel(2);
|
||||||
|
child.setParentId(1L);
|
||||||
|
|
||||||
|
when(zoneMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Arrays.asList(parent, child));
|
||||||
|
|
||||||
|
List<Map<String, Object>> tree = zoneService.getZoneTree();
|
||||||
|
assertNotNull(tree);
|
||||||
|
assertEquals(1, tree.size());
|
||||||
|
assertEquals("总区", tree.get(0).get("zoneName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("删除分区-存在子分区时失败")
|
||||||
|
void testDeleteZoneWithChildren() {
|
||||||
|
when(zoneMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
|
||||||
|
|
||||||
|
assertThrows(RuntimeException.class, () -> {
|
||||||
|
zoneService.delete(1L);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("删除分区-无子分区时成功")
|
||||||
|
void testDeleteZoneSuccess() {
|
||||||
|
when(zoneMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
||||||
|
when(zoneMapper.deleteById(1L)).thenReturn(1);
|
||||||
|
|
||||||
|
zoneService.delete(1L);
|
||||||
|
verify(zoneMapper).deleteById(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("获取所有分区列表")
|
||||||
|
void testListAll() {
|
||||||
|
DmaZone zone = new DmaZone();
|
||||||
|
zone.setId(1L);
|
||||||
|
zone.setZoneName("测试分区");
|
||||||
|
|
||||||
|
when(zoneMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(zone));
|
||||||
|
|
||||||
|
List<DmaZone> zones = zoneService.listAll();
|
||||||
|
assertEquals(1, zones.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.water.dma.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.water.dma.entity.WaterBalance;
|
||||||
|
import com.water.dma.mapper.WaterBalanceMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 水平衡服务测试
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class WaterBalanceServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private WaterBalanceMapper balanceMapper;
|
||||||
|
|
||||||
|
private WaterBalanceService balanceService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
balanceService = new WaterBalanceService(balanceMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("创建水平衡记录-自动计算漏损")
|
||||||
|
void testCreate() {
|
||||||
|
WaterBalance balance = new WaterBalance();
|
||||||
|
balance.setZoneId(1L);
|
||||||
|
balance.setPeriod("monthly");
|
||||||
|
balance.setStatDate(LocalDate.of(2024, 1, 1));
|
||||||
|
balance.setTotalSupply(new BigDecimal("10000"));
|
||||||
|
balance.setTotalSale(new BigDecimal("8500"));
|
||||||
|
|
||||||
|
when(balanceMapper.insert(any(WaterBalance.class))).thenReturn(1);
|
||||||
|
|
||||||
|
WaterBalance created = balanceService.create(balance);
|
||||||
|
|
||||||
|
assertNotNull(created);
|
||||||
|
assertEquals(0, new BigDecimal("1500").compareTo(created.getTotalLoss()));
|
||||||
|
assertEquals(0, new BigDecimal("15.00").compareTo(created.getNrwRate()));
|
||||||
|
verify(balanceMapper).insert(any(WaterBalance.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("创建水平衡-零供水")
|
||||||
|
void testCreateZeroSupply() {
|
||||||
|
WaterBalance balance = new WaterBalance();
|
||||||
|
balance.setZoneId(1L);
|
||||||
|
balance.setPeriod("daily");
|
||||||
|
balance.setStatDate(LocalDate.now());
|
||||||
|
balance.setTotalSupply(BigDecimal.ZERO);
|
||||||
|
balance.setTotalSale(BigDecimal.ZERO);
|
||||||
|
balance.setTotalLoss(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
when(balanceMapper.insert(any(WaterBalance.class))).thenReturn(1);
|
||||||
|
|
||||||
|
WaterBalance created = balanceService.create(balance);
|
||||||
|
assertNotNull(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("生成水平衡报告-有数据")
|
||||||
|
void testGenerateReport() {
|
||||||
|
WaterBalance b1 = new WaterBalance();
|
||||||
|
b1.setTotalSupply(new BigDecimal("5000"));
|
||||||
|
b1.setTotalSale(new BigDecimal("4200"));
|
||||||
|
b1.setTotalLoss(new BigDecimal("800"));
|
||||||
|
b1.setApparentLoss(new BigDecimal("200"));
|
||||||
|
b1.setRealLoss(new BigDecimal("600"));
|
||||||
|
b1.setBillingSale(new BigDecimal("4000"));
|
||||||
|
b1.setFreeSupply(new BigDecimal("200"));
|
||||||
|
|
||||||
|
when(balanceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(b1));
|
||||||
|
|
||||||
|
Map<String, Object> report = balanceService.generateReport(1L, "monthly",
|
||||||
|
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 31));
|
||||||
|
|
||||||
|
assertNotNull(report);
|
||||||
|
assertEquals(1, report.get("recordCount"));
|
||||||
|
assertEquals(new BigDecimal("5000"), report.get("totalSupply"));
|
||||||
|
assertEquals(new BigDecimal("800"), report.get("totalLoss"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("生成水平衡报告-无数据")
|
||||||
|
void testGenerateReportEmpty() {
|
||||||
|
when(balanceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of());
|
||||||
|
|
||||||
|
Map<String, Object> report = balanceService.generateReport(1L, "monthly",
|
||||||
|
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 31));
|
||||||
|
|
||||||
|
assertEquals(0, report.get("recordCount"));
|
||||||
|
assertEquals(BigDecimal.ZERO, report.get("totalSupply"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("删除水平衡记录")
|
||||||
|
void testDelete() {
|
||||||
|
when(balanceMapper.deleteById(1L)).thenReturn(1);
|
||||||
|
balanceService.delete(1L);
|
||||||
|
verify(balanceMapper).deleteById(1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package com.water.production.controller;
|
||||||
|
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.production.entity.DashboardSummary;
|
||||||
|
import com.water.production.entity.EnergyConsumption;
|
||||||
|
import com.water.production.service.DashboardService;
|
||||||
|
import com.water.production.service.EnergyService;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总览大屏控制器
|
||||||
|
* 提供进出水量、水质、设备、报警、能耗等大屏数据接口
|
||||||
|
*/
|
||||||
|
@Tag(name = "总览大屏")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/production/dashboard")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DashboardController {
|
||||||
|
|
||||||
|
private final DashboardService dashboardService;
|
||||||
|
private final EnergyService energyService;
|
||||||
|
|
||||||
|
// ==================== 综合大屏 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "获取完整大屏数据(聚合所有维度)")
|
||||||
|
@GetMapping("/full")
|
||||||
|
public R<Map<String, Object>> fullDashboard(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||||
|
return R.ok(dashboardService.getFullDashboard(area));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 进出水量 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "进出水量概览(今日/昨日/本月 + 趋势对比)")
|
||||||
|
@GetMapping("/flow/summary")
|
||||||
|
public R<Map<String, Object>> flowSummary(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||||
|
return R.ok(dashboardService.getFlowSummary(area));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "月度进出水量趋势")
|
||||||
|
@GetMapping("/flow/monthly-trend")
|
||||||
|
public R<List<Map<String, Object>>> monthlyFlowTrend(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||||
|
@RequestParam(defaultValue = "6") int months) {
|
||||||
|
return R.ok(dashboardService.getMonthlyFlowTrend(area, months));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 水质概览 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "水质概览(原水/出厂水/末梢水 + 合格率)")
|
||||||
|
@GetMapping("/water-quality/summary")
|
||||||
|
public R<Map<String, Object>> waterQualitySummary(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||||
|
return R.ok(dashboardService.getWaterQualitySummary(area));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 设备概况 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "设备概况(在线/离线/故障 + 在线率)")
|
||||||
|
@GetMapping("/device/summary")
|
||||||
|
public R<Map<String, Object>> deviceSummary(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||||
|
return R.ok(dashboardService.getDeviceSummary(area));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 报警概览 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "报警概览(今日报警/活跃报警/级别分布)")
|
||||||
|
@GetMapping("/alert/summary")
|
||||||
|
public R<Map<String, Object>> alertSummary(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||||
|
return R.ok(dashboardService.getAlertSummary(area));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 能耗数据 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "今日能耗汇总(电耗/药耗)")
|
||||||
|
@GetMapping("/energy/today")
|
||||||
|
public R<Map<String, Object>> todayEnergy(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||||
|
return R.ok(energyService.getTodaySummary(area));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "能耗趋势(按日,最近N天)")
|
||||||
|
@GetMapping("/energy/trend")
|
||||||
|
public R<List<Map<String, Object>>> energyTrend(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||||
|
@RequestParam(defaultValue = "7") int days) {
|
||||||
|
return R.ok(energyService.getEnergyTrend(area, days));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "月度能耗趋势")
|
||||||
|
@GetMapping("/energy/monthly-trend")
|
||||||
|
public R<List<Map<String, Object>>> monthlyEnergyTrend(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||||
|
@RequestParam(defaultValue = "6") int months) {
|
||||||
|
return R.ok(energyService.getMonthlyTrend(area, months));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "单位产水能耗")
|
||||||
|
@GetMapping("/energy/unit-consumption")
|
||||||
|
public R<Map<String, Object>> unitEnergyConsumption(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||||
|
@RequestParam String startDate,
|
||||||
|
@RequestParam String endDate) {
|
||||||
|
return R.ok(energyService.getUnitEnergyConsumption(area, startDate, endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "能耗记录查询(按类型)")
|
||||||
|
@GetMapping("/energy/records")
|
||||||
|
public R<List<EnergyConsumption>> energyRecords(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||||
|
@RequestParam String energyType,
|
||||||
|
@RequestParam String startDate,
|
||||||
|
@RequestParam String endDate) {
|
||||||
|
return R.ok(energyService.getRecordsByType(area, energyType, startDate, endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 历史快照 ====================
|
||||||
|
|
||||||
|
@Operation(summary = "历史总览快照(最近N天)")
|
||||||
|
@GetMapping("/history/snapshots")
|
||||||
|
public R<List<DashboardSummary>> historySnapshots(
|
||||||
|
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||||
|
@RequestParam(defaultValue = "7") int days) {
|
||||||
|
return R.ok(dashboardService.getHistorySnapshots(area, days));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package com.water.production.controller;
|
||||||
|
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.production.dto.MonitorExportRequest;
|
||||||
|
import com.water.production.dto.MonitorQueryRequest;
|
||||||
|
import com.water.production.service.MonitorExportService;
|
||||||
|
import com.water.production.service.MonitorListService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线监测列表 Controller
|
||||||
|
* 提供设备列表、实时数据、统计、导出等接口
|
||||||
|
*/
|
||||||
|
@Tag(name = "在线监测管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/production/monitor")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MonitorController {
|
||||||
|
|
||||||
|
private final MonitorListService monitorListService;
|
||||||
|
private final MonitorExportService monitorExportService;
|
||||||
|
|
||||||
|
// ========== 1. 设备列表(分页 + 多维筛选) ==========
|
||||||
|
@Operation(summary = "在线监测设备列表(分页+多维筛选)")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public R<Map<String, Object>> list(MonitorQueryRequest request) {
|
||||||
|
return R.ok(monitorListService.queryDeviceList(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 2. 设备详情 ==========
|
||||||
|
@Operation(summary = "获取设备详情")
|
||||||
|
@GetMapping("/device/{deviceId}")
|
||||||
|
public R<Map<String, Object>> deviceDetail(@PathVariable Long deviceId) {
|
||||||
|
Map<String, Object> detail = monitorListService.getDeviceDetail(deviceId);
|
||||||
|
if (detail.isEmpty()) {
|
||||||
|
return R.fail(404, "设备不存在");
|
||||||
|
}
|
||||||
|
return R.ok(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 3. 设备实时数据 ==========
|
||||||
|
@Operation(summary = "获取设备实时监测数据")
|
||||||
|
@GetMapping("/device/{deviceId}/realtime")
|
||||||
|
public R<List<Map<String, Object>>> deviceRealtime(@PathVariable Long deviceId) {
|
||||||
|
return R.ok(monitorListService.getDeviceRealtimeData(deviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 4. 设备统计概览 ==========
|
||||||
|
@Operation(summary = "监测设备统计概览(按状态/类型/区域分组)")
|
||||||
|
@GetMapping("/statistics")
|
||||||
|
public R<Map<String, Object>> statistics() {
|
||||||
|
return R.ok(monitorListService.getDeviceStatistics());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 5. 更新设备状态 ==========
|
||||||
|
@Operation(summary = "更新设备状态(online/offline/fault/abnormal)")
|
||||||
|
@PutMapping("/device/{deviceId}/status")
|
||||||
|
public R<String> updateStatus(@PathVariable Long deviceId, @RequestParam String status) {
|
||||||
|
monitorListService.updateDeviceStatus(deviceId, status);
|
||||||
|
return R.ok("状态已更新");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 6. 获取区域列表 ==========
|
||||||
|
@Operation(summary = "获取所有监测区域列表")
|
||||||
|
@GetMapping("/areas")
|
||||||
|
public R<List<String>> areas() {
|
||||||
|
return R.ok(monitorListService.getAreaList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 7. 获取设备类型列表 ==========
|
||||||
|
@Operation(summary = "获取所有设备类型列表")
|
||||||
|
@GetMapping("/device-types")
|
||||||
|
public R<List<String>> deviceTypes() {
|
||||||
|
return R.ok(monitorListService.getDeviceTypeList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 8. 导出 Excel ==========
|
||||||
|
@Operation(summary = "导出在线监测数据(Excel)")
|
||||||
|
@PostMapping("/export/excel")
|
||||||
|
public ResponseEntity<byte[]> exportExcel(@RequestBody MonitorExportRequest request) {
|
||||||
|
request.setFormat("excel");
|
||||||
|
byte[] data = monitorExportService.exportExcel(request);
|
||||||
|
String filename = URLEncoder.encode("在线监测数据.xlsx", StandardCharsets.UTF_8);
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
|
||||||
|
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||||
|
.body(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 9. 导出 CSV ==========
|
||||||
|
@Operation(summary = "导出在线监测数据(CSV)")
|
||||||
|
@PostMapping("/export/csv")
|
||||||
|
public ResponseEntity<byte[]> exportCsv(@RequestBody MonitorExportRequest request) {
|
||||||
|
request.setFormat("csv");
|
||||||
|
byte[] data = monitorExportService.exportCsv(request);
|
||||||
|
String filename = URLEncoder.encode("在线监测数据.csv", StandardCharsets.UTF_8);
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
|
||||||
|
.contentType(MediaType.parseMediaType("text/csv; charset=UTF-8"))
|
||||||
|
.body(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 10. 通用导出(按 format 参数自动选择) ==========
|
||||||
|
@Operation(summary = "通用导出(format=excel|csv)")
|
||||||
|
@PostMapping("/export")
|
||||||
|
public ResponseEntity<byte[]> export(@RequestBody MonitorExportRequest request) {
|
||||||
|
if ("csv".equalsIgnoreCase(request.getFormat())) {
|
||||||
|
return exportCsv(request);
|
||||||
|
}
|
||||||
|
return exportExcel(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 11. 批量获取实时数据 ==========
|
||||||
|
@Operation(summary = "批量获取多设备实时数据")
|
||||||
|
@PostMapping("/realtime/batch")
|
||||||
|
public R<List<Map<String, Object>>> batchRealtime(@RequestBody List<Long> deviceIds) {
|
||||||
|
return R.ok(monitorListService.getBatchRealtimeData(deviceIds));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.water.production.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.common.core.result.R;
|
||||||
|
import com.water.production.entity.IntrusionEvent;
|
||||||
|
import com.water.production.entity.VideoCamera;
|
||||||
|
import com.water.production.entity.VideoRecording;
|
||||||
|
import com.water.production.service.IntrusionDetectionService;
|
||||||
|
import com.water.production.service.VideoMonitorService;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Tag(name = "视频监控与AI检测")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/production/video")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VideoController {
|
||||||
|
|
||||||
|
private final VideoMonitorService videoMonitorService;
|
||||||
|
private final IntrusionDetectionService intrusionService;
|
||||||
|
|
||||||
|
// === 摄像头管理 ===
|
||||||
|
@GetMapping("/cameras")
|
||||||
|
public R<Page<VideoCamera>> listCameras(@RequestParam(defaultValue = "1") int current,
|
||||||
|
@RequestParam(defaultValue = "20") int size,
|
||||||
|
@RequestParam(required = false) String area,
|
||||||
|
@RequestParam(required = false) Integer status,
|
||||||
|
@RequestParam(required = false) String keyword) {
|
||||||
|
return R.ok(videoMonitorService.pageCameras(current, size, area, status, keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/cameras/all")
|
||||||
|
public R<List<VideoCamera>> listAllCameras() {
|
||||||
|
return R.ok(videoMonitorService.listAllCameras());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/cameras/{id}")
|
||||||
|
public R<VideoCamera> getCamera(@PathVariable Long id) {
|
||||||
|
return R.ok(videoMonitorService.getCameraById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cameras")
|
||||||
|
public R<VideoCamera> createCamera(@RequestBody VideoCamera camera) {
|
||||||
|
return R.ok(videoMonitorService.createCamera(camera));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/cameras/{id}")
|
||||||
|
public R<String> updateCamera(@PathVariable Long id, @RequestBody VideoCamera camera) {
|
||||||
|
camera.setId(id);
|
||||||
|
videoMonitorService.updateCamera(camera);
|
||||||
|
return R.ok("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/cameras/{id}")
|
||||||
|
public R<String> deleteCamera(@PathVariable Long id) {
|
||||||
|
videoMonitorService.deleteCamera(id);
|
||||||
|
return R.ok("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/cameras/{id}/status")
|
||||||
|
public R<String> updateCameraStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||||
|
videoMonitorService.updateCameraStatus(id, status);
|
||||||
|
return R.ok("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 视频流 ===
|
||||||
|
@GetMapping("/cameras/{id}/stream")
|
||||||
|
public R<Map<String, Object>> getStreamUrl(@PathVariable Long id) {
|
||||||
|
return R.ok(videoMonitorService.getStreamUrls(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/cameras/{id}/stream")
|
||||||
|
public R<String> updateStreamUrls(@PathVariable Long id,
|
||||||
|
@RequestParam(required = false) String rtsp,
|
||||||
|
@RequestParam(required = false) String hls,
|
||||||
|
@RequestParam(required = false) String flv) {
|
||||||
|
videoMonitorService.updateStreamUrls(id, rtsp, hls, flv);
|
||||||
|
return R.ok("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cameras/refresh-status")
|
||||||
|
public R<Map<String, Object>> refreshAllStatus() {
|
||||||
|
return R.ok(videoMonitorService.refreshAllStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 录像回放 ===
|
||||||
|
@GetMapping("/recordings")
|
||||||
|
public R<Page<VideoRecording>> getRecordings(@RequestParam(defaultValue = "1") int current,
|
||||||
|
@RequestParam(defaultValue = "20") int size,
|
||||||
|
@RequestParam(required = false) Long cameraId,
|
||||||
|
@RequestParam(required = false) String date) {
|
||||||
|
return R.ok(videoMonitorService.pageRecordings(current, size, cameraId, null, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/recordings/{recordingId}/playback")
|
||||||
|
public R<Map<String, Object>> getPlaybackUrl(@PathVariable Long recordingId) {
|
||||||
|
return R.ok(videoMonitorService.getPlaybackUrl(recordingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/recordings")
|
||||||
|
public R<VideoRecording> createRecording(@RequestBody VideoRecording recording) {
|
||||||
|
return R.ok(videoMonitorService.createRecording(recording));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/recordings/{id}")
|
||||||
|
public R<String> deleteRecording(@PathVariable Long id) {
|
||||||
|
videoMonitorService.deleteRecording(id);
|
||||||
|
return R.ok("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 监控统计 ===
|
||||||
|
@GetMapping("/statistics/online")
|
||||||
|
public R<Map<String, Object>> getOnlineStats() {
|
||||||
|
return R.ok(videoMonitorService.getDeviceOnlineStats());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/statistics/area")
|
||||||
|
public R<List<Map<String, Object>>> getAreaStats() {
|
||||||
|
return R.ok(videoMonitorService.getCameraStatsByArea());
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AI 闯入检测 ===
|
||||||
|
@PostMapping("/intrusion/detect")
|
||||||
|
public R<IntrusionEvent> detectIntrusion(@RequestParam String cameraId,
|
||||||
|
@RequestParam(required = false) String area) {
|
||||||
|
return R.ok(intrusionService.detect(cameraId, area));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/intrusion/{id}/handle")
|
||||||
|
public R<IntrusionEvent> handleEvent(@PathVariable Long id,
|
||||||
|
@RequestParam String action,
|
||||||
|
@RequestParam String operator) {
|
||||||
|
return R.ok(intrusionService.handleEvent(id, action, operator));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/intrusion/list")
|
||||||
|
public R<Page<IntrusionEvent>> listEvents(@RequestParam(required = false) String status,
|
||||||
|
@RequestParam(required = false) String area,
|
||||||
|
@RequestParam(required = false) String alertLevel,
|
||||||
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
|
@RequestParam(defaultValue = "20") int pageSize) {
|
||||||
|
return R.ok(intrusionService.listEvents(status, area, alertLevel, pageNum, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/intrusion/statistics")
|
||||||
|
public R<Map<String, Object>> getIntrusionStatistics() {
|
||||||
|
return R.ok(intrusionService.getStatistics());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.water.production.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线监测数据导出请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MonitorExportRequest {
|
||||||
|
|
||||||
|
/** 导出格式: excel/csv */
|
||||||
|
private String format = "excel";
|
||||||
|
|
||||||
|
/** 区域 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
/** 设备类型 */
|
||||||
|
private String deviceType;
|
||||||
|
|
||||||
|
/** 设备状态 */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 关键词 */
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
/** 开始时间 */
|
||||||
|
private String startTime;
|
||||||
|
|
||||||
|
/** 结束时间 */
|
||||||
|
private String endTime;
|
||||||
|
|
||||||
|
/** 指定导出的设备ID列表(可选,为空则按筛选条件导出) */
|
||||||
|
private List<Long> deviceIds;
|
||||||
|
|
||||||
|
/** 是否包含实时数据 */
|
||||||
|
private Boolean includeRealtime = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.water.production.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线监测列表查询请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MonitorQueryRequest {
|
||||||
|
|
||||||
|
/** 区域 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
/** 设备类型: flow/pressure/level/quality */
|
||||||
|
private String deviceType;
|
||||||
|
|
||||||
|
/** 设备状态: online/offline/fault/abnormal */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 关键词(设备编号/名称模糊搜索) */
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
/** 开始时间(最后上报时间范围) */
|
||||||
|
private String startTime;
|
||||||
|
|
||||||
|
/** 结束时间 */
|
||||||
|
private String endTime;
|
||||||
|
|
||||||
|
/** 排序字段: deviceCode/deviceName/status/lastReportTime */
|
||||||
|
private String sortField;
|
||||||
|
|
||||||
|
/** 排序方向: asc/desc */
|
||||||
|
private String sortOrder;
|
||||||
|
|
||||||
|
/** 页码(默认1) */
|
||||||
|
private Integer pageNum = 1;
|
||||||
|
|
||||||
|
/** 每页条数(默认20) */
|
||||||
|
private Integer pageSize = 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.water.production.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总览大屏汇总实体
|
||||||
|
* 存储每日聚合的进出水量、水质、设备、报警、能耗快照数据
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("prod_dashboard_summary")
|
||||||
|
public class DashboardSummary {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 统计日期 */
|
||||||
|
private LocalDate summaryDate;
|
||||||
|
|
||||||
|
/** 所属区域 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
// ---- 进出水量 ----
|
||||||
|
/** 今日进水量(m³) */
|
||||||
|
private BigDecimal todayInflow;
|
||||||
|
|
||||||
|
/** 今日出水量(m³) */
|
||||||
|
private BigDecimal todayOutflow;
|
||||||
|
|
||||||
|
/** 昨日进水量(m³) */
|
||||||
|
private BigDecimal yesterdayInflow;
|
||||||
|
|
||||||
|
/** 昨日出水量(m³) */
|
||||||
|
private BigDecimal yesterdayOutflow;
|
||||||
|
|
||||||
|
/** 本月累计进水量(m³) */
|
||||||
|
private BigDecimal monthInflow;
|
||||||
|
|
||||||
|
/** 本月累计出水量(m³) */
|
||||||
|
private BigDecimal monthOutflow;
|
||||||
|
|
||||||
|
// ---- 水质概览 ----
|
||||||
|
/** 原水合格率(%) */
|
||||||
|
private BigDecimal rawWaterPassRate;
|
||||||
|
|
||||||
|
/** 出厂水合格率(%) */
|
||||||
|
private BigDecimal factoryWaterPassRate;
|
||||||
|
|
||||||
|
/** 末梢水合格率(%) */
|
||||||
|
private BigDecimal terminalWaterPassRate;
|
||||||
|
|
||||||
|
/** 综合合格率(%) */
|
||||||
|
private BigDecimal overallPassRate;
|
||||||
|
|
||||||
|
// ---- 设备概况 ----
|
||||||
|
/** 设备总数 */
|
||||||
|
private Integer deviceTotal;
|
||||||
|
|
||||||
|
/** 在线设备数 */
|
||||||
|
private Integer deviceOnline;
|
||||||
|
|
||||||
|
/** 离线设备数 */
|
||||||
|
private Integer deviceOffline;
|
||||||
|
|
||||||
|
/** 故障设备数 */
|
||||||
|
private Integer deviceFault;
|
||||||
|
|
||||||
|
/** 设备在线率(%) */
|
||||||
|
private BigDecimal deviceOnlineRate;
|
||||||
|
|
||||||
|
// ---- 报警概览 ----
|
||||||
|
/** 今日报警总数 */
|
||||||
|
private Integer todayAlertTotal;
|
||||||
|
|
||||||
|
/** 活跃报警数 */
|
||||||
|
private Integer activeAlertCount;
|
||||||
|
|
||||||
|
/** 一般报警数 */
|
||||||
|
private Integer generalAlertCount;
|
||||||
|
|
||||||
|
/** 重要报警数 */
|
||||||
|
private Integer importantAlertCount;
|
||||||
|
|
||||||
|
/** 紧急报警数 */
|
||||||
|
private Integer urgentAlertCount;
|
||||||
|
|
||||||
|
// ---- 能耗数据 ----
|
||||||
|
/** 今日电耗(kWh) */
|
||||||
|
private BigDecimal todayPowerKwh;
|
||||||
|
|
||||||
|
/** 今日药耗(kg) */
|
||||||
|
private BigDecimal todayChemicalKg;
|
||||||
|
|
||||||
|
/** 单位产水能耗(kWh/m³) */
|
||||||
|
private BigDecimal unitEnergyConsumption;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.water.production.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 能耗数据实体
|
||||||
|
* 记录每日电耗、药耗及产水量,用于能耗分析和成本核算
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("prod_energy_consumption")
|
||||||
|
public class EnergyConsumption {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 统计日期 */
|
||||||
|
private LocalDate recordDate;
|
||||||
|
|
||||||
|
/** 所属区域/水厂 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
/** 能耗类型: power(电耗)/coagulant(混凝剂)/disinfectant(消毒剂) */
|
||||||
|
private String energyType;
|
||||||
|
|
||||||
|
/** 能耗数值 */
|
||||||
|
private BigDecimal consumption;
|
||||||
|
|
||||||
|
/** 计量单位: kWh/kg */
|
||||||
|
private String unit;
|
||||||
|
|
||||||
|
/** 当日产水量(m³),用于计算单位能耗 */
|
||||||
|
private BigDecimal productionVolume;
|
||||||
|
|
||||||
|
/** 单位产水能耗(kWh/m³ 或 kg/m³) */
|
||||||
|
private BigDecimal unitConsumption;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.water.production.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("prod_intrusion_event")
|
||||||
|
public class IntrusionEvent {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String eventNo;
|
||||||
|
|
||||||
|
/** 关联摄像头ID */
|
||||||
|
private String cameraId;
|
||||||
|
|
||||||
|
/** 所属区域 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
/** 检测时间 */
|
||||||
|
private LocalDateTime detectedTime;
|
||||||
|
|
||||||
|
/** AI识别置信度(0~1) */
|
||||||
|
private Double confidence;
|
||||||
|
|
||||||
|
/** 是否检测到闯入 */
|
||||||
|
private Boolean detected;
|
||||||
|
|
||||||
|
/** 事件状态: ACTIVE/CONFIRMED/DISMISSED/RESOLVED/FALSE_POSITIVE */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 报警等级: 一般/重要/紧急 */
|
||||||
|
private String alertLevel;
|
||||||
|
|
||||||
|
/** 是否触发报警 */
|
||||||
|
private Boolean alertTriggered;
|
||||||
|
|
||||||
|
/** 抓拍图片URL */
|
||||||
|
private String snapshotUrl;
|
||||||
|
|
||||||
|
/** 描述 */
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/** 确认人 */
|
||||||
|
private String confirmedBy;
|
||||||
|
|
||||||
|
/** 确认时间 */
|
||||||
|
private LocalDateTime confirmedTime;
|
||||||
|
|
||||||
|
/** 解决时间 */
|
||||||
|
private LocalDateTime resolvedTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.water.production.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线监测设备实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("prod_monitor_device")
|
||||||
|
public class MonitorDevice {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 设备编号 */
|
||||||
|
private String deviceCode;
|
||||||
|
|
||||||
|
/** 设备名称 */
|
||||||
|
private String deviceName;
|
||||||
|
|
||||||
|
/** 设备类型: flow/pressure/level/quality */
|
||||||
|
private String deviceType;
|
||||||
|
|
||||||
|
/** 所属区域 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
/** 安装位置 */
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
/** 经度 */
|
||||||
|
private BigDecimal lng;
|
||||||
|
|
||||||
|
/** 纬度 */
|
||||||
|
private BigDecimal lat;
|
||||||
|
|
||||||
|
/** 设备状态: online/offline/fault/abnormal */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 最后上报时间 */
|
||||||
|
private LocalDateTime lastReportTime;
|
||||||
|
|
||||||
|
/** 设备品牌/型号 */
|
||||||
|
private String brand;
|
||||||
|
|
||||||
|
/** 安装日期 */
|
||||||
|
private LocalDateTime installTime;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.water.production.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监测实时数据实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("prod_monitor_realtime_data")
|
||||||
|
public class MonitorRealtimeData {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 关联设备ID */
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
/** 设备编号 */
|
||||||
|
private String deviceCode;
|
||||||
|
|
||||||
|
/** 监测参数类型: flow/pressure/level/turbidity/ph/residual_chlorine/temperature */
|
||||||
|
private String metricKey;
|
||||||
|
|
||||||
|
/** 实时值 */
|
||||||
|
private BigDecimal metricValue;
|
||||||
|
|
||||||
|
/** 单位 */
|
||||||
|
private String unit;
|
||||||
|
|
||||||
|
/** 阈值上限 */
|
||||||
|
private BigDecimal thresholdHigh;
|
||||||
|
|
||||||
|
/** 阈值下限 */
|
||||||
|
private BigDecimal thresholdLow;
|
||||||
|
|
||||||
|
/** 是否异常: 0正常 1异常 */
|
||||||
|
private Integer isAbnormal;
|
||||||
|
|
||||||
|
/** 采集时间 */
|
||||||
|
private LocalDateTime collectTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
}
|
||||||
@@ -1,10 +1,75 @@
|
|||||||
package com.water.production.entity;
|
package com.water.production.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@Data @TableName("prod_video_camera")
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频监控摄像头实体(增强版)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("prod_video_camera")
|
||||||
public class VideoCamera {
|
public class VideoCamera {
|
||||||
@TableId(type = IdType.AUTO) private Long id;
|
|
||||||
private String cameraId, name, area;
|
@TableId(type = IdType.AUTO)
|
||||||
private String streamUrl; private Integer status; // 0离线 1在线
|
private Long id;
|
||||||
private Double lng, lat;
|
|
||||||
|
/** 摄像头唯一编号 */
|
||||||
|
private String cameraId;
|
||||||
|
|
||||||
|
/** 摄像头名称 */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** 所属区域 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
/** RTSP 视频流地址 */
|
||||||
|
private String streamUrlRtsp;
|
||||||
|
|
||||||
|
/** HLS 视频流地址 */
|
||||||
|
private String streamUrlHls;
|
||||||
|
|
||||||
|
/** FLV 视频流地址 */
|
||||||
|
private String streamUrlFlv;
|
||||||
|
|
||||||
|
/** 状态: 0=离线, 1=在线, 2=故障 */
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/** 设备厂商 */
|
||||||
|
private String manufacturer;
|
||||||
|
|
||||||
|
/** 设备型号 */
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
/** 经度 */
|
||||||
|
private Double lng;
|
||||||
|
|
||||||
|
/** 纬度 */
|
||||||
|
private Double lat;
|
||||||
|
|
||||||
|
/** 安装位置描述 */
|
||||||
|
private String installLocation;
|
||||||
|
|
||||||
|
/** 安装日期 */
|
||||||
|
private LocalDate installDate;
|
||||||
|
|
||||||
|
/** 最后在线时间 */
|
||||||
|
private LocalDateTime lastOnlineTime;
|
||||||
|
|
||||||
|
/** 是否启用AI检测: 0=未启用, 1=已启用 */
|
||||||
|
private Integer aiEnabled;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.water.production.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频录像记录实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("prod_video_recording")
|
||||||
|
public class VideoRecording {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 关联摄像头ID */
|
||||||
|
private Long cameraId;
|
||||||
|
|
||||||
|
/** 摄像头名称 */
|
||||||
|
private String cameraName;
|
||||||
|
|
||||||
|
/** 所属区域 */
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
/** 录像开始时间 */
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
|
||||||
|
/** 录像结束时间 */
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
|
||||||
|
/** 时长(秒) */
|
||||||
|
private Integer durationSec;
|
||||||
|
|
||||||
|
/** 文件大小(MB) */
|
||||||
|
private BigDecimal fileSizeMb;
|
||||||
|
|
||||||
|
/** 存储路径 */
|
||||||
|
private String storagePath;
|
||||||
|
|
||||||
|
/** 回放地址 */
|
||||||
|
private String playbackUrl;
|
||||||
|
|
||||||
|
/** 录像类型: scheduled=计划录像, event_triggered=事件触发, manual=手动录像 */
|
||||||
|
private String recordType;
|
||||||
|
|
||||||
|
/** 关联闯入事件ID(事件触发时有值) */
|
||||||
|
private Long eventId;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.water.production.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.production.entity.DashboardSummary;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface DashboardSummaryMapper extends BaseMapper<DashboardSummary> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询最近N天的总览快照(按日期倒序)
|
||||||
|
*/
|
||||||
|
@Select("SELECT * FROM prod_dashboard_summary WHERE area = #{area} " +
|
||||||
|
"ORDER BY summary_date DESC LIMIT #{days}")
|
||||||
|
List<DashboardSummary> findRecent(@Param("area") String area, @Param("days") int days);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按月份汇总进出水量趋势
|
||||||
|
*/
|
||||||
|
@Select("SELECT DATE_TRUNC('month', summary_date) as month, " +
|
||||||
|
"SUM(today_inflow) as total_inflow, SUM(today_outflow) as total_outflow " +
|
||||||
|
"FROM prod_dashboard_summary WHERE area = #{area} AND summary_date >= #{startDate} " +
|
||||||
|
"GROUP BY DATE_TRUNC('month', summary_date) ORDER BY month")
|
||||||
|
List<Map<String, Object>> monthlyFlowTrend(@Param("area") String area, @Param("startDate") String startDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期查询进出水量趋势(最近N天)
|
||||||
|
*/
|
||||||
|
@Select("SELECT summary_date, today_inflow, today_outflow " +
|
||||||
|
"FROM prod_dashboard_summary WHERE area = #{area} " +
|
||||||
|
"ORDER BY summary_date DESC LIMIT #{days}")
|
||||||
|
List<Map<String, Object>> dailyFlowTrend(@Param("area") String area, @Param("days") int days);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.water.production.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.production.entity.EnergyConsumption;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface EnergyConsumptionMapper extends BaseMapper<EnergyConsumption> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按区域和日期范围查询能耗数据
|
||||||
|
*/
|
||||||
|
@Select("SELECT * FROM prod_energy_consumption WHERE area = #{area} " +
|
||||||
|
"AND record_date BETWEEN #{startDate} AND #{endDate} ORDER BY record_date")
|
||||||
|
List<EnergyConsumption> findByDateRange(@Param("area") String area,
|
||||||
|
@Param("startDate") String startDate,
|
||||||
|
@Param("endDate") String endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按月份汇总能耗趋势
|
||||||
|
*/
|
||||||
|
@Select("SELECT DATE_TRUNC('month', record_date) as month, energy_type, " +
|
||||||
|
"SUM(consumption) as total_consumption, SUM(production_volume) as total_production " +
|
||||||
|
"FROM prod_energy_consumption WHERE area = #{area} AND record_date >= #{startDate} " +
|
||||||
|
"GROUP BY DATE_TRUNC('month', record_date), energy_type ORDER BY month")
|
||||||
|
List<Map<String, Object>> monthlyEnergyTrend(@Param("area") String area,
|
||||||
|
@Param("startDate") String startDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计指定日期的能耗汇总
|
||||||
|
*/
|
||||||
|
@Select("SELECT energy_type, SUM(consumption) as total, unit " +
|
||||||
|
"FROM prod_energy_consumption WHERE area = #{area} AND record_date = #{date} " +
|
||||||
|
"GROUP BY energy_type, unit")
|
||||||
|
List<Map<String, Object>> dailySummary(@Param("area") String area, @Param("date") String date);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单位产水能耗(指定日期范围)
|
||||||
|
*/
|
||||||
|
@Select("SELECT SUM(CASE WHEN energy_type='power' THEN consumption ELSE 0 END) as total_power, " +
|
||||||
|
"SUM(production_volume) as total_production " +
|
||||||
|
"FROM prod_energy_consumption WHERE area = #{area} " +
|
||||||
|
"AND record_date BETWEEN #{startDate} AND #{endDate}")
|
||||||
|
Map<String, Object> unitEnergyStats(@Param("area") String area,
|
||||||
|
@Param("startDate") String startDate,
|
||||||
|
@Param("endDate") String endDate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.water.production.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.production.entity.IntrusionEvent;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface IntrusionEventMapper extends BaseMapper<IntrusionEvent> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.water.production.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.production.entity.MonitorDevice;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线监测设备 Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface MonitorDeviceMapper extends BaseMapper<MonitorDevice> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询设备列表(含最新实时数据)
|
||||||
|
*/
|
||||||
|
@Select("<script>" +
|
||||||
|
"SELECT d.*, " +
|
||||||
|
" (SELECT COUNT(*) FROM prod_monitor_realtime_data r WHERE r.device_id = d.id) AS metric_count " +
|
||||||
|
"FROM prod_monitor_device d " +
|
||||||
|
"WHERE 1=1 " +
|
||||||
|
"<if test='area != null and area != \"\"'> AND d.area = #{area}</if> " +
|
||||||
|
"<if test='deviceType != null and deviceType != \"\"'> AND d.device_type = #{deviceType}</if> " +
|
||||||
|
"<if test='status != null and status != \"\"'> AND d.status = #{status}</if> " +
|
||||||
|
"<if test='keyword != null and keyword != \"\"'> AND (d.device_code ILIKE CONCAT('%',#{keyword},'%') OR d.device_name ILIKE CONCAT('%',#{keyword},'%'))</if> " +
|
||||||
|
"<if test='startTime != null and startTime != \"\"'> AND d.last_report_time >= #{startTime}::timestamp</if> " +
|
||||||
|
"<if test='endTime != null and endTime != \"\"'> AND d.last_report_time <= #{endTime}::timestamp</if> " +
|
||||||
|
"<if test='sortField == \"deviceCode\"'> ORDER BY d.device_code ${sortOrder}</if> " +
|
||||||
|
"<if test='sortField == \"deviceName\"'> ORDER BY d.device_name ${sortOrder}</if> " +
|
||||||
|
"<if test='sortField == \"status\"'> ORDER BY d.status ${sortOrder}</if> " +
|
||||||
|
"<if test='sortField == \"lastReportTime\"'> ORDER BY d.last_report_time ${sortOrder}</if> " +
|
||||||
|
"<if test='sortField == null or sortField == \"\"'> ORDER BY d.last_report_time DESC</if> " +
|
||||||
|
"</script>")
|
||||||
|
List<Map<String, Object>> selectDeviceListWithMetrics(
|
||||||
|
@Param("area") String area,
|
||||||
|
@Param("deviceType") String deviceType,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("startTime") String startTime,
|
||||||
|
@Param("endTime") String endTime,
|
||||||
|
@Param("sortField") String sortField,
|
||||||
|
@Param("sortOrder") String sortOrder);
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package com.water.production.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.production.entity.MonitorRealtimeData;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监测实时数据 Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface MonitorRealtimeDataMapper extends BaseMapper<MonitorRealtimeData> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定设备的最新实时数据(所有指标)
|
||||||
|
*/
|
||||||
|
@Select("SELECT r.* FROM prod_monitor_realtime_data r " +
|
||||||
|
"INNER JOIN (SELECT device_id, metric_key, MAX(collect_time) AS max_time " +
|
||||||
|
" FROM prod_monitor_realtime_data WHERE device_id = #{deviceId} GROUP BY device_id, metric_key) latest " +
|
||||||
|
"ON r.device_id = latest.device_id AND r.metric_key = latest.metric_key AND r.collect_time = latest.max_time")
|
||||||
|
List<MonitorRealtimeData> selectLatestByDeviceId(@Param("deviceId") Long deviceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取多个设备的最新实时数据
|
||||||
|
*/
|
||||||
|
@Select("<script>" +
|
||||||
|
"SELECT r.* FROM prod_monitor_realtime_data r " +
|
||||||
|
"INNER JOIN (SELECT device_id, metric_key, MAX(collect_time) AS max_time " +
|
||||||
|
" FROM prod_monitor_realtime_data " +
|
||||||
|
" WHERE device_id IN " +
|
||||||
|
" <foreach item='id' collection='deviceIds' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||||
|
" GROUP BY device_id, metric_key) latest " +
|
||||||
|
"ON r.device_id = latest.device_id AND r.metric_key = latest.metric_key AND r.collect_time = latest.max_time " +
|
||||||
|
"ORDER BY r.device_id, r.metric_key" +
|
||||||
|
"</script>")
|
||||||
|
List<Map<String, Object>> selectLatestBatch(@Param("deviceIds") List<Long> deviceIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按设备ID统计异常数据数量
|
||||||
|
*/
|
||||||
|
@Select("SELECT device_id, COUNT(*) AS cnt FROM prod_monitor_realtime_data " +
|
||||||
|
"WHERE is_abnormal = 1 AND device_id IN " +
|
||||||
|
"<script><foreach item='id' collection='deviceIds' open='(' separator=',' close=')'>#{id}</foreach></script> " +
|
||||||
|
"GROUP BY device_id")
|
||||||
|
List<Map<String, Object>> countAbnormalByDevices(@Param("deviceIds") List<Long> deviceIds);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.water.production.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.water.production.entity.VideoRecording;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface VideoRecordingMapper extends BaseMapper<VideoRecording> {
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
package com.water.production.service;
|
package com.water.production.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.water.production.entity.DashboardSummary;
|
||||||
|
import com.water.production.mapper.DashboardSummaryMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -11,6 +17,7 @@ import java.util.*;
|
|||||||
public class DashboardService {
|
public class DashboardService {
|
||||||
|
|
||||||
private final JdbcTemplate jdbc;
|
private final JdbcTemplate jdbc;
|
||||||
|
private final DashboardSummaryMapper dashboardSummaryMapper;
|
||||||
|
|
||||||
/** 获取供水总览数据(按角色自动定位区域) */
|
/** 获取供水总览数据(按角色自动定位区域) */
|
||||||
public Map<String, Object> getOverview(String area, String roleType) {
|
public Map<String, Object> getOverview(String area, String roleType) {
|
||||||
@@ -58,4 +65,266 @@ public class DashboardService {
|
|||||||
sql.append(" ORDER BY last_report_time DESC LIMIT 100");
|
sql.append(" ORDER BY last_report_time DESC LIMIT 100");
|
||||||
return jdbc.queryForList(sql.toString());
|
return jdbc.queryForList(sql.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 总览大屏增强接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进出水量概览(今日/昨日/本月 + 趋势对比)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getFlowSummary(String area) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("area", area);
|
||||||
|
|
||||||
|
// 今日进出水量
|
||||||
|
try {
|
||||||
|
Map<String, Object> todayFlow = jdbc.queryForMap(
|
||||||
|
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
|
||||||
|
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
|
||||||
|
"FROM iot_telemetry WHERE ts >= CURRENT_DATE AND area = ?", area);
|
||||||
|
result.put("todayInflow", todayFlow.get("inflow"));
|
||||||
|
result.put("todayOutflow", todayFlow.get("outflow"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("todayInflow", 0);
|
||||||
|
result.put("todayOutflow", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 昨日进出水量
|
||||||
|
try {
|
||||||
|
Map<String, Object> yesterdayFlow = jdbc.queryForMap(
|
||||||
|
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
|
||||||
|
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
|
||||||
|
"FROM iot_telemetry WHERE ts >= CURRENT_DATE - 1 AND ts < CURRENT_DATE AND area = ?", area);
|
||||||
|
result.put("yesterdayInflow", yesterdayFlow.get("inflow"));
|
||||||
|
result.put("yesterdayOutflow", yesterdayFlow.get("outflow"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("yesterdayInflow", 0);
|
||||||
|
result.put("yesterdayOutflow", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本月累计
|
||||||
|
try {
|
||||||
|
Map<String, Object> monthFlow = jdbc.queryForMap(
|
||||||
|
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
|
||||||
|
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
|
||||||
|
"FROM iot_telemetry WHERE ts >= DATE_TRUNC('month', CURRENT_DATE) AND area = ?", area);
|
||||||
|
result.put("monthInflow", monthFlow.get("inflow"));
|
||||||
|
result.put("monthOutflow", monthFlow.get("outflow"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("monthInflow", 0);
|
||||||
|
result.put("monthOutflow", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日环比
|
||||||
|
computeDayOverDay(result);
|
||||||
|
|
||||||
|
// 最近7天趋势
|
||||||
|
result.put("trend", dashboardSummaryMapper.dailyFlowTrend(area, 7));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 水质概览(原水/出厂水/末梢水关键指标 + 合格率)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getWaterQualitySummary(String area) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("area", area);
|
||||||
|
|
||||||
|
// 各类型水质合格率
|
||||||
|
String[] types = {"raw", "factory", "terminal"};
|
||||||
|
String[] labels = {"原水", "出厂水", "末梢水"};
|
||||||
|
int totalTests = 0;
|
||||||
|
int totalPassed = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < types.length; i++) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> stats = jdbc.queryForMap(
|
||||||
|
"SELECT COUNT(*) as total, " +
|
||||||
|
"COALESCE(SUM(CASE WHEN result='合格' THEN 1 ELSE 0 END),0) as passed " +
|
||||||
|
"FROM prod_water_quality WHERE position_type = ?", types[i]);
|
||||||
|
int total = ((Number) stats.get("total")).intValue();
|
||||||
|
int passed = ((Number) stats.get("passed")).intValue();
|
||||||
|
double rate = total > 0 ? (passed * 100.0 / total) : 100.0;
|
||||||
|
|
||||||
|
Map<String, Object> typeResult = new LinkedHashMap<>();
|
||||||
|
typeResult.put("label", labels[i]);
|
||||||
|
typeResult.put("totalTests", total);
|
||||||
|
typeResult.put("passedTests", passed);
|
||||||
|
typeResult.put("passRate", BigDecimal.valueOf(rate).setScale(1, RoundingMode.HALF_UP));
|
||||||
|
result.put(types[i] + "Water", typeResult);
|
||||||
|
|
||||||
|
totalTests += total;
|
||||||
|
totalPassed += passed;
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put(types[i] + "Water", Map.of("label", labels[i], "totalTests", 0, "passedTests", 0, "passRate", 100.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 综合合格率
|
||||||
|
double overallRate = totalTests > 0 ? (totalPassed * 100.0 / totalTests) : 100.0;
|
||||||
|
result.put("overallPassRate", BigDecimal.valueOf(overallRate).setScale(1, RoundingMode.HALF_UP));
|
||||||
|
|
||||||
|
// 最新水质记录
|
||||||
|
try {
|
||||||
|
result.put("latestRecords", jdbc.queryForList(
|
||||||
|
"SELECT * FROM prod_water_quality ORDER BY created_time DESC LIMIT 5"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("latestRecords", Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备概况(在线/离线/故障数量 + 在线率)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getDeviceSummary(String area) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("area", area);
|
||||||
|
|
||||||
|
int online = 0, offline = 0, fault = 0;
|
||||||
|
try {
|
||||||
|
List<Map<String, Object>> stats = jdbc.queryForList(
|
||||||
|
"SELECT status, COUNT(*) as count FROM prod_device_status WHERE area = ? GROUP BY status", area);
|
||||||
|
for (Map<String, Object> row : stats) {
|
||||||
|
int status = ((Number) row.get("status")).intValue();
|
||||||
|
int count = ((Number) row.get("count")).intValue();
|
||||||
|
switch (status) {
|
||||||
|
case 1 -> online = count;
|
||||||
|
case 0 -> offline = count;
|
||||||
|
case 2 -> fault = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
int total = online + offline + fault;
|
||||||
|
double onlineRate = total > 0 ? (online * 100.0 / total) : 0.0;
|
||||||
|
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("online", online);
|
||||||
|
result.put("offline", offline);
|
||||||
|
result.put("fault", fault);
|
||||||
|
result.put("onlineRate", BigDecimal.valueOf(onlineRate).setScale(1, RoundingMode.HALF_UP));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报警概览(今日报警数/活跃报警/各级别分布)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getAlertSummary(String area) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("area", area);
|
||||||
|
|
||||||
|
// 今日报警数
|
||||||
|
try {
|
||||||
|
Integer todayTotal = jdbc.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM prod_alert_record WHERE created_time >= CURRENT_DATE", Integer.class);
|
||||||
|
result.put("todayTotal", todayTotal != null ? todayTotal : 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("todayTotal", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 活跃报警数
|
||||||
|
try {
|
||||||
|
Integer activeCount = jdbc.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM prod_alert_record WHERE status < 4", Integer.class);
|
||||||
|
result.put("activeCount", activeCount != null ? activeCount : 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("activeCount", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 各级别分布
|
||||||
|
try {
|
||||||
|
List<Map<String, Object>> levelStats = jdbc.queryForList(
|
||||||
|
"SELECT alert_level, COUNT(*) as count FROM prod_alert_record " +
|
||||||
|
"WHERE created_time >= CURRENT_DATE GROUP BY alert_level");
|
||||||
|
Map<String, Integer> levelMap = new LinkedHashMap<>();
|
||||||
|
levelMap.put("general", 0);
|
||||||
|
levelMap.put("important", 0);
|
||||||
|
levelMap.put("urgent", 0);
|
||||||
|
for (Map<String, Object> row : levelStats) {
|
||||||
|
String level = (String) row.get("alert_level");
|
||||||
|
int count = ((Number) row.get("count")).intValue();
|
||||||
|
levelMap.put(level, count);
|
||||||
|
}
|
||||||
|
result.put("levelDistribution", levelMap);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("levelDistribution", Map.of("general", 0, "important", 0, "urgent", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最近7天趋势
|
||||||
|
try {
|
||||||
|
result.put("trend", jdbc.queryForList(
|
||||||
|
"SELECT DATE(created_time) as date, COUNT(*) as count " +
|
||||||
|
"FROM prod_alert_record WHERE created_time >= CURRENT_DATE - 7 " +
|
||||||
|
"GROUP BY DATE(created_time) ORDER BY date"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("trend", Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取总览大屏完整快照(聚合所有维度)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getFullDashboard(String area) {
|
||||||
|
Map<String, Object> dashboard = new LinkedHashMap<>();
|
||||||
|
dashboard.put("flow", getFlowSummary(area));
|
||||||
|
dashboard.put("waterQuality", getWaterQualitySummary(area));
|
||||||
|
dashboard.put("device", getDeviceSummary(area));
|
||||||
|
dashboard.put("alert", getAlertSummary(area));
|
||||||
|
dashboard.put("area", area);
|
||||||
|
dashboard.put("timestamp", System.currentTimeMillis());
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询历史总览快照
|
||||||
|
*/
|
||||||
|
public List<DashboardSummary> getHistorySnapshots(String area, int days) {
|
||||||
|
return dashboardSummaryMapper.findRecent(area, days);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 月度进出水量趋势
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getMonthlyFlowTrend(String area, int months) {
|
||||||
|
String startDate = LocalDate.now().minusMonths(months).toString();
|
||||||
|
return dashboardSummaryMapper.monthlyFlowTrend(area, startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算日环比
|
||||||
|
*/
|
||||||
|
private void computeDayOverDay(Map<String, Object> result) {
|
||||||
|
try {
|
||||||
|
Object todayIn = result.get("todayInflow");
|
||||||
|
Object yesterdayIn = result.get("yesterdayInflow");
|
||||||
|
if (todayIn != null && yesterdayIn != null) {
|
||||||
|
double ti = ((Number) todayIn).doubleValue();
|
||||||
|
double yi = ((Number) yesterdayIn).doubleValue();
|
||||||
|
if (yi > 0) {
|
||||||
|
double rate = ((ti - yi) / yi) * 100;
|
||||||
|
result.put("inflowDayOverDay", BigDecimal.valueOf(rate).setScale(1, RoundingMode.HALF_UP));
|
||||||
|
} else {
|
||||||
|
result.put("inflowDayOverDay", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object todayOut = result.get("todayOutflow");
|
||||||
|
Object yesterdayOut = result.get("yesterdayOutflow");
|
||||||
|
if (todayOut != null && yesterdayOut != null) {
|
||||||
|
double to = ((Number) todayOut).doubleValue();
|
||||||
|
double yo = ((Number) yesterdayOut).doubleValue();
|
||||||
|
if (yo > 0) {
|
||||||
|
double rate = ((to - yo) / yo) * 100;
|
||||||
|
result.put("outflowDayOverDay", BigDecimal.valueOf(rate).setScale(1, RoundingMode.HALF_UP));
|
||||||
|
} else {
|
||||||
|
result.put("outflowDayOverDay", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package com.water.production.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.water.production.entity.EnergyConsumption;
|
||||||
|
import com.water.production.mapper.EnergyConsumptionMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 能耗数据服务
|
||||||
|
* 提供电耗、药耗统计及单位产水能耗分析
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EnergyService {
|
||||||
|
|
||||||
|
private final EnergyConsumptionMapper energyMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今日能耗汇总
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getTodaySummary(String area) {
|
||||||
|
String today = LocalDate.now().toString();
|
||||||
|
List<Map<String, Object>> items = energyMapper.dailySummary(area, today);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("area", area);
|
||||||
|
result.put("date", today);
|
||||||
|
|
||||||
|
BigDecimal totalPower = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalChemical = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (Map<String, Object> item : items) {
|
||||||
|
String type = (String) item.get("energy_type");
|
||||||
|
BigDecimal total = item.get("total") != null ? new BigDecimal(item.get("total").toString()) : BigDecimal.ZERO;
|
||||||
|
if ("power".equals(type)) {
|
||||||
|
totalPower = total;
|
||||||
|
result.put("powerKwh", total);
|
||||||
|
result.put("powerUnit", item.get("unit"));
|
||||||
|
} else {
|
||||||
|
totalChemical = totalChemical.add(total);
|
||||||
|
result.put("chemical_" + type + "_kg", total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("totalPowerKwh", totalPower);
|
||||||
|
result.put("totalChemicalKg", totalChemical);
|
||||||
|
result.put("items", items);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能耗趋势(按日)
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getEnergyTrend(String area, int days) {
|
||||||
|
LambdaQueryWrapper<EnergyConsumption> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(EnergyConsumption::getArea, area)
|
||||||
|
.ge(EnergyConsumption::getRecordDate, LocalDate.now().minusDays(days))
|
||||||
|
.orderByAsc(EnergyConsumption::getRecordDate);
|
||||||
|
List<EnergyConsumption> list = energyMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
List<Map<String, Object>> trend = new ArrayList<>();
|
||||||
|
for (EnergyConsumption ec : list) {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("date", ec.getRecordDate());
|
||||||
|
item.put("energyType", ec.getEnergyType());
|
||||||
|
item.put("consumption", ec.getConsumption());
|
||||||
|
item.put("unit", ec.getUnit());
|
||||||
|
item.put("productionVolume", ec.getProductionVolume());
|
||||||
|
item.put("unitConsumption", ec.getUnitConsumption());
|
||||||
|
trend.add(item);
|
||||||
|
}
|
||||||
|
return trend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取月度能耗趋势
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getMonthlyTrend(String area, int months) {
|
||||||
|
String startDate = LocalDate.now().minusMonths(months).toString();
|
||||||
|
return energyMapper.monthlyEnergyTrend(area, startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单位产水能耗
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getUnitEnergyConsumption(String area, String startDate, String endDate) {
|
||||||
|
Map<String, Object> stats = energyMapper.unitEnergyStats(area, startDate, endDate);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("area", area);
|
||||||
|
result.put("startDate", startDate);
|
||||||
|
result.put("endDate", endDate);
|
||||||
|
|
||||||
|
if (stats != null) {
|
||||||
|
BigDecimal totalPower = stats.get("total_power") != null
|
||||||
|
? new BigDecimal(stats.get("total_power").toString()) : BigDecimal.ZERO;
|
||||||
|
BigDecimal totalProduction = stats.get("total_production") != null
|
||||||
|
? new BigDecimal(stats.get("total_production").toString()) : BigDecimal.ZERO;
|
||||||
|
|
||||||
|
result.put("totalPowerKwh", totalPower);
|
||||||
|
result.put("totalProductionM3", totalProduction);
|
||||||
|
|
||||||
|
if (totalProduction.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
result.put("unitEnergyKwhPerM3",
|
||||||
|
totalPower.divide(totalProduction, 4, RoundingMode.HALF_UP));
|
||||||
|
} else {
|
||||||
|
result.put("unitEnergyKwhPerM3", BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.put("totalPowerKwh", BigDecimal.ZERO);
|
||||||
|
result.put("totalProductionM3", BigDecimal.ZERO);
|
||||||
|
result.put("unitEnergyKwhPerM3", BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型查询能耗记录
|
||||||
|
*/
|
||||||
|
public List<EnergyConsumption> getRecordsByType(String area, String energyType, String startDate, String endDate) {
|
||||||
|
LambdaQueryWrapper<EnergyConsumption> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(EnergyConsumption::getArea, area)
|
||||||
|
.eq(EnergyConsumption::getEnergyType, energyType)
|
||||||
|
.ge(EnergyConsumption::getRecordDate, startDate)
|
||||||
|
.le(EnergyConsumption::getRecordDate, endDate)
|
||||||
|
.orderByAsc(EnergyConsumption::getRecordDate);
|
||||||
|
return energyMapper.selectList(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package com.water.production.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.production.entity.IntrusionEvent;
|
||||||
|
import com.water.production.mapper.IntrusionEventMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class IntrusionDetectionService {
|
||||||
|
|
||||||
|
private final IntrusionEventMapper eventMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟AI检测闯入事件
|
||||||
|
*/
|
||||||
|
public IntrusionEvent detect(String cameraId, String area) {
|
||||||
|
double confidence = 0.7 + Math.random() * 0.3;
|
||||||
|
boolean detected = confidence > 0.85;
|
||||||
|
|
||||||
|
IntrusionEvent event = new IntrusionEvent();
|
||||||
|
event.setEventNo("INT-" + System.currentTimeMillis());
|
||||||
|
event.setCameraId(cameraId);
|
||||||
|
event.setArea(area);
|
||||||
|
event.setDetectedTime(LocalDateTime.now());
|
||||||
|
event.setConfidence(confidence);
|
||||||
|
event.setDetected(detected);
|
||||||
|
event.setStatus(detected ? "ACTIVE" : "FALSE_POSITIVE");
|
||||||
|
event.setAlertLevel(detected ? (confidence > 0.95 ? "紧急" : "重要") : "一般");
|
||||||
|
event.setSnapshotUrl("/snapshots/" + event.getEventNo() + ".jpg");
|
||||||
|
|
||||||
|
if (detected) {
|
||||||
|
event.setAlertTriggered(true);
|
||||||
|
event.setDescription(String.format("AI检测到人员闯入 (置信度: %.1f%%)", confidence * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
eventMapper.insert(event);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认/处理闯入事件
|
||||||
|
*/
|
||||||
|
public IntrusionEvent handleEvent(Long eventId, String action, String operator) {
|
||||||
|
IntrusionEvent event = eventMapper.selectById(eventId);
|
||||||
|
if (event == null) throw new RuntimeException("事件不存在");
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "confirm" -> { event.setStatus("CONFIRMED"); event.setConfirmedBy(operator); event.setConfirmedTime(LocalDateTime.now()); }
|
||||||
|
case "dismiss" -> { event.setStatus("DISMISSED"); event.setConfirmedBy(operator); event.setConfirmedTime(LocalDateTime.now()); }
|
||||||
|
case "resolve" -> { event.setStatus("RESOLVED"); event.setResolvedTime(LocalDateTime.now()); }
|
||||||
|
default -> throw new RuntimeException("未知操作: " + action);
|
||||||
|
}
|
||||||
|
eventMapper.updateById(event);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询闯入事件
|
||||||
|
*/
|
||||||
|
public Page<IntrusionEvent> listEvents(String status, String area, String alertLevel,
|
||||||
|
int pageNum, int pageSize) {
|
||||||
|
Page<IntrusionEvent> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<IntrusionEvent> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (status != null && !status.isBlank()) wrapper.eq(IntrusionEvent::getStatus, status);
|
||||||
|
if (area != null && !area.isBlank()) wrapper.like(IntrusionEvent::getArea, area);
|
||||||
|
if (alertLevel != null && !alertLevel.isBlank()) wrapper.eq(IntrusionEvent::getAlertLevel, alertLevel);
|
||||||
|
wrapper.orderByDesc(IntrusionEvent::getDetectedTime);
|
||||||
|
return eventMapper.selectPage(page, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 闯入事件统计
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getStatistics() {
|
||||||
|
long total = eventMapper.selectCount(null);
|
||||||
|
long active = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
|
||||||
|
.eq(IntrusionEvent::getStatus, "ACTIVE"));
|
||||||
|
long confirmed = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
|
||||||
|
.eq(IntrusionEvent::getStatus, "CONFIRMED"));
|
||||||
|
long todayEvents = eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
|
||||||
|
.ge(IntrusionEvent::getDetectedTime, LocalDateTime.now().toLocalDate().atStartOfDay()));
|
||||||
|
|
||||||
|
double avgConfidence = eventMapper.selectList(null).stream()
|
||||||
|
.mapToDouble(e -> e.getConfidence() != null ? e.getConfidence() : 0)
|
||||||
|
.average().orElse(0);
|
||||||
|
|
||||||
|
Map<String, Object> stats = new LinkedHashMap<>();
|
||||||
|
stats.put("total", total);
|
||||||
|
stats.put("active", active);
|
||||||
|
stats.put("confirmed", confirmed);
|
||||||
|
stats.put("todayEvents", todayEvents);
|
||||||
|
stats.put("avgConfidence", avgConfidence);
|
||||||
|
stats.put("falsePositiveRate", total > 0 ?
|
||||||
|
(double) eventMapper.selectCount(new LambdaQueryWrapper<IntrusionEvent>()
|
||||||
|
.eq(IntrusionEvent::getStatus, "FALSE_POSITIVE")) / total : 0);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package com.water.production.service;
|
||||||
|
|
||||||
|
import com.alibaba.excel.EasyExcel;
|
||||||
|
import com.water.production.dto.MonitorExportRequest;
|
||||||
|
import com.water.production.dto.MonitorQueryRequest;
|
||||||
|
import com.water.production.entity.MonitorDevice;
|
||||||
|
import com.water.production.entity.MonitorRealtimeData;
|
||||||
|
import com.water.production.mapper.MonitorRealtimeDataMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线监测数据导出服务
|
||||||
|
* 支持 Excel / CSV 格式导出
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MonitorExportService {
|
||||||
|
|
||||||
|
private final MonitorListService monitorListService;
|
||||||
|
private final MonitorRealtimeDataMapper realtimeDataMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 Excel 格式
|
||||||
|
*/
|
||||||
|
public byte[] exportExcel(MonitorExportRequest req) {
|
||||||
|
List<MonitorDevice> devices = getDevicesForExport(req);
|
||||||
|
if (devices.isEmpty()) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建导出数据
|
||||||
|
List<List<String>> head = buildExcelHead();
|
||||||
|
List<List<Object>> data = buildExcelData(devices, req.getIncludeRealtime());
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||||
|
EasyExcel.write(bos)
|
||||||
|
.sheet("在线监测数据")
|
||||||
|
.head(head.stream()
|
||||||
|
.map(row -> row.stream()
|
||||||
|
.map(c -> (String) c)
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.doWrite(data);
|
||||||
|
return bos.toByteArray();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Excel 导出失败", e);
|
||||||
|
throw new RuntimeException("Excel 导出失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 CSV 格式
|
||||||
|
*/
|
||||||
|
public byte[] exportCsv(MonitorExportRequest req) {
|
||||||
|
List<MonitorDevice> devices = getDevicesForExport(req);
|
||||||
|
if (devices.isEmpty()) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
PrintWriter writer = new PrintWriter(new OutputStreamWriter(bos, StandardCharsets.UTF_8))) {
|
||||||
|
// BOM for Excel UTF-8
|
||||||
|
bos.write(0xEF);
|
||||||
|
bos.write(0xBB);
|
||||||
|
bos.write(0xBF);
|
||||||
|
|
||||||
|
// CSV header
|
||||||
|
writer.println("设备编号,设备名称,设备类型,区域,安装位置,状态,最后上报时间,流量(m³/h),压力(MPa),液位(m),浊度(NTU),pH,余氯(mg/L)");
|
||||||
|
|
||||||
|
for (MonitorDevice device : devices) {
|
||||||
|
StringBuilder line = new StringBuilder();
|
||||||
|
line.append(csvEsc(device.getDeviceCode())).append(",");
|
||||||
|
line.append(csvEsc(device.getDeviceName())).append(",");
|
||||||
|
line.append(csvEsc(device.getDeviceType())).append(",");
|
||||||
|
line.append(csvEsc(device.getArea())).append(",");
|
||||||
|
line.append(csvEsc(device.getLocation())).append(",");
|
||||||
|
line.append(csvEsc(device.getStatus())).append(",");
|
||||||
|
line.append(device.getLastReportTime() != null ? device.getLastReportTime().toString() : "");
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(req.getIncludeRealtime())) {
|
||||||
|
List<MonitorRealtimeData> realtimeList = realtimeDataMapper.selectLatestByDeviceId(device.getId());
|
||||||
|
Map<String, String> metricMap = new LinkedHashMap<>();
|
||||||
|
for (MonitorRealtimeData data : realtimeList) {
|
||||||
|
metricMap.put(data.getMetricKey(),
|
||||||
|
data.getMetricValue() != null ? data.getMetricValue().toPlainString() : "");
|
||||||
|
}
|
||||||
|
line.append(",").append(metricMap.getOrDefault("flow", ""));
|
||||||
|
line.append(",").append(metricMap.getOrDefault("pressure", ""));
|
||||||
|
line.append(",").append(metricMap.getOrDefault("level", ""));
|
||||||
|
line.append(",").append(metricMap.getOrDefault("turbidity", ""));
|
||||||
|
line.append(",").append(metricMap.getOrDefault("ph", ""));
|
||||||
|
line.append(",").append(metricMap.getOrDefault("residual_chlorine", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.println(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.flush();
|
||||||
|
return bos.toByteArray();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("CSV 导出失败", e);
|
||||||
|
throw new RuntimeException("CSV 导出失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MonitorDevice> getDevicesForExport(MonitorExportRequest req) {
|
||||||
|
// 如果指定了设备ID列表,直接按ID查询
|
||||||
|
if (req.getDeviceIds() != null && !req.getDeviceIds().isEmpty()) {
|
||||||
|
MonitorQueryRequest queryReq = new MonitorQueryRequest();
|
||||||
|
queryReq.setPageSize(10000);
|
||||||
|
return monitorListService.queryDevicesForExport(queryReq).stream()
|
||||||
|
.filter(d -> req.getDeviceIds().contains(d.getId()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按筛选条件查询
|
||||||
|
MonitorQueryRequest queryReq = new MonitorQueryRequest();
|
||||||
|
queryReq.setArea(req.getArea());
|
||||||
|
queryReq.setDeviceType(req.getDeviceType());
|
||||||
|
queryReq.setStatus(req.getStatus());
|
||||||
|
queryReq.setKeyword(req.getKeyword());
|
||||||
|
queryReq.setStartTime(req.getStartTime());
|
||||||
|
queryReq.setEndTime(req.getEndTime());
|
||||||
|
queryReq.setPageSize(10000);
|
||||||
|
return monitorListService.queryDevicesForExport(queryReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<String>> buildExcelHead() {
|
||||||
|
List<List<String>> head = new ArrayList<>();
|
||||||
|
head.add(List.of("设备编号"));
|
||||||
|
head.add(List.of("设备名称"));
|
||||||
|
head.add(List.of("设备类型"));
|
||||||
|
head.add(List.of("区域"));
|
||||||
|
head.add(List.of("安装位置"));
|
||||||
|
head.add(List.of("状态"));
|
||||||
|
head.add(List.of("最后上报时间"));
|
||||||
|
head.add(List.of("流量(m³/h)"));
|
||||||
|
head.add(List.of("压力(MPa)"));
|
||||||
|
head.add(List.of("液位(m)"));
|
||||||
|
head.add(List.of("浊度(NTU)"));
|
||||||
|
head.add(List.of("pH"));
|
||||||
|
head.add(List.of("余氯(mg/L)"));
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<Object>> buildExcelData(List<MonitorDevice> devices, Boolean includeRealtime) {
|
||||||
|
List<List<Object>> data = new ArrayList<>();
|
||||||
|
for (MonitorDevice device : devices) {
|
||||||
|
List<Object> row = new ArrayList<>();
|
||||||
|
row.add(device.getDeviceCode());
|
||||||
|
row.add(device.getDeviceName());
|
||||||
|
row.add(device.getDeviceType());
|
||||||
|
row.add(device.getArea());
|
||||||
|
row.add(device.getLocation());
|
||||||
|
row.add(formatStatus(device.getStatus()));
|
||||||
|
row.add(device.getLastReportTime() != null ? device.getLastReportTime().toString() : "");
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(includeRealtime)) {
|
||||||
|
List<MonitorRealtimeData> realtimeList = realtimeDataMapper.selectLatestByDeviceId(device.getId());
|
||||||
|
Map<String, Object> metricMap = new LinkedHashMap<>();
|
||||||
|
for (MonitorRealtimeData d : realtimeList) {
|
||||||
|
metricMap.put(d.getMetricKey(), d.getMetricValue());
|
||||||
|
}
|
||||||
|
row.add(metricMap.getOrDefault("flow", ""));
|
||||||
|
row.add(metricMap.getOrDefault("pressure", ""));
|
||||||
|
row.add(metricMap.getOrDefault("level", ""));
|
||||||
|
row.add(metricMap.getOrDefault("turbidity", ""));
|
||||||
|
row.add(metricMap.getOrDefault("ph", ""));
|
||||||
|
row.add(metricMap.getOrDefault("residual_chlorine", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.add(row);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatStatus(String status) {
|
||||||
|
if (status == null) return "未知";
|
||||||
|
return switch (status) {
|
||||||
|
case "online" -> "在线";
|
||||||
|
case "offline" -> "离线";
|
||||||
|
case "fault" -> "故障";
|
||||||
|
case "abnormal" -> "数据异常";
|
||||||
|
default -> status;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String csvEsc(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
|
||||||
|
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package com.water.production.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.production.dto.MonitorQueryRequest;
|
||||||
|
import com.water.production.entity.MonitorDevice;
|
||||||
|
import com.water.production.entity.MonitorRealtimeData;
|
||||||
|
import com.water.production.mapper.MonitorDeviceMapper;
|
||||||
|
import com.water.production.mapper.MonitorRealtimeDataMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线监测列表服务
|
||||||
|
* 提供设备列表查询、实时数据获取、设备详情、统计等功能
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MonitorListService {
|
||||||
|
|
||||||
|
private final MonitorDeviceMapper deviceMapper;
|
||||||
|
private final MonitorRealtimeDataMapper realtimeDataMapper;
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询在线监测设备列表(含最新实时数据)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> queryDeviceList(MonitorQueryRequest req) {
|
||||||
|
// 使用 MyBatis-Plus 分页
|
||||||
|
Page<MonitorDevice> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||||
|
|
||||||
|
LambdaQueryWrapper<MonitorDevice> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (req.getArea() != null && !req.getArea().isEmpty()) {
|
||||||
|
wrapper.eq(MonitorDevice::getArea, req.getArea());
|
||||||
|
}
|
||||||
|
if (req.getDeviceType() != null && !req.getDeviceType().isEmpty()) {
|
||||||
|
wrapper.eq(MonitorDevice::getDeviceType, req.getDeviceType());
|
||||||
|
}
|
||||||
|
if (req.getStatus() != null && !req.getStatus().isEmpty()) {
|
||||||
|
wrapper.eq(MonitorDevice::getStatus, req.getStatus());
|
||||||
|
}
|
||||||
|
if (req.getKeyword() != null && !req.getKeyword().isEmpty()) {
|
||||||
|
wrapper.and(w -> w.like(MonitorDevice::getDeviceCode, req.getKeyword())
|
||||||
|
.or().like(MonitorDevice::getDeviceName, req.getKeyword()));
|
||||||
|
}
|
||||||
|
if (req.getStartTime() != null && !req.getStartTime().isEmpty()) {
|
||||||
|
wrapper.ge(MonitorDevice::getLastReportTime, LocalDateTime.parse(req.getStartTime(), DTF));
|
||||||
|
}
|
||||||
|
if (req.getEndTime() != null && !req.getEndTime().isEmpty()) {
|
||||||
|
wrapper.le(MonitorDevice::getLastReportTime, LocalDateTime.parse(req.getEndTime(), DTF));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
String sortField = req.getSortField();
|
||||||
|
boolean isAsc = "asc".equalsIgnoreCase(req.getSortOrder());
|
||||||
|
if ("deviceCode".equals(sortField)) {
|
||||||
|
wrapper.orderBy(true, isAsc, MonitorDevice::getDeviceCode);
|
||||||
|
} else if ("deviceName".equals(sortField)) {
|
||||||
|
wrapper.orderBy(true, isAsc, MonitorDevice::getDeviceName);
|
||||||
|
} else if ("status".equals(sortField)) {
|
||||||
|
wrapper.orderBy(true, isAsc, MonitorDevice::getStatus);
|
||||||
|
} else {
|
||||||
|
wrapper.orderByDesc(MonitorDevice::getLastReportTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page<MonitorDevice> result = deviceMapper.selectPage(page, wrapper);
|
||||||
|
|
||||||
|
// 填充实时数据
|
||||||
|
List<Map<String, Object>> deviceList = new ArrayList<>();
|
||||||
|
for (MonitorDevice device : result.getRecords()) {
|
||||||
|
Map<String, Object> deviceMap = new LinkedHashMap<>();
|
||||||
|
deviceMap.put("id", device.getId());
|
||||||
|
deviceMap.put("deviceCode", device.getDeviceCode());
|
||||||
|
deviceMap.put("deviceName", device.getDeviceName());
|
||||||
|
deviceMap.put("deviceType", device.getDeviceType());
|
||||||
|
deviceMap.put("area", device.getArea());
|
||||||
|
deviceMap.put("location", device.getLocation());
|
||||||
|
deviceMap.put("lng", device.getLng());
|
||||||
|
deviceMap.put("lat", device.getLat());
|
||||||
|
deviceMap.put("status", device.getStatus());
|
||||||
|
deviceMap.put("lastReportTime", device.getLastReportTime());
|
||||||
|
deviceMap.put("brand", device.getBrand());
|
||||||
|
|
||||||
|
// 获取最新实时数据
|
||||||
|
List<MonitorRealtimeData> realtimeList = realtimeDataMapper.selectLatestByDeviceId(device.getId());
|
||||||
|
Map<String, Object> realtimeMap = new LinkedHashMap<>();
|
||||||
|
for (MonitorRealtimeData data : realtimeList) {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("metricKey", data.getMetricKey());
|
||||||
|
item.put("value", data.getMetricValue());
|
||||||
|
item.put("unit", data.getUnit());
|
||||||
|
item.put("thresholdHigh", data.getThresholdHigh());
|
||||||
|
item.put("thresholdLow", data.getThresholdLow());
|
||||||
|
item.put("isAbnormal", data.getIsAbnormal());
|
||||||
|
item.put("collectTime", data.getCollectTime());
|
||||||
|
realtimeMap.put(data.getMetricKey(), item);
|
||||||
|
}
|
||||||
|
deviceMap.put("realtimeData", realtimeMap);
|
||||||
|
deviceList.add(deviceMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> pageResult = new LinkedHashMap<>();
|
||||||
|
pageResult.put("records", deviceList);
|
||||||
|
pageResult.put("total", result.getTotal());
|
||||||
|
pageResult.put("pageNum", result.getCurrent());
|
||||||
|
pageResult.put("pageSize", result.getSize());
|
||||||
|
pageResult.put("pages", result.getPages());
|
||||||
|
return pageResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个设备详情(含实时数据)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getDeviceDetail(Long deviceId) {
|
||||||
|
MonitorDevice device = deviceMapper.selectById(deviceId);
|
||||||
|
if (device == null) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> detail = new LinkedHashMap<>();
|
||||||
|
detail.put("id", device.getId());
|
||||||
|
detail.put("deviceCode", device.getDeviceCode());
|
||||||
|
detail.put("deviceName", device.getDeviceName());
|
||||||
|
detail.put("deviceType", device.getDeviceType());
|
||||||
|
detail.put("area", device.getArea());
|
||||||
|
detail.put("location", device.getLocation());
|
||||||
|
detail.put("lng", device.getLng());
|
||||||
|
detail.put("lat", device.getLat());
|
||||||
|
detail.put("status", device.getStatus());
|
||||||
|
detail.put("lastReportTime", device.getLastReportTime());
|
||||||
|
detail.put("brand", device.getBrand());
|
||||||
|
detail.put("installTime", device.getInstallTime());
|
||||||
|
detail.put("remark", device.getRemark());
|
||||||
|
|
||||||
|
List<MonitorRealtimeData> realtimeList = realtimeDataMapper.selectLatestByDeviceId(deviceId);
|
||||||
|
detail.put("realtimeData", realtimeList);
|
||||||
|
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备实时数据(多参数)
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getDeviceRealtimeData(Long deviceId) {
|
||||||
|
List<MonitorRealtimeData> list = realtimeDataMapper.selectLatestByDeviceId(deviceId);
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (MonitorRealtimeData data : list) {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("metricKey", data.getMetricKey());
|
||||||
|
map.put("value", data.getMetricValue());
|
||||||
|
map.put("unit", data.getUnit());
|
||||||
|
map.put("thresholdHigh", data.getThresholdHigh());
|
||||||
|
map.put("thresholdLow", data.getThresholdLow());
|
||||||
|
map.put("isAbnormal", data.getIsAbnormal());
|
||||||
|
map.put("collectTime", data.getCollectTime());
|
||||||
|
result.add(map);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备统计概览(按状态、类型、区域分组)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getDeviceStatistics() {
|
||||||
|
Map<String, Object> stats = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// 按状态统计
|
||||||
|
List<MonitorDevice> allDevices = deviceMapper.selectList(null);
|
||||||
|
Map<String, Long> statusCount = allDevices.stream()
|
||||||
|
.collect(Collectors.groupingBy(d -> d.getStatus() != null ? d.getStatus() : "unknown", Collectors.counting()));
|
||||||
|
stats.put("statusDistribution", statusCount);
|
||||||
|
stats.put("totalDevices", allDevices.size());
|
||||||
|
stats.put("onlineCount", statusCount.getOrDefault("online", 0L));
|
||||||
|
stats.put("offlineCount", statusCount.getOrDefault("offline", 0L));
|
||||||
|
stats.put("faultCount", statusCount.getOrDefault("fault", 0L));
|
||||||
|
stats.put("abnormalCount", statusCount.getOrDefault("abnormal", 0L));
|
||||||
|
|
||||||
|
// 按类型统计
|
||||||
|
Map<String, Long> typeCount = allDevices.stream()
|
||||||
|
.collect(Collectors.groupingBy(d -> d.getDeviceType() != null ? d.getDeviceType() : "unknown", Collectors.counting()));
|
||||||
|
stats.put("typeDistribution", typeCount);
|
||||||
|
|
||||||
|
// 按区域统计
|
||||||
|
Map<String, Long> areaCount = allDevices.stream()
|
||||||
|
.collect(Collectors.groupingBy(d -> d.getArea() != null ? d.getArea() : "unknown", Collectors.counting()));
|
||||||
|
stats.put("areaDistribution", areaCount);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新设备状态
|
||||||
|
*/
|
||||||
|
public void updateDeviceStatus(Long deviceId, String status) {
|
||||||
|
MonitorDevice device = new MonitorDevice();
|
||||||
|
device.setId(deviceId);
|
||||||
|
device.setStatus(status);
|
||||||
|
device.setUpdatedTime(LocalDateTime.now());
|
||||||
|
deviceMapper.updateById(device);
|
||||||
|
log.info("设备 {} 状态更新为 {}", deviceId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有支持的区域列表
|
||||||
|
*/
|
||||||
|
public List<String> getAreaList() {
|
||||||
|
LambdaQueryWrapper<MonitorDevice> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.select(MonitorDevice::getArea).groupBy(MonitorDevice::getArea);
|
||||||
|
return deviceMapper.selectList(wrapper).stream()
|
||||||
|
.map(MonitorDevice::getArea)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有支持的设备类型列表
|
||||||
|
*/
|
||||||
|
public List<String> getDeviceTypeList() {
|
||||||
|
LambdaQueryWrapper<MonitorDevice> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.select(MonitorDevice::getDeviceType).groupBy(MonitorDevice::getDeviceType);
|
||||||
|
return deviceMapper.selectList(wrapper).stream()
|
||||||
|
.map(MonitorDevice::getDeviceType)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取设备实时数据(用于导出)
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getBatchRealtimeData(List<Long> deviceIds) {
|
||||||
|
if (deviceIds == null || deviceIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return realtimeDataMapper.selectLatestBatch(deviceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据筛选条件查询设备列表(不分页,用于导出)
|
||||||
|
*/
|
||||||
|
public List<MonitorDevice> queryDevicesForExport(MonitorQueryRequest req) {
|
||||||
|
LambdaQueryWrapper<MonitorDevice> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (req.getArea() != null && !req.getArea().isEmpty()) {
|
||||||
|
wrapper.eq(MonitorDevice::getArea, req.getArea());
|
||||||
|
}
|
||||||
|
if (req.getDeviceType() != null && !req.getDeviceType().isEmpty()) {
|
||||||
|
wrapper.eq(MonitorDevice::getDeviceType, req.getDeviceType());
|
||||||
|
}
|
||||||
|
if (req.getStatus() != null && !req.getStatus().isEmpty()) {
|
||||||
|
wrapper.eq(MonitorDevice::getStatus, req.getStatus());
|
||||||
|
}
|
||||||
|
if (req.getKeyword() != null && !req.getKeyword().isEmpty()) {
|
||||||
|
wrapper.and(w -> w.like(MonitorDevice::getDeviceCode, req.getKeyword())
|
||||||
|
.or().like(MonitorDevice::getDeviceName, req.getKeyword()));
|
||||||
|
}
|
||||||
|
wrapper.orderByDesc(MonitorDevice::getLastReportTime);
|
||||||
|
return deviceMapper.selectList(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package com.water.production.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.water.production.entity.VideoCamera;
|
||||||
|
import com.water.production.entity.VideoRecording;
|
||||||
|
import com.water.production.mapper.VideoCameraMapper;
|
||||||
|
import com.water.production.mapper.VideoRecordingMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频监控管理服务
|
||||||
|
* 提供摄像头CRUD、视频流地址管理、状态监控、录像查询、统计等能力
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VideoMonitorService {
|
||||||
|
|
||||||
|
private final VideoCameraMapper cameraMapper;
|
||||||
|
private final VideoRecordingMapper recordingMapper;
|
||||||
|
|
||||||
|
// ==================== 摄像头 CRUD ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询摄像头列表
|
||||||
|
*/
|
||||||
|
public Page<VideoCamera> pageCameras(int current, int size, String area, Integer status, String keyword) {
|
||||||
|
Page<VideoCamera> page = new Page<>(current, size);
|
||||||
|
LambdaQueryWrapper<VideoCamera> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(area != null, VideoCamera::getArea, area)
|
||||||
|
.eq(status != null, VideoCamera::getStatus, status)
|
||||||
|
.and(StringUtils.hasText(keyword), w ->
|
||||||
|
w.like(VideoCamera::getName, keyword)
|
||||||
|
.or().like(VideoCamera::getCameraId, keyword))
|
||||||
|
.orderByDesc(VideoCamera::getCreatedTime);
|
||||||
|
return cameraMapper.selectPage(page, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有摄像头
|
||||||
|
*/
|
||||||
|
public List<VideoCamera> listAllCameras() {
|
||||||
|
return cameraMapper.selectList(new LambdaQueryWrapper<VideoCamera>()
|
||||||
|
.orderByDesc(VideoCamera::getCreatedTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取摄像头详情
|
||||||
|
*/
|
||||||
|
public VideoCamera getCameraById(Long id) {
|
||||||
|
return cameraMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建摄像头
|
||||||
|
*/
|
||||||
|
public VideoCamera createCamera(VideoCamera camera) {
|
||||||
|
camera.setStatus(camera.getStatus() != null ? camera.getStatus() : 0);
|
||||||
|
camera.setAiEnabled(camera.getAiEnabled() != null ? camera.getAiEnabled() : 0);
|
||||||
|
camera.setCreatedTime(LocalDateTime.now());
|
||||||
|
camera.setUpdatedTime(LocalDateTime.now());
|
||||||
|
camera.setDeleted(0);
|
||||||
|
cameraMapper.insert(camera);
|
||||||
|
log.info("创建摄像头: {} ({})", camera.getName(), camera.getCameraId());
|
||||||
|
return camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新摄像头
|
||||||
|
*/
|
||||||
|
public boolean updateCamera(VideoCamera camera) {
|
||||||
|
camera.setUpdatedTime(LocalDateTime.now());
|
||||||
|
int rows = cameraMapper.updateById(camera);
|
||||||
|
if (rows > 0) {
|
||||||
|
log.info("更新摄像头: id={}", camera.getId());
|
||||||
|
}
|
||||||
|
return rows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除摄像头
|
||||||
|
*/
|
||||||
|
public boolean deleteCamera(Long id) {
|
||||||
|
int rows = cameraMapper.deleteById(id);
|
||||||
|
if (rows > 0) {
|
||||||
|
log.info("删除摄像头: id={}", id);
|
||||||
|
}
|
||||||
|
return rows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 视频流地址管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取摄像头的所有视频流地址
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getStreamUrls(Long cameraId) {
|
||||||
|
VideoCamera camera = cameraMapper.selectById(cameraId);
|
||||||
|
if (camera == null) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Object> urls = new LinkedHashMap<>();
|
||||||
|
urls.put("cameraId", camera.getCameraId());
|
||||||
|
urls.put("name", camera.getName());
|
||||||
|
urls.put("rtsp", camera.getStreamUrlRtsp());
|
||||||
|
urls.put("hls", camera.getStreamUrlHls());
|
||||||
|
urls.put("flv", camera.getStreamUrlFlv());
|
||||||
|
urls.put("status", camera.getStatus());
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新视频流地址
|
||||||
|
*/
|
||||||
|
public boolean updateStreamUrls(Long cameraId, String rtsp, String hls, String flv) {
|
||||||
|
VideoCamera camera = new VideoCamera();
|
||||||
|
camera.setId(cameraId);
|
||||||
|
camera.setStreamUrlRtsp(rtsp);
|
||||||
|
camera.setStreamUrlHls(hls);
|
||||||
|
camera.setStreamUrlFlv(flv);
|
||||||
|
camera.setUpdatedTime(LocalDateTime.now());
|
||||||
|
return cameraMapper.updateById(camera) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 状态监控 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新摄像头在线状态(模拟心跳检测)
|
||||||
|
*/
|
||||||
|
public boolean updateCameraStatus(Long cameraId, Integer status) {
|
||||||
|
VideoCamera camera = new VideoCamera();
|
||||||
|
camera.setId(cameraId);
|
||||||
|
camera.setStatus(status);
|
||||||
|
if (status != null && status == 1) {
|
||||||
|
camera.setLastOnlineTime(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
camera.setUpdatedTime(LocalDateTime.now());
|
||||||
|
return cameraMapper.updateById(camera) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量刷新摄像头状态(模拟)
|
||||||
|
* 实际场景: 从流媒体服务器获取在线状态
|
||||||
|
*/
|
||||||
|
public Map<String, Object> refreshAllStatus() {
|
||||||
|
List<VideoCamera> cameras = cameraMapper.selectList(null);
|
||||||
|
int online = 0, offline = 0, fault = 0;
|
||||||
|
for (VideoCamera cam : cameras) {
|
||||||
|
// 模拟: 根据最后在线时间判断状态
|
||||||
|
if (cam.getLastOnlineTime() != null
|
||||||
|
&& Duration.between(cam.getLastOnlineTime(), LocalDateTime.now()).toMinutes() < 5) {
|
||||||
|
online++;
|
||||||
|
} else if (cam.getStatus() != null && cam.getStatus() == 2) {
|
||||||
|
fault++;
|
||||||
|
} else {
|
||||||
|
offline++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("total", cameras.size());
|
||||||
|
result.put("online", online);
|
||||||
|
result.put("offline", offline);
|
||||||
|
result.put("fault", fault);
|
||||||
|
result.put("refreshTime", LocalDateTime.now());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 视频录像 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询录像记录
|
||||||
|
*/
|
||||||
|
public Page<VideoRecording> pageRecordings(int current, int size, Long cameraId,
|
||||||
|
String recordType, String startDate, String endDate) {
|
||||||
|
Page<VideoRecording> page = new Page<>(current, size);
|
||||||
|
LambdaQueryWrapper<VideoRecording> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(cameraId != null, VideoRecording::getCameraId, cameraId)
|
||||||
|
.eq(recordType != null, VideoRecording::getRecordType, recordType)
|
||||||
|
.ge(StringUtils.hasText(startDate), VideoRecording::getStartTime,
|
||||||
|
startDate != null ? startDate + " 00:00:00" : null)
|
||||||
|
.le(StringUtils.hasText(endDate), VideoRecording::getEndTime,
|
||||||
|
endDate != null ? endDate + " 23:59:59" : null)
|
||||||
|
.orderByDesc(VideoRecording::getStartTime);
|
||||||
|
return recordingMapper.selectPage(page, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成回放地址
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getPlaybackUrl(Long recordingId) {
|
||||||
|
VideoRecording recording = recordingMapper.selectById(recordingId);
|
||||||
|
if (recording == null) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("recordingId", recording.getId());
|
||||||
|
result.put("cameraId", recording.getCameraId());
|
||||||
|
result.put("cameraName", recording.getCameraName());
|
||||||
|
result.put("startTime", recording.getStartTime());
|
||||||
|
result.put("endTime", recording.getEndTime());
|
||||||
|
result.put("durationSec", recording.getDurationSec());
|
||||||
|
result.put("playbackUrl", recording.getPlaybackUrl());
|
||||||
|
result.put("fileSizeMb", recording.getFileSizeMb());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建录像记录
|
||||||
|
*/
|
||||||
|
public VideoRecording createRecording(VideoRecording recording) {
|
||||||
|
recording.setCreatedTime(LocalDateTime.now());
|
||||||
|
recording.setUpdatedTime(LocalDateTime.now());
|
||||||
|
recording.setDeleted(0);
|
||||||
|
recordingMapper.insert(recording);
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除录像记录
|
||||||
|
*/
|
||||||
|
public boolean deleteRecording(Long id) {
|
||||||
|
return recordingMapper.deleteById(id) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 统计 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备在线率统计
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getDeviceOnlineStats() {
|
||||||
|
List<VideoCamera> cameras = cameraMapper.selectList(null);
|
||||||
|
long total = cameras.size();
|
||||||
|
long online = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 1).count();
|
||||||
|
long offline = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 0).count();
|
||||||
|
long fault = cameras.stream().filter(c -> c.getStatus() != null && c.getStatus() == 2).count();
|
||||||
|
double onlineRate = total > 0 ? (double) online / total * 100 : 0;
|
||||||
|
|
||||||
|
Map<String, Object> stats = new LinkedHashMap<>();
|
||||||
|
stats.put("total", total);
|
||||||
|
stats.put("online", online);
|
||||||
|
stats.put("offline", offline);
|
||||||
|
stats.put("fault", fault);
|
||||||
|
stats.put("onlineRate", Math.round(onlineRate * 100.0) / 100.0);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按区域统计摄像头分布
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getCameraStatsByArea() {
|
||||||
|
List<VideoCamera> cameras = cameraMapper.selectList(null);
|
||||||
|
Map<String, List<VideoCamera>> grouped = cameras.stream()
|
||||||
|
.filter(c -> c.getArea() != null)
|
||||||
|
.collect(Collectors.groupingBy(VideoCamera::getArea));
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<VideoCamera>> entry : grouped.entrySet()) {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("area", entry.getKey());
|
||||||
|
item.put("total", entry.getValue().size());
|
||||||
|
long online = entry.getValue().stream().filter(c -> c.getStatus() != null && c.getStatus() == 1).count();
|
||||||
|
item.put("online", online);
|
||||||
|
item.put("onlineRate", entry.getValue().isEmpty() ? 0 :
|
||||||
|
Math.round((double) online / entry.getValue().size() * 10000.0) / 100.0);
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- V2: 总览大屏 + 能耗数据表
|
||||||
|
-- Issue #61: 总览大屏(进出水量/水质/设备/报警/能耗)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 总览大屏每日快照表
|
||||||
|
CREATE TABLE IF NOT EXISTS prod_dashboard_summary (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
summary_date DATE NOT NULL,
|
||||||
|
area VARCHAR(100) NOT NULL DEFAULT '一体化水厂',
|
||||||
|
|
||||||
|
-- 进出水量
|
||||||
|
today_inflow NUMERIC(14,2) DEFAULT 0,
|
||||||
|
today_outflow NUMERIC(14,2) DEFAULT 0,
|
||||||
|
yesterday_inflow NUMERIC(14,2) DEFAULT 0,
|
||||||
|
yesterday_outflow NUMERIC(14,2) DEFAULT 0,
|
||||||
|
month_inflow NUMERIC(16,2) DEFAULT 0,
|
||||||
|
month_outflow NUMERIC(16,2) DEFAULT 0,
|
||||||
|
|
||||||
|
-- 水质概览
|
||||||
|
raw_water_pass_rate NUMERIC(5,2) DEFAULT 100,
|
||||||
|
factory_water_pass_rate NUMERIC(5,2) DEFAULT 100,
|
||||||
|
terminal_water_pass_rate NUMERIC(5,2) DEFAULT 100,
|
||||||
|
overall_pass_rate NUMERIC(5,2) DEFAULT 100,
|
||||||
|
|
||||||
|
-- 设备概况
|
||||||
|
device_total INT DEFAULT 0,
|
||||||
|
device_online INT DEFAULT 0,
|
||||||
|
device_offline INT DEFAULT 0,
|
||||||
|
device_fault INT DEFAULT 0,
|
||||||
|
device_online_rate NUMERIC(5,2) DEFAULT 0,
|
||||||
|
|
||||||
|
-- 报警概览
|
||||||
|
today_alert_total INT DEFAULT 0,
|
||||||
|
active_alert_count INT DEFAULT 0,
|
||||||
|
general_alert_count INT DEFAULT 0,
|
||||||
|
important_alert_count INT DEFAULT 0,
|
||||||
|
urgent_alert_count INT DEFAULT 0,
|
||||||
|
|
||||||
|
-- 能耗数据
|
||||||
|
today_power_kwh NUMERIC(12,2) DEFAULT 0,
|
||||||
|
today_chemical_kg NUMERIC(12,2) DEFAULT 0,
|
||||||
|
unit_energy_consumption NUMERIC(8,4) DEFAULT 0,
|
||||||
|
|
||||||
|
created_time TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_time TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 唯一约束:每个区域每天一条快照
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_dashboard_date_area
|
||||||
|
ON prod_dashboard_summary (summary_date, area);
|
||||||
|
|
||||||
|
-- 索引:按区域查询
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dashboard_area
|
||||||
|
ON prod_dashboard_summary (area);
|
||||||
|
|
||||||
|
-- 能耗数据明细表
|
||||||
|
CREATE TABLE IF NOT EXISTS prod_energy_consumption (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
record_date DATE NOT NULL,
|
||||||
|
area VARCHAR(100) NOT NULL DEFAULT '一体化水厂',
|
||||||
|
energy_type VARCHAR(30) NOT NULL, -- power/coagulant/disinfectant
|
||||||
|
consumption NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
unit VARCHAR(20) NOT NULL DEFAULT 'kWh',
|
||||||
|
production_volume NUMERIC(14,2) DEFAULT 0,
|
||||||
|
unit_consumption NUMERIC(8,4) DEFAULT 0,
|
||||||
|
remark VARCHAR(500),
|
||||||
|
created_time TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_time TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引:按区域+日期
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_energy_area_date
|
||||||
|
ON prod_energy_consumption (area, record_date);
|
||||||
|
|
||||||
|
-- 索引:按能耗类型
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_energy_type
|
||||||
|
ON prod_energy_consumption (energy_type);
|
||||||
|
|
||||||
|
-- 唯一约束:每个区域每天每种能耗类型一条记录
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_energy_date_area_type
|
||||||
|
ON prod_energy_consumption (record_date, area, energy_type);
|
||||||
|
|
||||||
|
COMMENT ON TABLE prod_dashboard_summary IS '总览大屏每日快照表';
|
||||||
|
COMMENT ON TABLE prod_energy_consumption IS '能耗数据明细表';
|
||||||
|
COMMENT ON COLUMN prod_energy_consumption.energy_type IS '能耗类型: power(电耗)/coagulant(混凝剂)/disinfectant(消毒剂)';
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
-- Video Monitor & AI Intrusion Detection DDL
|
||||||
|
|
||||||
|
-- Enhance prod_video_camera with more fields
|
||||||
|
ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS area VARCHAR(50);
|
||||||
|
ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS stream_url VARCHAR(500);
|
||||||
|
ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'online';
|
||||||
|
ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS lng DOUBLE PRECISION;
|
||||||
|
ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS lat DOUBLE PRECISION;
|
||||||
|
ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS created_time TIMESTAMP DEFAULT NOW();
|
||||||
|
ALTER TABLE prod_video_camera ADD COLUMN IF NOT EXISTS last_online_time TIMESTAMP;
|
||||||
|
|
||||||
|
-- Video recordings
|
||||||
|
CREATE TABLE IF NOT EXISTS prod_video_recording (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
camera_id BIGINT,
|
||||||
|
recording_no VARCHAR(50),
|
||||||
|
start_time TIMESTAMP,
|
||||||
|
end_time TIMESTAMP,
|
||||||
|
duration_seconds INT,
|
||||||
|
file_size BIGINT,
|
||||||
|
file_path VARCHAR(500),
|
||||||
|
storage_type VARCHAR(20) DEFAULT 'local',
|
||||||
|
created_time TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Intrusion events
|
||||||
|
CREATE TABLE IF NOT EXISTS prod_intrusion_event (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
event_no VARCHAR(50) UNIQUE,
|
||||||
|
camera_id VARCHAR(50),
|
||||||
|
area VARCHAR(50),
|
||||||
|
detected_time TIMESTAMP,
|
||||||
|
confidence DOUBLE PRECISION,
|
||||||
|
detected BOOLEAN DEFAULT false,
|
||||||
|
status VARCHAR(20) DEFAULT 'ACTIVE',
|
||||||
|
alert_level VARCHAR(10),
|
||||||
|
alert_triggered BOOLEAN DEFAULT false,
|
||||||
|
snapshot_url VARCHAR(500),
|
||||||
|
description TEXT,
|
||||||
|
confirmed_by VARCHAR(50),
|
||||||
|
confirmed_time TIMESTAMP,
|
||||||
|
resolved_time TIMESTAMP,
|
||||||
|
created_time TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vr_camera ON prod_video_recording(camera_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vr_time ON prod_video_recording(start_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ie_status ON prod_intrusion_event(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ie_area ON prod_intrusion_event(area);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ie_time ON prod_intrusion_event(detected_time);
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
package com.water.production.service;
|
||||||
|
|
||||||
|
import com.water.production.entity.DashboardSummary;
|
||||||
|
import com.water.production.entity.EnergyConsumption;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DashboardService + EnergyService 单元测试
|
||||||
|
* 测试实体构建、数据聚合逻辑、环比计算等核心逻辑
|
||||||
|
*/
|
||||||
|
class DashboardServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("测试 DashboardSummary 实体字段完整性")
|
||||||
|
void testDashboardSummaryEntityFields() {
|
||||||
|
DashboardSummary summary = new DashboardSummary();
|
||||||
|
summary.setId(1L);
|
||||||
|
summary.setSummaryDate(LocalDate.of(2026, 6, 14));
|
||||||
|
summary.setArea("一体化水厂");
|
||||||
|
|
||||||
|
// 进出水量
|
||||||
|
summary.setTodayInflow(new BigDecimal("15000.50"));
|
||||||
|
summary.setTodayOutflow(new BigDecimal("14200.30"));
|
||||||
|
summary.setYesterdayInflow(new BigDecimal("14500.00"));
|
||||||
|
summary.setYesterdayOutflow(new BigDecimal("13800.00"));
|
||||||
|
summary.setMonthInflow(new BigDecimal("420000.00"));
|
||||||
|
summary.setMonthOutflow(new BigDecimal("398000.00"));
|
||||||
|
|
||||||
|
// 水质
|
||||||
|
summary.setRawWaterPassRate(new BigDecimal("98.5"));
|
||||||
|
summary.setFactoryWaterPassRate(new BigDecimal("99.2"));
|
||||||
|
summary.setTerminalWaterPassRate(new BigDecimal("97.8"));
|
||||||
|
summary.setOverallPassRate(new BigDecimal("98.5"));
|
||||||
|
|
||||||
|
// 设备
|
||||||
|
summary.setDeviceTotal(120);
|
||||||
|
summary.setDeviceOnline(105);
|
||||||
|
summary.setDeviceOffline(10);
|
||||||
|
summary.setDeviceFault(5);
|
||||||
|
summary.setDeviceOnlineRate(new BigDecimal("87.5"));
|
||||||
|
|
||||||
|
// 报警
|
||||||
|
summary.setTodayAlertTotal(8);
|
||||||
|
summary.setActiveAlertCount(3);
|
||||||
|
summary.setGeneralAlertCount(5);
|
||||||
|
summary.setImportantAlertCount(2);
|
||||||
|
summary.setUrgentAlertCount(1);
|
||||||
|
|
||||||
|
// 能耗
|
||||||
|
summary.setTodayPowerKwh(new BigDecimal("1250.50"));
|
||||||
|
summary.setTodayChemicalKg(new BigDecimal("57.50"));
|
||||||
|
summary.setUnitEnergyConsumption(new BigDecimal("0.0834"));
|
||||||
|
|
||||||
|
// 验证所有字段
|
||||||
|
assertEquals(1L, summary.getId());
|
||||||
|
assertEquals(LocalDate.of(2026, 6, 14), summary.getSummaryDate());
|
||||||
|
assertEquals("一体化水厂", summary.getArea());
|
||||||
|
assertEquals(new BigDecimal("15000.50"), summary.getTodayInflow());
|
||||||
|
assertEquals(new BigDecimal("14200.30"), summary.getTodayOutflow());
|
||||||
|
assertEquals(120, summary.getDeviceTotal());
|
||||||
|
assertEquals(105, summary.getDeviceOnline());
|
||||||
|
assertEquals(8, summary.getTodayAlertTotal());
|
||||||
|
assertEquals(new BigDecimal("1250.50"), summary.getTodayPowerKwh());
|
||||||
|
assertEquals(new BigDecimal("0.0834"), summary.getUnitEnergyConsumption());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("测试 EnergyConsumption 实体字段及单位能耗计算")
|
||||||
|
void testEnergyConsumptionEntity() {
|
||||||
|
EnergyConsumption ec = new EnergyConsumption();
|
||||||
|
ec.setId(1L);
|
||||||
|
ec.setRecordDate(LocalDate.of(2026, 6, 14));
|
||||||
|
ec.setArea("一体化水厂");
|
||||||
|
ec.setEnergyType("power");
|
||||||
|
ec.setConsumption(new BigDecimal("1250.50"));
|
||||||
|
ec.setUnit("kWh");
|
||||||
|
ec.setProductionVolume(new BigDecimal("15000.00"));
|
||||||
|
|
||||||
|
// 计算单位能耗
|
||||||
|
BigDecimal unitConsumption = ec.getConsumption()
|
||||||
|
.divide(ec.getProductionVolume(), 4, RoundingMode.HALF_UP);
|
||||||
|
ec.setUnitConsumption(unitConsumption);
|
||||||
|
|
||||||
|
assertEquals("power", ec.getEnergyType());
|
||||||
|
assertEquals(new BigDecimal("1250.50"), ec.getConsumption());
|
||||||
|
assertEquals("kWh", ec.getUnit());
|
||||||
|
assertEquals(new BigDecimal("15000.00"), ec.getProductionVolume());
|
||||||
|
assertEquals(new BigDecimal("0.0834"), ec.getUnitConsumption());
|
||||||
|
|
||||||
|
// 测试药剂类型
|
||||||
|
EnergyConsumption chemical = new EnergyConsumption();
|
||||||
|
chemical.setEnergyType("coagulant");
|
||||||
|
chemical.setConsumption(new BigDecimal("45.00"));
|
||||||
|
chemical.setUnit("kg");
|
||||||
|
chemical.setProductionVolume(new BigDecimal("15000.00"));
|
||||||
|
BigDecimal chemUnit = chemical.getConsumption()
|
||||||
|
.divide(chemical.getProductionVolume(), 4, RoundingMode.HALF_UP);
|
||||||
|
chemical.setUnitConsumption(chemUnit);
|
||||||
|
|
||||||
|
assertEquals("coagulant", chemical.getEnergyType());
|
||||||
|
assertEquals(new BigDecimal("0.0030"), chemical.getUnitConsumption());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("测试日环比计算逻辑")
|
||||||
|
void testDayOverDayCalculation() {
|
||||||
|
// 模拟进出水量日环比计算
|
||||||
|
double todayInflow = 15000.0;
|
||||||
|
double yesterdayInflow = 14000.0;
|
||||||
|
double expectedRate = ((todayInflow - yesterdayInflow) / yesterdayInflow) * 100;
|
||||||
|
BigDecimal rate = BigDecimal.valueOf(expectedRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("7.1"), rate);
|
||||||
|
|
||||||
|
// 测试下降情况
|
||||||
|
todayInflow = 13000.0;
|
||||||
|
yesterdayInflow = 14000.0;
|
||||||
|
expectedRate = ((todayInflow - yesterdayInflow) / yesterdayInflow) * 100;
|
||||||
|
rate = BigDecimal.valueOf(expectedRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("-7.1"), rate);
|
||||||
|
|
||||||
|
// 测试昨日为零的情况(除以零保护)
|
||||||
|
yesterdayInflow = 0;
|
||||||
|
if (yesterdayInflow > 0) {
|
||||||
|
fail("Should not reach here");
|
||||||
|
}
|
||||||
|
// 应该返回null,不计算
|
||||||
|
BigDecimal nullRate = null;
|
||||||
|
assertNull(nullRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("测试水质合格率计算逻辑")
|
||||||
|
void testWaterQualityPassRateCalculation() {
|
||||||
|
// 模拟原水检测数据
|
||||||
|
int totalTests = 200;
|
||||||
|
int passedTests = 195;
|
||||||
|
double passRate = totalTests > 0 ? (passedTests * 100.0 / totalTests) : 100.0;
|
||||||
|
BigDecimal rate = BigDecimal.valueOf(passRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("97.5"), rate);
|
||||||
|
|
||||||
|
// 模拟全部合格
|
||||||
|
totalTests = 150;
|
||||||
|
passedTests = 150;
|
||||||
|
passRate = totalTests > 0 ? (passedTests * 100.0 / totalTests) : 100.0;
|
||||||
|
rate = BigDecimal.valueOf(passRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("100.0"), rate);
|
||||||
|
|
||||||
|
// 模拟无检测数据
|
||||||
|
totalTests = 0;
|
||||||
|
passedTests = 0;
|
||||||
|
passRate = totalTests > 0 ? (passedTests * 100.0 / totalTests) : 100.0;
|
||||||
|
rate = BigDecimal.valueOf(passRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("100.0"), rate);
|
||||||
|
|
||||||
|
// 模拟综合合格率
|
||||||
|
int rawTotal = 200, rawPassed = 195;
|
||||||
|
int factoryTotal = 180, factoryPassed = 178;
|
||||||
|
int terminalTotal = 160, terminalPassed = 155;
|
||||||
|
int allTotal = rawTotal + factoryTotal + terminalTotal;
|
||||||
|
int allPassed = rawPassed + factoryPassed + terminalPassed;
|
||||||
|
double overallRate = allTotal > 0 ? (allPassed * 100.0 / allTotal) : 100.0;
|
||||||
|
BigDecimal overallBd = BigDecimal.valueOf(overallRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("97.8"), overallBd);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("测试设备在线率计算逻辑")
|
||||||
|
void testDeviceOnlineRateCalculation() {
|
||||||
|
int online = 105, offline = 10, fault = 5;
|
||||||
|
int total = online + offline + fault;
|
||||||
|
assertEquals(120, total);
|
||||||
|
|
||||||
|
double onlineRate = total > 0 ? (online * 100.0 / total) : 0.0;
|
||||||
|
BigDecimal rate = BigDecimal.valueOf(onlineRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("87.5"), rate);
|
||||||
|
|
||||||
|
// 测试全部离线
|
||||||
|
online = 0;
|
||||||
|
offline = 50;
|
||||||
|
fault = 10;
|
||||||
|
total = online + offline + fault;
|
||||||
|
onlineRate = total > 0 ? (online * 100.0 / total) : 0.0;
|
||||||
|
rate = BigDecimal.valueOf(onlineRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("0.0"), rate);
|
||||||
|
|
||||||
|
// 测试无设备
|
||||||
|
online = 0;
|
||||||
|
offline = 0;
|
||||||
|
fault = 0;
|
||||||
|
total = 0;
|
||||||
|
onlineRate = total > 0 ? (online * 100.0 / total) : 0.0;
|
||||||
|
rate = BigDecimal.valueOf(onlineRate).setScale(1, RoundingMode.HALF_UP);
|
||||||
|
assertEquals(new BigDecimal("0.0"), rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("测试报警级别分布统计逻辑")
|
||||||
|
void testAlertLevelDistribution() {
|
||||||
|
// 模拟数据库返回的级别统计
|
||||||
|
List<Map<String, Object>> levelStats = new ArrayList<>();
|
||||||
|
levelStats.add(Map.of("alert_level", "general", "count", 5L));
|
||||||
|
levelStats.add(Map.of("alert_level", "important", "count", 2L));
|
||||||
|
levelStats.add(Map.of("alert_level", "urgent", "count", 1L));
|
||||||
|
|
||||||
|
Map<String, Integer> levelMap = new LinkedHashMap<>();
|
||||||
|
levelMap.put("general", 0);
|
||||||
|
levelMap.put("important", 0);
|
||||||
|
levelMap.put("urgent", 0);
|
||||||
|
|
||||||
|
for (Map<String, Object> row : levelStats) {
|
||||||
|
String level = (String) row.get("alert_level");
|
||||||
|
int count = ((Number) row.get("count")).intValue();
|
||||||
|
levelMap.put(level, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(5, levelMap.get("general"));
|
||||||
|
assertEquals(2, levelMap.get("important"));
|
||||||
|
assertEquals(1, levelMap.get("urgent"));
|
||||||
|
assertEquals(8, levelMap.values().stream().mapToInt(Integer::intValue).sum());
|
||||||
|
|
||||||
|
// 测试缺少某个级别时的默认值
|
||||||
|
List<Map<String, Object>> partialStats = new ArrayList<>();
|
||||||
|
partialStats.add(Map.of("alert_level", "urgent", "count", 3L));
|
||||||
|
|
||||||
|
Map<String, Integer> partialMap = new LinkedHashMap<>();
|
||||||
|
partialMap.put("general", 0);
|
||||||
|
partialMap.put("important", 0);
|
||||||
|
partialMap.put("urgent", 0);
|
||||||
|
|
||||||
|
for (Map<String, Object> row : partialStats) {
|
||||||
|
String level = (String) row.get("alert_level");
|
||||||
|
int count = ((Number) row.get("count")).intValue();
|
||||||
|
partialMap.put(level, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0, partialMap.get("general"));
|
||||||
|
assertEquals(0, partialMap.get("important"));
|
||||||
|
assertEquals(3, partialMap.get("urgent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("测试月度进出水量趋势数据结构")
|
||||||
|
void testMonthlyFlowTrendStructure() {
|
||||||
|
// 模拟月度趋势数据
|
||||||
|
List<Map<String, Object>> trend = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
Map<String, Object> month = new LinkedHashMap<>();
|
||||||
|
month.put("month", LocalDate.of(2026, i + 1, 1));
|
||||||
|
month.put("total_inflow", new BigDecimal(String.valueOf(400000 + i * 10000)));
|
||||||
|
month.put("total_outflow", new BigDecimal(String.valueOf(380000 + i * 9500)));
|
||||||
|
trend.add(month);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(6, trend.size());
|
||||||
|
assertEquals(LocalDate.of(2026, 1, 1), trend.get(0).get("month"));
|
||||||
|
assertEquals(new BigDecimal("400000"), trend.get(0).get("total_inflow"));
|
||||||
|
assertEquals(new BigDecimal("450000"), trend.get(5).get("total_inflow"));
|
||||||
|
|
||||||
|
// 验证趋势递增
|
||||||
|
for (int i = 1; i < trend.size(); i++) {
|
||||||
|
BigDecimal prev = (BigDecimal) trend.get(i - 1).get("total_inflow");
|
||||||
|
BigDecimal curr = (BigDecimal) trend.get(i).get("total_inflow");
|
||||||
|
assertTrue(curr.compareTo(prev) > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("测试能耗汇总按类型聚合逻辑")
|
||||||
|
void testEnergySummaryAggregation() {
|
||||||
|
// 模拟能耗数据
|
||||||
|
List<Map<String, Object>> items = new ArrayList<>();
|
||||||
|
items.add(Map.of("energy_type", "power", "total", new BigDecimal("1250.50"), "unit", "kWh"));
|
||||||
|
items.add(Map.of("energy_type", "coagulant", "total", new BigDecimal("45.00"), "unit", "kg"));
|
||||||
|
items.add(Map.of("energy_type", "disinfectant", "total", new BigDecimal("12.50"), "unit", "kg"));
|
||||||
|
|
||||||
|
BigDecimal totalPower = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalChemical = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (Map<String, Object> item : items) {
|
||||||
|
String type = (String) item.get("energy_type");
|
||||||
|
BigDecimal total = (BigDecimal) item.get("total");
|
||||||
|
if ("power".equals(type)) {
|
||||||
|
totalPower = total;
|
||||||
|
} else {
|
||||||
|
totalChemical = totalChemical.add(total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("1250.50"), totalPower);
|
||||||
|
assertEquals(new BigDecimal("57.50"), totalChemical);
|
||||||
|
|
||||||
|
// 验证总能耗
|
||||||
|
BigDecimal grandTotal = totalPower.add(totalChemical);
|
||||||
|
assertEquals(new BigDecimal("1308.00"), grandTotal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
package com.water.production.service;
|
||||||
|
|
||||||
|
import com.water.production.dto.MonitorExportRequest;
|
||||||
|
import com.water.production.dto.MonitorQueryRequest;
|
||||||
|
import com.water.production.entity.MonitorDevice;
|
||||||
|
import com.water.production.entity.MonitorRealtimeData;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线监测列表单元测试
|
||||||
|
* 覆盖实体、DTO、查询筛选逻辑、导出格式、状态判断等
|
||||||
|
*/
|
||||||
|
class MonitorListServiceTest {
|
||||||
|
|
||||||
|
// ========== 1. 实体构建测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("MonitorDevice 实体字段完整性")
|
||||||
|
void testMonitorDeviceEntity() {
|
||||||
|
MonitorDevice device = new MonitorDevice();
|
||||||
|
device.setId(1L);
|
||||||
|
device.setDeviceCode("MON-FLOW-001");
|
||||||
|
device.setDeviceName("一号泵站出口流量计");
|
||||||
|
device.setDeviceType("flow");
|
||||||
|
device.setArea("一体化水厂");
|
||||||
|
device.setLocation("一号泵站出口");
|
||||||
|
device.setLng(new BigDecimal("82.07123456"));
|
||||||
|
device.setLat(new BigDecimal("44.84567890"));
|
||||||
|
device.setStatus("online");
|
||||||
|
device.setLastReportTime(LocalDateTime.of(2026, 6, 14, 16, 0, 0));
|
||||||
|
device.setBrand("E+H Promag 50");
|
||||||
|
device.setInstallTime(LocalDateTime.of(2025, 1, 15, 10, 0, 0));
|
||||||
|
device.setRemark("正常运行");
|
||||||
|
|
||||||
|
assertEquals(1L, device.getId());
|
||||||
|
assertEquals("MON-FLOW-001", device.getDeviceCode());
|
||||||
|
assertEquals("flow", device.getDeviceType());
|
||||||
|
assertEquals("online", device.getStatus());
|
||||||
|
assertEquals(new BigDecimal("82.07123456"), device.getLng());
|
||||||
|
assertNotNull(device.getLastReportTime());
|
||||||
|
assertEquals("E+H Promag 50", device.getBrand());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("MonitorRealtimeData 实体字段完整性")
|
||||||
|
void testMonitorRealtimeDataEntity() {
|
||||||
|
MonitorRealtimeData data = new MonitorRealtimeData();
|
||||||
|
data.setId(1L);
|
||||||
|
data.setDeviceId(1L);
|
||||||
|
data.setDeviceCode("MON-FLOW-001");
|
||||||
|
data.setMetricKey("flow");
|
||||||
|
data.setMetricValue(new BigDecimal("125.5000"));
|
||||||
|
data.setUnit("m³/h");
|
||||||
|
data.setThresholdHigh(new BigDecimal("200.0000"));
|
||||||
|
data.setThresholdLow(new BigDecimal("10.0000"));
|
||||||
|
data.setIsAbnormal(0);
|
||||||
|
data.setCollectTime(LocalDateTime.of(2026, 6, 14, 15, 55, 0));
|
||||||
|
|
||||||
|
assertEquals("flow", data.getMetricKey());
|
||||||
|
assertEquals(new BigDecimal("125.5000"), data.getMetricValue());
|
||||||
|
assertEquals("m³/h", data.getUnit());
|
||||||
|
assertEquals(0, data.getIsAbnormal());
|
||||||
|
assertNotNull(data.getCollectTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 2. DTO 默认值测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("MonitorQueryRequest 默认分页参数")
|
||||||
|
void testQueryRequestDefaults() {
|
||||||
|
MonitorQueryRequest req = new MonitorQueryRequest();
|
||||||
|
assertEquals(1, req.getPageNum());
|
||||||
|
assertEquals(20, req.getPageSize());
|
||||||
|
assertNull(req.getArea());
|
||||||
|
assertNull(req.getDeviceType());
|
||||||
|
assertNull(req.getStatus());
|
||||||
|
assertNull(req.getSortField());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("MonitorExportRequest 默认格式为 excel")
|
||||||
|
void testExportRequestDefaults() {
|
||||||
|
MonitorExportRequest req = new MonitorExportRequest();
|
||||||
|
assertEquals("excel", req.getFormat());
|
||||||
|
assertTrue(req.getIncludeRealtime());
|
||||||
|
assertNull(req.getDeviceIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 3. 状态判断逻辑测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("设备状态格式化逻辑")
|
||||||
|
void testStatusFormatting() {
|
||||||
|
// 模拟 Controller 中的状态格式化
|
||||||
|
Map<String, String> statusMap = Map.of(
|
||||||
|
"online", "在线",
|
||||||
|
"offline", "离线",
|
||||||
|
"fault", "故障",
|
||||||
|
"abnormal", "数据异常"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("在线", statusMap.get("online"));
|
||||||
|
assertEquals("离线", statusMap.get("offline"));
|
||||||
|
assertEquals("故障", statusMap.get("fault"));
|
||||||
|
assertEquals("数据异常", statusMap.get("abnormal"));
|
||||||
|
assertNull(statusMap.get("unknown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("设备异常判定逻辑")
|
||||||
|
void testAbnormalDetection() {
|
||||||
|
// 模拟异常判定:值超出阈值范围
|
||||||
|
BigDecimal value = new BigDecimal("250.00");
|
||||||
|
BigDecimal high = new BigDecimal("200.00");
|
||||||
|
BigDecimal low = new BigDecimal("10.00");
|
||||||
|
|
||||||
|
boolean isAbnormal = (high != null && value.compareTo(high) > 0)
|
||||||
|
|| (low != null && value.compareTo(low) < 0);
|
||||||
|
assertTrue(isAbnormal, "超出上限应判定为异常");
|
||||||
|
|
||||||
|
// 正常范围
|
||||||
|
value = new BigDecimal("125.50");
|
||||||
|
isAbnormal = (high != null && value.compareTo(high) > 0)
|
||||||
|
|| (low != null && value.compareTo(low) < 0);
|
||||||
|
assertFalse(isAbnormal, "正常范围不应判定为异常");
|
||||||
|
|
||||||
|
// 低于下限
|
||||||
|
value = new BigDecimal("5.00");
|
||||||
|
isAbnormal = (high != null && value.compareTo(high) > 0)
|
||||||
|
|| (low != null && value.compareTo(low) < 0);
|
||||||
|
assertTrue(isAbnormal, "低于下限应判定为异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 4. 筛选逻辑测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("多维度筛选条件构建")
|
||||||
|
void testMultiDimensionFilter() {
|
||||||
|
// 模拟设备列表
|
||||||
|
List<MonitorDevice> devices = buildMockDevices();
|
||||||
|
|
||||||
|
// 按区域筛选
|
||||||
|
List<MonitorDevice> filtered = devices.stream()
|
||||||
|
.filter(d -> "一体化水厂".equals(d.getArea()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(3, filtered.size());
|
||||||
|
|
||||||
|
// 按设备类型筛选
|
||||||
|
filtered = devices.stream()
|
||||||
|
.filter(d -> "pressure".equals(d.getDeviceType()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(2, filtered.size());
|
||||||
|
|
||||||
|
// 按状态筛选
|
||||||
|
filtered = devices.stream()
|
||||||
|
.filter(d -> "online".equals(d.getStatus()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(3, filtered.size());
|
||||||
|
|
||||||
|
// 组合筛选:区域 + 类型
|
||||||
|
filtered = devices.stream()
|
||||||
|
.filter(d -> "一体化水厂".equals(d.getArea()) && "flow".equals(d.getDeviceType()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(2, filtered.size());
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
String keyword = "流量计";
|
||||||
|
filtered = devices.stream()
|
||||||
|
.filter(d -> (d.getDeviceName() != null && d.getDeviceName().contains(keyword))
|
||||||
|
|| (d.getDeviceCode() != null && d.getDeviceCode().contains(keyword)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(2, filtered.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("排序逻辑测试")
|
||||||
|
void testSortLogic() {
|
||||||
|
List<MonitorDevice> devices = buildMockDevices();
|
||||||
|
|
||||||
|
// 按设备编号升序
|
||||||
|
devices.sort(Comparator.comparing(MonitorDevice::getDeviceCode));
|
||||||
|
assertEquals("MON-FLOW-001", devices.get(0).getDeviceCode());
|
||||||
|
|
||||||
|
// 按设备编号降序
|
||||||
|
devices.sort(Comparator.comparing(MonitorDevice::getDeviceCode).reversed());
|
||||||
|
assertEquals("MON-QUAL-001", devices.get(0).getDeviceCode());
|
||||||
|
|
||||||
|
// 按最后上报时间降序(最新在前)
|
||||||
|
devices.sort(Comparator.comparing(MonitorDevice::getLastReportTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
|
assertNotNull(devices.get(0).getLastReportTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 5. 统计逻辑测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("设备统计聚合逻辑")
|
||||||
|
void testDeviceStatistics() {
|
||||||
|
List<MonitorDevice> devices = buildMockDevices();
|
||||||
|
|
||||||
|
// 按状态统计
|
||||||
|
Map<String, Long> statusCount = devices.stream()
|
||||||
|
.collect(Collectors.groupingBy(MonitorDevice::getStatus, Collectors.counting()));
|
||||||
|
assertEquals(3L, statusCount.get("online"));
|
||||||
|
assertEquals(1L, statusCount.get("offline"));
|
||||||
|
assertEquals(1L, statusCount.get("fault"));
|
||||||
|
|
||||||
|
// 按类型统计
|
||||||
|
Map<String, Long> typeCount = devices.stream()
|
||||||
|
.collect(Collectors.groupingBy(MonitorDevice::getDeviceType, Collectors.counting()));
|
||||||
|
assertEquals(2L, typeCount.get("flow"));
|
||||||
|
assertEquals(2L, typeCount.get("pressure"));
|
||||||
|
assertEquals(1L, typeCount.get("quality"));
|
||||||
|
|
||||||
|
// 按区域统计
|
||||||
|
Map<String, Long> areaCount = devices.stream()
|
||||||
|
.collect(Collectors.groupingBy(MonitorDevice::getArea, Collectors.counting()));
|
||||||
|
assertEquals(3L, areaCount.get("一体化水厂"));
|
||||||
|
assertEquals(2L, areaCount.get("管网一区"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 6. 分页计算测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("分页参数计算")
|
||||||
|
void testPagination() {
|
||||||
|
int total = 50;
|
||||||
|
int pageSize = 20;
|
||||||
|
|
||||||
|
int pages = (int) Math.ceil((double) total / pageSize);
|
||||||
|
assertEquals(3, pages);
|
||||||
|
|
||||||
|
// 第1页偏移量
|
||||||
|
int offset = (1 - 1) * pageSize;
|
||||||
|
assertEquals(0, offset);
|
||||||
|
|
||||||
|
// 第3页偏移量
|
||||||
|
offset = (3 - 1) * pageSize;
|
||||||
|
assertEquals(40, offset);
|
||||||
|
|
||||||
|
// 整除情况
|
||||||
|
total = 40;
|
||||||
|
pages = (int) Math.ceil((double) total / pageSize);
|
||||||
|
assertEquals(2, pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 7. CSV 转义测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("CSV 字段转义逻辑")
|
||||||
|
void testCsvEscaping() {
|
||||||
|
// 包含逗号
|
||||||
|
String value = "一号泵站,出口";
|
||||||
|
String escaped = csvEsc(value);
|
||||||
|
assertEquals("\"一号泵站,出口\"", escaped);
|
||||||
|
|
||||||
|
// 包含引号
|
||||||
|
value = "含\"引号\"的文本";
|
||||||
|
escaped = csvEsc(value);
|
||||||
|
assertEquals("\"含\"\"引号\"\"的文本\"", escaped);
|
||||||
|
|
||||||
|
// 普通文本
|
||||||
|
value = "正常文本";
|
||||||
|
escaped = csvEsc(value);
|
||||||
|
assertEquals("正常文本", escaped);
|
||||||
|
|
||||||
|
// null
|
||||||
|
escaped = csvEsc(null);
|
||||||
|
assertEquals("", escaped);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 8. 实时数据结构测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("多参数实时数据结构完整性")
|
||||||
|
void testRealtimeDataStructure() {
|
||||||
|
// 模拟多参数实时数据
|
||||||
|
List<String> expectedMetrics = List.of("flow", "pressure", "level", "turbidity", "ph", "residual_chlorine");
|
||||||
|
Map<String, MonitorRealtimeData> metricMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (String metric : expectedMetrics) {
|
||||||
|
MonitorRealtimeData data = new MonitorRealtimeData();
|
||||||
|
data.setDeviceId(1L);
|
||||||
|
data.setMetricKey(metric);
|
||||||
|
data.setMetricValue(new BigDecimal("100.00"));
|
||||||
|
data.setUnit("unit_" + metric);
|
||||||
|
data.setCollectTime(LocalDateTime.now());
|
||||||
|
metricMap.put(metric, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(6, metricMap.size());
|
||||||
|
assertTrue(metricMap.containsKey("flow"));
|
||||||
|
assertTrue(metricMap.containsKey("pressure"));
|
||||||
|
assertTrue(metricMap.containsKey("level"));
|
||||||
|
assertTrue(metricMap.containsKey("turbidity"));
|
||||||
|
assertTrue(metricMap.containsKey("ph"));
|
||||||
|
assertTrue(metricMap.containsKey("residual_chlorine"));
|
||||||
|
|
||||||
|
// 验证各参数值
|
||||||
|
for (String metric : expectedMetrics) {
|
||||||
|
assertNotNull(metricMap.get(metric).getMetricValue());
|
||||||
|
assertEquals(metric, metricMap.get(metric).getMetricKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 9. 设备在线率计算 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("设备在线率计算")
|
||||||
|
void testOnlineRateCalculation() {
|
||||||
|
List<MonitorDevice> devices = buildMockDevices();
|
||||||
|
int total = devices.size();
|
||||||
|
long onlineCount = devices.stream().filter(d -> "online".equals(d.getStatus())).count();
|
||||||
|
|
||||||
|
BigDecimal onlineRate = total > 0
|
||||||
|
? BigDecimal.valueOf(onlineCount * 100.0 / total).setScale(1, RoundingMode.HALF_UP)
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
|
||||||
|
assertEquals(5, total);
|
||||||
|
assertEquals(3, onlineCount);
|
||||||
|
assertEquals(new BigDecimal("60.0"), onlineRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 10. 导出数据量限制测试 ==========
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("导出最大数据量限制")
|
||||||
|
void testExportMaxLimit() {
|
||||||
|
MonitorExportRequest req = new MonitorExportRequest();
|
||||||
|
int maxExportLimit = 10000;
|
||||||
|
|
||||||
|
// 模拟大量设备
|
||||||
|
List<MonitorDevice> devices = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 15000; i++) {
|
||||||
|
MonitorDevice d = new MonitorDevice();
|
||||||
|
d.setId((long) (i + 1));
|
||||||
|
d.setDeviceCode("MON-" + String.format("%05d", i));
|
||||||
|
devices.add(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出应限制在 maxExportLimit 内
|
||||||
|
List<MonitorDevice> exportList = devices.size() > maxExportLimit
|
||||||
|
? devices.subList(0, maxExportLimit)
|
||||||
|
: devices;
|
||||||
|
|
||||||
|
assertEquals(maxExportLimit, exportList.size());
|
||||||
|
assertTrue(devices.size() > maxExportLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Helper Methods ==========
|
||||||
|
|
||||||
|
private List<MonitorDevice> buildMockDevices() {
|
||||||
|
List<MonitorDevice> devices = new ArrayList<>();
|
||||||
|
|
||||||
|
MonitorDevice d1 = new MonitorDevice();
|
||||||
|
d1.setId(1L);
|
||||||
|
d1.setDeviceCode("MON-FLOW-001");
|
||||||
|
d1.setDeviceName("一号泵站出口流量计");
|
||||||
|
d1.setDeviceType("flow");
|
||||||
|
d1.setArea("一体化水厂");
|
||||||
|
d1.setStatus("online");
|
||||||
|
d1.setLastReportTime(LocalDateTime.of(2026, 6, 14, 16, 0, 0));
|
||||||
|
devices.add(d1);
|
||||||
|
|
||||||
|
MonitorDevice d2 = new MonitorDevice();
|
||||||
|
d2.setId(2L);
|
||||||
|
d2.setDeviceCode("MON-FLOW-002");
|
||||||
|
d2.setDeviceName("二号泵站流量计");
|
||||||
|
d2.setDeviceType("flow");
|
||||||
|
d2.setArea("一体化水厂");
|
||||||
|
d2.setStatus("online");
|
||||||
|
d2.setLastReportTime(LocalDateTime.of(2026, 6, 14, 15, 50, 0));
|
||||||
|
devices.add(d2);
|
||||||
|
|
||||||
|
MonitorDevice d3 = new MonitorDevice();
|
||||||
|
d3.setId(3L);
|
||||||
|
d3.setDeviceCode("MON-PRES-001");
|
||||||
|
d3.setDeviceName("管网压力监测点A");
|
||||||
|
d3.setDeviceType("pressure");
|
||||||
|
d3.setArea("管网一区");
|
||||||
|
d3.setStatus("online");
|
||||||
|
d3.setLastReportTime(LocalDateTime.of(2026, 6, 14, 15, 55, 0));
|
||||||
|
devices.add(d3);
|
||||||
|
|
||||||
|
MonitorDevice d4 = new MonitorDevice();
|
||||||
|
d4.setId(4L);
|
||||||
|
d4.setDeviceCode("MON-PRES-002");
|
||||||
|
d4.setDeviceName("管网压力监测点B");
|
||||||
|
d4.setDeviceType("pressure");
|
||||||
|
d4.setArea("管网一区");
|
||||||
|
d4.setStatus("offline");
|
||||||
|
d4.setLastReportTime(LocalDateTime.of(2026, 6, 14, 14, 0, 0));
|
||||||
|
devices.add(d4);
|
||||||
|
|
||||||
|
MonitorDevice d5 = new MonitorDevice();
|
||||||
|
d5.setId(5L);
|
||||||
|
d5.setDeviceCode("MON-QUAL-001");
|
||||||
|
d5.setDeviceName("出厂水质监测仪");
|
||||||
|
d5.setDeviceType("quality");
|
||||||
|
d5.setArea("一体化水厂");
|
||||||
|
d5.setStatus("fault");
|
||||||
|
d5.setLastReportTime(LocalDateTime.of(2026, 6, 14, 15, 30, 0));
|
||||||
|
devices.add(d5);
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String csvEsc(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
|
||||||
|
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user