xiaoyi
22 小时以前 8782476e9cb4480be5da588888d08022b7942d0c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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;
    }
}