package cn.iocoder.yudao.module.mes.service.pro.ai; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.module.ai.api.chat.AiChatApi; import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi; import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO; import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; 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.workstation.MesMdWorkstationMachineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskIssueDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO; 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.workstation.MesMdWorkstationMachineMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskIssueMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper; import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_WORK_ORDER_NOT_EXISTS; @Slf4j @Service public class MesProAiServiceImpl implements MesProAiService { // ==================== System Prompts ==================== private static final String SYSTEM_PROMPT_MATERIAL = """ 你是一位资深的制造业物料管理专家,擅长分析生产工单的物料齐套性和短缺风险。 你需要综合分析以下信息,给出专业的物料预测建议: ## 分析维度 1. BOM 需求 vs 库存可用量:对比每个物料的预计使用量与当前库存可用量 2. 库存可用量 = 在库数量 - 冻结数量 - 已占用数量 3. 已投料情况:已经发放到工位的物料是否充足 4. 风险评估:对有短缺风险的物料标注风险等级 ## 风险等级定义 - 充足:可用量 >= 需求量 - 紧张:可用量 >= 需求量的50% 但 < 需求量 - 短缺:可用量 < 需求量的50% 或可用量为0 ## 输出格式 请按以下JSON格式返回(不要包含其他内容): {"riskLevel": 1, "reasoning": "综合分析...", "riskItems": [{"itemCode": "...", "itemName": "...", "requiredQuantity": 100.0, "availableQuantity": 50.0, "shortageQuantity": 50.0, "riskLevel": "紧张", "suggestion": "建议紧急采购或调拨"}]} riskLevel 取值:1=低风险 2=中风险 3=高风险 """; private static final String SYSTEM_PROMPT_DURATION = """ 你是一位资深的生产计划专家,擅长预测生产工单的实际生产时长。 你需要综合分析以下信息,给出专业的时长预测建议: ## 分析依据 1. 标准工时:工艺路线设定的标准准备时间、等待时间、生产时间 2. 设备效率:关联设备的每小时产量,传送履带倍率 3. 生产数量:工单计划数量 4. 实际历史数据:已完成工序的实际生产时长 5. 计算公式:预测时长 = 准备时间 + 等待时间 + (计划数量 / (设备效率 * 传送履带系数))(效率单位为每小时产量) ## 输出格式 请按以下JSON格式返回(不要包含其他内容): {"predictedTotalHours": 48.5, "standardTotalHours": 40.0, "reasoning": "...", "keyPoints": ["关键工序A是瓶颈"], "processBreakdown": [{"processName": "...", "sort": 1, "standardPrepareTime": 30, "standardWaitTime": 15, "standardProductionTime": 8.0, "predictedProductionTime": 10.5}]} predictedTotalHours 和 standardTotalHours 为小数 """; private static final String SYSTEM_PROMPT_RISK = """ 你是一位资深的制造风险管理专家,擅长识别生产过程中的各类风险。 你需要综合分析以下信息,给出专业的风险评估: ## 分析维度 1. 质量风险:过程检验(IPQC)的缺陷率数据(致命/严重/轻微缺陷率) 2. 设备风险:设备状态、上次保养时间、上次点检时间、生产效率 3. 物料风险:物料库存可用量与需求量的对比 4. 工艺瓶颈:关键工序的设备产能、任务排队情况 ## 风险等级定义 - 高:存在致命缺陷率>5% 或 设备故障 或 关键物料短缺>50% 或 关键工序严重积压 - 中:存在严重缺陷率>10% 或 设备长期未保养 或 物料紧张 - 低:各项指标正常 ## 输出格式 请按以下JSON格式返回(不要包含其他内容): {"overallRiskLevel": 2, "reasoning": "...", "keyPoints": ["关注点1"], "risks": [{"category": "设备", "description": "设备A上次保养距今已超30天", "severity": "中", "suggestion": "建议生产前保养"}]} overallRiskLevel 取值:1=低风险 2=中风险 3=高风险 riskLevel 取值:充足/紧张/短缺 severity 取值:高/中/低 category 取值:质量/设备/物料/工艺瓶颈 """; private static final String SYSTEM_PROMPT_DELIVERY = """ 你是一位资深的订单交付管理专家,擅长评估工单是否能按时交付。 你需要综合分析以下信息,给出专业的交付评估: ## 分析依据 1. 当前进度:已生产数量 / 计划数量的百分比 2. 剩余时间:距离需求日期的天数(工作日按每天8小时计算) 3. 预计剩余工时:根据工艺路线和设备效率预测的剩余生产时长 4. 生产速度:根据已完成任务的实际生产速率 5. 任务状态:各工序的完成情况 ## 判定逻辑 - 按时交付:剩余天数 * 8小时 >= 预计剩余工时,且进度正常 - 有延期风险:剩余天数 * 8小时 >= 预计剩余工时的80%,但不足100% - 无法按时交付:剩余天数 * 8小时 < 预计剩余工时的80%,或关键工序卡阻 ## 输出格式 请按以下JSON格式返回(不要包含其他内容): {"onTime": true, "confidence": 85.5, "reasoning": "...", "delayFactors": [], "progressPercent": 60.0, "remainingDays": 10} onTime: true=可按时交付 false=无法按时交付 confidence: 0-100的数字,表示评估置信度 """; // ==================== Dependencies ==================== @Resource private MesProWorkOrderService workOrderService; @Resource private MesProWorkOrderBomMapper workOrderBomMapper; @Resource private MesProWorkOrderProcessService workOrderProcessService; @Resource private MesProTaskMapper taskMapper; @Resource private MesProTaskIssueMapper taskIssueMapper; @Resource private MesProFeedbackMapper feedbackMapper; @Resource private MesQcIpqcMapper ipqcMapper; @Resource private MesWmMaterialStockMapper materialStockMapper; @Resource private MesMdWorkstationMachineMapper workstationMachineMapper; @Resource private MesDvMachineryMapper machineryMapper; @Resource private MesProMpsMapper mpsMapper; @Resource private MesProProcessService processService; @Resource private MdmItemApi mdmItemApi; @Resource private CrmCustomerApi crmCustomerApi; @Resource private AiChatApi aiChatApi; // ==================== 1. 物料短缺预测 ==================== @Override public MesProAiMaterialPredictRespVO predictMaterial(MesProAiMaterialPredictReqVO reqVO) { try { Map data = gatherMaterialData(reqVO.getWorkOrderId()); String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_MATERIAL, JSONUtil.toJsonPrettyStr(data)); log.info("AI 物料预测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse); return parseMaterialResponse(aiResponse); } catch (Exception e) { log.warn("AI 物料预测失败,workOrderId={}", reqVO.getWorkOrderId(), e); MesProAiMaterialPredictRespVO fallback = new MesProAiMaterialPredictRespVO(); fallback.setReasoning("AI 分析暂时不可用,请稍后重试"); return fallback; } } private Map gatherMaterialData(Long workOrderId) { Map data = new LinkedHashMap<>(); MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId); if (workOrder == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS); data.put("工单编号", workOrder.getCode()); data.put("工单名称", workOrder.getName()); data.put("计划数量", workOrder.getQuantity()); data.put("已生产数量", workOrder.getQuantityProduced()); fillItemInfo(data, workOrder.getProductId()); // BOM 物料需求 List bomList = workOrderBomMapper.selectListByWorkOrderId(workOrderId); if (CollUtil.isNotEmpty(bomList)) { List> materials = new ArrayList<>(); for (MesProWorkOrderBomDO bom : bomList) { Map item = new LinkedHashMap<>(); item.put("物料ID", bom.getItemId()); String itemName = resolveItemName(bom.getItemId()); item.put("物料名称", itemName); item.put("需求量", bom.getQuantity()); // 查询库存可用量 List stocks = materialStockMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(MesWmMaterialStockDO::getItemId, bom.getItemId()) .ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO)); BigDecimal available = BigDecimal.ZERO; if (CollUtil.isNotEmpty(stocks)) { for (MesWmMaterialStockDO stock : stocks) { BigDecimal frozen = stock.getFrozenQuantity() != null ? stock.getFrozenQuantity() : BigDecimal.ZERO; BigDecimal reserved = stock.getReservedQuantity() != null ? stock.getReservedQuantity() : BigDecimal.ZERO; available = available.add(stock.getQuantity().subtract(frozen).subtract(reserved)); } } item.put("库存可用量", available.compareTo(BigDecimal.ZERO) > 0 ? available : BigDecimal.ZERO); materials.add(item); } data.put("物料需求清单", materials); } // 已投料情况 List tasks = taskMapper.selectListByWorkOrderId(workOrderId); if (CollUtil.isNotEmpty(tasks)) { List> issuedMaterials = new ArrayList<>(); for (MesProTaskDO task : tasks) { List issues = taskIssueMapper.selectListByTaskId(task.getId()); for (MesProTaskIssueDO issue : issues) { Map issued = new LinkedHashMap<>(); issued.put("物料ID", issue.getItemId()); issued.put("已投料数量", issue.getIssuedQuantity()); issued.put("已使用数量", issue.getUsedQuantity()); issued.put("可用数量", issue.getAvailableQuantity()); issuedMaterials.add(issued); } } if (!issuedMaterials.isEmpty()) { data.put("已投料记录", issuedMaterials); } } return data; } private MesProAiMaterialPredictRespVO parseMaterialResponse(String aiResponse) { MesProAiMaterialPredictRespVO respVO = new MesProAiMaterialPredictRespVO(); try { String json = extractJson(aiResponse); Map parsed = JSONUtil.toBean(json, Map.class); Object riskLevel = parsed.get("riskLevel"); if (riskLevel instanceof Number num) respVO.setRiskLevel(num.intValue()); respVO.setReasoning((String) parsed.get("reasoning")); @SuppressWarnings("unchecked") List> items = (List>) parsed.get("riskItems"); if (CollUtil.isNotEmpty(items)) { respVO.setRiskItems(items.stream().map(m -> { MesProAiMaterialPredictRespVO.RiskItem ri = new MesProAiMaterialPredictRespVO.RiskItem(); ri.setItemCode((String) m.get("itemCode")); ri.setItemName((String) m.get("itemName")); ri.setRequiredQuantity(toBigDecimal(m.get("requiredQuantity"))); ri.setAvailableQuantity(toBigDecimal(m.get("availableQuantity"))); ri.setShortageQuantity(toBigDecimal(m.get("shortageQuantity"))); ri.setRiskLevel((String) m.get("riskLevel")); ri.setSuggestion((String) m.get("suggestion")); return ri; }).collect(Collectors.toList())); } } catch (Exception e) { log.warn("解析 AI 物料预测响应失败: {}", aiResponse, e); respVO.setReasoning(aiResponse); } return respVO; } // ==================== 2. 生产时长预测 ==================== @Override public MesProAiDurationPredictRespVO predictDuration(MesProAiDurationPredictReqVO reqVO) { try { Map data = gatherDurationData(reqVO.getWorkOrderId()); String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_DURATION, JSONUtil.toJsonPrettyStr(data)); log.info("AI 时长预测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse); return parseDurationResponse(aiResponse); } catch (Exception e) { log.warn("AI 时长预测失败,workOrderId={}", reqVO.getWorkOrderId(), e); MesProAiDurationPredictRespVO fallback = new MesProAiDurationPredictRespVO(); fallback.setReasoning("AI 分析暂时不可用,请稍后重试"); return fallback; } } private Map gatherDurationData(Long workOrderId) { Map data = new LinkedHashMap<>(); MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId); if (workOrder == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS); data.put("工单编号", workOrder.getCode()); data.put("计划数量", workOrder.getQuantity()); data.put("已生产数量", workOrder.getQuantityProduced()); fillItemInfo(data, workOrder.getProductId()); // 工序信息 List processes = workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId); List tasks = taskMapper.selectListByWorkOrderId(workOrderId); Map taskByProcessId = new HashMap<>(); if (CollUtil.isNotEmpty(tasks)) { for (MesProTaskDO task : tasks) { if (task.getProcessId() != null) { taskByProcessId.put(task.getProcessId(), task); } } } List> processList = new ArrayList<>(); BigDecimal standardTotalHours = BigDecimal.ZERO; for (MesProWorkOrderProcessDO proc : processes) { Map item = new LinkedHashMap<>(); String processName = resolveProcessName(proc.getProcessId()); item.put("工序名称", processName); item.put("顺序", proc.getSort()); item.put("标准准备时间(分钟)", proc.getPrepareTime() != null ? proc.getPrepareTime() : 0); item.put("标准等待时间(分钟)", proc.getWaitTime() != null ? proc.getWaitTime() : 0); item.put("是否关键工序", proc.getKeyFlag() != null && proc.getKeyFlag()); item.put("是否质检工序", proc.getCheckFlag() != null && proc.getCheckFlag()); // 设备效率 MesProTaskDO task = taskByProcessId.get(proc.getProcessId()); if (task != null && task.getWorkstationId() != null) { BigDecimal totalEfficiency = getWorkstationEfficiency(task.getWorkstationId()); item.put("设备总效率(件/小时)", totalEfficiency); if (totalEfficiency.compareTo(BigDecimal.ZERO) > 0) { BigDecimal productionHours = workOrder.getQuantity() .subtract(workOrder.getQuantityProduced() != null ? workOrder.getQuantityProduced() : BigDecimal.ZERO) .divide(totalEfficiency, 4, RoundingMode.HALF_UP); item.put("预计生产时间(小时)", productionHours); } item.put("任务状态", task.getStatus()); if (task.getDuration() != null) item.put("已用时长(小时)", task.getDuration()); } else { item.put("设备总效率(件/小时)", "未排产无设备数据"); } // 标准时间 int prepareMin = proc.getPrepareTime() != null ? proc.getPrepareTime() : 0; int waitMin = proc.getWaitTime() != null ? proc.getWaitTime() : 0; BigDecimal standardHours = BigDecimal.valueOf(prepareMin + waitMin).divide(BigDecimal.valueOf(60), 4, RoundingMode.HALF_UP); standardTotalHours = standardTotalHours.add(standardHours); item.put("标准准备+等待时间(小时)", standardHours); processList.add(item); } data.put("工序明细", processList); data.put("标准总准备+等待时间(小时)", standardTotalHours); return data; } private BigDecimal getWorkstationEfficiency(Long workstationId) { BigDecimal totalEfficiency = BigDecimal.ZERO; List machines = workstationMachineMapper.selectListByWorkstationId(workstationId); if (CollUtil.isNotEmpty(machines)) { for (MesMdWorkstationMachineDO wm : machines) { MesDvMachineryDO machinery = machineryMapper.selectById(wm.getMachineryId()); if (machinery != null && machinery.getProductionEfficiency() != null) { BigDecimal efficiency = machinery.getProductionEfficiency(); if (machinery.getConveyorBelt() != null && machinery.getConveyorBelt() > 0) { efficiency = efficiency.multiply(BigDecimal.valueOf(machinery.getConveyorBelt())); } totalEfficiency = totalEfficiency.add(efficiency); } } } return totalEfficiency; } private MesProAiDurationPredictRespVO parseDurationResponse(String aiResponse) { MesProAiDurationPredictRespVO respVO = new MesProAiDurationPredictRespVO(); try { String json = extractJson(aiResponse); Map parsed = JSONUtil.toBean(json, Map.class); respVO.setPredictedTotalHours(toBigDecimal(parsed.get("predictedTotalHours"))); respVO.setStandardTotalHours(toBigDecimal(parsed.get("standardTotalHours"))); respVO.setReasoning((String) parsed.get("reasoning")); @SuppressWarnings("unchecked") List keyPoints = (List) parsed.get("keyPoints"); respVO.setKeyPoints(keyPoints); @SuppressWarnings("unchecked") List> breakdown = (List>) parsed.get("processBreakdown"); if (CollUtil.isNotEmpty(breakdown)) { respVO.setProcessBreakdown(breakdown.stream().map(m -> { MesProAiDurationPredictRespVO.ProcessTimeBreakdown pt = new MesProAiDurationPredictRespVO.ProcessTimeBreakdown(); pt.setProcessName((String) m.get("processName")); pt.setSort(toInteger(m.get("sort"))); pt.setStandardPrepareTime(toInteger(m.get("standardPrepareTime"))); pt.setStandardWaitTime(toInteger(m.get("standardWaitTime"))); pt.setStandardProductionTime(toBigDecimal(m.get("standardProductionTime"))); pt.setPredictedProductionTime(toBigDecimal(m.get("predictedProductionTime"))); return pt; }).collect(Collectors.toList())); } } catch (Exception e) { log.warn("解析 AI 时长预测响应失败: {}", aiResponse, e); respVO.setReasoning(aiResponse); } return respVO; } // ==================== 3. 生产风险预测 ==================== @Override public MesProAiRiskPredictRespVO predictRisk(MesProAiRiskPredictReqVO reqVO) { try { Map data = gatherRiskData(reqVO.getWorkOrderId()); String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_RISK, JSONUtil.toJsonPrettyStr(data)); log.info("AI 风险预测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse); return parseRiskResponse(aiResponse); } catch (Exception e) { log.warn("AI 风险预测失败,workOrderId={}", reqVO.getWorkOrderId(), e); MesProAiRiskPredictRespVO fallback = new MesProAiRiskPredictRespVO(); fallback.setReasoning("AI 分析暂时不可用,请稍后重试"); return fallback; } } private Map gatherRiskData(Long workOrderId) { Map data = new LinkedHashMap<>(); MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId); if (workOrder == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS); data.put("工单编号", workOrder.getCode()); data.put("工单名称", workOrder.getName()); data.put("计划数量", workOrder.getQuantity()); data.put("已生产数量", workOrder.getQuantityProduced()); fillItemInfo(data, workOrder.getProductId()); // 质量风险 - IPQC List ipqcList = ipqcMapper.selectList( new LambdaQueryWrapper() .eq(MesQcIpqcDO::getWorkOrderId, workOrderId)); if (CollUtil.isNotEmpty(ipqcList)) { List> qcData = ipqcList.stream().map(ipqc -> { Map item = new LinkedHashMap<>(); item.put("检验单编号", ipqc.getCode()); item.put("致命缺陷率", ipqc.getCriticalRate() != null ? ipqc.getCriticalRate() + "%" : "0%"); item.put("严重缺陷率", ipqc.getMajorRate() != null ? ipqc.getMajorRate() + "%" : "0%"); item.put("轻微缺陷率", ipqc.getMinorRate() != null ? ipqc.getMinorRate() + "%" : "0%"); item.put("致命缺陷数", ipqc.getCriticalQuantity() != null ? ipqc.getCriticalQuantity() : 0); item.put("严重缺陷数", ipqc.getMajorQuantity() != null ? ipqc.getMajorQuantity() : 0); item.put("轻微缺陷数", ipqc.getMinorQuantity() != null ? ipqc.getMinorQuantity() : 0); item.put("检验结果", ipqc.getCheckResult()); return item; }).collect(Collectors.toList()); data.put("过程检验(IPQC)记录", qcData); } // 设备风险 List tasks = taskMapper.selectListByWorkOrderId(workOrderId); if (CollUtil.isNotEmpty(tasks)) { Set machineryIds = new HashSet<>(); for (MesProTaskDO task : tasks) { if (task.getWorkstationId() != null) { List wms = workstationMachineMapper.selectListByWorkstationId(task.getWorkstationId()); for (MesMdWorkstationMachineDO wm : wms) { machineryIds.add(wm.getMachineryId()); } } } if (!machineryIds.isEmpty()) { List> equipmentData = new ArrayList<>(); for (Long mid : machineryIds) { MesDvMachineryDO machinery = machineryMapper.selectById(mid); if (machinery != null) { Map eq = new LinkedHashMap<>(); eq.put("设备编号", machinery.getCode()); eq.put("设备名称", machinery.getName()); eq.put("设备状态", machinery.getStatus()); eq.put("生产效率(件/小时)", machinery.getProductionEfficiency()); eq.put("传送履带倍数", machinery.getConveyorBelt()); eq.put("上次保养时间", machinery.getLastMaintenTime() != null ? machinery.getLastMaintenTime().toString() : "无记录"); eq.put("上次点检时间", machinery.getLastCheckTime() != null ? machinery.getLastCheckTime().toString() : "无记录"); equipmentData.add(eq); } } data.put("关联设备信息", equipmentData); } } // 物料风险(轻量级:仅标记可用量 < 20%的物料) List bomList = workOrderBomMapper.selectListByWorkOrderId(workOrderId); if (CollUtil.isNotEmpty(bomList)) { List> materialRisks = new ArrayList<>(); for (MesProWorkOrderBomDO bom : bomList) { List stocks = materialStockMapper.selectList( new LambdaQueryWrapper() .eq(MesWmMaterialStockDO::getItemId, bom.getItemId()) .ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO)); BigDecimal available = BigDecimal.ZERO; if (CollUtil.isNotEmpty(stocks)) { for (MesWmMaterialStockDO stock : stocks) { BigDecimal frozen = stock.getFrozenQuantity() != null ? stock.getFrozenQuantity() : BigDecimal.ZERO; BigDecimal reserved = stock.getReservedQuantity() != null ? stock.getReservedQuantity() : BigDecimal.ZERO; available = available.add(stock.getQuantity().subtract(frozen).subtract(reserved)); } } if (bom.getQuantity() != null && bom.getQuantity().compareTo(BigDecimal.ZERO) > 0 && available.compareTo(bom.getQuantity().multiply(BigDecimal.valueOf(0.2))) < 0) { Map risk = new LinkedHashMap<>(); risk.put("物料ID", bom.getItemId()); risk.put("物料名称", resolveItemName(bom.getItemId())); risk.put("需求量", bom.getQuantity()); risk.put("可用量", available); risk.put("缺口比例", available.compareTo(BigDecimal.ZERO) > 0 ? BigDecimal.ONE.subtract(available.divide(bom.getQuantity(), 2, RoundingMode.HALF_UP)).multiply(BigDecimal.valueOf(100)) + "%" : "100%"); materialRisks.add(risk); } } if (!materialRisks.isEmpty()) { data.put("物料短缺风险", materialRisks); } } // 工艺瓶颈 - 关键工序信息 List processes = workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId); if (CollUtil.isNotEmpty(processes)) { List> keyProcesses = new ArrayList<>(); for (MesProWorkOrderProcessDO proc : processes) { if (proc.getKeyFlag() != null && proc.getKeyFlag()) { Map kp = new LinkedHashMap<>(); kp.put("工序名称", resolveProcessName(proc.getProcessId())); kp.put("顺序", proc.getSort()); kp.put("标准准备时间(分钟)", proc.getPrepareTime()); kp.put("标准等待时间(分钟)", proc.getWaitTime()); keyProcesses.add(kp); } } if (!keyProcesses.isEmpty()) { data.put("关键工序", keyProcesses); } } return data; } private MesProAiRiskPredictRespVO parseRiskResponse(String aiResponse) { MesProAiRiskPredictRespVO respVO = new MesProAiRiskPredictRespVO(); try { String json = extractJson(aiResponse); Map parsed = JSONUtil.toBean(json, Map.class); Object riskLevel = parsed.get("overallRiskLevel"); if (riskLevel instanceof Number num) respVO.setOverallRiskLevel(num.intValue()); respVO.setReasoning((String) parsed.get("reasoning")); @SuppressWarnings("unchecked") List keyPoints = (List) parsed.get("keyPoints"); respVO.setKeyPoints(keyPoints); @SuppressWarnings("unchecked") List> risks = (List>) parsed.get("risks"); if (CollUtil.isNotEmpty(risks)) { respVO.setRisks(risks.stream().map(m -> { MesProAiRiskPredictRespVO.RiskItem ri = new MesProAiRiskPredictRespVO.RiskItem(); ri.setCategory((String) m.get("category")); ri.setDescription((String) m.get("description")); ri.setSeverity((String) m.get("severity")); ri.setSuggestion((String) m.get("suggestion")); return ri; }).collect(Collectors.toList())); } } catch (Exception e) { log.warn("解析 AI 风险预测响应失败: {}", aiResponse, e); respVO.setReasoning(aiResponse); } return respVO; } // ==================== 4. 是否能按时交付 ==================== @Override public MesProAiDeliveryPredictRespVO predictDelivery(MesProAiDeliveryPredictReqVO reqVO) { try { Map data = gatherDeliveryData(reqVO.getWorkOrderId()); String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_DELIVERY, JSONUtil.toJsonPrettyStr(data)); log.info("AI 交付预测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse); return parseDeliveryResponse(aiResponse); } catch (Exception e) { log.warn("AI 交付预测失败,workOrderId={}", reqVO.getWorkOrderId(), e); MesProAiDeliveryPredictRespVO fallback = new MesProAiDeliveryPredictRespVO(); fallback.setReasoning("AI 分析暂时不可用,请稍后重试"); return fallback; } } private Map gatherDeliveryData(Long workOrderId) { Map data = new LinkedHashMap<>(); MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId); if (workOrder == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS); data.put("工单编号", workOrder.getCode()); data.put("工单名称", workOrder.getName()); data.put("计划数量", workOrder.getQuantity()); data.put("已生产数量", workOrder.getQuantityProduced()); data.put("工单状态", workOrder.getStatus()); data.put("需求日期", workOrder.getRequestDate() != null ? workOrder.getRequestDate().toString() : "未设置"); data.put("完成日期", workOrder.getFinishDate() != null ? workOrder.getFinishDate().toString() : "未完成"); fillItemInfo(data, workOrder.getProductId()); // 进度计算 BigDecimal progressPercent = BigDecimal.ZERO; if (workOrder.getQuantity() != null && workOrder.getQuantity().compareTo(BigDecimal.ZERO) > 0 && workOrder.getQuantityProduced() != null) { progressPercent = workOrder.getQuantityProduced() .divide(workOrder.getQuantity(), 4, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)); } data.put("生产进度(%)", progressPercent.setScale(1, RoundingMode.HALF_UP)); // 剩余天数 int remainingDays = 0; if (workOrder.getRequestDate() != null) { remainingDays = (int) ChronoUnit.DAYS.between(LocalDate.now(), workOrder.getRequestDate().toLocalDate()); } data.put("距离需求日期剩余天数", Math.max(0, remainingDays)); // 任务完成情况 List tasks = taskMapper.selectListByWorkOrderId(workOrderId); if (CollUtil.isNotEmpty(tasks)) { List> taskData = new ArrayList<>(); long finishedTasks = 0; long totalDuration = 0; for (MesProTaskDO task : tasks) { Map t = new LinkedHashMap<>(); t.put("任务编号", task.getCode()); t.put("任务名称", task.getName()); t.put("任务状态", task.getStatus()); t.put("计划数量", task.getQuantity()); t.put("已生产数量", task.getProducedQuantity()); t.put("实际时长(小时)", task.getDuration()); if (task.getStartTime() != null) t.put("开始时间", task.getStartTime().toString()); if (task.getEndTime() != null) t.put("结束时间", task.getEndTime().toString()); if (task.getLastFeedbackTime() != null) t.put("最后报工时间", task.getLastFeedbackTime().toString()); taskData.add(t); if (task.getStatus() != null && task.getStatus() >= 3) finishedTasks++; if (task.getDuration() != null) totalDuration += task.getDuration(); } data.put("任务列表", taskData); data.put("已完成任务数", finishedTasks); data.put("总任务数", tasks.size()); data.put("已完成总时长(小时)", totalDuration); } // MPS 和客户信息 MesProMpsDO mps = mpsMapper.selectOne( new LambdaQueryWrapper() .eq(MesProMpsDO::getWorkOrderId, workOrderId)); if (mps != null) { data.put("MPS 编号", mps.getCode()); data.put("销售订单号", mps.getSaleOrderNo()); data.put("MPS 需求日期", mps.getRequestDate() != null ? mps.getRequestDate().toString() : ""); data.put("MPS 下发日期", mps.getIssueDate() != null ? mps.getIssueDate().toString() : ""); fillClientInfo(data, mps.getClientId()); } return data; } private MesProAiDeliveryPredictRespVO parseDeliveryResponse(String aiResponse) { MesProAiDeliveryPredictRespVO respVO = new MesProAiDeliveryPredictRespVO(); try { String json = extractJson(aiResponse); Map parsed = JSONUtil.toBean(json, Map.class); Object onTime = parsed.get("onTime"); if (onTime instanceof Boolean b) respVO.setOnTime(b); Object confidence = parsed.get("confidence"); if (confidence instanceof Number num) respVO.setConfidence(num.doubleValue()); respVO.setReasoning((String) parsed.get("reasoning")); @SuppressWarnings("unchecked") List delayFactors = (List) parsed.get("delayFactors"); respVO.setDelayFactors(delayFactors); respVO.setProgressPercent(toBigDecimal(parsed.get("progressPercent"))); respVO.setRemainingDays(toInteger(parsed.get("remainingDays"))); } catch (Exception e) { log.warn("解析 AI 交付预测响应失败: {}", aiResponse, e); respVO.setReasoning(aiResponse); } return respVO; } // ==================== Shared Utilities ==================== private String extractJson(String aiResponse) { int start = aiResponse.indexOf("{"); int end = aiResponse.lastIndexOf("}"); if (start >= 0 && end > start) { return aiResponse.substring(start, end + 1); } return aiResponse; } private BigDecimal toBigDecimal(Object value) { if (value == null) return BigDecimal.ZERO; if (value instanceof BigDecimal v) return v; if (value instanceof Number n) return BigDecimal.valueOf(n.doubleValue()); try { return new BigDecimal(value.toString()); } catch (Exception e) { return BigDecimal.ZERO; } } private Integer toInteger(Object value) { if (value == null) return 0; if (value instanceof Integer v) return v; if (value instanceof Number n) return n.intValue(); try { return Integer.valueOf(value.toString()); } catch (Exception e) { return 0; } } private String resolveItemName(Long itemId) { if (itemId == null) return ""; try { MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData(); if (item != null && StrUtil.isNotBlank(item.getName())) return item.getName(); } catch (Exception ignored) {} return "物料" + itemId; } private String resolveProcessName(Long processId) { if (processId == null) return ""; try { MesProProcessDO process = processService.getProcess(processId); if (process != null && StrUtil.isNotBlank(process.getName())) return process.getName(); } catch (Exception ignored) {} return "工序" + processId; } private void fillItemInfo(Map data, Long itemId) { if (itemId == null) return; try { MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData(); if (item != null) { data.put("物料编码", item.getCode() != null ? item.getCode() : itemId); data.put("物料名称", item.getName() != null ? item.getName() : ""); } } catch (Exception e) { data.put("物料ID", itemId); } } private void fillClientInfo(Map data, Long clientId) { if (clientId == null) return; try { CrmCustomerRespDTO customer = crmCustomerApi.getCustomer(clientId); if (customer != null) { data.put("客户名称", customer.getName() != null ? customer.getName() : ""); } } catch (Exception e) { data.put("客户ID", clientId); } } }