5 小时以前 b630f868ae5cb4bc2e92c48d275341d36d260499
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java
@@ -19,7 +19,6 @@
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;
@@ -28,8 +27,8 @@
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;
@@ -100,14 +99,13 @@
            你需要综合分析以下信息,给出专业的风险评估:
            ## 分析维度
            1. 质量风险:过程检验(IPQC)的缺陷率数据(致命/严重/轻微缺陷率)
            2. 设备风险:设备状态、上次保养时间、上次点检时间、生产效率
            3. 物料风险:物料库存可用量与需求量的对比
            4. 工艺瓶颈:关键工序的设备产能、任务排队情况
            1. 设备风险:设备状态、上次保养时间、上次点检时间、生产效率
            2. 物料风险:物料库存可用量与需求量的对比
            3. 工艺瓶颈:关键工序的设备产能、任务排队情况
            ## 风险等级定义
            - 高:存在致命缺陷率>5% 或 设备故障 或 关键物料短缺>50% 或 关键工序严重积压
            - 中:存在严重缺陷率>10% 或 设备长期未保养 或 物料紧张
            - 高:存在设备故障 或 关键物料短缺>50% 或 关键工序严重积压
            - 中:存在设备长期未保养 或 物料紧张
            - 低:各项指标正常
            ## 输出格式
@@ -117,7 +115,7 @@
            overallRiskLevel 取值:1=低风险 2=中风险 3=高风险
            riskLevel 取值:充足/紧张/短缺
            severity 取值:高/中/低
            category 取值:质量/设备/物料/工艺瓶颈
            category 取值:设备/物料/工艺瓶颈
            """;
    private static final String SYSTEM_PROMPT_DELIVERY = """
@@ -159,8 +157,6 @@
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesQcIpqcMapper ipqcMapper;
    @Resource
    private MesWmMaterialStockMapper materialStockMapper;
    @Resource
    private MesMdWorkstationMachineMapper workstationMachineMapper;
@@ -168,6 +164,8 @@
    private MesDvMachineryMapper machineryMapper;
    @Resource
    private MesProMpsMapper mpsMapper;
    @Resource
    private MesProMpsService mpsService;
    @Resource
    private MesProProcessService processService;
    @Resource
@@ -182,12 +180,20 @@
    @Override
    public MesProAiMaterialPredictRespVO predictMaterial(MesProAiMaterialPredictReqVO reqVO) {
        try {
            Map<String, Object> data = gatherMaterialData(reqVO.getWorkOrderId());
            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 物料预测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse);
            log.info("AI 物料预测: {}, response={}", logPrefix, aiResponse);
            return parseMaterialResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI 物料预测失败,workOrderId={}", reqVO.getWorkOrderId(), e);
            log.warn("AI 物料预测失败,workOrderId={}, mpsId={}", reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
            MesProAiMaterialPredictRespVO fallback = new MesProAiMaterialPredictRespVO();
            fallback.setReasoning("AI 分析暂时不可用,请稍后重试");
            return fallback;
@@ -258,6 +264,38 @@
        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 {
@@ -293,12 +331,20 @@
    @Override
    public MesProAiDurationPredictRespVO predictDuration(MesProAiDurationPredictReqVO reqVO) {
        try {
            Map<String, Object> data = gatherDurationData(reqVO.getWorkOrderId());
            Map<String, Object> data;
            String logPrefix;
            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 时长预测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse);
            log.info("AI 时长预测: {}, response={}", logPrefix, aiResponse);
            return parseDurationResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI 时长预测失败,workOrderId={}", reqVO.getWorkOrderId(), e);
            log.warn("AI 时长预测失败,workOrderId={}, mpsId={}", reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
            MesProAiDurationPredictRespVO fallback = new MesProAiDurationPredictRespVO();
            fallback.setReasoning("AI 分析暂时不可用,请稍后重试");
            return fallback;
@@ -389,6 +435,22 @@
        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 {
@@ -426,12 +488,20 @@
    @Override
    public MesProAiRiskPredictRespVO predictRisk(MesProAiRiskPredictReqVO reqVO) {
        try {
            Map<String, Object> data = gatherRiskData(reqVO.getWorkOrderId());
            Map<String, Object> data;
            String logPrefix;
            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 风险预测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse);
            log.info("AI 风险预测: {}, response={}", logPrefix, aiResponse);
            return parseRiskResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI 风险预测失败,workOrderId={}", reqVO.getWorkOrderId(), e);
            log.warn("AI 风险预测失败,workOrderId={}, mpsId={}", reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
            MesProAiRiskPredictRespVO fallback = new MesProAiRiskPredictRespVO();
            fallback.setReasoning("AI 分析暂时不可用,请稍后重试");
            return fallback;
@@ -448,26 +518,6 @@
        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);
@@ -560,6 +610,40 @@
        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 {
@@ -595,12 +679,20 @@
    @Override
    public MesProAiDeliveryPredictRespVO predictDelivery(MesProAiDeliveryPredictReqVO reqVO) {
        try {
            Map<String, Object> data = gatherDeliveryData(reqVO.getWorkOrderId());
            Map<String, Object> data;
            String logPrefix;
            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 交付预测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse);
            log.info("AI 交付预测: {}, response={}", logPrefix, aiResponse);
            return parseDeliveryResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI 交付预测失败,workOrderId={}", reqVO.getWorkOrderId(), e);
            log.warn("AI 交付预测失败,workOrderId={}, mpsId={}", reqVO.getWorkOrderId(), reqVO.getMpsId(), e);
            MesProAiDeliveryPredictRespVO fallback = new MesProAiDeliveryPredictRespVO();
            fallback.setReasoning("AI 分析暂时不可用,请稍后重试");
            return fallback;
@@ -681,6 +773,42 @@
        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 {