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 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() .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 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 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 tasks) { if (historicalDepth.get() > 0) return; Set 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 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 occupied = new HashSet<>(); if (tasks.isEmpty()) return occupied; var latest = new HashMap(); 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 affected) { if (affected.isEmpty()) return; Set 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() .eq(MesDvMachineryDO::getId, machine.getId()).eq(MesDvMachineryDO::getStatus, machine.getStatus()) .set(MesDvMachineryDO::getStatus, target)); if (changed == 0 && target == 2) fail("设备状态已变化,请重新核对设备后提交"); } } private Set ids(String csv) { if (csv == null || csv.isBlank()) return Set.of(); try { Set 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); } }