liyong
2026-08-03 1d63fd149e20f6cfea9c081c7e80974550a1ea50
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java
@@ -30,6 +30,7 @@
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;
@@ -169,6 +170,8 @@
    @Resource
    private MesProMpsMapper mpsMapper;
    @Resource
    private MesProMpsService mpsService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MdmItemApi mdmItemApi;
@@ -182,12 +185,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 +269,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 +336,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 +440,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 +493,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;
@@ -560,6 +635,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 +704,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 +798,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 {