fix(personal-item): 个人事项&任务添加type类型字段
This commit is contained in:
@@ -19,6 +19,9 @@ public class PersonalItemRespVO {
|
|||||||
@Schema(description = "个人事项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "整理供应商沟通纪要")
|
@Schema(description = "个人事项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "整理供应商沟通纪要")
|
||||||
private String taskTitle;
|
private String taskTitle;
|
||||||
|
|
||||||
|
@Schema(description = "个人事项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "todo")
|
||||||
|
private String type;
|
||||||
|
|
||||||
@Schema(description = "负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
@Schema(description = "负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ public class PersonalItemSaveReqVO {
|
|||||||
@Size(max = 300, message = "个人事项标题长度不能超过300个字符")
|
@Size(max = 300, message = "个人事项标题长度不能超过300个字符")
|
||||||
private String taskTitle;
|
private String taskTitle;
|
||||||
|
|
||||||
|
@Schema(description = "个人事项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "todo")
|
||||||
|
@NotBlank(message = "个人事项类型不能为空")
|
||||||
|
@Size(max = 32, message = "个人事项类型长度不能超过32个字符")
|
||||||
|
private String type;
|
||||||
|
|
||||||
@Schema(description = "个人事项进度(0~100)", example = "60.00")
|
@Schema(description = "个人事项进度(0~100)", example = "60.00")
|
||||||
@DecimalMin(value = "0.00", message = "个人事项进度不能小于 0")
|
@DecimalMin(value = "0.00", message = "个人事项进度不能小于 0")
|
||||||
@DecimalMax(value = "100.00", message = "个人事项进度不能大于 100")
|
@DecimalMax(value = "100.00", message = "个人事项进度不能大于 100")
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ public class ProjectTaskRespVO {
|
|||||||
private Long executionOwnerId;
|
private Long executionOwnerId;
|
||||||
@Schema(description = "任务标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "接口联调任务")
|
@Schema(description = "任务标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "接口联调任务")
|
||||||
private String taskTitle;
|
private String taskTitle;
|
||||||
|
@Schema(description = "任务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "feature")
|
||||||
|
private String type;
|
||||||
@Schema(description = "任务负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002")
|
@Schema(description = "任务负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
@Schema(description = "任务负责人昵称", example = "李四")
|
@Schema(description = "任务负责人昵称", example = "李四")
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ public class ProjectTaskSaveReqVO {
|
|||||||
@Size(max = 300, message = "任务标题长度不能超过300个字符")
|
@Size(max = 300, message = "任务标题长度不能超过300个字符")
|
||||||
private String taskTitle;
|
private String taskTitle;
|
||||||
|
|
||||||
|
@Schema(description = "任务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "feature")
|
||||||
|
@NotBlank(message = "任务类型不能为空")
|
||||||
|
@Size(max = 32, message = "任务类型长度不能超过32个字符")
|
||||||
|
private String type;
|
||||||
|
|
||||||
@Schema(description = "任务负责人用户编号;子任务不传时继承父任务负责人", example = "3002")
|
@Schema(description = "任务负责人用户编号;子任务不传时继承父任务负责人", example = "3002")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public class PersonalItemDO extends BaseDO {
|
|||||||
|
|
||||||
private String taskTitle;
|
private String taskTitle;
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
private String statusCode;
|
private String statusCode;
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ public class ProjectTaskDO extends BaseDO {
|
|||||||
* 任务标题
|
* 任务标题
|
||||||
*/
|
*/
|
||||||
private String taskTitle;
|
private String taskTitle;
|
||||||
|
/**
|
||||||
|
* 任务类型
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
/**
|
/**
|
||||||
* 任务负责人用户编号
|
* 任务负责人用户编号
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
|
|
||||||
PersonalItemDO item = new PersonalItemDO();
|
PersonalItemDO item = new PersonalItemDO();
|
||||||
item.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
item.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
||||||
|
item.setType(normalizeRequiredType(reqVO.getType(), "个人事项类型不能为空"));
|
||||||
item.setOwnerId(loginUserId);
|
item.setOwnerId(loginUserId);
|
||||||
item.setStatusCode(getInitialStatusCode());
|
item.setStatusCode(getInitialStatusCode());
|
||||||
item.setProgressRate(normalizeProgress(reqVO.getProgressRate()));
|
item.setProgressRate(normalizeProgress(reqVO.getProgressRate()));
|
||||||
@@ -123,6 +124,7 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
|
|
||||||
PersonalItemDO before = cloneItem(item);
|
PersonalItemDO before = cloneItem(item);
|
||||||
item.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
item.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
||||||
|
item.setType(normalizeRequiredType(reqVO.getType(), "个人事项类型不能为空"));
|
||||||
item.setProgressRate(normalizeProgress(reqVO.getProgressRate()));
|
item.setProgressRate(normalizeProgress(reqVO.getProgressRate()));
|
||||||
item.setPlannedStartDate(reqVO.getPlannedStartDate());
|
item.setPlannedStartDate(reqVO.getPlannedStartDate());
|
||||||
item.setPlannedEndDate(reqVO.getPlannedEndDate());
|
item.setPlannedEndDate(reqVO.getPlannedEndDate());
|
||||||
@@ -361,6 +363,7 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
task.setExecutionId(executionId);
|
task.setExecutionId(executionId);
|
||||||
task.setParentTaskId(null);
|
task.setParentTaskId(null);
|
||||||
task.setTaskTitle(item.getTaskTitle());
|
task.setTaskTitle(item.getTaskTitle());
|
||||||
|
task.setType(item.getType());
|
||||||
task.setOwnerId(item.getOwnerId());
|
task.setOwnerId(item.getOwnerId());
|
||||||
task.setStatusCode(item.getStatusCode());
|
task.setStatusCode(item.getStatusCode());
|
||||||
task.setProgressRate(item.getProgressRate());
|
task.setProgressRate(item.getProgressRate());
|
||||||
@@ -546,6 +549,7 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
PersonalItemDO target = new PersonalItemDO();
|
PersonalItemDO target = new PersonalItemDO();
|
||||||
target.setId(source.getId());
|
target.setId(source.getId());
|
||||||
target.setTaskTitle(source.getTaskTitle());
|
target.setTaskTitle(source.getTaskTitle());
|
||||||
|
target.setType(source.getType());
|
||||||
target.setOwnerId(source.getOwnerId());
|
target.setOwnerId(source.getOwnerId());
|
||||||
target.setStatusCode(source.getStatusCode());
|
target.setStatusCode(source.getStatusCode());
|
||||||
target.setProgressRate(source.getProgressRate());
|
target.setProgressRate(source.getProgressRate());
|
||||||
@@ -563,6 +567,8 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||||
appendFieldChange(fieldChanges, "taskTitle", valueOf(before, PersonalItemDO::getTaskTitle),
|
appendFieldChange(fieldChanges, "taskTitle", valueOf(before, PersonalItemDO::getTaskTitle),
|
||||||
valueOf(after, PersonalItemDO::getTaskTitle));
|
valueOf(after, PersonalItemDO::getTaskTitle));
|
||||||
|
appendFieldChange(fieldChanges, "type", valueOf(before, PersonalItemDO::getType),
|
||||||
|
valueOf(after, PersonalItemDO::getType));
|
||||||
appendFieldChange(fieldChanges, "ownerId", valueOf(before, PersonalItemDO::getOwnerId),
|
appendFieldChange(fieldChanges, "ownerId", valueOf(before, PersonalItemDO::getOwnerId),
|
||||||
valueOf(after, PersonalItemDO::getOwnerId));
|
valueOf(after, PersonalItemDO::getOwnerId));
|
||||||
appendFieldChange(fieldChanges, "statusCode", valueOf(before, PersonalItemDO::getStatusCode),
|
appendFieldChange(fieldChanges, "statusCode", valueOf(before, PersonalItemDO::getStatusCode),
|
||||||
@@ -732,6 +738,13 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeRequiredType(String value, String message) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
throw invalidParamException(message);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeNullableText(String value) {
|
private String normalizeNullableText(String value) {
|
||||||
if (!StringUtils.hasText(value)) {
|
if (!StringUtils.hasText(value)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
task.setExecutionId(executionId);
|
task.setExecutionId(executionId);
|
||||||
task.setParentTaskId(parentTask == null ? null : parentTask.getId());
|
task.setParentTaskId(parentTask == null ? null : parentTask.getId());
|
||||||
task.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
task.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
||||||
|
task.setType(normalizeRequiredType(reqVO.getType(), "任务类型不能为空"));
|
||||||
task.setOwnerId(ownerId);
|
task.setOwnerId(ownerId);
|
||||||
task.setStatusCode(getInitialTaskStatusCode());
|
task.setStatusCode(getInitialTaskStatusCode());
|
||||||
// 任务进度统一由 worklog 驱动;新建任务强制为 0
|
// 任务进度统一由 worklog 驱动;新建任务强制为 0
|
||||||
@@ -202,6 +203,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
ProjectTaskDO before = cloneTask(task);
|
ProjectTaskDO before = cloneTask(task);
|
||||||
task.setParentTaskId(newParentId);
|
task.setParentTaskId(newParentId);
|
||||||
task.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
task.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
||||||
|
task.setType(normalizeRequiredType(reqVO.getType(), "任务类型不能为空"));
|
||||||
task.setOwnerId(ownerId);
|
task.setOwnerId(ownerId);
|
||||||
task.setPlannedStartDate(reqVO.getPlannedStartDate());
|
task.setPlannedStartDate(reqVO.getPlannedStartDate());
|
||||||
task.setPlannedEndDate(reqVO.getPlannedEndDate());
|
task.setPlannedEndDate(reqVO.getPlannedEndDate());
|
||||||
@@ -939,6 +941,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
target.setExecutionId(source.getExecutionId());
|
target.setExecutionId(source.getExecutionId());
|
||||||
target.setParentTaskId(source.getParentTaskId());
|
target.setParentTaskId(source.getParentTaskId());
|
||||||
target.setTaskTitle(source.getTaskTitle());
|
target.setTaskTitle(source.getTaskTitle());
|
||||||
|
target.setType(source.getType());
|
||||||
target.setOwnerId(source.getOwnerId());
|
target.setOwnerId(source.getOwnerId());
|
||||||
target.setStatusCode(source.getStatusCode());
|
target.setStatusCode(source.getStatusCode());
|
||||||
target.setProgressRate(source.getProgressRate());
|
target.setProgressRate(source.getProgressRate());
|
||||||
@@ -961,6 +964,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
valueOf(after, ProjectTaskDO::getParentTaskId));
|
valueOf(after, ProjectTaskDO::getParentTaskId));
|
||||||
appendFieldChange(fieldChanges, "taskTitle", valueOf(before, ProjectTaskDO::getTaskTitle),
|
appendFieldChange(fieldChanges, "taskTitle", valueOf(before, ProjectTaskDO::getTaskTitle),
|
||||||
valueOf(after, ProjectTaskDO::getTaskTitle));
|
valueOf(after, ProjectTaskDO::getTaskTitle));
|
||||||
|
appendFieldChange(fieldChanges, "type", valueOf(before, ProjectTaskDO::getType),
|
||||||
|
valueOf(after, ProjectTaskDO::getType));
|
||||||
appendFieldChange(fieldChanges, "ownerId", valueOf(before, ProjectTaskDO::getOwnerId),
|
appendFieldChange(fieldChanges, "ownerId", valueOf(before, ProjectTaskDO::getOwnerId),
|
||||||
valueOf(after, ProjectTaskDO::getOwnerId));
|
valueOf(after, ProjectTaskDO::getOwnerId));
|
||||||
appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProjectTaskDO::getStatusCode),
|
appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProjectTaskDO::getStatusCode),
|
||||||
@@ -1091,6 +1096,13 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeRequiredType(String value, String message) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
throw invalidParamException(message);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeNullableText(String value) {
|
private String normalizeNullableText(String value) {
|
||||||
if (!StringUtils.hasText(value)) {
|
if (!StringUtils.hasText(value)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,691 +0,0 @@
|
|||||||
package com.njcn.rdms.module.project.service.project.task;
|
|
||||||
|
|
||||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
|
||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
|
||||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskSaveReqVO;
|
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusActionReqVO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
|
||||||
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.ProjectTaskStatusLogDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
|
||||||
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.ProjectTaskStatusLogMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
|
||||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
|
|
||||||
import com.njcn.rdms.module.project.framework.security.service.ProjectObjectAuthorizationService;
|
|
||||||
import com.njcn.rdms.module.project.service.project.ProjectService;
|
|
||||||
import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeService;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.MockedStatic;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.lenient;
|
|
||||||
import static org.mockito.Mockito.mockStatic;
|
|
||||||
import static org.mockito.Mockito.never;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
class ProjectTaskServiceImplTest extends BaseMockitoUnitTest {
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private ProjectTaskServiceImpl projectTaskService;
|
|
||||||
@Mock
|
|
||||||
private ProjectMapper projectMapper;
|
|
||||||
@Mock
|
|
||||||
private ProjectExecutionMapper projectExecutionMapper;
|
|
||||||
@Mock
|
|
||||||
private ExecutionAssigneeMapper executionAssigneeMapper;
|
|
||||||
@Mock
|
|
||||||
private ProjectTaskMapper projectTaskMapper;
|
|
||||||
@Mock
|
|
||||||
private TaskWorklogMapper taskWorklogMapper;
|
|
||||||
@Mock
|
|
||||||
private ProjectTaskStatusLogMapper projectTaskStatusLogMapper;
|
|
||||||
@Mock
|
|
||||||
private BizAuditLogMapper bizAuditLogMapper;
|
|
||||||
@Mock
|
|
||||||
private ObjectStatusModelMapper objectStatusModelMapper;
|
|
||||||
@Mock
|
|
||||||
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
|
||||||
@Mock
|
|
||||||
private ProjectService projectService;
|
|
||||||
@Mock
|
|
||||||
private TaskAssigneeService taskAssigneeService;
|
|
||||||
@Mock
|
|
||||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
|
||||||
@Mock
|
|
||||||
private ProjectObjectAuthorizationService projectObjectAuthorizationService;
|
|
||||||
@Mock
|
|
||||||
private com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver visibilityScopeResolver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认让 VisibilityScopeResolver 放行(seesAll=true),既有测试无需关心 scope。
|
|
||||||
*/
|
|
||||||
@BeforeEach
|
|
||||||
void setupVisibilityScopeAll() {
|
|
||||||
lenient().when(visibilityScopeResolver.resolveForProject(any(), any()))
|
|
||||||
.thenReturn(com.njcn.rdms.module.project.service.project.permission.VisibilityScope.all());
|
|
||||||
lenient().when(visibilityScopeResolver.resolveForExecution(any(), any(), any()))
|
|
||||||
.thenReturn(com.njcn.rdms.module.project.service.project.permission.VisibilityScope.all());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createTask_shouldInsertPendingTaskAndAutoStartProject() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setOwnerId(3002L);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectInitialByObjectTypeEnabled("task"))
|
|
||||||
.thenReturn(createStatus("task", "pending", true));
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3002L))
|
|
||||||
.thenReturn(createExecutionAssignee(executionId, 3002L));
|
|
||||||
when(projectTaskMapper.insert(any(ProjectTaskDO.class))).thenAnswer(invocation -> {
|
|
||||||
ProjectTaskDO task = invocation.getArgument(0);
|
|
||||||
task.setId(9001L);
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
Long taskId = projectTaskService.createTask(projectId, executionId, reqVO);
|
|
||||||
|
|
||||||
assertNotNull(taskId);
|
|
||||||
ArgumentCaptor<ProjectTaskDO> taskCaptor = ArgumentCaptor.forClass(ProjectTaskDO.class);
|
|
||||||
verify(projectTaskMapper).insert(taskCaptor.capture());
|
|
||||||
assertEquals(projectId, taskCaptor.getValue().getProjectId());
|
|
||||||
assertEquals(executionId, taskCaptor.getValue().getExecutionId());
|
|
||||||
assertEquals("接口联调任务", taskCaptor.getValue().getTaskTitle());
|
|
||||||
assertEquals(3002L, taskCaptor.getValue().getOwnerId());
|
|
||||||
assertEquals("pending", taskCaptor.getValue().getStatusCode());
|
|
||||||
assertEquals(BigDecimal.ZERO, taskCaptor.getValue().getProgressRate());
|
|
||||||
verify(projectService).autoStartProjectIfPending(projectId, "create_task");
|
|
||||||
verify(bizAuditLogMapper).insert(any(BizAuditLogDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createTask_shouldPersistAttachmentFileId() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setOwnerId(3002L);
|
|
||||||
AttachmentItem attachment = new AttachmentItem();
|
|
||||||
attachment.setId("10001");
|
|
||||||
attachment.setUrl("http://oss.example.com/task/2026/05/report.txt");
|
|
||||||
attachment.setName("report.txt");
|
|
||||||
attachment.setSize(3164L);
|
|
||||||
attachment.setContentType("text/plain");
|
|
||||||
reqVO.setAttachments(List.of(attachment));
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectInitialByObjectTypeEnabled("task"))
|
|
||||||
.thenReturn(createStatus("task", "pending", true));
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3002L))
|
|
||||||
.thenReturn(createExecutionAssignee(executionId, 3002L));
|
|
||||||
when(projectTaskMapper.insert(any(ProjectTaskDO.class))).thenAnswer(invocation -> {
|
|
||||||
ProjectTaskDO task = invocation.getArgument(0);
|
|
||||||
task.setId(9001L);
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
projectTaskService.createTask(projectId, executionId, reqVO);
|
|
||||||
|
|
||||||
ArgumentCaptor<ProjectTaskDO> taskCaptor = ArgumentCaptor.forClass(ProjectTaskDO.class);
|
|
||||||
verify(projectTaskMapper).insert(taskCaptor.capture());
|
|
||||||
verify(attachmentFileIdResolver).resolve(reqVO.getAttachments());
|
|
||||||
assertEquals("10001", taskCaptor.getValue().getAttachments().get(0).getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createSubTask_whenOwnerBlank_shouldInheritParentOwner() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
ProjectTaskDO parentTask = createTask(projectId, executionId, 8001L, 3002L);
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setParentTaskId(parentTask.getId());
|
|
||||||
reqVO.setOwnerId(null);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, parentTask.getId()))
|
|
||||||
.thenReturn(parentTask);
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectInitialByObjectTypeEnabled("task"))
|
|
||||||
.thenReturn(createStatus("task", "pending", true));
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3002L))
|
|
||||||
.thenReturn(createExecutionAssignee(executionId, 3002L));
|
|
||||||
when(projectTaskMapper.insert(any(ProjectTaskDO.class))).thenAnswer(invocation -> {
|
|
||||||
ProjectTaskDO task = invocation.getArgument(0);
|
|
||||||
task.setId(9002L);
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
projectTaskService.createTask(projectId, executionId, reqVO);
|
|
||||||
|
|
||||||
ArgumentCaptor<ProjectTaskDO> taskCaptor = ArgumentCaptor.forClass(ProjectTaskDO.class);
|
|
||||||
verify(projectTaskMapper).insert(taskCaptor.capture());
|
|
||||||
assertEquals(3002L, taskCaptor.getValue().getOwnerId());
|
|
||||||
assertEquals(parentTask.getId(), taskCaptor.getValue().getParentTaskId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createTask_whenOwnerNotActiveExecutionAssignee_shouldThrow() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setOwnerId(3999L);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3999L)).thenReturn(null);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> projectTaskService.createTask(projectId, executionId, reqVO));
|
|
||||||
|
|
||||||
assertEquals(ErrorCodeConstants.PROJECT_TASK_OWNER_INVALID.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createTask_whenParentNotInSameExecution_shouldThrow() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setParentTaskId(8001L);
|
|
||||||
reqVO.setOwnerId(3002L);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, 8001L)).thenReturn(null);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> projectTaskService.createTask(projectId, executionId, reqVO));
|
|
||||||
|
|
||||||
assertEquals(ErrorCodeConstants.PROJECT_TASK_PARENT_INVALID.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeTaskStatus_shouldUseTransitionAndWriteLogs() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long taskId = 9001L;
|
|
||||||
ProjectTaskDO task = createTask(projectId, executionId, taskId, 3002L);
|
|
||||||
ProjectTaskStatusActionReqVO reqVO = new ProjectTaskStatusActionReqVO();
|
|
||||||
reqVO.setActionCode("start");
|
|
||||||
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, taskId)).thenReturn(task);
|
|
||||||
when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("task", "pending", "start"))
|
|
||||||
.thenReturn(createTransition("start", "active", false));
|
|
||||||
when(projectTaskMapper.updateStatusByIdAndStatus(taskId, "pending", "active", null)).thenReturn(1);
|
|
||||||
|
|
||||||
projectTaskService.changeTaskStatus(projectId, executionId, taskId, reqVO);
|
|
||||||
|
|
||||||
verify(projectObjectAuthorizationService).checkOwnerOrProjectPermission(projectId, 3002L,
|
|
||||||
"project:task:status");
|
|
||||||
verify(projectTaskMapper).updateStatusByIdAndStatus(taskId, "pending", "active", null);
|
|
||||||
ArgumentCaptor<ProjectTaskStatusLogDO> statusCaptor = ArgumentCaptor.forClass(ProjectTaskStatusLogDO.class);
|
|
||||||
verify(projectTaskStatusLogMapper).insert(statusCaptor.capture());
|
|
||||||
assertEquals("start", statusCaptor.getValue().getActionType());
|
|
||||||
assertEquals("pending", statusCaptor.getValue().getFromStatus());
|
|
||||||
assertEquals("active", statusCaptor.getValue().getToStatus());
|
|
||||||
verify(bizAuditLogMapper).insert(any(BizAuditLogDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeTaskStatus_whenReasonRequiredButBlank_shouldThrow() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long taskId = 9001L;
|
|
||||||
ProjectTaskDO task = createTask(projectId, executionId, taskId, 3002L);
|
|
||||||
ProjectTaskStatusActionReqVO reqVO = new ProjectTaskStatusActionReqVO();
|
|
||||||
reqVO.setActionCode("cancel");
|
|
||||||
reqVO.setReason(" ");
|
|
||||||
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, taskId)).thenReturn(task);
|
|
||||||
when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("task", "pending", "cancel"))
|
|
||||||
.thenReturn(createTransition("cancel", "cancelled", true));
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> projectTaskService.changeTaskStatus(projectId, executionId, taskId, reqVO));
|
|
||||||
|
|
||||||
assertEquals(ErrorCodeConstants.PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeTaskStatus_whenConcurrentModified_shouldThrow() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long taskId = 9001L;
|
|
||||||
ProjectTaskDO task = createTask(projectId, executionId, taskId, 3002L);
|
|
||||||
ProjectTaskStatusActionReqVO reqVO = new ProjectTaskStatusActionReqVO();
|
|
||||||
reqVO.setActionCode("start");
|
|
||||||
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, taskId)).thenReturn(task);
|
|
||||||
when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("task", "pending", "start"))
|
|
||||||
.thenReturn(createTransition("start", "active", false));
|
|
||||||
when(projectTaskMapper.updateStatusByIdAndStatus(taskId, "pending", "active", null)).thenReturn(0);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> projectTaskService.changeTaskStatus(projectId, executionId, taskId, reqVO));
|
|
||||||
|
|
||||||
assertEquals(ErrorCodeConstants.PROJECT_TASK_STATUS_CONCURRENT_MODIFIED.getCode(), ex.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- Phase 3 进度自动汇总 + 叶子转父限制 --------------------
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeTaskStatus_whenCancelChild_shouldRecalculateParentProgressWithExcludedStatuses() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long parentTaskId = 8001L;
|
|
||||||
Long taskId = 9001L;
|
|
||||||
ProjectTaskDO task = createTask(projectId, executionId, taskId, 3002L);
|
|
||||||
task.setParentTaskId(parentTaskId);
|
|
||||||
ProjectTaskDO parent = createTask(projectId, executionId, parentTaskId, 3002L);
|
|
||||||
parent.setProgressRate(new BigDecimal("50.00"));
|
|
||||||
ProjectTaskDO remainingChild = createTask(projectId, executionId, 9002L, 3002L);
|
|
||||||
remainingChild.setProgressRate(new BigDecimal("100.00"));
|
|
||||||
ProjectTaskStatusActionReqVO reqVO = new ProjectTaskStatusActionReqVO();
|
|
||||||
reqVO.setActionCode("cancel");
|
|
||||||
reqVO.setReason("任务取消");
|
|
||||||
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, taskId)).thenReturn(task);
|
|
||||||
when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("task", "pending", "cancel"))
|
|
||||||
.thenReturn(createTransition("cancel", "cancelled", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("task", "cancelled"))
|
|
||||||
.thenReturn(createTerminalStatus("task", "cancelled"));
|
|
||||||
when(projectTaskMapper.updateStatusByIdAndStatus(taskId, "pending", "cancelled", "任务取消")).thenReturn(1);
|
|
||||||
when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task"))
|
|
||||||
.thenReturn(List.of("cancelled"));
|
|
||||||
when(projectTaskMapper.selectById(parentTaskId)).thenReturn(parent);
|
|
||||||
when(projectTaskMapper.selectChildrenProgressByParentTaskId(parentTaskId, List.of("cancelled")))
|
|
||||||
.thenReturn(List.of(remainingChild));
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3002L)) {
|
|
||||||
projectTaskService.changeTaskStatus(projectId, executionId, taskId, reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(projectTaskMapper).selectChildrenProgressByParentTaskId(parentTaskId, List.of("cancelled"));
|
|
||||||
verify(projectTaskMapper).updateProgressRateById(eq(parentTaskId),
|
|
||||||
argThat(v -> new BigDecimal("100.00").compareTo(v) == 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changeTaskStatus_whenAllChildrenExcluded_shouldResetParentProgressToZero() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long parentTaskId = 8001L;
|
|
||||||
Long taskId = 9001L;
|
|
||||||
ProjectTaskDO task = createTask(projectId, executionId, taskId, 3002L);
|
|
||||||
task.setParentTaskId(parentTaskId);
|
|
||||||
ProjectTaskDO parent = createTask(projectId, executionId, parentTaskId, 3002L);
|
|
||||||
parent.setProgressRate(new BigDecimal("80.00"));
|
|
||||||
ProjectTaskStatusActionReqVO reqVO = new ProjectTaskStatusActionReqVO();
|
|
||||||
reqVO.setActionCode("cancel");
|
|
||||||
reqVO.setReason("任务取消");
|
|
||||||
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, taskId)).thenReturn(task);
|
|
||||||
when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("task", "pending", "cancel"))
|
|
||||||
.thenReturn(createTransition("cancel", "cancelled", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("task", "cancelled"))
|
|
||||||
.thenReturn(createTerminalStatus("task", "cancelled"));
|
|
||||||
when(projectTaskMapper.updateStatusByIdAndStatus(taskId, "pending", "cancelled", "任务取消")).thenReturn(1);
|
|
||||||
when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task"))
|
|
||||||
.thenReturn(List.of("cancelled"));
|
|
||||||
when(projectTaskMapper.selectById(parentTaskId)).thenReturn(parent);
|
|
||||||
when(projectTaskMapper.selectChildrenProgressByParentTaskId(parentTaskId, List.of("cancelled")))
|
|
||||||
.thenReturn(List.of());
|
|
||||||
|
|
||||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3002L)) {
|
|
||||||
projectTaskService.changeTaskStatus(projectId, executionId, taskId, reqVO);
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(projectTaskMapper).selectChildrenProgressByParentTaskId(parentTaskId, List.of("cancelled"));
|
|
||||||
verify(projectTaskMapper).updateProgressRateById(eq(parentTaskId),
|
|
||||||
argThat(v -> new BigDecimal("0.00").compareTo(v) == 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createTask_whenParentIsLeafWithProgress_shouldThrowLeafToParentForbiddenProgress() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long parentTaskId = 8001L;
|
|
||||||
ProjectTaskDO parentTask = createTask(projectId, executionId, parentTaskId, 3002L);
|
|
||||||
parentTask.setProgressRate(new BigDecimal("30.00"));
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setParentTaskId(parentTaskId);
|
|
||||||
reqVO.setOwnerId(3002L);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, parentTaskId))
|
|
||||||
.thenReturn(parentTask);
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3002L))
|
|
||||||
.thenReturn(createExecutionAssignee(executionId, 3002L));
|
|
||||||
when(projectTaskMapper.countChildrenByParentTaskId(parentTaskId)).thenReturn(0);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> projectTaskService.createTask(projectId, executionId, reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.PROJECT_TASK_LEAF_TO_PARENT_FORBIDDEN_PROGRESS.getCode(), ex.getCode());
|
|
||||||
verify(projectTaskMapper, never()).insert(any(ProjectTaskDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createTask_whenParentIsLeafWithWorklog_shouldThrowLeafToParentForbiddenWorklog() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long parentTaskId = 8001L;
|
|
||||||
ProjectTaskDO parentTask = createTask(projectId, executionId, parentTaskId, 3002L);
|
|
||||||
parentTask.setProgressRate(BigDecimal.ZERO);
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setParentTaskId(parentTaskId);
|
|
||||||
reqVO.setOwnerId(3002L);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, parentTaskId))
|
|
||||||
.thenReturn(parentTask);
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3002L))
|
|
||||||
.thenReturn(createExecutionAssignee(executionId, 3002L));
|
|
||||||
when(projectTaskMapper.countChildrenByParentTaskId(parentTaskId)).thenReturn(0);
|
|
||||||
when(taskWorklogMapper.existsByTaskId(parentTaskId)).thenReturn(true);
|
|
||||||
|
|
||||||
ServiceException ex = assertThrows(ServiceException.class,
|
|
||||||
() -> projectTaskService.createTask(projectId, executionId, reqVO));
|
|
||||||
assertEquals(ErrorCodeConstants.PROJECT_TASK_LEAF_TO_PARENT_FORBIDDEN_WORKLOG.getCode(), ex.getCode());
|
|
||||||
verify(projectTaskMapper, never()).insert(any(ProjectTaskDO.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createTask_whenParentAlreadyHasChildren_shouldSkipLeafCheckAndRecalc() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long parentTaskId = 8001L;
|
|
||||||
// 父任务进度 60%,但已有 1 个子任务,已是父任务,不再触发"叶子转父"校验
|
|
||||||
ProjectTaskDO parentTask = createTask(projectId, executionId, parentTaskId, 3002L);
|
|
||||||
parentTask.setProgressRate(new BigDecimal("60.00"));
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setParentTaskId(parentTaskId);
|
|
||||||
reqVO.setOwnerId(3002L);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectInitialByObjectTypeEnabled("task"))
|
|
||||||
.thenReturn(createStatus("task", "pending", true));
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, parentTaskId))
|
|
||||||
.thenReturn(parentTask);
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3002L))
|
|
||||||
.thenReturn(createExecutionAssignee(executionId, 3002L));
|
|
||||||
// 已有 1 个子任务 → 跳过叶子转父校验
|
|
||||||
when(projectTaskMapper.countChildrenByParentTaskId(parentTaskId)).thenReturn(1);
|
|
||||||
// 插入新任务(id=9002)后,父任务下有 2 个子,进度分别 60.00 与 0.00 → AVG 30.00
|
|
||||||
when(projectTaskMapper.insert(any(ProjectTaskDO.class))).thenAnswer(inv -> {
|
|
||||||
ProjectTaskDO task = inv.getArgument(0);
|
|
||||||
task.setId(9002L);
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
when(projectTaskMapper.selectById(parentTaskId)).thenReturn(parentTask);
|
|
||||||
ProjectTaskDO existingChild = createTask(projectId, executionId, 9001L, 3002L);
|
|
||||||
existingChild.setProgressRate(new BigDecimal("60.00"));
|
|
||||||
ProjectTaskDO newChild = createTask(projectId, executionId, 9002L, 3002L);
|
|
||||||
newChild.setProgressRate(BigDecimal.ZERO);
|
|
||||||
when(projectTaskMapper.selectChildrenProgressByParentTaskId(parentTaskId, Collections.emptyList()))
|
|
||||||
.thenReturn(List.of(existingChild, newChild));
|
|
||||||
|
|
||||||
projectTaskService.createTask(projectId, executionId, reqVO);
|
|
||||||
|
|
||||||
ArgumentCaptor<BigDecimal> progressCaptor = ArgumentCaptor.forClass(BigDecimal.class);
|
|
||||||
verify(projectTaskMapper).updateProgressRateById(eq(parentTaskId), progressCaptor.capture());
|
|
||||||
assertEquals(0, new BigDecimal("30.00").compareTo(progressCaptor.getValue()));
|
|
||||||
verify(taskWorklogMapper, never()).existsByTaskId(parentTaskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createTask_withLeafParentZeroProgress_shouldRecalcGrandparentChain() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long grandparentId = 7000L;
|
|
||||||
Long parentTaskId = 8001L;
|
|
||||||
ProjectTaskDO parentTask = createTask(projectId, executionId, parentTaskId, 3002L);
|
|
||||||
parentTask.setProgressRate(BigDecimal.ZERO);
|
|
||||||
parentTask.setParentTaskId(grandparentId);
|
|
||||||
// 爷爷的初值刻意设为 50.00,便于稳定验证"递归爬到爷爷且发生写入"
|
|
||||||
ProjectTaskDO grandparent = createTask(projectId, executionId, grandparentId, 3002L);
|
|
||||||
grandparent.setProgressRate(new BigDecimal("50.00"));
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setParentTaskId(parentTaskId);
|
|
||||||
reqVO.setOwnerId(3002L);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectInitialByObjectTypeEnabled("task"))
|
|
||||||
.thenReturn(createStatus("task", "pending", true));
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, parentTaskId))
|
|
||||||
.thenReturn(parentTask);
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3002L))
|
|
||||||
.thenReturn(createExecutionAssignee(executionId, 3002L));
|
|
||||||
when(projectTaskMapper.countChildrenByParentTaskId(parentTaskId)).thenReturn(0);
|
|
||||||
when(taskWorklogMapper.existsByTaskId(parentTaskId)).thenReturn(false);
|
|
||||||
when(projectTaskMapper.insert(any(ProjectTaskDO.class))).thenAnswer(inv -> {
|
|
||||||
ProjectTaskDO t = inv.getArgument(0);
|
|
||||||
t.setId(9002L);
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
// 父链:parent(8001) -> grandparent(7000) -> null
|
|
||||||
when(projectTaskMapper.selectById(parentTaskId)).thenReturn(parentTask);
|
|
||||||
when(projectTaskMapper.selectById(grandparentId)).thenReturn(grandparent);
|
|
||||||
ProjectTaskDO newChild = createTask(projectId, executionId, 9002L, 3002L);
|
|
||||||
newChild.setProgressRate(new BigDecimal("80.00"));
|
|
||||||
when(projectTaskMapper.selectChildrenProgressByParentTaskId(parentTaskId, Collections.emptyList()))
|
|
||||||
.thenReturn(List.of(newChild));
|
|
||||||
when(projectTaskMapper.selectChildrenProgressByParentTaskId(grandparentId, Collections.emptyList()))
|
|
||||||
.thenReturn(List.of(parentTask));
|
|
||||||
|
|
||||||
projectTaskService.createTask(projectId, executionId, reqVO);
|
|
||||||
|
|
||||||
// parent: AVG([80.00]) = 80.00
|
|
||||||
verify(projectTaskMapper).updateProgressRateById(eq(parentTaskId),
|
|
||||||
argThat(v -> new BigDecimal("80.00").compareTo(v) == 0));
|
|
||||||
// 爷爷:mock 返回的 parentTask 仍是初始引用(progress=0,未被自动改写)
|
|
||||||
// → 爷爷应被设为 0;而其原值 50.00 不等,会触发更新
|
|
||||||
verify(projectTaskMapper).updateProgressRateById(eq(grandparentId), any(BigDecimal.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateTask_movedToNewParent_shouldValidateAndRecalcBothChains() {
|
|
||||||
Long projectId = 2001L;
|
|
||||||
Long executionId = 5001L;
|
|
||||||
Long taskId = 9001L;
|
|
||||||
Long oldParentId = 8001L;
|
|
||||||
Long newParentId = 8002L;
|
|
||||||
ProjectTaskDO task = createTask(projectId, executionId, taskId, 3002L);
|
|
||||||
task.setParentTaskId(oldParentId);
|
|
||||||
task.setProgressRate(new BigDecimal("70.00"));
|
|
||||||
ProjectTaskDO oldParent = createTask(projectId, executionId, oldParentId, 3002L);
|
|
||||||
ProjectTaskDO newParent = createTask(projectId, executionId, newParentId, 3002L);
|
|
||||||
newParent.setProgressRate(BigDecimal.ZERO);
|
|
||||||
|
|
||||||
ProjectTaskSaveReqVO reqVO = createTaskReqVO();
|
|
||||||
reqVO.setId(taskId);
|
|
||||||
reqVO.setParentTaskId(newParentId);
|
|
||||||
reqVO.setOwnerId(3002L);
|
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
|
||||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId))
|
|
||||||
.thenReturn(createEditableExecution(projectId, executionId));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
|
||||||
.thenReturn(createStatus("project", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
|
||||||
.thenReturn(createStatus("execution", "pending", true));
|
|
||||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("task", "pending"))
|
|
||||||
.thenReturn(createStatus("task", "pending", true));
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, taskId)).thenReturn(task);
|
|
||||||
when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, newParentId))
|
|
||||||
.thenReturn(newParent);
|
|
||||||
when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, 3002L))
|
|
||||||
.thenReturn(createExecutionAssignee(executionId, 3002L));
|
|
||||||
// 校验"叶子转父":新父 8002 当前是叶子,进度=0,无工时 → 通过
|
|
||||||
when(projectTaskMapper.countChildrenByParentTaskId(newParentId)).thenReturn(0);
|
|
||||||
when(taskWorklogMapper.existsByTaskId(newParentId)).thenReturn(false);
|
|
||||||
// 当前任务自身:是叶子
|
|
||||||
when(projectTaskMapper.countChildrenByParentTaskId(taskId)).thenReturn(0);
|
|
||||||
// 递归刷新两条链:旧父 8001 与 新父 8002
|
|
||||||
when(projectTaskMapper.selectById(oldParentId)).thenReturn(oldParent);
|
|
||||||
when(projectTaskMapper.selectById(newParentId)).thenReturn(newParent);
|
|
||||||
when(projectTaskMapper.selectChildrenProgressByParentTaskId(oldParentId, Collections.emptyList()))
|
|
||||||
.thenReturn(List.of()); // 旧父在迁移后无有效子任务,按新口径归零;当前值已为 0 不重复更新
|
|
||||||
when(projectTaskMapper.selectChildrenProgressByParentTaskId(newParentId, Collections.emptyList()))
|
|
||||||
.thenReturn(List.of(task));
|
|
||||||
|
|
||||||
projectTaskService.updateTask(projectId, executionId, reqVO);
|
|
||||||
|
|
||||||
verify(projectTaskMapper).updateProgressRateById(eq(newParentId),
|
|
||||||
argThat(v -> new BigDecimal("70.00").compareTo(v) == 0));
|
|
||||||
// 旧父无有效子任务且当前已为 0,不重复更新
|
|
||||||
verify(projectTaskMapper, never()).updateProgressRateById(eq(oldParentId), any(BigDecimal.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProjectTaskSaveReqVO createTaskReqVO() {
|
|
||||||
ProjectTaskSaveReqVO reqVO = new ProjectTaskSaveReqVO();
|
|
||||||
reqVO.setTaskTitle("接口联调任务");
|
|
||||||
reqVO.setTaskDesc("完成接口联调");
|
|
||||||
return reqVO;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProjectDO createEditableProject(Long projectId) {
|
|
||||||
ProjectDO project = new ProjectDO();
|
|
||||||
project.setId(projectId);
|
|
||||||
project.setProjectName("测试项目");
|
|
||||||
project.setStatusCode("pending");
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProjectExecutionDO createEditableExecution(Long projectId, Long executionId) {
|
|
||||||
ProjectExecutionDO execution = new ProjectExecutionDO();
|
|
||||||
execution.setId(executionId);
|
|
||||||
execution.setProjectId(projectId);
|
|
||||||
execution.setExecutionName("接口联调");
|
|
||||||
execution.setStatusCode("pending");
|
|
||||||
return execution;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProjectTaskDO createTask(Long projectId, Long executionId, Long taskId, Long ownerId) {
|
|
||||||
ProjectTaskDO task = new ProjectTaskDO();
|
|
||||||
task.setId(taskId);
|
|
||||||
task.setProjectId(projectId);
|
|
||||||
task.setExecutionId(executionId);
|
|
||||||
task.setTaskTitle("接口联调任务");
|
|
||||||
task.setOwnerId(ownerId);
|
|
||||||
task.setStatusCode("pending");
|
|
||||||
task.setProgressRate(BigDecimal.ZERO);
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExecutionAssigneeDO createExecutionAssignee(Long executionId, Long userId) {
|
|
||||||
ExecutionAssigneeDO member = new ExecutionAssigneeDO();
|
|
||||||
member.setExecutionId(executionId);
|
|
||||||
member.setUserId(userId);
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ObjectStatusModelDO createStatus(String objectType, String statusCode, boolean allowEdit) {
|
|
||||||
ObjectStatusModelDO status = new ObjectStatusModelDO();
|
|
||||||
status.setObjectType(objectType);
|
|
||||||
status.setStatusCode(statusCode);
|
|
||||||
status.setStatusName(statusCode);
|
|
||||||
status.setAllowEdit(allowEdit);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ObjectStatusModelDO createTerminalStatus(String objectType, String statusCode) {
|
|
||||||
ObjectStatusModelDO status = createStatus(objectType, statusCode, false);
|
|
||||||
status.setTerminalFlag(true);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ObjectStatusTransitionDO createTransition(String actionCode, String toStatus, boolean needReason) {
|
|
||||||
ObjectStatusTransitionDO transition = new ObjectStatusTransitionDO();
|
|
||||||
transition.setActionCode(actionCode);
|
|
||||||
transition.setActionName(actionCode);
|
|
||||||
transition.setToStatusCode(toStatus);
|
|
||||||
transition.setNeedReason(needReason);
|
|
||||||
return transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId) {
|
|
||||||
MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
|
|
||||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
|
||||||
return mockedStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user