From 836e9a0162d9fc9b43e5f2168f45f37e63e43db3 Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Tue, 10 Mar 2026 14:51:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=82=AC=E5=8A=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../erf/controller/ErfExpApplyController.java | 30 ++ .../service/ErfApprovalReminderService.java | 10 + .../impl/ErfApprovalReminderServiceImpl.java | 335 +++++++++++++++++- 3 files changed, 372 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/xujie/sys/modules/erf/controller/ErfExpApplyController.java b/src/main/java/com/xujie/sys/modules/erf/controller/ErfExpApplyController.java index 2f0c1778..0b803d6a 100644 --- a/src/main/java/com/xujie/sys/modules/erf/controller/ErfExpApplyController.java +++ b/src/main/java/com/xujie/sys/modules/erf/controller/ErfExpApplyController.java @@ -7,6 +7,7 @@ import com.xujie.sys.modules.erf.data.ErfFlowApprovalData; import com.xujie.sys.modules.erf.data.ErfFlowStatusData; import com.xujie.sys.modules.erf.data.ErfPlannerScheduleData; import com.xujie.sys.modules.erf.entity.ErfFlowApproveLog; +import com.xujie.sys.modules.erf.service.ErfApprovalReminderService; import com.xujie.sys.modules.erf.service.ErfExpApplyService; import com.xujie.sys.modules.erf.service.ErfFieldAuthService; import com.xujie.sys.modules.sys.controller.AbstractController; @@ -35,6 +36,9 @@ public class ErfExpApplyController extends AbstractController { @Autowired private ErfFieldAuthService erfFieldAuthService; + @Autowired + private ErfApprovalReminderService erfApprovalReminderService; + /** * 查询申请单列表 * @@ -417,4 +421,30 @@ public class ErfExpApplyController extends AbstractController { return R.error("录入失败: " + e.getMessage()); } } + + /** + * 手动催办:向指定申请单所有未确认的审批人(技术经理、生产经理、质量经理、计划员)发送催办邮件 + * + *

不含三方确认人员

+ * + * @param data 包含applyNo的请求数据 + * @return 操作结果,包含实际发送邮件人数 + */ + @PostMapping("/urgeApproval") + @ResponseBody + public R urgeApproval(@RequestBody ErfExpApplyData data) { + try { + if (data.getApplyNo() == null || data.getApplyNo().trim().isEmpty()) { + return R.error("申请单号不能为空"); + } + int sentCount = erfApprovalReminderService.sendManualUrgeEmail(data.getApplyNo().trim()); + if (sentCount == 0) { + return R.ok("当前没有未确认的审批人,无需催办"); + } + return R.ok(String.format("催办邮件已发送,共通知 %d 位审批人", sentCount)); + } catch (Exception e) { + log.error("催办失败: " + e.getMessage(), e); + return R.error("催办失败: " + e.getMessage()); + } + } } diff --git a/src/main/java/com/xujie/sys/modules/erf/service/ErfApprovalReminderService.java b/src/main/java/com/xujie/sys/modules/erf/service/ErfApprovalReminderService.java index 9c9eb80c..b6b605e8 100644 --- a/src/main/java/com/xujie/sys/modules/erf/service/ErfApprovalReminderService.java +++ b/src/main/java/com/xujie/sys/modules/erf/service/ErfApprovalReminderService.java @@ -35,4 +35,14 @@ public interface ErfApprovalReminderService { *

提醒生产、质量、技术部门有待确认的工序

*/ void sendTriConfirmReminder(); + + /** + * 手动催办:向指定申请单所有未确认的审批人发送催办邮件 + * + *

催办范围:技术经理、生产经理、质量经理、计划员(不含三方确认人员)

+ * + * @param applyNo 申请单号 + * @return 实际发送催办邮件的人数 + */ + int sendManualUrgeEmail(String applyNo); } diff --git a/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfApprovalReminderServiceImpl.java b/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfApprovalReminderServiceImpl.java index 349d8bb5..760cd0cd 100644 --- a/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfApprovalReminderServiceImpl.java +++ b/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfApprovalReminderServiceImpl.java @@ -1,14 +1,18 @@ package com.xujie.sys.modules.erf.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.xujie.sys.common.utils.MailUtil; import com.xujie.sys.modules.erf.entity.ErfExpApply; import com.xujie.sys.modules.erf.entity.ErfExpTriConfirm; import com.xujie.sys.modules.erf.entity.ErfExpTriConfirmDetail; +import com.xujie.sys.modules.erf.entity.ErfFlowInstance; import com.xujie.sys.modules.erf.entity.ErfFlowNodeInstance; import com.xujie.sys.modules.erf.mapper.ErfExpApplyMapper; import com.xujie.sys.modules.erf.mapper.ErfExpTriConfirmDetailMapper; import com.xujie.sys.modules.erf.mapper.ErfExpTriConfirmMapper; +import com.xujie.sys.modules.erf.mapper.ErfFlowInstanceMapper; import com.xujie.sys.modules.erf.mapper.ErfFlowNodeInstanceMapper; import com.xujie.sys.modules.erf.service.ErfApprovalReminderService; import com.xujie.sys.modules.pms.data.MailSendAddressData; @@ -39,6 +43,9 @@ public class ErfApprovalReminderServiceImpl implements ErfApprovalReminderServic @Autowired private ErfFlowNodeInstanceMapper erfFlowNodeInstanceMapper; + @Autowired + private ErfFlowInstanceMapper erfFlowInstanceMapper; + @Autowired private ErfExpApplyMapper erfExpApplyMapper; @@ -54,6 +61,11 @@ public class ErfApprovalReminderServiceImpl implements ErfApprovalReminderServic @Autowired private ErfExpTriConfirmMapper erfExpTriConfirmMapper; + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + private static final String APPROVAL_PAGE_URL = "http://172.26.68.20:9001/#/erf-expApplyApproval"; + private static final String PLANNER_PAGE_URL = "http://172.26.68.20:9001/#/erf-plannerSchedule"; + @Value("${erf.reminder.manager.enabled:false}") private boolean managerReminderEnabled; @@ -475,7 +487,7 @@ public class ErfApprovalReminderServiceImpl implements ErfApprovalReminderServic emailBody.append(""); emailBody.append(""); emailBody.append("
"); - emailBody.append("前往查看"); + emailBody.append("前往查看"); emailBody.append("

发送时间:").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("

"); emailBody.append(""); @@ -548,7 +560,7 @@ public class ErfApprovalReminderServiceImpl implements ErfApprovalReminderServic emailBody.append(""); emailBody.append(""); emailBody.append("
"); - emailBody.append("前往查看"); + emailBody.append("前往查看"); emailBody.append("

发送时间:").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("

"); emailBody.append(""); @@ -639,7 +651,7 @@ public class ErfApprovalReminderServiceImpl implements ErfApprovalReminderServic emailBody.append(""); } - emailBody.append("前往查看"); + emailBody.append("前往三方确认页面"); emailBody.append("

发送时间:").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("

"); emailBody.append(""); @@ -708,6 +720,323 @@ public class ErfApprovalReminderServiceImpl implements ErfApprovalReminderServic } } + @Override + public int sendManualUrgeEmail(String applyNo) { + log.info("=== 开始手动催办 === 申请单: {}", applyNo); + + // 1. 查询申请单基本信息 + ErfExpApply apply = erfExpApplyMapper.selectOne( + new QueryWrapper().eq("apply_no", applyNo)); + if (apply == null) { + throw new RuntimeException("申请单不存在: " + applyNo); + } + + // 2. 查询流程实例,从remark字段解析出下达时指定的全量审批人 + // 节点实例是逐步创建的(如计划员节点在经理审批完成后才创建), + // 因此必须从流程实例的remark中读取全量审批人,而非仅查当前"待审核"节点 + ErfFlowInstance flowInstance = erfFlowInstanceMapper.selectOne( + new QueryWrapper().eq("apply_no", applyNo)); + if (flowInstance == null) { + throw new RuntimeException("申请单 " + applyNo + " 尚未提交审批流程"); + } + + String approverJson = flowInstance.getRemark(); + if (approverJson == null || approverJson.trim().isEmpty()) { + throw new RuntimeException("申请单 " + applyNo + " 流程实例中没有审批人信息"); + } + + // 3. 解析JSON获取全量审批人ID列表 + long techManagerId; + List prodManagerIds; + List qualityManagerIds; + List plannerIds; + try { + JsonNode root = JSON_MAPPER.readTree(approverJson); + techManagerId = root.path("techManagerId").asLong(0); + prodManagerIds = new ArrayList<>(); + root.path("prodManagerIds").forEach(n -> prodManagerIds.add(n.asLong())); + qualityManagerIds = new ArrayList<>(); + root.path("qualityManagerIds").forEach(n -> qualityManagerIds.add(n.asLong())); + plannerIds = new ArrayList<>(); + root.path("plannerIds").forEach(n -> plannerIds.add(n.asLong())); + } catch (Exception e) { + throw new RuntimeException("解析审批人JSON失败: " + e.getMessage(), e); + } + + log.info("申请单 {} 全量审批人 - 技术经理: {}, 生产经理: {}, 质量经理: {}, 计划员: {}", + applyNo, techManagerId, prodManagerIds, qualityManagerIds, plannerIds); + + // 4. 查询该申请单已存在的节点实例,判断谁已完成 + // "已批准"或"已完成"表示该人已经处理完毕,无需再催 + List existingNodes = erfFlowNodeInstanceMapper.selectList( + new QueryWrapper() + .eq("apply_no", applyNo) + .in("node_code", "技术经理审批", "生产经理审批", "质量经理审批", "计划员排产")); + + Set completedTechManagers = new HashSet<>(); + Set completedProdManagers = new HashSet<>(); + Set completedQualityManagers = new HashSet<>(); + Set completedPlanners = new HashSet<>(); + + for (ErfFlowNodeInstance node : existingNodes) { + if (node.getAssigneeUserId() == null) continue; + boolean done = "已批准".equals(node.getStatus()) || "已完成".equals(node.getStatus()); + if (!done) continue; + switch (node.getNodeCode()) { + case "技术经理审批": completedTechManagers.add(node.getAssigneeUserId()); break; + case "生产经理审批": completedProdManagers.add(node.getAssigneeUserId()); break; + case "质量经理审批": completedQualityManagers.add(node.getAssigneeUserId()); break; + case "计划员排产": completedPlanners.add(node.getAssigneeUserId()); break; + default: break; + } + } + + // 5. 查询当前实际"待审核"的节点,构建"userId -> 当前活跃节点编码集合"映射 + // 必须按角色(节点编码)判断,而非仅凭userId,否则同一人担任多个角色时会误判 + // 例:某人同时是技术经理和计划员,技术经理节点"待审核"时, + // 若只用userId判断,其计划员催办邮件也会显示"当前正等待您处理",实际尚未轮到 + String currentNodeCode = flowInstance.getCurrentNodeCode(); + List activeNodes = existingNodes.stream() + .filter(n -> "待审核".equals(n.getStatus())) + .collect(Collectors.toList()); + + // 当前待处理人姓名(用于告知非当前步骤的收件人) + String currentPendingNames = activeNodes.stream() + .filter(n -> n.getAssigneeName() != null) + .map(ErfFlowNodeInstance::getAssigneeName) + .distinct() + .collect(Collectors.joining("、")); + + // userId -> 该用户当前"待审核"的节点编码集合(精确到角色) + Map> activeNodesByUser = new HashMap<>(); + for (ErfFlowNodeInstance node : activeNodes) { + if (node.getAssigneeUserId() != null) { + activeNodesByUser + .computeIfAbsent(node.getAssigneeUserId(), k -> new HashSet<>()) + .add(node.getNodeCode()); + } + } + + log.info("申请单 {} 当前节点: {}, 当前待处理人: {}", applyNo, currentNodeCode, currentPendingNames); + + // 6. 按角色分别判断 isCurrentStep,再发送催办邮件 + // isCurrentStep = 该人在该角色对应的节点上有"待审核"记录 + int sentCount = 0; + + // 技术经理 + if (techManagerId != 0 && !completedTechManagers.contains(techManagerId)) { + boolean isCurrent = activeNodesByUser + .getOrDefault(techManagerId, Collections.emptySet()) + .contains("技术经理审批"); + if (sendManagerUrgeEmail(apply, techManagerId, currentNodeCode, currentPendingNames, isCurrent)) { + sentCount++; + } + } + // 生产经理 + for (Long id : prodManagerIds) { + if (!completedProdManagers.contains(id)) { + boolean isCurrent = activeNodesByUser + .getOrDefault(id, Collections.emptySet()) + .contains("生产经理审批"); + if (sendManagerUrgeEmail(apply, id, currentNodeCode, currentPendingNames, isCurrent)) { + sentCount++; + } + } + } + // 质量经理 + for (Long id : qualityManagerIds) { + if (!completedQualityManagers.contains(id)) { + boolean isCurrent = activeNodesByUser + .getOrDefault(id, Collections.emptySet()) + .contains("质量经理审批"); + if (sendManagerUrgeEmail(apply, id, currentNodeCode, currentPendingNames, isCurrent)) { + sentCount++; + } + } + } + // 计划员 + for (Long id : plannerIds) { + if (!completedPlanners.contains(id)) { + boolean isCurrent = activeNodesByUser + .getOrDefault(id, Collections.emptySet()) + .contains("计划员排产"); + if (sendPlannerUrgeEmail(apply, id, currentNodeCode, currentPendingNames, isCurrent)) { + sentCount++; + } + } + } + + log.info("=== 手动催办完成 === 申请单: {}, 成功发送邮件人数: {}", applyNo, sentCount); + return sentCount; + } + + /** + * 向经理(技术/生产/质量)发送催办邮件,链接指向审批待办页 + * + * @param apply 申请单信息 + * @param managerId 收件经理用户ID + * @param currentNodeCode 当前流程节点编码 + * @param currentPendingNames 当前待处理人姓名(逗号分隔) + * @param isCurrentStep 该经理是否就是当前步骤的待处理人 + */ + private boolean sendManagerUrgeEmail(ErfExpApply apply, Long managerId, + String currentNodeCode, String currentPendingNames, + boolean isCurrentStep) { + try { + UserEmailInfoDto manager = sysUserDao.getUserEmailInfoById(managerId); + if (manager == null) { + log.warn("经理ID {} 不存在,跳过催办邮件", managerId); + return false; + } + if (manager.getEmail() == null || manager.getEmail().trim().isEmpty()) { + log.warn("经理 {} 未配置邮箱,跳过催办邮件", manager.getUsername()); + return false; + } + + String submitTimeStr = formatDate(apply.getSubmitTime()); + String subject = String.format("【催办提醒】申请单 %s 等待您审批确认", apply.getApplyNo()); + + StringBuilder body = new StringBuilder(); + body.append(""); + body.append("

⚠ 审批催办提醒

"); + body.append("

尊敬的 ").append(manager.getUsername()).append(",您好!

"); + + if (isCurrentStep) { + // 当前步骤就是该经理,需要立即处理 + body.append("

🚨 当前正等待您完成审批,请尽快处理!

"); + } else { + // 非当前步骤,告知当前进度 + body.append("

本申请单已向您发出催办,您的审批步骤尚未到达。

"); + body.append(buildCurrentStepBlock(currentNodeCode, currentPendingNames)); + } + + body.append("
"); + body.append(buildApplyInfoTable(apply, submitTimeStr)); + body.append("

请点击以下链接前往审批:

"); + body.append("

前往审批待办页面

"); + body.append(buildFooter()); + body.append(""); + + sendMail(subject, body.toString(), new String[]{manager.getEmail()}, "手动催办-经理"); + log.info("已向经理 {} ({}) 发送催办邮件,是否当前步骤: {}", manager.getUsername(), manager.getEmail(), isCurrentStep); + return true; + } catch (Exception e) { + log.error("向经理ID {} 发送催办邮件失败: {}", managerId, e.getMessage(), e); + return false; + } + } + + /** + * 向计划员发送催办邮件,链接指向计划员排产页 + * + * @param apply 申请单信息 + * @param plannerId 收件计划员用户ID + * @param currentNodeCode 当前流程节点编码 + * @param currentPendingNames 当前待处理人姓名(逗号分隔) + * @param isCurrentStep 该计划员是否就是当前步骤的待处理人 + */ + private boolean sendPlannerUrgeEmail(ErfExpApply apply, Long plannerId, + String currentNodeCode, String currentPendingNames, + boolean isCurrentStep) { + try { + UserEmailInfoDto planner = sysUserDao.getUserEmailInfoById(plannerId); + if (planner == null) { + log.warn("计划员ID {} 不存在,跳过催办邮件", plannerId); + return false; + } + if (planner.getEmail() == null || planner.getEmail().trim().isEmpty()) { + log.warn("计划员 {} 未配置邮箱,跳过催办邮件", planner.getUsername()); + return false; + } + + String submitTimeStr = formatDate(apply.getSubmitTime()); + String subject = String.format("【催办提醒】申请单 %s 等待您安排排产", apply.getApplyNo()); + + StringBuilder body = new StringBuilder(); + body.append(""); + body.append("

⚠ 排产催办提醒

"); + body.append("

尊敬的 ").append(planner.getUsername()).append(" 计划员,您好!

"); + + if (isCurrentStep) { + body.append("

🚨 当前正等待您安排排产,请尽快处理!

"); + } else { + body.append("

本申请单已向您发出催办,排产步骤尚未到达(需经理审批完成后方可排产)。

"); + body.append(buildCurrentStepBlock(currentNodeCode, currentPendingNames)); + } + + body.append("
"); + body.append(buildApplyInfoTable(apply, submitTimeStr)); + body.append("

请点击以下链接前往排产:

"); + body.append("

前往计划员排产页面

"); + body.append(buildFooter()); + body.append(""); + + sendMail(subject, body.toString(), new String[]{planner.getEmail()}, "手动催办-计划员"); + log.info("已向计划员 {} ({}) 发送催办邮件,是否当前步骤: {}", planner.getUsername(), planner.getEmail(), isCurrentStep); + return true; + } catch (Exception e) { + log.error("向计划员ID {} 发送催办邮件失败: {}", plannerId, e.getMessage(), e); + return false; + } + } + + /** + * 构建"当前流程进度"提示块(用于非当前步骤收件人) + */ + private String buildCurrentStepBlock(String currentNodeCode, String currentPendingNames) { + if (currentNodeCode == null || currentNodeCode.isEmpty()) { + return ""; + } + String pendingPart = (currentPendingNames != null && !currentPendingNames.isEmpty()) + ? ",待处理人:" + currentPendingNames + "" + : ""; + return "
" + + "🕐 当前流程进度
" + + "正在进行:" + currentNodeCode + "" + pendingPart + + "
"; + } + + /** + * 构建申请单信息HTML表格 + */ + private String buildApplyInfoTable(ErfExpApply apply, String submitTimeStr) { + return "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
申请单号事业部试验名称试验类型申请人下达时间
" + apply.getApplyNo() + "" + nvl(apply.getBuNo()) + "" + nvl(apply.getTitle()) + "" + nvl(apply.getExperimentType()) + "" + nvl(apply.getCreatorName()) + "" + submitTimeStr + "
"; + } + + private String buildFooter() { + return "

发送时间:" + + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + + "

"; + } + + private String formatDate(Date date) { + if (date == null) return ""; + return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + .format(date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()); + } + + private String nvl(String s) { + return s != null ? s : ""; + } + private void sendMail(String subject, String body, String[] toEmails, String mailType) { try { MailSendAddressData mailSendData = qcMapper.getSendMailFromAddress();