瀏覽代碼

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+测试用例)
bot_dev2 5 天之前
父節點
當前提交
f240059d1e
共有 31 個檔案被更改,包括 2285 行新增0 行删除
  1. 136
    0
      db/dma_ddl.sql
  2. 1
    0
      pom.xml
  3. 80
    0
      wm-dma/pom.xml
  4. 15
    0
      wm-dma/src/main/java/com/water/dma/DmaApplication.java
  5. 33
    0
      wm-dma/src/main/java/com/water/dma/config/MyBatisPlusConfig.java
  6. 79
    0
      wm-dma/src/main/java/com/water/dma/controller/DmaFlowController.java
  7. 70
    0
      wm-dma/src/main/java/com/water/dma/controller/DmaLeakageController.java
  8. 74
    0
      wm-dma/src/main/java/com/water/dma/controller/DmaMeterController.java
  9. 73
    0
      wm-dma/src/main/java/com/water/dma/controller/DmaZoneController.java
  10. 75
    0
      wm-dma/src/main/java/com/water/dma/controller/WaterBalanceController.java
  11. 39
    0
      wm-dma/src/main/java/com/water/dma/entity/DmaFlowRecord.java
  12. 57
    0
      wm-dma/src/main/java/com/water/dma/entity/DmaLeakageAnalysis.java
  13. 50
    0
      wm-dma/src/main/java/com/water/dma/entity/DmaMeter.java
  14. 47
    0
      wm-dma/src/main/java/com/water/dma/entity/DmaZone.java
  15. 63
    0
      wm-dma/src/main/java/com/water/dma/entity/WaterBalance.java
  16. 28
    0
      wm-dma/src/main/java/com/water/dma/mapper/DmaFlowRecordMapper.java
  17. 12
    0
      wm-dma/src/main/java/com/water/dma/mapper/DmaLeakageAnalysisMapper.java
  18. 12
    0
      wm-dma/src/main/java/com/water/dma/mapper/DmaMeterMapper.java
  19. 12
    0
      wm-dma/src/main/java/com/water/dma/mapper/DmaZoneMapper.java
  20. 12
    0
      wm-dma/src/main/java/com/water/dma/mapper/WaterBalanceMapper.java
  21. 137
    0
      wm-dma/src/main/java/com/water/dma/service/DmaFlowService.java
  22. 179
    0
      wm-dma/src/main/java/com/water/dma/service/DmaLeakageService.java
  23. 99
    0
      wm-dma/src/main/java/com/water/dma/service/DmaMeterService.java
  24. 122
    0
      wm-dma/src/main/java/com/water/dma/service/DmaZoneService.java
  25. 168
    0
      wm-dma/src/main/java/com/water/dma/service/WaterBalanceService.java
  26. 29
    0
      wm-dma/src/main/resources/application.yml
  27. 130
    0
      wm-dma/src/test/java/com/water/dma/service/DmaFlowServiceTest.java
  28. 124
    0
      wm-dma/src/test/java/com/water/dma/service/DmaLeakageServiceTest.java
  29. 100
    0
      wm-dma/src/test/java/com/water/dma/service/DmaMeterServiceTest.java
  30. 112
    0
      wm-dma/src/test/java/com/water/dma/service/DmaZoneServiceTest.java
  31. 117
    0
      wm-dma/src/test/java/com/water/dma/service/WaterBalanceServiceTest.java

+ 136
- 0
db/dma_ddl.sql 查看文件

@@ -0,0 +1,136 @@
1
+-- =====================================================
2
+-- DMA分区计量与漏损分析 DDL
3
+-- 数据库: PostgreSQL
4
+-- =====================================================
5
+
6
+-- DMA分区表
7
+CREATE TABLE IF NOT EXISTS dma_zone (
8
+    id              BIGSERIAL PRIMARY KEY,
9
+    zone_name       VARCHAR(100) NOT NULL,
10
+    zone_code       VARCHAR(50) NOT NULL UNIQUE,
11
+    parent_id       BIGINT REFERENCES dma_zone(id),
12
+    zone_level      INTEGER NOT NULL DEFAULT 1,
13
+    area            VARCHAR(100),
14
+    area_size       NUMERIC(10, 2),
15
+    population      INTEGER,
16
+    pipe_length     NUMERIC(10, 2),
17
+    status          VARCHAR(20) DEFAULT 'active',
18
+    remark          VARCHAR(500),
19
+    deleted         INTEGER DEFAULT 0,
20
+    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
21
+    updated_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
22
+);
23
+
24
+COMMENT ON TABLE dma_zone IS 'DMA分区表';
25
+COMMENT ON COLUMN dma_zone.zone_level IS '分区层级: 1=一级/2=二级/3=三级';
26
+COMMENT ON COLUMN dma_zone.status IS '状态: active/inactive';
27
+
28
+-- DMA计量表
29
+CREATE TABLE IF NOT EXISTS dma_meter (
30
+    id              BIGSERIAL PRIMARY KEY,
31
+    zone_id         BIGINT REFERENCES dma_zone(id),
32
+    meter_code      VARCHAR(50) NOT NULL UNIQUE,
33
+    meter_name      VARCHAR(100),
34
+    meter_type      VARCHAR(20) NOT NULL,
35
+    location        VARCHAR(200),
36
+    longitude       NUMERIC(12, 8),
37
+    latitude        NUMERIC(12, 8),
38
+    caliber         INTEGER,
39
+    brand           VARCHAR(100),
40
+    status          VARCHAR(20) DEFAULT 'online',
41
+    remark          VARCHAR(500),
42
+    deleted         INTEGER DEFAULT 0,
43
+    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
44
+    updated_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
45
+);
46
+
47
+COMMENT ON TABLE dma_meter IS 'DMA计量表';
48
+COMMENT ON COLUMN dma_meter.meter_type IS '表计类型: inlet=进水表/outlet=出水表/boundary=边界表';
49
+COMMENT ON COLUMN dma_meter.status IS '状态: online/offline/fault';
50
+
51
+CREATE INDEX IF NOT EXISTS idx_meter_zone ON dma_meter(zone_id);
52
+
53
+-- DMA流量记录表
54
+CREATE TABLE IF NOT EXISTS dma_flow_record (
55
+    id              BIGSERIAL PRIMARY KEY,
56
+    zone_id         BIGINT NOT NULL REFERENCES dma_zone(id),
57
+    meter_id        BIGINT NOT NULL REFERENCES dma_meter(id),
58
+    instant_flow    NUMERIC(12, 4),
59
+    total_flow      NUMERIC(14, 4),
60
+    pressure        NUMERIC(8, 4),
61
+    collect_time    TIMESTAMP NOT NULL,
62
+    data_quality    VARCHAR(20) DEFAULT 'good',
63
+    deleted         INTEGER DEFAULT 0,
64
+    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
65
+    updated_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
66
+);
67
+
68
+COMMENT ON TABLE dma_flow_record IS 'DMA流量记录表';
69
+COMMENT ON COLUMN dma_flow_record.instant_flow IS '瞬时流量(m³/h)';
70
+COMMENT ON COLUMN dma_flow_record.total_flow IS '累计流量(m³)';
71
+COMMENT ON COLUMN dma_flow_record.pressure IS '压力(MPa)';
72
+COMMENT ON COLUMN dma_flow_record.data_quality IS '数据质量: good/bad/missing';
73
+
74
+CREATE INDEX IF NOT EXISTS idx_flow_zone_time ON dma_flow_record(zone_id, collect_time);
75
+CREATE INDEX IF NOT EXISTS idx_flow_meter_time ON dma_flow_record(meter_id, collect_time);
76
+
77
+-- DMA漏损分析表
78
+CREATE TABLE IF NOT EXISTS dma_leakage_analysis (
79
+    id                  BIGSERIAL PRIMARY KEY,
80
+    zone_id             BIGINT NOT NULL REFERENCES dma_zone(id),
81
+    analysis_date       DATE NOT NULL,
82
+    supply_volume       NUMERIC(14, 4),
83
+    sale_volume         NUMERIC(14, 4),
84
+    leakage_volume      NUMERIC(14, 4),
85
+    nrw_rate            NUMERIC(8, 2),
86
+    leakage_rate        NUMERIC(8, 2),
87
+    mnf                 NUMERIC(10, 4),
88
+    mnf_time            VARCHAR(20),
89
+    background_leakage  NUMERIC(10, 4),
90
+    burst_leakage       NUMERIC(10, 4),
91
+    alarm_level         VARCHAR(20) DEFAULT 'normal',
92
+    remark              VARCHAR(500),
93
+    deleted             INTEGER DEFAULT 0,
94
+    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
95
+    updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
96
+);
97
+
98
+COMMENT ON TABLE dma_leakage_analysis IS 'DMA漏损分析表';
99
+COMMENT ON COLUMN dma_leakage_analysis.nrw_rate IS '产销差率(%)';
100
+COMMENT ON COLUMN dma_leakage_analysis.leakage_rate IS '漏损率(%)';
101
+COMMENT ON COLUMN dma_leakage_analysis.mnf IS '最小夜间流量(m³/h)';
102
+COMMENT ON COLUMN dma_leakage_analysis.alarm_level IS '报警级别: normal/warning/critical';
103
+
104
+CREATE INDEX IF NOT EXISTS idx_leakage_zone_date ON dma_leakage_analysis(zone_id, analysis_date);
105
+CREATE UNIQUE INDEX IF NOT EXISTS uk_leakage_zone_date ON dma_leakage_analysis(zone_id, analysis_date) WHERE deleted = 0;
106
+
107
+-- 水平衡表
108
+CREATE TABLE IF NOT EXISTS dma_water_balance (
109
+    id                  BIGSERIAL PRIMARY KEY,
110
+    zone_id             BIGINT NOT NULL REFERENCES dma_zone(id),
111
+    period              VARCHAR(20) NOT NULL,
112
+    stat_date           DATE NOT NULL,
113
+    total_supply        NUMERIC(14, 4),
114
+    total_sale          NUMERIC(14, 4),
115
+    billing_sale        NUMERIC(14, 4),
116
+    free_supply         NUMERIC(14, 4),
117
+    apparent_loss       NUMERIC(14, 4),
118
+    real_loss           NUMERIC(14, 4),
119
+    background_loss     NUMERIC(14, 4),
120
+    burst_loss          NUMERIC(14, 4),
121
+    total_loss          NUMERIC(14, 4),
122
+    nrw_rate            NUMERIC(8, 2),
123
+    leakage_rate        NUMERIC(8, 2),
124
+    remark              VARCHAR(500),
125
+    deleted             INTEGER DEFAULT 0,
126
+    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
127
+    updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
128
+);
129
+
130
+COMMENT ON TABLE dma_water_balance IS '水平衡表';
131
+COMMENT ON COLUMN dma_water_balance.period IS '统计周期: daily/monthly/yearly';
132
+COMMENT ON COLUMN dma_water_balance.apparent_loss IS '表观漏损(m³) - 计量误差+偷水';
133
+COMMENT ON COLUMN dma_water_balance.real_loss IS '实际漏损(m³) - 物理漏损';
134
+
135
+CREATE INDEX IF NOT EXISTS idx_balance_zone_date ON dma_water_balance(zone_id, stat_date);
136
+CREATE INDEX IF NOT EXISTS idx_balance_period ON dma_water_balance(period);

+ 1
- 0
pom.xml 查看文件

@@ -50,6 +50,7 @@
50 50
         <module>wm-system</module>
51 51
         <module>wm-mobile-app</module>
52 52
         <module>wm-config</module>
53
+        <module>wm-dma</module>
53 54
     </modules>
54 55
 
55 56
     <dependencyManagement>

+ 80
- 0
wm-dma/pom.xml 查看文件

@@ -0,0 +1,80 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<project xmlns="http://maven.apache.org/POM/4.0.0"
3
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5
+    <modelVersion>4.0.0</modelVersion>
6
+    <parent>
7
+        <groupId>com.water</groupId>
8
+        <artifactId>wm-parent</artifactId>
9
+        <version>1.0.0-SNAPSHOT</version>
10
+    </parent>
11
+    <artifactId>wm-dma</artifactId>
12
+    <name>wm-dma</name>
13
+    <description>DMA分区计量与漏损分析模块</description>
14
+
15
+    <dependencies>
16
+        <!-- 公共模块 -->
17
+        <dependency>
18
+            <groupId>com.water</groupId>
19
+            <artifactId>wm-common</artifactId>
20
+        </dependency>
21
+
22
+        <!-- Web -->
23
+        <dependency>
24
+            <groupId>org.springframework.boot</groupId>
25
+            <artifactId>spring-boot-starter-web</artifactId>
26
+        </dependency>
27
+
28
+        <!-- Nacos 服务发现 -->
29
+        <dependency>
30
+            <groupId>com.alibaba.cloud</groupId>
31
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
32
+        </dependency>
33
+
34
+        <!-- PostgreSQL -->
35
+        <dependency>
36
+            <groupId>org.postgresql</groupId>
37
+            <artifactId>postgresql</artifactId>
38
+        </dependency>
39
+
40
+        <!-- MyBatis-Plus -->
41
+        <dependency>
42
+            <groupId>com.baomidou</groupId>
43
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
44
+        </dependency>
45
+
46
+        <!-- Hutool -->
47
+        <dependency>
48
+            <groupId>cn.hutool</groupId>
49
+            <artifactId>hutool-all</artifactId>
50
+        </dependency>
51
+
52
+        <!-- Knife4j OpenAPI3 -->
53
+        <dependency>
54
+            <groupId>com.github.xiaoymin</groupId>
55
+            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
56
+        </dependency>
57
+
58
+        <!-- Test -->
59
+        <dependency>
60
+            <groupId>org.springframework.boot</groupId>
61
+            <artifactId>spring-boot-starter-test</artifactId>
62
+            <scope>test</scope>
63
+        </dependency>
64
+
65
+        <dependency>
66
+            <groupId>com.h2database</groupId>
67
+            <artifactId>h2</artifactId>
68
+            <scope>test</scope>
69
+        </dependency>
70
+    </dependencies>
71
+
72
+    <build>
73
+        <plugins>
74
+            <plugin>
75
+                <groupId>org.springframework.boot</groupId>
76
+                <artifactId>spring-boot-maven-plugin</artifactId>
77
+            </plugin>
78
+        </plugins>
79
+    </build>
80
+</project>

+ 15
- 0
wm-dma/src/main/java/com/water/dma/DmaApplication.java 查看文件

@@ -0,0 +1,15 @@
1
+package com.water.dma;
2
+
3
+import org.mybatis.spring.annotation.MapperScan;
4
+import org.springframework.boot.SpringApplication;
5
+import org.springframework.boot.autoconfigure.SpringBootApplication;
6
+import org.springframework.scheduling.annotation.EnableScheduling;
7
+
8
+@SpringBootApplication(scanBasePackages = "com.water")
9
+@MapperScan("com.water.dma.mapper")
10
+@EnableScheduling
11
+public class DmaApplication {
12
+    public static void main(String[] args) {
13
+        SpringApplication.run(DmaApplication.class, args);
14
+    }
15
+}

+ 33
- 0
wm-dma/src/main/java/com/water/dma/config/MyBatisPlusConfig.java 查看文件

@@ -0,0 +1,33 @@
1
+package com.water.dma.config;
2
+
3
+import com.baomidou.mybatisplus.annotation.DbType;
4
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
5
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
6
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
7
+import org.apache.ibatis.reflection.MetaObject;
8
+import org.springframework.context.annotation.Bean;
9
+import org.springframework.context.annotation.Configuration;
10
+
11
+import java.time.LocalDateTime;
12
+
13
+@Configuration
14
+public class MyBatisPlusConfig implements MetaObjectHandler {
15
+
16
+    @Bean
17
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
18
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
19
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
20
+        return interceptor;
21
+    }
22
+
23
+    @Override
24
+    public void insertFill(MetaObject metaObject) {
25
+        this.strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
26
+        this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
27
+    }
28
+
29
+    @Override
30
+    public void updateFill(MetaObject metaObject) {
31
+        this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
32
+    }
33
+}

+ 79
- 0
wm-dma/src/main/java/com/water/dma/controller/DmaFlowController.java 查看文件

@@ -0,0 +1,79 @@
1
+package com.water.dma.controller;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.common.core.result.R;
5
+import com.water.dma.entity.DmaFlowRecord;
6
+import com.water.dma.service.DmaFlowService;
7
+import io.swagger.v3.oas.annotations.Operation;
8
+import io.swagger.v3.oas.annotations.tags.Tag;
9
+import lombok.RequiredArgsConstructor;
10
+import org.springframework.format.annotation.DateTimeFormat;
11
+import org.springframework.web.bind.annotation.*;
12
+
13
+import java.time.LocalDate;
14
+import java.time.LocalDateTime;
15
+import java.util.List;
16
+import java.util.Map;
17
+
18
+/**
19
+ * DMA流量计量控制器
20
+ */
21
+@Tag(name = "DMA流量计量")
22
+@RestController
23
+@RequestMapping("/api/dma/flow")
24
+@RequiredArgsConstructor
25
+public class DmaFlowController {
26
+
27
+    private final DmaFlowService flowService;
28
+
29
+    @Operation(summary = "分页查询流量记录")
30
+    @GetMapping("/page")
31
+    public R<Page<DmaFlowRecord>> page(
32
+            @RequestParam(defaultValue = "1") int pageNum,
33
+            @RequestParam(defaultValue = "20") int pageSize,
34
+            @RequestParam(required = false) Long zoneId,
35
+            @RequestParam(required = false) Long meterId,
36
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
37
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
38
+        return R.ok(flowService.page(pageNum, pageSize, zoneId, meterId, startTime, endTime));
39
+    }
40
+
41
+    @Operation(summary = "创建流量记录")
42
+    @PostMapping
43
+    public R<DmaFlowRecord> create(@RequestBody DmaFlowRecord record) {
44
+        return R.ok(flowService.create(record));
45
+    }
46
+
47
+    @Operation(summary = "批量创建流量记录")
48
+    @PostMapping("/batch")
49
+    public R<String> batchCreate(@RequestBody List<DmaFlowRecord> records) {
50
+        flowService.batchCreate(records);
51
+        return R.ok("批量创建成功");
52
+    }
53
+
54
+    @Operation(summary = "获取分区进出水量汇总")
55
+    @GetMapping("/summary/{zoneId}")
56
+    public R<Map<String, Object>> getZoneFlowSummary(
57
+            @PathVariable Long zoneId,
58
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
59
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
60
+        return R.ok(flowService.getZoneFlowSummary(zoneId, startTime, endTime));
61
+    }
62
+
63
+    @Operation(summary = "最小夜间流量(MNF)分析")
64
+    @GetMapping("/mnf/{zoneId}")
65
+    public R<Map<String, Object>> getMNFAnalysis(
66
+            @PathVariable Long zoneId,
67
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
68
+        return R.ok(flowService.getMNFAnalysis(zoneId, date));
69
+    }
70
+
71
+    @Operation(summary = "获取流量趋势")
72
+    @GetMapping("/trend/{zoneId}")
73
+    public R<List<Map<String, Object>>> getFlowTrend(
74
+            @PathVariable Long zoneId,
75
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
76
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
77
+        return R.ok(flowService.getFlowTrend(zoneId, startTime, endTime));
78
+    }
79
+}

+ 70
- 0
wm-dma/src/main/java/com/water/dma/controller/DmaLeakageController.java 查看文件

@@ -0,0 +1,70 @@
1
+package com.water.dma.controller;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.common.core.result.R;
5
+import com.water.dma.entity.DmaLeakageAnalysis;
6
+import com.water.dma.service.DmaLeakageService;
7
+import io.swagger.v3.oas.annotations.Operation;
8
+import io.swagger.v3.oas.annotations.tags.Tag;
9
+import lombok.RequiredArgsConstructor;
10
+import org.springframework.format.annotation.DateTimeFormat;
11
+import org.springframework.web.bind.annotation.*;
12
+
13
+import java.math.BigDecimal;
14
+import java.time.LocalDate;
15
+import java.util.List;
16
+import java.util.Map;
17
+
18
+/**
19
+ * DMA漏损分析控制器
20
+ */
21
+@Tag(name = "DMA漏损分析")
22
+@RestController
23
+@RequestMapping("/api/dma/leakage")
24
+@RequiredArgsConstructor
25
+public class DmaLeakageController {
26
+
27
+    private final DmaLeakageService leakageService;
28
+
29
+    @Operation(summary = "分页查询漏损分析")
30
+    @GetMapping("/page")
31
+    public R<Page<DmaLeakageAnalysis>> page(
32
+            @RequestParam(defaultValue = "1") int pageNum,
33
+            @RequestParam(defaultValue = "10") int pageSize,
34
+            @RequestParam(required = false) Long zoneId,
35
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
36
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
37
+        return R.ok(leakageService.page(pageNum, pageSize, zoneId, startDate, endDate));
38
+    }
39
+
40
+    @Operation(summary = "执行漏损分析")
41
+    @PostMapping("/analyze")
42
+    public R<DmaLeakageAnalysis> analyze(
43
+            @RequestParam Long zoneId,
44
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
45
+            @RequestParam BigDecimal supplyVolume,
46
+            @RequestParam BigDecimal saleVolume) {
47
+        return R.ok(leakageService.analyze(zoneId, date, supplyVolume, saleVolume));
48
+    }
49
+
50
+    @Operation(summary = "获取漏损趋势")
51
+    @GetMapping("/trend/{zoneId}")
52
+    public R<List<Map<String, Object>>> getTrend(
53
+            @PathVariable Long zoneId,
54
+            @RequestParam(defaultValue = "30") int days) {
55
+        return R.ok(leakageService.getTrend(zoneId, days));
56
+    }
57
+
58
+    @Operation(summary = "获取报警列表")
59
+    @GetMapping("/alarms")
60
+    public R<List<DmaLeakageAnalysis>> getAlarms(
61
+            @RequestParam(required = false) String alarmLevel) {
62
+        return R.ok(leakageService.getAlarms(alarmLevel));
63
+    }
64
+
65
+    @Operation(summary = "获取分区漏损汇总")
66
+    @GetMapping("/summary/{zoneId}")
67
+    public R<Map<String, Object>> getZoneSummary(@PathVariable Long zoneId) {
68
+        return R.ok(leakageService.getZoneSummary(zoneId));
69
+    }
70
+}

+ 74
- 0
wm-dma/src/main/java/com/water/dma/controller/DmaMeterController.java 查看文件

@@ -0,0 +1,74 @@
1
+package com.water.dma.controller;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.common.core.result.R;
5
+import com.water.dma.entity.DmaMeter;
6
+import com.water.dma.service.DmaMeterService;
7
+import io.swagger.v3.oas.annotations.Operation;
8
+import io.swagger.v3.oas.annotations.tags.Tag;
9
+import lombok.RequiredArgsConstructor;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+import java.util.List;
13
+
14
+/**
15
+ * DMA计量表管理控制器
16
+ */
17
+@Tag(name = "DMA计量表管理")
18
+@RestController
19
+@RequestMapping("/api/dma/meter")
20
+@RequiredArgsConstructor
21
+public class DmaMeterController {
22
+
23
+    private final DmaMeterService meterService;
24
+
25
+    @Operation(summary = "分页查询计量表")
26
+    @GetMapping("/page")
27
+    public R<Page<DmaMeter>> page(
28
+            @RequestParam(defaultValue = "1") int pageNum,
29
+            @RequestParam(defaultValue = "10") int pageSize,
30
+            @RequestParam(required = false) Long zoneId,
31
+            @RequestParam(required = false) String meterType) {
32
+        return R.ok(meterService.page(pageNum, pageSize, zoneId, meterType));
33
+    }
34
+
35
+    @Operation(summary = "获取计量表详情")
36
+    @GetMapping("/{id}")
37
+    public R<DmaMeter> getById(@PathVariable Long id) {
38
+        return R.ok(meterService.getById(id));
39
+    }
40
+
41
+    @Operation(summary = "创建计量表")
42
+    @PostMapping
43
+    public R<DmaMeter> create(@RequestBody DmaMeter meter) {
44
+        return R.ok(meterService.create(meter));
45
+    }
46
+
47
+    @Operation(summary = "更新计量表")
48
+    @PutMapping("/{id}")
49
+    public R<String> update(@PathVariable Long id, @RequestBody DmaMeter meter) {
50
+        meter.setId(id);
51
+        meterService.update(meter);
52
+        return R.ok("更新成功");
53
+    }
54
+
55
+    @Operation(summary = "删除计量表")
56
+    @DeleteMapping("/{id}")
57
+    public R<String> delete(@PathVariable Long id) {
58
+        meterService.delete(id);
59
+        return R.ok("删除成功");
60
+    }
61
+
62
+    @Operation(summary = "获取分区下的计量表")
63
+    @GetMapping("/zone/{zoneId}")
64
+    public R<List<DmaMeter>> listByZoneId(@PathVariable Long zoneId) {
65
+        return R.ok(meterService.listByZoneId(zoneId));
66
+    }
67
+
68
+    @Operation(summary = "绑定计量表到分区")
69
+    @PostMapping("/{meterId}/bind/{zoneId}")
70
+    public R<String> bindToZone(@PathVariable Long meterId, @PathVariable Long zoneId) {
71
+        meterService.bindToZone(meterId, zoneId);
72
+        return R.ok("绑定成功");
73
+    }
74
+}

+ 73
- 0
wm-dma/src/main/java/com/water/dma/controller/DmaZoneController.java 查看文件

@@ -0,0 +1,73 @@
1
+package com.water.dma.controller;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.common.core.result.R;
5
+import com.water.dma.entity.DmaZone;
6
+import com.water.dma.service.DmaZoneService;
7
+import io.swagger.v3.oas.annotations.Operation;
8
+import io.swagger.v3.oas.annotations.tags.Tag;
9
+import lombok.RequiredArgsConstructor;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+import java.util.List;
13
+import java.util.Map;
14
+
15
+/**
16
+ * DMA分区管理控制器
17
+ */
18
+@Tag(name = "DMA分区管理")
19
+@RestController
20
+@RequestMapping("/api/dma/zone")
21
+@RequiredArgsConstructor
22
+public class DmaZoneController {
23
+
24
+    private final DmaZoneService zoneService;
25
+
26
+    @Operation(summary = "分页查询分区")
27
+    @GetMapping("/page")
28
+    public R<Page<DmaZone>> page(
29
+            @RequestParam(defaultValue = "1") int pageNum,
30
+            @RequestParam(defaultValue = "10") int pageSize,
31
+            @RequestParam(required = false) String zoneName) {
32
+        return R.ok(zoneService.page(pageNum, pageSize, zoneName));
33
+    }
34
+
35
+    @Operation(summary = "获取分区详情")
36
+    @GetMapping("/{id}")
37
+    public R<DmaZone> getById(@PathVariable Long id) {
38
+        return R.ok(zoneService.getById(id));
39
+    }
40
+
41
+    @Operation(summary = "创建分区")
42
+    @PostMapping
43
+    public R<DmaZone> create(@RequestBody DmaZone zone) {
44
+        return R.ok(zoneService.create(zone));
45
+    }
46
+
47
+    @Operation(summary = "更新分区")
48
+    @PutMapping("/{id}")
49
+    public R<String> update(@PathVariable Long id, @RequestBody DmaZone zone) {
50
+        zone.setId(id);
51
+        zoneService.update(zone);
52
+        return R.ok("更新成功");
53
+    }
54
+
55
+    @Operation(summary = "删除分区")
56
+    @DeleteMapping("/{id}")
57
+    public R<String> delete(@PathVariable Long id) {
58
+        zoneService.delete(id);
59
+        return R.ok("删除成功");
60
+    }
61
+
62
+    @Operation(summary = "获取分区树")
63
+    @GetMapping("/tree")
64
+    public R<List<Map<String, Object>>> getZoneTree() {
65
+        return R.ok(zoneService.getZoneTree());
66
+    }
67
+
68
+    @Operation(summary = "获取所有分区")
69
+    @GetMapping("/list")
70
+    public R<List<DmaZone>> listAll() {
71
+        return R.ok(zoneService.listAll());
72
+    }
73
+}

+ 75
- 0
wm-dma/src/main/java/com/water/dma/controller/WaterBalanceController.java 查看文件

@@ -0,0 +1,75 @@
1
+package com.water.dma.controller;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.common.core.result.R;
5
+import com.water.dma.entity.WaterBalance;
6
+import com.water.dma.service.WaterBalanceService;
7
+import io.swagger.v3.oas.annotations.Operation;
8
+import io.swagger.v3.oas.annotations.tags.Tag;
9
+import lombok.RequiredArgsConstructor;
10
+import org.springframework.format.annotation.DateTimeFormat;
11
+import org.springframework.web.bind.annotation.*;
12
+
13
+import java.time.LocalDate;
14
+import java.util.Map;
15
+
16
+/**
17
+ * 水平衡分析控制器
18
+ */
19
+@Tag(name = "水平衡分析")
20
+@RestController
21
+@RequestMapping("/api/dma/balance")
22
+@RequiredArgsConstructor
23
+public class WaterBalanceController {
24
+
25
+    private final WaterBalanceService balanceService;
26
+
27
+    @Operation(summary = "分页查询水平衡")
28
+    @GetMapping("/page")
29
+    public R<Page<WaterBalance>> page(
30
+            @RequestParam(defaultValue = "1") int pageNum,
31
+            @RequestParam(defaultValue = "10") int pageSize,
32
+            @RequestParam(required = false) Long zoneId,
33
+            @RequestParam(required = false) String period,
34
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
35
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
36
+        return R.ok(balanceService.page(pageNum, pageSize, zoneId, period, startDate, endDate));
37
+    }
38
+
39
+    @Operation(summary = "获取水平衡详情")
40
+    @GetMapping("/{id}")
41
+    public R<WaterBalance> getById(@PathVariable Long id) {
42
+        return R.ok(balanceService.getById(id));
43
+    }
44
+
45
+    @Operation(summary = "创建水平衡记录")
46
+    @PostMapping
47
+    public R<WaterBalance> create(@RequestBody WaterBalance balance) {
48
+        return R.ok(balanceService.create(balance));
49
+    }
50
+
51
+    @Operation(summary = "更新水平衡记录")
52
+    @PutMapping("/{id}")
53
+    public R<String> update(@PathVariable Long id, @RequestBody WaterBalance balance) {
54
+        balance.setId(id);
55
+        balanceService.update(balance);
56
+        return R.ok("更新成功");
57
+    }
58
+
59
+    @Operation(summary = "删除水平衡记录")
60
+    @DeleteMapping("/{id}")
61
+    public R<String> delete(@PathVariable Long id) {
62
+        balanceService.delete(id);
63
+        return R.ok("删除成功");
64
+    }
65
+
66
+    @Operation(summary = "生成水平衡分析报告")
67
+    @GetMapping("/report/{zoneId}")
68
+    public R<Map<String, Object>> generateReport(
69
+            @PathVariable Long zoneId,
70
+            @RequestParam(defaultValue = "monthly") String period,
71
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
72
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
73
+        return R.ok(balanceService.generateReport(zoneId, period, startDate, endDate));
74
+    }
75
+}

+ 39
- 0
wm-dma/src/main/java/com/water/dma/entity/DmaFlowRecord.java 查看文件

@@ -0,0 +1,39 @@
1
+package com.water.dma.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.math.BigDecimal;
9
+import java.time.LocalDateTime;
10
+
11
+/**
12
+ * DMA流量记录实体
13
+ */
14
+@Data
15
+@EqualsAndHashCode(callSuper = true)
16
+@TableName("dma_flow_record")
17
+public class DmaFlowRecord extends BaseEntity {
18
+
19
+    /** 所属分区ID */
20
+    private Long zoneId;
21
+
22
+    /** 表计ID */
23
+    private Long meterId;
24
+
25
+    /** 瞬时流量(m³/h) */
26
+    private BigDecimal instantFlow;
27
+
28
+    /** 累计流量(m³) */
29
+    private BigDecimal totalFlow;
30
+
31
+    /** 压力(MPa) */
32
+    private BigDecimal pressure;
33
+
34
+    /** 采集时间 */
35
+    private LocalDateTime collectTime;
36
+
37
+    /** 数据质量: good/bad/missing */
38
+    private String dataQuality;
39
+}

+ 57
- 0
wm-dma/src/main/java/com/water/dma/entity/DmaLeakageAnalysis.java 查看文件

@@ -0,0 +1,57 @@
1
+package com.water.dma.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.math.BigDecimal;
9
+import java.time.LocalDate;
10
+
11
+/**
12
+ * DMA漏损分析实体
13
+ */
14
+@Data
15
+@EqualsAndHashCode(callSuper = true)
16
+@TableName("dma_leakage_analysis")
17
+public class DmaLeakageAnalysis extends BaseEntity {
18
+
19
+    /** 所属分区ID */
20
+    private Long zoneId;
21
+
22
+    /** 分析日期 */
23
+    private LocalDate analysisDate;
24
+
25
+    /** 供水量(m³) */
26
+    private BigDecimal supplyVolume;
27
+
28
+    /** 售水量(m³) */
29
+    private BigDecimal saleVolume;
30
+
31
+    /** 漏损量(m³) */
32
+    private BigDecimal leakageVolume;
33
+
34
+    /** 产销差率(%) */
35
+    private BigDecimal nrwRate;
36
+
37
+    /** 漏损率(%) */
38
+    private BigDecimal leakageRate;
39
+
40
+    /** 最小夜间流量(m³/h) */
41
+    private BigDecimal mnf;
42
+
43
+    /** MNF发生时间 */
44
+    private String mnfTime;
45
+
46
+    /** 背景漏损(m³/h) */
47
+    private BigDecimal backgroundLeakage;
48
+
49
+    /** 爆管漏损(m³/h) */
50
+    private BigDecimal burstLeakage;
51
+
52
+    /** 报警级别: normal/warning/critical */
53
+    private String alarmLevel;
54
+
55
+    /** 备注 */
56
+    private String remark;
57
+}

+ 50
- 0
wm-dma/src/main/java/com/water/dma/entity/DmaMeter.java 查看文件

@@ -0,0 +1,50 @@
1
+package com.water.dma.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.math.BigDecimal;
9
+
10
+/**
11
+ * DMA分区计量表实体
12
+ */
13
+@Data
14
+@EqualsAndHashCode(callSuper = true)
15
+@TableName("dma_meter")
16
+public class DmaMeter extends BaseEntity {
17
+
18
+    /** 所属分区ID */
19
+    private Long zoneId;
20
+
21
+    /** 表计编号 */
22
+    private String meterCode;
23
+
24
+    /** 表计名称 */
25
+    private String meterName;
26
+
27
+    /** 表计类型: inlet=进水表/outlet=出水表/boundary=边界表 */
28
+    private String meterType;
29
+
30
+    /** 安装位置 */
31
+    private String location;
32
+
33
+    /** 经度 */
34
+    private BigDecimal longitude;
35
+
36
+    /** 纬度 */
37
+    private BigDecimal latitude;
38
+
39
+    /** 口径(mm) */
40
+    private Integer caliber;
41
+
42
+    /** 品牌/型号 */
43
+    private String brand;
44
+
45
+    /** 状态: online/offline/fault */
46
+    private String status;
47
+
48
+    /** 备注 */
49
+    private String remark;
50
+}

+ 47
- 0
wm-dma/src/main/java/com/water/dma/entity/DmaZone.java 查看文件

@@ -0,0 +1,47 @@
1
+package com.water.dma.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.math.BigDecimal;
9
+
10
+/**
11
+ * DMA分区实体
12
+ */
13
+@Data
14
+@EqualsAndHashCode(callSuper = true)
15
+@TableName("dma_zone")
16
+public class DmaZone extends BaseEntity {
17
+
18
+    /** 分区名称 */
19
+    private String zoneName;
20
+
21
+    /** 分区编码(唯一) */
22
+    private String zoneCode;
23
+
24
+    /** 父级分区ID(顶级为null) */
25
+    private Long parentId;
26
+
27
+    /** 分区层级: 1=一级/2=二级/3=三级 */
28
+    private Integer zoneLevel;
29
+
30
+    /** 所属区域 */
31
+    private String area;
32
+
33
+    /** 分区面积(km²) */
34
+    private BigDecimal areaSize;
35
+
36
+    /** 服务人口数 */
37
+    private Integer population;
38
+
39
+    /** 管网长度(km) */
40
+    private BigDecimal pipeLength;
41
+
42
+    /** 状态: active/inactive */
43
+    private String status;
44
+
45
+    /** 备注 */
46
+    private String remark;
47
+}

+ 63
- 0
wm-dma/src/main/java/com/water/dma/entity/WaterBalance.java 查看文件

@@ -0,0 +1,63 @@
1
+package com.water.dma.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.math.BigDecimal;
9
+import java.time.LocalDate;
10
+
11
+/**
12
+ * 水平衡表实体
13
+ */
14
+@Data
15
+@EqualsAndHashCode(callSuper = true)
16
+@TableName("dma_water_balance")
17
+public class WaterBalance extends BaseEntity {
18
+
19
+    /** 所属分区ID */
20
+    private Long zoneId;
21
+
22
+    /** 统计周期: daily/monthly/yearly */
23
+    private String period;
24
+
25
+    /** 统计日期 */
26
+    private LocalDate statDate;
27
+
28
+    /** 总供水量(m³) */
29
+    private BigDecimal totalSupply;
30
+
31
+    /** 总售水量(m³) */
32
+    private BigDecimal totalSale;
33
+
34
+    /** 计费售水量(m³) */
35
+    private BigDecimal billingSale;
36
+
37
+    /** 免费供水量(m³) */
38
+    private BigDecimal freeSupply;
39
+
40
+    /** 表观漏损(m³) - 计量误差+偷水 */
41
+    private BigDecimal apparentLoss;
42
+
43
+    /** 实际漏损(m³) - 物理漏损 */
44
+    private BigDecimal realLoss;
45
+
46
+    /** 背景漏损(m³) */
47
+    private BigDecimal backgroundLoss;
48
+
49
+    /** 爆管漏损(m³) */
50
+    private BigDecimal burstLoss;
51
+
52
+    /** 总漏损量(m³) */
53
+    private BigDecimal totalLoss;
54
+
55
+    /** 产销差率(%) */
56
+    private BigDecimal nrwRate;
57
+
58
+    /** 漏损率(%) */
59
+    private BigDecimal leakageRate;
60
+
61
+    /** 备注 */
62
+    private String remark;
63
+}

+ 28
- 0
wm-dma/src/main/java/com/water/dma/mapper/DmaFlowRecordMapper.java 查看文件

@@ -0,0 +1,28 @@
1
+package com.water.dma.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dma.entity.DmaFlowRecord;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Select;
7
+
8
+import java.math.BigDecimal;
9
+import java.time.LocalDateTime;
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+/**
14
+ * DMA流量记录Mapper
15
+ */
16
+@Mapper
17
+public interface DmaFlowRecordMapper extends BaseMapper<DmaFlowRecord> {
18
+
19
+    @Select("SELECT meter_id, SUM(instant_flow) as total_flow FROM dma_flow_record " +
20
+            "WHERE zone_id = #{zoneId} AND collect_time BETWEEN #{startTime} AND #{endTime} " +
21
+            "GROUP BY meter_id")
22
+    List<Map<String, Object>> sumFlowByMeter(Long zoneId, LocalDateTime startTime, LocalDateTime endTime);
23
+
24
+    @Select("SELECT MIN(instant_flow) as mnf FROM dma_flow_record " +
25
+            "WHERE zone_id = #{zoneId} AND collect_time::time BETWEEN '02:00:00' AND '04:00:00' " +
26
+            "AND collect_time::date = #{date}")
27
+    BigDecimal getMNF(Long zoneId, java.time.LocalDate date);
28
+}

+ 12
- 0
wm-dma/src/main/java/com/water/dma/mapper/DmaLeakageAnalysisMapper.java 查看文件

@@ -0,0 +1,12 @@
1
+package com.water.dma.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dma.entity.DmaLeakageAnalysis;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+/**
8
+ * DMA漏损分析Mapper
9
+ */
10
+@Mapper
11
+public interface DmaLeakageAnalysisMapper extends BaseMapper<DmaLeakageAnalysis> {
12
+}

+ 12
- 0
wm-dma/src/main/java/com/water/dma/mapper/DmaMeterMapper.java 查看文件

@@ -0,0 +1,12 @@
1
+package com.water.dma.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dma.entity.DmaMeter;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+/**
8
+ * DMA计量表Mapper
9
+ */
10
+@Mapper
11
+public interface DmaMeterMapper extends BaseMapper<DmaMeter> {
12
+}

+ 12
- 0
wm-dma/src/main/java/com/water/dma/mapper/DmaZoneMapper.java 查看文件

@@ -0,0 +1,12 @@
1
+package com.water.dma.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dma.entity.DmaZone;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+/**
8
+ * DMA分区Mapper
9
+ */
10
+@Mapper
11
+public interface DmaZoneMapper extends BaseMapper<DmaZone> {
12
+}

+ 12
- 0
wm-dma/src/main/java/com/water/dma/mapper/WaterBalanceMapper.java 查看文件

@@ -0,0 +1,12 @@
1
+package com.water.dma.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dma.entity.WaterBalance;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+/**
8
+ * 水平衡表Mapper
9
+ */
10
+@Mapper
11
+public interface WaterBalanceMapper extends BaseMapper<WaterBalance> {
12
+}

+ 137
- 0
wm-dma/src/main/java/com/water/dma/service/DmaFlowService.java 查看文件

@@ -0,0 +1,137 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.dma.entity.DmaFlowRecord;
6
+import com.water.dma.mapper.DmaFlowRecordMapper;
7
+import lombok.RequiredArgsConstructor;
8
+import lombok.extern.slf4j.Slf4j;
9
+import org.springframework.stereotype.Service;
10
+
11
+import java.math.BigDecimal;
12
+import java.time.LocalDate;
13
+import java.time.LocalDateTime;
14
+import java.util.*;
15
+
16
+/**
17
+ * DMA流量记录服务
18
+ */
19
+@Slf4j
20
+@Service
21
+@RequiredArgsConstructor
22
+public class DmaFlowService {
23
+
24
+    private final DmaFlowRecordMapper flowRecordMapper;
25
+
26
+    /**
27
+     * 分页查询流量记录
28
+     */
29
+    public Page<DmaFlowRecord> page(int pageNum, int pageSize, Long zoneId, Long meterId,
30
+                                     LocalDateTime startTime, LocalDateTime endTime) {
31
+        LambdaQueryWrapper<DmaFlowRecord> wrapper = new LambdaQueryWrapper<>();
32
+        if (zoneId != null) {
33
+            wrapper.eq(DmaFlowRecord::getZoneId, zoneId);
34
+        }
35
+        if (meterId != null) {
36
+            wrapper.eq(DmaFlowRecord::getMeterId, meterId);
37
+        }
38
+        if (startTime != null) {
39
+            wrapper.ge(DmaFlowRecord::getCollectTime, startTime);
40
+        }
41
+        if (endTime != null) {
42
+            wrapper.le(DmaFlowRecord::getCollectTime, endTime);
43
+        }
44
+        wrapper.orderByDesc(DmaFlowRecord::getCollectTime);
45
+        return flowRecordMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
46
+    }
47
+
48
+    /**
49
+     * 批量创建流量记录
50
+     */
51
+    public void batchCreate(List<DmaFlowRecord> records) {
52
+        for (DmaFlowRecord record : records) {
53
+            if (record.getDataQuality() == null) {
54
+                record.setDataQuality("good");
55
+            }
56
+            flowRecordMapper.insert(record);
57
+        }
58
+    }
59
+
60
+    /**
61
+     * 创建单条流量记录
62
+     */
63
+    public DmaFlowRecord create(DmaFlowRecord record) {
64
+        if (record.getDataQuality() == null) {
65
+            record.setDataQuality("good");
66
+        }
67
+        flowRecordMapper.insert(record);
68
+        return record;
69
+    }
70
+
71
+    /**
72
+     * 获取分区进出水量汇总
73
+     */
74
+    public Map<String, Object> getZoneFlowSummary(Long zoneId, LocalDateTime startTime, LocalDateTime endTime) {
75
+        List<Map<String, Object>> flowSums = flowRecordMapper.sumFlowByMeter(zoneId, startTime, endTime);
76
+        
77
+        Map<String, Object> result = new LinkedHashMap<>();
78
+        BigDecimal totalInflow = BigDecimal.ZERO;
79
+        BigDecimal totalOutflow = BigDecimal.ZERO;
80
+
81
+        for (Map<String, Object> row : flowSums) {
82
+            Object flowObj = row.get("total_flow");
83
+            BigDecimal flow = flowObj != null ? new BigDecimal(flowObj.toString()) : BigDecimal.ZERO;
84
+            totalInflow = totalInflow.add(flow);
85
+        }
86
+
87
+        result.put("zoneId", zoneId);
88
+        result.put("startTime", startTime);
89
+        result.put("endTime", endTime);
90
+        result.put("totalInflow", totalInflow);
91
+        result.put("totalOutflow", totalOutflow);
92
+        result.put("netFlow", totalInflow.subtract(totalOutflow));
93
+        result.put("meterCount", flowSums.size());
94
+
95
+        return result;
96
+    }
97
+
98
+    /**
99
+     * 最小夜间流量(MNF)分析
100
+     */
101
+    public Map<String, Object> getMNFAnalysis(Long zoneId, LocalDate date) {
102
+        BigDecimal mnf = flowRecordMapper.getMNF(zoneId, date);
103
+
104
+        Map<String, Object> result = new LinkedHashMap<>();
105
+        result.put("zoneId", zoneId);
106
+        result.put("date", date);
107
+        result.put("mnf", mnf != null ? mnf : BigDecimal.ZERO);
108
+        result.put("mnfTime", "02:00-04:00");
109
+        result.put("analysisResult", mnf != null && mnf.compareTo(new BigDecimal("5")) > 0 ? "异常" : "正常");
110
+
111
+        return result;
112
+    }
113
+
114
+    /**
115
+     * 获取流量趋势
116
+     */
117
+    public List<Map<String, Object>> getFlowTrend(Long zoneId, LocalDateTime startTime, LocalDateTime endTime) {
118
+        LambdaQueryWrapper<DmaFlowRecord> wrapper = new LambdaQueryWrapper<>();
119
+        wrapper.eq(DmaFlowRecord::getZoneId, zoneId);
120
+        wrapper.ge(DmaFlowRecord::getCollectTime, startTime);
121
+        wrapper.le(DmaFlowRecord::getCollectTime, endTime);
122
+        wrapper.orderByAsc(DmaFlowRecord::getCollectTime);
123
+
124
+        List<DmaFlowRecord> records = flowRecordMapper.selectList(wrapper);
125
+
126
+        List<Map<String, Object>> trend = new ArrayList<>();
127
+        for (DmaFlowRecord record : records) {
128
+            Map<String, Object> point = new LinkedHashMap<>();
129
+            point.put("time", record.getCollectTime());
130
+            point.put("instantFlow", record.getInstantFlow());
131
+            point.put("totalFlow", record.getTotalFlow());
132
+            point.put("pressure", record.getPressure());
133
+            trend.add(point);
134
+        }
135
+        return trend;
136
+    }
137
+}

+ 179
- 0
wm-dma/src/main/java/com/water/dma/service/DmaLeakageService.java 查看文件

@@ -0,0 +1,179 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.dma.entity.DmaLeakageAnalysis;
6
+import com.water.dma.entity.DmaZone;
7
+import com.water.dma.mapper.DmaLeakageAnalysisMapper;
8
+import com.water.dma.mapper.DmaZoneMapper;
9
+import lombok.RequiredArgsConstructor;
10
+import lombok.extern.slf4j.Slf4j;
11
+import org.springframework.stereotype.Service;
12
+
13
+import java.math.BigDecimal;
14
+import java.math.RoundingMode;
15
+import java.time.LocalDate;
16
+import java.util.*;
17
+
18
+/**
19
+ * DMA漏损分析服务
20
+ */
21
+@Slf4j
22
+@Service
23
+@RequiredArgsConstructor
24
+public class DmaLeakageService {
25
+
26
+    private final DmaLeakageAnalysisMapper leakageMapper;
27
+    private final DmaZoneMapper zoneMapper;
28
+
29
+    /**
30
+     * 分页查询漏损分析
31
+     */
32
+    public Page<DmaLeakageAnalysis> page(int pageNum, int pageSize, Long zoneId,
33
+                                          LocalDate startDate, LocalDate endDate) {
34
+        LambdaQueryWrapper<DmaLeakageAnalysis> wrapper = new LambdaQueryWrapper<>();
35
+        if (zoneId != null) {
36
+            wrapper.eq(DmaLeakageAnalysis::getZoneId, zoneId);
37
+        }
38
+        if (startDate != null) {
39
+            wrapper.ge(DmaLeakageAnalysis::getAnalysisDate, startDate);
40
+        }
41
+        if (endDate != null) {
42
+            wrapper.le(DmaLeakageAnalysis::getAnalysisDate, endDate);
43
+        }
44
+        wrapper.orderByDesc(DmaLeakageAnalysis::getAnalysisDate);
45
+        return leakageMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
46
+    }
47
+
48
+    /**
49
+     * 执行漏损分析
50
+     */
51
+    public DmaLeakageAnalysis analyze(Long zoneId, LocalDate date, BigDecimal supplyVolume, BigDecimal saleVolume) {
52
+        DmaLeakageAnalysis analysis = new DmaLeakageAnalysis();
53
+        analysis.setZoneId(zoneId);
54
+        analysis.setAnalysisDate(date);
55
+        analysis.setSupplyVolume(supplyVolume);
56
+        analysis.setSaleVolume(saleVolume);
57
+
58
+        // 计算漏损量
59
+        BigDecimal leakageVolume = supplyVolume.subtract(saleVolume);
60
+        analysis.setLeakageVolume(leakageVolume);
61
+
62
+        // 计算产销差率
63
+        if (supplyVolume.compareTo(BigDecimal.ZERO) > 0) {
64
+            BigDecimal nrwRate = leakageVolume.multiply(new BigDecimal("100"))
65
+                    .divide(supplyVolume, 2, RoundingMode.HALF_UP);
66
+            analysis.setNrwRate(nrwRate);
67
+            analysis.setLeakageRate(nrwRate);
68
+        } else {
69
+            analysis.setNrwRate(BigDecimal.ZERO);
70
+            analysis.setLeakageRate(BigDecimal.ZERO);
71
+        }
72
+
73
+        // 设置报警级别
74
+        String alarmLevel = determineAlarmLevel(analysis.getNrwRate());
75
+        analysis.setAlarmLevel(alarmLevel);
76
+
77
+        leakageMapper.insert(analysis);
78
+        return analysis;
79
+    }
80
+
81
+    /**
82
+     * 确定报警级别
83
+     */
84
+    private String determineAlarmLevel(BigDecimal nrwRate) {
85
+        if (nrwRate == null) return "normal";
86
+        if (nrwRate.compareTo(new BigDecimal("20")) > 0) {
87
+            return "critical";
88
+        } else if (nrwRate.compareTo(new BigDecimal("12")) > 0) {
89
+            return "warning";
90
+        }
91
+        return "normal";
92
+    }
93
+
94
+    /**
95
+     * 获取漏损趋势分析
96
+     */
97
+    public List<Map<String, Object>> getTrend(Long zoneId, int days) {
98
+        LocalDate endDate = LocalDate.now();
99
+        LocalDate startDate = endDate.minusDays(days);
100
+
101
+        LambdaQueryWrapper<DmaLeakageAnalysis> wrapper = new LambdaQueryWrapper<>();
102
+        wrapper.eq(DmaLeakageAnalysis::getZoneId, zoneId);
103
+        wrapper.ge(DmaLeakageAnalysis::getAnalysisDate, startDate);
104
+        wrapper.le(DmaLeakageAnalysis::getAnalysisDate, endDate);
105
+        wrapper.orderByAsc(DmaLeakageAnalysis::getAnalysisDate);
106
+
107
+        List<DmaLeakageAnalysis> analyses = leakageMapper.selectList(wrapper);
108
+
109
+        List<Map<String, Object>> trend = new ArrayList<>();
110
+        for (DmaLeakageAnalysis a : analyses) {
111
+            Map<String, Object> point = new LinkedHashMap<>();
112
+            point.put("date", a.getAnalysisDate());
113
+            point.put("supplyVolume", a.getSupplyVolume());
114
+            point.put("saleVolume", a.getSaleVolume());
115
+            point.put("leakageVolume", a.getLeakageVolume());
116
+            point.put("nrwRate", a.getNrwRate());
117
+            point.put("alarmLevel", a.getAlarmLevel());
118
+            trend.add(point);
119
+        }
120
+        return trend;
121
+    }
122
+
123
+    /**
124
+     * 获取报警列表
125
+     */
126
+    public List<DmaLeakageAnalysis> getAlarms(String alarmLevel) {
127
+        LambdaQueryWrapper<DmaLeakageAnalysis> wrapper = new LambdaQueryWrapper<>();
128
+        if (alarmLevel != null && !alarmLevel.isEmpty()) {
129
+            wrapper.eq(DmaLeakageAnalysis::getAlarmLevel, alarmLevel);
130
+        } else {
131
+            wrapper.in(DmaLeakageAnalysis::getAlarmLevel, "warning", "critical");
132
+        }
133
+        wrapper.orderByDesc(DmaLeakageAnalysis::getAnalysisDate);
134
+        return leakageMapper.selectList(wrapper);
135
+    }
136
+
137
+    /**
138
+     * 获取分区漏损汇总
139
+     */
140
+    public Map<String, Object> getZoneSummary(Long zoneId) {
141
+        LambdaQueryWrapper<DmaLeakageAnalysis> wrapper = new LambdaQueryWrapper<>();
142
+        wrapper.eq(DmaLeakageAnalysis::getZoneId, zoneId);
143
+        wrapper.orderByDesc(DmaLeakageAnalysis::getAnalysisDate);
144
+        wrapper.last("LIMIT 30");
145
+
146
+        List<DmaLeakageAnalysis> recent = leakageMapper.selectList(wrapper);
147
+
148
+        Map<String, Object> summary = new LinkedHashMap<>();
149
+        if (recent.isEmpty()) {
150
+            summary.put("zoneId", zoneId);
151
+            summary.put("avgNrwRate", BigDecimal.ZERO);
152
+            summary.put("totalLeakage", BigDecimal.ZERO);
153
+            summary.put("alarmCount", 0);
154
+            return summary;
155
+        }
156
+
157
+        BigDecimal totalNrw = BigDecimal.ZERO;
158
+        BigDecimal totalLeakage = BigDecimal.ZERO;
159
+        int alarmCount = 0;
160
+
161
+        for (DmaLeakageAnalysis a : recent) {
162
+            if (a.getNrwRate() != null) totalNrw = totalNrw.add(a.getNrwRate());
163
+            if (a.getLeakageVolume() != null) totalLeakage = totalLeakage.add(a.getLeakageVolume());
164
+            if (!"normal".equals(a.getAlarmLevel())) alarmCount++;
165
+        }
166
+
167
+        BigDecimal avgNrw = totalNrw.divide(new BigDecimal(recent.size()), 2, RoundingMode.HALF_UP);
168
+
169
+        DmaZone zone = zoneMapper.selectById(zoneId);
170
+        summary.put("zoneId", zoneId);
171
+        summary.put("zoneName", zone != null ? zone.getZoneName() : "");
172
+        summary.put("dataDays", recent.size());
173
+        summary.put("avgNrwRate", avgNrw);
174
+        summary.put("totalLeakage", totalLeakage);
175
+        summary.put("alarmCount", alarmCount);
176
+
177
+        return summary;
178
+    }
179
+}

+ 99
- 0
wm-dma/src/main/java/com/water/dma/service/DmaMeterService.java 查看文件

@@ -0,0 +1,99 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.dma.entity.DmaMeter;
6
+import com.water.dma.mapper.DmaMeterMapper;
7
+import lombok.RequiredArgsConstructor;
8
+import lombok.extern.slf4j.Slf4j;
9
+import org.springframework.stereotype.Service;
10
+
11
+import java.util.List;
12
+
13
+/**
14
+ * DMA计量表管理服务
15
+ */
16
+@Slf4j
17
+@Service
18
+@RequiredArgsConstructor
19
+public class DmaMeterService {
20
+
21
+    private final DmaMeterMapper meterMapper;
22
+
23
+    /**
24
+     * 分页查询计量表
25
+     */
26
+    public Page<DmaMeter> page(int pageNum, int pageSize, Long zoneId, String meterType) {
27
+        LambdaQueryWrapper<DmaMeter> wrapper = new LambdaQueryWrapper<>();
28
+        if (zoneId != null) {
29
+            wrapper.eq(DmaMeter::getZoneId, zoneId);
30
+        }
31
+        if (meterType != null && !meterType.isEmpty()) {
32
+            wrapper.eq(DmaMeter::getMeterType, meterType);
33
+        }
34
+        wrapper.orderByAsc(DmaMeter::getMeterCode);
35
+        return meterMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
36
+    }
37
+
38
+    /**
39
+     * 获取计量表详情
40
+     */
41
+    public DmaMeter getById(Long id) {
42
+        return meterMapper.selectById(id);
43
+    }
44
+
45
+    /**
46
+     * 创建计量表
47
+     */
48
+    public DmaMeter create(DmaMeter meter) {
49
+        if (meter.getStatus() == null) {
50
+            meter.setStatus("online");
51
+        }
52
+        meterMapper.insert(meter);
53
+        return meter;
54
+    }
55
+
56
+    /**
57
+     * 更新计量表
58
+     */
59
+    public void update(DmaMeter meter) {
60
+        meterMapper.updateById(meter);
61
+    }
62
+
63
+    /**
64
+     * 删除计量表
65
+     */
66
+    public void delete(Long id) {
67
+        meterMapper.deleteById(id);
68
+    }
69
+
70
+    /**
71
+     * 获取分区下的所有计量表
72
+     */
73
+    public List<DmaMeter> listByZoneId(Long zoneId) {
74
+        LambdaQueryWrapper<DmaMeter> wrapper = new LambdaQueryWrapper<>();
75
+        wrapper.eq(DmaMeter::getZoneId, zoneId);
76
+        return meterMapper.selectList(wrapper);
77
+    }
78
+
79
+    /**
80
+     * 统计分区表计数量
81
+     */
82
+    public Long countByZoneId(Long zoneId) {
83
+        LambdaQueryWrapper<DmaMeter> wrapper = new LambdaQueryWrapper<>();
84
+        wrapper.eq(DmaMeter::getZoneId, zoneId);
85
+        return meterMapper.selectCount(wrapper);
86
+    }
87
+
88
+    /**
89
+     * 绑定计量表到分区
90
+     */
91
+    public void bindToZone(Long meterId, Long zoneId) {
92
+        DmaMeter meter = meterMapper.selectById(meterId);
93
+        if (meter == null) {
94
+            throw new RuntimeException("计量表不存在");
95
+        }
96
+        meter.setZoneId(zoneId);
97
+        meterMapper.updateById(meter);
98
+    }
99
+}

+ 122
- 0
wm-dma/src/main/java/com/water/dma/service/DmaZoneService.java 查看文件

@@ -0,0 +1,122 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.dma.entity.DmaZone;
6
+import com.water.dma.mapper.DmaZoneMapper;
7
+import lombok.RequiredArgsConstructor;
8
+import lombok.extern.slf4j.Slf4j;
9
+import org.springframework.stereotype.Service;
10
+
11
+import java.util.*;
12
+
13
+/**
14
+ * DMA分区管理服务
15
+ */
16
+@Slf4j
17
+@Service
18
+@RequiredArgsConstructor
19
+public class DmaZoneService {
20
+
21
+    private final DmaZoneMapper zoneMapper;
22
+
23
+    /**
24
+     * 分页查询分区
25
+     */
26
+    public Page<DmaZone> page(int pageNum, int pageSize, String zoneName) {
27
+        LambdaQueryWrapper<DmaZone> wrapper = new LambdaQueryWrapper<>();
28
+        if (zoneName != null && !zoneName.isEmpty()) {
29
+            wrapper.like(DmaZone::getZoneName, zoneName);
30
+        }
31
+        wrapper.orderByAsc(DmaZone::getZoneLevel, DmaZone::getZoneCode);
32
+        return zoneMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
33
+    }
34
+
35
+    /**
36
+     * 获取分区详情
37
+     */
38
+    public DmaZone getById(Long id) {
39
+        return zoneMapper.selectById(id);
40
+    }
41
+
42
+    /**
43
+     * 创建分区
44
+     */
45
+    public DmaZone create(DmaZone zone) {
46
+        if (zone.getStatus() == null) {
47
+            zone.setStatus("active");
48
+        }
49
+        zoneMapper.insert(zone);
50
+        return zone;
51
+    }
52
+
53
+    /**
54
+     * 更新分区
55
+     */
56
+    public void update(DmaZone zone) {
57
+        zoneMapper.updateById(zone);
58
+    }
59
+
60
+    /**
61
+     * 删除分区
62
+     */
63
+    public void delete(Long id) {
64
+        // 检查是否有子分区
65
+        LambdaQueryWrapper<DmaZone> wrapper = new LambdaQueryWrapper<>();
66
+        wrapper.eq(DmaZone::getParentId, id);
67
+        Long count = zoneMapper.selectCount(wrapper);
68
+        if (count > 0) {
69
+            throw new RuntimeException("该分区存在子分区,无法删除");
70
+        }
71
+        zoneMapper.deleteById(id);
72
+    }
73
+
74
+    /**
75
+     * 获取分区树形结构
76
+     */
77
+    public List<Map<String, Object>> getZoneTree() {
78
+        LambdaQueryWrapper<DmaZone> wrapper = new LambdaQueryWrapper<>();
79
+        wrapper.orderByAsc(DmaZone::getZoneLevel, DmaZone::getZoneCode);
80
+        List<DmaZone> allZones = zoneMapper.selectList(wrapper);
81
+
82
+        Map<Long, Map<String, Object>> zoneMap = new LinkedHashMap<>();
83
+        List<Map<String, Object>> roots = new ArrayList<>();
84
+
85
+        for (DmaZone zone : allZones) {
86
+            Map<String, Object> node = new LinkedHashMap<>();
87
+            node.put("id", zone.getId());
88
+            node.put("zoneName", zone.getZoneName());
89
+            node.put("zoneCode", zone.getZoneCode());
90
+            node.put("zoneLevel", zone.getZoneLevel());
91
+            node.put("parentId", zone.getParentId());
92
+            node.put("area", zone.getArea());
93
+            node.put("status", zone.getStatus());
94
+            node.put("children", new ArrayList<>());
95
+            zoneMap.put(zone.getId(), node);
96
+        }
97
+
98
+        for (Map.Entry<Long, Map<String, Object>> entry : zoneMap.entrySet()) {
99
+            Map<String, Object> node = entry.getValue();
100
+            Long parentId = (Long) node.get("parentId");
101
+            if (parentId == null) {
102
+                roots.add(node);
103
+            } else {
104
+                Map<String, Object> parent = zoneMap.get(parentId);
105
+                if (parent != null) {
106
+                    @SuppressWarnings("unchecked")
107
+                    List<Map<String, Object>> children = (List<Map<String, Object>>) parent.get("children");
108
+                    children.add(node);
109
+                }
110
+            }
111
+        }
112
+
113
+        return roots;
114
+    }
115
+
116
+    /**
117
+     * 获取所有分区列表
118
+     */
119
+    public List<DmaZone> listAll() {
120
+        return zoneMapper.selectList(new LambdaQueryWrapper<>());
121
+    }
122
+}

+ 168
- 0
wm-dma/src/main/java/com/water/dma/service/WaterBalanceService.java 查看文件

@@ -0,0 +1,168 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.dma.entity.WaterBalance;
6
+import com.water.dma.mapper.WaterBalanceMapper;
7
+import lombok.RequiredArgsConstructor;
8
+import lombok.extern.slf4j.Slf4j;
9
+import org.springframework.stereotype.Service;
10
+
11
+import java.math.BigDecimal;
12
+import java.math.RoundingMode;
13
+import java.time.LocalDate;
14
+import java.util.*;
15
+
16
+/**
17
+ * 水平衡分析服务
18
+ */
19
+@Slf4j
20
+@Service
21
+@RequiredArgsConstructor
22
+public class WaterBalanceService {
23
+
24
+    private final WaterBalanceMapper balanceMapper;
25
+
26
+    /**
27
+     * 分页查询水平衡数据
28
+     */
29
+    public Page<WaterBalance> page(int pageNum, int pageSize, Long zoneId, String period,
30
+                                    LocalDate startDate, LocalDate endDate) {
31
+        LambdaQueryWrapper<WaterBalance> wrapper = new LambdaQueryWrapper<>();
32
+        if (zoneId != null) {
33
+            wrapper.eq(WaterBalance::getZoneId, zoneId);
34
+        }
35
+        if (period != null && !period.isEmpty()) {
36
+            wrapper.eq(WaterBalance::getPeriod, period);
37
+        }
38
+        if (startDate != null) {
39
+            wrapper.ge(WaterBalance::getStatDate, startDate);
40
+        }
41
+        if (endDate != null) {
42
+            wrapper.le(WaterBalance::getStatDate, endDate);
43
+        }
44
+        wrapper.orderByDesc(WaterBalance::getStatDate);
45
+        return balanceMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
46
+    }
47
+
48
+    /**
49
+     * 创建水平衡记录
50
+     */
51
+    public WaterBalance create(WaterBalance balance) {
52
+        // 自动计算总漏损
53
+        if (balance.getTotalLoss() == null && balance.getTotalSupply() != null && balance.getTotalSale() != null) {
54
+            balance.setTotalLoss(balance.getTotalSupply().subtract(balance.getTotalSale()));
55
+        }
56
+        // 计算产销差率
57
+        calculateRates(balance);
58
+        balanceMapper.insert(balance);
59
+        return balance;
60
+    }
61
+
62
+    /**
63
+     * 更新水平衡记录
64
+     */
65
+    public void update(WaterBalance balance) {
66
+        calculateRates(balance);
67
+        balanceMapper.updateById(balance);
68
+    }
69
+
70
+    /**
71
+     * 删除水平衡记录
72
+     */
73
+    public void delete(Long id) {
74
+        balanceMapper.deleteById(id);
75
+    }
76
+
77
+    /**
78
+     * 计算产销差率和漏损率
79
+     */
80
+    private void calculateRates(WaterBalance balance) {
81
+        if (balance.getTotalSupply() != null && balance.getTotalSupply().compareTo(BigDecimal.ZERO) > 0) {
82
+            BigDecimal totalLoss = balance.getTotalLoss() != null ? balance.getTotalLoss() : BigDecimal.ZERO;
83
+            BigDecimal rate = totalLoss.multiply(new BigDecimal("100"))
84
+                    .divide(balance.getTotalSupply(), 2, RoundingMode.HALF_UP);
85
+            balance.setNrwRate(rate);
86
+            balance.setLeakageRate(rate);
87
+        }
88
+    }
89
+
90
+    /**
91
+     * 生成水平衡分析报告
92
+     */
93
+    public Map<String, Object> generateReport(Long zoneId, String period, LocalDate startDate, LocalDate endDate) {
94
+        LambdaQueryWrapper<WaterBalance> wrapper = new LambdaQueryWrapper<>();
95
+        wrapper.eq(WaterBalance::getZoneId, zoneId);
96
+        wrapper.eq(WaterBalance::getPeriod, period);
97
+        wrapper.ge(WaterBalance::getStatDate, startDate);
98
+        wrapper.le(WaterBalance::getStatDate, endDate);
99
+
100
+        List<WaterBalance> records = balanceMapper.selectList(wrapper);
101
+
102
+        Map<String, Object> report = new LinkedHashMap<>();
103
+        report.put("zoneId", zoneId);
104
+        report.put("period", period);
105
+        report.put("startDate", startDate);
106
+        report.put("endDate", endDate);
107
+
108
+        if (records.isEmpty()) {
109
+            report.put("totalSupply", BigDecimal.ZERO);
110
+            report.put("totalSale", BigDecimal.ZERO);
111
+            report.put("totalLoss", BigDecimal.ZERO);
112
+            report.put("avgNrwRate", BigDecimal.ZERO);
113
+            report.put("recordCount", 0);
114
+            return report;
115
+        }
116
+
117
+        BigDecimal totalSupply = BigDecimal.ZERO;
118
+        BigDecimal totalSale = BigDecimal.ZERO;
119
+        BigDecimal totalLoss = BigDecimal.ZERO;
120
+        BigDecimal totalApparentLoss = BigDecimal.ZERO;
121
+        BigDecimal totalRealLoss = BigDecimal.ZERO;
122
+
123
+        for (WaterBalance r : records) {
124
+            if (r.getTotalSupply() != null) totalSupply = totalSupply.add(r.getTotalSupply());
125
+            if (r.getTotalSale() != null) totalSale = totalSale.add(r.getTotalSale());
126
+            if (r.getTotalLoss() != null) totalLoss = totalLoss.add(r.getTotalLoss());
127
+            if (r.getApparentLoss() != null) totalApparentLoss = totalApparentLoss.add(r.getApparentLoss());
128
+            if (r.getRealLoss() != null) totalRealLoss = totalRealLoss.add(r.getRealLoss());
129
+        }
130
+
131
+        BigDecimal avgNrwRate = BigDecimal.ZERO;
132
+        if (totalSupply.compareTo(BigDecimal.ZERO) > 0) {
133
+            avgNrwRate = totalLoss.multiply(new BigDecimal("100"))
134
+                    .divide(totalSupply, 2, RoundingMode.HALF_UP);
135
+        }
136
+
137
+        report.put("totalSupply", totalSupply);
138
+        report.put("totalSale", totalSale);
139
+        report.put("totalLoss", totalLoss);
140
+        report.put("apparentLoss", totalApparentLoss);
141
+        report.put("realLoss", totalRealLoss);
142
+        report.put("avgNrwRate", avgNrwRate);
143
+        report.put("recordCount", records.size());
144
+
145
+        // IWA水平衡组成
146
+        Map<String, Object> iwa = new LinkedHashMap<>();
147
+        iwa.put("billingSale", records.stream()
148
+                .map(WaterBalance::getBillingSale)
149
+                .filter(Objects::nonNull)
150
+                .reduce(BigDecimal.ZERO, BigDecimal::add));
151
+        iwa.put("freeSupply", records.stream()
152
+                .map(WaterBalance::getFreeSupply)
153
+                .filter(Objects::nonNull)
154
+                .reduce(BigDecimal.ZERO, BigDecimal::add));
155
+        iwa.put("apparentLoss", totalApparentLoss);
156
+        iwa.put("realLoss", totalRealLoss);
157
+        report.put("iwaComponents", iwa);
158
+
159
+        return report;
160
+    }
161
+
162
+    /**
163
+     * 获取水平衡详情
164
+     */
165
+    public WaterBalance getById(Long id) {
166
+        return balanceMapper.selectById(id);
167
+    }
168
+}

+ 29
- 0
wm-dma/src/main/resources/application.yml 查看文件

@@ -0,0 +1,29 @@
1
+server:
2
+  port: 8090
3
+
4
+spring:
5
+  application:
6
+    name: wm-dma
7
+  datasource:
8
+    url: jdbc:postgresql://localhost:5432/water_management
9
+    username: water
10
+    password: water123
11
+    driver-class-name: org.postgresql.Driver
12
+  cloud:
13
+    nacos:
14
+      discovery:
15
+        server-addr: localhost:8848
16
+
17
+mybatis-plus:
18
+  global-config:
19
+    db-config:
20
+      logic-delete-field: deleted
21
+      logic-delete-value: 1
22
+      logic-not-delete-value: 0
23
+  configuration:
24
+    map-underscore-to-camel-case: true
25
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
26
+
27
+logging:
28
+  level:
29
+    com.water.dma: DEBUG

+ 130
- 0
wm-dma/src/test/java/com/water/dma/service/DmaFlowServiceTest.java 查看文件

@@ -0,0 +1,130 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dma.entity.DmaFlowRecord;
5
+import com.water.dma.mapper.DmaFlowRecordMapper;
6
+import org.junit.jupiter.api.BeforeEach;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.Mock;
11
+import org.mockito.junit.jupiter.MockitoExtension;
12
+
13
+import java.math.BigDecimal;
14
+import java.time.LocalDate;
15
+import java.time.LocalDateTime;
16
+import java.util.*;
17
+
18
+import static org.junit.jupiter.api.Assertions.*;
19
+import static org.mockito.ArgumentMatchers.*;
20
+import static org.mockito.Mockito.*;
21
+
22
+/**
23
+ * DMA流量服务测试
24
+ */
25
+@ExtendWith(MockitoExtension.class)
26
+class DmaFlowServiceTest {
27
+
28
+    @Mock
29
+    private DmaFlowRecordMapper flowRecordMapper;
30
+
31
+    private DmaFlowService flowService;
32
+
33
+    @BeforeEach
34
+    void setUp() {
35
+        flowService = new DmaFlowService(flowRecordMapper);
36
+    }
37
+
38
+    @Test
39
+    @DisplayName("创建流量记录")
40
+    void testCreate() {
41
+        DmaFlowRecord record = new DmaFlowRecord();
42
+        record.setZoneId(1L);
43
+        record.setMeterId(1L);
44
+        record.setInstantFlow(new BigDecimal("12.5"));
45
+        record.setTotalFlow(new BigDecimal("1000"));
46
+        record.setCollectTime(LocalDateTime.now());
47
+
48
+        when(flowRecordMapper.insert(any(DmaFlowRecord.class))).thenReturn(1);
49
+
50
+        DmaFlowRecord created = flowService.create(record);
51
+        assertNotNull(created);
52
+        assertEquals("good", created.getDataQuality());
53
+        verify(flowRecordMapper).insert(any(DmaFlowRecord.class));
54
+    }
55
+
56
+    @Test
57
+    @DisplayName("批量创建流量记录")
58
+    void testBatchCreate() {
59
+        DmaFlowRecord r1 = new DmaFlowRecord();
60
+        r1.setZoneId(1L);
61
+        r1.setMeterId(1L);
62
+        r1.setInstantFlow(new BigDecimal("10"));
63
+        r1.setCollectTime(LocalDateTime.now());
64
+
65
+        DmaFlowRecord r2 = new DmaFlowRecord();
66
+        r2.setZoneId(1L);
67
+        r2.setMeterId(2L);
68
+        r2.setInstantFlow(new BigDecimal("15"));
69
+        r2.setCollectTime(LocalDateTime.now());
70
+
71
+        when(flowRecordMapper.insert(any(DmaFlowRecord.class))).thenReturn(1);
72
+
73
+        flowService.batchCreate(Arrays.asList(r1, r2));
74
+        verify(flowRecordMapper, times(2)).insert(any(DmaFlowRecord.class));
75
+    }
76
+
77
+    @Test
78
+    @DisplayName("获取分区进出水量汇总")
79
+    void testGetZoneFlowSummary() {
80
+        Map<String, Object> row = new HashMap<>();
81
+        row.put("meter_id", 1L);
82
+        row.put("total_flow", new BigDecimal("100"));
83
+
84
+        when(flowRecordMapper.sumFlowByMeter(eq(1L), any(), any())).thenReturn(List.of(row));
85
+
86
+        Map<String, Object> summary = flowService.getZoneFlowSummary(1L,
87
+                LocalDateTime.now().minusHours(24), LocalDateTime.now());
88
+
89
+        assertNotNull(summary);
90
+        assertEquals(1L, summary.get("zoneId"));
91
+        assertEquals(1, summary.get("meterCount"));
92
+    }
93
+
94
+    @Test
95
+    @DisplayName("MNF分析-正常")
96
+    void testGetMNFAnalysis() {
97
+        when(flowRecordMapper.getMNF(eq(1L), any())).thenReturn(new BigDecimal("3.5"));
98
+
99
+        Map<String, Object> result = flowService.getMNFAnalysis(1L, LocalDate.now());
100
+
101
+        assertNotNull(result);
102
+        assertEquals(new BigDecimal("3.5"), result.get("mnf"));
103
+        assertEquals("正常", result.get("analysisResult"));
104
+    }
105
+
106
+    @Test
107
+    @DisplayName("MNF分析-无数据")
108
+    void testGetMNFAnalysisNoData() {
109
+        when(flowRecordMapper.getMNF(eq(1L), any())).thenReturn(null);
110
+
111
+        Map<String, Object> result = flowService.getMNFAnalysis(1L, LocalDate.now());
112
+
113
+        assertEquals(BigDecimal.ZERO, result.get("mnf"));
114
+    }
115
+
116
+    @Test
117
+    @DisplayName("获取流量趋势")
118
+    void testGetFlowTrend() {
119
+        DmaFlowRecord record = new DmaFlowRecord();
120
+        record.setCollectTime(LocalDateTime.now());
121
+        record.setInstantFlow(new BigDecimal("12.5"));
122
+
123
+        when(flowRecordMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(record));
124
+
125
+        List<Map<String, Object>> trend = flowService.getFlowTrend(1L,
126
+                LocalDateTime.now().minusHours(24), LocalDateTime.now());
127
+
128
+        assertEquals(1, trend.size());
129
+    }
130
+}

+ 124
- 0
wm-dma/src/test/java/com/water/dma/service/DmaLeakageServiceTest.java 查看文件

@@ -0,0 +1,124 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dma.entity.DmaLeakageAnalysis;
5
+import com.water.dma.entity.DmaZone;
6
+import com.water.dma.mapper.DmaLeakageAnalysisMapper;
7
+import com.water.dma.mapper.DmaZoneMapper;
8
+import org.junit.jupiter.api.BeforeEach;
9
+import org.junit.jupiter.api.DisplayName;
10
+import org.junit.jupiter.api.Test;
11
+import org.junit.jupiter.api.extension.ExtendWith;
12
+import org.mockito.Mock;
13
+import org.mockito.junit.jupiter.MockitoExtension;
14
+
15
+import java.math.BigDecimal;
16
+import java.time.LocalDate;
17
+import java.util.List;
18
+import java.util.Map;
19
+
20
+import static org.junit.jupiter.api.Assertions.*;
21
+import static org.mockito.ArgumentMatchers.*;
22
+import static org.mockito.Mockito.*;
23
+
24
+/**
25
+ * DMA漏损分析服务测试
26
+ */
27
+@ExtendWith(MockitoExtension.class)
28
+class DmaLeakageServiceTest {
29
+
30
+    @Mock
31
+    private DmaLeakageAnalysisMapper leakageMapper;
32
+
33
+    @Mock
34
+    private DmaZoneMapper zoneMapper;
35
+
36
+    private DmaLeakageService leakageService;
37
+
38
+    @BeforeEach
39
+    void setUp() {
40
+        leakageService = new DmaLeakageService(leakageMapper, zoneMapper);
41
+    }
42
+
43
+    @Test
44
+    @DisplayName("执行漏损分析-正常")
45
+    void testAnalyze() {
46
+        BigDecimal supply = new BigDecimal("1000");
47
+        BigDecimal sale = new BigDecimal("850");
48
+
49
+        when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
50
+
51
+        DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
52
+
53
+        assertNotNull(result);
54
+        assertEquals(new BigDecimal("150"), result.getLeakageVolume());
55
+        assertEquals(0, new BigDecimal("15.00").compareTo(result.getNrwRate()));
56
+        assertEquals("warning", result.getAlarmLevel());
57
+        verify(leakageMapper).insert(any(DmaLeakageAnalysis.class));
58
+    }
59
+
60
+    @Test
61
+    @DisplayName("执行漏损分析-高漏损报警")
62
+    void testAnalyzeHighAlarm() {
63
+        BigDecimal supply = new BigDecimal("1000");
64
+        BigDecimal sale = new BigDecimal("700");
65
+
66
+        when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
67
+
68
+        DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
69
+
70
+        assertEquals("critical", result.getAlarmLevel());
71
+    }
72
+
73
+    @Test
74
+    @DisplayName("执行漏损分析-零供水")
75
+    void testAnalyzeZeroSupply() {
76
+        BigDecimal supply = BigDecimal.ZERO;
77
+        BigDecimal sale = BigDecimal.ZERO;
78
+
79
+        when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
80
+
81
+        DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
82
+
83
+        assertEquals(0, BigDecimal.ZERO.compareTo(result.getNrwRate()));
84
+        assertEquals("normal", result.getAlarmLevel());
85
+    }
86
+
87
+    @Test
88
+    @DisplayName("获取漏损趋势")
89
+    void testGetTrend() {
90
+        DmaLeakageAnalysis analysis = new DmaLeakageAnalysis();
91
+        analysis.setZoneId(1L);
92
+        analysis.setAnalysisDate(LocalDate.now());
93
+        analysis.setNrwRate(new BigDecimal("12.5"));
94
+
95
+        when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(analysis));
96
+
97
+        List<Map<String, Object>> trend = leakageService.getTrend(1L, 30);
98
+        assertEquals(1, trend.size());
99
+        assertEquals(new BigDecimal("12.5"), trend.get(0).get("nrwRate"));
100
+    }
101
+
102
+    @Test
103
+    @DisplayName("获取分区漏损汇总-无数据")
104
+    void testGetZoneSummaryEmpty() {
105
+        when(zoneMapper.selectById(1L)).thenReturn(null);
106
+        when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of());
107
+
108
+        Map<String, Object> summary = leakageService.getZoneSummary(1L);
109
+        assertEquals(0, ((BigDecimal) summary.get("avgNrwRate")).compareTo(BigDecimal.ZERO));
110
+        assertEquals(0, summary.get("alarmCount"));
111
+    }
112
+
113
+    @Test
114
+    @DisplayName("获取报警列表")
115
+    void testGetAlarms() {
116
+        DmaLeakageAnalysis alarm = new DmaLeakageAnalysis();
117
+        alarm.setAlarmLevel("critical");
118
+
119
+        when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(alarm));
120
+
121
+        List<DmaLeakageAnalysis> alarms = leakageService.getAlarms(null);
122
+        assertFalse(alarms.isEmpty());
123
+    }
124
+}

+ 100
- 0
wm-dma/src/test/java/com/water/dma/service/DmaMeterServiceTest.java 查看文件

@@ -0,0 +1,100 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dma.entity.DmaMeter;
5
+import com.water.dma.mapper.DmaMeterMapper;
6
+import org.junit.jupiter.api.BeforeEach;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.Mock;
11
+import org.mockito.junit.jupiter.MockitoExtension;
12
+
13
+import java.math.BigDecimal;
14
+import java.util.List;
15
+
16
+import static org.junit.jupiter.api.Assertions.*;
17
+import static org.mockito.ArgumentMatchers.*;
18
+import static org.mockito.Mockito.*;
19
+
20
+/**
21
+ * DMA计量表服务测试
22
+ */
23
+@ExtendWith(MockitoExtension.class)
24
+class DmaMeterServiceTest {
25
+
26
+    @Mock
27
+    private DmaMeterMapper meterMapper;
28
+
29
+    private DmaMeterService meterService;
30
+
31
+    @BeforeEach
32
+    void setUp() {
33
+        meterService = new DmaMeterService(meterMapper);
34
+    }
35
+
36
+    @Test
37
+    @DisplayName("创建计量表")
38
+    void testCreateMeter() {
39
+        DmaMeter meter = new DmaMeter();
40
+        meter.setZoneId(1L);
41
+        meter.setMeterCode("M-001");
42
+        meter.setMeterName("进水表1");
43
+        meter.setMeterType("inlet");
44
+
45
+        when(meterMapper.insert(any(DmaMeter.class))).thenReturn(1);
46
+
47
+        DmaMeter created = meterService.create(meter);
48
+        assertNotNull(created);
49
+        assertEquals("online", created.getStatus());
50
+        verify(meterMapper).insert(any(DmaMeter.class));
51
+    }
52
+
53
+    @Test
54
+    @DisplayName("绑定计量表到分区")
55
+    void testBindToZone() {
56
+        DmaMeter meter = new DmaMeter();
57
+        meter.setId(1L);
58
+        meter.setMeterCode("M-001");
59
+        meter.setZoneId(null);
60
+
61
+        when(meterMapper.selectById(1L)).thenReturn(meter);
62
+        when(meterMapper.updateById(any(DmaMeter.class))).thenReturn(1);
63
+
64
+        meterService.bindToZone(1L, 2L);
65
+        assertEquals(2L, meter.getZoneId());
66
+        verify(meterMapper).updateById(any(DmaMeter.class));
67
+    }
68
+
69
+    @Test
70
+    @DisplayName("绑定不存在的计量表-失败")
71
+    void testBindToZoneNotFound() {
72
+        when(meterMapper.selectById(999L)).thenReturn(null);
73
+
74
+        assertThrows(RuntimeException.class, () -> {
75
+            meterService.bindToZone(999L, 1L);
76
+        });
77
+    }
78
+
79
+    @Test
80
+    @DisplayName("统计分区表计数量")
81
+    void testCountByZoneId() {
82
+        when(meterMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(5L);
83
+
84
+        Long count = meterService.countByZoneId(1L);
85
+        assertEquals(5L, count);
86
+    }
87
+
88
+    @Test
89
+    @DisplayName("获取分区下的所有计量表")
90
+    void testListByZoneId() {
91
+        DmaMeter meter = new DmaMeter();
92
+        meter.setId(1L);
93
+        meter.setZoneId(1L);
94
+
95
+        when(meterMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(meter));
96
+
97
+        List<DmaMeter> meters = meterService.listByZoneId(1L);
98
+        assertEquals(1, meters.size());
99
+    }
100
+}

+ 112
- 0
wm-dma/src/test/java/com/water/dma/service/DmaZoneServiceTest.java 查看文件

@@ -0,0 +1,112 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dma.entity.DmaZone;
5
+import com.water.dma.mapper.DmaZoneMapper;
6
+import org.junit.jupiter.api.BeforeEach;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.Mock;
11
+import org.mockito.junit.jupiter.MockitoExtension;
12
+
13
+import java.math.BigDecimal;
14
+import java.util.Arrays;
15
+import java.util.List;
16
+import java.util.Map;
17
+
18
+import static org.junit.jupiter.api.Assertions.*;
19
+import static org.mockito.ArgumentMatchers.*;
20
+import static org.mockito.Mockito.*;
21
+
22
+/**
23
+ * DMA分区服务测试
24
+ */
25
+@ExtendWith(MockitoExtension.class)
26
+class DmaZoneServiceTest {
27
+
28
+    @Mock
29
+    private DmaZoneMapper zoneMapper;
30
+
31
+    private DmaZoneService zoneService;
32
+
33
+    @BeforeEach
34
+    void setUp() {
35
+        zoneService = new DmaZoneService(zoneMapper);
36
+    }
37
+
38
+    @Test
39
+    @DisplayName("创建DMA分区")
40
+    void testCreateZone() {
41
+        DmaZone zone = new DmaZone();
42
+        zone.setZoneName("测试分区A");
43
+        zone.setZoneCode("DMA-001");
44
+        zone.setZoneLevel(1);
45
+        zone.setArea("城北区");
46
+
47
+        when(zoneMapper.insert(any(DmaZone.class))).thenReturn(1);
48
+
49
+        DmaZone created = zoneService.create(zone);
50
+        assertNotNull(created);
51
+        assertEquals("active", created.getStatus());
52
+        verify(zoneMapper).insert(any(DmaZone.class));
53
+    }
54
+
55
+    @Test
56
+    @DisplayName("获取分区树形结构")
57
+    void testGetZoneTree() {
58
+        DmaZone parent = new DmaZone();
59
+        parent.setId(1L);
60
+        parent.setZoneName("总区");
61
+        parent.setZoneCode("ROOT");
62
+        parent.setZoneLevel(1);
63
+        parent.setParentId(null);
64
+
65
+        DmaZone child = new DmaZone();
66
+        child.setId(2L);
67
+        child.setZoneName("子区A");
68
+        child.setZoneCode("A");
69
+        child.setZoneLevel(2);
70
+        child.setParentId(1L);
71
+
72
+        when(zoneMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Arrays.asList(parent, child));
73
+
74
+        List<Map<String, Object>> tree = zoneService.getZoneTree();
75
+        assertNotNull(tree);
76
+        assertEquals(1, tree.size());
77
+        assertEquals("总区", tree.get(0).get("zoneName"));
78
+    }
79
+
80
+    @Test
81
+    @DisplayName("删除分区-存在子分区时失败")
82
+    void testDeleteZoneWithChildren() {
83
+        when(zoneMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
84
+
85
+        assertThrows(RuntimeException.class, () -> {
86
+            zoneService.delete(1L);
87
+        });
88
+    }
89
+
90
+    @Test
91
+    @DisplayName("删除分区-无子分区时成功")
92
+    void testDeleteZoneSuccess() {
93
+        when(zoneMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
94
+        when(zoneMapper.deleteById(1L)).thenReturn(1);
95
+
96
+        zoneService.delete(1L);
97
+        verify(zoneMapper).deleteById(1L);
98
+    }
99
+
100
+    @Test
101
+    @DisplayName("获取所有分区列表")
102
+    void testListAll() {
103
+        DmaZone zone = new DmaZone();
104
+        zone.setId(1L);
105
+        zone.setZoneName("测试分区");
106
+
107
+        when(zoneMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(zone));
108
+
109
+        List<DmaZone> zones = zoneService.listAll();
110
+        assertEquals(1, zones.size());
111
+    }
112
+}

+ 117
- 0
wm-dma/src/test/java/com/water/dma/service/WaterBalanceServiceTest.java 查看文件

@@ -0,0 +1,117 @@
1
+package com.water.dma.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dma.entity.WaterBalance;
5
+import com.water.dma.mapper.WaterBalanceMapper;
6
+import org.junit.jupiter.api.BeforeEach;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.Mock;
11
+import org.mockito.junit.jupiter.MockitoExtension;
12
+
13
+import java.math.BigDecimal;
14
+import java.time.LocalDate;
15
+import java.util.List;
16
+import java.util.Map;
17
+
18
+import static org.junit.jupiter.api.Assertions.*;
19
+import static org.mockito.ArgumentMatchers.*;
20
+import static org.mockito.Mockito.*;
21
+
22
+/**
23
+ * 水平衡服务测试
24
+ */
25
+@ExtendWith(MockitoExtension.class)
26
+class WaterBalanceServiceTest {
27
+
28
+    @Mock
29
+    private WaterBalanceMapper balanceMapper;
30
+
31
+    private WaterBalanceService balanceService;
32
+
33
+    @BeforeEach
34
+    void setUp() {
35
+        balanceService = new WaterBalanceService(balanceMapper);
36
+    }
37
+
38
+    @Test
39
+    @DisplayName("创建水平衡记录-自动计算漏损")
40
+    void testCreate() {
41
+        WaterBalance balance = new WaterBalance();
42
+        balance.setZoneId(1L);
43
+        balance.setPeriod("monthly");
44
+        balance.setStatDate(LocalDate.of(2024, 1, 1));
45
+        balance.setTotalSupply(new BigDecimal("10000"));
46
+        balance.setTotalSale(new BigDecimal("8500"));
47
+
48
+        when(balanceMapper.insert(any(WaterBalance.class))).thenReturn(1);
49
+
50
+        WaterBalance created = balanceService.create(balance);
51
+
52
+        assertNotNull(created);
53
+        assertEquals(0, new BigDecimal("1500").compareTo(created.getTotalLoss()));
54
+        assertEquals(0, new BigDecimal("15.00").compareTo(created.getNrwRate()));
55
+        verify(balanceMapper).insert(any(WaterBalance.class));
56
+    }
57
+
58
+    @Test
59
+    @DisplayName("创建水平衡-零供水")
60
+    void testCreateZeroSupply() {
61
+        WaterBalance balance = new WaterBalance();
62
+        balance.setZoneId(1L);
63
+        balance.setPeriod("daily");
64
+        balance.setStatDate(LocalDate.now());
65
+        balance.setTotalSupply(BigDecimal.ZERO);
66
+        balance.setTotalSale(BigDecimal.ZERO);
67
+        balance.setTotalLoss(BigDecimal.ZERO);
68
+
69
+        when(balanceMapper.insert(any(WaterBalance.class))).thenReturn(1);
70
+
71
+        WaterBalance created = balanceService.create(balance);
72
+        assertNotNull(created);
73
+    }
74
+
75
+    @Test
76
+    @DisplayName("生成水平衡报告-有数据")
77
+    void testGenerateReport() {
78
+        WaterBalance b1 = new WaterBalance();
79
+        b1.setTotalSupply(new BigDecimal("5000"));
80
+        b1.setTotalSale(new BigDecimal("4200"));
81
+        b1.setTotalLoss(new BigDecimal("800"));
82
+        b1.setApparentLoss(new BigDecimal("200"));
83
+        b1.setRealLoss(new BigDecimal("600"));
84
+        b1.setBillingSale(new BigDecimal("4000"));
85
+        b1.setFreeSupply(new BigDecimal("200"));
86
+
87
+        when(balanceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(b1));
88
+
89
+        Map<String, Object> report = balanceService.generateReport(1L, "monthly",
90
+                LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 31));
91
+
92
+        assertNotNull(report);
93
+        assertEquals(1, report.get("recordCount"));
94
+        assertEquals(new BigDecimal("5000"), report.get("totalSupply"));
95
+        assertEquals(new BigDecimal("800"), report.get("totalLoss"));
96
+    }
97
+
98
+    @Test
99
+    @DisplayName("生成水平衡报告-无数据")
100
+    void testGenerateReportEmpty() {
101
+        when(balanceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of());
102
+
103
+        Map<String, Object> report = balanceService.generateReport(1L, "monthly",
104
+                LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 31));
105
+
106
+        assertEquals(0, report.get("recordCount"));
107
+        assertEquals(BigDecimal.ZERO, report.get("totalSupply"));
108
+    }
109
+
110
+    @Test
111
+    @DisplayName("删除水平衡记录")
112
+    void testDelete() {
113
+        when(balanceMapper.deleteById(1L)).thenReturn(1);
114
+        balanceService.delete(1L);
115
+        verify(balanceMapper).deleteById(1L);
116
+    }
117
+}