| | |
| | | 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 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; |
| | |
| | | |
| | | @Resource |
| | | private BiForecastDataMapper forecastDataMapper; |
| | | @Resource |
| | | private BiKpiDefinitionMapper kpiMapper; |
| | | @Resource |
| | | private BiKpiValueHistoryMapper kpiValueHistoryMapper; |
| | | @Resource |
| | | private MesProWorkOrderApi mesProWorkOrderApi; |
| | | |
| | |
| | | |
| | | @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 |