From 2f51b77994bb727b185bb11fafc4164ba50d67c9 Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Thu, 2 Apr 2026 10:07:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=8A=A0=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../erf/controller/ErfExpApplyController.java | 11 +- .../impl/ErfApprovalReminderServiceImpl.java | 3 +- .../service/impl/ErfExpApplyServiceImpl.java | 395 ++++++++++++------ 3 files changed, 278 insertions(+), 131 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 4cd4d676..3cc67b4e 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 @@ -1,5 +1,6 @@ package com.xujie.sys.modules.erf.controller; +import com.xujie.sys.common.exception.XJException; import com.xujie.sys.common.utils.PageUtils; import com.xujie.sys.common.utils.R; import com.xujie.sys.modules.erf.data.ErfExpApplyData; @@ -87,9 +88,12 @@ public class ErfExpApplyController extends AbstractController { try { String applyNo = erfExpApplyService.saveExpApply(data); return R.ok("保存成功").put("applyNo", applyNo); + } catch (XJException e) { + log.warn("保存申请单业务校验失败: {}", e.getMessage()); + return R.error(e.getMessage()); } catch (Exception e) { log.error("保存申请单失败: " + e.getMessage(), e); - return R.error("保存失败: " + e.getMessage()); + return R.error("保存失败,请联系管理员"); } } @@ -276,9 +280,12 @@ public class ErfExpApplyController extends AbstractController { sourceApplyNo,applyNo, copyTriConfirm, copyAttachment,copyRawMaterialList, currentUserId); return R.ok("复制成功").put("newApplyNo", newApplyNo); + } catch (XJException e) { + log.warn("复制申请单业务校验失败: {}", e.getMessage()); + return R.error(e.getMessage()); } catch (Exception e) { log.error("复制失败: " + e.getMessage(), e); - return R.error("复制失败: " + e.getMessage()); + return R.error("复制失败,请联系管理员"); } } 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 ecd548c4..322b55f3 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 @@ -66,6 +66,7 @@ public class ErfApprovalReminderServiceImpl implements ErfApprovalReminderServic 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"; + private static final String TRI_CONFIRM_PAGE_URL = "http://172.26.68.20:9001/#/erf-triConfirm"; @Value("${erf.reminder.manager.enabled:false}") private boolean managerReminderEnabled; @@ -658,7 +659,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(""); 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 c7494220..d5a77592 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 @@ -59,7 +59,10 @@ import static com.xujie.sys.modules.sys.controller.AbstractController.getUser; @Transactional(rollbackFor = Exception.class) public class ErfExpApplyServiceImpl extends ServiceImpl implements ErfExpApplyService { + private static final String APPLY_PAGE_URL = "http://172.26.68.20:9001/#/erf-expApplyList"; 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"; + private static final String TRI_CONFIRM_PAGE_URL = "http://172.26.68.20:9001/#/erf-triConfirm"; private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); @Autowired @@ -128,44 +131,73 @@ public class ErfExpApplyServiceImpl extends ServiceImpl {}", originalApplyNo, newApplyNo); - erfExpApplyMapper.updateFlowInstanceApplyNo(originalApplyNo, newApplyNo); - erfExpApplyMapper.updateFlowNodeInstanceApplyNo(originalApplyNo, newApplyNo); - erfExpApplyMapper.updateFlowApproveLogApplyNo(originalApplyNo, newApplyNo); - erfExpApplyMapper.updateTriConfirmApplyNo(originalApplyNo, newApplyNo); - erfExpApplyMapper.updateTriConfirmDetailApplyNo(originalApplyNo, newApplyNo); - erfExpApplyMapper.updateRawMaterialApplyNo(originalApplyNo, newApplyNo); - erfExpApplyMapper.updateOssApplyNo(originalApplyNo, newApplyNo); - // 第二步:更新主表主键(用旧值定位,改为新值) - erfExpApplyMapper.updateMainApplyNo(originalApplyNo, newApplyNo); - } - // 第三步:更新主表其他字段(此时主键已是新值,updateById可正确定位) - this.updateById(entity); - log.info("修改工程试验单: {} (原单号: {})", newApplyNo, originalApplyNo); + try { + if (isNew) { + // 新增前先校验试验单号,避免将数据库主键异常直接暴露给前端 + if (this.getById(newApplyNo) != null) { + throw new XJException("试验单号已存在,请修改后重试"); + } + entity.setStatus("草稿"); + entity.setCreateTime(new Date()); + entity.setUpdateTime(new Date()); + String site = erfExpApplyMapper.getSiteByBu(data.getBuNo()); + entity.setSite(site); + this.save(entity); + log.info("新增工程试验单: {}", newApplyNo); + } else { + // 修改 - 通过原始单号查找记录 + originalApplyNo = originalApplyNo.trim(); + ErfExpApply existing = this.getById(originalApplyNo); + if (existing == null) { + throw new XJException("试验单不存在: " + originalApplyNo); + } + entity.setUpdateTime(new Date()); + if (!newApplyNo.equals(originalApplyNo)) { + // 试验单号发生变更时,先校验目标单号是否已存在 + if (this.getById(newApplyNo) != null) { + throw new XJException("试验单号已存在,请修改后重试"); + } + // 第一步:级联更新所有关联表(Druid防注入不允许多语句,逐条执行) + log.info("试验单号变更: {} -> {}", originalApplyNo, newApplyNo); + erfExpApplyMapper.updateFlowInstanceApplyNo(originalApplyNo, newApplyNo); + erfExpApplyMapper.updateFlowNodeInstanceApplyNo(originalApplyNo, newApplyNo); + erfExpApplyMapper.updateFlowApproveLogApplyNo(originalApplyNo, newApplyNo); + erfExpApplyMapper.updateTriConfirmApplyNo(originalApplyNo, newApplyNo); + erfExpApplyMapper.updateTriConfirmDetailApplyNo(originalApplyNo, newApplyNo); + erfExpApplyMapper.updateRawMaterialApplyNo(originalApplyNo, newApplyNo); + erfExpApplyMapper.updateOssApplyNo(originalApplyNo, newApplyNo); + // 第二步:更新主表主键(用旧值定位,改为新值) + erfExpApplyMapper.updateMainApplyNo(originalApplyNo, newApplyNo); + } + // 第三步:更新主表其他字段(此时主键已是新值,updateById可正确定位) + this.updateById(entity); + log.info("修改工程试验单: {} (原单号: {})", newApplyNo, originalApplyNo); + } + } catch (Exception e) { + if (isDuplicateApplyNoException(e)) { + log.warn("保存试验单发生主键重复,applyNo={}, error={}", newApplyNo, e.getMessage()); + throw new XJException("试验单号已存在,请修改后重试"); + } + throw e; } return newApplyNo; } + private boolean isDuplicateApplyNoException(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + String message = current.getMessage(); + if (message != null && (message.contains("PK_erf_exp_apply") + || message.contains("PRIMARY KEY") + || message.contains("重复键值") + || message.contains("duplicate key"))) { + return true; + } + current = current.getCause(); + } + return false; + } + @Override public void submitExpApply(ErfExpApplyData data) { ErfExpApply entity = this.getById(data.getApplyNo()); @@ -320,7 +352,7 @@ public class ErfExpApplyServiceImpl extends ServiceImpl plannerUserIds = extractPlannerIdsFromFlowRemark(data.getApplyNo()); if (!plannerUserIds.isEmpty()) { - sendPlannerSubmitNotification(approvedEntity, plannerUserIds); + sendPlannerApprovalPassedNotification(approvedEntity, plannerUserIds); } else { log.warn("流程 remark 中未解析到计划员ID,跳过计划员邮件: {}", data.getApplyNo()); } @@ -1677,9 +1709,10 @@ public class ErfExpApplyServiceImpl extends ServiceImpl emailSet = new LinkedHashSet<>(); - LinkedHashSet roleSummarySet = new LinkedHashSet<>(); + LinkedHashSet successEmailSet = new LinkedHashSet<>(); List recipientSummary = new ArrayList<>(); + String subject = String.format("【工程试验申请取消通知】%s - %s", + entity.getApplyNo(), StringUtils.isNotBlank(entity.getTitle()) ? entity.getTitle() : "无标题"); for (Map.Entry> entry : recipientRoleMap.entrySet()) { Long userId = entry.getKey(); @@ -1688,38 +1721,50 @@ public class ErfExpApplyServiceImpl extends ServiceImpl userRoles = entry.getValue() == null || entry.getValue().isEmpty() + ? new LinkedHashSet<>(Collections.singletonList("流程相关人员")) + : new LinkedHashSet<>(entry.getValue()); + String roleSummary = String.join("、", userRoles); + + LinkedHashSet recipientEmails = new LinkedHashSet<>(); + appendEmailRecipients(recipientEmails, userInfo.getEmail()); + appendEmailRecipients(recipientEmails, userInfo.getEmail2()); + if (recipientEmails.isEmpty()) { log.warn("取消通知收件人未配置邮箱,userId={}, username={}, applyNo={}", userId, userInfo.getUsername(), entity.getApplyNo()); continue; } - emailSet.add(email); - roleSummarySet.addAll(entry.getValue()); + + String body = buildCancelNotificationBody(entity, operatorName, userRoles); + try { + MailUtil.sendMail(subject, body, recipientEmails.toArray(new String[0]), mailSendData); + successEmailSet.addAll(recipientEmails); + } catch (Exception ex) { + log.error("发送取消通知邮件失败,userId={}, username={}, applyNo={}, error={}", + userId, userInfo.getUsername(), entity.getApplyNo(), ex.getMessage(), ex); + continue; + } + recipientSummary.add(String.format("%s(%s)", - userInfo.getUsername(), String.join("、", entry.getValue()))); + StringUtils.isNotBlank(userInfo.getUsername()) ? userInfo.getUsername() : String.valueOf(userId), + roleSummary)); } - if (emailSet.isEmpty()) { + if (successEmailSet.isEmpty()) { log.warn("已收集收件人但无有效邮箱,取消通知未发送,applyNo={}", entity.getApplyNo()); return; } - String subject = String.format("【工程试验申请取消通知】%s - %s", - entity.getApplyNo(), StringUtils.isNotBlank(entity.getTitle()) ? entity.getTitle() : "无标题"); - String body = buildCancelNotificationBody(entity, operatorName, roleSummarySet); - - MailUtil.sendMail(subject, body, emailSet.toArray(new String[0]), mailSendData); - SendMailRecord mailRecord = new SendMailRecord(); mailRecord.setType("工程试验申请取消通知"); mailRecord.setDocumentNo(entity.getApplyNo()); - mailRecord.setRecipient(String.join(";", emailSet)); + mailRecord.setRecipient(String.join(";", successEmailSet)); mailRecord.setSendDate(new Date()); qcMapper.saveSendMailRecord(mailRecord); log.info("取消通知邮件发送完成,applyNo={}, 收件人数量={}, 收件人={}", - entity.getApplyNo(), emailSet.size(), recipientSummary); + entity.getApplyNo(), successEmailSet.size(), recipientSummary); } catch (Exception e) { log.error("发送试验单取消通知失败,applyNo={}, error={}", entity.getApplyNo(), e.getMessage(), e); } @@ -1896,15 +1941,15 @@ public class ErfExpApplyServiceImpl extends ServiceImpl roleSummarySet) { + private String buildCancelNotificationBody(ErfExpApply entity, String operatorName, Set recipientRoleSet) { String cancelTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); String expectedFinishDateStr = entity.getExpectedFinishDate() != null ? DateTimeFormatter.ofPattern("yyyy-MM-dd") .format(entity.getExpectedFinishDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()) : ""; - String roleSummary = roleSummarySet == null || roleSummarySet.isEmpty() + String roleSummary = recipientRoleSet == null || recipientRoleSet.isEmpty() ? "流程相关人员" - : String.join("、", roleSummarySet); + : String.join("、", recipientRoleSet); StringBuilder body = new StringBuilder(); body.append("") @@ -1916,7 +1961,7 @@ public class ErfExpApplyServiceImpl extends ServiceImpl取消时间:") .append(cancelTime) .append("

") - .append("

通知范围:") + .append("

您的接收角色:") .append(roleSummary) .append("

") .append("
") @@ -1937,6 +1982,7 @@ public class ErfExpApplyServiceImpl extends ServiceImpl说明:该试验单为 High Risk,三方确认相关人员已纳入本次通知范围。

"); } + body.append(buildMailJumpLinkBlock(recipientRoleSet)); body.append("

发送时间:") .append(cancelTime) .append("

") @@ -1944,6 +1990,57 @@ public class ErfExpApplyServiceImpl extends ServiceImpl roleSet) { + LinkedHashMap jumpLinkMap = new LinkedHashMap<>(); + if (roleSet != null) { + for (String roleName : roleSet) { + if (StringUtils.isBlank(roleName)) { + continue; + } + if (isTriConfirmRoleForLink(roleName)) { + jumpLinkMap.put(TRI_CONFIRM_PAGE_URL, "前往三方确认页面"); + continue; + } + if (isPlannerRoleForLink(roleName)) { + jumpLinkMap.put(PLANNER_PAGE_URL, "前往计划员排产页面"); + continue; + } + if (isManagerRoleForLink(roleName)) { + jumpLinkMap.put(APPROVAL_PAGE_URL, "前往审批待办页面"); + } + } + } + if (jumpLinkMap.isEmpty()) { + jumpLinkMap.put(APPROVAL_PAGE_URL, "前往审批待办页面"); + } + + StringBuilder jumpLinks = new StringBuilder(); + jumpLinks.append("

快捷跳转:

"); + for (Map.Entry entry : jumpLinkMap.entrySet()) { + jumpLinks.append("

") + .append(entry.getValue()) + .append("

"); + } + return jumpLinks.toString(); + } + + private boolean isManagerRoleForLink(String roleName) { + return roleName.contains("经理"); + } + + private boolean isPlannerRoleForLink(String roleName) { + return roleName.contains("计划员"); + } + + private boolean isTriConfirmRoleForLink(String roleName) { + return roleName.contains("三方确认") + || roleName.contains("生产负责人") + || roleName.contains("质量负责人") + || roleName.contains("技术负责人"); + } + @Override public String copyExpApply(String sourceApplyNo,String applyNo, Boolean copyTriConfirm, Boolean copyAttachment,Boolean copyRawMaterialList, Long currentUserId) { log.info("=== 开始复制试验单 === 源试验单号: {}, 复制三方确认: {}, 复制附件: {}", @@ -1952,73 +2049,88 @@ public class ErfExpApplyServiceImpl extends ServiceImpl plannerUserIds) { + String subject = String.format("【工程试验申请下达通知】%s - %s", + entity.getApplyNo(), entity.getTitle() != null ? entity.getTitle() : ""); + String introText = "您好!以下工程试验单已下达,请及时安排生产。"; + sendPlannerNotification(entity, plannerUserIds, subject, introText, + "工程试验申请下达通知", "下达通知"); + } + + /** + * 经理审批通过后发送邮件通知计划员进入排产 + * + * @param entity 试验单实体 + * @param plannerUserIds 计划员用户ID列表 + */ + private void sendPlannerApprovalPassedNotification(ErfExpApply entity, List plannerUserIds) { + String subject = String.format("【工程试验申请待排产】%s - %s", + entity.getApplyNo(), entity.getTitle() != null ? entity.getTitle() : ""); + String introText = "您好!以下工程试验单经理审批已完成,当前已进入计划员排产环节,请及时安排生产。"; + sendPlannerNotification(entity, plannerUserIds, subject, introText, + "工程试验申请经理审批通过待排产通知", "待排产通知"); + } + + /** + * 发送计划员通知邮件(下达通知 / 待排产通知) + */ + private void sendPlannerNotification(ErfExpApply entity, List plannerUserIds, + String subject, String introText, + String mailRecordType, String sceneName) { try { MailSendAddressData mailSendData = qcMapper.getSendMailFromAddress(); if (mailSendData == null) { - log.error("邮件发送配置未设置,无法发送下达通知邮件"); + log.error("邮件发送配置未设置,无法发送{}邮件", sceneName); return; } @@ -2198,9 +2337,6 @@ public class ErfExpApplyServiceImpl extends ServiceImpl plannerEmails = new LinkedHashSet<>(); appendEmailRecipients(plannerEmails, planner.getEmail()); appendEmailRecipients(plannerEmails, planner.getEmail2()); if (plannerEmails.isEmpty()) { - log.warn("计划员 {} 未配置邮箱(email/email2),跳过下达邮件通知", planner.getUsername()); + log.warn("计划员 {} 未配置邮箱(email/email2),跳过{}邮件通知", planner.getUsername(), sceneName); continue; } String body = "" + "

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

" - + "

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

" + + "

" + introText + "

" + "
" + tableBody + + "

前往计划员排产页面

" + "

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

"; @@ -2232,16 +2369,16 @@ public class ErfExpApplyServiceImpl extends ServiceImpl您好!您作为试验负责人负责的工程试验单,经理层审批已全部通过,流程已进入计划员排产环节。

" + "

请留意后续排产与执行安排。

" + "
" + tableBody + + "

前往工程试验申请管理页面

" + "

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

"; @@ -2483,6 +2621,7 @@ public class ErfExpApplyServiceImpl extends ServiceImpl您好!以下工程试验单已完成排产,请做好相应备料准备。

" + "
" + mainTable + "

原材料清单:

" + rawMaterialTable + + "

前往计划员排产页面

" + "

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

";