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/BiForecastServiceImpl.java |  132 ++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 122 insertions(+), 10 deletions(-)

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 6f183d3..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
@@ -1,9 +1,15 @@
 package cn.iocoder.yudao.module.bi.service.decision;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateUtil;
+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;
@@ -12,10 +18,13 @@
 import org.springframework.transaction.annotation.Transactional;
 
 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;
+import static cn.iocoder.yudao.module.bi.enums.ErrorCodeConstants.BI_FORECAST_NO_HISTORY;
 import static cn.iocoder.yudao.module.bi.enums.ErrorCodeConstants.BI_FORECAST_NOT_EXISTS;
 
 /**
@@ -37,6 +46,10 @@
     @Resource
     private BiForecastDataMapper forecastDataMapper;
     @Resource
+    private BiKpiDefinitionMapper kpiMapper;
+    @Resource
+    private BiKpiValueHistoryMapper kpiValueHistoryMapper;
+    @Resource
     private MesProWorkOrderApi mesProWorkOrderApi;
 
     @Override
@@ -44,23 +57,122 @@
         return forecastDataMapper.selectListByCodeAndTimeRange(forecastCode, beginTime, endTime);
     }
 
+    /**
+     * 绉诲姩骞冲潎绐楀彛澶у皬
+     */
+    private static final int SMA_WINDOW = 12;
+
+    @Override
+    public Long createForecastHistory(DecisionForecastHistorySaveReqVO reqVO) {
+        BiForecastDataDO data = BiForecastDataDO.builder()
+                .forecastCode(reqVO.getForecastCode())
+                .forecastName(reqVO.getForecastName())
+                .pointTime(reqVO.getPointTime())
+                .forecastValue(reqVO.getForecastValue())
+                .actualValue(reqVO.getActualValue())
+                .dimension(reqVO.getDimension())
+                .dimensionValue(reqVO.getDimensionValue())
+                .remark(reqVO.getRemark())
+                .modelVersion("manual-history")
+                .build();
+        forecastDataMapper.insert(data);
+        return data.getId();
+    }
+
     @Override
     public void generateForecast(String forecastCode, LocalDateTime pointTime) {
-        // 鏈湡鍗犱綅锛氱畝鍗曠Щ鍔ㄥ钩鍧囬娴�
-        // 瀹為檯椤圭洰搴旈泦鎴� ML 妯″瀷锛圓RIMA/Prophet/LSTM锛�
-        log.info("[generateForecast][鐢熸垚棰勬祴] forecastCode={}, pointTime={}, 褰撳墠涓哄崰浣嶅疄鐜�", forecastCode, pointTime);
+        // 榛樿浣跨敤 SMA 妯″瀷
+        generateForecast(forecastCode, "SMA", SMA_WINDOW, 7, pointTime);
+    }
 
-        BiForecastDataDO forecast = BiForecastDataDO.builder()
+    @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);
+        }
+        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("璐熻嵎棰勬祴(鍗犱綅)")
+                .forecastName(history.get(0).getForecastName())
                 .pointTime(pointTime)
-                .forecastValue(BigDecimal.ZERO)
-                .lowerBound(BigDecimal.ZERO)
-                .upperBound(BigDecimal.ZERO)
+                .forecastValue(forecast.value())
+                .lowerBound(forecast.lowerBound())
+                .upperBound(forecast.upperBound())
                 .confidenceLevel(new BigDecimal("95.00"))
-                .modelVersion("v1.0-placeholder")
+                .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

--
Gitblit v1.9.3