From 71e87ac75fdeb98efa3005198c238d5e62ab56e2 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期一, 19 十月 2026 13:11:03 +0800
Subject: [PATCH] fix(mes): AI 预测在未配置大模型时回退本地规则引擎
---
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java | 687 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 665 insertions(+), 22 deletions(-)
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java
index 208b2d9..9fd272d 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java
@@ -10,6 +10,7 @@
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationMachineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
@@ -22,6 +23,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdProductBomMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkstationMachineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMapper;
@@ -152,6 +154,8 @@
@Resource
private MesProWorkOrderBomMapper workOrderBomMapper;
@Resource
+ private MesMdProductBomMapper productBomMapper;
+ @Resource
private MesProWorkOrderProcessService workOrderProcessService;
@Resource
private MesProTaskMapper taskMapper;
@@ -198,11 +202,209 @@
log.info("AI 鐗╂枡棰勬祴: {}, response={}", logPrefix, aiResponse);
return parseMaterialResponse(aiResponse);
} catch (Exception e) {
- log.warn("AI 鐗╂枡棰勬祴澶辫触锛寃orkOrderId={}, mpsId={}", reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
- MesProAiMaterialPredictRespVO fallback = new MesProAiMaterialPredictRespVO();
- fallback.setReasoning("AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�");
- return fallback;
+ log.warn("AI 鐗╂枡棰勬祴澶辫触锛寃orkOrderId={}, mpsId={}锛屽垏鎹㈡湰鍦拌鍒欐牳绠�",
+ reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
+ // AI 澶фā鍨嬫湭閰嶇疆鎴栬皟鐢ㄥけ璐ユ椂锛岄��鍖栦负鏈湴瑙勫垯鏍哥畻锛屼繚璇佸姛鑳藉彲鐢�
+ return buildMaterialFallback(reqVO);
}
+ }
+
+ /**
+ * AI 涓嶅彲鐢ㄦ椂鐨勬湰鍦板厹搴曪細鍩轰簬 BOM 闇�姹傞噺涓庡疄鏃跺簱瀛樺彲鐢ㄩ噺鍋氱‘瀹氭�ф牳绠椼��
+ *
+ * 搴撳瓨鍙敤閲� = 鍦ㄥ簱鏁伴噺 - 鍐荤粨鏁伴噺 - 宸插崰鐢ㄦ暟閲�
+ */
+ private MesProAiMaterialPredictRespVO buildMaterialFallback(MesProAiMaterialPredictReqVO reqVO) {
+ MesProAiMaterialPredictRespVO respVO = new MesProAiMaterialPredictRespVO();
+ try {
+ List<MaterialDemand> demands = reqVO.getMpsId() != null
+ ? buildMaterialDemandsFromMps(reqVO.getMpsId())
+ : buildMaterialDemandsFromWorkOrder(reqVO.getWorkOrderId());
+ if (CollUtil.isEmpty(demands)) {
+ respVO.setRiskLevel(1);
+ respVO.setRiskItems(Collections.emptyList());
+ respVO.setReasoning("AI 鏈嶅姟鏆備笉鍙敤锛屼笖鏈壘鍒板彲鏍哥畻鐨� BOM 鐗╂枡锛屾棤娉曡繘琛岀墿鏂欑煭缂洪娴嬨��"
+ + "璇峰厛缁存姢浜у搧 BOM锛屾垨鍦ㄣ�孉I 澶фā鍨� - 妯″瀷绠$悊銆嶄腑閰嶇疆鍙敤鐨勫璇濇ā鍨嬨��");
+ return respVO;
+ }
+
+ Map<Long, BigDecimal> stockMap = getAvailableStockMap(
+ demands.stream().map(MaterialDemand::itemId).collect(Collectors.toSet()));
+
+ List<MesProAiMaterialPredictRespVO.RiskItem> riskItems = new ArrayList<>(demands.size());
+ boolean hasShortage = false;
+ boolean hasTight = false;
+ int shortageCount = 0;
+ for (MaterialDemand demand : demands) {
+ BigDecimal required = demand.requiredQuantity();
+ BigDecimal available = stockMap.getOrDefault(demand.itemId(), BigDecimal.ZERO);
+ BigDecimal shortage = required.subtract(available).max(BigDecimal.ZERO);
+
+ String level;
+ String suggestion;
+ if (shortage.compareTo(BigDecimal.ZERO) <= 0) {
+ level = "鍏呰冻";
+ suggestion = "搴撳瓨鍏呰冻锛屾棤闇�琛ュ厖";
+ } else if (available.compareTo(required.multiply(BigDecimal.valueOf(0.5))) >= 0) {
+ level = "绱у紶";
+ suggestion = "搴撳瓨鍋忕揣锛屽缓璁敖蹇噰璐垨璋冩嫧锛岀己鍙� "
+ + shortage.stripTrailingZeros().toPlainString();
+ hasTight = true;
+ } else {
+ level = "鐭己";
+ suggestion = "搴撳瓨涓嶈冻锛屽缓璁揣鎬ラ噰璐垨璋冩嫧锛岀己鍙� "
+ + shortage.stripTrailingZeros().toPlainString();
+ hasShortage = true;
+ shortageCount++;
+ }
+
+ MesProAiMaterialPredictRespVO.RiskItem item = new MesProAiMaterialPredictRespVO.RiskItem();
+ MdmItemRespDTO itemInfo = getItemInfoQuietly(demand.itemId());
+ item.setItemCode(itemInfo != null && StrUtil.isNotBlank(itemInfo.getCode())
+ ? itemInfo.getCode() : String.valueOf(demand.itemId()));
+ item.setItemName(itemInfo != null && StrUtil.isNotBlank(itemInfo.getName())
+ ? itemInfo.getName() : "鐗╂枡" + demand.itemId());
+ item.setRequiredQuantity(required);
+ item.setAvailableQuantity(available);
+ item.setShortageQuantity(shortage);
+ item.setRiskLevel(level);
+ item.setSuggestion(suggestion);
+ riskItems.add(item);
+ }
+
+ // 鐭己椤逛紭鍏堝睍绀猴紝缂哄彛澶х殑鍦ㄥ墠
+ riskItems.sort(Comparator
+ .comparingInt((MesProAiMaterialPredictRespVO.RiskItem item) -> riskSeverity(item.getRiskLevel()))
+ .reversed()
+ .thenComparing(MesProAiMaterialPredictRespVO.RiskItem::getShortageQuantity,
+ Comparator.reverseOrder()));
+
+ respVO.setRiskLevel(hasShortage ? 3 : (hasTight ? 2 : 1));
+ respVO.setRiskItems(riskItems);
+ String source = reqVO.getMpsId() != null ? "MPS 浜у搧 BOM" : "宸ュ崟 BOM";
+ respVO.setReasoning(String.format(
+ "AI 澶фā鍨嬫殏涓嶅彲鐢紝宸茶嚜鍔ㄥ垏鎹㈡湰鍦拌鍒欏紩鎿庡畬鎴愮墿鏂欐牳绠楋細鍩轰簬%s涓庡疄鏃跺簱瀛樺彲鐢ㄩ噺"
+ + "锛堝湪搴� - 鍐荤粨 - 宸插崰鐢級閫愰」姣斿锛屽叡鏍哥畻 %d 椤圭墿鏂欙紝鍏朵腑鐭己 %d 椤广�佺揣寮� %d 椤广��",
+ source, demands.size(), shortageCount, riskItems.size() - shortageCount - countSufficient(riskItems)));
+ } catch (Exception e) {
+ log.warn("鏈湴鐗╂枡鏍哥畻澶辫触锛寃orkOrderId={}, mpsId={}",
+ reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
+ respVO.setRiskLevel(1);
+ respVO.setRiskItems(Collections.emptyList());
+ respVO.setReasoning("AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝涓旀湰鍦扮墿鏂欐暟鎹牳绠楀け璐ワ紝璇风◢鍚庨噸璇曟垨妫�鏌ュ伐鍗�/MPS 鏁版嵁銆�");
+ }
+ return respVO;
+ }
+
+ private List<MaterialDemand> buildMaterialDemandsFromWorkOrder(Long workOrderId) {
+ if (workOrderId == null) {
+ return Collections.emptyList();
+ }
+ List<MesProWorkOrderBomDO> bomList = workOrderBomMapper.selectListByWorkOrderId(workOrderId);
+ if (CollUtil.isEmpty(bomList)) {
+ return Collections.emptyList();
+ }
+ Map<Long, BigDecimal> demandByItem = new LinkedHashMap<>();
+ for (MesProWorkOrderBomDO bom : bomList) {
+ if (bom.getItemId() == null || bom.getQuantity() == null
+ || bom.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
+ continue;
+ }
+ demandByItem.merge(bom.getItemId(), bom.getQuantity(), BigDecimal::add);
+ }
+ return toMaterialDemands(demandByItem);
+ }
+
+ private List<MaterialDemand> buildMaterialDemandsFromMps(Long mpsId) {
+ if (mpsId == null) {
+ return Collections.emptyList();
+ }
+ MesProMpsDO mps = mpsService.getMps(mpsId);
+ if (mps == null) {
+ return Collections.emptyList();
+ }
+ // 宸蹭笅鍙戝伐鍗曟椂浼樺厛浣跨敤宸ュ崟 BOM锛堟洿璐磋繎瀹為檯鐢熶骇闇�姹傦級
+ if (mps.getWorkOrderId() != null) {
+ List<MaterialDemand> workOrderDemands = buildMaterialDemandsFromWorkOrder(mps.getWorkOrderId());
+ if (CollUtil.isNotEmpty(workOrderDemands)) {
+ return workOrderDemands;
+ }
+ }
+ if (mps.getProductId() == null || mps.getQuantity() == null) {
+ return Collections.emptyList();
+ }
+ BigDecimal produced = mps.getQuantityProduced() != null ? mps.getQuantityProduced() : BigDecimal.ZERO;
+ BigDecimal remaining = mps.getQuantity().subtract(produced).max(BigDecimal.ZERO);
+ if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
+ return Collections.emptyList();
+ }
+ List<MesMdProductBomDO> productBomList = productBomMapper.selectByItemId(mps.getProductId());
+ if (CollUtil.isEmpty(productBomList)) {
+ return Collections.emptyList();
+ }
+ Map<Long, BigDecimal> demandByItem = new LinkedHashMap<>();
+ for (MesMdProductBomDO bom : productBomList) {
+ if (bom.getBomItemId() == null || bom.getQuantity() == null
+ || bom.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
+ continue;
+ }
+ demandByItem.merge(bom.getBomItemId(), remaining.multiply(bom.getQuantity()), BigDecimal::add);
+ }
+ return toMaterialDemands(demandByItem);
+ }
+
+ private List<MaterialDemand> toMaterialDemands(Map<Long, BigDecimal> demandByItem) {
+ List<MaterialDemand> demands = new ArrayList<>(demandByItem.size());
+ demandByItem.forEach((itemId, quantity) -> demands.add(new MaterialDemand(itemId, quantity)));
+ return demands;
+ }
+
+ private Map<Long, BigDecimal> getAvailableStockMap(Collection<Long> itemIds) {
+ if (CollUtil.isEmpty(itemIds)) {
+ return Collections.emptyMap();
+ }
+ List<MesWmMaterialStockDO> stocks = materialStockMapper.selectList(
+ new LambdaQueryWrapper<MesWmMaterialStockDO>()
+ .in(MesWmMaterialStockDO::getItemId, itemIds));
+ Map<Long, BigDecimal> stockMap = new HashMap<>();
+ for (MesWmMaterialStockDO stock : stocks) {
+ if (stock.getItemId() == null || stock.getQuantity() == null) {
+ continue;
+ }
+ BigDecimal frozen = stock.getFrozenQuantity() != null ? stock.getFrozenQuantity() : BigDecimal.ZERO;
+ BigDecimal reserved = stock.getReservedQuantity() != null ? stock.getReservedQuantity() : BigDecimal.ZERO;
+ BigDecimal available = stock.getQuantity().subtract(frozen).subtract(reserved);
+ stockMap.merge(stock.getItemId(), available, BigDecimal::add);
+ }
+ stockMap.replaceAll((itemId, available) -> available.max(BigDecimal.ZERO));
+ return stockMap;
+ }
+
+ private MdmItemRespDTO getItemInfoQuietly(Long itemId) {
+ if (itemId == null) {
+ return null;
+ }
+ try {
+ return mdmItemApi.getItem(itemId).getCheckedData();
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private int riskSeverity(String riskLevel) {
+ return switch (riskLevel) {
+ case "鐭己" -> 3;
+ case "绱у紶" -> 2;
+ case "鍏呰冻" -> 1;
+ default -> 0;
+ };
+ }
+
+ private long countSufficient(List<MesProAiMaterialPredictRespVO.RiskItem> riskItems) {
+ return riskItems.stream().filter(item -> "鍏呰冻".equals(item.getRiskLevel())).count();
+ }
+
+ private record MaterialDemand(Long itemId, BigDecimal requiredQuantity) {
}
private Map<String, Object> gatherMaterialData(Long workOrderId) {
@@ -335,9 +537,9 @@
@Override
public MesProAiDurationPredictRespVO predictDuration(MesProAiDurationPredictReqVO reqVO) {
+ Map<String, Object> data = null;
+ String logPrefix;
try {
- Map<String, Object> data;
- String logPrefix;
if (reqVO.getMpsId() != null) {
data = gatherDurationDataFromMps(reqVO.getMpsId());
logPrefix = "mpsId=" + reqVO.getMpsId();
@@ -349,10 +551,125 @@
log.info("AI 鏃堕暱棰勬祴: {}, response={}", logPrefix, aiResponse);
return parseDurationResponse(aiResponse);
} catch (Exception e) {
- log.warn("AI 鏃堕暱棰勬祴澶辫触锛寃orkOrderId={}, mpsId={}", reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
- MesProAiDurationPredictRespVO fallback = new MesProAiDurationPredictRespVO();
- fallback.setReasoning("AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�");
- return fallback;
+ log.warn("AI 鏃堕暱棰勬祴澶辫触锛寃orkOrderId={}, mpsId={}锛屽垏鎹㈡湰鍦拌鍒欎及绠�",
+ reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
+ return buildDurationFallback(reqVO, data);
+ }
+ }
+
+ /**
+ * AI 涓嶅彲鐢ㄦ椂鐨勬湰鍦版椂闀夸及绠楋細鏍囧噯鍑嗗/绛夊緟鏃堕棿 + 璁″垝鏁伴噺 / 璁惧鏁堢巼锛岀敓浜ф椂闂撮鐣� 10% 缂撳啿銆�
+ */
+ private MesProAiDurationPredictRespVO buildDurationFallback(MesProAiDurationPredictReqVO reqVO,
+ Map<String, Object> gathered) {
+ MesProAiDurationPredictRespVO respVO = new MesProAiDurationPredictRespVO();
+ try {
+ Map<String, Object> data = resolveWorkOrderId(reqVO.getWorkOrderId(), reqVO.getMpsId()) != null
+ ? gatherDurationData(resolveWorkOrderId(reqVO.getWorkOrderId(), reqVO.getMpsId()))
+ : gathered;
+ DurationEstimate estimate = computeDurationEstimate(data);
+ if (!estimate.hasProcessData()) {
+ respVO.setPredictedTotalHours(BigDecimal.ZERO);
+ respVO.setStandardTotalHours(BigDecimal.ZERO);
+ respVO.setProcessBreakdown(Collections.emptyList());
+ respVO.setKeyPoints(Collections.emptyList());
+ respVO.setReasoning("AI 澶фā鍨嬫殏涓嶅彲鐢紝涓斿綋鍓嶅伐鍗�/MPS 娌℃湁鍙敤鐨勫伐搴忔垨璁惧鏁版嵁锛屾棤娉曚及绠楃敓浜ф椂闀裤��"
+ + "璇峰厛缁存姢宸ヨ壓璺嚎骞跺畬鎴愭帓浜э紝鎴栧湪銆孉I 澶фā鍨� - 妯″瀷绠$悊銆嶄腑閰嶇疆鍙敤鐨勫璇濇ā鍨嬨��");
+ return respVO;
+ }
+ respVO.setPredictedTotalHours(estimate.predictedHours());
+ respVO.setStandardTotalHours(estimate.standardHours());
+ respVO.setProcessBreakdown(estimate.breakdown());
+ respVO.setKeyPoints(estimate.keyPoints());
+ respVO.setReasoning("AI 澶фā鍨嬫殏涓嶅彲鐢紝宸茶嚜鍔ㄥ垏鎹㈡湰鍦拌鍒欏紩鎿庝及绠楋細鎸夊伐鑹鸿矾绾挎爣鍑嗗噯澶�/绛夊緟鏃堕棿"
+ + "鍔犱笂銆屽墿浣欒鍒掓暟閲� 梅 璁惧鏁堢巼銆嶈绠楃敓浜ф椂闂达紝骞跺鐢熶骇鏃堕棿棰勭暀 10% 缂撳啿銆�");
+ } catch (Exception e) {
+ log.warn("鏈湴鏃堕暱浼扮畻澶辫触锛寃orkOrderId={}, mpsId={}",
+ reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
+ respVO.setPredictedTotalHours(BigDecimal.ZERO);
+ respVO.setStandardTotalHours(BigDecimal.ZERO);
+ respVO.setProcessBreakdown(Collections.emptyList());
+ respVO.setKeyPoints(Collections.emptyList());
+ respVO.setReasoning("AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝涓旀湰鍦板伐搴忔暟鎹牳绠楀け璐ワ紝璇风◢鍚庨噸璇曟垨妫�鏌ュ伐鑹鸿矾绾�/鎺掍骇鏁版嵁銆�");
+ }
+ return respVO;
+ }
+
+ /** MPS 宸蹭笅鍙戞椂浼樺厛浣跨敤鍏宠仈宸ュ崟鐨勬暟鎹� */
+ private Long resolveWorkOrderId(Long workOrderId, Long mpsId) {
+ if (workOrderId != null) {
+ return workOrderId;
+ }
+ if (mpsId != null) {
+ MesProMpsDO mps = mpsService.getMps(mpsId);
+ if (mps != null) {
+ return mps.getWorkOrderId();
+ }
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private DurationEstimate computeDurationEstimate(Map<String, Object> data) {
+ if (data == null) {
+ return DurationEstimate.empty();
+ }
+ Object rawProcesses = data.get("宸ュ簭鏄庣粏");
+ List<Map<String, Object>> processes = rawProcesses instanceof List<?>
+ ? (List<Map<String, Object>>) rawProcesses : Collections.emptyList();
+ if (CollUtil.isEmpty(processes)) {
+ return DurationEstimate.empty();
+ }
+ BigDecimal prepareWaitHours = toBigDecimal(data.get("鏍囧噯鎬诲噯澶�+绛夊緟鏃堕棿(灏忔椂)"));
+ BigDecimal standardProduction = BigDecimal.ZERO;
+ BigDecimal predictedProduction = BigDecimal.ZERO;
+ List<MesProAiDurationPredictRespVO.ProcessTimeBreakdown> breakdown = new ArrayList<>(processes.size());
+ List<String> keyPoints = new ArrayList<>();
+ int missingEfficiency = 0;
+
+ for (Map<String, Object> proc : processes) {
+ MesProAiDurationPredictRespVO.ProcessTimeBreakdown item =
+ new MesProAiDurationPredictRespVO.ProcessTimeBreakdown();
+ String processName = (String) proc.get("宸ュ簭鍚嶇О");
+ item.setProcessName(processName);
+ item.setSort(toInteger(proc.get("椤哄簭")));
+ item.setStandardPrepareTime(toInteger(proc.get("鏍囧噯鍑嗗鏃堕棿(鍒嗛挓)")));
+ item.setStandardWaitTime(toInteger(proc.get("鏍囧噯绛夊緟鏃堕棿(鍒嗛挓)")));
+
+ BigDecimal productionHours = proc.get("棰勮鐢熶骇鏃堕棿(灏忔椂)") != null
+ ? toBigDecimal(proc.get("棰勮鐢熶骇鏃堕棿(灏忔椂)")) : BigDecimal.ZERO;
+ BigDecimal predictedHours = productionHours.multiply(BigDecimal.valueOf(1.1));
+ standardProduction = standardProduction.add(productionHours);
+ predictedProduction = predictedProduction.add(predictedHours);
+
+ item.setStandardProductionTime(productionHours.setScale(2, RoundingMode.HALF_UP));
+ item.setPredictedProductionTime(predictedHours.setScale(2, RoundingMode.HALF_UP));
+ breakdown.add(item);
+
+ if (productionHours.compareTo(BigDecimal.ZERO) <= 0) {
+ missingEfficiency++;
+ }
+ if (Boolean.TRUE.equals(proc.get("鏄惁鍏抽敭宸ュ簭"))) {
+ keyPoints.add(String.format("鍏抽敭宸ュ簭銆�%s銆嶉璁� %.2f 灏忔椂锛堝惈 10%% 缂撳啿锛�",
+ processName, predictedHours.doubleValue()));
+ }
+ }
+ if (missingEfficiency > 0) {
+ keyPoints.add(String.format("鏈� %d 閬撳伐搴忕己灏戣澶囨晥鐜囨垨鎺掍骇鏁版嵁锛岀敓浜ф椂闂存寜 0 璁★紝寤鸿琛ラ綈璁惧浜ц兘鍚庨噸鏂伴娴�",
+ missingEfficiency));
+ }
+ BigDecimal standardHours = prepareWaitHours.add(standardProduction).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal predictedHours = prepareWaitHours.add(predictedProduction).setScale(2, RoundingMode.HALF_UP);
+ return new DurationEstimate(standardHours, predictedHours, breakdown, keyPoints, true);
+ }
+
+ private record DurationEstimate(BigDecimal standardHours, BigDecimal predictedHours,
+ List<MesProAiDurationPredictRespVO.ProcessTimeBreakdown> breakdown,
+ List<String> keyPoints, boolean hasProcessData) {
+
+ private static DurationEstimate empty() {
+ return new DurationEstimate(BigDecimal.ZERO, BigDecimal.ZERO,
+ Collections.emptyList(), Collections.emptyList(), false);
}
}
@@ -492,9 +809,9 @@
@Override
public MesProAiRiskPredictRespVO predictRisk(MesProAiRiskPredictReqVO reqVO) {
+ Map<String, Object> data = null;
+ String logPrefix;
try {
- Map<String, Object> data;
- String logPrefix;
if (reqVO.getMpsId() != null) {
data = gatherRiskDataFromMps(reqVO.getMpsId());
logPrefix = "mpsId=" + reqVO.getMpsId();
@@ -506,11 +823,160 @@
log.info("AI 椋庨櫓棰勬祴: {}, response={}", logPrefix, aiResponse);
return parseRiskResponse(aiResponse);
} catch (Exception e) {
- log.warn("AI 椋庨櫓棰勬祴澶辫触锛寃orkOrderId={}, mpsId={}", reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
- MesProAiRiskPredictRespVO fallback = new MesProAiRiskPredictRespVO();
- fallback.setReasoning("AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�");
- return fallback;
+ log.warn("AI 椋庨櫓棰勬祴澶辫触锛寃orkOrderId={}, mpsId={}锛屽垏鎹㈡湰鍦拌鍒欒瘎浼�",
+ reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
+ return buildRiskFallback(reqVO, data);
}
+ }
+
+ /**
+ * AI 涓嶅彲鐢ㄦ椂鐨勬湰鍦伴闄╄瘎浼帮細IPQC 缂洪櫡鐜� + 璁惧鐘舵��/淇濆吇鐐规 + 鐗╂枡鍙敤閲� + 鍏抽敭宸ュ簭銆�
+ */
+ private MesProAiRiskPredictRespVO buildRiskFallback(MesProAiRiskPredictReqVO reqVO,
+ Map<String, Object> gathered) {
+ MesProAiRiskPredictRespVO respVO = new MesProAiRiskPredictRespVO();
+ try {
+ Long effectiveWorkOrderId = resolveWorkOrderId(reqVO.getWorkOrderId(), reqVO.getMpsId());
+ Map<String, Object> data = effectiveWorkOrderId != null
+ ? gatherRiskData(effectiveWorkOrderId) : gathered;
+ List<MesProAiRiskPredictRespVO.RiskItem> risks = new ArrayList<>();
+ List<String> keyPoints = new ArrayList<>();
+
+ if (data == null) {
+ respVO.setOverallRiskLevel(1);
+ respVO.setRisks(Collections.emptyList());
+ respVO.setKeyPoints(Collections.emptyList());
+ respVO.setReasoning("AI 鏈嶅姟鏆備笉鍙敤锛屼笖鏈幏鍙栧埌宸ュ崟/MPS 椋庨櫓鏁版嵁锛屾棤娉曡瘎浼般��");
+ return respVO;
+ }
+
+ // 1. 璐ㄩ噺椋庨櫓
+ for (Map<String, Object> qc : asMapList(data.get("杩囩▼妫�楠�(IPQC)璁板綍"))) {
+ BigDecimal critical = parsePercent(qc.get("鑷村懡缂洪櫡鐜�"));
+ BigDecimal major = parsePercent(qc.get("涓ラ噸缂洪櫡鐜�"));
+ BigDecimal minor = parsePercent(qc.get("杞诲井缂洪櫡鐜�"));
+ String code = StrUtil.blankToDefault((String) qc.get("妫�楠屽崟缂栧彿"), "杩囩▼妫�楠屽崟");
+ if (critical.compareTo(BigDecimal.valueOf(5)) > 0) {
+ risks.add(risk("璐ㄩ噺", String.format("%s 鑷村懡缂洪櫡鐜� %s%%锛岃秴杩� 5%% 闃堝��", code, critical.stripTrailingZeros().toPlainString()),
+ "楂�", "寤鸿绔嬪嵆鍋滅嚎鎺掓煡锛屽宸茬敓浜ф壒娆¤繘琛岄殧绂诲妫�"));
+ } else if (major.compareTo(BigDecimal.TEN) > 0) {
+ risks.add(risk("璐ㄩ噺", String.format("%s 涓ラ噸缂洪櫡鐜� %s%%锛岃秴杩� 10%% 闃堝��", code, major.stripTrailingZeros().toPlainString()),
+ "涓�", "寤鸿鍒嗘瀽缂洪櫡鏍瑰洜骞跺姞寮鸿繃绋嬬鎺�"));
+ } else if (critical.compareTo(BigDecimal.ZERO) > 0 || major.compareTo(BigDecimal.ZERO) > 0
+ || minor.compareTo(BigDecimal.ZERO) > 0) {
+ risks.add(risk("璐ㄩ噺", String.format("%s 瀛樺湪涓嶈壇鍝侊紙鑷村懡 %s%%銆佷弗閲� %s%%銆佽交寰� %s%%锛�",
+ code, critical.stripTrailingZeros().toPlainString(),
+ major.stripTrailingZeros().toPlainString(),
+ minor.stripTrailingZeros().toPlainString()),
+ "浣�", "寤鸿鎸佺画璺熻釜妫�楠屾暟鎹紝鍏虫敞缂洪櫡鐜囧彉鍖栬秼鍔�"));
+ }
+ }
+
+ // 2. 璁惧椋庨櫓
+ for (Map<String, Object> eq : asMapList(data.get("鍏宠仈璁惧淇℃伅"))) {
+ String name = StrUtil.blankToDefault((String) eq.get("璁惧鍚嶇О"),
+ String.valueOf(eq.get("璁惧缂栧彿")));
+ int status = toInteger(eq.get("璁惧鐘舵��"));
+ if (status == 1) {
+ risks.add(risk("璁惧", name + " 褰撳墠澶勪簬鍋滄満鐘舵��", "楂�",
+ "寤鸿鐢熶骇鍓嶇‘璁ゅ仠鏈哄師鍥犲苟瀹屾垚鎭㈠锛岄伩鍏嶅奖鍝嶅紑宸�"));
+ } else if (status == 3) {
+ risks.add(risk("璁惧", name + " 褰撳墠澶勪簬缁存姢涓�", "涓�",
+ "寤鸿纭缁存姢瀹屾垚鏃堕棿锛屽繀瑕佹椂璋冩暣鎺掍骇璁″垝"));
+ }
+ Long maintenanceDays = daysSince(eq.get("涓婃淇濆吇鏃堕棿"));
+ if (maintenanceDays != null && maintenanceDays > 30) {
+ risks.add(risk("璁惧", String.format("%s 宸� %d 澶╂湭淇濆吇", name, maintenanceDays), "涓�",
+ "寤鸿鍦ㄧ敓浜у墠瀹夋帓淇濆吇锛岄檷浣庤澶囨晠闅滈闄�"));
+ }
+ Long checkDays = daysSince(eq.get("涓婃鐐规鏃堕棿"));
+ if (checkDays != null && checkDays > 14) {
+ risks.add(risk("璁惧", String.format("%s 宸� %d 澶╂湭鐐规", name, checkDays), "浣�",
+ "寤鸿寮�宸ュ墠瀹屾垚璁惧鐐规"));
+ }
+ }
+
+ // 3. 鐗╂枡椋庨櫓
+ for (Map<String, Object> material : asMapList(data.get("鐗╂枡鐭己椋庨櫓"))) {
+ BigDecimal required = toBigDecimal(material.get("闇�姹傞噺"));
+ BigDecimal available = toBigDecimal(material.get("鍙敤閲�"));
+ BigDecimal shortage = required.subtract(available).max(BigDecimal.ZERO);
+ if (shortage.compareTo(BigDecimal.ZERO) <= 0) {
+ continue;
+ }
+ boolean severe = available.compareTo(required.multiply(BigDecimal.valueOf(0.5))) < 0;
+ risks.add(risk("鐗╂枡", String.format("%s 闇�姹� %s銆佸彲鐢� %s锛岀己鍙� %s",
+ material.get("鐗╂枡鍚嶇О"), required.stripTrailingZeros().toPlainString(),
+ available.stripTrailingZeros().toPlainString(),
+ shortage.stripTrailingZeros().toPlainString()),
+ severe ? "楂�" : "涓�",
+ severe ? "寤鸿绱ф�ラ噰璐垨璋冩嫧锛岄伩鍏嶅奖鍝嶅紑宸�" : "寤鸿灏藉揩琛ュ厖閲囪喘鎴栧唴閮ㄨ皟鎷�"));
+ }
+
+ // 3.1 MPS 鏈笅鍙戞椂锛屾寜浜у搧 BOM 琛ュ厖鐗╂枡椋庨櫓
+ if (effectiveWorkOrderId == null && reqVO.getMpsId() != null) {
+ List<MaterialDemand> demands = buildMaterialDemandsFromMps(reqVO.getMpsId());
+ Map<Long, BigDecimal> stockMap = getAvailableStockMap(
+ demands.stream().map(MaterialDemand::itemId).collect(Collectors.toSet()));
+ for (MaterialDemand demand : demands) {
+ BigDecimal available = stockMap.getOrDefault(demand.itemId(), BigDecimal.ZERO);
+ BigDecimal shortage = demand.requiredQuantity().subtract(available).max(BigDecimal.ZERO);
+ if (shortage.compareTo(BigDecimal.ZERO) <= 0) {
+ continue;
+ }
+ boolean severe = available.compareTo(
+ demand.requiredQuantity().multiply(BigDecimal.valueOf(0.5))) < 0;
+ risks.add(risk("鐗╂枡", String.format("鐗╂枡 %s 闇�姹� %s銆佸彲鐢� %s锛岀己鍙� %s",
+ resolveItemName(demand.itemId()),
+ demand.requiredQuantity().stripTrailingZeros().toPlainString(),
+ available.stripTrailingZeros().toPlainString(),
+ shortage.stripTrailingZeros().toPlainString()),
+ severe ? "楂�" : "涓�",
+ severe ? "寤鸿绱ф�ラ噰璐垨璋冩嫧" : "寤鸿灏藉揩琛ュ厖閲囪喘鎴栧唴閮ㄨ皟鎷�"));
+ }
+ }
+
+ // 4. 鍏抽敭宸ュ簭鍏虫敞鐐�
+ for (Map<String, Object> proc : asMapList(data.get("鍏抽敭宸ュ簭"))) {
+ keyPoints.add(String.format("鍏抽敭宸ュ簭銆�%s銆嶏紙椤哄簭 %s锛夐渶閲嶇偣鍏虫敞浜ц兘涓庢帓闃熸儏鍐�",
+ proc.get("宸ュ簭鍚嶇О"), proc.get("椤哄簭")));
+ }
+ keyPoints.removeIf(StrUtil::isBlank);
+
+ int overallLevel = risks.stream()
+ .mapToInt(item -> severityRank(item.getSeverity()))
+ .max().orElse(1);
+ respVO.setOverallRiskLevel(overallLevel);
+ respVO.setRisks(risks);
+ respVO.setKeyPoints(keyPoints);
+ if (risks.isEmpty()) {
+ respVO.setReasoning("AI 澶фā鍨嬫殏涓嶅彲鐢紝宸茶嚜鍔ㄥ垏鎹㈡湰鍦拌鍒欏紩鎿庯細缁煎悎 IPQC 缂洪櫡鐜囥�佽澶囩姸鎬佷笌淇濆吇鐐规銆�"
+ + "鐗╂枡鍙敤閲忓強鍏抽敭宸ュ簭杩涜璇勪及锛屾湭鍙戠幇鏄庢樉椋庨櫓銆�");
+ } else {
+ respVO.setReasoning(String.format(
+ "AI 澶фā鍨嬫殏涓嶅彲鐢紝宸茶嚜鍔ㄥ垏鎹㈡湰鍦拌鍒欏紩鎿庯細缁煎悎 IPQC 缂洪櫡鐜囥�佽澶囩姸鎬佷笌淇濆吇鐐规銆�"
+ + "鐗╂枡鍙敤閲忓強鍏抽敭宸ュ簭杩涜璇勪及锛屽叡璇嗗埆 %d 椤归闄╋紝鏁翠綋椋庨櫓绛夌骇涓�%s銆�",
+ risks.size(), overallLevel == 3 ? "楂橀闄�" : (overallLevel == 2 ? "涓闄�" : "浣庨闄�")));
+ }
+ } catch (Exception e) {
+ log.warn("鏈湴椋庨櫓璇勪及澶辫触锛寃orkOrderId={}, mpsId={}",
+ reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
+ respVO.setOverallRiskLevel(1);
+ respVO.setRisks(Collections.emptyList());
+ respVO.setKeyPoints(Collections.emptyList());
+ respVO.setReasoning("AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝涓旀湰鍦伴闄╂暟鎹牳绠楀け璐ワ紝璇风◢鍚庨噸璇曟垨妫�鏌ュ伐鍗曟暟鎹��");
+ }
+ return respVO;
+ }
+
+ private MesProAiRiskPredictRespVO.RiskItem risk(String category, String description,
+ String severity, String suggestion) {
+ MesProAiRiskPredictRespVO.RiskItem item = new MesProAiRiskPredictRespVO.RiskItem();
+ item.setCategory(category);
+ item.setDescription(description);
+ item.setSeverity(severity);
+ item.setSuggestion(suggestion);
+ return item;
}
private Map<String, Object> gatherRiskData(Long workOrderId) {
@@ -703,9 +1169,9 @@
@Override
public MesProAiDeliveryPredictRespVO predictDelivery(MesProAiDeliveryPredictReqVO reqVO) {
+ Map<String, Object> data = null;
+ String logPrefix;
try {
- Map<String, Object> data;
- String logPrefix;
if (reqVO.getMpsId() != null) {
data = gatherDeliveryDataFromMps(reqVO.getMpsId());
logPrefix = "mpsId=" + reqVO.getMpsId();
@@ -717,11 +1183,133 @@
log.info("AI 浜や粯棰勬祴: {}, response={}", logPrefix, aiResponse);
return parseDeliveryResponse(aiResponse);
} catch (Exception e) {
- log.warn("AI 浜や粯棰勬祴澶辫触锛寃orkOrderId={}, mpsId={}", reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
- MesProAiDeliveryPredictRespVO fallback = new MesProAiDeliveryPredictRespVO();
- fallback.setReasoning("AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�");
- return fallback;
+ log.warn("AI 浜や粯棰勬祴澶辫触锛寃orkOrderId={}, mpsId={}锛屽垏鎹㈡湰鍦拌鍒欒瘎浼�",
+ reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
+ return buildDeliveryFallback(reqVO, data);
}
+ }
+
+ /**
+ * AI 涓嶅彲鐢ㄦ椂鐨勬湰鍦颁氦浠樿瘎浼帮細鐢熶骇杩涘害 + 鍓╀綑浜ゆ湡锛堟寜 8 灏忔椂/澶╋級+ 鍓╀綑棰勬祴宸ユ椂 + 鐗╂枡缂哄彛銆�
+ */
+ private MesProAiDeliveryPredictRespVO buildDeliveryFallback(MesProAiDeliveryPredictReqVO reqVO,
+ Map<String, Object> gathered) {
+ MesProAiDeliveryPredictRespVO respVO = new MesProAiDeliveryPredictRespVO();
+ List<String> delayFactors = new ArrayList<>();
+ try {
+ Long effectiveWorkOrderId = resolveWorkOrderId(reqVO.getWorkOrderId(), reqVO.getMpsId());
+ Map<String, Object> data = effectiveWorkOrderId != null
+ ? gatherDeliveryData(effectiveWorkOrderId) : gathered;
+ if (data == null) {
+ respVO.setOnTime(false);
+ respVO.setConfidence(0D);
+ respVO.setProgressPercent(BigDecimal.ZERO);
+ respVO.setRemainingDays(0);
+ respVO.setDelayFactors(Collections.emptyList());
+ respVO.setReasoning("AI 鏈嶅姟鏆備笉鍙敤锛屼笖鏈幏鍙栧埌宸ュ崟/MPS 杩涘害鏁版嵁锛屾棤娉曡瘎浼颁氦浠樸��");
+ return respVO;
+ }
+
+ BigDecimal progressPercent = toBigDecimal(data.get("鐢熶骇杩涘害(%)"));
+ int remainingDays = toInteger(data.get("璺濈闇�姹傛棩鏈熷墿浣欏ぉ鏁�"));
+ respVO.setProgressPercent(progressPercent);
+ respVO.setRemainingDays(remainingDays);
+
+ if (progressPercent.compareTo(BigDecimal.valueOf(100)) >= 0) {
+ respVO.setOnTime(true);
+ respVO.setConfidence(99D);
+ respVO.setDelayFactors(Collections.emptyList());
+ respVO.setReasoning("AI 澶фā鍨嬫殏涓嶅彲鐢紝宸茶嚜鍔ㄥ垏鎹㈡湰鍦拌鍒欏紩鎿庯細宸ュ崟宸插叏閮ㄥ畬鎴愶紝鍙寜鏃朵氦浠樸��");
+ return respVO;
+ }
+
+ BigDecimal availableHours = BigDecimal.valueOf(Math.max(remainingDays, 0))
+ .multiply(BigDecimal.valueOf(8));
+ BigDecimal remainingHours = null;
+ if (effectiveWorkOrderId != null) {
+ DurationEstimate estimate = computeDurationEstimate(gatherDurationData(effectiveWorkOrderId));
+ if (estimate.hasProcessData()) {
+ remainingHours = estimate.predictedHours();
+ }
+ }
+
+ boolean onTime;
+ double confidence;
+ if (remainingDays <= 0) {
+ onTime = false;
+ confidence = 15D;
+ delayFactors.add("闇�姹傛棩鏈熷凡鍒版垨宸茶繃鏈燂紝浣嗗伐鍗曞皻鏈畬鎴�");
+ } else if (remainingHours == null) {
+ // 缂哄皯宸ュ簭/璁惧鏁版嵁鏃讹紝閫�鍖栦负鎸夎繘搴︿笌浜ゆ湡绮楀垽
+ onTime = progressPercent.compareTo(BigDecimal.valueOf(50)) >= 0 || remainingDays >= 7;
+ confidence = 55D;
+ delayFactors.add("缂哄皯宸ュ簭/璁惧鏁堢巼鏁版嵁锛屾棤娉曠簿纭绠楀墿浣欏伐鏃讹紝浠呮寜杩涘害涓庝氦鏈熺矖鍒�");
+ } else {
+ BigDecimal requiredWithBuffer = remainingHours.multiply(BigDecimal.valueOf(0.8));
+ onTime = availableHours.compareTo(requiredWithBuffer) >= 0;
+ BigDecimal ratio = availableHours.compareTo(BigDecimal.ZERO) > 0
+ ? availableHours.divide(remainingHours, 4, RoundingMode.HALF_UP)
+ : BigDecimal.ZERO;
+ confidence = ratio.compareTo(BigDecimal.valueOf(1.5)) >= 0 ? 92D
+ : ratio.compareTo(BigDecimal.valueOf(1.1)) >= 0 ? 82D
+ : ratio.compareTo(BigDecimal.ONE) >= 0 ? 70D
+ : ratio.compareTo(BigDecimal.valueOf(0.8)) >= 0 ? 45D : 20D;
+ if (availableHours.compareTo(remainingHours) < 0) {
+ delayFactors.add(String.format(
+ "鍓╀綑浜ゆ湡 %d 澶╋紙鎸� 8 灏忔椂/澶╁叡 %.1f 灏忔椂锛夛紝棰勮浠嶉渶 %.1f 灏忔椂锛屽伐鏈熶笉瓒�",
+ remainingDays, availableHours.doubleValue(), remainingHours.doubleValue()));
+ }
+ }
+
+ // 浠诲姟瀹屾垚鎯呭喌
+ long finishedTasks = toInteger(data.get("宸插畬鎴愪换鍔℃暟"));
+ long totalTasks = toInteger(data.get("鎬讳换鍔℃暟"));
+ if (totalTasks > 0 && finishedTasks < totalTasks) {
+ delayFactors.add(String.format("浠嶆湁 %d 涓换鍔℃湭瀹屾垚锛堝叡 %d 涓級",
+ totalTasks - finishedTasks, totalTasks));
+ }
+
+ // 鐗╂枡缂哄彛
+ List<MaterialDemand> materialDemands = effectiveWorkOrderId != null
+ ? buildMaterialDemandsFromWorkOrder(effectiveWorkOrderId)
+ : (reqVO.getMpsId() != null ? buildMaterialDemandsFromMps(reqVO.getMpsId())
+ : Collections.emptyList());
+ Map<Long, BigDecimal> stockMap = getAvailableStockMap(
+ materialDemands.stream().map(MaterialDemand::itemId).collect(Collectors.toSet()));
+ long shortageItems = materialDemands.stream()
+ .filter(demand -> stockMap.getOrDefault(demand.itemId(), BigDecimal.ZERO)
+ .compareTo(demand.requiredQuantity()) < 0)
+ .count();
+ if (shortageItems > 0) {
+ delayFactors.add(String.format("鏈� %d 椤圭墿鏂欏簱瀛樹笉瓒筹紝鍙兘褰卞搷寮�宸�", shortageItems));
+ if (onTime) {
+ onTime = false;
+ confidence = Math.min(confidence, 45D);
+ }
+ }
+
+ respVO.setOnTime(onTime);
+ respVO.setConfidence(confidence);
+ respVO.setDelayFactors(delayFactors);
+ respVO.setReasoning(String.format(
+ "AI 澶фā鍨嬫殏涓嶅彲鐢紝宸茶嚜鍔ㄥ垏鎹㈡湰鍦拌鍒欏紩鎿庯細褰撳墠鐢熶骇杩涘害 %s%%锛屽墿浣欎氦鏈� %d 澶�"
+ + "锛堟寜姣忓ぉ 8 灏忔椂鎶樼畻 %.1f 灏忔椂锛�%s銆�",
+ progressPercent.stripTrailingZeros().toPlainString(), remainingDays,
+ availableHours.doubleValue(),
+ remainingHours != null
+ ? String.format("锛岄璁″墿浣欏伐鏃� %.1f 灏忔椂", remainingHours.doubleValue())
+ : "锛屽洜缂哄皯宸ュ簭/璁惧鏁版嵁鏈绠楀墿浣欏伐鏃�"));
+ } catch (Exception e) {
+ log.warn("鏈湴浜や粯璇勪及澶辫触锛寃orkOrderId={}, mpsId={}",
+ reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
+ respVO.setOnTime(false);
+ respVO.setConfidence(0D);
+ respVO.setProgressPercent(BigDecimal.ZERO);
+ respVO.setRemainingDays(0);
+ respVO.setDelayFactors(Collections.emptyList());
+ respVO.setReasoning("AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝涓旀湰鍦颁氦浠樻暟鎹牳绠楀け璐ワ紝璇风◢鍚庨噸璇曟垨妫�鏌ュ伐鍗曟暟鎹��");
+ }
+ return respVO;
}
private Map<String, Object> gatherDeliveryData(Long workOrderId) {
@@ -858,6 +1446,61 @@
// ==================== Shared Utilities ====================
+ @SuppressWarnings("unchecked")
+ private List<Map<String, Object>> asMapList(Object value) {
+ if (value instanceof List<?> list) {
+ return (List<Map<String, Object>>) list;
+ }
+ return Collections.emptyList();
+ }
+
+ private BigDecimal parsePercent(Object value) {
+ if (value == null) {
+ return BigDecimal.ZERO;
+ }
+ String text = value.toString().replace("%", "").trim();
+ try {
+ return new BigDecimal(text);
+ } catch (Exception e) {
+ return BigDecimal.ZERO;
+ }
+ }
+
+ /** 璁$畻璺濅粖澶╂暟锛涙棤娉曡В鏋愭垨鈥滄棤璁板綍鈥濇椂杩斿洖 null */
+ private Long daysSince(Object value) {
+ if (value == null) {
+ return null;
+ }
+ String text = value.toString().trim();
+ if (StrUtil.isBlank(text) || "鏃犺褰�".equals(text)) {
+ return null;
+ }
+ try {
+ String normalized = text.replace(' ', 'T');
+ if (normalized.length() >= 19) {
+ normalized = normalized.substring(0, 19);
+ }
+ LocalDateTime time = normalized.length() > 10
+ ? LocalDateTime.parse(normalized)
+ : LocalDate.parse(normalized).atStartOfDay();
+ return ChronoUnit.DAYS.between(time.toLocalDate(), LocalDate.now());
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private int severityRank(String severity) {
+ if (severity == null) {
+ return 1;
+ }
+ return switch (severity) {
+ case "楂�" -> 3;
+ case "涓�" -> 2;
+ case "浣�" -> 1;
+ default -> 1;
+ };
+ }
+
private String extractJson(String aiResponse) {
int start = aiResponse.indexOf("{");
int end = aiResponse.lastIndexOf("}");
--
Gitblit v1.9.3