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;
|
|
/**
|
* 生产任务执行反馈分析。
|
*
|
* <p>把计划、报工、设备采集、物料消耗、交期偏差汇总,用于定位延期原因。
|
* 口径说明:
|
* <ul>
|
* <li>实际耗时 = 首次报工到末次报工(无报工则为空,不用计划值代替);</li>
|
* <li>延期判定 = 末次报工时间(或当前时间) 晚于计划完成时间;</li>
|
* <li>延期原因只列可观测信号(缺料、告警、设备离线、报工滞后),不推断管理原因。</li>
|
* </ul>
|
*/
|
@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<MesCollectRealtimeHistoryDO> history = historyMapper.selectList(
|
new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
|
.eq(MesCollectRealtimeHistoryDO::getTaskId, taskId));
|
List<MesProTaskExecutionRespVO.DeviceRun> deviceRuns = new ArrayList<>();
|
Map<Long, List<MesCollectRealtimeHistoryDO>> byMachinery = history.stream()
|
.filter(h -> h.getMachineryId() != null)
|
.collect(Collectors.groupingBy(MesCollectRealtimeHistoryDO::getMachineryId));
|
for (var e : byMachinery.entrySet()) {
|
List<MesCollectRealtimeHistoryDO> 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<MesCollectAlarmDO>()
|
.eq(MesCollectAlarmDO::getTaskId, taskId)
|
.eq(MesCollectAlarmDO::getMachineryId, e.getKey())));
|
deviceRuns.add(run);
|
}
|
resp.setDeviceRuns(deviceRuns);
|
|
// 物料消耗
|
List<MesProTaskIssueDO> issues = taskIssueMapper.selectList(
|
new LambdaQueryWrapperX<MesProTaskIssueDO>().eq(MesProTaskIssueDO::getTaskId, taskId));
|
Set<Long> itemIds = issues.stream().map(MesProTaskIssueDO::getItemId)
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
Map<Long, MesMdItemDO> itemMap = itemIds.isEmpty() ? Map.of()
|
: itemMapper.selectBatchIds(itemIds).stream()
|
.collect(Collectors.toMap(MesMdItemDO::getId, i -> i, (a, b) -> a));
|
Set<Long> unitIds = itemMap.values().stream().map(MesMdItemDO::getUnitMeasureId)
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
Map<Long, String> unitNameMap = unitIds.isEmpty() ? Map.of()
|
: unitMeasureMapper.selectBatchIds(unitIds).stream()
|
.collect(Collectors.toMap(MesMdUnitMeasureDO::getId, MesMdUnitMeasureDO::getName, (a, b) -> a));
|
Map<Long, MesProTaskExecutionRespVO.MaterialConsumption> 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<String> 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;
|
}
|
}
|