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 | 187 ++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 171 insertions(+), 16 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 5455fb5..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,14 +1,31 @@
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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+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;
/**
* 璐熻嵎棰勬祴 Service 瀹炵幇
@@ -21,31 +38,169 @@
@Service
public class BiForecastServiceImpl implements BiForecastService {
+ /**
+ * 宸ュ崟鏉ユ簮绫诲瀷锛氳礋鑽烽娴嬶紙涓庡瓧鍏� mes_pro_work_order_source_type 鐨� value=4 涓�鑷达級
+ */
+ private static final Integer WORK_ORDER_SOURCE_TYPE_FORECAST = 4;
+
@Resource
private BiForecastDataMapper forecastDataMapper;
+ @Resource
+ private BiKpiDefinitionMapper kpiMapper;
+ @Resource
+ private BiKpiValueHistoryMapper kpiValueHistoryMapper;
+ @Resource
+ private MesProWorkOrderApi mesProWorkOrderApi;
@Override
public List<BiForecastDataDO> getForecastList(String forecastCode, LocalDateTime beginTime, LocalDateTime endTime) {
return forecastDataMapper.selectListByCodeAndTimeRange(forecastCode, beginTime, endTime);
}
- @Override
- public void generateForecast(String forecastCode, LocalDateTime pointTime) {
- // 鏈湡鍗犱綅锛氱畝鍗曠Щ鍔ㄥ钩鍧囬娴�
- // 瀹為檯椤圭洰搴旈泦鎴� ML 妯″瀷锛圓RIMA/Prophet/LSTM锛�
- log.info("[generateForecast][鐢熸垚棰勬祴] forecastCode={}, pointTime={}, 褰撳墠涓哄崰浣嶅疄鐜�", forecastCode, pointTime);
+ /**
+ * 绉诲姩骞冲潎绐楀彛澶у皬
+ */
+ private static final int SMA_WINDOW = 12;
- BiForecastDataDO forecast = BiForecastDataDO.builder()
- .forecastCode(forecastCode)
- .forecastName("璐熻嵎棰勬祴(鍗犱綅)")
- .pointTime(pointTime)
- .forecastValue(BigDecimal.ZERO)
- .lowerBound(BigDecimal.ZERO)
- .upperBound(BigDecimal.ZERO)
- .confidenceLevel(new BigDecimal("95.00"))
- .modelVersion("v1.0-placeholder")
+ @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(forecast);
+ forecastDataMapper.insert(data);
+ return data.getId();
}
-}
\ No newline at end of file
+ @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);
+ }
+ 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(forecast.value())
+ .lowerBound(forecast.lowerBound())
+ .upperBound(forecast.upperBound())
+ .confidenceLevel(new BigDecimal("95.00"))
+ .modelVersion("v2.0-" + model.toLowerCase())
+ .build();
+ 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
+ @Transactional(rollbackFor = Exception.class)
+ public Long createWorkOrderFromForecast(DecisionForecastWorkOrderCreateReqVO reqVO) {
+ // 1. 鏍¢獙棰勬祴鏁版嵁瀛樺湪
+ BiForecastDataDO forecast = forecastDataMapper.selectById(reqVO.getForecastId());
+ if (forecast == null) {
+ throw exception(BI_FORECAST_NOT_EXISTS);
+ }
+ // 2. 缁勮宸ュ崟 DTO 骞惰皟鐢� MES API 鍒涘缓锛堟棤浜у搧宸ュ崟锛岃烦 BOM锛�
+ MesProWorkOrderCreateReqDTO dto = new MesProWorkOrderCreateReqDTO();
+ dto.setCode(generateWorkOrderCode());
+ dto.setName(reqVO.getWorkOrderName() != null
+ ? reqVO.getWorkOrderName()
+ : ("璐熻嵎棰勬祴-" + forecast.getForecastName()));
+ dto.setType(reqVO.getWorkOrderType());
+ dto.setOrderSourceType(WORK_ORDER_SOURCE_TYPE_FORECAST);
+ dto.setOrderSourceCode(forecast.getForecastCode());
+ dto.setRequestDate(reqVO.getRequestDate());
+ dto.setQuantity(reqVO.getQuantity());
+ dto.setRemark(reqVO.getRemark());
+ return mesProWorkOrderApi.createWorkOrder(dto);
+ }
+
+ private String generateWorkOrderCode() {
+ return "GZ" + DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss")
+ + String.format("%03d", (int) (Math.random() * 1000));
+ }
+
+}
--
Gitblit v1.9.3