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.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;
|
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.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;
|
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.mps.MesProMpsService;
|
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.beans.factory.annotation.Autowired;
|
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 MesMdProductBomMapper productBomMapper;
|
@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 MesProMpsService mpsService;
|
@Resource
|
private MesProProcessService processService;
|
@Resource
|
private MdmItemApi mdmItemApi;
|
@Resource
|
private CrmCustomerApi crmCustomerApi;
|
@Autowired(required = false)
|
private AiChatApi aiChatApi;
|
|
// ==================== 1. 物料短缺预测 ====================
|
|
@Override
|
public MesProAiMaterialPredictRespVO predictMaterial(MesProAiMaterialPredictReqVO reqVO) {
|
try {
|
Map<String, Object> data;
|
String logPrefix;
|
if (reqVO.getMpsId() != null) {
|
data = gatherMaterialDataFromMps(reqVO.getMpsId());
|
logPrefix = "mpsId=" + reqVO.getMpsId();
|
} else {
|
data = gatherMaterialData(reqVO.getWorkOrderId());
|
logPrefix = "workOrderId=" + reqVO.getWorkOrderId();
|
}
|
String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_MATERIAL, JSONUtil.toJsonPrettyStr(data));
|
log.info("AI 物料预测: {}, response={}", logPrefix, aiResponse);
|
return parseMaterialResponse(aiResponse);
|
} catch (Exception e) {
|
log.warn("AI 物料预测失败,workOrderId={}, 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,或在「AI 大模型 - 模型管理」中配置可用的对话模型。");
|
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("本地物料核算失败,workOrderId={}, 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) {
|
Map<String, Object> 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<MesProWorkOrderBomDO> bomList = workOrderBomMapper.selectListByWorkOrderId(workOrderId);
|
if (CollUtil.isNotEmpty(bomList)) {
|
List<Map<String, Object>> materials = new ArrayList<>();
|
for (MesProWorkOrderBomDO bom : bomList) {
|
Map<String, Object> item = new LinkedHashMap<>();
|
item.put("物料ID", bom.getItemId());
|
String itemName = resolveItemName(bom.getItemId());
|
item.put("物料名称", itemName);
|
item.put("需求量", bom.getQuantity());
|
|
// 查询库存可用量
|
List<MesWmMaterialStockDO> stocks = materialStockMapper.selectList(
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesWmMaterialStockDO>()
|
.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<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
|
if (CollUtil.isNotEmpty(tasks)) {
|
List<Map<String, Object>> issuedMaterials = new ArrayList<>();
|
for (MesProTaskDO task : tasks) {
|
List<MesProTaskIssueDO> issues = taskIssueMapper.selectListByTaskId(task.getId());
|
for (MesProTaskIssueDO issue : issues) {
|
Map<String, Object> 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 Map<String, Object> gatherMaterialDataFromMps(Long mpsId) {
|
Map<String, Object> data = new LinkedHashMap<>();
|
MesProMpsDO mps = mpsService.getMps(mpsId);
|
if (mps == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS);
|
|
data.put("MPS 编号", mps.getCode());
|
data.put("来源", "MPS 未下发,基于产品信息预测");
|
data.put("计划数量", mps.getQuantity());
|
data.put("已生产数量", mps.getQuantityProduced() != null ? mps.getQuantityProduced() : BigDecimal.ZERO);
|
fillItemInfo(data, mps.getProductId());
|
|
// 查询产品在库物料库存(通用,非工单特定 BOM)
|
List<MesWmMaterialStockDO> stocks = materialStockMapper.selectList(
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesWmMaterialStockDO>()
|
.ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO));
|
if (CollUtil.isNotEmpty(stocks)) {
|
List<Map<String, Object>> materials = new ArrayList<>();
|
for (MesWmMaterialStockDO stock : stocks) {
|
Map<String, Object> item = new LinkedHashMap<>();
|
item.put("物料ID", stock.getItemId());
|
item.put("物料名称", resolveItemName(stock.getItemId()));
|
item.put("库存可用量", stock.getQuantity());
|
materials.add(item);
|
}
|
data.put("当前库存物料", materials);
|
} else {
|
data.put("提示", "暂无库存数据,请参考产品编码评估物料需求");
|
}
|
|
return data;
|
}
|
|
private MesProAiMaterialPredictRespVO parseMaterialResponse(String aiResponse) {
|
MesProAiMaterialPredictRespVO respVO = new MesProAiMaterialPredictRespVO();
|
try {
|
String json = extractJson(aiResponse);
|
Map<String, Object> 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<Map<String, Object>> items = (List<Map<String, Object>>) 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) {
|
Map<String, Object> data = null;
|
String logPrefix;
|
try {
|
if (reqVO.getMpsId() != null) {
|
data = gatherDurationDataFromMps(reqVO.getMpsId());
|
logPrefix = "mpsId=" + reqVO.getMpsId();
|
} else {
|
data = gatherDurationData(reqVO.getWorkOrderId());
|
logPrefix = "workOrderId=" + reqVO.getWorkOrderId();
|
}
|
String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_DURATION, JSONUtil.toJsonPrettyStr(data));
|
log.info("AI 时长预测: {}, response={}", logPrefix, aiResponse);
|
return parseDurationResponse(aiResponse);
|
} catch (Exception e) {
|
log.warn("AI 时长预测失败,workOrderId={}, 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 没有可用的工序或设备数据,无法估算生产时长。"
|
+ "请先维护工艺路线并完成排产,或在「AI 大模型 - 模型管理」中配置可用的对话模型。");
|
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("本地时长估算失败,workOrderId={}, 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);
|
}
|
}
|
|
private Map<String, Object> gatherDurationData(Long workOrderId) {
|
Map<String, Object> 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<MesProWorkOrderProcessDO> processes = workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId);
|
List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
|
Map<Long, MesProTaskDO> taskByProcessId = new HashMap<>();
|
if (CollUtil.isNotEmpty(tasks)) {
|
for (MesProTaskDO task : tasks) {
|
if (task.getProcessId() != null) {
|
taskByProcessId.put(task.getProcessId(), task);
|
}
|
}
|
}
|
|
List<Map<String, Object>> processList = new ArrayList<>();
|
BigDecimal standardTotalHours = BigDecimal.ZERO;
|
for (MesProWorkOrderProcessDO proc : processes) {
|
Map<String, Object> 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<MesMdWorkstationMachineDO> 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 Map<String, Object> gatherDurationDataFromMps(Long mpsId) {
|
Map<String, Object> data = new LinkedHashMap<>();
|
MesProMpsDO mps = mpsService.getMps(mpsId);
|
if (mps == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS);
|
|
data.put("MPS 编号", mps.getCode());
|
data.put("来源", "MPS 未下发,基于产品信息估算");
|
data.put("计划数量", mps.getQuantity());
|
data.put("已生产数量", mps.getQuantityProduced() != null ? mps.getQuantityProduced() : BigDecimal.ZERO);
|
data.put("需求日期", mps.getRequestDate() != null ? mps.getRequestDate().toString() : "未设置");
|
fillItemInfo(data, mps.getProductId());
|
data.put("提示", "MPS 未关联工单工序,请基于产品编码和经验估算生产时长");
|
|
return data;
|
}
|
|
private MesProAiDurationPredictRespVO parseDurationResponse(String aiResponse) {
|
MesProAiDurationPredictRespVO respVO = new MesProAiDurationPredictRespVO();
|
try {
|
String json = extractJson(aiResponse);
|
Map<String, Object> 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<String> keyPoints = (List<String>) parsed.get("keyPoints");
|
respVO.setKeyPoints(keyPoints);
|
@SuppressWarnings("unchecked")
|
List<Map<String, Object>> breakdown = (List<Map<String, Object>>) 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) {
|
Map<String, Object> data = null;
|
String logPrefix;
|
try {
|
if (reqVO.getMpsId() != null) {
|
data = gatherRiskDataFromMps(reqVO.getMpsId());
|
logPrefix = "mpsId=" + reqVO.getMpsId();
|
} else {
|
data = gatherRiskData(reqVO.getWorkOrderId());
|
logPrefix = "workOrderId=" + reqVO.getWorkOrderId();
|
}
|
String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_RISK, JSONUtil.toJsonPrettyStr(data));
|
log.info("AI 风险预测: {}, response={}", logPrefix, aiResponse);
|
return parseRiskResponse(aiResponse);
|
} catch (Exception e) {
|
log.warn("AI 风险预测失败,workOrderId={}, 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("本地风险评估失败,workOrderId={}, 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) {
|
Map<String, Object> 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<MesQcIpqcDO> ipqcList = ipqcMapper.selectList(
|
new LambdaQueryWrapper<MesQcIpqcDO>()
|
.eq(MesQcIpqcDO::getWorkOrderId, workOrderId));
|
if (CollUtil.isNotEmpty(ipqcList)) {
|
List<Map<String, Object>> qcData = ipqcList.stream().map(ipqc -> {
|
Map<String, Object> 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<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
|
if (CollUtil.isNotEmpty(tasks)) {
|
Set<Long> machineryIds = new HashSet<>();
|
for (MesProTaskDO task : tasks) {
|
if (task.getWorkstationId() != null) {
|
List<MesMdWorkstationMachineDO> wms = workstationMachineMapper.selectListByWorkstationId(task.getWorkstationId());
|
for (MesMdWorkstationMachineDO wm : wms) {
|
machineryIds.add(wm.getMachineryId());
|
}
|
}
|
}
|
if (!machineryIds.isEmpty()) {
|
List<Map<String, Object>> equipmentData = new ArrayList<>();
|
for (Long mid : machineryIds) {
|
MesDvMachineryDO machinery = machineryMapper.selectById(mid);
|
if (machinery != null) {
|
Map<String, Object> 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<MesProWorkOrderBomDO> bomList = workOrderBomMapper.selectListByWorkOrderId(workOrderId);
|
if (CollUtil.isNotEmpty(bomList)) {
|
List<Map<String, Object>> materialRisks = new ArrayList<>();
|
for (MesProWorkOrderBomDO bom : bomList) {
|
List<MesWmMaterialStockDO> stocks = materialStockMapper.selectList(
|
new LambdaQueryWrapper<MesWmMaterialStockDO>()
|
.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<String, Object> 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<MesProWorkOrderProcessDO> processes = workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId);
|
if (CollUtil.isNotEmpty(processes)) {
|
List<Map<String, Object>> keyProcesses = new ArrayList<>();
|
for (MesProWorkOrderProcessDO proc : processes) {
|
if (proc.getKeyFlag() != null && proc.getKeyFlag()) {
|
Map<String, Object> 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 Map<String, Object> gatherRiskDataFromMps(Long mpsId) {
|
Map<String, Object> data = new LinkedHashMap<>();
|
MesProMpsDO mps = mpsService.getMps(mpsId);
|
if (mps == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS);
|
|
data.put("MPS 编号", mps.getCode());
|
data.put("来源", "MPS 未下发,风险评估基于产品信息");
|
data.put("计划数量", mps.getQuantity());
|
data.put("已生产数量", mps.getQuantityProduced() != null ? mps.getQuantityProduced() : BigDecimal.ZERO);
|
data.put("需求日期", mps.getRequestDate() != null ? mps.getRequestDate().toString() : "未设置");
|
data.put("下发日期", mps.getIssueDate() != null ? mps.getIssueDate().toString() : "未下发");
|
fillItemInfo(data, mps.getProductId());
|
|
// 产品库存情况
|
List<MesWmMaterialStockDO> stocks = materialStockMapper.selectList(
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesWmMaterialStockDO>()
|
.ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO));
|
if (CollUtil.isNotEmpty(stocks)) {
|
List<Map<String, Object>> stockList = new ArrayList<>();
|
for (MesWmMaterialStockDO stock : stocks) {
|
Map<String, Object> item = new LinkedHashMap<>();
|
item.put("物料ID", stock.getItemId());
|
item.put("物料名称", resolveItemName(stock.getItemId()));
|
item.put("可用量", stock.getQuantity());
|
stockList.add(item);
|
}
|
data.put("库存物料", stockList);
|
}
|
|
data.put("提示", "MPS 未下发无工单数据,请结合产品特性和库存自行判断风险");
|
|
return data;
|
}
|
|
private MesProAiRiskPredictRespVO parseRiskResponse(String aiResponse) {
|
MesProAiRiskPredictRespVO respVO = new MesProAiRiskPredictRespVO();
|
try {
|
String json = extractJson(aiResponse);
|
Map<String, Object> 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<String> keyPoints = (List<String>) parsed.get("keyPoints");
|
respVO.setKeyPoints(keyPoints);
|
@SuppressWarnings("unchecked")
|
List<Map<String, Object>> risks = (List<Map<String, Object>>) 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) {
|
Map<String, Object> data = null;
|
String logPrefix;
|
try {
|
if (reqVO.getMpsId() != null) {
|
data = gatherDeliveryDataFromMps(reqVO.getMpsId());
|
logPrefix = "mpsId=" + reqVO.getMpsId();
|
} else {
|
data = gatherDeliveryData(reqVO.getWorkOrderId());
|
logPrefix = "workOrderId=" + reqVO.getWorkOrderId();
|
}
|
String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_DELIVERY, JSONUtil.toJsonPrettyStr(data));
|
log.info("AI 交付预测: {}, response={}", logPrefix, aiResponse);
|
return parseDeliveryResponse(aiResponse);
|
} catch (Exception e) {
|
log.warn("AI 交付预测失败,workOrderId={}, 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("本地交付评估失败,workOrderId={}, 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) {
|
Map<String, Object> 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<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
|
if (CollUtil.isNotEmpty(tasks)) {
|
List<Map<String, Object>> taskData = new ArrayList<>();
|
long finishedTasks = 0;
|
long totalDuration = 0;
|
for (MesProTaskDO task : tasks) {
|
Map<String, Object> 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<MesProMpsDO>()
|
.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 Map<String, Object> gatherDeliveryDataFromMps(Long mpsId) {
|
Map<String, Object> data = new LinkedHashMap<>();
|
MesProMpsDO mps = mpsService.getMps(mpsId);
|
if (mps == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS);
|
|
data.put("MPS 编号", mps.getCode());
|
data.put("来源", "MPS 未下发,基于产品信息评估");
|
data.put("计划数量", mps.getQuantity());
|
data.put("已生产数量", mps.getQuantityProduced() != null ? mps.getQuantityProduced() : BigDecimal.ZERO);
|
data.put("MPS 状态", mps.getStatus());
|
data.put("销售订单号", mps.getSaleOrderNo());
|
data.put("需求日期", mps.getRequestDate() != null ? mps.getRequestDate().toString() : "未设置");
|
data.put("下发日期", mps.getIssueDate() != null ? mps.getIssueDate().toString() : "未下发");
|
fillItemInfo(data, mps.getProductId());
|
fillClientInfo(data, mps.getClientId());
|
|
// 进度计算
|
BigDecimal progressPercent = BigDecimal.ZERO;
|
if (mps.getQuantity() != null && mps.getQuantity().compareTo(BigDecimal.ZERO) > 0
|
&& mps.getQuantityProduced() != null) {
|
progressPercent = mps.getQuantityProduced()
|
.divide(mps.getQuantity(), 4, RoundingMode.HALF_UP)
|
.multiply(BigDecimal.valueOf(100));
|
}
|
data.put("生产进度(%)", progressPercent.setScale(1, RoundingMode.HALF_UP));
|
|
// 剩余天数
|
int remainingDays = 0;
|
if (mps.getRequestDate() != null) {
|
remainingDays = (int) ChronoUnit.DAYS.between(LocalDate.now(), mps.getRequestDate().toLocalDate());
|
}
|
data.put("距离需求日期剩余天数", Math.max(0, remainingDays));
|
|
return data;
|
}
|
|
private MesProAiDeliveryPredictRespVO parseDeliveryResponse(String aiResponse) {
|
MesProAiDeliveryPredictRespVO respVO = new MesProAiDeliveryPredictRespVO();
|
try {
|
String json = extractJson(aiResponse);
|
Map<String, Object> 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<String> delayFactors = (List<String>) 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 ====================
|
|
@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("}");
|
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<String, Object> 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<String, Object> 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);
|
}
|
}
|
|
}
|