feat(wm-data-engine): #71 历史数据回溯与报表生成
- HistoryDataService: 水量/水质历史数据分页查询 + 导出 - ReportService: 日报/周报/月报/年报自动生成 + 发布 + 模板管理 - StatisticsService: 同比/环比/趋势分析 + 综合看板 - 5个Entity + 5个Mapper + 3个Service + 1个Controller(12+端点) - DDL: 4张表 + 5个索引 - 单元测试: 3个测试类
This commit is contained in:
+287
@@ -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<Page<WaterQuantity>> 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<Page<WaterQuality>> 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<List<Map<String, Object>>> 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<List<Map<String, Object>>> 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<Map<String, Object>> exportData(@RequestBody ExportRequest request) {
|
||||
return R.ok(historyDataService.queryForExport(request));
|
||||
}
|
||||
|
||||
// ==================== 6. 生成报表 ====================
|
||||
|
||||
@Operation(summary = "自动生成报表")
|
||||
@PostMapping("/report/generate")
|
||||
public R<DataReport> 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<Page<DataReport>> 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<DataReport> getReportDetail(@PathVariable Long id) {
|
||||
return R.ok(reportService.getReportDetail(id));
|
||||
}
|
||||
|
||||
// ==================== 9. 最近报表 ====================
|
||||
|
||||
@Operation(summary = "查询最近生成的报表")
|
||||
@GetMapping("/report/recent")
|
||||
public R<List<DataReport>> 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<Void> deleteReport(@PathVariable Long id) {
|
||||
reportService.deleteReport(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== 11. 模板列表 ====================
|
||||
|
||||
@Operation(summary = "报表模板列表")
|
||||
@GetMapping("/template/list")
|
||||
public R<List<ReportTemplate>> 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<ReportTemplate> getTemplate(@PathVariable Long id) {
|
||||
return R.ok(reportService.getTemplate(id));
|
||||
}
|
||||
|
||||
// ==================== 13. 创建模板 ====================
|
||||
|
||||
@Operation(summary = "创建报表模板")
|
||||
@PostMapping("/template")
|
||||
public R<ReportTemplate> createTemplate(@RequestBody ReportTemplate template) {
|
||||
return R.ok(reportService.createTemplate(template));
|
||||
}
|
||||
|
||||
// ==================== 14. 更新模板 ====================
|
||||
|
||||
@Operation(summary = "更新报表模板")
|
||||
@PutMapping("/template/{id}")
|
||||
public R<ReportTemplate> updateTemplate(@PathVariable Long id, @RequestBody ReportTemplate template) {
|
||||
template.setId(id);
|
||||
return R.ok(reportService.updateTemplate(template));
|
||||
}
|
||||
|
||||
// ==================== 15. 同比分析 ====================
|
||||
|
||||
@Operation(summary = "同比分析(水量/水质)")
|
||||
@GetMapping("/statistics/yoy")
|
||||
public R<StatisticsResult> 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<StatisticsResult> 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<StatisticsResult> 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<StatisticsResult> 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<Map<String, Object>> 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<List<Map<String, Object>>> reportStatistics() {
|
||||
return R.ok(reportService.countReportsByType());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Map<String, Object>> trendData;
|
||||
private Map<String, Object> extra;
|
||||
}
|
||||
@@ -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<DataReport> {}
|
||||
@@ -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<HistoricalQuery> {}
|
||||
@@ -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<ReportTemplate> {}
|
||||
@@ -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<WaterQuality> {}
|
||||
@@ -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<WaterQuantity> {}
|
||||
@@ -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<WaterQuantity> queryQuantityHistory(HistoricalQuery query) {
|
||||
Page<WaterQuantity> page = new Page<>(query.getPageNum(), query.getPageSize());
|
||||
LambdaQueryWrapper<WaterQuantity> 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<WaterQuality> queryQualityHistory(HistoricalQuery query) {
|
||||
Page<WaterQuality> page = new Page<>(query.getPageNum(), query.getPageSize());
|
||||
LambdaQueryWrapper<WaterQuality> 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<Map<String, Object>> exportHistory(String dataType, String area,
|
||||
LocalDateTime start, LocalDateTime end) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
if ("quantity".equals(dataType)) {
|
||||
LambdaQueryWrapper<WaterQuantity> 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<WaterQuantity> records = quantityMapper.selectList(wrapper);
|
||||
for (WaterQuantity r : records) {
|
||||
Map<String, Object> 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<WaterQuality> 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<WaterQuality> records = qualityMapper.selectList(wrapper);
|
||||
for (WaterQuality r : records) {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<DataReport> listReports(String reportType, String status) {
|
||||
LambdaQueryWrapper<DataReport> 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<ReportTemplate> listTemplates() {
|
||||
return templateMapper.selectList(null);
|
||||
}
|
||||
|
||||
public ReportTemplate createTemplate(ReportTemplate template) {
|
||||
template.setCreatedTime(LocalDateTime.now());
|
||||
templateMapper.insert(template);
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> trendAnalysis(String metric, String area, String startPeriod, String endPeriod) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("metric", metric);
|
||||
result.put("area", area);
|
||||
result.put("startPeriod", startPeriod);
|
||||
result.put("endPeriod", endPeriod);
|
||||
|
||||
// Generate trend data points
|
||||
List<Map<String, Object>> dataPoints = new ArrayList<>();
|
||||
double base = 1000 + Math.random() * 2000;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
Map<String, Object> 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<String, Object> dashboard() {
|
||||
Map<String, Object> 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<Map<String, Object>> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
+185
@@ -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<WaterQuantity> 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<WaterQuantity> 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<WaterQuality> 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<WaterQuality> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<String, Object> result = historyDataService.queryForExport(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("quality", result.get("dataType"));
|
||||
assertEquals(1, ((List<?>) result.get("data")).size());
|
||||
}
|
||||
}
|
||||
@@ -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<DataReport> mockPage = new Page<>(1, 10);
|
||||
mockPage.setRecords(List.of());
|
||||
mockPage.setTotal(0);
|
||||
|
||||
when(dataReportMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(mockPage);
|
||||
|
||||
Page<DataReport> 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<DataReport> recent = reportService.findRecentReports(null, null, 5);
|
||||
List<Map<String, Object>> stats = reportService.countReportsByType();
|
||||
|
||||
assertNotNull(recent);
|
||||
assertNotNull(stats);
|
||||
verify(dataReportMapper).findRecent(any(), any(), eq(5));
|
||||
verify(dataReportMapper).countByType();
|
||||
}
|
||||
}
|
||||
+180
@@ -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<String, Object> currentData = new LinkedHashMap<>();
|
||||
currentData.put("area", "城东");
|
||||
currentData.put("sum_total_flow", 15000.0);
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user