feat(wm-production): #61 总览大屏后端服务
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package com.water.production.controller;
|
||||
|
||||
import com.water.common.core.result.R;
|
||||
import com.water.production.entity.DashboardSummary;
|
||||
import com.water.production.entity.EnergyConsumption;
|
||||
import com.water.production.service.DashboardService;
|
||||
import com.water.production.service.EnergyService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 总览大屏控制器
|
||||
* 提供进出水量、水质、设备、报警、能耗等大屏数据接口
|
||||
*/
|
||||
@Tag(name = "总览大屏")
|
||||
@RestController
|
||||
@RequestMapping("/api/production/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
private final EnergyService energyService;
|
||||
|
||||
// ==================== 综合大屏 ====================
|
||||
|
||||
@Operation(summary = "获取完整大屏数据(聚合所有维度)")
|
||||
@GetMapping("/full")
|
||||
public R<Map<String, Object>> fullDashboard(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||
return R.ok(dashboardService.getFullDashboard(area));
|
||||
}
|
||||
|
||||
// ==================== 进出水量 ====================
|
||||
|
||||
@Operation(summary = "进出水量概览(今日/昨日/本月 + 趋势对比)")
|
||||
@GetMapping("/flow/summary")
|
||||
public R<Map<String, Object>> flowSummary(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||
return R.ok(dashboardService.getFlowSummary(area));
|
||||
}
|
||||
|
||||
@Operation(summary = "月度进出水量趋势")
|
||||
@GetMapping("/flow/monthly-trend")
|
||||
public R<List<Map<String, Object>>> monthlyFlowTrend(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||
@RequestParam(defaultValue = "6") int months) {
|
||||
return R.ok(dashboardService.getMonthlyFlowTrend(area, months));
|
||||
}
|
||||
|
||||
// ==================== 水质概览 ====================
|
||||
|
||||
@Operation(summary = "水质概览(原水/出厂水/末梢水 + 合格率)")
|
||||
@GetMapping("/water-quality/summary")
|
||||
public R<Map<String, Object>> waterQualitySummary(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||
return R.ok(dashboardService.getWaterQualitySummary(area));
|
||||
}
|
||||
|
||||
// ==================== 设备概况 ====================
|
||||
|
||||
@Operation(summary = "设备概况(在线/离线/故障 + 在线率)")
|
||||
@GetMapping("/device/summary")
|
||||
public R<Map<String, Object>> deviceSummary(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||
return R.ok(dashboardService.getDeviceSummary(area));
|
||||
}
|
||||
|
||||
// ==================== 报警概览 ====================
|
||||
|
||||
@Operation(summary = "报警概览(今日报警/活跃报警/级别分布)")
|
||||
@GetMapping("/alert/summary")
|
||||
public R<Map<String, Object>> alertSummary(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||
return R.ok(dashboardService.getAlertSummary(area));
|
||||
}
|
||||
|
||||
// ==================== 能耗数据 ====================
|
||||
|
||||
@Operation(summary = "今日能耗汇总(电耗/药耗)")
|
||||
@GetMapping("/energy/today")
|
||||
public R<Map<String, Object>> todayEnergy(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area) {
|
||||
return R.ok(energyService.getTodaySummary(area));
|
||||
}
|
||||
|
||||
@Operation(summary = "能耗趋势(按日,最近N天)")
|
||||
@GetMapping("/energy/trend")
|
||||
public R<List<Map<String, Object>>> energyTrend(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||
@RequestParam(defaultValue = "7") int days) {
|
||||
return R.ok(energyService.getEnergyTrend(area, days));
|
||||
}
|
||||
|
||||
@Operation(summary = "月度能耗趋势")
|
||||
@GetMapping("/energy/monthly-trend")
|
||||
public R<List<Map<String, Object>>> monthlyEnergyTrend(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||
@RequestParam(defaultValue = "6") int months) {
|
||||
return R.ok(energyService.getMonthlyTrend(area, months));
|
||||
}
|
||||
|
||||
@Operation(summary = "单位产水能耗")
|
||||
@GetMapping("/energy/unit-consumption")
|
||||
public R<Map<String, Object>> unitEnergyConsumption(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
return R.ok(energyService.getUnitEnergyConsumption(area, startDate, endDate));
|
||||
}
|
||||
|
||||
@Operation(summary = "能耗记录查询(按类型)")
|
||||
@GetMapping("/energy/records")
|
||||
public R<List<EnergyConsumption>> energyRecords(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||
@RequestParam String energyType,
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
return R.ok(energyService.getRecordsByType(area, energyType, startDate, endDate));
|
||||
}
|
||||
|
||||
// ==================== 历史快照 ====================
|
||||
|
||||
@Operation(summary = "历史总览快照(最近N天)")
|
||||
@GetMapping("/history/snapshots")
|
||||
public R<List<DashboardSummary>> historySnapshots(
|
||||
@RequestParam(defaultValue = "一体化水厂") String area,
|
||||
@RequestParam(defaultValue = "7") int days) {
|
||||
return R.ok(dashboardService.getHistorySnapshots(area, days));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 总览大屏汇总实体
|
||||
* 存储每日聚合的进出水量、水质、设备、报警、能耗快照数据
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_dashboard_summary")
|
||||
public class DashboardSummary {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 统计日期 */
|
||||
private LocalDate summaryDate;
|
||||
|
||||
/** 所属区域 */
|
||||
private String area;
|
||||
|
||||
// ---- 进出水量 ----
|
||||
/** 今日进水量(m³) */
|
||||
private BigDecimal todayInflow;
|
||||
|
||||
/** 今日出水量(m³) */
|
||||
private BigDecimal todayOutflow;
|
||||
|
||||
/** 昨日进水量(m³) */
|
||||
private BigDecimal yesterdayInflow;
|
||||
|
||||
/** 昨日出水量(m³) */
|
||||
private BigDecimal yesterdayOutflow;
|
||||
|
||||
/** 本月累计进水量(m³) */
|
||||
private BigDecimal monthInflow;
|
||||
|
||||
/** 本月累计出水量(m³) */
|
||||
private BigDecimal monthOutflow;
|
||||
|
||||
// ---- 水质概览 ----
|
||||
/** 原水合格率(%) */
|
||||
private BigDecimal rawWaterPassRate;
|
||||
|
||||
/** 出厂水合格率(%) */
|
||||
private BigDecimal factoryWaterPassRate;
|
||||
|
||||
/** 末梢水合格率(%) */
|
||||
private BigDecimal terminalWaterPassRate;
|
||||
|
||||
/** 综合合格率(%) */
|
||||
private BigDecimal overallPassRate;
|
||||
|
||||
// ---- 设备概况 ----
|
||||
/** 设备总数 */
|
||||
private Integer deviceTotal;
|
||||
|
||||
/** 在线设备数 */
|
||||
private Integer deviceOnline;
|
||||
|
||||
/** 离线设备数 */
|
||||
private Integer deviceOffline;
|
||||
|
||||
/** 故障设备数 */
|
||||
private Integer deviceFault;
|
||||
|
||||
/** 设备在线率(%) */
|
||||
private BigDecimal deviceOnlineRate;
|
||||
|
||||
// ---- 报警概览 ----
|
||||
/** 今日报警总数 */
|
||||
private Integer todayAlertTotal;
|
||||
|
||||
/** 活跃报警数 */
|
||||
private Integer activeAlertCount;
|
||||
|
||||
/** 一般报警数 */
|
||||
private Integer generalAlertCount;
|
||||
|
||||
/** 重要报警数 */
|
||||
private Integer importantAlertCount;
|
||||
|
||||
/** 紧急报警数 */
|
||||
private Integer urgentAlertCount;
|
||||
|
||||
// ---- 能耗数据 ----
|
||||
/** 今日电耗(kWh) */
|
||||
private BigDecimal todayPowerKwh;
|
||||
|
||||
/** 今日药耗(kg) */
|
||||
private BigDecimal todayChemicalKg;
|
||||
|
||||
/** 单位产水能耗(kWh/m³) */
|
||||
private BigDecimal unitEnergyConsumption;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.water.production.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 能耗数据实体
|
||||
* 记录每日电耗、药耗及产水量,用于能耗分析和成本核算
|
||||
*/
|
||||
@Data
|
||||
@TableName("prod_energy_consumption")
|
||||
public class EnergyConsumption {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 统计日期 */
|
||||
private LocalDate recordDate;
|
||||
|
||||
/** 所属区域/水厂 */
|
||||
private String area;
|
||||
|
||||
/** 能耗类型: power(电耗)/coagulant(混凝剂)/disinfectant(消毒剂) */
|
||||
private String energyType;
|
||||
|
||||
/** 能耗数值 */
|
||||
private BigDecimal consumption;
|
||||
|
||||
/** 计量单位: kWh/kg */
|
||||
private String unit;
|
||||
|
||||
/** 当日产水量(m³),用于计算单位能耗 */
|
||||
private BigDecimal productionVolume;
|
||||
|
||||
/** 单位产水能耗(kWh/m³ 或 kg/m³) */
|
||||
private BigDecimal unitConsumption;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.DashboardSummary;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface DashboardSummaryMapper extends BaseMapper<DashboardSummary> {
|
||||
|
||||
/**
|
||||
* 查询最近N天的总览快照(按日期倒序)
|
||||
*/
|
||||
@Select("SELECT * FROM prod_dashboard_summary WHERE area = #{area} " +
|
||||
"ORDER BY summary_date DESC LIMIT #{days}")
|
||||
List<DashboardSummary> findRecent(@Param("area") String area, @Param("days") int days);
|
||||
|
||||
/**
|
||||
* 按月份汇总进出水量趋势
|
||||
*/
|
||||
@Select("SELECT DATE_TRUNC('month', summary_date) as month, " +
|
||||
"SUM(today_inflow) as total_inflow, SUM(today_outflow) as total_outflow " +
|
||||
"FROM prod_dashboard_summary WHERE area = #{area} AND summary_date >= #{startDate} " +
|
||||
"GROUP BY DATE_TRUNC('month', summary_date) ORDER BY month")
|
||||
List<Map<String, Object>> monthlyFlowTrend(@Param("area") String area, @Param("startDate") String startDate);
|
||||
|
||||
/**
|
||||
* 按日期查询进出水量趋势(最近N天)
|
||||
*/
|
||||
@Select("SELECT summary_date, today_inflow, today_outflow " +
|
||||
"FROM prod_dashboard_summary WHERE area = #{area} " +
|
||||
"ORDER BY summary_date DESC LIMIT #{days}")
|
||||
List<Map<String, Object>> dailyFlowTrend(@Param("area") String area, @Param("days") int days);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.water.production.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.water.production.entity.EnergyConsumption;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface EnergyConsumptionMapper extends BaseMapper<EnergyConsumption> {
|
||||
|
||||
/**
|
||||
* 按区域和日期范围查询能耗数据
|
||||
*/
|
||||
@Select("SELECT * FROM prod_energy_consumption WHERE area = #{area} " +
|
||||
"AND record_date BETWEEN #{startDate} AND #{endDate} ORDER BY record_date")
|
||||
List<EnergyConsumption> findByDateRange(@Param("area") String area,
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
|
||||
/**
|
||||
* 按月份汇总能耗趋势
|
||||
*/
|
||||
@Select("SELECT DATE_TRUNC('month', record_date) as month, energy_type, " +
|
||||
"SUM(consumption) as total_consumption, SUM(production_volume) as total_production " +
|
||||
"FROM prod_energy_consumption WHERE area = #{area} AND record_date >= #{startDate} " +
|
||||
"GROUP BY DATE_TRUNC('month', record_date), energy_type ORDER BY month")
|
||||
List<Map<String, Object>> monthlyEnergyTrend(@Param("area") String area,
|
||||
@Param("startDate") String startDate);
|
||||
|
||||
/**
|
||||
* 统计指定日期的能耗汇总
|
||||
*/
|
||||
@Select("SELECT energy_type, SUM(consumption) as total, unit " +
|
||||
"FROM prod_energy_consumption WHERE area = #{area} AND record_date = #{date} " +
|
||||
"GROUP BY energy_type, unit")
|
||||
List<Map<String, Object>> dailySummary(@Param("area") String area, @Param("date") String date);
|
||||
|
||||
/**
|
||||
* 计算单位产水能耗(指定日期范围)
|
||||
*/
|
||||
@Select("SELECT SUM(CASE WHEN energy_type='power' THEN consumption ELSE 0 END) as total_power, " +
|
||||
"SUM(production_volume) as total_production " +
|
||||
"FROM prod_energy_consumption WHERE area = #{area} " +
|
||||
"AND record_date BETWEEN #{startDate} AND #{endDate}")
|
||||
Map<String, Object> unitEnergyStats(@Param("area") String area,
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.production.entity.DashboardSummary;
|
||||
import com.water.production.mapper.DashboardSummaryMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@@ -11,6 +17,7 @@ import java.util.*;
|
||||
public class DashboardService {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final DashboardSummaryMapper dashboardSummaryMapper;
|
||||
|
||||
/** 获取供水总览数据(按角色自动定位区域) */
|
||||
public Map<String, Object> getOverview(String area, String roleType) {
|
||||
@@ -58,4 +65,266 @@ public class DashboardService {
|
||||
sql.append(" ORDER BY last_report_time DESC LIMIT 100");
|
||||
return jdbc.queryForList(sql.toString());
|
||||
}
|
||||
|
||||
// ==================== 总览大屏增强接口 ====================
|
||||
|
||||
/**
|
||||
* 进出水量概览(今日/昨日/本月 + 趋势对比)
|
||||
*/
|
||||
public Map<String, Object> getFlowSummary(String area) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("area", area);
|
||||
|
||||
// 今日进出水量
|
||||
try {
|
||||
Map<String, Object> todayFlow = jdbc.queryForMap(
|
||||
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
|
||||
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
|
||||
"FROM iot_telemetry WHERE ts >= CURRENT_DATE AND area = ?", area);
|
||||
result.put("todayInflow", todayFlow.get("inflow"));
|
||||
result.put("todayOutflow", todayFlow.get("outflow"));
|
||||
} catch (Exception e) {
|
||||
result.put("todayInflow", 0);
|
||||
result.put("todayOutflow", 0);
|
||||
}
|
||||
|
||||
// 昨日进出水量
|
||||
try {
|
||||
Map<String, Object> yesterdayFlow = jdbc.queryForMap(
|
||||
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
|
||||
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
|
||||
"FROM iot_telemetry WHERE ts >= CURRENT_DATE - 1 AND ts < CURRENT_DATE AND area = ?", area);
|
||||
result.put("yesterdayInflow", yesterdayFlow.get("inflow"));
|
||||
result.put("yesterdayOutflow", yesterdayFlow.get("outflow"));
|
||||
} catch (Exception e) {
|
||||
result.put("yesterdayInflow", 0);
|
||||
result.put("yesterdayOutflow", 0);
|
||||
}
|
||||
|
||||
// 本月累计
|
||||
try {
|
||||
Map<String, Object> monthFlow = jdbc.queryForMap(
|
||||
"SELECT COALESCE(SUM(CASE WHEN metric_key='inflow' THEN metric_value ELSE 0 END),0) AS inflow, " +
|
||||
"COALESCE(SUM(CASE WHEN metric_key='outflow' THEN metric_value ELSE 0 END),0) AS outflow " +
|
||||
"FROM iot_telemetry WHERE ts >= DATE_TRUNC('month', CURRENT_DATE) AND area = ?", area);
|
||||
result.put("monthInflow", monthFlow.get("inflow"));
|
||||
result.put("monthOutflow", monthFlow.get("outflow"));
|
||||
} catch (Exception e) {
|
||||
result.put("monthInflow", 0);
|
||||
result.put("monthOutflow", 0);
|
||||
}
|
||||
|
||||
// 日环比
|
||||
computeDayOverDay(result);
|
||||
|
||||
// 最近7天趋势
|
||||
result.put("trend", dashboardSummaryMapper.dailyFlowTrend(area, 7));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 水质概览(原水/出厂水/末梢水关键指标 + 合格率)
|
||||
*/
|
||||
public Map<String, Object> getWaterQualitySummary(String area) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("area", area);
|
||||
|
||||
// 各类型水质合格率
|
||||
String[] types = {"raw", "factory", "terminal"};
|
||||
String[] labels = {"原水", "出厂水", "末梢水"};
|
||||
int totalTests = 0;
|
||||
int totalPassed = 0;
|
||||
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
try {
|
||||
Map<String, Object> stats = jdbc.queryForMap(
|
||||
"SELECT COUNT(*) as total, " +
|
||||
"COALESCE(SUM(CASE WHEN result='合格' THEN 1 ELSE 0 END),0) as passed " +
|
||||
"FROM prod_water_quality WHERE position_type = ?", types[i]);
|
||||
int total = ((Number) stats.get("total")).intValue();
|
||||
int passed = ((Number) stats.get("passed")).intValue();
|
||||
double rate = total > 0 ? (passed * 100.0 / total) : 100.0;
|
||||
|
||||
Map<String, Object> typeResult = new LinkedHashMap<>();
|
||||
typeResult.put("label", labels[i]);
|
||||
typeResult.put("totalTests", total);
|
||||
typeResult.put("passedTests", passed);
|
||||
typeResult.put("passRate", BigDecimal.valueOf(rate).setScale(1, RoundingMode.HALF_UP));
|
||||
result.put(types[i] + "Water", typeResult);
|
||||
|
||||
totalTests += total;
|
||||
totalPassed += passed;
|
||||
} catch (Exception e) {
|
||||
result.put(types[i] + "Water", Map.of("label", labels[i], "totalTests", 0, "passedTests", 0, "passRate", 100.0));
|
||||
}
|
||||
}
|
||||
|
||||
// 综合合格率
|
||||
double overallRate = totalTests > 0 ? (totalPassed * 100.0 / totalTests) : 100.0;
|
||||
result.put("overallPassRate", BigDecimal.valueOf(overallRate).setScale(1, RoundingMode.HALF_UP));
|
||||
|
||||
// 最新水质记录
|
||||
try {
|
||||
result.put("latestRecords", jdbc.queryForList(
|
||||
"SELECT * FROM prod_water_quality ORDER BY created_time DESC LIMIT 5"));
|
||||
} catch (Exception e) {
|
||||
result.put("latestRecords", Collections.emptyList());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备概况(在线/离线/故障数量 + 在线率)
|
||||
*/
|
||||
public Map<String, Object> getDeviceSummary(String area) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("area", area);
|
||||
|
||||
int online = 0, offline = 0, fault = 0;
|
||||
try {
|
||||
List<Map<String, Object>> stats = jdbc.queryForList(
|
||||
"SELECT status, COUNT(*) as count FROM prod_device_status WHERE area = ? GROUP BY status", area);
|
||||
for (Map<String, Object> row : stats) {
|
||||
int status = ((Number) row.get("status")).intValue();
|
||||
int count = ((Number) row.get("count")).intValue();
|
||||
switch (status) {
|
||||
case 1 -> online = count;
|
||||
case 0 -> offline = count;
|
||||
case 2 -> fault = count;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
int total = online + offline + fault;
|
||||
double onlineRate = total > 0 ? (online * 100.0 / total) : 0.0;
|
||||
|
||||
result.put("total", total);
|
||||
result.put("online", online);
|
||||
result.put("offline", offline);
|
||||
result.put("fault", fault);
|
||||
result.put("onlineRate", BigDecimal.valueOf(onlineRate).setScale(1, RoundingMode.HALF_UP));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 报警概览(今日报警数/活跃报警/各级别分布)
|
||||
*/
|
||||
public Map<String, Object> getAlertSummary(String area) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("area", area);
|
||||
|
||||
// 今日报警数
|
||||
try {
|
||||
Integer todayTotal = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM prod_alert_record WHERE created_time >= CURRENT_DATE", Integer.class);
|
||||
result.put("todayTotal", todayTotal != null ? todayTotal : 0);
|
||||
} catch (Exception e) {
|
||||
result.put("todayTotal", 0);
|
||||
}
|
||||
|
||||
// 活跃报警数
|
||||
try {
|
||||
Integer activeCount = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM prod_alert_record WHERE status < 4", Integer.class);
|
||||
result.put("activeCount", activeCount != null ? activeCount : 0);
|
||||
} catch (Exception e) {
|
||||
result.put("activeCount", 0);
|
||||
}
|
||||
|
||||
// 各级别分布
|
||||
try {
|
||||
List<Map<String, Object>> levelStats = jdbc.queryForList(
|
||||
"SELECT alert_level, COUNT(*) as count FROM prod_alert_record " +
|
||||
"WHERE created_time >= CURRENT_DATE GROUP BY alert_level");
|
||||
Map<String, Integer> levelMap = new LinkedHashMap<>();
|
||||
levelMap.put("general", 0);
|
||||
levelMap.put("important", 0);
|
||||
levelMap.put("urgent", 0);
|
||||
for (Map<String, Object> row : levelStats) {
|
||||
String level = (String) row.get("alert_level");
|
||||
int count = ((Number) row.get("count")).intValue();
|
||||
levelMap.put(level, count);
|
||||
}
|
||||
result.put("levelDistribution", levelMap);
|
||||
} catch (Exception e) {
|
||||
result.put("levelDistribution", Map.of("general", 0, "important", 0, "urgent", 0));
|
||||
}
|
||||
|
||||
// 最近7天趋势
|
||||
try {
|
||||
result.put("trend", jdbc.queryForList(
|
||||
"SELECT DATE(created_time) as date, COUNT(*) as count " +
|
||||
"FROM prod_alert_record WHERE created_time >= CURRENT_DATE - 7 " +
|
||||
"GROUP BY DATE(created_time) ORDER BY date"));
|
||||
} catch (Exception e) {
|
||||
result.put("trend", Collections.emptyList());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取总览大屏完整快照(聚合所有维度)
|
||||
*/
|
||||
public Map<String, Object> getFullDashboard(String area) {
|
||||
Map<String, Object> dashboard = new LinkedHashMap<>();
|
||||
dashboard.put("flow", getFlowSummary(area));
|
||||
dashboard.put("waterQuality", getWaterQualitySummary(area));
|
||||
dashboard.put("device", getDeviceSummary(area));
|
||||
dashboard.put("alert", getAlertSummary(area));
|
||||
dashboard.put("area", area);
|
||||
dashboard.put("timestamp", System.currentTimeMillis());
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询历史总览快照
|
||||
*/
|
||||
public List<DashboardSummary> getHistorySnapshots(String area, int days) {
|
||||
return dashboardSummaryMapper.findRecent(area, days);
|
||||
}
|
||||
|
||||
/**
|
||||
* 月度进出水量趋势
|
||||
*/
|
||||
public List<Map<String, Object>> getMonthlyFlowTrend(String area, int months) {
|
||||
String startDate = LocalDate.now().minusMonths(months).toString();
|
||||
return dashboardSummaryMapper.monthlyFlowTrend(area, startDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算日环比
|
||||
*/
|
||||
private void computeDayOverDay(Map<String, Object> result) {
|
||||
try {
|
||||
Object todayIn = result.get("todayInflow");
|
||||
Object yesterdayIn = result.get("yesterdayInflow");
|
||||
if (todayIn != null && yesterdayIn != null) {
|
||||
double ti = ((Number) todayIn).doubleValue();
|
||||
double yi = ((Number) yesterdayIn).doubleValue();
|
||||
if (yi > 0) {
|
||||
double rate = ((ti - yi) / yi) * 100;
|
||||
result.put("inflowDayOverDay", BigDecimal.valueOf(rate).setScale(1, RoundingMode.HALF_UP));
|
||||
} else {
|
||||
result.put("inflowDayOverDay", null);
|
||||
}
|
||||
}
|
||||
|
||||
Object todayOut = result.get("todayOutflow");
|
||||
Object yesterdayOut = result.get("yesterdayOutflow");
|
||||
if (todayOut != null && yesterdayOut != null) {
|
||||
double to = ((Number) todayOut).doubleValue();
|
||||
double yo = ((Number) yesterdayOut).doubleValue();
|
||||
if (yo > 0) {
|
||||
double rate = ((to - yo) / yo) * 100;
|
||||
result.put("outflowDayOverDay", BigDecimal.valueOf(rate).setScale(1, RoundingMode.HALF_UP));
|
||||
} else {
|
||||
result.put("outflowDayOverDay", null);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.water.production.entity.EnergyConsumption;
|
||||
import com.water.production.mapper.EnergyConsumptionMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 能耗数据服务
|
||||
* 提供电耗、药耗统计及单位产水能耗分析
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EnergyService {
|
||||
|
||||
private final EnergyConsumptionMapper energyMapper;
|
||||
|
||||
/**
|
||||
* 获取今日能耗汇总
|
||||
*/
|
||||
public Map<String, Object> getTodaySummary(String area) {
|
||||
String today = LocalDate.now().toString();
|
||||
List<Map<String, Object>> items = energyMapper.dailySummary(area, today);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("area", area);
|
||||
result.put("date", today);
|
||||
|
||||
BigDecimal totalPower = BigDecimal.ZERO;
|
||||
BigDecimal totalChemical = BigDecimal.ZERO;
|
||||
|
||||
for (Map<String, Object> item : items) {
|
||||
String type = (String) item.get("energy_type");
|
||||
BigDecimal total = item.get("total") != null ? new BigDecimal(item.get("total").toString()) : BigDecimal.ZERO;
|
||||
if ("power".equals(type)) {
|
||||
totalPower = total;
|
||||
result.put("powerKwh", total);
|
||||
result.put("powerUnit", item.get("unit"));
|
||||
} else {
|
||||
totalChemical = totalChemical.add(total);
|
||||
result.put("chemical_" + type + "_kg", total);
|
||||
}
|
||||
}
|
||||
|
||||
result.put("totalPowerKwh", totalPower);
|
||||
result.put("totalChemicalKg", totalChemical);
|
||||
result.put("items", items);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取能耗趋势(按日)
|
||||
*/
|
||||
public List<Map<String, Object>> getEnergyTrend(String area, int days) {
|
||||
LambdaQueryWrapper<EnergyConsumption> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EnergyConsumption::getArea, area)
|
||||
.ge(EnergyConsumption::getRecordDate, LocalDate.now().minusDays(days))
|
||||
.orderByAsc(EnergyConsumption::getRecordDate);
|
||||
List<EnergyConsumption> list = energyMapper.selectList(wrapper);
|
||||
|
||||
List<Map<String, Object>> trend = new ArrayList<>();
|
||||
for (EnergyConsumption ec : list) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("date", ec.getRecordDate());
|
||||
item.put("energyType", ec.getEnergyType());
|
||||
item.put("consumption", ec.getConsumption());
|
||||
item.put("unit", ec.getUnit());
|
||||
item.put("productionVolume", ec.getProductionVolume());
|
||||
item.put("unitConsumption", ec.getUnitConsumption());
|
||||
trend.add(item);
|
||||
}
|
||||
return trend;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取月度能耗趋势
|
||||
*/
|
||||
public List<Map<String, Object>> getMonthlyTrend(String area, int months) {
|
||||
String startDate = LocalDate.now().minusMonths(months).toString();
|
||||
return energyMapper.monthlyEnergyTrend(area, startDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单位产水能耗
|
||||
*/
|
||||
public Map<String, Object> getUnitEnergyConsumption(String area, String startDate, String endDate) {
|
||||
Map<String, Object> stats = energyMapper.unitEnergyStats(area, startDate, endDate);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("area", area);
|
||||
result.put("startDate", startDate);
|
||||
result.put("endDate", endDate);
|
||||
|
||||
if (stats != null) {
|
||||
BigDecimal totalPower = stats.get("total_power") != null
|
||||
? new BigDecimal(stats.get("total_power").toString()) : BigDecimal.ZERO;
|
||||
BigDecimal totalProduction = stats.get("total_production") != null
|
||||
? new BigDecimal(stats.get("total_production").toString()) : BigDecimal.ZERO;
|
||||
|
||||
result.put("totalPowerKwh", totalPower);
|
||||
result.put("totalProductionM3", totalProduction);
|
||||
|
||||
if (totalProduction.compareTo(BigDecimal.ZERO) > 0) {
|
||||
result.put("unitEnergyKwhPerM3",
|
||||
totalPower.divide(totalProduction, 4, RoundingMode.HALF_UP));
|
||||
} else {
|
||||
result.put("unitEnergyKwhPerM3", BigDecimal.ZERO);
|
||||
}
|
||||
} else {
|
||||
result.put("totalPowerKwh", BigDecimal.ZERO);
|
||||
result.put("totalProductionM3", BigDecimal.ZERO);
|
||||
result.put("unitEnergyKwhPerM3", BigDecimal.ZERO);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型查询能耗记录
|
||||
*/
|
||||
public List<EnergyConsumption> getRecordsByType(String area, String energyType, String startDate, String endDate) {
|
||||
LambdaQueryWrapper<EnergyConsumption> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(EnergyConsumption::getArea, area)
|
||||
.eq(EnergyConsumption::getEnergyType, energyType)
|
||||
.ge(EnergyConsumption::getRecordDate, startDate)
|
||||
.le(EnergyConsumption::getRecordDate, endDate)
|
||||
.orderByAsc(EnergyConsumption::getRecordDate);
|
||||
return energyMapper.selectList(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
-- ============================================================
|
||||
-- V2: 总览大屏 + 能耗数据表
|
||||
-- Issue #61: 总览大屏(进出水量/水质/设备/报警/能耗)
|
||||
-- ============================================================
|
||||
|
||||
-- 总览大屏每日快照表
|
||||
CREATE TABLE IF NOT EXISTS prod_dashboard_summary (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
summary_date DATE NOT NULL,
|
||||
area VARCHAR(100) NOT NULL DEFAULT '一体化水厂',
|
||||
|
||||
-- 进出水量
|
||||
today_inflow NUMERIC(14,2) DEFAULT 0,
|
||||
today_outflow NUMERIC(14,2) DEFAULT 0,
|
||||
yesterday_inflow NUMERIC(14,2) DEFAULT 0,
|
||||
yesterday_outflow NUMERIC(14,2) DEFAULT 0,
|
||||
month_inflow NUMERIC(16,2) DEFAULT 0,
|
||||
month_outflow NUMERIC(16,2) DEFAULT 0,
|
||||
|
||||
-- 水质概览
|
||||
raw_water_pass_rate NUMERIC(5,2) DEFAULT 100,
|
||||
factory_water_pass_rate NUMERIC(5,2) DEFAULT 100,
|
||||
terminal_water_pass_rate NUMERIC(5,2) DEFAULT 100,
|
||||
overall_pass_rate NUMERIC(5,2) DEFAULT 100,
|
||||
|
||||
-- 设备概况
|
||||
device_total INT DEFAULT 0,
|
||||
device_online INT DEFAULT 0,
|
||||
device_offline INT DEFAULT 0,
|
||||
device_fault INT DEFAULT 0,
|
||||
device_online_rate NUMERIC(5,2) DEFAULT 0,
|
||||
|
||||
-- 报警概览
|
||||
today_alert_total INT DEFAULT 0,
|
||||
active_alert_count INT DEFAULT 0,
|
||||
general_alert_count INT DEFAULT 0,
|
||||
important_alert_count INT DEFAULT 0,
|
||||
urgent_alert_count INT DEFAULT 0,
|
||||
|
||||
-- 能耗数据
|
||||
today_power_kwh NUMERIC(12,2) DEFAULT 0,
|
||||
today_chemical_kg NUMERIC(12,2) DEFAULT 0,
|
||||
unit_energy_consumption NUMERIC(8,4) DEFAULT 0,
|
||||
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 唯一约束:每个区域每天一条快照
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_dashboard_date_area
|
||||
ON prod_dashboard_summary (summary_date, area);
|
||||
|
||||
-- 索引:按区域查询
|
||||
CREATE INDEX IF NOT EXISTS idx_dashboard_area
|
||||
ON prod_dashboard_summary (area);
|
||||
|
||||
-- 能耗数据明细表
|
||||
CREATE TABLE IF NOT EXISTS prod_energy_consumption (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
record_date DATE NOT NULL,
|
||||
area VARCHAR(100) NOT NULL DEFAULT '一体化水厂',
|
||||
energy_type VARCHAR(30) NOT NULL, -- power/coagulant/disinfectant
|
||||
consumption NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||
unit VARCHAR(20) NOT NULL DEFAULT 'kWh',
|
||||
production_volume NUMERIC(14,2) DEFAULT 0,
|
||||
unit_consumption NUMERIC(8,4) DEFAULT 0,
|
||||
remark VARCHAR(500),
|
||||
created_time TIMESTAMP DEFAULT NOW(),
|
||||
updated_time TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 索引:按区域+日期
|
||||
CREATE INDEX IF NOT EXISTS idx_energy_area_date
|
||||
ON prod_energy_consumption (area, record_date);
|
||||
|
||||
-- 索引:按能耗类型
|
||||
CREATE INDEX IF NOT EXISTS idx_energy_type
|
||||
ON prod_energy_consumption (energy_type);
|
||||
|
||||
-- 唯一约束:每个区域每天每种能耗类型一条记录
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_energy_date_area_type
|
||||
ON prod_energy_consumption (record_date, area, energy_type);
|
||||
|
||||
COMMENT ON TABLE prod_dashboard_summary IS '总览大屏每日快照表';
|
||||
COMMENT ON TABLE prod_energy_consumption IS '能耗数据明细表';
|
||||
COMMENT ON COLUMN prod_energy_consumption.energy_type IS '能耗类型: power(电耗)/coagulant(混凝剂)/disinfectant(消毒剂)';
|
||||
@@ -0,0 +1,304 @@
|
||||
package com.water.production.service;
|
||||
|
||||
import com.water.production.entity.DashboardSummary;
|
||||
import com.water.production.entity.EnergyConsumption;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* DashboardService + EnergyService 单元测试
|
||||
* 测试实体构建、数据聚合逻辑、环比计算等核心逻辑
|
||||
*/
|
||||
class DashboardServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("测试 DashboardSummary 实体字段完整性")
|
||||
void testDashboardSummaryEntityFields() {
|
||||
DashboardSummary summary = new DashboardSummary();
|
||||
summary.setId(1L);
|
||||
summary.setSummaryDate(LocalDate.of(2026, 6, 14));
|
||||
summary.setArea("一体化水厂");
|
||||
|
||||
// 进出水量
|
||||
summary.setTodayInflow(new BigDecimal("15000.50"));
|
||||
summary.setTodayOutflow(new BigDecimal("14200.30"));
|
||||
summary.setYesterdayInflow(new BigDecimal("14500.00"));
|
||||
summary.setYesterdayOutflow(new BigDecimal("13800.00"));
|
||||
summary.setMonthInflow(new BigDecimal("420000.00"));
|
||||
summary.setMonthOutflow(new BigDecimal("398000.00"));
|
||||
|
||||
// 水质
|
||||
summary.setRawWaterPassRate(new BigDecimal("98.5"));
|
||||
summary.setFactoryWaterPassRate(new BigDecimal("99.2"));
|
||||
summary.setTerminalWaterPassRate(new BigDecimal("97.8"));
|
||||
summary.setOverallPassRate(new BigDecimal("98.5"));
|
||||
|
||||
// 设备
|
||||
summary.setDeviceTotal(120);
|
||||
summary.setDeviceOnline(105);
|
||||
summary.setDeviceOffline(10);
|
||||
summary.setDeviceFault(5);
|
||||
summary.setDeviceOnlineRate(new BigDecimal("87.5"));
|
||||
|
||||
// 报警
|
||||
summary.setTodayAlertTotal(8);
|
||||
summary.setActiveAlertCount(3);
|
||||
summary.setGeneralAlertCount(5);
|
||||
summary.setImportantAlertCount(2);
|
||||
summary.setUrgentAlertCount(1);
|
||||
|
||||
// 能耗
|
||||
summary.setTodayPowerKwh(new BigDecimal("1250.50"));
|
||||
summary.setTodayChemicalKg(new BigDecimal("57.50"));
|
||||
summary.setUnitEnergyConsumption(new BigDecimal("0.0834"));
|
||||
|
||||
// 验证所有字段
|
||||
assertEquals(1L, summary.getId());
|
||||
assertEquals(LocalDate.of(2026, 6, 14), summary.getSummaryDate());
|
||||
assertEquals("一体化水厂", summary.getArea());
|
||||
assertEquals(new BigDecimal("15000.50"), summary.getTodayInflow());
|
||||
assertEquals(new BigDecimal("14200.30"), summary.getTodayOutflow());
|
||||
assertEquals(120, summary.getDeviceTotal());
|
||||
assertEquals(105, summary.getDeviceOnline());
|
||||
assertEquals(8, summary.getTodayAlertTotal());
|
||||
assertEquals(new BigDecimal("1250.50"), summary.getTodayPowerKwh());
|
||||
assertEquals(new BigDecimal("0.0834"), summary.getUnitEnergyConsumption());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试 EnergyConsumption 实体字段及单位能耗计算")
|
||||
void testEnergyConsumptionEntity() {
|
||||
EnergyConsumption ec = new EnergyConsumption();
|
||||
ec.setId(1L);
|
||||
ec.setRecordDate(LocalDate.of(2026, 6, 14));
|
||||
ec.setArea("一体化水厂");
|
||||
ec.setEnergyType("power");
|
||||
ec.setConsumption(new BigDecimal("1250.50"));
|
||||
ec.setUnit("kWh");
|
||||
ec.setProductionVolume(new BigDecimal("15000.00"));
|
||||
|
||||
// 计算单位能耗
|
||||
BigDecimal unitConsumption = ec.getConsumption()
|
||||
.divide(ec.getProductionVolume(), 4, RoundingMode.HALF_UP);
|
||||
ec.setUnitConsumption(unitConsumption);
|
||||
|
||||
assertEquals("power", ec.getEnergyType());
|
||||
assertEquals(new BigDecimal("1250.50"), ec.getConsumption());
|
||||
assertEquals("kWh", ec.getUnit());
|
||||
assertEquals(new BigDecimal("15000.00"), ec.getProductionVolume());
|
||||
assertEquals(new BigDecimal("0.0834"), ec.getUnitConsumption());
|
||||
|
||||
// 测试药剂类型
|
||||
EnergyConsumption chemical = new EnergyConsumption();
|
||||
chemical.setEnergyType("coagulant");
|
||||
chemical.setConsumption(new BigDecimal("45.00"));
|
||||
chemical.setUnit("kg");
|
||||
chemical.setProductionVolume(new BigDecimal("15000.00"));
|
||||
BigDecimal chemUnit = chemical.getConsumption()
|
||||
.divide(chemical.getProductionVolume(), 4, RoundingMode.HALF_UP);
|
||||
chemical.setUnitConsumption(chemUnit);
|
||||
|
||||
assertEquals("coagulant", chemical.getEnergyType());
|
||||
assertEquals(new BigDecimal("0.0030"), chemical.getUnitConsumption());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试日环比计算逻辑")
|
||||
void testDayOverDayCalculation() {
|
||||
// 模拟进出水量日环比计算
|
||||
double todayInflow = 15000.0;
|
||||
double yesterdayInflow = 14000.0;
|
||||
double expectedRate = ((todayInflow - yesterdayInflow) / yesterdayInflow) * 100;
|
||||
BigDecimal rate = BigDecimal.valueOf(expectedRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("7.1"), rate);
|
||||
|
||||
// 测试下降情况
|
||||
todayInflow = 13000.0;
|
||||
yesterdayInflow = 14000.0;
|
||||
expectedRate = ((todayInflow - yesterdayInflow) / yesterdayInflow) * 100;
|
||||
rate = BigDecimal.valueOf(expectedRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("-7.1"), rate);
|
||||
|
||||
// 测试昨日为零的情况(除以零保护)
|
||||
yesterdayInflow = 0;
|
||||
if (yesterdayInflow > 0) {
|
||||
fail("Should not reach here");
|
||||
}
|
||||
// 应该返回null,不计算
|
||||
BigDecimal nullRate = null;
|
||||
assertNull(nullRate);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试水质合格率计算逻辑")
|
||||
void testWaterQualityPassRateCalculation() {
|
||||
// 模拟原水检测数据
|
||||
int totalTests = 200;
|
||||
int passedTests = 195;
|
||||
double passRate = totalTests > 0 ? (passedTests * 100.0 / totalTests) : 100.0;
|
||||
BigDecimal rate = BigDecimal.valueOf(passRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("97.5"), rate);
|
||||
|
||||
// 模拟全部合格
|
||||
totalTests = 150;
|
||||
passedTests = 150;
|
||||
passRate = totalTests > 0 ? (passedTests * 100.0 / totalTests) : 100.0;
|
||||
rate = BigDecimal.valueOf(passRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("100.0"), rate);
|
||||
|
||||
// 模拟无检测数据
|
||||
totalTests = 0;
|
||||
passedTests = 0;
|
||||
passRate = totalTests > 0 ? (passedTests * 100.0 / totalTests) : 100.0;
|
||||
rate = BigDecimal.valueOf(passRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("100.0"), rate);
|
||||
|
||||
// 模拟综合合格率
|
||||
int rawTotal = 200, rawPassed = 195;
|
||||
int factoryTotal = 180, factoryPassed = 178;
|
||||
int terminalTotal = 160, terminalPassed = 155;
|
||||
int allTotal = rawTotal + factoryTotal + terminalTotal;
|
||||
int allPassed = rawPassed + factoryPassed + terminalPassed;
|
||||
double overallRate = allTotal > 0 ? (allPassed * 100.0 / allTotal) : 100.0;
|
||||
BigDecimal overallBd = BigDecimal.valueOf(overallRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("97.8"), overallBd);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试设备在线率计算逻辑")
|
||||
void testDeviceOnlineRateCalculation() {
|
||||
int online = 105, offline = 10, fault = 5;
|
||||
int total = online + offline + fault;
|
||||
assertEquals(120, total);
|
||||
|
||||
double onlineRate = total > 0 ? (online * 100.0 / total) : 0.0;
|
||||
BigDecimal rate = BigDecimal.valueOf(onlineRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("87.5"), rate);
|
||||
|
||||
// 测试全部离线
|
||||
online = 0;
|
||||
offline = 50;
|
||||
fault = 10;
|
||||
total = online + offline + fault;
|
||||
onlineRate = total > 0 ? (online * 100.0 / total) : 0.0;
|
||||
rate = BigDecimal.valueOf(onlineRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("0.0"), rate);
|
||||
|
||||
// 测试无设备
|
||||
online = 0;
|
||||
offline = 0;
|
||||
fault = 0;
|
||||
total = 0;
|
||||
onlineRate = total > 0 ? (online * 100.0 / total) : 0.0;
|
||||
rate = BigDecimal.valueOf(onlineRate).setScale(1, RoundingMode.HALF_UP);
|
||||
assertEquals(new BigDecimal("0.0"), rate);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试报警级别分布统计逻辑")
|
||||
void testAlertLevelDistribution() {
|
||||
// 模拟数据库返回的级别统计
|
||||
List<Map<String, Object>> levelStats = new ArrayList<>();
|
||||
levelStats.add(Map.of("alert_level", "general", "count", 5L));
|
||||
levelStats.add(Map.of("alert_level", "important", "count", 2L));
|
||||
levelStats.add(Map.of("alert_level", "urgent", "count", 1L));
|
||||
|
||||
Map<String, Integer> levelMap = new LinkedHashMap<>();
|
||||
levelMap.put("general", 0);
|
||||
levelMap.put("important", 0);
|
||||
levelMap.put("urgent", 0);
|
||||
|
||||
for (Map<String, Object> row : levelStats) {
|
||||
String level = (String) row.get("alert_level");
|
||||
int count = ((Number) row.get("count")).intValue();
|
||||
levelMap.put(level, count);
|
||||
}
|
||||
|
||||
assertEquals(5, levelMap.get("general"));
|
||||
assertEquals(2, levelMap.get("important"));
|
||||
assertEquals(1, levelMap.get("urgent"));
|
||||
assertEquals(8, levelMap.values().stream().mapToInt(Integer::intValue).sum());
|
||||
|
||||
// 测试缺少某个级别时的默认值
|
||||
List<Map<String, Object>> partialStats = new ArrayList<>();
|
||||
partialStats.add(Map.of("alert_level", "urgent", "count", 3L));
|
||||
|
||||
Map<String, Integer> partialMap = new LinkedHashMap<>();
|
||||
partialMap.put("general", 0);
|
||||
partialMap.put("important", 0);
|
||||
partialMap.put("urgent", 0);
|
||||
|
||||
for (Map<String, Object> row : partialStats) {
|
||||
String level = (String) row.get("alert_level");
|
||||
int count = ((Number) row.get("count")).intValue();
|
||||
partialMap.put(level, count);
|
||||
}
|
||||
|
||||
assertEquals(0, partialMap.get("general"));
|
||||
assertEquals(0, partialMap.get("important"));
|
||||
assertEquals(3, partialMap.get("urgent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试月度进出水量趋势数据结构")
|
||||
void testMonthlyFlowTrendStructure() {
|
||||
// 模拟月度趋势数据
|
||||
List<Map<String, Object>> trend = new ArrayList<>();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
Map<String, Object> month = new LinkedHashMap<>();
|
||||
month.put("month", LocalDate.of(2026, i + 1, 1));
|
||||
month.put("total_inflow", new BigDecimal(String.valueOf(400000 + i * 10000)));
|
||||
month.put("total_outflow", new BigDecimal(String.valueOf(380000 + i * 9500)));
|
||||
trend.add(month);
|
||||
}
|
||||
|
||||
assertEquals(6, trend.size());
|
||||
assertEquals(LocalDate.of(2026, 1, 1), trend.get(0).get("month"));
|
||||
assertEquals(new BigDecimal("400000"), trend.get(0).get("total_inflow"));
|
||||
assertEquals(new BigDecimal("450000"), trend.get(5).get("total_inflow"));
|
||||
|
||||
// 验证趋势递增
|
||||
for (int i = 1; i < trend.size(); i++) {
|
||||
BigDecimal prev = (BigDecimal) trend.get(i - 1).get("total_inflow");
|
||||
BigDecimal curr = (BigDecimal) trend.get(i).get("total_inflow");
|
||||
assertTrue(curr.compareTo(prev) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试能耗汇总按类型聚合逻辑")
|
||||
void testEnergySummaryAggregation() {
|
||||
// 模拟能耗数据
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
items.add(Map.of("energy_type", "power", "total", new BigDecimal("1250.50"), "unit", "kWh"));
|
||||
items.add(Map.of("energy_type", "coagulant", "total", new BigDecimal("45.00"), "unit", "kg"));
|
||||
items.add(Map.of("energy_type", "disinfectant", "total", new BigDecimal("12.50"), "unit", "kg"));
|
||||
|
||||
BigDecimal totalPower = BigDecimal.ZERO;
|
||||
BigDecimal totalChemical = BigDecimal.ZERO;
|
||||
|
||||
for (Map<String, Object> item : items) {
|
||||
String type = (String) item.get("energy_type");
|
||||
BigDecimal total = (BigDecimal) item.get("total");
|
||||
if ("power".equals(type)) {
|
||||
totalPower = total;
|
||||
} else {
|
||||
totalChemical = totalChemical.add(total);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(new BigDecimal("1250.50"), totalPower);
|
||||
assertEquals(new BigDecimal("57.50"), totalChemical);
|
||||
|
||||
// 验证总能耗
|
||||
BigDecimal grandTotal = totalPower.add(totalChemical);
|
||||
assertEquals(new BigDecimal("1308.00"), grandTotal);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user