From f240059d1e214549e0afa39c3888fc04ce1f8c63 Mon Sep 17 00:00:00 2001 From: bot_dev2 Date: Sun, 14 Jun 2026 16:06:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(wm-dma):=20#59=20DMA=E5=88=86=E5=8C=BA?= =?UTF-8?q?=E8=AE=A1=E9=87=8F=E4=B8=8E=E6=BC=8F=E6=8D=9F=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 wm-dma 模块 - Entity: DmaZone, DmaMeter, DmaFlowRecord, DmaLeakageAnalysis, WaterBalance - Mapper + Service + Controller(18个端点) - DMA分区管理: 分区层级定义(CRUD) + 区域划分 + 关联设备 + 树形结构 - 分区计量: 各分区进出水量汇总 + 最小夜间流量(MNF)分析 + 流量趋势 - 漏损分析: 产销差计算 + 漏损率评估 + 趋势分析 + 报警 - 水平衡表: 供水量/售水量/漏损量平衡分析 + IWA组成 - DDL: dma_ddl.sql - 单元测试: 5个Service测试类(25+测试用例) --- db/dma_ddl.sql | 136 +++++++++++++ pom.xml | 1 + wm-dma/pom.xml | 80 ++++++++ .../java/com/water/dma/DmaApplication.java | 15 ++ .../water/dma/config/MyBatisPlusConfig.java | 33 ++++ .../dma/controller/DmaFlowController.java | 79 ++++++++ .../dma/controller/DmaLeakageController.java | 70 +++++++ .../dma/controller/DmaMeterController.java | 74 ++++++++ .../dma/controller/DmaZoneController.java | 73 +++++++ .../controller/WaterBalanceController.java | 75 ++++++++ .../com/water/dma/entity/DmaFlowRecord.java | 39 ++++ .../water/dma/entity/DmaLeakageAnalysis.java | 57 ++++++ .../java/com/water/dma/entity/DmaMeter.java | 50 +++++ .../java/com/water/dma/entity/DmaZone.java | 47 +++++ .../com/water/dma/entity/WaterBalance.java | 63 ++++++ .../water/dma/mapper/DmaFlowRecordMapper.java | 28 +++ .../dma/mapper/DmaLeakageAnalysisMapper.java | 12 ++ .../com/water/dma/mapper/DmaMeterMapper.java | 12 ++ .../com/water/dma/mapper/DmaZoneMapper.java | 12 ++ .../water/dma/mapper/WaterBalanceMapper.java | 12 ++ .../com/water/dma/service/DmaFlowService.java | 137 ++++++++++++++ .../water/dma/service/DmaLeakageService.java | 179 ++++++++++++++++++ .../water/dma/service/DmaMeterService.java | 99 ++++++++++ .../com/water/dma/service/DmaZoneService.java | 122 ++++++++++++ .../dma/service/WaterBalanceService.java | 168 ++++++++++++++++ wm-dma/src/main/resources/application.yml | 29 +++ .../water/dma/service/DmaFlowServiceTest.java | 130 +++++++++++++ .../dma/service/DmaLeakageServiceTest.java | 124 ++++++++++++ .../dma/service/DmaMeterServiceTest.java | 100 ++++++++++ .../water/dma/service/DmaZoneServiceTest.java | 112 +++++++++++ .../dma/service/WaterBalanceServiceTest.java | 117 ++++++++++++ 31 files changed, 2285 insertions(+) create mode 100644 db/dma_ddl.sql create mode 100644 wm-dma/pom.xml create mode 100644 wm-dma/src/main/java/com/water/dma/DmaApplication.java create mode 100644 wm-dma/src/main/java/com/water/dma/config/MyBatisPlusConfig.java create mode 100644 wm-dma/src/main/java/com/water/dma/controller/DmaFlowController.java create mode 100644 wm-dma/src/main/java/com/water/dma/controller/DmaLeakageController.java create mode 100644 wm-dma/src/main/java/com/water/dma/controller/DmaMeterController.java create mode 100644 wm-dma/src/main/java/com/water/dma/controller/DmaZoneController.java create mode 100644 wm-dma/src/main/java/com/water/dma/controller/WaterBalanceController.java create mode 100644 wm-dma/src/main/java/com/water/dma/entity/DmaFlowRecord.java create mode 100644 wm-dma/src/main/java/com/water/dma/entity/DmaLeakageAnalysis.java create mode 100644 wm-dma/src/main/java/com/water/dma/entity/DmaMeter.java create mode 100644 wm-dma/src/main/java/com/water/dma/entity/DmaZone.java create mode 100644 wm-dma/src/main/java/com/water/dma/entity/WaterBalance.java create mode 100644 wm-dma/src/main/java/com/water/dma/mapper/DmaFlowRecordMapper.java create mode 100644 wm-dma/src/main/java/com/water/dma/mapper/DmaLeakageAnalysisMapper.java create mode 100644 wm-dma/src/main/java/com/water/dma/mapper/DmaMeterMapper.java create mode 100644 wm-dma/src/main/java/com/water/dma/mapper/DmaZoneMapper.java create mode 100644 wm-dma/src/main/java/com/water/dma/mapper/WaterBalanceMapper.java create mode 100644 wm-dma/src/main/java/com/water/dma/service/DmaFlowService.java create mode 100644 wm-dma/src/main/java/com/water/dma/service/DmaLeakageService.java create mode 100644 wm-dma/src/main/java/com/water/dma/service/DmaMeterService.java create mode 100644 wm-dma/src/main/java/com/water/dma/service/DmaZoneService.java create mode 100644 wm-dma/src/main/java/com/water/dma/service/WaterBalanceService.java create mode 100644 wm-dma/src/main/resources/application.yml create mode 100644 wm-dma/src/test/java/com/water/dma/service/DmaFlowServiceTest.java create mode 100644 wm-dma/src/test/java/com/water/dma/service/DmaLeakageServiceTest.java create mode 100644 wm-dma/src/test/java/com/water/dma/service/DmaMeterServiceTest.java create mode 100644 wm-dma/src/test/java/com/water/dma/service/DmaZoneServiceTest.java create mode 100644 wm-dma/src/test/java/com/water/dma/service/WaterBalanceServiceTest.java diff --git a/db/dma_ddl.sql b/db/dma_ddl.sql new file mode 100644 index 00000000..bb7c1326 --- /dev/null +++ b/db/dma_ddl.sql @@ -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); diff --git a/pom.xml b/pom.xml index 2cb55ca7..0163513f 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,7 @@ wm-system wm-mobile-app wm-config + wm-dma diff --git a/wm-dma/pom.xml b/wm-dma/pom.xml new file mode 100644 index 00000000..71fef7a5 --- /dev/null +++ b/wm-dma/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + com.water + wm-parent + 1.0.0-SNAPSHOT + + wm-dma + wm-dma + DMA分区计量与漏损分析模块 + + + + + com.water + wm-common + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.postgresql + postgresql + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + + cn.hutool + hutool-all + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/wm-dma/src/main/java/com/water/dma/DmaApplication.java b/wm-dma/src/main/java/com/water/dma/DmaApplication.java new file mode 100644 index 00000000..9d4389cf --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/DmaApplication.java @@ -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); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/config/MyBatisPlusConfig.java b/wm-dma/src/main/java/com/water/dma/config/MyBatisPlusConfig.java new file mode 100644 index 00000000..6cd64aa1 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/config/MyBatisPlusConfig.java @@ -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); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/controller/DmaFlowController.java b/wm-dma/src/main/java/com/water/dma/controller/DmaFlowController.java new file mode 100644 index 00000000..7f11a15e --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/controller/DmaFlowController.java @@ -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( + @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 create(@RequestBody DmaFlowRecord record) { + return R.ok(flowService.create(record)); + } + + @Operation(summary = "批量创建流量记录") + @PostMapping("/batch") + public R batchCreate(@RequestBody List records) { + flowService.batchCreate(records); + return R.ok("批量创建成功"); + } + + @Operation(summary = "获取分区进出水量汇总") + @GetMapping("/summary/{zoneId}") + public R> 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> 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>> 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)); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/controller/DmaLeakageController.java b/wm-dma/src/main/java/com/water/dma/controller/DmaLeakageController.java new file mode 100644 index 00000000..57bc1890 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/controller/DmaLeakageController.java @@ -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( + @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 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>> getTrend( + @PathVariable Long zoneId, + @RequestParam(defaultValue = "30") int days) { + return R.ok(leakageService.getTrend(zoneId, days)); + } + + @Operation(summary = "获取报警列表") + @GetMapping("/alarms") + public R> getAlarms( + @RequestParam(required = false) String alarmLevel) { + return R.ok(leakageService.getAlarms(alarmLevel)); + } + + @Operation(summary = "获取分区漏损汇总") + @GetMapping("/summary/{zoneId}") + public R> getZoneSummary(@PathVariable Long zoneId) { + return R.ok(leakageService.getZoneSummary(zoneId)); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/controller/DmaMeterController.java b/wm-dma/src/main/java/com/water/dma/controller/DmaMeterController.java new file mode 100644 index 00000000..26c73468 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/controller/DmaMeterController.java @@ -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( + @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 getById(@PathVariable Long id) { + return R.ok(meterService.getById(id)); + } + + @Operation(summary = "创建计量表") + @PostMapping + public R create(@RequestBody DmaMeter meter) { + return R.ok(meterService.create(meter)); + } + + @Operation(summary = "更新计量表") + @PutMapping("/{id}") + public R update(@PathVariable Long id, @RequestBody DmaMeter meter) { + meter.setId(id); + meterService.update(meter); + return R.ok("更新成功"); + } + + @Operation(summary = "删除计量表") + @DeleteMapping("/{id}") + public R delete(@PathVariable Long id) { + meterService.delete(id); + return R.ok("删除成功"); + } + + @Operation(summary = "获取分区下的计量表") + @GetMapping("/zone/{zoneId}") + public R> listByZoneId(@PathVariable Long zoneId) { + return R.ok(meterService.listByZoneId(zoneId)); + } + + @Operation(summary = "绑定计量表到分区") + @PostMapping("/{meterId}/bind/{zoneId}") + public R bindToZone(@PathVariable Long meterId, @PathVariable Long zoneId) { + meterService.bindToZone(meterId, zoneId); + return R.ok("绑定成功"); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/controller/DmaZoneController.java b/wm-dma/src/main/java/com/water/dma/controller/DmaZoneController.java new file mode 100644 index 00000000..7cf075fc --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/controller/DmaZoneController.java @@ -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( + @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 getById(@PathVariable Long id) { + return R.ok(zoneService.getById(id)); + } + + @Operation(summary = "创建分区") + @PostMapping + public R create(@RequestBody DmaZone zone) { + return R.ok(zoneService.create(zone)); + } + + @Operation(summary = "更新分区") + @PutMapping("/{id}") + public R update(@PathVariable Long id, @RequestBody DmaZone zone) { + zone.setId(id); + zoneService.update(zone); + return R.ok("更新成功"); + } + + @Operation(summary = "删除分区") + @DeleteMapping("/{id}") + public R delete(@PathVariable Long id) { + zoneService.delete(id); + return R.ok("删除成功"); + } + + @Operation(summary = "获取分区树") + @GetMapping("/tree") + public R>> getZoneTree() { + return R.ok(zoneService.getZoneTree()); + } + + @Operation(summary = "获取所有分区") + @GetMapping("/list") + public R> listAll() { + return R.ok(zoneService.listAll()); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/controller/WaterBalanceController.java b/wm-dma/src/main/java/com/water/dma/controller/WaterBalanceController.java new file mode 100644 index 00000000..752c6d53 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/controller/WaterBalanceController.java @@ -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( + @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 getById(@PathVariable Long id) { + return R.ok(balanceService.getById(id)); + } + + @Operation(summary = "创建水平衡记录") + @PostMapping + public R create(@RequestBody WaterBalance balance) { + return R.ok(balanceService.create(balance)); + } + + @Operation(summary = "更新水平衡记录") + @PutMapping("/{id}") + public R update(@PathVariable Long id, @RequestBody WaterBalance balance) { + balance.setId(id); + balanceService.update(balance); + return R.ok("更新成功"); + } + + @Operation(summary = "删除水平衡记录") + @DeleteMapping("/{id}") + public R delete(@PathVariable Long id) { + balanceService.delete(id); + return R.ok("删除成功"); + } + + @Operation(summary = "生成水平衡分析报告") + @GetMapping("/report/{zoneId}") + public R> 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)); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/entity/DmaFlowRecord.java b/wm-dma/src/main/java/com/water/dma/entity/DmaFlowRecord.java new file mode 100644 index 00000000..6a76409c --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/entity/DmaFlowRecord.java @@ -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; +} diff --git a/wm-dma/src/main/java/com/water/dma/entity/DmaLeakageAnalysis.java b/wm-dma/src/main/java/com/water/dma/entity/DmaLeakageAnalysis.java new file mode 100644 index 00000000..e3f504b7 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/entity/DmaLeakageAnalysis.java @@ -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; +} diff --git a/wm-dma/src/main/java/com/water/dma/entity/DmaMeter.java b/wm-dma/src/main/java/com/water/dma/entity/DmaMeter.java new file mode 100644 index 00000000..33669387 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/entity/DmaMeter.java @@ -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; +} diff --git a/wm-dma/src/main/java/com/water/dma/entity/DmaZone.java b/wm-dma/src/main/java/com/water/dma/entity/DmaZone.java new file mode 100644 index 00000000..b5b0e7e7 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/entity/DmaZone.java @@ -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; +} diff --git a/wm-dma/src/main/java/com/water/dma/entity/WaterBalance.java b/wm-dma/src/main/java/com/water/dma/entity/WaterBalance.java new file mode 100644 index 00000000..39f82a74 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/entity/WaterBalance.java @@ -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; +} diff --git a/wm-dma/src/main/java/com/water/dma/mapper/DmaFlowRecordMapper.java b/wm-dma/src/main/java/com/water/dma/mapper/DmaFlowRecordMapper.java new file mode 100644 index 00000000..3308034b --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/mapper/DmaFlowRecordMapper.java @@ -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 { + + @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> 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); +} diff --git a/wm-dma/src/main/java/com/water/dma/mapper/DmaLeakageAnalysisMapper.java b/wm-dma/src/main/java/com/water/dma/mapper/DmaLeakageAnalysisMapper.java new file mode 100644 index 00000000..c9c529e2 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/mapper/DmaLeakageAnalysisMapper.java @@ -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 { +} diff --git a/wm-dma/src/main/java/com/water/dma/mapper/DmaMeterMapper.java b/wm-dma/src/main/java/com/water/dma/mapper/DmaMeterMapper.java new file mode 100644 index 00000000..88d9e92f --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/mapper/DmaMeterMapper.java @@ -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 { +} diff --git a/wm-dma/src/main/java/com/water/dma/mapper/DmaZoneMapper.java b/wm-dma/src/main/java/com/water/dma/mapper/DmaZoneMapper.java new file mode 100644 index 00000000..4a5474b5 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/mapper/DmaZoneMapper.java @@ -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 { +} diff --git a/wm-dma/src/main/java/com/water/dma/mapper/WaterBalanceMapper.java b/wm-dma/src/main/java/com/water/dma/mapper/WaterBalanceMapper.java new file mode 100644 index 00000000..9b8a901a --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/mapper/WaterBalanceMapper.java @@ -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 { +} diff --git a/wm-dma/src/main/java/com/water/dma/service/DmaFlowService.java b/wm-dma/src/main/java/com/water/dma/service/DmaFlowService.java new file mode 100644 index 00000000..fc4a83ba --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/service/DmaFlowService.java @@ -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 page(int pageNum, int pageSize, Long zoneId, Long meterId, + LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper 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 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 getZoneFlowSummary(Long zoneId, LocalDateTime startTime, LocalDateTime endTime) { + List> flowSums = flowRecordMapper.sumFlowByMeter(zoneId, startTime, endTime); + + Map result = new LinkedHashMap<>(); + BigDecimal totalInflow = BigDecimal.ZERO; + BigDecimal totalOutflow = BigDecimal.ZERO; + + for (Map 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 getMNFAnalysis(Long zoneId, LocalDate date) { + BigDecimal mnf = flowRecordMapper.getMNF(zoneId, date); + + Map 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> getFlowTrend(Long zoneId, LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DmaFlowRecord::getZoneId, zoneId); + wrapper.ge(DmaFlowRecord::getCollectTime, startTime); + wrapper.le(DmaFlowRecord::getCollectTime, endTime); + wrapper.orderByAsc(DmaFlowRecord::getCollectTime); + + List records = flowRecordMapper.selectList(wrapper); + + List> trend = new ArrayList<>(); + for (DmaFlowRecord record : records) { + Map 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; + } +} diff --git a/wm-dma/src/main/java/com/water/dma/service/DmaLeakageService.java b/wm-dma/src/main/java/com/water/dma/service/DmaLeakageService.java new file mode 100644 index 00000000..dfd57fd1 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/service/DmaLeakageService.java @@ -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 page(int pageNum, int pageSize, Long zoneId, + LocalDate startDate, LocalDate endDate) { + LambdaQueryWrapper 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> getTrend(Long zoneId, int days) { + LocalDate endDate = LocalDate.now(); + LocalDate startDate = endDate.minusDays(days); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DmaLeakageAnalysis::getZoneId, zoneId); + wrapper.ge(DmaLeakageAnalysis::getAnalysisDate, startDate); + wrapper.le(DmaLeakageAnalysis::getAnalysisDate, endDate); + wrapper.orderByAsc(DmaLeakageAnalysis::getAnalysisDate); + + List analyses = leakageMapper.selectList(wrapper); + + List> trend = new ArrayList<>(); + for (DmaLeakageAnalysis a : analyses) { + Map 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 getAlarms(String alarmLevel) { + LambdaQueryWrapper 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 getZoneSummary(Long zoneId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DmaLeakageAnalysis::getZoneId, zoneId); + wrapper.orderByDesc(DmaLeakageAnalysis::getAnalysisDate); + wrapper.last("LIMIT 30"); + + List recent = leakageMapper.selectList(wrapper); + + Map 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; + } +} diff --git a/wm-dma/src/main/java/com/water/dma/service/DmaMeterService.java b/wm-dma/src/main/java/com/water/dma/service/DmaMeterService.java new file mode 100644 index 00000000..241ab2b8 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/service/DmaMeterService.java @@ -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 page(int pageNum, int pageSize, Long zoneId, String meterType) { + LambdaQueryWrapper 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 listByZoneId(Long zoneId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DmaMeter::getZoneId, zoneId); + return meterMapper.selectList(wrapper); + } + + /** + * 统计分区表计数量 + */ + public Long countByZoneId(Long zoneId) { + LambdaQueryWrapper 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); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/service/DmaZoneService.java b/wm-dma/src/main/java/com/water/dma/service/DmaZoneService.java new file mode 100644 index 00000000..a0ed6974 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/service/DmaZoneService.java @@ -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 page(int pageNum, int pageSize, String zoneName) { + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DmaZone::getParentId, id); + Long count = zoneMapper.selectCount(wrapper); + if (count > 0) { + throw new RuntimeException("该分区存在子分区,无法删除"); + } + zoneMapper.deleteById(id); + } + + /** + * 获取分区树形结构 + */ + public List> getZoneTree() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByAsc(DmaZone::getZoneLevel, DmaZone::getZoneCode); + List allZones = zoneMapper.selectList(wrapper); + + Map> zoneMap = new LinkedHashMap<>(); + List> roots = new ArrayList<>(); + + for (DmaZone zone : allZones) { + Map 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> entry : zoneMap.entrySet()) { + Map node = entry.getValue(); + Long parentId = (Long) node.get("parentId"); + if (parentId == null) { + roots.add(node); + } else { + Map parent = zoneMap.get(parentId); + if (parent != null) { + @SuppressWarnings("unchecked") + List> children = (List>) parent.get("children"); + children.add(node); + } + } + } + + return roots; + } + + /** + * 获取所有分区列表 + */ + public List listAll() { + return zoneMapper.selectList(new LambdaQueryWrapper<>()); + } +} diff --git a/wm-dma/src/main/java/com/water/dma/service/WaterBalanceService.java b/wm-dma/src/main/java/com/water/dma/service/WaterBalanceService.java new file mode 100644 index 00000000..950f68e0 --- /dev/null +++ b/wm-dma/src/main/java/com/water/dma/service/WaterBalanceService.java @@ -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 page(int pageNum, int pageSize, Long zoneId, String period, + LocalDate startDate, LocalDate endDate) { + LambdaQueryWrapper 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 generateReport(Long zoneId, String period, LocalDate startDate, LocalDate endDate) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(WaterBalance::getZoneId, zoneId); + wrapper.eq(WaterBalance::getPeriod, period); + wrapper.ge(WaterBalance::getStatDate, startDate); + wrapper.le(WaterBalance::getStatDate, endDate); + + List records = balanceMapper.selectList(wrapper); + + Map 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 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); + } +} diff --git a/wm-dma/src/main/resources/application.yml b/wm-dma/src/main/resources/application.yml new file mode 100644 index 00000000..a8af5bab --- /dev/null +++ b/wm-dma/src/main/resources/application.yml @@ -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 diff --git a/wm-dma/src/test/java/com/water/dma/service/DmaFlowServiceTest.java b/wm-dma/src/test/java/com/water/dma/service/DmaFlowServiceTest.java new file mode 100644 index 00000000..99a6528a --- /dev/null +++ b/wm-dma/src/test/java/com/water/dma/service/DmaFlowServiceTest.java @@ -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 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 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 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 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> trend = flowService.getFlowTrend(1L, + LocalDateTime.now().minusHours(24), LocalDateTime.now()); + + assertEquals(1, trend.size()); + } +} diff --git a/wm-dma/src/test/java/com/water/dma/service/DmaLeakageServiceTest.java b/wm-dma/src/test/java/com/water/dma/service/DmaLeakageServiceTest.java new file mode 100644 index 00000000..a11a8738 --- /dev/null +++ b/wm-dma/src/test/java/com/water/dma/service/DmaLeakageServiceTest.java @@ -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> 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 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 alarms = leakageService.getAlarms(null); + assertFalse(alarms.isEmpty()); + } +} diff --git a/wm-dma/src/test/java/com/water/dma/service/DmaMeterServiceTest.java b/wm-dma/src/test/java/com/water/dma/service/DmaMeterServiceTest.java new file mode 100644 index 00000000..8c21cab4 --- /dev/null +++ b/wm-dma/src/test/java/com/water/dma/service/DmaMeterServiceTest.java @@ -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 meters = meterService.listByZoneId(1L); + assertEquals(1, meters.size()); + } +} diff --git a/wm-dma/src/test/java/com/water/dma/service/DmaZoneServiceTest.java b/wm-dma/src/test/java/com/water/dma/service/DmaZoneServiceTest.java new file mode 100644 index 00000000..2b20e3da --- /dev/null +++ b/wm-dma/src/test/java/com/water/dma/service/DmaZoneServiceTest.java @@ -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> 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 zones = zoneService.listAll(); + assertEquals(1, zones.size()); + } +} diff --git a/wm-dma/src/test/java/com/water/dma/service/WaterBalanceServiceTest.java b/wm-dma/src/test/java/com/water/dma/service/WaterBalanceServiceTest.java new file mode 100644 index 00000000..f0f88d31 --- /dev/null +++ b/wm-dma/src/test/java/com/water/dma/service/WaterBalanceServiceTest.java @@ -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 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 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); + } +}