From 5e0640513226d9d9f2d766c075f79832c9d290ba Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 09 九月 2026 13:45:44 +0800
Subject: [PATCH] feat(bi): 新增BI决策支持KPI历史快照与预测功能
---
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/KpiSnapshotTask.java | 110 ++++++++
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastEngine.java | 189 ++++++++++++++
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/mysql/decision/BiKpiValueHistoryMapper.java | 63 ++++
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastService.java | 25 +
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionForecastController.java | 25 +
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/dataobject/decision/BiKpiValueHistoryDO.java | 77 +++++
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiService.java | 12
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionKpiController.java | 24 +
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastServiceImpl.java | 106 +++++++-
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiServiceImpl.java | 107 ++++++++
yudao-server/src/main/resources/application-test.yaml | 9
yudao-server/src/main/resources/application-local.yaml | 9
12 files changed, 739 insertions(+), 17 deletions(-)
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionForecastController.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionForecastController.java
index 2f003bb..580ba22 100644
--- a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionForecastController.java
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionForecastController.java
@@ -48,13 +48,32 @@
}
@PostMapping("/generate")
- @Operation(summary = "鐢熸垚棰勬祴锛堝崰浣嶏級")
+ @Operation(summary = "鐢熸垚棰勬祴锛堟敮鎸佹寚瀹氭ā鍨嬶級")
@PreAuthorize("@ss.hasPermission('bi:decision:kpi:query')")
- public CommonResult<Boolean> generateForecast(@RequestParam("forecastCode") String forecastCode) {
- forecastService.generateForecast(forecastCode, LocalDateTime.now());
+ public CommonResult<Boolean> generateForecast(@RequestParam("forecastCode") String forecastCode,
+ @RequestParam(value = "model", required = false) String model,
+ @RequestParam(value = "window", required = false, defaultValue = "12") Integer window,
+ @RequestParam(value = "period", required = false, defaultValue = "7") Integer period) {
+ if (model == null || model.isBlank()) {
+ forecastService.generateForecast(forecastCode, LocalDateTime.now());
+ } else {
+ forecastService.generateForecast(forecastCode, model, window, period, LocalDateTime.now());
+ }
return success(true);
}
+ @PostMapping("/generate-from-kpi")
+ @Operation(summary = "鍩轰簬KPI鍘嗗彶蹇収鐢熸垚棰勬祴锛堣礋鑽�/浜ч噺/閲囪喘/鍥炴锛�")
+ @PreAuthorize("@ss.hasPermission('bi:decision:kpi:query')")
+ public CommonResult<Integer> generateKpiForecast(@RequestParam("kpiCode") String kpiCode,
+ @RequestParam(value = "model", required = false) String model,
+ @RequestParam(value = "window", required = false, defaultValue = "12") Integer window,
+ @RequestParam(value = "period", required = false, defaultValue = "7") Integer period,
+ @RequestParam(value = "periods", required = false, defaultValue = "30") Integer periods,
+ @RequestParam(value = "intervalMinutes", required = false, defaultValue = "360") Integer intervalMinutes) {
+ return success(forecastService.generateKpiForecast(kpiCode, model, window, period, periods, intervalMinutes));
+ }
+
@PostMapping("/generate-work-order")
@Operation(summary = "璐熻嵎棰勬祴鑱斿姩鍒涘缓鐢熶骇宸ュ崟")
@PreAuthorize("@ss.hasPermission('bi:decision-forecast:generate-work-order')")
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionKpiController.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionKpiController.java
index de14c43..d4ca1ce 100644
--- a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionKpiController.java
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/controller/admin/decision/DecisionKpiController.java
@@ -6,14 +6,17 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
+import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
+import java.time.LocalDateTime;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Tag(name = "绠$悊鍚庡彴 - 鍐崇瓥鏀寔 KPI")
@RestController
@@ -52,4 +55,25 @@
return success(decisionKpiService.refreshKpi(kpiCode));
}
+ @GetMapping("/trend/{kpiCode}")
+ @Operation(summary = "鑾峰彇KPI鍘嗗彶蹇収瓒嬪娍")
+ @PreAuthorize("@ss.hasPermission('bi:decision:kpi:query')")
+ public CommonResult<Map<String, Object>> getKpiTrend(@PathVariable("kpiCode") String kpiCode,
+ @RequestParam(value = "beginTime", required = false)
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ LocalDateTime beginTime,
+ @RequestParam(value = "endTime", required = false)
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ LocalDateTime endTime,
+ @RequestParam(value = "periodType", required = false) String periodType) {
+ return success(decisionKpiService.getKpiTrend(kpiCode, beginTime, endTime, periodType));
+ }
+
+ @GetMapping("/compare/{kpiCode}")
+ @Operation(summary = "鑾峰彇KPI鐜瘮鐮斿垽")
+ @PreAuthorize("@ss.hasPermission('bi:decision:kpi:query')")
+ public CommonResult<Map<String, Object>> getKpiCompare(@PathVariable("kpiCode") String kpiCode) {
+ return success(decisionKpiService.getKpiCompare(kpiCode));
+ }
+
}
\ No newline at end of file
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/dataobject/decision/BiKpiValueHistoryDO.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/dataobject/decision/BiKpiValueHistoryDO.java
new file mode 100644
index 0000000..b69aa87
--- /dev/null
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/dataobject/decision/BiKpiValueHistoryDO.java
@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.bi.dal.dataobject.decision;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * KPI鎸囨爣鍘嗗彶蹇収 DO
+ *
+ * 鐢卞畾鏃朵换鍔� KpiSnapshotTask 鎸夊懆鏈熻仛鍚堬紝鐢ㄤ簬瓒嬪娍鐮斿垽銆佸悓姣旂幆姣斾笌缁熻棰勬祴
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@TableName("bi_kpi_value_history")
+@KeySequence("bi_kpi_value_history_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BiKpiValueHistoryDO extends BaseDO {
+
+ @TableId
+ private Long id;
+
+ /**
+ * 鍏宠仈KPI鎸囨爣ID
+ */
+ private Long kpiId;
+
+ /**
+ * KPI缂栫爜
+ */
+ private String kpiCode;
+
+ /**
+ * KPI鍚嶇О
+ */
+ private String kpiName;
+
+ /**
+ * 鍒嗙被
+ */
+ private String category;
+
+ /**
+ * 蹇収鍊�
+ */
+ private BigDecimal kpiValue;
+
+ /**
+ * 蹇収鏃堕棿鐐癸紙鏁寸偣锛�
+ */
+ private LocalDateTime snapshotTime;
+
+ /**
+ * 缁熻鍛ㄦ湡锛歨our/day/week/month
+ */
+ private String periodType;
+
+ /**
+ * 涓氬姟缁村害鍊硷紝榛樿 all
+ */
+ private String dimensionValue;
+
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/mysql/decision/BiKpiValueHistoryMapper.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/mysql/decision/BiKpiValueHistoryMapper.java
new file mode 100644
index 0000000..ede8513
--- /dev/null
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/mysql/decision/BiKpiValueHistoryMapper.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.bi.dal.mysql.decision;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.bi.dal.dataobject.decision.BiKpiValueHistoryDO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * KPI鍘嗗彶蹇収 Mapper
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface BiKpiValueHistoryMapper extends BaseMapperX<BiKpiValueHistoryDO> {
+
+ /**
+ * 鏌ヨ鎸囧畾 KPI 鍦ㄦ椂闂磋寖鍥村唴鐨勫揩鐓э紙鎸夋椂闂村崌搴忥紝鐢ㄤ簬瓒嬪娍/棰勬祴锛�
+ */
+ default List<BiKpiValueHistoryDO> selectByKpiCodeAndTimeRange(String kpiCode,
+ LocalDateTime beginTime,
+ LocalDateTime endTime) {
+ return selectList(new LambdaQueryWrapper<BiKpiValueHistoryDO>()
+ .eq(BiKpiValueHistoryDO::getKpiCode, kpiCode)
+ .between(BiKpiValueHistoryDO::getSnapshotTime, beginTime, endTime)
+ .orderByAsc(BiKpiValueHistoryDO::getSnapshotTime));
+ }
+
+ /**
+ * 鏌ヨ鎸囧畾 KPI 鏈�杩� n 鏉″揩鐓э紙鎸夋椂闂村�掑簭锛岀敤浜庣Щ鍔ㄥ钩鍧�/瓒嬪娍鐮斿垽锛�
+ */
+ default List<BiKpiValueHistoryDO> selectRecentByKpiCode(String kpiCode, int limit) {
+ return selectList(new LambdaQueryWrapper<BiKpiValueHistoryDO>()
+ .eq(BiKpiValueHistoryDO::getKpiCode, kpiCode)
+ .orderByDesc(BiKpiValueHistoryDO::getSnapshotTime)
+ .last("LIMIT " + limit));
+ }
+
+ /**
+ * 鏌ヨ鎸囧畾 KPI 鍦ㄦ煇鏁寸偣鐨勫揩鐓э紙鐢ㄤ簬鍘婚噸/瑕嗙洊锛�
+ */
+ default BiKpiValueHistoryDO selectByKpiCodeAndSnapshotTime(String kpiCode, LocalDateTime snapshotTime) {
+ return selectOne(new LambdaQueryWrapper<BiKpiValueHistoryDO>()
+ .eq(BiKpiValueHistoryDO::getKpiCode, kpiCode)
+ .eq(BiKpiValueHistoryDO::getSnapshotTime, snapshotTime)
+ .last("LIMIT 1"));
+ }
+
+ /**
+ * 鏌ヨ鎸囧畾鍒嗙被鍦ㄦ椂闂磋寖鍥村唴鐨勫揩鐓�
+ */
+ default List<BiKpiValueHistoryDO> selectByCategoryAndTimeRange(String category,
+ LocalDateTime beginTime,
+ LocalDateTime endTime) {
+ return selectList(new LambdaQueryWrapper<BiKpiValueHistoryDO>()
+ .eq(BiKpiValueHistoryDO::getCategory, category)
+ .between(BiKpiValueHistoryDO::getSnapshotTime, beginTime, endTime)
+ .orderByAsc(BiKpiValueHistoryDO::getSnapshotTime));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiService.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiService.java
index 8511efe..7ffe4aa 100644
--- a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiService.java
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiService.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.bi.service.decision;
import java.math.BigDecimal;
+import java.time.LocalDateTime;
import java.util.Map;
/**
@@ -32,4 +33,15 @@
*/
BigDecimal refreshKpi(String kpiCode);
+ /**
+ * 鑾峰彇 KPI 鍘嗗彶蹇収瓒嬪娍锛堝熀浜� bi_kpi_value_history锛�
+ */
+ Map<String, Object> getKpiTrend(String kpiCode, LocalDateTime beginTime,
+ LocalDateTime endTime, String periodType);
+
+ /**
+ * KPI 鐜瘮鐮斿垽锛氬綋鍓嶅揩鐓у�艰緝涓婁竴鍛ㄦ湡蹇収鐨勫彉鍖栫巼鍙婅繎鏈熺粺璁℃眹鎬�
+ */
+ Map<String, Object> getKpiCompare(String kpiCode);
+
}
\ No newline at end of file
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiServiceImpl.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiServiceImpl.java
index 5eea031..6e115e4 100644
--- a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiServiceImpl.java
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiDecisionKpiServiceImpl.java
@@ -1,13 +1,18 @@
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.*;
/**
@@ -23,6 +28,9 @@
@Resource
private BiKpiDefinitionMapper kpiMapper;
+
+ @Resource
+ private BiKpiValueHistoryMapper kpiValueHistoryMapper;
@Resource
private JdbcTemplate jdbcTemplate;
@@ -105,6 +113,105 @@
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
*/
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastEngine.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastEngine.java
new file mode 100644
index 0000000..03ad5a4
--- /dev/null
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastEngine.java
@@ -0,0 +1,189 @@
+package cn.iocoder.yudao.module.bi.service.decision;
+
+import cn.hutool.core.collection.CollUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Java 缁熻棰勬祴寮曟搸
+ *
+ * 鎻愪緵 SMA/WMA/绾挎�у洖褰�/瀛h妭鎸囨暟/鍚屾瘮鐜瘮澶栨帹 绛夌粺璁¢娴嬬畻娉曪紝
+ * 杈撳叆鍘嗗彶搴忓垪锛岃繑鍥為娴嬪�煎強 95% 缃俊鍖洪棿銆�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public final class BiForecastEngine {
+
+ private BiForecastEngine() {
+ }
+
+ /**
+ * 棰勬祴缁撴灉锛氬�� + 缃俊鍖洪棿
+ */
+ public record Forecast(BigDecimal value, BigDecimal lowerBound, BigDecimal upperBound) {
+ }
+
+ /**
+ * 缁熶竴棰勬祴鍏ュ彛
+ *
+ * @param model SMA / WMA / LR / SEASONAL / YOY
+ * @param history 鍘嗗彶搴忓垪锛堟寜鏃堕棿鍗囧簭锛�
+ * @param window 绉诲姩骞冲潎绐楀彛锛圫MA/WMA 浣跨敤锛�
+ * @param period 瀛h妭鍛ㄦ湡闀垮害锛圫EASONAL 浣跨敤锛�
+ * @return 涓嬩竴鏈熼娴�
+ */
+ public static Forecast forecast(String model, List<BigDecimal> history,
+ int window, int period) {
+ List<BigDecimal> values = CollUtil.isNotEmpty(history)
+ ? history.stream().filter(v -> v != null).toList()
+ : List.of();
+ if (values.isEmpty()) {
+ return null;
+ }
+ String m = model == null ? "SMA" : model.toUpperCase();
+ BigDecimal value = switch (m) {
+ case "WMA" -> wma(values, window > 0 ? window : 12);
+ case "LR" -> linearRegressionForecast(values, 1);
+ case "SEASONAL" -> seasonalForecast(values, period > 0 ? period : 7);
+ case "YOY" -> yoyForecast(values);
+ default -> sma(values, window > 0 ? window : 12);
+ };
+ if (value == null) {
+ return null;
+ }
+ // 95% 缃俊鍖洪棿锛毬�10% 骞呭害
+ BigDecimal bound = value.abs().multiply(new BigDecimal("0.10"));
+ return new Forecast(value, value.subtract(bound), value.add(bound));
+ }
+
+ /**
+ * 绠�鍗曠Щ鍔ㄥ钩鍧�
+ */
+ public static BigDecimal sma(List<BigDecimal> values, int window) {
+ if (CollUtil.isEmpty(values)) {
+ return null;
+ }
+ int n = Math.min(window, values.size());
+ int from = values.size() - n;
+ BigDecimal sum = BigDecimal.ZERO;
+ for (int i = from; i < values.size(); i++) {
+ sum = sum.add(values.get(i));
+ }
+ return sum.divide(BigDecimal.valueOf(n), 4, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * 鍔犳潈绉诲姩骞冲潎锛氳繎鏈熸潈閲嶉�掑
+ */
+ public static BigDecimal wma(List<BigDecimal> values, int window) {
+ if (CollUtil.isEmpty(values)) {
+ return null;
+ }
+ int n = Math.min(window, values.size());
+ int from = values.size() - n;
+ BigDecimal weightSum = BigDecimal.ZERO;
+ BigDecimal sum = BigDecimal.ZERO;
+ for (int i = from; i < values.size(); i++) {
+ int weight = i - from + 1;
+ sum = sum.add(values.get(i).multiply(BigDecimal.valueOf(weight)));
+ weightSum = weightSum.add(BigDecimal.valueOf(weight));
+ }
+ return sum.divide(weightSum, 4, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * 涓�鍏冪嚎鎬у洖褰掞紙鏈�灏忎簩涔橈級锛岄娴嬫湭鏉� extend 鏈�
+ */
+ public static BigDecimal linearRegressionForecast(List<BigDecimal> values, int extend) {
+ int n = values.size();
+ if (n < 2) {
+ return values.get(0);
+ }
+ BigDecimal sumX = BigDecimal.ZERO;
+ BigDecimal sumY = BigDecimal.ZERO;
+ BigDecimal sumXY = BigDecimal.ZERO;
+ BigDecimal sumXX = BigDecimal.ZERO;
+ for (int i = 0; i < n; i++) {
+ BigDecimal x = BigDecimal.valueOf(i);
+ BigDecimal y = values.get(i);
+ sumX = sumX.add(x);
+ sumY = sumY.add(y);
+ sumXY = sumXY.add(x.multiply(y));
+ sumXX = sumXX.add(x.multiply(x));
+ }
+ BigDecimal nbd = BigDecimal.valueOf(n);
+ BigDecimal slope = nbd.multiply(sumXY).subtract(sumX.multiply(sumY))
+ .divide(nbd.multiply(sumXX).subtract(sumX.multiply(sumX)), 6, RoundingMode.HALF_UP);
+ BigDecimal intercept = sumY.subtract(slope.multiply(sumX)).divide(nbd, 6, RoundingMode.HALF_UP);
+ BigDecimal xNext = BigDecimal.valueOf(n - 1 + extend);
+ return slope.multiply(xNext).add(intercept).setScale(4, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * 瀛h妭鎸囨暟棰勬祴锛氭寜鍛ㄦ湡褰掍竴鍖栵紝鐢ㄥ懆鏈熷唴鍚岀浉浣嶅潎鍊煎鎺�
+ */
+ public static BigDecimal seasonalForecast(List<BigDecimal> values, int period) {
+ int n = values.size();
+ if (n < period) {
+ // 鏁版嵁涓嶈冻涓�涓懆鏈燂紝閫�鍖栦负绾挎�у洖褰�
+ return linearRegressionForecast(values, 1);
+ }
+ // 璁$畻鏁翠綋鍧囧�硷紙鍘昏秼鍔垮弬鑰冿級
+ BigDecimal avg = sma(values, n);
+ if (avg == null || avg.compareTo(BigDecimal.ZERO) == 0) {
+ avg = BigDecimal.ONE;
+ }
+ // 鍛ㄦ湡鍧囧�硷紙姣忔湡鍩哄噯锛�
+ BigDecimal periodAvg = BigDecimal.ZERO;
+ int baseCount = n / period;
+ for (int i = 0; i < n; i++) {
+ periodAvg = periodAvg.add(values.get(i));
+ }
+ periodAvg = periodAvg.divide(BigDecimal.valueOf(n), 6, RoundingMode.HALF_UP);
+
+ // 鍚勭浉浣嶅潎鍊�
+ List<BigDecimal> phaseAvg = new ArrayList<>(period);
+ List<Integer> phaseCount = new ArrayList<>(period);
+ for (int i = 0; i < period; i++) {
+ phaseAvg.add(BigDecimal.ZERO);
+ phaseCount.add(0);
+ }
+ for (int i = 0; i < n; i++) {
+ int phase = i % period;
+ phaseAvg.set(phase, phaseAvg.get(phase).add(values.get(i)));
+ phaseCount.set(phase, phaseCount.get(phase) + 1);
+ }
+ BigDecimal xNext = BigDecimal.valueOf(n - 1 + 1); // 涓嬩竴鏈�
+ int nextPhase = n % period;
+ BigDecimal phaseBase = phaseCount.get(nextPhase) > 0
+ ? phaseAvg.get(nextPhase).divide(BigDecimal.valueOf(phaseCount.get(nextPhase)), 6, RoundingMode.HALF_UP)
+ : periodAvg;
+ // 绾挎�ц秼鍔胯皟鏁�
+ BigDecimal trend = linearRegressionForecast(values, 1);
+ BigDecimal trendAdjust = trend.multiply(avg).divide(periodAvg, 6, RoundingMode.HALF_UP);
+ return phaseBase.add(trendAdjust.subtract(periodAvg)).setScale(4, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * 鍚屾瘮/鐜瘮澶栨帹锛氭渶鍚庝竴涓懆鏈熺浉瀵瑰墠涓�涓懆鏈熺殑鍙樺寲鐜囧鎺�
+ */
+ public static BigDecimal yoyForecast(List<BigDecimal> values) {
+ int n = values.size();
+ if (n < 3) {
+ return values.get(n - 1);
+ }
+ BigDecimal last = values.get(n - 1);
+ BigDecimal prev = values.get(n - 2);
+ BigDecimal before = values.get(n - 3);
+ if (prev == null || prev.compareTo(BigDecimal.ZERO) == 0) {
+ return last;
+ }
+ // 鍓嶄竴鍛ㄦ湡鍙樺寲鐜�
+ BigDecimal growth = prev.subtract(before).divide(prev, 6, RoundingMode.HALF_UP);
+ return last.multiply(BigDecimal.ONE.add(growth)).setScale(4, RoundingMode.HALF_UP);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastService.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastService.java
index 3026616..e565613 100644
--- a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastService.java
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastService.java
@@ -16,8 +16,33 @@
List<BiForecastDataDO> getForecastList(String forecastCode, LocalDateTime beginTime, LocalDateTime endTime);
+ /**
+ * 鍩轰簬鍘嗗彶棰勬祴鏁版嵁鐢熸垚棰勬祴锛堥粯璁� SMA锛�
+ */
void generateForecast(String forecastCode, LocalDateTime pointTime);
+ /**
+ * 鍩轰簬鍘嗗彶棰勬祴鏁版嵁鎸夋寚瀹氭ā鍨嬬敓鎴愰娴�
+ *
+ * @param model SMA/WMA/LR/SEASONAL/YOY
+ */
+ void generateForecast(String forecastCode, String model, int window, int period,
+ LocalDateTime pointTime);
+
+ /**
+ * 鍩轰簬 KPI 鍘嗗彶蹇収鐢熸垚棰勬祴骞惰惤搴擄紙璐熻嵎/浜ч噺/閲囪喘/鍥炴绛夊喅绛栧満鏅級
+ *
+ * @param kpiCode KPI 缂栫爜
+ * @param model SMA/WMA/LR/SEASONAL/YOY
+ * @param window SMA/WMA 绐楀彛
+ * @param period SEASONAL 鍛ㄦ湡
+ * @param periods 棰勬祴鏈熸暟锛堢敓鎴愭湭鏉� N 鏈熼娴嬬偣锛�
+ * @param intervalMinutes 棰勬祴鐐归棿闅旓紙鍒嗛挓锛�
+ * @return 鐢熸垚鐨勯娴嬭褰曟暟
+ */
+ int generateKpiForecast(String kpiCode, String model, int window, int period,
+ int periods, int intervalMinutes);
+
Long createForecastHistory(DecisionForecastHistorySaveReqVO reqVO);
/**
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastServiceImpl.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastServiceImpl.java
index 478da23..84ceec9 100644
--- a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastServiceImpl.java
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/BiForecastServiceImpl.java
@@ -5,7 +5,11 @@
import cn.iocoder.yudao.module.bi.controller.admin.decision.vo.DecisionForecastHistorySaveReqVO;
import cn.iocoder.yudao.module.bi.controller.admin.decision.vo.DecisionForecastWorkOrderCreateReqVO;
import cn.iocoder.yudao.module.bi.dal.dataobject.decision.BiForecastDataDO;
+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.BiForecastDataMapper;
+import cn.iocoder.yudao.module.bi.dal.mysql.decision.BiKpiDefinitionMapper;
+import cn.iocoder.yudao.module.bi.dal.mysql.decision.BiKpiValueHistoryMapper;
import cn.iocoder.yudao.module.mes.api.workorder.MesProWorkOrderApi;
import cn.iocoder.yudao.module.mes.api.workorder.dto.MesProWorkOrderCreateReqDTO;
import jakarta.annotation.Resource;
@@ -16,6 +20,7 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
+import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -40,6 +45,10 @@
@Resource
private BiForecastDataMapper forecastDataMapper;
+ @Resource
+ private BiKpiDefinitionMapper kpiMapper;
+ @Resource
+ private BiKpiValueHistoryMapper kpiValueHistoryMapper;
@Resource
private MesProWorkOrderApi mesProWorkOrderApi;
@@ -72,29 +81,98 @@
@Override
public void generateForecast(String forecastCode, LocalDateTime pointTime) {
- // 鍩轰簬鍚岄娴嬬紪鐮佺殑鍘嗗彶棰勬祴鏁版嵁鍋氱畝鍗曠Щ鍔ㄥ钩鍧囷紱鏃犲巻鍙叉暟鎹椂涓嶄吉閫犻浂鍊�
+ // 榛樿浣跨敤 SMA 妯″瀷
+ generateForecast(forecastCode, "SMA", SMA_WINDOW, 7, pointTime);
+ }
+
+ @Override
+ public void generateForecast(String forecastCode, String model, int window, int period,
+ LocalDateTime pointTime) {
+ // 鍩轰簬鍚岄娴嬬紪鐮佺殑鍘嗗彶棰勬祴鏁版嵁鍋氱粺璁¢娴嬶紱鏃犲巻鍙叉暟鎹椂涓嶄吉閫犻浂鍊�
List<BiForecastDataDO> history = forecastDataMapper.selectRecentByCode(forecastCode, SMA_WINDOW);
if (CollUtil.isEmpty(history)) {
throw exception(BI_FORECAST_NO_HISTORY);
}
- BigDecimal sum = history.stream()
- .map(BiForecastDataDO::getForecastValue)
- .filter(value -> value != null)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal avg = sum.divide(BigDecimal.valueOf(history.size()), 4, RoundingMode.HALF_UP);
- BigDecimal bound = avg.multiply(new BigDecimal("0.10"));
-
- BiForecastDataDO forecast = BiForecastDataDO.builder()
+ List<BigDecimal> values = new ArrayList<>();
+ for (BiForecastDataDO h : history) {
+ if (h.getForecastValue() != null) {
+ values.add(h.getForecastValue());
+ }
+ }
+ BiForecastEngine.Forecast forecast = BiForecastEngine.forecast(model, values, window, period);
+ if (forecast == null) {
+ throw exception(BI_FORECAST_NO_HISTORY);
+ }
+ BiForecastDataDO data = BiForecastDataDO.builder()
.forecastCode(forecastCode)
.forecastName(history.get(0).getForecastName())
.pointTime(pointTime)
- .forecastValue(avg)
- .lowerBound(avg.subtract(bound))
- .upperBound(avg.add(bound))
+ .forecastValue(forecast.value())
+ .lowerBound(forecast.lowerBound())
+ .upperBound(forecast.upperBound())
.confidenceLevel(new BigDecimal("95.00"))
- .modelVersion("v1.0-sma")
+ .modelVersion("v2.0-" + model.toLowerCase())
.build();
- forecastDataMapper.insert(forecast);
+ forecastDataMapper.insert(data);
+ }
+
+ @Override
+ public int generateKpiForecast(String kpiCode, String model, int window, int period,
+ int periods, int intervalMinutes) {
+ // 1. 鏍¢獙 KPI 瀹氫箟
+ BiKpiDefinitionDO kpi = kpiMapper.selectOne(BiKpiDefinitionDO::getCode, kpiCode);
+ if (kpi == null) {
+ throw exception(BI_FORECAST_NOT_EXISTS);
+ }
+ // 2. 鍙� KPI 鍘嗗彶蹇収锛堝崌搴忥級锛屾棤鏁版嵁涓嶉娴�
+ List<BiKpiValueHistoryDO> historyList = kpiValueHistoryMapper.selectByKpiCodeAndTimeRange(
+ kpiCode, LocalDateTime.now().minusMonths(6), LocalDateTime.now());
+ if (CollUtil.isEmpty(historyList)) {
+ throw exception(BI_FORECAST_NO_HISTORY);
+ }
+ List<BigDecimal> history = new ArrayList<>();
+ LocalDateTime lastSnapshotTime = null;
+ for (BiKpiValueHistoryDO h : historyList) {
+ if (h.getKpiValue() != null) {
+ history.add(h.getKpiValue());
+ lastSnapshotTime = h.getSnapshotTime();
+ }
+ }
+ if (history.isEmpty()) {
+ throw exception(BI_FORECAST_NO_HISTORY);
+ }
+ // 3. 閫愭湡婊氬姩棰勬祴
+ int targetPeriods = periods > 0 ? Math.min(periods, 90) : 30;
+ int stepMinutes = intervalMinutes > 0 ? intervalMinutes : 360;
+ int count = 0;
+ LocalDateTime nextTime = lastSnapshotTime;
+ for (int i = 0; i < targetPeriods; i++) {
+ BiForecastEngine.Forecast f = BiForecastEngine.forecast(model, history, window, period);
+ if (f == null) {
+ break;
+ }
+ nextTime = nextTime.plusMinutes(stepMinutes);
+ BiForecastDataDO data = BiForecastDataDO.builder()
+ .forecastCode(kpiCode)
+ .forecastName(kpi.getName() + " 棰勬祴")
+ .pointTime(nextTime)
+ .forecastValue(f.value())
+ .lowerBound(f.lowerBound())
+ .upperBound(f.upperBound())
+ .confidenceLevel(new BigDecimal("95.00"))
+ .modelVersion("v2.0-" + (model == null ? "sma" : model.toLowerCase()))
+ .dimension("kpi")
+ .dimensionValue(kpiCode)
+ .build();
+ forecastDataMapper.insert(data);
+ count++;
+ // 灏嗛娴嬪�艰拷鍔犺繘鍘嗗彶锛屼緵涓嬩竴鏈熸粴鍔�
+ history.add(f.value());
+ if (history.size() > 200) {
+ history.remove(0);
+ }
+ }
+ return count;
}
@Override
diff --git a/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/KpiSnapshotTask.java b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/KpiSnapshotTask.java
new file mode 100644
index 0000000..3759216
--- /dev/null
+++ b/yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/service/decision/KpiSnapshotTask.java
@@ -0,0 +1,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;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml
index 61026f9..5063739 100644
--- a/yudao-server/src/main/resources/application-local.yaml
+++ b/yudao-server/src/main/resources/application-local.yaml
@@ -204,3 +204,12 @@
telemetry:
base-url: http://106.227.91.252:3100 # 鏁伴噰鏈嶅姟鍦板潃
path: /api/jxjs/mesTb/getDeviceDetails
+
+# BI 鍐崇瓥鏀寔瀹氭椂浠诲姟閰嶇疆
+bi:
+ alert:
+ evaluation:
+ interval: 300000 # 棰勮璇勪及闂撮殧锛堟绉掞級锛岄粯璁�5鍒嗛挓
+ kpi:
+ snapshot:
+ cron: "0 0 * * * *" # KPI鍘嗗彶蹇収鑱氬悎 cron锛岄粯璁ゆ瘡灏忔椂鏁寸偣
diff --git a/yudao-server/src/main/resources/application-test.yaml b/yudao-server/src/main/resources/application-test.yaml
index 5e34067..ef4c1cf 100644
--- a/yudao-server/src/main/resources/application-test.yaml
+++ b/yudao-server/src/main/resources/application-test.yaml
@@ -220,3 +220,12 @@
compress: true # 鏄惁鍘嬬缉鍥剧墖
need-compress-size: 10MB # 瑙﹀彂鍘嬬缉鐨勬枃浠跺ぇ灏忛槇鍊�
compress-quality: 0.5 # 鍘嬬缉璐ㄩ噺(0.0-1.0)
+
+# BI 鍐崇瓥鏀寔瀹氭椂浠诲姟閰嶇疆
+bi:
+ alert:
+ evaluation:
+ interval: 300000 # 棰勮璇勪及闂撮殧锛堟绉掞級锛岄粯璁�5鍒嗛挓
+ kpi:
+ snapshot:
+ cron: "0 0 * * * *" # KPI鍘嗗彶蹇収鑱氬悎 cron锛岄粯璁ゆ瘡灏忔椂鏁寸偣
--
Gitblit v1.9.3