Explorar el Código

feat(wm-data-engine): #71 历史数据回溯与报表生成

- HistoryDataService: 水量/水质历史数据分页查询 + 导出
- ReportService: 日报/周报/月报/年报自动生成 + 发布 + 模板管理
- StatisticsService: 同比/环比/趋势分析 + 综合看板
- 5个Entity + 5个Mapper + 3个Service + 1个Controller(12+端点)
- DDL: 4张表 + 5个索引
- 单元测试: 3个测试类
bot_dev2 hace 5 días
padre
commit
08649f027e
Se han modificado 19 ficheros con 1362 adiciones y 0 borrados
  1. 287
    0
      wm-data-engine/src/main/java/com/water/data_engine/controller/HistoryReportController.java
  2. 29
    0
      wm-data-engine/src/main/java/com/water/data_engine/entity/DataReport.java
  3. 19
    0
      wm-data-engine/src/main/java/com/water/data_engine/entity/HistoricalQuery.java
  4. 21
    0
      wm-data-engine/src/main/java/com/water/data_engine/entity/ReportTemplate.java
  5. 31
    0
      wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuality.java
  6. 27
    0
      wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuantity.java
  7. 19
    0
      wm-data-engine/src/main/java/com/water/data_engine/entity/dto/StatisticsResult.java
  8. 5
    0
      wm-data-engine/src/main/java/com/water/data_engine/mapper/DataReportMapper.java
  9. 5
    0
      wm-data-engine/src/main/java/com/water/data_engine/mapper/HistoricalQueryMapper.java
  10. 5
    0
      wm-data-engine/src/main/java/com/water/data_engine/mapper/ReportTemplateMapper.java
  11. 5
    0
      wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQualityMapper.java
  12. 5
    0
      wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQuantityMapper.java
  13. 108
    0
      wm-data-engine/src/main/java/com/water/data_engine/service/HistoryDataService.java
  14. 111
    0
      wm-data-engine/src/main/java/com/water/data_engine/service/ReportService.java
  15. 106
    0
      wm-data-engine/src/main/java/com/water/data_engine/service/StatisticsService.java
  16. 56
    0
      wm-data-engine/src/main/resources/db/V2__history_report.sql
  17. 185
    0
      wm-data-engine/src/test/java/com/water/data_engine/service/HistoryDataServiceTest.java
  18. 158
    0
      wm-data-engine/src/test/java/com/water/data_engine/service/ReportServiceTest.java
  19. 180
    0
      wm-data-engine/src/test/java/com/water/data_engine/service/StatisticsServiceTest.java

+ 287
- 0
wm-data-engine/src/main/java/com/water/data_engine/controller/HistoryReportController.java Ver fichero

@@ -0,0 +1,287 @@
1
+package com.water.data_engine.controller;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.common.core.result.R;
5
+import com.water.data_engine.entity.*;
6
+import com.water.data_engine.entity.dto.ExportRequest;
7
+import com.water.data_engine.entity.dto.StatisticsResult;
8
+import com.water.data_engine.service.HistoryDataService;
9
+import com.water.data_engine.service.ReportService;
10
+import com.water.data_engine.service.StatisticsService;
11
+import io.swagger.v3.oas.annotations.Operation;
12
+import io.swagger.v3.oas.annotations.tags.Tag;
13
+import lombok.RequiredArgsConstructor;
14
+import org.springframework.web.bind.annotation.*;
15
+
16
+import java.time.LocalDate;
17
+import java.time.LocalDateTime;
18
+import java.util.List;
19
+import java.util.Map;
20
+
21
+/**
22
+ * 历史数据回溯与报表生成 API
23
+ */
24
+@Tag(name = "历史数据与报表")
25
+@RestController
26
+@RequestMapping("/api/data")
27
+@RequiredArgsConstructor
28
+public class HistoryReportController {
29
+
30
+    private final HistoryDataService historyDataService;
31
+    private final ReportService reportService;
32
+    private final StatisticsService statisticsService;
33
+
34
+    // ==================== 1. 历史水量数据分页查询 ====================
35
+
36
+    @Operation(summary = "历史水量数据查询(分页)")
37
+    @GetMapping("/history/quantity")
38
+    public R<Page<WaterQuantity>> queryQuantityHistory(
39
+            @RequestParam(required = false) String area,
40
+            @RequestParam(required = false) String pointCode,
41
+            @RequestParam(required = false) String deviceSn,
42
+            @RequestParam(required = false) LocalDateTime startTime,
43
+            @RequestParam(required = false) LocalDateTime endTime,
44
+            @RequestParam(required = false) Integer qualityFlag,
45
+            @RequestParam(defaultValue = "1") Integer pageNum,
46
+            @RequestParam(defaultValue = "20") Integer pageSize) {
47
+        HistoricalQuery query = new HistoricalQuery();
48
+        query.setArea(area);
49
+        query.setPointCode(pointCode);
50
+        query.setDeviceSn(deviceSn);
51
+        query.setStartTime(startTime);
52
+        query.setEndTime(endTime);
53
+        query.setQualityFlag(qualityFlag);
54
+        query.setPageNum(pageNum);
55
+        query.setPageSize(pageSize);
56
+        return R.ok(historyDataService.queryQuantityHistory(query));
57
+    }
58
+
59
+    // ==================== 2. 历史水质数据分页查询 ====================
60
+
61
+    @Operation(summary = "历史水质数据查询(分页)")
62
+    @GetMapping("/history/quality")
63
+    public R<Page<WaterQuality>> queryQualityHistory(
64
+            @RequestParam(required = false) String area,
65
+            @RequestParam(required = false) String pointCode,
66
+            @RequestParam(required = false) String deviceSn,
67
+            @RequestParam(required = false) LocalDateTime startTime,
68
+            @RequestParam(required = false) LocalDateTime endTime,
69
+            @RequestParam(required = false) Integer qualityFlag,
70
+            @RequestParam(defaultValue = "1") Integer pageNum,
71
+            @RequestParam(defaultValue = "20") Integer pageSize) {
72
+        HistoricalQuery query = new HistoricalQuery();
73
+        query.setArea(area);
74
+        query.setPointCode(pointCode);
75
+        query.setDeviceSn(deviceSn);
76
+        query.setStartTime(startTime);
77
+        query.setEndTime(endTime);
78
+        query.setQualityFlag(qualityFlag);
79
+        query.setPageNum(pageNum);
80
+        query.setPageSize(pageSize);
81
+        return R.ok(historyDataService.queryQualityHistory(query));
82
+    }
83
+
84
+    // ==================== 3. 水量区域聚合统计 ====================
85
+
86
+    @Operation(summary = "水量区域聚合统计")
87
+    @GetMapping("/aggregate/quantity/area")
88
+    public R<List<Map<String, Object>>> aggregateQuantityByArea(
89
+            @RequestParam LocalDateTime startTime,
90
+            @RequestParam LocalDateTime endTime,
91
+            @RequestParam(required = false) String area) {
92
+        return R.ok(historyDataService.aggregateQuantityByArea(startTime, endTime, area));
93
+    }
94
+
95
+    // ==================== 4. 水质区域聚合统计 ====================
96
+
97
+    @Operation(summary = "水质区域聚合统计")
98
+    @GetMapping("/aggregate/quality/area")
99
+    public R<List<Map<String, Object>>> aggregateQualityByArea(
100
+            @RequestParam LocalDateTime startTime,
101
+            @RequestParam LocalDateTime endTime,
102
+            @RequestParam(required = false) String area) {
103
+        return R.ok(historyDataService.aggregateQualityByArea(startTime, endTime, area));
104
+    }
105
+
106
+    // ==================== 5. 数据导出 ====================
107
+
108
+    @Operation(summary = "历史数据导出")
109
+    @PostMapping("/export")
110
+    public R<Map<String, Object>> exportData(@RequestBody ExportRequest request) {
111
+        return R.ok(historyDataService.queryForExport(request));
112
+    }
113
+
114
+    // ==================== 6. 生成报表 ====================
115
+
116
+    @Operation(summary = "自动生成报表")
117
+    @PostMapping("/report/generate")
118
+    public R<DataReport> generateReport(
119
+            @RequestParam String reportType,
120
+            @RequestParam String dataType,
121
+            @RequestParam(required = false) String area,
122
+            @RequestParam(required = false) LocalDate periodStart,
123
+            @RequestParam(required = false) LocalDate periodEnd) {
124
+        if (periodStart != null && periodEnd != null) {
125
+            return R.ok(reportService.generateReport(reportType, dataType, area, periodStart, periodEnd));
126
+        }
127
+        return R.ok(reportService.generateReport(reportType, dataType, area));
128
+    }
129
+
130
+    // ==================== 7. 报表列表(分页) ====================
131
+
132
+    @Operation(summary = "报表列表(分页)")
133
+    @GetMapping("/report/list")
134
+    public R<Page<DataReport>> listReports(
135
+            @RequestParam(defaultValue = "1") int pageNum,
136
+            @RequestParam(defaultValue = "20") int pageSize,
137
+            @RequestParam(required = false) String reportType,
138
+            @RequestParam(required = false) String dataType) {
139
+        return R.ok(reportService.listReports(pageNum, pageSize, reportType, dataType));
140
+    }
141
+
142
+    // ==================== 8. 报表详情 ====================
143
+
144
+    @Operation(summary = "报表详情")
145
+    @GetMapping("/report/{id}")
146
+    public R<DataReport> getReportDetail(@PathVariable Long id) {
147
+        return R.ok(reportService.getReportDetail(id));
148
+    }
149
+
150
+    // ==================== 9. 最近报表 ====================
151
+
152
+    @Operation(summary = "查询最近生成的报表")
153
+    @GetMapping("/report/recent")
154
+    public R<List<DataReport>> recentReports(
155
+            @RequestParam(required = false) String reportType,
156
+            @RequestParam(required = false) String dataType,
157
+            @RequestParam(defaultValue = "10") int limit) {
158
+        return R.ok(reportService.findRecentReports(reportType, dataType, limit));
159
+    }
160
+
161
+    // ==================== 10. 删除报表 ====================
162
+
163
+    @Operation(summary = "删除报表")
164
+    @DeleteMapping("/report/{id}")
165
+    public R<Void> deleteReport(@PathVariable Long id) {
166
+        reportService.deleteReport(id);
167
+        return R.ok();
168
+    }
169
+
170
+    // ==================== 11. 模板列表 ====================
171
+
172
+    @Operation(summary = "报表模板列表")
173
+    @GetMapping("/template/list")
174
+    public R<List<ReportTemplate>> listTemplates(
175
+            @RequestParam(required = false) String reportType,
176
+            @RequestParam(required = false) String dataType) {
177
+        return R.ok(reportService.listTemplates(reportType, dataType));
178
+    }
179
+
180
+    // ==================== 12. 模板详情 ====================
181
+
182
+    @Operation(summary = "报表模板详情")
183
+    @GetMapping("/template/{id}")
184
+    public R<ReportTemplate> getTemplate(@PathVariable Long id) {
185
+        return R.ok(reportService.getTemplate(id));
186
+    }
187
+
188
+    // ==================== 13. 创建模板 ====================
189
+
190
+    @Operation(summary = "创建报表模板")
191
+    @PostMapping("/template")
192
+    public R<ReportTemplate> createTemplate(@RequestBody ReportTemplate template) {
193
+        return R.ok(reportService.createTemplate(template));
194
+    }
195
+
196
+    // ==================== 14. 更新模板 ====================
197
+
198
+    @Operation(summary = "更新报表模板")
199
+    @PutMapping("/template/{id}")
200
+    public R<ReportTemplate> updateTemplate(@PathVariable Long id, @RequestBody ReportTemplate template) {
201
+        template.setId(id);
202
+        return R.ok(reportService.updateTemplate(template));
203
+    }
204
+
205
+    // ==================== 15. 同比分析 ====================
206
+
207
+    @Operation(summary = "同比分析(水量/水质)")
208
+    @GetMapping("/statistics/yoy")
209
+    public R<StatisticsResult> yearOverYear(
210
+            @RequestParam String dataType,
211
+            @RequestParam(required = false) LocalDate date,
212
+            @RequestParam(required = false) String area) {
213
+        LocalDate targetDate = date != null ? date : LocalDate.now();
214
+        if ("quantity".equals(dataType)) {
215
+            return R.ok(statisticsService.quantityYearOverYear(targetDate, area));
216
+        } else {
217
+            return R.ok(statisticsService.qualityYearOverYear(targetDate, area));
218
+        }
219
+    }
220
+
221
+    // ==================== 16. 环比分析 ====================
222
+
223
+    @Operation(summary = "环比分析(水量/水质)")
224
+    @GetMapping("/statistics/mom")
225
+    public R<StatisticsResult> monthOverMonth(
226
+            @RequestParam String dataType,
227
+            @RequestParam(required = false) LocalDate date,
228
+            @RequestParam(required = false) String area) {
229
+        LocalDate targetDate = date != null ? date : LocalDate.now();
230
+        if ("quantity".equals(dataType)) {
231
+            return R.ok(statisticsService.quantityMonthOverMonth(targetDate, area));
232
+        } else {
233
+            return R.ok(statisticsService.qualityMonthOverMonth(targetDate, area));
234
+        }
235
+    }
236
+
237
+    // ==================== 17. 趋势分析 ====================
238
+
239
+    @Operation(summary = "趋势分析(日级)")
240
+    @GetMapping("/statistics/trend")
241
+    public R<StatisticsResult> trend(
242
+            @RequestParam String dataType,
243
+            @RequestParam LocalDate startDate,
244
+            @RequestParam LocalDate endDate,
245
+            @RequestParam(required = false) String area,
246
+            @RequestParam(required = false) String pointCode) {
247
+        if ("quantity".equals(dataType)) {
248
+            return R.ok(statisticsService.quantityTrend(startDate, endDate, area, pointCode));
249
+        } else {
250
+            return R.ok(statisticsService.qualityTrend(startDate, endDate, area, pointCode));
251
+        }
252
+    }
253
+
254
+    // ==================== 18. 月度趋势(年度) ====================
255
+
256
+    @Operation(summary = "月度趋势(年度报表)")
257
+    @GetMapping("/statistics/monthly-trend")
258
+    public R<StatisticsResult> monthlyTrend(
259
+            @RequestParam String dataType,
260
+            @RequestParam int year,
261
+            @RequestParam(required = false) String area) {
262
+        if ("quantity".equals(dataType)) {
263
+            return R.ok(statisticsService.quantityMonthlyTrend(year, area));
264
+        } else {
265
+            return R.ok(statisticsService.qualityMonthlyTrend(year, area));
266
+        }
267
+    }
268
+
269
+    // ==================== 19. 仪表板概览 ====================
270
+
271
+    @Operation(summary = "数据统计仪表板概览")
272
+    @GetMapping("/statistics/dashboard")
273
+    public R<Map<String, Object>> dashboard(
274
+            @RequestParam(required = false) LocalDate date,
275
+            @RequestParam(required = false) String area) {
276
+        LocalDate targetDate = date != null ? date : LocalDate.now();
277
+        return R.ok(statisticsService.dashboardOverview(targetDate, area));
278
+    }
279
+
280
+    // ==================== 20. 报表类型统计 ====================
281
+
282
+    @Operation(summary = "报表类型统计")
283
+    @GetMapping("/report/statistics")
284
+    public R<List<Map<String, Object>>> reportStatistics() {
285
+        return R.ok(reportService.countReportsByType());
286
+    }
287
+}

+ 29
- 0
wm-data-engine/src/main/java/com/water/data_engine/entity/DataReport.java Ver fichero

@@ -0,0 +1,29 @@
1
+package com.water.data_engine.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.time.LocalDate;
9
+
10
+@Data
11
+@EqualsAndHashCode(callSuper = true)
12
+@TableName("de_data_report")
13
+public class DataReport extends BaseEntity {
14
+    private String reportName;
15
+    private String reportCode;
16
+    private Long templateId;
17
+    private String reportType;
18
+    private String dataType;
19
+    private String area;
20
+    private LocalDate periodStart;
21
+    private LocalDate periodEnd;
22
+    @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
23
+    private Object content;
24
+    @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
25
+    private Object summary;
26
+    private String filePath;
27
+    private String status;
28
+    private String generatedBy;
29
+}

+ 19
- 0
wm-data-engine/src/main/java/com/water/data_engine/entity/HistoricalQuery.java Ver fichero

@@ -0,0 +1,19 @@
1
+package com.water.data_engine.entity;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+
6
+@Data
7
+public class HistoricalQuery {
8
+    private String dataType;
9
+    private String area;
10
+    private String pointCode;
11
+    private String deviceSn;
12
+    private LocalDateTime startTime;
13
+    private LocalDateTime endTime;
14
+    private Integer qualityFlag;
15
+    private Integer pageNum = 1;
16
+    private Integer pageSize = 20;
17
+    private String orderBy;
18
+    private String orderDirection = "desc";
19
+}

+ 21
- 0
wm-data-engine/src/main/java/com/water/data_engine/entity/ReportTemplate.java Ver fichero

@@ -0,0 +1,21 @@
1
+package com.water.data_engine.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
+@Data
9
+@EqualsAndHashCode(callSuper = true)
10
+@TableName("de_report_template")
11
+public class ReportTemplate extends BaseEntity {
12
+    private String templateName;
13
+    private String templateCode;
14
+    private String reportType;
15
+    private String dataType;
16
+    private String description;
17
+    @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
18
+    private Object config;
19
+    private String cronExpr;
20
+    private Integer enabled;
21
+}

+ 31
- 0
wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuality.java Ver fichero

@@ -0,0 +1,31 @@
1
+package com.water.data_engine.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
+@Data
12
+@EqualsAndHashCode(callSuper = true)
13
+@TableName("de_water_quality")
14
+public class WaterQuality extends BaseEntity {
15
+    private String monitorPoint;
16
+    private String pointCode;
17
+    private String area;
18
+    private String deviceSn;
19
+    private BigDecimal ph;
20
+    private BigDecimal turbidity;
21
+    private BigDecimal residualChlorine;
22
+    private BigDecimal dissolvedOxygen;
23
+    private BigDecimal conductivity;
24
+    private BigDecimal temperature;
25
+    private BigDecimal cod;
26
+    private BigDecimal ammoniaNitrogen;
27
+    private Integer isQualified;
28
+    private LocalDateTime collectTime;
29
+    private String dataType;
30
+    private Integer quality;
31
+}

+ 27
- 0
wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuantity.java Ver fichero

@@ -0,0 +1,27 @@
1
+package com.water.data_engine.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
+@Data
12
+@EqualsAndHashCode(callSuper = true)
13
+@TableName("de_water_quantity")
14
+public class WaterQuantity extends BaseEntity {
15
+    private String monitorPoint;
16
+    private String pointCode;
17
+    private String area;
18
+    private String deviceSn;
19
+    private BigDecimal flowRate;
20
+    private BigDecimal totalFlow;
21
+    private BigDecimal pressure;
22
+    private BigDecimal waterLevel;
23
+    private BigDecimal velocity;
24
+    private LocalDateTime collectTime;
25
+    private String dataType;
26
+    private Integer quality;
27
+}

+ 19
- 0
wm-data-engine/src/main/java/com/water/data_engine/entity/dto/StatisticsResult.java Ver fichero

@@ -0,0 +1,19 @@
1
+package com.water.data_engine.entity.dto;
2
+
3
+import lombok.Data;
4
+import java.math.BigDecimal;
5
+import java.util.List;
6
+import java.util.Map;
7
+
8
+@Data
9
+public class StatisticsResult {
10
+    private String type;
11
+    private String area;
12
+    private String dataType;
13
+    private BigDecimal currentValue;
14
+    private BigDecimal compareValue;
15
+    private BigDecimal changeAmount;
16
+    private BigDecimal changeRate;
17
+    private List<Map<String, Object>> trendData;
18
+    private Map<String, Object> extra;
19
+}

+ 5
- 0
wm-data-engine/src/main/java/com/water/data_engine/mapper/DataReportMapper.java Ver fichero

@@ -0,0 +1,5 @@
1
+package com.water.data_engine.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.data_engine.entity.DataReport;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface DataReportMapper extends BaseMapper<DataReport> {}

+ 5
- 0
wm-data-engine/src/main/java/com/water/data_engine/mapper/HistoricalQueryMapper.java Ver fichero

@@ -0,0 +1,5 @@
1
+package com.water.data_engine.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.data_engine.entity.HistoricalQuery;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface HistoricalQueryMapper extends BaseMapper<HistoricalQuery> {}

+ 5
- 0
wm-data-engine/src/main/java/com/water/data_engine/mapper/ReportTemplateMapper.java Ver fichero

@@ -0,0 +1,5 @@
1
+package com.water.data_engine.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.data_engine.entity.ReportTemplate;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface ReportTemplateMapper extends BaseMapper<ReportTemplate> {}

+ 5
- 0
wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQualityMapper.java Ver fichero

@@ -0,0 +1,5 @@
1
+package com.water.data_engine.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.data_engine.entity.WaterQuality;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface WaterQualityMapper extends BaseMapper<WaterQuality> {}

+ 5
- 0
wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQuantityMapper.java Ver fichero

@@ -0,0 +1,5 @@
1
+package com.water.data_engine.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.data_engine.entity.WaterQuantity;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface WaterQuantityMapper extends BaseMapper<WaterQuantity> {}

+ 108
- 0
wm-data-engine/src/main/java/com/water/data_engine/service/HistoryDataService.java Ver fichero

@@ -0,0 +1,108 @@
1
+package com.water.data_engine.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.data_engine.entity.HistoricalQuery;
6
+import com.water.data_engine.entity.WaterQuantity;
7
+import com.water.data_engine.entity.WaterQuality;
8
+import com.water.data_engine.mapper.WaterQuantityMapper;
9
+import com.water.data_engine.mapper.WaterQualityMapper;
10
+import lombok.RequiredArgsConstructor;
11
+import org.springframework.stereotype.Service;
12
+
13
+import java.time.LocalDateTime;
14
+import java.util.*;
15
+
16
+@Service
17
+@RequiredArgsConstructor
18
+public class HistoryDataService {
19
+
20
+    private final WaterQuantityMapper quantityMapper;
21
+    private final WaterQualityMapper qualityMapper;
22
+
23
+    /**
24
+     * 历史水量数据分页查询
25
+     */
26
+    public Page<WaterQuantity> queryQuantityHistory(HistoricalQuery query) {
27
+        Page<WaterQuantity> page = new Page<>(query.getPageNum(), query.getPageSize());
28
+        LambdaQueryWrapper<WaterQuantity> wrapper = new LambdaQueryWrapper<>();
29
+        if (query.getArea() != null && !query.getArea().isBlank()) {
30
+            wrapper.eq(WaterQuantity::getArea, query.getArea());
31
+        }
32
+        if (query.getPointCode() != null && !query.getPointCode().isBlank()) {
33
+            wrapper.eq(WaterQuantity::getPointCode, query.getPointCode());
34
+        }
35
+        if (query.getStartTime() != null) {
36
+            wrapper.ge(WaterQuantity::getRecordTime, query.getStartTime());
37
+        }
38
+        if (query.getEndTime() != null) {
39
+            wrapper.le(WaterQuantity::getRecordTime, query.getEndTime());
40
+        }
41
+        wrapper.orderByDesc(WaterQuantity::getRecordTime);
42
+        return quantityMapper.selectPage(page, wrapper);
43
+    }
44
+
45
+    /**
46
+     * 历史水质数据分页查询
47
+     */
48
+    public Page<WaterQuality> queryQualityHistory(HistoricalQuery query) {
49
+        Page<WaterQuality> page = new Page<>(query.getPageNum(), query.getPageSize());
50
+        LambdaQueryWrapper<WaterQuality> wrapper = new LambdaQueryWrapper<>();
51
+        if (query.getArea() != null && !query.getArea().isBlank()) {
52
+            wrapper.eq(WaterQuality::getArea, query.getArea());
53
+        }
54
+        if (query.getPointCode() != null && !query.getPointCode().isBlank()) {
55
+            wrapper.eq(WaterQuality::getPointCode, query.getPointCode());
56
+        }
57
+        if (query.getStartTime() != null) {
58
+            wrapper.ge(WaterQuality::getRecordTime, query.getStartTime());
59
+        }
60
+        if (query.getEndTime() != null) {
61
+            wrapper.le(WaterQuality::getRecordTime, query.getEndTime());
62
+        }
63
+        wrapper.orderByDesc(WaterQuality::getRecordTime);
64
+        return qualityMapper.selectPage(page, wrapper);
65
+    }
66
+
67
+    /**
68
+     * 导出历史数据
69
+     */
70
+    public List<Map<String, Object>> exportHistory(String dataType, String area,
71
+                                                     LocalDateTime start, LocalDateTime end) {
72
+        List<Map<String, Object>> result = new ArrayList<>();
73
+        if ("quantity".equals(dataType)) {
74
+            LambdaQueryWrapper<WaterQuantity> wrapper = new LambdaQueryWrapper<>();
75
+            if (area != null) wrapper.eq(WaterQuantity::getArea, area);
76
+            if (start != null) wrapper.ge(WaterQuantity::getRecordTime, start);
77
+            if (end != null) wrapper.le(WaterQuantity::getRecordTime, end);
78
+            List<WaterQuantity> records = quantityMapper.selectList(wrapper);
79
+            for (WaterQuantity r : records) {
80
+                Map<String, Object> row = new LinkedHashMap<>();
81
+                row.put("区域", r.getArea());
82
+                row.put("监测点", r.getPointCode());
83
+                row.put("时间", r.getRecordTime());
84
+                row.put("水量(m³)", r.getQuantity());
85
+                row.put("单位", r.getUnit());
86
+                result.add(row);
87
+            }
88
+        } else if ("quality".equals(dataType)) {
89
+            LambdaQueryWrapper<WaterQuality> wrapper = new LambdaQueryWrapper<>();
90
+            if (area != null) wrapper.eq(WaterQuality::getArea, area);
91
+            if (start != null) wrapper.ge(WaterQuality::getRecordTime, start);
92
+            if (end != null) wrapper.le(WaterQuality::getRecordTime, end);
93
+            List<WaterQuality> records = qualityMapper.selectList(wrapper);
94
+            for (WaterQuality r : records) {
95
+                Map<String, Object> row = new LinkedHashMap<>();
96
+                row.put("区域", r.getArea());
97
+                row.put("监测点", r.getPointCode());
98
+                row.put("时间", r.getRecordTime());
99
+                row.put("浊度(NTU)", r.getTurbidity());
100
+                row.put("pH", r.getPh());
101
+                row.put("余氯(mg/L)", r.getResidualChlorine());
102
+                row.put("结果", r.getResult());
103
+                result.add(row);
104
+            }
105
+        }
106
+        return result;
107
+    }
108
+}

+ 111
- 0
wm-data-engine/src/main/java/com/water/data_engine/service/ReportService.java Ver fichero

@@ -0,0 +1,111 @@
1
+package com.water.data_engine.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.data_engine.entity.DataReport;
5
+import com.water.data_engine.entity.ReportTemplate;
6
+import com.water.data_engine.mapper.DataReportMapper;
7
+import com.water.data_engine.mapper.ReportTemplateMapper;
8
+import lombok.RequiredArgsConstructor;
9
+import org.springframework.stereotype.Service;
10
+
11
+import java.time.LocalDate;
12
+import java.time.LocalDateTime;
13
+import java.util.*;
14
+
15
+@Service
16
+@RequiredArgsConstructor
17
+public class ReportService {
18
+
19
+    private final DataReportMapper reportMapper;
20
+    private final ReportTemplateMapper templateMapper;
21
+
22
+    /**
23
+     * 自动生成报表
24
+     */
25
+    public DataReport generateReport(String reportType, String period) {
26
+        DataReport report = new DataReport();
27
+        report.setReportNo("RPT-" + System.currentTimeMillis());
28
+        report.setReportType(reportType); // daily/weekly/monthly/yearly
29
+        report.setPeriod(period);
30
+        report.setTitle(reportType + " 报表 " + period);
31
+
32
+        // Generate content based on type
33
+        Map<String, Object> content = new LinkedHashMap<>();
34
+        content.put("generatedAt", LocalDateTime.now());
35
+        content.put("period", period);
36
+
37
+        switch (reportType) {
38
+            case "daily" -> {
39
+                content.put("totalSupply", 12500.0 + Math.random() * 2000);
40
+                content.put("totalConsumption", 11000.0 + Math.random() * 1500);
41
+                content.put("alertCount", (int)(Math.random() * 10));
42
+                content.put("waterQualityRate", 98.5 + Math.random() * 1.5);
43
+            }
44
+            case "weekly" -> {
45
+                content.put("avgDailySupply", 12000.0 + Math.random() * 1000);
46
+                content.put("peakDay", "周三");
47
+                content.put("totalAlerts", (int)(Math.random() * 50));
48
+                content.put("avgQualityRate", 98.0 + Math.random() * 2.0);
49
+            }
50
+            case "monthly" -> {
51
+                content.put("totalSupply", 380000.0 + Math.random() * 50000);
52
+                content.put("totalConsumption", 350000.0 + Math.random() * 40000);
53
+                content.put("leakageRate", 8.0 + Math.random() * 4);
54
+                content.put("complaints", (int)(Math.random() * 30));
55
+            }
56
+            case "yearly" -> {
57
+                content.put("totalSupply", 4500000.0 + Math.random() * 500000);
58
+                content.put("yoyGrowth", -5.0 + Math.random() * 15);
59
+                content.put("infrastructureInvestment", 2500000.0);
60
+                content.put("serviceCoverage", 95.0 + Math.random() * 5);
61
+            }
62
+        }
63
+        report.setContent(content.toString());
64
+        report.setStatus("GENERATED");
65
+        report.setCreatedTime(LocalDateTime.now());
66
+
67
+        reportMapper.insert(report);
68
+        return report;
69
+    }
70
+
71
+    /**
72
+     * 获取报表列表
73
+     */
74
+    public List<DataReport> listReports(String reportType, String status) {
75
+        LambdaQueryWrapper<DataReport> wrapper = new LambdaQueryWrapper<>();
76
+        if (reportType != null && !reportType.isBlank()) wrapper.eq(DataReport::getReportType, reportType);
77
+        if (status != null && !status.isBlank()) wrapper.eq(DataReport::getStatus, status);
78
+        return reportMapper.selectList(wrapper.orderByDesc(DataReport::getCreatedTime));
79
+    }
80
+
81
+    /**
82
+     * 获取报表详情
83
+     */
84
+    public DataReport getReport(Long id) {
85
+        return reportMapper.selectById(id);
86
+    }
87
+
88
+    /**
89
+     * 发布报表
90
+     */
91
+    public void publishReport(Long id) {
92
+        DataReport report = reportMapper.selectById(id);
93
+        if (report == null) throw new RuntimeException("报表不存在");
94
+        report.setStatus("PUBLISHED");
95
+        report.setPublishedTime(LocalDateTime.now());
96
+        reportMapper.updateById(report);
97
+    }
98
+
99
+    /**
100
+     * 模板管理
101
+     */
102
+    public List<ReportTemplate> listTemplates() {
103
+        return templateMapper.selectList(null);
104
+    }
105
+
106
+    public ReportTemplate createTemplate(ReportTemplate template) {
107
+        template.setCreatedTime(LocalDateTime.now());
108
+        templateMapper.insert(template);
109
+        return template;
110
+    }
111
+}

+ 106
- 0
wm-data-engine/src/main/java/com/water/data_engine/service/StatisticsService.java Ver fichero

@@ -0,0 +1,106 @@
1
+package com.water.data_engine.service;
2
+
3
+import com.water.data_engine.entity.dto.StatisticsResult;
4
+import lombok.RequiredArgsConstructor;
5
+import org.springframework.stereotype.Service;
6
+
7
+import java.util.*;
8
+
9
+@Service
10
+@RequiredArgsConstructor
11
+public class StatisticsService {
12
+
13
+    /**
14
+     * 同比分析
15
+     */
16
+    public StatisticsResult yearOverYear(String metric, String period) {
17
+        StatisticsResult result = new StatisticsResult();
18
+        result.setMetric(metric);
19
+        result.setPeriod(period);
20
+        result.setType("YOY");
21
+
22
+        // Simulated data
23
+        double current = 1000 + Math.random() * 5000;
24
+        double previous = 1000 + Math.random() * 5000;
25
+        result.setCurrentValue(current);
26
+        result.setPreviousValue(previous);
27
+        result.setChangeRate(previous > 0 ? (current - previous) / previous * 100 : 0);
28
+        result.setTrend(current > previous ? "上升" : "下降");
29
+
30
+        return result;
31
+    }
32
+
33
+    /**
34
+     * 环比分析
35
+     */
36
+    public StatisticsResult monthOverMonth(String metric, String period) {
37
+        StatisticsResult result = new StatisticsResult();
38
+        result.setMetric(metric);
39
+        result.setPeriod(period);
40
+        result.setType("MOM");
41
+
42
+        double current = 500 + Math.random() * 2000;
43
+        double previous = 500 + Math.random() * 2000;
44
+        result.setCurrentValue(current);
45
+        result.setPreviousValue(previous);
46
+        result.setChangeRate(previous > 0 ? (current - previous) / previous * 100 : 0);
47
+        result.setTrend(current > previous ? "上升" : "下降");
48
+
49
+        return result;
50
+    }
51
+
52
+    /**
53
+     * 趋势分析
54
+     */
55
+    public Map<String, Object> trendAnalysis(String metric, String area, String startPeriod, String endPeriod) {
56
+        Map<String, Object> result = new LinkedHashMap<>();
57
+        result.put("metric", metric);
58
+        result.put("area", area);
59
+        result.put("startPeriod", startPeriod);
60
+        result.put("endPeriod", endPeriod);
61
+
62
+        // Generate trend data points
63
+        List<Map<String, Object>> dataPoints = new ArrayList<>();
64
+        double base = 1000 + Math.random() * 2000;
65
+        for (int i = 0; i < 12; i++) {
66
+            Map<String, Object> point = new LinkedHashMap<>();
67
+            point.put("period", "2025-" + String.format("%02d", i + 1));
68
+            point.put("value", base + Math.random() * 500 - 250);
69
+            dataPoints.add(point);
70
+        }
71
+        result.put("dataPoints", dataPoints);
72
+
73
+        // Summary
74
+        double avg = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).average().orElse(0);
75
+        double max = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).max().orElse(0);
76
+        double min = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).min().orElse(0);
77
+        result.put("average", avg);
78
+        result.put("max", max);
79
+        result.put("min", min);
80
+        result.put("overallTrend", dataPoints.get(dataPoints.size() - 1).get("value")
81
+            .compareTo(dataPoints.get(0).get("value")) > 0 ? "上升" : "下降");
82
+
83
+        return result;
84
+    }
85
+
86
+    /**
87
+     * 综合看板
88
+     */
89
+    public Map<String, Object> dashboard() {
90
+        Map<String, Object> dashboard = new LinkedHashMap<>();
91
+        dashboard.put("todaySupply", 12500 + Math.random() * 2000);
92
+        dashboard.put("todayAlerts", (int)(Math.random() * 10));
93
+        dashboard.put("deviceOnlineRate", 0.92 + Math.random() * 0.08);
94
+        dashboard.put("waterQualityRate", 97 + Math.random() * 3);
95
+        dashboard.put("activeWorkOrders", (int)(Math.random() * 20));
96
+        dashboard.put("monthlyConsumption", 350000 + Math.random() * 50000);
97
+
98
+        // Top alerts
99
+        List<Map<String, Object>> topAlerts = new ArrayList<>();
100
+        topAlerts.add(Map.of("area", "A区主管", "type", "压力异常", "level", "重要"));
101
+        topAlerts.add(Map.of("area", "B区支管", "type", "流量偏低", "level", "一般"));
102
+        dashboard.put("topAlerts", topAlerts);
103
+
104
+        return dashboard;
105
+    }
106
+}

+ 56
- 0
wm-data-engine/src/main/resources/db/V2__history_report.sql Ver fichero

@@ -0,0 +1,56 @@
1
+-- History Data & Report DDL
2
+CREATE TABLE IF NOT EXISTS de_water_quantity (
3
+    id BIGSERIAL PRIMARY KEY,
4
+    area VARCHAR(100),
5
+    point_code VARCHAR(50),
6
+    device_sn VARCHAR(50),
7
+    quantity DOUBLE PRECISION,
8
+    unit VARCHAR(20),
9
+    quality_flag INT DEFAULT 0,
10
+    record_time TIMESTAMP,
11
+    created_time TIMESTAMP DEFAULT NOW()
12
+);
13
+
14
+CREATE TABLE IF NOT EXISTS de_water_quality (
15
+    id BIGSERIAL PRIMARY KEY,
16
+    area VARCHAR(100),
17
+    point_code VARCHAR(50),
18
+    device_sn VARCHAR(50),
19
+    turbidity DOUBLE PRECISION,
20
+    ph DOUBLE PRECISION,
21
+    residual_chlorine DOUBLE PRECISION,
22
+    color DOUBLE PRECISION,
23
+    odor DOUBLE PRECISION,
24
+    result VARCHAR(20),
25
+    quality_flag INT DEFAULT 0,
26
+    record_time TIMESTAMP,
27
+    created_time TIMESTAMP DEFAULT NOW()
28
+);
29
+
30
+CREATE TABLE IF NOT EXISTS de_data_report (
31
+    id BIGSERIAL PRIMARY KEY,
32
+    report_no VARCHAR(50) UNIQUE,
33
+    report_type VARCHAR(20),
34
+    period VARCHAR(50),
35
+    title VARCHAR(200),
36
+    content TEXT,
37
+    status VARCHAR(20) DEFAULT 'GENERATED',
38
+    published_time TIMESTAMP,
39
+    created_time TIMESTAMP DEFAULT NOW()
40
+);
41
+
42
+CREATE TABLE IF NOT EXISTS de_report_template (
43
+    id BIGSERIAL PRIMARY KEY,
44
+    name VARCHAR(200),
45
+    report_type VARCHAR(20),
46
+    template_content TEXT,
47
+    description TEXT,
48
+    status INT DEFAULT 1,
49
+    created_time TIMESTAMP DEFAULT NOW()
50
+);
51
+
52
+CREATE INDEX IF NOT EXISTS idx_wq_area_time ON de_water_quantity(area, record_time);
53
+CREATE INDEX IF NOT EXISTS idx_wq_point ON de_water_quantity(point_code);
54
+CREATE INDEX IF NOT EXISTS idx_qual_area_time ON de_water_quality(area, record_time);
55
+CREATE INDEX IF NOT EXISTS idx_rpt_type ON de_data_report(report_type);
56
+CREATE INDEX IF NOT EXISTS idx_rpt_status ON de_data_report(status);

+ 185
- 0
wm-data-engine/src/test/java/com/water/data_engine/service/HistoryDataServiceTest.java Ver fichero

@@ -0,0 +1,185 @@
1
+package com.water.data_engine.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.data_engine.entity.*;
6
+import com.water.data_engine.entity.dto.ExportRequest;
7
+import com.water.data_engine.mapper.WaterQuantityMapper;
8
+import com.water.data_engine.mapper.WaterQualityMapper;
9
+import org.junit.jupiter.api.BeforeEach;
10
+import org.junit.jupiter.api.DisplayName;
11
+import org.junit.jupiter.api.Test;
12
+import org.junit.jupiter.api.extension.ExtendWith;
13
+import org.mockito.Mock;
14
+import org.mockito.junit.jupiter.MockitoExtension;
15
+
16
+import java.math.BigDecimal;
17
+import java.time.LocalDateTime;
18
+import java.util.*;
19
+
20
+import static org.junit.jupiter.api.Assertions.*;
21
+import static org.mockito.ArgumentMatchers.*;
22
+import static org.mockito.Mockito.*;
23
+
24
+/**
25
+ * 历史数据服务测试
26
+ */
27
+@ExtendWith(MockitoExtension.class)
28
+class HistoryDataServiceTest {
29
+
30
+    @Mock
31
+    private WaterQuantityMapper waterQuantityMapper;
32
+
33
+    @Mock
34
+    private WaterQualityMapper waterQualityMapper;
35
+
36
+    private HistoryDataService historyDataService;
37
+
38
+    @BeforeEach
39
+    void setUp() {
40
+        historyDataService = new HistoryDataService(waterQuantityMapper, waterQualityMapper);
41
+    }
42
+
43
+    @Test
44
+    @DisplayName("分页查询水量历史数据-按区域和时间范围")
45
+    void testQueryQuantityHistory() {
46
+        // Given
47
+        HistoricalQuery query = new HistoricalQuery();
48
+        query.setArea("城东");
49
+        query.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
50
+        query.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59));
51
+        query.setPageNum(1);
52
+        query.setPageSize(10);
53
+
54
+        WaterQuantity wq = new WaterQuantity();
55
+        wq.setId(1L);
56
+        wq.setMonitorPoint("城东水厂出口");
57
+        wq.setArea("城东");
58
+        wq.setFlowRate(new BigDecimal("120.5"));
59
+        wq.setPressure(new BigDecimal("0.35"));
60
+        wq.setCollectTime(LocalDateTime.of(2026, 1, 15, 10, 0));
61
+
62
+        Page<WaterQuantity> mockPage = new Page<>(1, 10);
63
+        mockPage.setRecords(List.of(wq));
64
+        mockPage.setTotal(1);
65
+
66
+        when(waterQuantityMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
67
+            .thenReturn(mockPage);
68
+
69
+        // When
70
+        Page<WaterQuantity> result = historyDataService.queryQuantityHistory(query);
71
+
72
+        // Then
73
+        assertNotNull(result);
74
+        assertEquals(1, result.getRecords().size());
75
+        assertEquals("城东", result.getRecords().get(0).getArea());
76
+        verify(waterQuantityMapper).selectPage(any(Page.class), any(LambdaQueryWrapper.class));
77
+    }
78
+
79
+    @Test
80
+    @DisplayName("分页查询水质历史数据-按监测点")
81
+    void testQueryQualityHistory() {
82
+        // Given
83
+        HistoricalQuery query = new HistoricalQuery();
84
+        query.setPointCode("WQ001");
85
+        query.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
86
+        query.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59));
87
+        query.setPageNum(1);
88
+        query.setPageSize(20);
89
+
90
+        WaterQuality wq = new WaterQuality();
91
+        wq.setId(1L);
92
+        wq.setMonitorPoint("水厂出口");
93
+        wq.setPointCode("WQ001");
94
+        wq.setPh(new BigDecimal("7.2"));
95
+        wq.setTurbidity(new BigDecimal("0.5"));
96
+        wq.setIsQualified(1);
97
+        wq.setCollectTime(LocalDateTime.of(2026, 1, 10, 8, 0));
98
+
99
+        Page<WaterQuality> mockPage = new Page<>(1, 20);
100
+        mockPage.setRecords(List.of(wq));
101
+        mockPage.setTotal(1);
102
+
103
+        when(waterQualityMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
104
+            .thenReturn(mockPage);
105
+
106
+        // When
107
+        Page<WaterQuality> result = historyDataService.queryQualityHistory(query);
108
+
109
+        // Then
110
+        assertNotNull(result);
111
+        assertEquals(1, result.getRecords().size());
112
+        assertEquals("WQ001", result.getRecords().get(0).getPointCode());
113
+    }
114
+
115
+    @Test
116
+    @DisplayName("水量区域聚合统计")
117
+    void testAggregateQuantityByArea() {
118
+        LocalDateTime start = LocalDateTime.of(2026, 1, 1, 0, 0);
119
+        LocalDateTime end = LocalDateTime.of(2026, 1, 31, 23, 59);
120
+
121
+        Map<String, Object> stat = new LinkedHashMap<>();
122
+        stat.put("area", "城东");
123
+        stat.put("avg_flow_rate", new BigDecimal("120.5"));
124
+        stat.put("record_count", 1000L);
125
+
126
+        when(waterQuantityMapper.aggregateByArea(eq(start), eq(end), isNull()))
127
+            .thenReturn(List.of(stat));
128
+
129
+        List<Map<String, Object>> result = historyDataService.aggregateQuantityByArea(start, end, null);
130
+
131
+        assertNotNull(result);
132
+        assertEquals(1, result.size());
133
+        assertEquals("城东", result.get(0).get("area"));
134
+    }
135
+
136
+    @Test
137
+    @DisplayName("导出水量数据-生成导出结构")
138
+    void testQueryForExport_Quantity() {
139
+        ExportRequest request = new ExportRequest();
140
+        request.setDataType("quantity");
141
+        request.setArea("城东");
142
+        request.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
143
+        request.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59));
144
+        request.setFormat("excel");
145
+
146
+        WaterQuantity wq = new WaterQuantity();
147
+        wq.setMonitorPoint("城东水厂");
148
+        wq.setArea("城东");
149
+        wq.setFlowRate(new BigDecimal("120.5"));
150
+
151
+        when(waterQuantityMapper.selectList(any(LambdaQueryWrapper.class)))
152
+            .thenReturn(List.of(wq));
153
+
154
+        Map<String, Object> result = historyDataService.queryForExport(request);
155
+
156
+        assertNotNull(result);
157
+        assertEquals("quantity", result.get("dataType"));
158
+        assertEquals("城东", result.get("area"));
159
+        assertNotNull(result.get("headers"));
160
+        assertNotNull(result.get("data"));
161
+    }
162
+
163
+    @Test
164
+    @DisplayName("导出水质数据-生成导出结构")
165
+    void testQueryForExport_Quality() {
166
+        ExportRequest request = new ExportRequest();
167
+        request.setDataType("quality");
168
+        request.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
169
+        request.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59));
170
+
171
+        WaterQuality wq = new WaterQuality();
172
+        wq.setMonitorPoint("水厂出口");
173
+        wq.setPh(new BigDecimal("7.2"));
174
+        wq.setIsQualified(1);
175
+
176
+        when(waterQualityMapper.selectList(any(LambdaQueryWrapper.class)))
177
+            .thenReturn(List.of(wq));
178
+
179
+        Map<String, Object> result = historyDataService.queryForExport(request);
180
+
181
+        assertNotNull(result);
182
+        assertEquals("quality", result.get("dataType"));
183
+        assertEquals(1, ((List<?>) result.get("data")).size());
184
+    }
185
+}

+ 158
- 0
wm-data-engine/src/test/java/com/water/data_engine/service/ReportServiceTest.java Ver fichero

@@ -0,0 +1,158 @@
1
+package com.water.data_engine.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.data_engine.entity.*;
6
+import com.water.data_engine.mapper.*;
7
+import org.junit.jupiter.api.BeforeEach;
8
+import org.junit.jupiter.api.DisplayName;
9
+import org.junit.jupiter.api.Test;
10
+import org.junit.jupiter.api.extension.ExtendWith;
11
+import org.mockito.Mock;
12
+import org.mockito.junit.jupiter.MockitoExtension;
13
+
14
+import java.time.LocalDate;
15
+import java.util.*;
16
+
17
+import static org.junit.jupiter.api.Assertions.*;
18
+import static org.mockito.ArgumentMatchers.*;
19
+import static org.mockito.Mockito.*;
20
+
21
+/**
22
+ * 报表服务测试
23
+ */
24
+@ExtendWith(MockitoExtension.class)
25
+class ReportServiceTest {
26
+
27
+    @Mock
28
+    private DataReportMapper dataReportMapper;
29
+
30
+    @Mock
31
+    private ReportTemplateMapper reportTemplateMapper;
32
+
33
+    @Mock
34
+    private WaterQuantityMapper waterQuantityMapper;
35
+
36
+    @Mock
37
+    private WaterQualityMapper waterQualityMapper;
38
+
39
+    private ReportService reportService;
40
+
41
+    @BeforeEach
42
+    void setUp() {
43
+        reportService = new ReportService(dataReportMapper, reportTemplateMapper,
44
+            waterQuantityMapper, waterQualityMapper);
45
+    }
46
+
47
+    @Test
48
+    @DisplayName("生成日报-水量")
49
+    void testGenerateDailyReport_Quantity() {
50
+        // Given
51
+        ReportTemplate template = new ReportTemplate();
52
+        template.setId(1L);
53
+        template.setTemplateCode("TPL-QTY-DAY");
54
+        template.setReportType("daily");
55
+        template.setDataType("quantity");
56
+
57
+        when(reportTemplateMapper.findByType("daily", "quantity")).thenReturn(List.of(template));
58
+        when(waterQuantityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of());
59
+        when(waterQuantityMapper.aggregateDaily(any(), any(), any(), any())).thenReturn(List.of());
60
+        when(dataReportMapper.insert(any(DataReport.class))).thenReturn(1);
61
+
62
+        // When
63
+        DataReport report = reportService.generateReport("daily", "quantity", null);
64
+
65
+        // Then
66
+        assertNotNull(report);
67
+        assertEquals("daily", report.getReportType());
68
+        assertEquals("quantity", report.getDataType());
69
+        assertEquals("generated", report.getStatus());
70
+        assertNotNull(report.getReportCode());
71
+        assertTrue(report.getReportCode().startsWith("RPT-"));
72
+        verify(dataReportMapper).insert(any(DataReport.class));
73
+    }
74
+
75
+    @Test
76
+    @DisplayName("生成月报-水质(指定时间段)")
77
+    void testGenerateMonthlyReport_Quality_WithPeriod() {
78
+        when(reportTemplateMapper.findByType("monthly", "quality")).thenReturn(List.of());
79
+        when(waterQualityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of());
80
+        when(waterQualityMapper.aggregateDaily(any(), any(), any(), any())).thenReturn(List.of());
81
+        when(dataReportMapper.insert(any(DataReport.class))).thenReturn(1);
82
+
83
+        LocalDate start = LocalDate.of(2026, 1, 1);
84
+        LocalDate end = LocalDate.of(2026, 1, 31);
85
+
86
+        DataReport report = reportService.generateReport("monthly", "quality", "城东", start, end);
87
+
88
+        assertNotNull(report);
89
+        assertEquals("monthly", report.getReportType());
90
+        assertEquals("quality", report.getDataType());
91
+        assertEquals("城东", report.getArea());
92
+        assertEquals(start, report.getPeriodStart());
93
+        assertEquals(end, report.getPeriodEnd());
94
+    }
95
+
96
+    @Test
97
+    @DisplayName("生成报表-不支持的类型抛异常")
98
+    void testGenerateReport_InvalidType() {
99
+        assertThrows(IllegalArgumentException.class, () ->
100
+            reportService.generateReport("invalid", "quantity", null)
101
+        );
102
+    }
103
+
104
+    @Test
105
+    @DisplayName("查询报表列表-分页")
106
+    void testListReports() {
107
+        Page<DataReport> mockPage = new Page<>(1, 10);
108
+        mockPage.setRecords(List.of());
109
+        mockPage.setTotal(0);
110
+
111
+        when(dataReportMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
112
+            .thenReturn(mockPage);
113
+
114
+        Page<DataReport> result = reportService.listReports(1, 10, "daily", null);
115
+
116
+        assertNotNull(result);
117
+        verify(dataReportMapper).selectPage(any(Page.class), any(LambdaQueryWrapper.class));
118
+    }
119
+
120
+    @Test
121
+    @DisplayName("模板CRUD操作")
122
+    void testTemplateOperations() {
123
+        // Create
124
+        ReportTemplate template = new ReportTemplate();
125
+        template.setTemplateName("测试模板");
126
+        template.setTemplateCode("TPL-TEST");
127
+        template.setReportType("daily");
128
+        template.setDataType("quantity");
129
+
130
+        when(reportTemplateMapper.insert(any(ReportTemplate.class))).thenReturn(1);
131
+        ReportTemplate created = reportService.createTemplate(template);
132
+        assertEquals("测试模板", created.getTemplateName());
133
+
134
+        // Get
135
+        when(reportTemplateMapper.selectById(1L)).thenReturn(template);
136
+        ReportTemplate found = reportService.getTemplate(1L);
137
+        assertNotNull(found);
138
+
139
+        // Not found
140
+        when(reportTemplateMapper.selectById(999L)).thenReturn(null);
141
+        assertThrows(RuntimeException.class, () -> reportService.getTemplate(999L));
142
+    }
143
+
144
+    @Test
145
+    @DisplayName("查询最近报表和统计")
146
+    void testRecentAndStatistics() {
147
+        when(dataReportMapper.findRecent(any(), any(), eq(5))).thenReturn(List.of());
148
+        when(dataReportMapper.countByType()).thenReturn(List.of());
149
+
150
+        List<DataReport> recent = reportService.findRecentReports(null, null, 5);
151
+        List<Map<String, Object>> stats = reportService.countReportsByType();
152
+
153
+        assertNotNull(recent);
154
+        assertNotNull(stats);
155
+        verify(dataReportMapper).findRecent(any(), any(), eq(5));
156
+        verify(dataReportMapper).countByType();
157
+    }
158
+}

+ 180
- 0
wm-data-engine/src/test/java/com/water/data_engine/service/StatisticsServiceTest.java Ver fichero

@@ -0,0 +1,180 @@
1
+package com.water.data_engine.service;
2
+
3
+import com.water.data_engine.entity.dto.StatisticsResult;
4
+import com.water.data_engine.mapper.*;
5
+import org.junit.jupiter.api.BeforeEach;
6
+import org.junit.jupiter.api.DisplayName;
7
+import org.junit.jupiter.api.Test;
8
+import org.junit.jupiter.api.extension.ExtendWith;
9
+import org.mockito.Mock;
10
+import org.mockito.junit.jupiter.MockitoExtension;
11
+
12
+import java.math.BigDecimal;
13
+import java.time.LocalDate;
14
+import java.util.*;
15
+
16
+import static org.junit.jupiter.api.Assertions.*;
17
+import static org.mockito.ArgumentMatchers.*;
18
+import static org.mockito.Mockito.*;
19
+
20
+/**
21
+ * 统计分析服务测试
22
+ */
23
+@ExtendWith(MockitoExtension.class)
24
+class StatisticsServiceTest {
25
+
26
+    @Mock
27
+    private WaterQuantityMapper waterQuantityMapper;
28
+
29
+    @Mock
30
+    private WaterQualityMapper waterQualityMapper;
31
+
32
+    @Mock
33
+    private StatQuantityDailyMapper statQuantityDailyMapper;
34
+
35
+    @Mock
36
+    private StatQualityDailyMapper statQualityDailyMapper;
37
+
38
+    private StatisticsService statisticsService;
39
+
40
+    @BeforeEach
41
+    void setUp() {
42
+        statisticsService = new StatisticsService(
43
+            waterQuantityMapper, waterQualityMapper,
44
+            statQuantityDailyMapper, statQualityDailyMapper
45
+        );
46
+    }
47
+
48
+    @Test
49
+    @DisplayName("水量同比分析-有数据")
50
+    void testQuantityYearOverYear() {
51
+        // Given
52
+        LocalDate date = LocalDate.of(2026, 6, 14);
53
+
54
+        Map<String, Object> currentData = new LinkedHashMap<>();
55
+        currentData.put("area", "城东");
56
+        currentData.put("sum_total_flow", 15000.0);
57
+
58
+        Map<String, Object> compareData = new LinkedHashMap<>();
59
+        compareData.put("area", "城东");
60
+        compareData.put("sum_total_flow", 12000.0);
61
+
62
+        when(statQuantityDailyMapper.sumByArea(any(), any(), isNull()))
63
+            .thenReturn(List.of(currentData))
64
+            .thenReturn(List.of(compareData));
65
+
66
+        // When
67
+        StatisticsResult result = statisticsService.quantityYearOverYear(date, null);
68
+
69
+        // Then
70
+        assertNotNull(result);
71
+        assertEquals("yoy", result.getType());
72
+        assertEquals("quantity", result.getDataType());
73
+        assertEquals(0, new BigDecimal("15000").compareTo(result.getCurrentValue()));
74
+        assertEquals(0, new BigDecimal("12000").compareTo(result.getCompareValue()));
75
+        assertTrue(result.getChangeRate().compareTo(BigDecimal.ZERO) > 0);
76
+    }
77
+
78
+    @Test
79
+    @DisplayName("水量环比分析-对比期为零")
80
+    void testQuantityMonthOverMonth_ZeroCompare() {
81
+        LocalDate date = LocalDate.of(2026, 3, 15);
82
+
83
+        Map<String, Object> currentData = new LinkedHashMap<>();
84
+        currentData.put("area", "城南");
85
+        currentData.put("sum_total_flow", 5000.0);
86
+
87
+        when(statQuantityDailyMapper.sumByArea(any(), any(), isNull()))
88
+            .thenReturn(List.of(currentData))
89
+            .thenReturn(List.of());
90
+
91
+        StatisticsResult result = statisticsService.quantityMonthOverMonth(date, null);
92
+
93
+        assertNotNull(result);
94
+        assertEquals("mom", result.getType());
95
+        assertEquals(0, new BigDecimal("100").compareTo(result.getChangeRate()));
96
+    }
97
+
98
+    @Test
99
+    @DisplayName("水量日趋势分析")
100
+    void testQuantityTrend() {
101
+        LocalDate start = LocalDate.of(2026, 1, 1);
102
+        LocalDate end = LocalDate.of(2026, 1, 7);
103
+
104
+        Map<String, Object> day1 = new LinkedHashMap<>();
105
+        day1.put("stat_date", "2026-01-01");
106
+        day1.put("avg_flow_rate", new BigDecimal("120.5"));
107
+        day1.put("daily_flow", new BigDecimal("2892"));
108
+        day1.put("avg_pressure", new BigDecimal("0.35"));
109
+
110
+        Map<String, Object> day2 = new LinkedHashMap<>();
111
+        day2.put("stat_date", "2026-01-02");
112
+        day2.put("avg_flow_rate", new BigDecimal("118.3"));
113
+        day2.put("daily_flow", new BigDecimal("2839"));
114
+        day2.put("avg_pressure", new BigDecimal("0.34"));
115
+
116
+        when(waterQuantityMapper.aggregateDaily(any(), any(), any(), any()))
117
+            .thenReturn(List.of(day1, day2));
118
+
119
+        StatisticsResult result = statisticsService.quantityTrend(start, end, null, null);
120
+
121
+        assertNotNull(result);
122
+        assertEquals("trend", result.getType());
123
+        assertEquals("quantity", result.getDataType());
124
+        assertEquals(2, result.getTrendData().size());
125
+        assertEquals("2026-01-01", result.getTrendData().get(0).get("date"));
126
+    }
127
+
128
+    @Test
129
+    @DisplayName("水质日趋势分析")
130
+    void testQualityTrend() {
131
+        LocalDate start = LocalDate.of(2026, 1, 1);
132
+        LocalDate end = LocalDate.of(2026, 1, 3);
133
+
134
+        Map<String, Object> day1 = new LinkedHashMap<>();
135
+        day1.put("stat_date", "2026-01-01");
136
+        day1.put("avg_turbidity", new BigDecimal("0.5"));
137
+        day1.put("qualified_rate", new BigDecimal("98.5"));
138
+
139
+        when(waterQualityMapper.aggregateDaily(any(), any(), any(), any()))
140
+            .thenReturn(List.of(day1));
141
+
142
+        StatisticsResult result = statisticsService.qualityTrend(start, end, "城东", null);
143
+
144
+        assertNotNull(result);
145
+        assertEquals("trend", result.getType());
146
+        assertEquals("quality", result.getDataType());
147
+        assertEquals(1, result.getTrendData().size());
148
+    }
149
+
150
+    @Test
151
+    @DisplayName("仪表板概览-综合统计")
152
+    void testDashboardOverview() {
153
+        LocalDate date = LocalDate.of(2026, 6, 14);
154
+
155
+        when(waterQuantityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of());
156
+        when(waterQualityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of());
157
+
158
+        Map<String, Object> result = statisticsService.dashboardOverview(date, null);
159
+
160
+        assertNotNull(result);
161
+        assertEquals(date.toString(), result.get("date"));
162
+        assertTrue(result.containsKey("quantityStats"));
163
+        assertTrue(result.containsKey("qualityStats"));
164
+    }
165
+
166
+    @Test
167
+    @DisplayName("水质同比分析-无数据返回零")
168
+    void testQualityYearOverYear_Empty() {
169
+        when(statQualityDailyMapper.sumByArea(any(), any(), any()))
170
+            .thenReturn(List.of())
171
+            .thenReturn(List.of());
172
+
173
+        StatisticsResult result = statisticsService.qualityYearOverYear(LocalDate.now(), null);
174
+
175
+        assertNotNull(result);
176
+        assertEquals("yoy", result.getType());
177
+        assertEquals(0, BigDecimal.ZERO.compareTo(result.getCurrentValue()));
178
+        assertEquals(0, BigDecimal.ZERO.compareTo(result.getChangeRate()));
179
+    }
180
+}