package cn.iocoder.yudao.module.mes.service.pro.feedback; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProTaskExecutionRespVO; import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectAlarmDO; import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeHistoryDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO; 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.mysql.collect.MesCollectAlarmMapper; import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectRealtimeHistoryMapper; import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper; import cn.iocoder.yudao.module.mes.dal.mysql.md.unitmeasure.MesMdUnitMeasureMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskIssueMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper; 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.Duration; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; /** * 生产任务执行反馈分析。 * *

把计划、报工、设备采集、物料消耗、交期偏差汇总,用于定位延期原因。 * 口径说明: *

*/ @Slf4j @Service public class MesProTaskExecutionService { @Resource private MesProTaskMapper taskMapper; @Resource private MesProTaskIssueMapper taskIssueMapper; @Resource private MesCollectRealtimeHistoryMapper historyMapper; @Resource private MesCollectAlarmMapper alarmMapper; @Resource private MesMdItemMapper itemMapper; @Resource private MesMdUnitMeasureMapper unitMeasureMapper; public MesProTaskExecutionRespVO getExecution(Long taskId) { MesProTaskDO task = taskId == null ? null : taskMapper.selectById(taskId); if (task == null) throw invalidParamException("生产任务不存在"); var resp = new MesProTaskExecutionRespVO(); resp.setTaskId(task.getId()); resp.setTaskCode(task.getCode()); resp.setTaskName(task.getName()); resp.setWorkOrderId(task.getWorkOrderId()); resp.setStatus(task.getStatus()); resp.setPlannedQuantity(task.getQuantity()); resp.setProducedQuantity(task.getProducedQuantity()); resp.setQualifyQuantity(task.getQualifyQuantity()); resp.setUnqualifyQuantity(task.getUnqualifyQuantity()); resp.setStartTime(task.getStartTime()); resp.setFirstFeedbackTime(task.getFirstFeedbackTime()); resp.setLastFeedbackTime(task.getLastFeedbackTime()); resp.setFinishDate(task.getFinishDate()); resp.setPlannedDuration(task.getDuration()); // 完成率 if (task.getQuantity() != null && task.getQuantity().signum() > 0 && task.getProducedQuantity() != null) { resp.setCompletionRate(task.getProducedQuantity() .multiply(BigDecimal.valueOf(100)) .divide(task.getQuantity(), 2, RoundingMode.HALF_UP)); } // 实际耗时 if (task.getFirstFeedbackTime() != null && task.getLastFeedbackTime() != null) { resp.setActualDuration(Duration.between(task.getFirstFeedbackTime(), task.getLastFeedbackTime()).toMinutes()); } // 设备运行数据 List history = historyMapper.selectList( new LambdaQueryWrapperX() .eq(MesCollectRealtimeHistoryDO::getTaskId, taskId)); List deviceRuns = new ArrayList<>(); Map> byMachinery = history.stream() .filter(h -> h.getMachineryId() != null) .collect(Collectors.groupingBy(MesCollectRealtimeHistoryDO::getMachineryId)); for (var e : byMachinery.entrySet()) { List list = e.getValue(); var run = new MesProTaskExecutionRespVO.DeviceRun(); run.setMachineryId(e.getKey()); run.setDeviceCode(list.get(0).getDeviceCode()); run.setSampleCount((long) list.size()); run.setTotalRunTime(list.stream().map(MesCollectRealtimeHistoryDO::getOt) .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); MesCollectRealtimeHistoryDO last = list.stream() .max(Comparator.comparing(MesCollectRealtimeHistoryDO::getCollectTime, Comparator.nullsFirst(Comparator.naturalOrder()))).orElse(null); if (last != null) { run.setLastCollectTime(last.getCollectTime()); run.setLastRunStatus(last.getSts()); } run.setAlarmCount(alarmMapper.selectCount(new LambdaQueryWrapperX() .eq(MesCollectAlarmDO::getTaskId, taskId) .eq(MesCollectAlarmDO::getMachineryId, e.getKey()))); deviceRuns.add(run); } resp.setDeviceRuns(deviceRuns); // 物料消耗 List issues = taskIssueMapper.selectList( new LambdaQueryWrapperX().eq(MesProTaskIssueDO::getTaskId, taskId)); Set itemIds = issues.stream().map(MesProTaskIssueDO::getItemId) .filter(Objects::nonNull).collect(Collectors.toSet()); Map itemMap = itemIds.isEmpty() ? Map.of() : itemMapper.selectBatchIds(itemIds).stream() .collect(Collectors.toMap(MesMdItemDO::getId, i -> i, (a, b) -> a)); Set unitIds = itemMap.values().stream().map(MesMdItemDO::getUnitMeasureId) .filter(Objects::nonNull).collect(Collectors.toSet()); Map unitNameMap = unitIds.isEmpty() ? Map.of() : unitMeasureMapper.selectBatchIds(unitIds).stream() .collect(Collectors.toMap(MesMdUnitMeasureDO::getId, MesMdUnitMeasureDO::getName, (a, b) -> a)); Map consumptionMap = new LinkedHashMap<>(); for (MesProTaskIssueDO issue : issues) { if (issue.getItemId() == null) continue; var mc = consumptionMap.computeIfAbsent(issue.getItemId(), id -> { var item = new MesProTaskExecutionRespVO.MaterialConsumption(); item.setItemId(id); MesMdItemDO md = itemMap.get(id); if (md != null) { item.setItemCode(md.getCode()); item.setItemName(md.getName()); if (md.getUnitMeasureId() != null) item.setUnit(unitNameMap.get(md.getUnitMeasureId())); } item.setQuantity(BigDecimal.ZERO); return item; }); BigDecimal used = issue.getUsedQuantity() != null ? issue.getUsedQuantity() : (issue.getIssuedQuantity() != null ? issue.getIssuedQuantity() : BigDecimal.ZERO); mc.setQuantity(mc.getQuantity().add(used)); } resp.setMaterialConsumptions(new ArrayList<>(consumptionMap.values())); // 交期偏差 boolean delayed = false; Long delayDays = null; LocalDateTime reference = task.getLastFeedbackTime() != null ? task.getLastFeedbackTime() : LocalDateTime.now(); if (task.getFinishDate() != null && reference.isAfter(task.getFinishDate())) { delayed = true; delayDays = Duration.between(task.getFinishDate(), reference).toDays(); } resp.setDelayed(delayed); resp.setDelayDays(delayDays); // 延期原因(可观测信号) List causes = new ArrayList<>(); if (delayed) { if (deviceRuns.stream().anyMatch(r -> r.getAlarmCount() != null && r.getAlarmCount() > 0)) causes.add("执行期间存在设备告警,可能影响进度"); if (task.getFirstFeedbackTime() == null) causes.add("任务尚未首次报工,实际开工时间缺失"); else if (task.getStartTime() != null && task.getFirstFeedbackTime().isAfter(task.getStartTime().plusDays(1))) causes.add("首次报工时间明显晚于计划开始时间"); boolean hasShortage = issues.stream().anyMatch(i -> i.getIssuedQuantity() != null && i.getUsedQuantity() != null && i.getUsedQuantity().compareTo(i.getIssuedQuantity()) >= 0); if (hasShortage) causes.add("投料量已用尽,存在缺料停工可能"); if (deviceRuns.isEmpty()) causes.add("无设备采集数据,无法判断设备侧影响"); if (task.getDuration() != null && resp.getActualDuration() != null && resp.getActualDuration() > task.getDuration()) causes.add(String.format("实际耗时 %d 分钟超过计划 %d 分钟", resp.getActualDuration(), task.getDuration())); } resp.setDelayCauses(causes); return resp; } }