diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java index b4eb879..0cb60f8 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java @@ -16,13 +16,13 @@ public class NotifyTemplateCodeConstants { /** 临期提醒-负责人:对象进入临期窗口时通知负责人(项目负责人/owner/需求处理人→提出人) */ public static final String DUE_ALERT_APPROACHING_OWNER = "due_alert_approaching_owner"; - /** 临期提醒-协办人:等级低一等的弱化文案(仅任务/执行有协办人) */ + /** 临期提醒-协办人:已停发(2026-07-15 起告警仅通知负责人),模板保留在库,常量供历史消息匹配 */ public static final String DUE_ALERT_APPROACHING_ASSIGNEE = "due_alert_approaching_assignee"; /** 逾期提醒-负责人:每天一条持续提醒,直到终态或暂停 */ public static final String DUE_ALERT_OVERDUE_OWNER = "due_alert_overdue_owner"; - /** 逾期提醒-协办人:等级低一等的弱化文案 */ + /** 逾期提醒-协办人:已停发(2026-07-15 起告警仅通知负责人),仍参与"未读不重发"历史消息匹配 */ public static final String DUE_ALERT_OVERDUE_ASSIGNEE = "due_alert_overdue_assignee"; /** 工作报告团队催办:主管催办下属提交指定周期工作报告 */ diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/duealert/DueAlertScanServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/duealert/DueAlertScanServiceImpl.java index 451f1fb..4f30dbb 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/duealert/DueAlertScanServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/duealert/DueAlertScanServiceImpl.java @@ -8,19 +8,15 @@ import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO; import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO; import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO; -import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO; import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO; import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO; -import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO; import com.njcn.rdms.module.project.dal.mysql.duealert.DueAlertRecordMapper; import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper; -import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper; import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper; import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper; -import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.enums.DueAlertObjectTypeEnum; import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; @@ -39,7 +35,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -68,10 +63,6 @@ public class DueAlertScanServiceImpl implements DueAlertScanService { @Resource private PersonalItemMapper personalItemMapper; @Resource - private TaskAssigneeMapper taskAssigneeMapper; - @Resource - private ExecutionAssigneeMapper executionAssigneeMapper; - @Resource private ObjectStatusModelMapper objectStatusModelMapper; @Resource private DueAlertRecordMapper dueAlertRecordMapper; @@ -293,11 +284,11 @@ public class DueAlertScanServiceImpl implements DueAlertScanService { return productRequirementMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream() .map(this::toCandidate).collect(Collectors.toList()); case EXECUTION: - return toExecutionCandidates( - projectExecutionMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes)); + return projectExecutionMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream() + .map(this::toCandidate).collect(Collectors.toList()); case TASK: - return toTaskCandidates( - projectTaskMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes)); + return projectTaskMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream() + .map(this::toCandidate).collect(Collectors.toList()); case PERSONAL_ITEM: return personalItemMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream() .map(this::toCandidate).collect(Collectors.toList()); @@ -331,35 +322,16 @@ public class DueAlertScanServiceImpl implements DueAlertScanService { item.getPlannedEndDate(), singletonOrEmpty(item.getOwnerId()), List.of()); } - /** 任务:负责人 + 生效协办人(批量查,剔除与负责人重叠者) */ - private List toTaskCandidates(List tasks) { - if (tasks.isEmpty()) { - return List.of(); - } - List taskIds = tasks.stream().map(ProjectTaskDO::getId).collect(Collectors.toList()); - Map> assigneeMap = taskAssigneeMapper.selectActiveListByTaskIds(taskIds).stream() - .collect(Collectors.groupingBy(TaskAssigneeDO::getTaskId, - Collectors.mapping(TaskAssigneeDO::getUserId, Collectors.toList()))); - return tasks.stream().map(t -> buildCandidate(DueAlertObjectTypeEnum.TASK, t.getId(), t.getTaskTitle(), - t.getPlannedEndDate(), singletonOrEmpty(t.getOwnerId()), - excludeOwner(assigneeMap.get(t.getId()), t.getOwnerId()))) - .collect(Collectors.toList()); + /** 任务:仅通知负责人(2026-07-15 定稿,协办人不再接收临期/逾期告警) */ + private DueAlertCandidate toCandidate(ProjectTaskDO task) { + return buildCandidate(DueAlertObjectTypeEnum.TASK, task.getId(), task.getTaskTitle(), + task.getPlannedEndDate(), singletonOrEmpty(task.getOwnerId()), List.of()); } - /** 执行:同任务结构 */ - private List toExecutionCandidates(List executions) { - if (executions.isEmpty()) { - return List.of(); - } - List executionIds = executions.stream().map(ProjectExecutionDO::getId).collect(Collectors.toList()); - Map> assigneeMap = executionAssigneeMapper.selectActiveListByExecutionIds(executionIds) - .stream() - .collect(Collectors.groupingBy(ExecutionAssigneeDO::getExecutionId, - Collectors.mapping(ExecutionAssigneeDO::getUserId, Collectors.toList()))); - return executions.stream().map(e -> buildCandidate(DueAlertObjectTypeEnum.EXECUTION, e.getId(), - e.getExecutionName(), e.getPlannedEndDate(), singletonOrEmpty(e.getOwnerId()), - excludeOwner(assigneeMap.get(e.getId()), e.getOwnerId()))) - .collect(Collectors.toList()); + /** 执行:仅通知负责人(同任务口径) */ + private DueAlertCandidate toCandidate(ProjectExecutionDO execution) { + return buildCandidate(DueAlertObjectTypeEnum.EXECUTION, execution.getId(), execution.getExecutionName(), + execution.getPlannedEndDate(), singletonOrEmpty(execution.getOwnerId()), List.of()); } private DueAlertCandidate buildCandidate(DueAlertObjectTypeEnum type, Long objectId, String objectName, @@ -378,16 +350,4 @@ public class DueAlertScanServiceImpl implements DueAlertScanService { return userId == null ? List.of() : List.of(userId); } - /** 协办人剔除负责人(双重身份只收负责人版)+ 去重去 null */ - private List excludeOwner(List assignees, Long ownerId) { - if (assignees == null || assignees.isEmpty()) { - return List.of(); - } - return assignees.stream() - .filter(Objects::nonNull) - .filter(userId -> !userId.equals(ownerId)) - .distinct() - .collect(Collectors.toList()); - } - } diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/duealert/DueAlertScanServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/duealert/DueAlertScanServiceImplTest.java index 23b707d..01fd2fc 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/duealert/DueAlertScanServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/duealert/DueAlertScanServiceImplTest.java @@ -8,16 +8,13 @@ import com.njcn.rdms.module.project.constant.ProjectTaskConstants; import com.njcn.rdms.module.project.dal.dataobject.duealert.DueAlertRecordDO; import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO; import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO; -import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO; import com.njcn.rdms.module.project.dal.mysql.duealert.DueAlertRecordMapper; import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper; -import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper; import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper; import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper; -import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants; @@ -67,10 +64,6 @@ class DueAlertScanServiceImplTest extends BaseMockitoUnitTest { @Mock private PersonalItemMapper personalItemMapper; @Mock - private TaskAssigneeMapper taskAssigneeMapper; - @Mock - private ExecutionAssigneeMapper executionAssigneeMapper; - @Mock private ObjectStatusModelMapper objectStatusModelMapper; @Mock private DueAlertRecordMapper dueAlertRecordMapper; @@ -199,25 +192,18 @@ class DueAlertScanServiceImplTest extends BaseMockitoUnitTest { } @Test - void scan_taskAssigneeOverlapOwner_shouldBeExcludedFromAssigneeGroup() { + void scan_task_shouldNotifyOwnerOnly() { stubDict(dict("task", "1")); when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection())) .thenReturn(List.of(task(1L, 11L, TODAY))); - TaskAssigneeDO sameAsOwner = new TaskAssigneeDO(); - sameAsOwner.setTaskId(1L); - sameAsOwner.setUserId(11L); // 与负责人重叠 → 剔除 - TaskAssigneeDO other = new TaskAssigneeDO(); - other.setTaskId(1L); - other.setUserId(22L); - when(taskAssigneeMapper.selectActiveListByTaskIds(anyCollection())) - .thenReturn(List.of(sameAsOwner, other)); dueAlertScanService.scan(TODAY); + // 2026-07-15 定稿:任务/执行仅通知负责人,协办人组恒为空(扫描层不再查协办人表) ArgumentCaptor captor = ArgumentCaptor.forClass(DueAlertCandidate.class); verify(dueAlertSendService).sendAlert(captor.capture(), any(), eq(TODAY)); assertEquals(List.of(11L), captor.getValue().getOwnerUserIds()); - assertEquals(List.of(22L), captor.getValue().getAssigneeUserIds()); + assertTrue(captor.getValue().getAssigneeUserIds().isEmpty()); } private NotifyUnreadMessageRespDTO unreadOverdueMsg(Long userId, String objectType, String objectId) { @@ -228,28 +214,6 @@ class DueAlertScanServiceImplTest extends BaseMockitoUnitTest { DueAlertConstants.PARAM_OBJECT_ID, objectId)); } - @Test - void scan_overdueUnread_ownerSkippedAssigneeStillSent() { - stubDict(dict("task", "1")); - when(projectTaskMapper.selectDueAlertCandidateList(eq(TODAY.plusDays(1)), anyCollection())) - .thenReturn(List.of(task(3L, 13L, TODAY.minusDays(1)))); - TaskAssigneeDO assignee = new TaskAssigneeDO(); - assignee.setTaskId(3L); - assignee.setUserId(22L); - when(taskAssigneeMapper.selectActiveListByTaskIds(anyCollection())).thenReturn(List.of(assignee)); - // 负责人 13 压着该对象的未读逾期提醒 → 本轮只发协办人 - when(notifyMessageSendApi.getUnreadNotifyMessageListByTemplateCodes(any())) - .thenReturn(CommonResult.success(List.of(unreadOverdueMsg(13L, "task", "3")))); - - dueAlertScanService.scan(TODAY); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DueAlertCandidate.class); - verify(dueAlertSendService).sendAlert(captor.capture(), - eq(DueAlertConstants.ALERT_TYPE_OVERDUE), eq(TODAY)); - assertTrue(captor.getValue().getOwnerUserIds().isEmpty()); - assertEquals(List.of(22L), captor.getValue().getAssigneeUserIds()); - } - @Test void scan_overdueUnread_allRecipientsUnread_skipWithoutRecord() { stubDict(dict("task", "1"));