refactor(duealert): 简化临期告警扫描逻辑,停发协办人通知

- 移除任务和执行的协办人相关数据对象和映射器依赖
- 删除Objects工具类导入以减少不必要的引用
- 修改EXECUTION和TASK类型的候选对象转换逻辑,直接使用stream映射
- 移除toTaskCandidates和toExecutionCandidates方法,统一使用toCandidate方法
- 更新测试类中的相关mock配置和断言逻辑
- 更新通知模板常量注释,标记协办人相关模板为已停发状态
- 简化候选对象构建逻辑,任务和执行仅通知负责人,协办人列表为空
This commit is contained in:
2026-07-15 10:46:15 +08:00
parent 57061d4f03
commit 0c7e5268e6
3 changed files with 17 additions and 93 deletions

View File

@@ -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";
/** 工作报告团队催办:主管催办下属提交指定周期工作报告 */

View File

@@ -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<DueAlertCandidate> toTaskCandidates(List<ProjectTaskDO> tasks) {
if (tasks.isEmpty()) {
return List.of();
}
List<Long> taskIds = tasks.stream().map(ProjectTaskDO::getId).collect(Collectors.toList());
Map<Long, List<Long>> 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<DueAlertCandidate> toExecutionCandidates(List<ProjectExecutionDO> executions) {
if (executions.isEmpty()) {
return List.of();
}
List<Long> executionIds = executions.stream().map(ProjectExecutionDO::getId).collect(Collectors.toList());
Map<Long, List<Long>> 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<Long> excludeOwner(List<Long> 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());
}
}

View File

@@ -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<DueAlertCandidate> 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<DueAlertCandidate> 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"));