package cn.iocoder.yudao.module.bi.service.decision; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.bi.dal.dataobject.decision.BiKpiValueHistoryDO; import cn.iocoder.yudao.module.bi.dal.mysql.decision.BiKpiDefinitionMapper; import cn.iocoder.yudao.module.bi.dal.dataobject.decision.BiKpiDefinitionDO; import cn.iocoder.yudao.module.bi.dal.mysql.decision.BiKpiValueHistoryMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.*; /** * 决策支持 KPI 聚合 Service 实现 * * 通过 JdbcTemplate 跨模块执行聚合 SQL * * @author 超级管理员 */ @Slf4j @Service public class BiDecisionKpiServiceImpl implements BiDecisionKpiService { @Resource private BiKpiDefinitionMapper kpiMapper; @Resource private BiKpiValueHistoryMapper kpiValueHistoryMapper; @Resource private JdbcTemplate jdbcTemplate; @Override public Map getKpiOverview() { List kpis = kpiMapper.selectList(); List> items = new ArrayList<>(); for (BiKpiDefinitionDO kpi : kpis) { Map item = new LinkedHashMap<>(); item.put("code", kpi.getCode()); item.put("name", kpi.getName()); item.put("category", kpi.getCategory()); item.put("unit", kpi.getUnit()); item.put("chartType", kpi.getChartType()); item.put("status", kpi.getStatus()); // 执行查询获取当前值 BigDecimal value = executeKpiSql(kpi.getQuerySql()); item.put("value", value); // 判断预警状态 String alertStatus = evaluateAlertStatus(kpi, value); item.put("alertStatus", alertStatus); items.add(item); } Map result = new LinkedHashMap<>(); result.put("kpis", items); result.put("total", items.size()); return result; } @Override public Map getKpiDetail(String kpiCode) { BiKpiDefinitionDO kpi = kpiMapper.selectOne(BiKpiDefinitionDO::getCode, kpiCode); if (kpi == null) { return Collections.emptyMap(); } Map result = new LinkedHashMap<>(); result.put("code", kpi.getCode()); result.put("name", kpi.getName()); result.put("unit", kpi.getUnit()); result.put("category", kpi.getCategory()); result.put("value", executeKpiSql(kpi.getQuerySql())); return result; } @Override public Map getKpiByCategory(String category) { List kpis = kpiMapper.selectList( BiKpiDefinitionDO::getCategory, category); List> items = new ArrayList<>(); for (BiKpiDefinitionDO kpi : kpis) { Map item = new LinkedHashMap<>(); item.put("code", kpi.getCode()); item.put("name", kpi.getName()); item.put("unit", kpi.getUnit()); item.put("value", executeKpiSql(kpi.getQuerySql())); items.add(item); } Map result = new LinkedHashMap<>(); result.put("category", category); result.put("kpis", items); return result; } @Override public BigDecimal refreshKpi(String kpiCode) { BiKpiDefinitionDO kpi = kpiMapper.selectOne(BiKpiDefinitionDO::getCode, kpiCode); if (kpi == null) { return BigDecimal.ZERO; } return executeKpiSql(kpi.getQuerySql()); } @Override public Map getKpiTrend(String kpiCode, LocalDateTime beginTime, LocalDateTime endTime, String periodType) { BiKpiDefinitionDO kpi = kpiMapper.selectOne(BiKpiDefinitionDO::getCode, kpiCode); Map result = new LinkedHashMap<>(); result.put("kpiCode", kpiCode); result.put("kpiName", kpi != null ? kpi.getName() : kpiCode); result.put("category", kpi != null ? kpi.getCategory() : null); result.put("unit", kpi != null ? kpi.getUnit() : null); result.put("periodType", periodType != null ? periodType : "hour"); if (kpi == null) { result.put("points", Collections.emptyList()); return result; } LocalDateTime begin = beginTime != null ? beginTime : LocalDateTime.now().minusDays(30); LocalDateTime end = endTime != null ? endTime : LocalDateTime.now(); List history = kpiValueHistoryMapper .selectByKpiCodeAndTimeRange(kpiCode, begin, end); List> points = new ArrayList<>(); for (BiKpiValueHistoryDO h : history) { Map p = new LinkedHashMap<>(); p.put("time", h.getSnapshotTime()); p.put("value", h.getKpiValue()); points.add(p); } result.put("points", points); return result; } @Override public Map getKpiCompare(String kpiCode) { BiKpiDefinitionDO kpi = kpiMapper.selectOne(BiKpiDefinitionDO::getCode, kpiCode); Map result = new LinkedHashMap<>(); result.put("kpiCode", kpiCode); result.put("kpiName", kpi != null ? kpi.getName() : kpiCode); result.put("unit", kpi != null ? kpi.getUnit() : null); // 取最近 24 条快照(按时间倒序),最近一条为当前值 List recent = kpiValueHistoryMapper.selectRecentByKpiCode(kpiCode, 24); if (CollUtil.isEmpty(recent)) { result.put("currentValue", null); result.put("previousValue", null); result.put("changeRate", null); result.put("avgValue", null); result.put("maxValue", null); result.put("minValue", null); return result; } // recent 为倒序,第 0 条最新 BiKpiValueHistoryDO current = recent.get(0); result.put("currentTime", current.getSnapshotTime()); result.put("currentValue", current.getKpiValue()); if (recent.size() > 1) { BiKpiValueHistoryDO previous = recent.get(1); result.put("previousTime", previous.getSnapshotTime()); result.put("previousValue", previous.getKpiValue()); result.put("changeRate", calcChangeRate(previous.getKpiValue(), current.getKpiValue())); } else { result.put("previousValue", null); result.put("changeRate", null); } // 近 24 期统计汇总 BigDecimal sum = BigDecimal.ZERO; BigDecimal max = null; BigDecimal min = null; for (BiKpiValueHistoryDO h : recent) { if (h.getKpiValue() == null) { continue; } sum = sum.add(h.getKpiValue()); max = max == null ? h.getKpiValue() : h.getKpiValue().max(max); min = min == null ? h.getKpiValue() : h.getKpiValue().min(min); } result.put("periodCount", recent.size()); if (recent.size() > 0) { result.put("avgValue", sum.divide(BigDecimal.valueOf(recent.size()), 4, RoundingMode.HALF_UP)); } else { result.put("avgValue", BigDecimal.ZERO); } result.put("maxValue", max); result.put("minValue", min); return result; } private BigDecimal calcChangeRate(BigDecimal previous, BigDecimal current) { if (previous == null || current == null || previous.compareTo(BigDecimal.ZERO) == 0) { return null; } return current.subtract(previous).divide(previous, 4, RoundingMode.HALF_UP) .multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP); } /** * 执行 KPI 查询 SQL */ private BigDecimal executeKpiSql(String querySql) { if (querySql == null || querySql.isBlank()) { return BigDecimal.ZERO; } try { List> result = jdbcTemplate.queryForList(querySql); if (result.isEmpty()) { return BigDecimal.ZERO; } // 取第一行第一列的值 Map row = result.get(0); Object value = row.values().iterator().next(); if (value instanceof Number) { return BigDecimal.valueOf(((Number) value).doubleValue()); } return BigDecimal.ZERO; } catch (Exception e) { log.error("[executeKpiSql][SQL执行失败] {}", querySql, e); return BigDecimal.ZERO; } } /** * 评估预警状态 */ private String evaluateAlertStatus(BiKpiDefinitionDO kpi, BigDecimal value) { if (kpi.getThresholdCritical() != null) { int cmp = value.compareTo(kpi.getThresholdCritical()); boolean triggered = ">".equals(kpi.getComparisonOperator()) ? cmp > 0 : "<".equals(kpi.getComparisonOperator()) ? cmp < 0 : ">=".equals(kpi.getComparisonOperator()) ? cmp >= 0 : "<=".equals(kpi.getComparisonOperator()) ? cmp <= 0 : false; if (triggered) { return "critical"; } } if (kpi.getThresholdWarn() != null) { int cmp = value.compareTo(kpi.getThresholdWarn()); boolean triggered = ">".equals(kpi.getComparisonOperator()) ? cmp > 0 : "<".equals(kpi.getComparisonOperator()) ? cmp < 0 : ">=".equals(kpi.getComparisonOperator()) ? cmp >= 0 : "<=".equals(kpi.getComparisonOperator()) ? cmp <= 0 : false; if (triggered) { return "warn"; } } return "normal"; } }