refactor(duealert): 简化临期告警扫描逻辑,停发协办人通知
- 移除任务和执行的协办人相关数据对象和映射器依赖 - 删除Objects工具类导入以减少不必要的引用 - 修改EXECUTION和TASK类型的候选对象转换逻辑,直接使用stream映射 - 移除toTaskCandidates和toExecutionCandidates方法,统一使用toCandidate方法 - 更新测试类中的相关mock配置和断言逻辑 - 更新通知模板常量注释,标记协办人相关模板为已停发状态 - 简化候选对象构建逻辑,任务和执行仅通知负责人,协办人列表为空
This commit is contained in:
@@ -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";
|
||||
|
||||
/** 工作报告团队催办:主管催办下属提交指定周期工作报告 */
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user