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