diff --git a/src/main/java/com/xujie/sys/config/ShiroConfig.java b/src/main/java/com/xujie/sys/config/ShiroConfig.java index c18423de..f0088d23 100644 --- a/src/main/java/com/xujie/sys/config/ShiroConfig.java +++ b/src/main/java/com/xujie/sys/config/ShiroConfig.java @@ -47,6 +47,7 @@ public class ShiroConfig { // 刷新接口 filterMap.put("/tcp", "anon"); filterMap.put("/ckp-file/**", "anon"); + filterMap.put("/oss/2/**", "anon"); // filterMap.put("/**", "authc"); filterMap.put("/webjars/**", "anon"); filterMap.put("/druid/**", "anon"); diff --git a/src/main/java/com/xujie/sys/modules/erf/data/ErfExpApplyData.java b/src/main/java/com/xujie/sys/modules/erf/data/ErfExpApplyData.java index 71be96b5..98087d99 100644 --- a/src/main/java/com/xujie/sys/modules/erf/data/ErfExpApplyData.java +++ b/src/main/java/com/xujie/sys/modules/erf/data/ErfExpApplyData.java @@ -91,10 +91,15 @@ public class ErfExpApplyData implements Serializable { private BigDecimal finalQuantity; /** - * 计划员用户ID + * 计划员用户ID(单个,兼容旧接口) */ private Long plannerUserId; + /** + * 计划员用户ID列表(下达时多选) + */ + private List plannerUserIds; + /** * 计划员姓名 */ 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 747a9dfe..349d8bb5 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 @@ -177,19 +177,41 @@ public class ErfApprovalReminderServiceImpl implements ErfApprovalReminderServic return; } - // 3. 获取计划员角色用户 - List planners = sysUserDao.getUserEmailListByRoleName("计划员"); + // 3. 从待排产节点实例中提取被指定的计划员(去重) + // 多计划员场景:只通知被分配到该申请单的计划员,而非全部计划员角色用户 + Set assignedPlannerIds = nodeList.stream() + .filter(n -> n.getAssigneeUserId() != null) + .map(ErfFlowNodeInstance::getAssigneeUserId) + .collect(Collectors.toSet()); - if (planners.isEmpty()) { - log.warn("系统中没有配置计划员角色的用户或用户未配置邮箱"); + if (assignedPlannerIds.isEmpty()) { + log.warn("待排产节点没有分配计划员,跳过提醒"); return; } - log.info("待排产申请单数: {}, 计划员数量: {}", validApplyList.size(), planners.size()); + log.info("待排产申请单数: {}, 被指定计划员数量: {}", validApplyList.size(), assignedPlannerIds.size()); + + // 4. 为每位被指定的计划员发送邮件(仅发送与其相关的申请单) + Set validApplyNoSet = validApplyList.stream() + .map(ErfExpApply::getApplyNo) + .collect(Collectors.toSet()); - // 4. 为每位计划员发送邮件 - for (UserEmailInfoDto planner : planners) { - sendPlannerReminderEmail(planner.getUserId(), validApplyList); + for (Long plannerId : assignedPlannerIds) { + // 筛选出该计划员负责且状态有效的申请单 + List plannerApplyNos = nodeList.stream() + .filter(n -> plannerId.equals(n.getAssigneeUserId()) + && validApplyNoSet.contains(n.getApplyNo())) + .map(ErfFlowNodeInstance::getApplyNo) + .distinct() + .collect(Collectors.toList()); + + List plannerApplyList = validApplyList.stream() + .filter(a -> plannerApplyNos.contains(a.getApplyNo())) + .collect(Collectors.toList()); + + if (!plannerApplyList.isEmpty()) { + sendPlannerReminderEmail(plannerId, plannerApplyList); + } } log.info("=== 计划员排产邮件提醒执行完成 ==="); diff --git a/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfExpApplyServiceImpl.java b/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfExpApplyServiceImpl.java index 403ae0e7..2ea87db7 100644 --- a/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfExpApplyServiceImpl.java +++ b/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfExpApplyServiceImpl.java @@ -7,8 +7,13 @@ import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.xujie.sys.common.exception.XJException; import com.xujie.sys.common.utils.DateUtils; +import com.xujie.sys.common.utils.MailUtil; import com.xujie.sys.common.utils.PageUtils; import com.xujie.sys.modules.erf.data.ErfExpApplyData; +import com.xujie.sys.modules.pms.data.MailSendAddressData; +import com.xujie.sys.modules.pms.data.SendMailRecord; +import com.xujie.sys.modules.pms.mapper.QcMapper; +import com.xujie.sys.modules.sys.dto.UserEmailInfoDto; import com.xujie.sys.modules.erf.data.ErfFlowApprovalData; import com.xujie.sys.modules.erf.data.ErfFlowStatusData; import com.xujie.sys.modules.erf.data.ErfPlannerScheduleData; @@ -74,6 +79,9 @@ public class ErfExpApplyServiceImpl extends ServiceImpl plannerUserIds = data.getPlannerUserIds(); + if (plannerUserIds == null || plannerUserIds.isEmpty()) { + if (data.getPlannerUserId() != null) { + plannerUserIds = Collections.singletonList(data.getPlannerUserId()); + } + } + if (plannerUserIds == null || plannerUserIds.isEmpty()) { throw new XJException("计划员未指定"); } @@ -153,12 +168,12 @@ public class ErfExpApplyServiceImpl extends ServiceImpl approverInfo = new HashMap<>(); approverInfo.put("techManagerId", data.getTechManagerId()); approverInfo.put("prodManagerIds", data.getProdManagerIds()); approverInfo.put("qualityManagerIds", data.getQualityManagerIds()); - approverInfo.put("plannerId", data.getPlannerUserId()); + approverInfo.put("plannerIds", plannerUserIds); // 无论首次还是重新下达,都新建节点(startFlowWithApprovers 内部会先删除旧节点) erfFlowEngineService.startFlowWithApprovers( @@ -172,16 +187,19 @@ public class ErfExpApplyServiceImpl extends ServiceImpl nodeQuery = new QueryWrapper<>(); + nodeQuery.eq("apply_no", data.getApplyNo()) + .eq("node_code", "计划员排产") + .eq("status", "待审核") + .eq("assignee_user_id", data.getPlannerUserId()) + .orderByDesc("attempt_no") + .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"); + ErfFlowNodeInstance nodeInstance = erfFlowNodeInstanceMapper.selectOne(nodeQuery); + if (nodeInstance == null) { + // 如果已经被其他计划员处理了,或者当前用户不是待办人,则提示具体是谁排产的 + QueryWrapper otherNodeQuery = new QueryWrapper<>(); + otherNodeQuery.eq("apply_no", data.getApplyNo()) + .eq("node_code", "计划员排产") + .eq("status", "已完成") + .orderByDesc("complete_time") + .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"); + ErfFlowNodeInstance otherNode = erfFlowNodeInstanceMapper.selectOne(otherNodeQuery); + if (otherNode != null) { + String otherPlannerName = otherNode.getAssigneeName(); + throw new XJException("排产失败,已被计划员 " + otherPlannerName + " 处理"); + } else { + throw new XJException("排产失败,当前用户没有权限或已被其他人处理"); + } + } ErfExpApply entity = this.getById(data.getApplyNo()); if (entity == null) { throw new XJException("申请单不存在"); @@ -508,13 +549,6 @@ public class ErfExpApplyServiceImpl extends ServiceImpl nodeQuery = new QueryWrapper<>(); - nodeQuery.eq("apply_no", data.getApplyNo()) - .eq("node_code", "计划员排产") - .eq("status", "待审核") - .orderByDesc("attempt_no") - .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"); - ErfFlowNodeInstance nodeInstance = erfFlowNodeInstanceMapper.selectOne(nodeQuery); if (nodeInstance != null) { nodeInstance.setStatus("已完成"); @@ -529,6 +563,27 @@ public class ErfExpApplyServiceImpl extends ServiceImpl otherPendingNodes = erfFlowNodeInstanceMapper.selectList( + new QueryWrapper() + .eq("apply_no", data.getApplyNo()) + .eq("node_code", "计划员排产") + .eq("status", "待审核") + ); + for (ErfFlowNodeInstance other : otherPendingNodes) { + other.setStatus("已跳过"); + other.setComment("其他计划员已完成排产,本任务自动跳过"); + other.setCompleteTime(new Date()); + erfFlowNodeInstanceMapper.update(other, new QueryWrapper() + .eq("apply_no", other.getApplyNo()) + .eq("node_code", other.getNodeCode()) + .eq("attempt_no", other.getAttemptNo())); + } + if (!otherPendingNodes.isEmpty()) { + log.info("已将 {} 个其他计划员排产节点标记为已跳过", otherPendingNodes.size()); + } + // 调用流程引擎自动流转到下一节点 erfFlowEngineService.moveToNextNode(data.getApplyNo(), "计划员排产", null); log.info("排产完成,已自动流转到下一节点"); @@ -1377,6 +1432,97 @@ public class ErfExpApplyServiceImpl extends ServiceImpl plannerUserIds) { + try { + MailSendAddressData mailSendData = qcMapper.getSendMailFromAddress(); + if (mailSendData == null) { + log.error("邮件发送配置未设置,无法发送下达通知邮件"); + return; + } + + String todayStr = DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()); + String submitDateStr = entity.getSubmitTime() != null + ? DateTimeFormatter.ofPattern("yyyy-MM-dd") + .format(entity.getSubmitTime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()) + : todayStr; + + String subject = String.format("【工程实验申请下达通知】%s - %s", + entity.getApplyNo(), entity.getTitle() != null ? entity.getTitle() : ""); + + // 构建邮件正文(内容固定,收件人不同) + String tableBody = buildPlannerNotificationTable(entity, todayStr, submitDateStr); + + List successEmails = new ArrayList<>(); + + for (Long plannerId : plannerUserIds) { + try { + UserEmailInfoDto planner = sysUserDao.getUserEmailInfoById(plannerId); + if (planner == null) { + log.warn("计划员ID {} 不存在,跳过下达邮件通知", plannerId); + continue; + } + String plannerEmail = planner.getEmail(); + if (plannerEmail == null || plannerEmail.trim().isEmpty()) { + log.warn("计划员 {} 未配置邮箱,跳过下达邮件通知", planner.getUsername()); + continue; + } + + String body = "" + + "

尊敬的 " + planner.getUsername() + " 计划员:

" + + "

您好!以下工程实验申请单已下达,请及时安排生产。

" + + "
" + tableBody + + "

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

"; + + MailUtil.sendMail(subject, body, new String[]{plannerEmail}, mailSendData); + successEmails.add(plannerEmail); + log.info("已向计划员 {} ({}) 发送下达通知邮件,申请单: {}", + planner.getUsername(), plannerEmail, entity.getApplyNo()); + } catch (Exception e) { + log.error("向计划员ID {} 发送邮件失败: {}", plannerId, e.getMessage()); + } + } + + if (!successEmails.isEmpty()) { + SendMailRecord mailRecord = new SendMailRecord(); + mailRecord.setType("工程实验申请下达通知"); + mailRecord.setDocumentNo(entity.getApplyNo()); + mailRecord.setRecipient(String.join(";", successEmails)); + mailRecord.setSendDate(new Date()); + qcMapper.saveSendMailRecord(mailRecord); + } + + } catch (Exception e) { + log.error("发送下达通知邮件失败,申请单: {}, 错误: {}", entity.getApplyNo(), e.getMessage(), e); + } + } + + private String buildPlannerNotificationTable(ErfExpApply entity, String todayStr, String submitDateStr) { + return "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
收到邮件日期项目号实验单号项目名称安排生产工序数量备注需求日期工程师
" + todayStr + "" + (entity.getProjectNo() != null ? entity.getProjectNo() : "") + "" + (entity.getWorkOrderNo()!=null ? entity.getWorkOrderNo() : "") + "" + (entity.getTitle() != null ? entity.getTitle() : "") + "" + (entity.getProcessRequirement() != null ? entity.getProcessRequirement() : "") + "" + (entity.getQuantityReq() != null ? entity.getQuantityReq() : "") + "" + (entity.getRemark() != null ? entity.getRemark() : "") + "" + submitDateStr + "" + (entity.getCreatorName() != null ? entity.getCreatorName() : "") + "
"; + } + /** * 复制附件 * 使用OSS系统(sys_oss表) diff --git a/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfFlowEngineServiceImpl.java b/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfFlowEngineServiceImpl.java index 01d3e3ce..71fbf6c4 100644 --- a/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfFlowEngineServiceImpl.java +++ b/src/main/java/com/xujie/sys/modules/erf/service/impl/ErfFlowEngineServiceImpl.java @@ -103,21 +103,31 @@ public class ErfFlowEngineServiceImpl implements ErfFlowEngineService { List prodManagerIds = (List) approverInfo.get("prodManagerIds"); @SuppressWarnings("unchecked") List qualityManagerIds = (List) approverInfo.get("qualityManagerIds"); - Long plannerId = (Long) approverInfo.get("plannerId"); + @SuppressWarnings("unchecked") + List plannerIds = (List) approverInfo.get("plannerIds"); + // 兼容旧字段 + if (plannerIds == null || plannerIds.isEmpty()) { + Long plannerId = (Long) approverInfo.get("plannerId"); + if (plannerId != null) { + plannerIds = Collections.singletonList(plannerId); + } + } + if (plannerIds == null) plannerIds = Collections.emptyList(); - log.info("审批人信息 - 技术经理: {}, 生产经理: {}, 质量经理: {}, 计划员: {}", - techManagerId, prodManagerIds, qualityManagerIds, plannerId); + log.info("审批人信息 - 技术经理: {}, 生产经理: {}, 质量经理: {}, 计划员(多): {}", + techManagerId, prodManagerIds, qualityManagerIds, plannerIds); // 1. 删除该申请单已有的所有节点实例,确保每次下达都是全新的节点 deleteFlowNodes(applyNo); - // 2. 将审批人信息序列化保存到remark字段(含下达时选定的计划员) + // 2. 将审批人信息序列化保存到remark字段(含下达时选定的多位计划员) + final List finalPlannerIds = plannerIds; String approverJson = String.format( - "{\"techManagerId\":%d,\"prodManagerIds\":[%s],\"qualityManagerIds\":[%s],\"plannerId\":%d}", + "{\"techManagerId\":%d,\"prodManagerIds\":[%s],\"qualityManagerIds\":[%s],\"plannerIds\":[%s]}", techManagerId, prodManagerIds.stream().map(String::valueOf).collect(Collectors.joining(",")), qualityManagerIds.stream().map(String::valueOf).collect(Collectors.joining(",")), - plannerId != null ? plannerId : 0 + finalPlannerIds.stream().map(String::valueOf).collect(Collectors.joining(",")) ); // 3. 创建或更新流程实例 @@ -204,7 +214,13 @@ public class ErfFlowEngineServiceImpl implements ErfFlowEngineService { // 驳回后重新审批的情况(从其他节点驳回到生产/质量经理) log.info("驳回后重新审批,创建节点:{}", nextNodeCode); createApproverNodesFromFlowInstance(applyNo, nextNodeCode, flowInstance); - }else if ("样品确认".equals(nextNodeCode)) { + + } else if ("计划员排产".equals(nextNodeCode)) { + // 计划员排产:可能有多个计划员,任意一人完成即可 + log.info("进入计划员排产阶段,为每位计划员创建节点"); + createApproverNodesFromFlowInstance(applyNo, "计划员排产", flowInstance); + + } else if ("样品确认".equals(nextNodeCode)) { // 样品确认不需要进行任何操作 log.info("样品确认节点"); } else { @@ -276,6 +292,22 @@ public class ErfFlowEngineServiceImpl implements ErfFlowEngineService { for (Long managerId : qualityManagerIds) { createSingleNodeInstance(applyNo, nodeCode, managerId); } + } else if ("计划员排产".equals(nodeCode)) { + // 计划员:可能有多个,任意一人排产即完成 + List plannerIds = extractPlannerIds(remark); + if (plannerIds.isEmpty()) { + // 降级:从角色查询 + Long fallbackId = getAssigneeUserId("计划员排产", applyNo); + plannerIds = fallbackId != null ? Collections.singletonList(fallbackId) : Collections.emptyList(); + } + if (plannerIds.isEmpty()) { + throw new XJException("计划员信息为空"); + } + + log.info("创建{}个计划员排产节点", plannerIds.size()); + for (Long plannerId : plannerIds) { + createSingleNodeInstance(applyNo, nodeCode, plannerId); + } } } catch (Exception e) { log.error("解析审批人信息失败: " + e.getMessage(), e); @@ -343,7 +375,6 @@ public class ErfFlowEngineServiceImpl implements ErfFlowEngineService { */ private List extractQualityManagerIds(String json) { try { - // 简单的JSON解析:{"techManagerId":1,"prodManagerIds":[2,3],"qualityManagerIds":[4,5]} String pattern = "\"qualityManagerIds\":\\[([0-9,]+)\\]"; java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern); java.util.regex.Matcher m = p.matcher(json); @@ -360,6 +391,40 @@ public class ErfFlowEngineServiceImpl implements ErfFlowEngineService { return new ArrayList<>(); } + /** + * 从JSON字符串中提取计划员ID列表 + * 支持新字段 plannerIds 和旧字段 plannerId 兼容 + */ + private List extractPlannerIds(String json) { + try { + // 新格式:plannerIds 数组 + String pattern = "\"plannerIds\":\\[([0-9,]+)\\]"; + java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern); + java.util.regex.Matcher m = p.matcher(json); + if (m.find()) { + String idsStr = m.group(1); + return Arrays.stream(idsStr.split(",")) + .map(s -> Long.valueOf(s.trim())) + .filter(id -> id > 0) + .collect(Collectors.toList()); + } + // 旧格式:单个 plannerId + int idx = json.indexOf("\"plannerId\":"); + if (idx >= 0) { + String sub = json.substring(idx + 12).trim(); + int end = sub.indexOf(','); + if (end < 0) end = sub.indexOf('}'); + if (end > 0) { + long id = Long.parseLong(sub.substring(0, end).trim()); + if (id > 0) return Collections.singletonList(id); + } + } + } catch (Exception e) { + log.error("解析计划员ID失败: " + e.getMessage()); + } + return new ArrayList<>(); + } + @Override public void rejectToNode(String applyNo, String targetNodeCode, ErfFlowApprovalData approvalData) { log.info("=== 驳回到指定节点 === 申请单: {}, 目标节点: {}", applyNo, targetNodeCode);