feat(wm-dma): #59 DMA分区计量与漏损分析
- 新增 wm-dma 模块 - Entity: DmaZone, DmaMeter, DmaFlowRecord, DmaLeakageAnalysis, WaterBalance - Mapper + Service + Controller(18个端点) - DMA分区管理: 分区层级定义(CRUD) + 区域划分 + 关联设备 + 树形结构 - 分区计量: 各分区进出水量汇总 + 最小夜间流量(MNF)分析 + 流量趋势 - 漏损分析: 产销差计算 + 漏损率评估 + 趋势分析 + 报警 - 水平衡表: 供水量/售水量/漏损量平衡分析 + IWA组成 - DDL: dma_ddl.sql - 单元测试: 5个Service测试类(25+测试用例)
This commit is contained in:
+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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user