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;
}
}