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<String, Object> getKpiOverview() {
|
List<BiKpiDefinitionDO> kpis = kpiMapper.selectList();
|
List<Map<String, Object>> items = new ArrayList<>();
|
|
for (BiKpiDefinitionDO kpi : kpis) {
|
Map<String, Object> 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<String, Object> result = new LinkedHashMap<>();
|
result.put("kpis", items);
|
result.put("total", items.size());
|
return result;
|
}
|
|
@Override
|
public Map<String, Object> getKpiDetail(String kpiCode) {
|
BiKpiDefinitionDO kpi = kpiMapper.selectOne(BiKpiDefinitionDO::getCode, kpiCode);
|
if (kpi == null) {
|
return Collections.emptyMap();
|
}
|
|
Map<String, Object> 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<String, Object> getKpiByCategory(String category) {
|
List<BiKpiDefinitionDO> kpis = kpiMapper.selectList(
|
BiKpiDefinitionDO::getCategory, category);
|
List<Map<String, Object>> items = new ArrayList<>();
|
|
for (BiKpiDefinitionDO kpi : kpis) {
|
Map<String, Object> 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<String, Object> 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<String, Object> getKpiTrend(String kpiCode, LocalDateTime beginTime,
|
LocalDateTime endTime, String periodType) {
|
BiKpiDefinitionDO kpi = kpiMapper.selectOne(BiKpiDefinitionDO::getCode, kpiCode);
|
Map<String, Object> 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<BiKpiValueHistoryDO> history = kpiValueHistoryMapper
|
.selectByKpiCodeAndTimeRange(kpiCode, begin, end);
|
|
List<Map<String, Object>> points = new ArrayList<>();
|
for (BiKpiValueHistoryDO h : history) {
|
Map<String, Object> 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<String, Object> getKpiCompare(String kpiCode) {
|
BiKpiDefinitionDO kpi = kpiMapper.selectOne(BiKpiDefinitionDO::getCode, kpiCode);
|
Map<String, Object> 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<BiKpiValueHistoryDO> 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<Map<String, Object>> result = jdbcTemplate.queryForList(querySql);
|
if (result.isEmpty()) {
|
return BigDecimal.ZERO;
|
}
|
// 取第一行第一列的值
|
Map<String, Object> 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";
|
}
|
|
}
|