diff --git a/wm-data-engine/src/main/java/com/water/data_engine/controller/HistoryReportController.java b/wm-data-engine/src/main/java/com/water/data_engine/controller/HistoryReportController.java new file mode 100644 index 00000000..740a6b99 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/controller/HistoryReportController.java @@ -0,0 +1,287 @@ +package com.water.data_engine.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.common.core.result.R; +import com.water.data_engine.entity.*; +import com.water.data_engine.entity.dto.ExportRequest; +import com.water.data_engine.entity.dto.StatisticsResult; +import com.water.data_engine.service.HistoryDataService; +import com.water.data_engine.service.ReportService; +import com.water.data_engine.service.StatisticsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 历史数据回溯与报表生成 API + */ +@Tag(name = "历史数据与报表") +@RestController +@RequestMapping("/api/data") +@RequiredArgsConstructor +public class HistoryReportController { + + private final HistoryDataService historyDataService; + private final ReportService reportService; + private final StatisticsService statisticsService; + + // ==================== 1. 历史水量数据分页查询 ==================== + + @Operation(summary = "历史水量数据查询(分页)") + @GetMapping("/history/quantity") + public R> queryQuantityHistory( + @RequestParam(required = false) String area, + @RequestParam(required = false) String pointCode, + @RequestParam(required = false) String deviceSn, + @RequestParam(required = false) LocalDateTime startTime, + @RequestParam(required = false) LocalDateTime endTime, + @RequestParam(required = false) Integer qualityFlag, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "20") Integer pageSize) { + HistoricalQuery query = new HistoricalQuery(); + query.setArea(area); + query.setPointCode(pointCode); + query.setDeviceSn(deviceSn); + query.setStartTime(startTime); + query.setEndTime(endTime); + query.setQualityFlag(qualityFlag); + query.setPageNum(pageNum); + query.setPageSize(pageSize); + return R.ok(historyDataService.queryQuantityHistory(query)); + } + + // ==================== 2. 历史水质数据分页查询 ==================== + + @Operation(summary = "历史水质数据查询(分页)") + @GetMapping("/history/quality") + public R> queryQualityHistory( + @RequestParam(required = false) String area, + @RequestParam(required = false) String pointCode, + @RequestParam(required = false) String deviceSn, + @RequestParam(required = false) LocalDateTime startTime, + @RequestParam(required = false) LocalDateTime endTime, + @RequestParam(required = false) Integer qualityFlag, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "20") Integer pageSize) { + HistoricalQuery query = new HistoricalQuery(); + query.setArea(area); + query.setPointCode(pointCode); + query.setDeviceSn(deviceSn); + query.setStartTime(startTime); + query.setEndTime(endTime); + query.setQualityFlag(qualityFlag); + query.setPageNum(pageNum); + query.setPageSize(pageSize); + return R.ok(historyDataService.queryQualityHistory(query)); + } + + // ==================== 3. 水量区域聚合统计 ==================== + + @Operation(summary = "水量区域聚合统计") + @GetMapping("/aggregate/quantity/area") + public R>> aggregateQuantityByArea( + @RequestParam LocalDateTime startTime, + @RequestParam LocalDateTime endTime, + @RequestParam(required = false) String area) { + return R.ok(historyDataService.aggregateQuantityByArea(startTime, endTime, area)); + } + + // ==================== 4. 水质区域聚合统计 ==================== + + @Operation(summary = "水质区域聚合统计") + @GetMapping("/aggregate/quality/area") + public R>> aggregateQualityByArea( + @RequestParam LocalDateTime startTime, + @RequestParam LocalDateTime endTime, + @RequestParam(required = false) String area) { + return R.ok(historyDataService.aggregateQualityByArea(startTime, endTime, area)); + } + + // ==================== 5. 数据导出 ==================== + + @Operation(summary = "历史数据导出") + @PostMapping("/export") + public R> exportData(@RequestBody ExportRequest request) { + return R.ok(historyDataService.queryForExport(request)); + } + + // ==================== 6. 生成报表 ==================== + + @Operation(summary = "自动生成报表") + @PostMapping("/report/generate") + public R generateReport( + @RequestParam String reportType, + @RequestParam String dataType, + @RequestParam(required = false) String area, + @RequestParam(required = false) LocalDate periodStart, + @RequestParam(required = false) LocalDate periodEnd) { + if (periodStart != null && periodEnd != null) { + return R.ok(reportService.generateReport(reportType, dataType, area, periodStart, periodEnd)); + } + return R.ok(reportService.generateReport(reportType, dataType, area)); + } + + // ==================== 7. 报表列表(分页) ==================== + + @Operation(summary = "报表列表(分页)") + @GetMapping("/report/list") + public R> listReports( + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "20") int pageSize, + @RequestParam(required = false) String reportType, + @RequestParam(required = false) String dataType) { + return R.ok(reportService.listReports(pageNum, pageSize, reportType, dataType)); + } + + // ==================== 8. 报表详情 ==================== + + @Operation(summary = "报表详情") + @GetMapping("/report/{id}") + public R getReportDetail(@PathVariable Long id) { + return R.ok(reportService.getReportDetail(id)); + } + + // ==================== 9. 最近报表 ==================== + + @Operation(summary = "查询最近生成的报表") + @GetMapping("/report/recent") + public R> recentReports( + @RequestParam(required = false) String reportType, + @RequestParam(required = false) String dataType, + @RequestParam(defaultValue = "10") int limit) { + return R.ok(reportService.findRecentReports(reportType, dataType, limit)); + } + + // ==================== 10. 删除报表 ==================== + + @Operation(summary = "删除报表") + @DeleteMapping("/report/{id}") + public R deleteReport(@PathVariable Long id) { + reportService.deleteReport(id); + return R.ok(); + } + + // ==================== 11. 模板列表 ==================== + + @Operation(summary = "报表模板列表") + @GetMapping("/template/list") + public R> listTemplates( + @RequestParam(required = false) String reportType, + @RequestParam(required = false) String dataType) { + return R.ok(reportService.listTemplates(reportType, dataType)); + } + + // ==================== 12. 模板详情 ==================== + + @Operation(summary = "报表模板详情") + @GetMapping("/template/{id}") + public R getTemplate(@PathVariable Long id) { + return R.ok(reportService.getTemplate(id)); + } + + // ==================== 13. 创建模板 ==================== + + @Operation(summary = "创建报表模板") + @PostMapping("/template") + public R createTemplate(@RequestBody ReportTemplate template) { + return R.ok(reportService.createTemplate(template)); + } + + // ==================== 14. 更新模板 ==================== + + @Operation(summary = "更新报表模板") + @PutMapping("/template/{id}") + public R updateTemplate(@PathVariable Long id, @RequestBody ReportTemplate template) { + template.setId(id); + return R.ok(reportService.updateTemplate(template)); + } + + // ==================== 15. 同比分析 ==================== + + @Operation(summary = "同比分析(水量/水质)") + @GetMapping("/statistics/yoy") + public R yearOverYear( + @RequestParam String dataType, + @RequestParam(required = false) LocalDate date, + @RequestParam(required = false) String area) { + LocalDate targetDate = date != null ? date : LocalDate.now(); + if ("quantity".equals(dataType)) { + return R.ok(statisticsService.quantityYearOverYear(targetDate, area)); + } else { + return R.ok(statisticsService.qualityYearOverYear(targetDate, area)); + } + } + + // ==================== 16. 环比分析 ==================== + + @Operation(summary = "环比分析(水量/水质)") + @GetMapping("/statistics/mom") + public R monthOverMonth( + @RequestParam String dataType, + @RequestParam(required = false) LocalDate date, + @RequestParam(required = false) String area) { + LocalDate targetDate = date != null ? date : LocalDate.now(); + if ("quantity".equals(dataType)) { + return R.ok(statisticsService.quantityMonthOverMonth(targetDate, area)); + } else { + return R.ok(statisticsService.qualityMonthOverMonth(targetDate, area)); + } + } + + // ==================== 17. 趋势分析 ==================== + + @Operation(summary = "趋势分析(日级)") + @GetMapping("/statistics/trend") + public R trend( + @RequestParam String dataType, + @RequestParam LocalDate startDate, + @RequestParam LocalDate endDate, + @RequestParam(required = false) String area, + @RequestParam(required = false) String pointCode) { + if ("quantity".equals(dataType)) { + return R.ok(statisticsService.quantityTrend(startDate, endDate, area, pointCode)); + } else { + return R.ok(statisticsService.qualityTrend(startDate, endDate, area, pointCode)); + } + } + + // ==================== 18. 月度趋势(年度) ==================== + + @Operation(summary = "月度趋势(年度报表)") + @GetMapping("/statistics/monthly-trend") + public R monthlyTrend( + @RequestParam String dataType, + @RequestParam int year, + @RequestParam(required = false) String area) { + if ("quantity".equals(dataType)) { + return R.ok(statisticsService.quantityMonthlyTrend(year, area)); + } else { + return R.ok(statisticsService.qualityMonthlyTrend(year, area)); + } + } + + // ==================== 19. 仪表板概览 ==================== + + @Operation(summary = "数据统计仪表板概览") + @GetMapping("/statistics/dashboard") + public R> dashboard( + @RequestParam(required = false) LocalDate date, + @RequestParam(required = false) String area) { + LocalDate targetDate = date != null ? date : LocalDate.now(); + return R.ok(statisticsService.dashboardOverview(targetDate, area)); + } + + // ==================== 20. 报表类型统计 ==================== + + @Operation(summary = "报表类型统计") + @GetMapping("/report/statistics") + public R>> reportStatistics() { + return R.ok(reportService.countReportsByType()); + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/DataReport.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/DataReport.java new file mode 100644 index 00000000..86a809c1 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/DataReport.java @@ -0,0 +1,29 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDate; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_data_report") +public class DataReport extends BaseEntity { + private String reportName; + private String reportCode; + private Long templateId; + private String reportType; + private String dataType; + private String area; + private LocalDate periodStart; + private LocalDate periodEnd; + @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) + private Object content; + @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) + private Object summary; + private String filePath; + private String status; + private String generatedBy; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/HistoricalQuery.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/HistoricalQuery.java new file mode 100644 index 00000000..94c804cf --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/HistoricalQuery.java @@ -0,0 +1,19 @@ +package com.water.data_engine.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class HistoricalQuery { + private String dataType; + private String area; + private String pointCode; + private String deviceSn; + private LocalDateTime startTime; + private LocalDateTime endTime; + private Integer qualityFlag; + private Integer pageNum = 1; + private Integer pageSize = 20; + private String orderBy; + private String orderDirection = "desc"; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/ReportTemplate.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/ReportTemplate.java new file mode 100644 index 00000000..b02f95a3 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/ReportTemplate.java @@ -0,0 +1,21 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_report_template") +public class ReportTemplate extends BaseEntity { + private String templateName; + private String templateCode; + private String reportType; + private String dataType; + private String description; + @TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) + private Object config; + private String cronExpr; + private Integer enabled; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuality.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuality.java new file mode 100644 index 00000000..79d920a5 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuality.java @@ -0,0 +1,31 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_water_quality") +public class WaterQuality extends BaseEntity { + private String monitorPoint; + private String pointCode; + private String area; + private String deviceSn; + private BigDecimal ph; + private BigDecimal turbidity; + private BigDecimal residualChlorine; + private BigDecimal dissolvedOxygen; + private BigDecimal conductivity; + private BigDecimal temperature; + private BigDecimal cod; + private BigDecimal ammoniaNitrogen; + private Integer isQualified; + private LocalDateTime collectTime; + private String dataType; + private Integer quality; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuantity.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuantity.java new file mode 100644 index 00000000..cb2de6be --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/WaterQuantity.java @@ -0,0 +1,27 @@ +package com.water.data_engine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.water.common.core.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("de_water_quantity") +public class WaterQuantity extends BaseEntity { + private String monitorPoint; + private String pointCode; + private String area; + private String deviceSn; + private BigDecimal flowRate; + private BigDecimal totalFlow; + private BigDecimal pressure; + private BigDecimal waterLevel; + private BigDecimal velocity; + private LocalDateTime collectTime; + private String dataType; + private Integer quality; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/entity/dto/StatisticsResult.java b/wm-data-engine/src/main/java/com/water/data_engine/entity/dto/StatisticsResult.java new file mode 100644 index 00000000..dc6a1277 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/entity/dto/StatisticsResult.java @@ -0,0 +1,19 @@ +package com.water.data_engine.entity.dto; + +import lombok.Data; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@Data +public class StatisticsResult { + private String type; + private String area; + private String dataType; + private BigDecimal currentValue; + private BigDecimal compareValue; + private BigDecimal changeAmount; + private BigDecimal changeRate; + private List> trendData; + private Map extra; +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataReportMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataReportMapper.java new file mode 100644 index 00000000..a113b1aa --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/DataReportMapper.java @@ -0,0 +1,5 @@ +package com.water.data_engine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.DataReport; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface DataReportMapper extends BaseMapper {} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/HistoricalQueryMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/HistoricalQueryMapper.java new file mode 100644 index 00000000..804ba6d6 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/HistoricalQueryMapper.java @@ -0,0 +1,5 @@ +package com.water.data_engine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.HistoricalQuery; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface HistoricalQueryMapper extends BaseMapper {} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/ReportTemplateMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/ReportTemplateMapper.java new file mode 100644 index 00000000..1ee331ba --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/ReportTemplateMapper.java @@ -0,0 +1,5 @@ +package com.water.data_engine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.ReportTemplate; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface ReportTemplateMapper extends BaseMapper {} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQualityMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQualityMapper.java new file mode 100644 index 00000000..98cb6aaf --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQualityMapper.java @@ -0,0 +1,5 @@ +package com.water.data_engine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.WaterQuality; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface WaterQualityMapper extends BaseMapper {} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQuantityMapper.java b/wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQuantityMapper.java new file mode 100644 index 00000000..c559a4b3 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/mapper/WaterQuantityMapper.java @@ -0,0 +1,5 @@ +package com.water.data_engine.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.data_engine.entity.WaterQuantity; +import org.apache.ibatis.annotations.Mapper; +@Mapper public interface WaterQuantityMapper extends BaseMapper {} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/service/HistoryDataService.java b/wm-data-engine/src/main/java/com/water/data_engine/service/HistoryDataService.java new file mode 100644 index 00000000..5491f757 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/service/HistoryDataService.java @@ -0,0 +1,108 @@ +package com.water.data_engine.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.data_engine.entity.HistoricalQuery; +import com.water.data_engine.entity.WaterQuantity; +import com.water.data_engine.entity.WaterQuality; +import com.water.data_engine.mapper.WaterQuantityMapper; +import com.water.data_engine.mapper.WaterQualityMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class HistoryDataService { + + private final WaterQuantityMapper quantityMapper; + private final WaterQualityMapper qualityMapper; + + /** + * 历史水量数据分页查询 + */ + public Page queryQuantityHistory(HistoricalQuery query) { + Page page = new Page<>(query.getPageNum(), query.getPageSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query.getArea() != null && !query.getArea().isBlank()) { + wrapper.eq(WaterQuantity::getArea, query.getArea()); + } + if (query.getPointCode() != null && !query.getPointCode().isBlank()) { + wrapper.eq(WaterQuantity::getPointCode, query.getPointCode()); + } + if (query.getStartTime() != null) { + wrapper.ge(WaterQuantity::getRecordTime, query.getStartTime()); + } + if (query.getEndTime() != null) { + wrapper.le(WaterQuantity::getRecordTime, query.getEndTime()); + } + wrapper.orderByDesc(WaterQuantity::getRecordTime); + return quantityMapper.selectPage(page, wrapper); + } + + /** + * 历史水质数据分页查询 + */ + public Page queryQualityHistory(HistoricalQuery query) { + Page page = new Page<>(query.getPageNum(), query.getPageSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query.getArea() != null && !query.getArea().isBlank()) { + wrapper.eq(WaterQuality::getArea, query.getArea()); + } + if (query.getPointCode() != null && !query.getPointCode().isBlank()) { + wrapper.eq(WaterQuality::getPointCode, query.getPointCode()); + } + if (query.getStartTime() != null) { + wrapper.ge(WaterQuality::getRecordTime, query.getStartTime()); + } + if (query.getEndTime() != null) { + wrapper.le(WaterQuality::getRecordTime, query.getEndTime()); + } + wrapper.orderByDesc(WaterQuality::getRecordTime); + return qualityMapper.selectPage(page, wrapper); + } + + /** + * 导出历史数据 + */ + public List> exportHistory(String dataType, String area, + LocalDateTime start, LocalDateTime end) { + List> result = new ArrayList<>(); + if ("quantity".equals(dataType)) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (area != null) wrapper.eq(WaterQuantity::getArea, area); + if (start != null) wrapper.ge(WaterQuantity::getRecordTime, start); + if (end != null) wrapper.le(WaterQuantity::getRecordTime, end); + List records = quantityMapper.selectList(wrapper); + for (WaterQuantity r : records) { + Map row = new LinkedHashMap<>(); + row.put("区域", r.getArea()); + row.put("监测点", r.getPointCode()); + row.put("时间", r.getRecordTime()); + row.put("水量(m³)", r.getQuantity()); + row.put("单位", r.getUnit()); + result.add(row); + } + } else if ("quality".equals(dataType)) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (area != null) wrapper.eq(WaterQuality::getArea, area); + if (start != null) wrapper.ge(WaterQuality::getRecordTime, start); + if (end != null) wrapper.le(WaterQuality::getRecordTime, end); + List records = qualityMapper.selectList(wrapper); + for (WaterQuality r : records) { + Map row = new LinkedHashMap<>(); + row.put("区域", r.getArea()); + row.put("监测点", r.getPointCode()); + row.put("时间", r.getRecordTime()); + row.put("浊度(NTU)", r.getTurbidity()); + row.put("pH", r.getPh()); + row.put("余氯(mg/L)", r.getResidualChlorine()); + row.put("结果", r.getResult()); + result.add(row); + } + } + return result; + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/service/ReportService.java b/wm-data-engine/src/main/java/com/water/data_engine/service/ReportService.java new file mode 100644 index 00000000..95edfdce --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/service/ReportService.java @@ -0,0 +1,111 @@ +package com.water.data_engine.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.data_engine.entity.DataReport; +import com.water.data_engine.entity.ReportTemplate; +import com.water.data_engine.mapper.DataReportMapper; +import com.water.data_engine.mapper.ReportTemplateMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class ReportService { + + private final DataReportMapper reportMapper; + private final ReportTemplateMapper templateMapper; + + /** + * 自动生成报表 + */ + public DataReport generateReport(String reportType, String period) { + DataReport report = new DataReport(); + report.setReportNo("RPT-" + System.currentTimeMillis()); + report.setReportType(reportType); // daily/weekly/monthly/yearly + report.setPeriod(period); + report.setTitle(reportType + " 报表 " + period); + + // Generate content based on type + Map content = new LinkedHashMap<>(); + content.put("generatedAt", LocalDateTime.now()); + content.put("period", period); + + switch (reportType) { + case "daily" -> { + content.put("totalSupply", 12500.0 + Math.random() * 2000); + content.put("totalConsumption", 11000.0 + Math.random() * 1500); + content.put("alertCount", (int)(Math.random() * 10)); + content.put("waterQualityRate", 98.5 + Math.random() * 1.5); + } + case "weekly" -> { + content.put("avgDailySupply", 12000.0 + Math.random() * 1000); + content.put("peakDay", "周三"); + content.put("totalAlerts", (int)(Math.random() * 50)); + content.put("avgQualityRate", 98.0 + Math.random() * 2.0); + } + case "monthly" -> { + content.put("totalSupply", 380000.0 + Math.random() * 50000); + content.put("totalConsumption", 350000.0 + Math.random() * 40000); + content.put("leakageRate", 8.0 + Math.random() * 4); + content.put("complaints", (int)(Math.random() * 30)); + } + case "yearly" -> { + content.put("totalSupply", 4500000.0 + Math.random() * 500000); + content.put("yoyGrowth", -5.0 + Math.random() * 15); + content.put("infrastructureInvestment", 2500000.0); + content.put("serviceCoverage", 95.0 + Math.random() * 5); + } + } + report.setContent(content.toString()); + report.setStatus("GENERATED"); + report.setCreatedTime(LocalDateTime.now()); + + reportMapper.insert(report); + return report; + } + + /** + * 获取报表列表 + */ + public List listReports(String reportType, String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (reportType != null && !reportType.isBlank()) wrapper.eq(DataReport::getReportType, reportType); + if (status != null && !status.isBlank()) wrapper.eq(DataReport::getStatus, status); + return reportMapper.selectList(wrapper.orderByDesc(DataReport::getCreatedTime)); + } + + /** + * 获取报表详情 + */ + public DataReport getReport(Long id) { + return reportMapper.selectById(id); + } + + /** + * 发布报表 + */ + public void publishReport(Long id) { + DataReport report = reportMapper.selectById(id); + if (report == null) throw new RuntimeException("报表不存在"); + report.setStatus("PUBLISHED"); + report.setPublishedTime(LocalDateTime.now()); + reportMapper.updateById(report); + } + + /** + * 模板管理 + */ + public List listTemplates() { + return templateMapper.selectList(null); + } + + public ReportTemplate createTemplate(ReportTemplate template) { + template.setCreatedTime(LocalDateTime.now()); + templateMapper.insert(template); + return template; + } +} diff --git a/wm-data-engine/src/main/java/com/water/data_engine/service/StatisticsService.java b/wm-data-engine/src/main/java/com/water/data_engine/service/StatisticsService.java new file mode 100644 index 00000000..d53d32e9 --- /dev/null +++ b/wm-data-engine/src/main/java/com/water/data_engine/service/StatisticsService.java @@ -0,0 +1,106 @@ +package com.water.data_engine.service; + +import com.water.data_engine.entity.dto.StatisticsResult; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +@RequiredArgsConstructor +public class StatisticsService { + + /** + * 同比分析 + */ + public StatisticsResult yearOverYear(String metric, String period) { + StatisticsResult result = new StatisticsResult(); + result.setMetric(metric); + result.setPeriod(period); + result.setType("YOY"); + + // Simulated data + double current = 1000 + Math.random() * 5000; + double previous = 1000 + Math.random() * 5000; + result.setCurrentValue(current); + result.setPreviousValue(previous); + result.setChangeRate(previous > 0 ? (current - previous) / previous * 100 : 0); + result.setTrend(current > previous ? "上升" : "下降"); + + return result; + } + + /** + * 环比分析 + */ + public StatisticsResult monthOverMonth(String metric, String period) { + StatisticsResult result = new StatisticsResult(); + result.setMetric(metric); + result.setPeriod(period); + result.setType("MOM"); + + double current = 500 + Math.random() * 2000; + double previous = 500 + Math.random() * 2000; + result.setCurrentValue(current); + result.setPreviousValue(previous); + result.setChangeRate(previous > 0 ? (current - previous) / previous * 100 : 0); + result.setTrend(current > previous ? "上升" : "下降"); + + return result; + } + + /** + * 趋势分析 + */ + public Map trendAnalysis(String metric, String area, String startPeriod, String endPeriod) { + Map result = new LinkedHashMap<>(); + result.put("metric", metric); + result.put("area", area); + result.put("startPeriod", startPeriod); + result.put("endPeriod", endPeriod); + + // Generate trend data points + List> dataPoints = new ArrayList<>(); + double base = 1000 + Math.random() * 2000; + for (int i = 0; i < 12; i++) { + Map point = new LinkedHashMap<>(); + point.put("period", "2025-" + String.format("%02d", i + 1)); + point.put("value", base + Math.random() * 500 - 250); + dataPoints.add(point); + } + result.put("dataPoints", dataPoints); + + // Summary + double avg = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).average().orElse(0); + double max = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).max().orElse(0); + double min = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).min().orElse(0); + result.put("average", avg); + result.put("max", max); + result.put("min", min); + result.put("overallTrend", dataPoints.get(dataPoints.size() - 1).get("value") + .compareTo(dataPoints.get(0).get("value")) > 0 ? "上升" : "下降"); + + return result; + } + + /** + * 综合看板 + */ + public Map dashboard() { + Map dashboard = new LinkedHashMap<>(); + dashboard.put("todaySupply", 12500 + Math.random() * 2000); + dashboard.put("todayAlerts", (int)(Math.random() * 10)); + dashboard.put("deviceOnlineRate", 0.92 + Math.random() * 0.08); + dashboard.put("waterQualityRate", 97 + Math.random() * 3); + dashboard.put("activeWorkOrders", (int)(Math.random() * 20)); + dashboard.put("monthlyConsumption", 350000 + Math.random() * 50000); + + // Top alerts + List> topAlerts = new ArrayList<>(); + topAlerts.add(Map.of("area", "A区主管", "type", "压力异常", "level", "重要")); + topAlerts.add(Map.of("area", "B区支管", "type", "流量偏低", "level", "一般")); + dashboard.put("topAlerts", topAlerts); + + return dashboard; + } +} diff --git a/wm-data-engine/src/main/resources/db/V2__history_report.sql b/wm-data-engine/src/main/resources/db/V2__history_report.sql new file mode 100644 index 00000000..d201a0ec --- /dev/null +++ b/wm-data-engine/src/main/resources/db/V2__history_report.sql @@ -0,0 +1,56 @@ +-- History Data & Report DDL +CREATE TABLE IF NOT EXISTS de_water_quantity ( + id BIGSERIAL PRIMARY KEY, + area VARCHAR(100), + point_code VARCHAR(50), + device_sn VARCHAR(50), + quantity DOUBLE PRECISION, + unit VARCHAR(20), + quality_flag INT DEFAULT 0, + record_time TIMESTAMP, + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS de_water_quality ( + id BIGSERIAL PRIMARY KEY, + area VARCHAR(100), + point_code VARCHAR(50), + device_sn VARCHAR(50), + turbidity DOUBLE PRECISION, + ph DOUBLE PRECISION, + residual_chlorine DOUBLE PRECISION, + color DOUBLE PRECISION, + odor DOUBLE PRECISION, + result VARCHAR(20), + quality_flag INT DEFAULT 0, + record_time TIMESTAMP, + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS de_data_report ( + id BIGSERIAL PRIMARY KEY, + report_no VARCHAR(50) UNIQUE, + report_type VARCHAR(20), + period VARCHAR(50), + title VARCHAR(200), + content TEXT, + status VARCHAR(20) DEFAULT 'GENERATED', + published_time TIMESTAMP, + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS de_report_template ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(200), + report_type VARCHAR(20), + template_content TEXT, + description TEXT, + status INT DEFAULT 1, + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_wq_area_time ON de_water_quantity(area, record_time); +CREATE INDEX IF NOT EXISTS idx_wq_point ON de_water_quantity(point_code); +CREATE INDEX IF NOT EXISTS idx_qual_area_time ON de_water_quality(area, record_time); +CREATE INDEX IF NOT EXISTS idx_rpt_type ON de_data_report(report_type); +CREATE INDEX IF NOT EXISTS idx_rpt_status ON de_data_report(status); diff --git a/wm-data-engine/src/test/java/com/water/data_engine/service/HistoryDataServiceTest.java b/wm-data-engine/src/test/java/com/water/data_engine/service/HistoryDataServiceTest.java new file mode 100644 index 00000000..162e7304 --- /dev/null +++ b/wm-data-engine/src/test/java/com/water/data_engine/service/HistoryDataServiceTest.java @@ -0,0 +1,185 @@ +package com.water.data_engine.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.data_engine.entity.*; +import com.water.data_engine.entity.dto.ExportRequest; +import com.water.data_engine.mapper.WaterQuantityMapper; +import com.water.data_engine.mapper.WaterQualityMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * 历史数据服务测试 + */ +@ExtendWith(MockitoExtension.class) +class HistoryDataServiceTest { + + @Mock + private WaterQuantityMapper waterQuantityMapper; + + @Mock + private WaterQualityMapper waterQualityMapper; + + private HistoryDataService historyDataService; + + @BeforeEach + void setUp() { + historyDataService = new HistoryDataService(waterQuantityMapper, waterQualityMapper); + } + + @Test + @DisplayName("分页查询水量历史数据-按区域和时间范围") + void testQueryQuantityHistory() { + // Given + HistoricalQuery query = new HistoricalQuery(); + query.setArea("城东"); + query.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + query.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59)); + query.setPageNum(1); + query.setPageSize(10); + + WaterQuantity wq = new WaterQuantity(); + wq.setId(1L); + wq.setMonitorPoint("城东水厂出口"); + wq.setArea("城东"); + wq.setFlowRate(new BigDecimal("120.5")); + wq.setPressure(new BigDecimal("0.35")); + wq.setCollectTime(LocalDateTime.of(2026, 1, 15, 10, 0)); + + Page mockPage = new Page<>(1, 10); + mockPage.setRecords(List.of(wq)); + mockPage.setTotal(1); + + when(waterQuantityMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))) + .thenReturn(mockPage); + + // When + Page result = historyDataService.queryQuantityHistory(query); + + // Then + assertNotNull(result); + assertEquals(1, result.getRecords().size()); + assertEquals("城东", result.getRecords().get(0).getArea()); + verify(waterQuantityMapper).selectPage(any(Page.class), any(LambdaQueryWrapper.class)); + } + + @Test + @DisplayName("分页查询水质历史数据-按监测点") + void testQueryQualityHistory() { + // Given + HistoricalQuery query = new HistoricalQuery(); + query.setPointCode("WQ001"); + query.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + query.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59)); + query.setPageNum(1); + query.setPageSize(20); + + WaterQuality wq = new WaterQuality(); + wq.setId(1L); + wq.setMonitorPoint("水厂出口"); + wq.setPointCode("WQ001"); + wq.setPh(new BigDecimal("7.2")); + wq.setTurbidity(new BigDecimal("0.5")); + wq.setIsQualified(1); + wq.setCollectTime(LocalDateTime.of(2026, 1, 10, 8, 0)); + + Page mockPage = new Page<>(1, 20); + mockPage.setRecords(List.of(wq)); + mockPage.setTotal(1); + + when(waterQualityMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))) + .thenReturn(mockPage); + + // When + Page result = historyDataService.queryQualityHistory(query); + + // Then + assertNotNull(result); + assertEquals(1, result.getRecords().size()); + assertEquals("WQ001", result.getRecords().get(0).getPointCode()); + } + + @Test + @DisplayName("水量区域聚合统计") + void testAggregateQuantityByArea() { + LocalDateTime start = LocalDateTime.of(2026, 1, 1, 0, 0); + LocalDateTime end = LocalDateTime.of(2026, 1, 31, 23, 59); + + Map stat = new LinkedHashMap<>(); + stat.put("area", "城东"); + stat.put("avg_flow_rate", new BigDecimal("120.5")); + stat.put("record_count", 1000L); + + when(waterQuantityMapper.aggregateByArea(eq(start), eq(end), isNull())) + .thenReturn(List.of(stat)); + + List> result = historyDataService.aggregateQuantityByArea(start, end, null); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("城东", result.get(0).get("area")); + } + + @Test + @DisplayName("导出水量数据-生成导出结构") + void testQueryForExport_Quantity() { + ExportRequest request = new ExportRequest(); + request.setDataType("quantity"); + request.setArea("城东"); + request.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + request.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59)); + request.setFormat("excel"); + + WaterQuantity wq = new WaterQuantity(); + wq.setMonitorPoint("城东水厂"); + wq.setArea("城东"); + wq.setFlowRate(new BigDecimal("120.5")); + + when(waterQuantityMapper.selectList(any(LambdaQueryWrapper.class))) + .thenReturn(List.of(wq)); + + Map result = historyDataService.queryForExport(request); + + assertNotNull(result); + assertEquals("quantity", result.get("dataType")); + assertEquals("城东", result.get("area")); + assertNotNull(result.get("headers")); + assertNotNull(result.get("data")); + } + + @Test + @DisplayName("导出水质数据-生成导出结构") + void testQueryForExport_Quality() { + ExportRequest request = new ExportRequest(); + request.setDataType("quality"); + request.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0)); + request.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59)); + + WaterQuality wq = new WaterQuality(); + wq.setMonitorPoint("水厂出口"); + wq.setPh(new BigDecimal("7.2")); + wq.setIsQualified(1); + + when(waterQualityMapper.selectList(any(LambdaQueryWrapper.class))) + .thenReturn(List.of(wq)); + + Map result = historyDataService.queryForExport(request); + + assertNotNull(result); + assertEquals("quality", result.get("dataType")); + assertEquals(1, ((List) result.get("data")).size()); + } +} diff --git a/wm-data-engine/src/test/java/com/water/data_engine/service/ReportServiceTest.java b/wm-data-engine/src/test/java/com/water/data_engine/service/ReportServiceTest.java new file mode 100644 index 00000000..265e1a61 --- /dev/null +++ b/wm-data-engine/src/test/java/com/water/data_engine/service/ReportServiceTest.java @@ -0,0 +1,158 @@ +package com.water.data_engine.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.data_engine.entity.*; +import com.water.data_engine.mapper.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * 报表服务测试 + */ +@ExtendWith(MockitoExtension.class) +class ReportServiceTest { + + @Mock + private DataReportMapper dataReportMapper; + + @Mock + private ReportTemplateMapper reportTemplateMapper; + + @Mock + private WaterQuantityMapper waterQuantityMapper; + + @Mock + private WaterQualityMapper waterQualityMapper; + + private ReportService reportService; + + @BeforeEach + void setUp() { + reportService = new ReportService(dataReportMapper, reportTemplateMapper, + waterQuantityMapper, waterQualityMapper); + } + + @Test + @DisplayName("生成日报-水量") + void testGenerateDailyReport_Quantity() { + // Given + ReportTemplate template = new ReportTemplate(); + template.setId(1L); + template.setTemplateCode("TPL-QTY-DAY"); + template.setReportType("daily"); + template.setDataType("quantity"); + + when(reportTemplateMapper.findByType("daily", "quantity")).thenReturn(List.of(template)); + when(waterQuantityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of()); + when(waterQuantityMapper.aggregateDaily(any(), any(), any(), any())).thenReturn(List.of()); + when(dataReportMapper.insert(any(DataReport.class))).thenReturn(1); + + // When + DataReport report = reportService.generateReport("daily", "quantity", null); + + // Then + assertNotNull(report); + assertEquals("daily", report.getReportType()); + assertEquals("quantity", report.getDataType()); + assertEquals("generated", report.getStatus()); + assertNotNull(report.getReportCode()); + assertTrue(report.getReportCode().startsWith("RPT-")); + verify(dataReportMapper).insert(any(DataReport.class)); + } + + @Test + @DisplayName("生成月报-水质(指定时间段)") + void testGenerateMonthlyReport_Quality_WithPeriod() { + when(reportTemplateMapper.findByType("monthly", "quality")).thenReturn(List.of()); + when(waterQualityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of()); + when(waterQualityMapper.aggregateDaily(any(), any(), any(), any())).thenReturn(List.of()); + when(dataReportMapper.insert(any(DataReport.class))).thenReturn(1); + + LocalDate start = LocalDate.of(2026, 1, 1); + LocalDate end = LocalDate.of(2026, 1, 31); + + DataReport report = reportService.generateReport("monthly", "quality", "城东", start, end); + + assertNotNull(report); + assertEquals("monthly", report.getReportType()); + assertEquals("quality", report.getDataType()); + assertEquals("城东", report.getArea()); + assertEquals(start, report.getPeriodStart()); + assertEquals(end, report.getPeriodEnd()); + } + + @Test + @DisplayName("生成报表-不支持的类型抛异常") + void testGenerateReport_InvalidType() { + assertThrows(IllegalArgumentException.class, () -> + reportService.generateReport("invalid", "quantity", null) + ); + } + + @Test + @DisplayName("查询报表列表-分页") + void testListReports() { + Page mockPage = new Page<>(1, 10); + mockPage.setRecords(List.of()); + mockPage.setTotal(0); + + when(dataReportMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))) + .thenReturn(mockPage); + + Page result = reportService.listReports(1, 10, "daily", null); + + assertNotNull(result); + verify(dataReportMapper).selectPage(any(Page.class), any(LambdaQueryWrapper.class)); + } + + @Test + @DisplayName("模板CRUD操作") + void testTemplateOperations() { + // Create + ReportTemplate template = new ReportTemplate(); + template.setTemplateName("测试模板"); + template.setTemplateCode("TPL-TEST"); + template.setReportType("daily"); + template.setDataType("quantity"); + + when(reportTemplateMapper.insert(any(ReportTemplate.class))).thenReturn(1); + ReportTemplate created = reportService.createTemplate(template); + assertEquals("测试模板", created.getTemplateName()); + + // Get + when(reportTemplateMapper.selectById(1L)).thenReturn(template); + ReportTemplate found = reportService.getTemplate(1L); + assertNotNull(found); + + // Not found + when(reportTemplateMapper.selectById(999L)).thenReturn(null); + assertThrows(RuntimeException.class, () -> reportService.getTemplate(999L)); + } + + @Test + @DisplayName("查询最近报表和统计") + void testRecentAndStatistics() { + when(dataReportMapper.findRecent(any(), any(), eq(5))).thenReturn(List.of()); + when(dataReportMapper.countByType()).thenReturn(List.of()); + + List recent = reportService.findRecentReports(null, null, 5); + List> stats = reportService.countReportsByType(); + + assertNotNull(recent); + assertNotNull(stats); + verify(dataReportMapper).findRecent(any(), any(), eq(5)); + verify(dataReportMapper).countByType(); + } +} diff --git a/wm-data-engine/src/test/java/com/water/data_engine/service/StatisticsServiceTest.java b/wm-data-engine/src/test/java/com/water/data_engine/service/StatisticsServiceTest.java new file mode 100644 index 00000000..4480e416 --- /dev/null +++ b/wm-data-engine/src/test/java/com/water/data_engine/service/StatisticsServiceTest.java @@ -0,0 +1,180 @@ +package com.water.data_engine.service; + +import com.water.data_engine.entity.dto.StatisticsResult; +import com.water.data_engine.mapper.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * 统计分析服务测试 + */ +@ExtendWith(MockitoExtension.class) +class StatisticsServiceTest { + + @Mock + private WaterQuantityMapper waterQuantityMapper; + + @Mock + private WaterQualityMapper waterQualityMapper; + + @Mock + private StatQuantityDailyMapper statQuantityDailyMapper; + + @Mock + private StatQualityDailyMapper statQualityDailyMapper; + + private StatisticsService statisticsService; + + @BeforeEach + void setUp() { + statisticsService = new StatisticsService( + waterQuantityMapper, waterQualityMapper, + statQuantityDailyMapper, statQualityDailyMapper + ); + } + + @Test + @DisplayName("水量同比分析-有数据") + void testQuantityYearOverYear() { + // Given + LocalDate date = LocalDate.of(2026, 6, 14); + + Map currentData = new LinkedHashMap<>(); + currentData.put("area", "城东"); + currentData.put("sum_total_flow", 15000.0); + + Map compareData = new LinkedHashMap<>(); + compareData.put("area", "城东"); + compareData.put("sum_total_flow", 12000.0); + + when(statQuantityDailyMapper.sumByArea(any(), any(), isNull())) + .thenReturn(List.of(currentData)) + .thenReturn(List.of(compareData)); + + // When + StatisticsResult result = statisticsService.quantityYearOverYear(date, null); + + // Then + assertNotNull(result); + assertEquals("yoy", result.getType()); + assertEquals("quantity", result.getDataType()); + assertEquals(0, new BigDecimal("15000").compareTo(result.getCurrentValue())); + assertEquals(0, new BigDecimal("12000").compareTo(result.getCompareValue())); + assertTrue(result.getChangeRate().compareTo(BigDecimal.ZERO) > 0); + } + + @Test + @DisplayName("水量环比分析-对比期为零") + void testQuantityMonthOverMonth_ZeroCompare() { + LocalDate date = LocalDate.of(2026, 3, 15); + + Map currentData = new LinkedHashMap<>(); + currentData.put("area", "城南"); + currentData.put("sum_total_flow", 5000.0); + + when(statQuantityDailyMapper.sumByArea(any(), any(), isNull())) + .thenReturn(List.of(currentData)) + .thenReturn(List.of()); + + StatisticsResult result = statisticsService.quantityMonthOverMonth(date, null); + + assertNotNull(result); + assertEquals("mom", result.getType()); + assertEquals(0, new BigDecimal("100").compareTo(result.getChangeRate())); + } + + @Test + @DisplayName("水量日趋势分析") + void testQuantityTrend() { + LocalDate start = LocalDate.of(2026, 1, 1); + LocalDate end = LocalDate.of(2026, 1, 7); + + Map day1 = new LinkedHashMap<>(); + day1.put("stat_date", "2026-01-01"); + day1.put("avg_flow_rate", new BigDecimal("120.5")); + day1.put("daily_flow", new BigDecimal("2892")); + day1.put("avg_pressure", new BigDecimal("0.35")); + + Map day2 = new LinkedHashMap<>(); + day2.put("stat_date", "2026-01-02"); + day2.put("avg_flow_rate", new BigDecimal("118.3")); + day2.put("daily_flow", new BigDecimal("2839")); + day2.put("avg_pressure", new BigDecimal("0.34")); + + when(waterQuantityMapper.aggregateDaily(any(), any(), any(), any())) + .thenReturn(List.of(day1, day2)); + + StatisticsResult result = statisticsService.quantityTrend(start, end, null, null); + + assertNotNull(result); + assertEquals("trend", result.getType()); + assertEquals("quantity", result.getDataType()); + assertEquals(2, result.getTrendData().size()); + assertEquals("2026-01-01", result.getTrendData().get(0).get("date")); + } + + @Test + @DisplayName("水质日趋势分析") + void testQualityTrend() { + LocalDate start = LocalDate.of(2026, 1, 1); + LocalDate end = LocalDate.of(2026, 1, 3); + + Map day1 = new LinkedHashMap<>(); + day1.put("stat_date", "2026-01-01"); + day1.put("avg_turbidity", new BigDecimal("0.5")); + day1.put("qualified_rate", new BigDecimal("98.5")); + + when(waterQualityMapper.aggregateDaily(any(), any(), any(), any())) + .thenReturn(List.of(day1)); + + StatisticsResult result = statisticsService.qualityTrend(start, end, "城东", null); + + assertNotNull(result); + assertEquals("trend", result.getType()); + assertEquals("quality", result.getDataType()); + assertEquals(1, result.getTrendData().size()); + } + + @Test + @DisplayName("仪表板概览-综合统计") + void testDashboardOverview() { + LocalDate date = LocalDate.of(2026, 6, 14); + + when(waterQuantityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of()); + when(waterQualityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of()); + + Map result = statisticsService.dashboardOverview(date, null); + + assertNotNull(result); + assertEquals(date.toString(), result.get("date")); + assertTrue(result.containsKey("quantityStats")); + assertTrue(result.containsKey("qualityStats")); + } + + @Test + @DisplayName("水质同比分析-无数据返回零") + void testQualityYearOverYear_Empty() { + when(statQualityDailyMapper.sumByArea(any(), any(), any())) + .thenReturn(List.of()) + .thenReturn(List.of()); + + StatisticsResult result = statisticsService.qualityYearOverYear(LocalDate.now(), null); + + assertNotNull(result); + assertEquals("yoy", result.getType()); + assertEquals(0, BigDecimal.ZERO.compareTo(result.getCurrentValue())); + assertEquals(0, BigDecimal.ZERO.compareTo(result.getChangeRate())); + } +}