3 天以前 5e0640513226d9d9f2d766c075f79832c9d290ba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package cn.iocoder.yudao.module.bi.service.decision;
 
import cn.iocoder.yudao.module.bi.dal.dataobject.decision.BiKpiDefinitionDO;
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.mysql.decision.BiKpiValueHistoryMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
 
/**
 * KPI历史快照聚合任务
 *
 * 定时扫描启用的 KPI 定义,执行查询 SQL 将当前值写入 bi_kpi_value_history,
 * 供趋势研判、同比环比与统计预测使用。同一 KPI 同一整点仅保留最新值。
 *
 * @author 超级管理员
 */
@Slf4j
@Component
public class KpiSnapshotTask {
 
    @Resource
    private BiKpiDefinitionMapper kpiMapper;
 
    @Resource
    private BiKpiValueHistoryMapper kpiValueHistoryMapper;
 
    @Resource
    private JdbcTemplate jdbcTemplate;
 
    /**
     * 每小时整点触发,默认 cron = 0 0 * * * *,可通过 bi.kpi.snapshot.cron 覆盖
     */
    @Scheduled(cron = "${bi.kpi.snapshot.cron:0 0 * * * *}")
    public void snapshotAllKpis() {
        log.debug("[snapshotAllKpis][开始 KPI 快照聚合]");
        List<BiKpiDefinitionDO> kpis = kpiMapper.selectList(BiKpiDefinitionDO::getStatus, 1);
        if (kpis.isEmpty()) {
            log.debug("[snapshotAllKpis][无启用 KPI]");
            return;
        }
 
        // 当前整点,保证同一 KPI 同整点幂等
        LocalDateTime snapshotTime = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
        for (BiKpiDefinitionDO kpi : kpis) {
            try {
                snapshotKpi(kpi, snapshotTime);
            } catch (Exception e) {
                log.error("[snapshotAllKpis][KPI {} 快照失败]", kpi.getCode(), e);
            }
        }
    }
 
    private void snapshotKpi(BiKpiDefinitionDO kpi, LocalDateTime snapshotTime) {
        BigDecimal value = executeKpiSql(kpi.getQuerySql());
        if (value == null) {
            log.warn("[snapshotKpi][KPI {} 无值,跳过快照]", kpi.getCode());
            return;
        }
 
        BiKpiValueHistoryDO exist = kpiValueHistoryMapper
                .selectByKpiCodeAndSnapshotTime(kpi.getCode(), snapshotTime);
        if (exist != null) {
            exist.setKpiValue(value);
            kpiValueHistoryMapper.updateById(exist);
        } else {
            BiKpiValueHistoryDO history = BiKpiValueHistoryDO.builder()
                    .kpiId(kpi.getId())
                    .kpiCode(kpi.getCode())
                    .kpiName(kpi.getName())
                    .category(kpi.getCategory())
                    .kpiValue(value)
                    .snapshotTime(snapshotTime)
                    .periodType("hour")
                    .dimensionValue("all")
                    .build();
            kpiValueHistoryMapper.insert(history);
        }
    }
 
    private BigDecimal executeKpiSql(String querySql) {
        if (querySql == null || querySql.isBlank()) {
            return null;
        }
        try {
            List<Map<String, Object>> result = jdbcTemplate.queryForList(querySql);
            if (result.isEmpty()) {
                return null;
            }
            Object value = result.get(0).values().iterator().next();
            if (value instanceof Number) {
                return BigDecimal.valueOf(((Number) value).doubleValue());
            }
            return null;
        } catch (Exception e) {
            log.error("[executeKpiSql][SQL执行失败] {}", querySql, e);
            return null;
        }
    }
 
}