package cn.iocoder.yudao.module.mes.service.pro.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO; import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskAssigneeMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper; import cn.iocoder.yudao.module.mes.dal.redis.pro.task.MesProTaskWaitRemindRedisDAO; import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService; import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.Duration; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; /** * MES 生产任务报工倒计时提醒 Service 实现类 * * @author 超级管理员 */ @Slf4j @Service public class MesProTaskWaitRemindServiceImpl implements MesProTaskWaitRemindService { /** * 三个提醒等级对应的站内信模板编号 */ private static final String[] REMIND_TEMPLATE_CODES = { "mes_task_wait_remind_10", // 等级 1:剩余 10 分钟 "mes_task_wait_remind_5", // 等级 2:剩余 5 分钟 "mes_task_wait_remind_end" // 等级 3:倒计时结束 }; @Resource private MesProTaskMapper taskMapper; @Resource private MesProTaskAssigneeMapper taskAssigneeMapper; @Resource private MesProProcessService processService; @Resource private MesProTaskWaitRemindRedisDAO waitRemindRedisDAO; @Resource private NotifyMessageSendApi notifyMessageSendApi; @Override public void sendWaitRemind() { try { // 1. 抢扫描门闩,避免多实例/多次并发重复扫描 if (!waitRemindRedisDAO.tryAcquireScanLock()) { return; } // 2. 查询已开始倒计时且未终态/未停工的任务 List taskList = taskMapper.selectListByWaitStartTimeNotNull(); if (CollUtil.isEmpty(taskList)) { return; } // 3. 按任务编号批量查询报工人员 Map> assigneeMap = convertMultiMap( taskAssigneeMapper.selectListByTaskIds( taskList.stream().map(MesProTaskDO::getId).collect(Collectors.toSet())), MesProTaskAssigneeDO::getTaskId); // 4. 逐任务按三个等级发送提醒 for (MesProTaskDO task : taskList) { try { processTaskRemind(task, assigneeMap.get(task.getId())); } catch (Exception ex) { // best-effort:单个任务提醒失败不影响其余任务 log.error("[sendWaitRemind][任务 {} 发送倒计时提醒失败]", task.getId(), ex); } } } catch (Exception ex) { // best-effort:提醒失败不影响其他流程 log.error("[sendWaitRemind][发送报工倒计时提醒失败]", ex); } } private void processTaskRemind(MesProTaskDO task, List assigneeList) { // 1. 取工序等待时长,未配置则无需倒计时提醒 MesProProcessDO process = processService.getProcess(task.getProcessId()); if (process == null || process.getWaitDuration() == null || process.getWaitDuration().compareTo(BigDecimal.ZERO) <= 0) { return; } // 2. 计算倒计时结束时间与剩余分钟 LocalDateTime waitStartTime = task.getWaitStartTime(); LocalDateTime readyTime = waitStartTime .plusMinutes((long) (process.getWaitDuration().doubleValue() * 60)); long remainingMinutes = Duration.between(LocalDateTime.now(), readyTime).toMinutes(); // 可为负 // 3. 三个等级独立判定,Redis 去重(首次标记成功才发送) sendRemindIfDue(task, process, assigneeList, waitStartTime, 1, remainingMinutes <= 10); sendRemindIfDue(task, process, assigneeList, waitStartTime, 2, remainingMinutes <= 5); sendRemindIfDue(task, process, assigneeList, waitStartTime, 3, remainingMinutes <= 0); } private void sendRemindIfDue(MesProTaskDO task, MesProProcessDO process, List assigneeList, LocalDateTime waitStartTime, int level, boolean due) { if (!due) { return; } if (!waitRemindRedisDAO.tryMarkReminded(task.getId(), level, waitStartTime)) { return; // 该等级已提醒过 } // 组装模板参数 Map params = new HashMap<>(); params.put("taskCode", task.getCode()); params.put("taskName", task.getName()); params.put("processName", process.getName()); // 接收人:优先报工人员;未指定则用任务创建人兜底 List receiverUserIds = assigneeList == null ? null : assigneeList.stream().map(MesProTaskAssigneeDO::getUserId).collect(Collectors.toList()); if (CollUtil.isEmpty(receiverUserIds)) { Long creatorUserId = parseCreatorUserId(task.getCreator()); if (creatorUserId == null) { return; } receiverUserIds = List.of(creatorUserId); } for (Long userId : receiverUserIds) { NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO() .setUserId(userId) .setTemplateCode(REMIND_TEMPLATE_CODES[level - 1]) .setTemplateParams(params); notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO); } } private Long parseCreatorUserId(String creator) { if (StrUtil.isBlank(creator)) { return null; } try { return Long.valueOf(creator.trim()); } catch (NumberFormatException ex) { return null; } } }