云
7 小时以前 3ff6604c75c887f4056ee03545d204bc9323286c
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package cn.iocoder.yudao.module.mes.service.pro.feedback;
 
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.task.schedule.MesProScheduleGuardService;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
 
import java.time.*;
import java.util.*;
import java.util.stream.Collectors;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_FEEDBACK_EQUIPMENT_INVALID;
 
/** 报工与设备状态联动;调用入口必须处于事务中,与排产共用资源锁。 */
@Service
public class MesProFeedbackEquipmentService {
    @Resource private MesProScheduleGuardService guard;
    @Resource private MesProTaskMapper taskMapper;
    @Resource private MesProFeedbackMapper feedbackMapper;
    @Resource private MesDvMachineryMapper machineryMapper;
    @Resource private MesMdWorkstationService workstationService;
    private Clock clock = Clock.system(ZoneId.of("Asia/Shanghai"));
    private final ThreadLocal<Integer> historicalDepth = ThreadLocal.withInitial(() -> 0);
 
    public interface ApprovalScope extends AutoCloseable { @Override void close(); }
 
    /** 历史报工可能触发整单自动完工,整个调用链都不改写实时设备状态。 */
    public ApprovalScope beginApproval(MesProFeedbackDO feedback) {
        if (!historical(feedback)) return () -> { };
        int previous = historicalDepth.get();
        historicalDepth.set(previous + 1);
        return () -> {
            if (previous == 0) historicalDepth.remove(); else historicalDepth.set(previous);
        };
    }
 
    public void lock() { guard.lock(); }
    public void resetSnapshot(Long id) {
        feedbackMapper.update(null, new LambdaUpdateWrapper<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, id).set(MesProFeedbackDO::getMachineryIds, null));
    }
    private LocalDateTime now() { return LocalDateTime.now(clock); }
    private boolean historical(MesProFeedbackDO f) {
        return f.getFeedbackTime() != null && f.getFeedbackTime().toLocalDate().isBefore(now().toLocalDate());
    }
 
    /** 草稿保存不校验实时设备状态,不切换设备;提交时才固定设备快照。 */
    public void validateSubmission(MesProFeedbackDO feedback) { validate(feedback, true); }
 
    /** 审批有延迟时,不用审批时的维护状态否认已提交的生产事实。 */
    public void validateApproval(MesProFeedbackDO feedback) { validate(feedback, feedback.getMachineryIds() == null); }
 
    private void validate(MesProFeedbackDO feedback, boolean checkCurrentEquipment) {
        var task = taskMapper.selectById(feedback.getTaskId());
        if (task == null || task.getStatus() == null || !MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            fail("任务不存在、已结束或已停工");
        }
        if (!Objects.equals(task.getWorkstationId(), feedback.getWorkstationId())
                || !Objects.equals(task.getWorkOrderId(), feedback.getWorkOrderId())
                || !Objects.equals(task.getProcessId(), feedback.getProcessId())
                || !Objects.equals(task.getRouteId(), feedback.getRouteId())) {
            fail("任务关联已变更,请重新编辑报工单");
        }
        if (feedback.getFeedbackTime() == null || feedback.getFeedbackTime().isAfter(now().plusMinutes(5))) {
            fail("请填写实际报工时间,不能提前报工");
        }
        // 补录往日生产不校验当前维护状态,也不补造历史使用设备,不改变实时设备状态。
        if (historical(feedback) || !checkCurrentEquipment) return;
        var station = task.getWorkstationId() == null ? null : workstationService.getWorkstation(task.getWorkstationId());
        if (station == null || !Objects.equals(station.getStatus(), 0)
                || !Objects.equals(station.getProcessId(), task.getProcessId())) {
            fail("工作站不存在、已停用或与任务工序不符");
        }
        if (feedback.getMachineryIds() == null) {
            String existing = taskSnapshot(task.getId());
            feedback.setMachineryIds(existing != null ? existing : availableMachines(task.getWorkstationId()));
        }
        Set<Long> ids = ids(feedback.getMachineryIds());
        if (ids.isEmpty()) fail("工作站未配置设备,请先绑定设备");
        var machines = machineryMapper.selectByIds(ids);
        if (machines.size() != ids.size()) fail("关联设备已删除,请核对报工单");
        for (var machine : machines) {
            if (!Objects.equals(machine.getStatus(), 1) && !Objects.equals(machine.getStatus(), 2)) {
                fail("设备「" + machine.getName() + "」维护中或状态无效");
            }
        }
        Set<Long> occupied = occupiedMachines(task.getId(), true);
        if (!Collections.disjoint(occupied, ids)) fail("关联设备仍被其他生产任务占用,请先完工或停工后再报工");
        if (feedback.getId() != null) {
            feedbackMapper.updateById(new MesProFeedbackDO().setId(feedback.getId()).setMachineryIds(feedback.getMachineryIds()));
        }
    }
 
    public void afterApproved(MesProFeedbackDO feedback) {
        var task = taskMapper.selectById(feedback.getTaskId());
        if (task != null && task.getQuantity() != null && task.getQuantity().signum() > 0
                && task.getProducedQuantity() != null && task.getProducedQuantity().compareTo(task.getQuantity()) >= 0
                && !MesProTaskStatusEnum.isEndStatus(task.getStatus())) {
            // 单道工序报满即可释放生产资源,质检仍独立保持待检验状态。
            taskMapper.updateById(new MesProTaskDO().setId(task.getId()).setStatus(MesProTaskStatusEnum.FINISHED.getStatus())
                    .setFinishDate(feedback.getFeedbackTime()));
        } else if (!historical(feedback) && task != null && remaining(task)
                && (Objects.equals(task.getStatus(), MesProTaskStatusEnum.PREPARE.getStatus())
                    || Objects.equals(task.getStatus(), MesProTaskStatusEnum.PRODUCING.getStatus()))) {
            taskMapper.updateById(new MesProTaskDO().setId(task.getId()).setStatus(MesProTaskStatusEnum.PARTIAL_FINISHED.getStatus()));
        }
        if (historical(feedback)) return;
        refresh(ids(feedback.getMachineryIds()));
    }
 
    /** 变更任务状态后重算共享设备;快照不随工作站重新绑定设备而漂移。 */
    public void afterTaskStateChanged(Collection<MesProTaskDO> tasks) {
        if (historicalDepth.get() > 0) return;
        Set<Long> affected = new TreeSet<>();
        var mapping = guard.machineIdsByStation();
        for (var task : tasks) {
            boolean hasSnapshot = false;
            for (var f : feedbackMapper.selectListByTaskId(task.getId())) {
                if (Objects.equals(f.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())
                        || Objects.equals(f.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
                    hasSnapshot |= f.getMachineryIds() != null && !f.getMachineryIds().isBlank();
                    affected.addAll(ids(f.getMachineryIds()));
                }
            }
            if (!hasSnapshot && (task.getFirstFeedbackTime() != null
                    || Objects.equals(task.getStatus(), MesProTaskStatusEnum.PRODUCING.getStatus())
                    || Objects.equals(task.getStatus(), MesProTaskStatusEnum.PARTIAL_FINISHED.getStatus())
                    || (Objects.equals(task.getStatus(), MesProTaskStatusEnum.PAUSED.getStatus())
                        && task.getProducedQuantity() != null && task.getProducedQuantity().signum() > 0))) {
                affected.addAll(mapping.getOrDefault(task.getWorkstationId(), Set.of()));
            }
        }
        refresh(affected);
    }
 
    /** 复工之前先校验实时设备,避免恢复维护中或其他任务占用的设备。 */
    public void validateResume(MesProTaskDO task) {
        var f = new MesProFeedbackDO().setTaskId(task.getId()).setWorkOrderId(task.getWorkOrderId())
                .setRouteId(task.getRouteId()).setProcessId(task.getProcessId()).setWorkstationId(task.getWorkstationId())
                .setFeedbackTime(now());
        // 调用方先恢复任务状态,校验失败时同事务回滚。
        validateSubmission(f);
    }
 
    private boolean remaining(MesProTaskDO t) {
        return t.getProducedQuantity() != null && t.getProducedQuantity().signum() > 0
                && t.getQuantity() != null && t.getProducedQuantity().compareTo(t.getQuantity()) < 0;
    }
 
    private boolean unfinished(MesProTaskDO t) {
        return t.getQuantity() == null || (t.getQuantity().signum() > 0
                && (t.getProducedQuantity() == null || t.getProducedQuantity().compareTo(t.getQuantity()) < 0));
    }
 
    private String taskSnapshot(Long taskId) {
        return feedbackMapper.selectListByTaskId(taskId).stream()
                .filter(f -> f.getMachineryIds() != null && (Objects.equals(f.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())
                        || Objects.equals(f.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())))
                .max(Comparator.comparing(MesProFeedbackDO::getId)).map(MesProFeedbackDO::getMachineryIds).orElse(null);
    }
 
    private String availableMachines(Long stationId) {
        var assigned = guard.machineIdsByStation().getOrDefault(stationId, Set.of());
        if (assigned.isEmpty()) fail("工作站未配置设备,请先绑定设备");
        // 与排产一致:同一工作站可有多台设备,维护中的设备不加入新的报工范围。
        // 已经提交过的任务使用原设备快照,其设备转为维护时仍必须拦截。
        var usable = machineryMapper.selectByIds(assigned).stream()
                .filter(m -> Objects.equals(m.getStatus(), 1) || Objects.equals(m.getStatus(), 2))
                .map(MesDvMachineryDO::getId).sorted().map(String::valueOf).collect(Collectors.joining(","));
        if (usable.isEmpty()) fail("工作站没有可用设备,关联设备维护中、已删除或状态无效");
        return usable;
    }
 
    private Set<Long> occupiedMachines(Long exceptTaskId, boolean includePending) {
        var tasks = taskMapper.selectSchedulingTasks().stream()
                .filter(t -> !Objects.equals(t.getId(), exceptTaskId))
                .filter(t -> !Objects.equals(t.getStatus(), MesProTaskStatusEnum.PAUSED.getStatus()))
                .filter(this::unfinished).toList();
        Set<Long> occupied = new HashSet<>();
        if (tasks.isEmpty()) return occupied;
        var latest = new HashMap<Long, MesProFeedbackDO>();
        for (var f : feedbackMapper.selectListByTaskIds(tasks.stream().map(MesProTaskDO::getId).toList())) {
            if (!Objects.equals(f.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())
                    && !Objects.equals(f.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())
                    && !(includePending && Objects.equals(f.getStatus(), MesProFeedbackStatusEnum.APPROVING.getStatus())
                        && f.getFeedbackTime() != null && !historical(f))) continue;
            latest.merge(f.getTaskId(), f, (a,b) -> a.getId() > b.getId() ? a : b);
        }
        var mapping = guard.machineIdsByStation();
        for (var t : tasks) {
            var f = latest.get(t.getId());
            boolean active = Objects.equals(t.getStatus(), MesProTaskStatusEnum.PRODUCING.getStatus())
                    || Objects.equals(t.getStatus(), MesProTaskStatusEnum.PARTIAL_FINISHED.getStatus())
                    || (f != null && f.getFeedbackTime() != null && !historical(f));
            if (active) occupied.addAll(f != null && f.getMachineryIds() != null
                    ? ids(f.getMachineryIds()) : mapping.getOrDefault(t.getWorkstationId(), Set.of()));
        }
        return occupied;
    }
 
    private void refresh(Set<Long> affected) {
        if (affected.isEmpty()) return;
        Set<Long> occupied = occupiedMachines(null, false);
        for (var machine : machineryMapper.selectByIds(affected)) {
            if (!Objects.equals(machine.getStatus(), 1) && !Objects.equals(machine.getStatus(), 2)) continue;
            int target = occupied.contains(machine.getId()) ? 2 : 1;
            if (Objects.equals(machine.getStatus(), target)) continue;
            int changed = machineryMapper.update(null, new LambdaUpdateWrapper<MesDvMachineryDO>()
                    .eq(MesDvMachineryDO::getId, machine.getId()).eq(MesDvMachineryDO::getStatus, machine.getStatus())
                    .set(MesDvMachineryDO::getStatus, target));
            if (changed == 0 && target == 2) fail("设备状态已变化,请重新核对设备后提交");
        }
    }
 
    private Set<Long> ids(String csv) {
        if (csv == null || csv.isBlank()) return Set.of();
        try {
            Set<Long> ids = Arrays.stream(csv.split(",")).map(String::trim).map(Long::valueOf)
                    .collect(Collectors.toCollection(TreeSet::new));
            if (ids.stream().anyMatch(id -> id <= 0)) fail("设备快照包含无效编号");
            return ids;
        } catch (NumberFormatException error) { throw exception(PRO_FEEDBACK_EQUIPMENT_INVALID, "设备快照格式异常"); }
    }
    private void fail(String message) { throw exception(PRO_FEEDBACK_EQUIPMENT_INVALID, message); }
}