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<String, Object> 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<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 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) {
|
try {
|
Map<String, Object> 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<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 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) {
|
try {
|
Map<String, Object> 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<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 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) {
|
try {
|
Map<String, Object> 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<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 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 ====================
|
|
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);
|
}
|
}
|
|
}
|