You've already forked DataMate
feat: 实现任务拆分和分配功能
## 功能概述 实现完整的任务拆分、分配和进度跟踪功能,支持将任务拆分为子任务并分配给不同用户。 ## Phase 1: 数据库层 - 新增 t_task_meta 表(任务元数据协调表) - 新增 t_task_assignment_log 表(分配日志表) - 新增 3 个权限条目(read/write/assign) - 新增 SQLAlchemy ORM 模型 ## Phase 2: 后端 API (Java) - 新增 task-coordination-service 模块(32 个文件) - 实现 11 个 API 端点: - 任务查询(列表、子任务、我的任务) - 任务拆分(支持 4 种策略) - 任务分配(单个、批量、重新分配、撤回) - 进度管理(查询、更新、聚合) - 分配日志 - 集成权限控制和路由规则 ## Phase 3: 前端 UI (React + TypeScript) - 新增 10 个文件(模型、API、组件、页面) - 实现 5 个核心组件: - SplitTaskDialog - 任务拆分对话框 - AssignTaskDialog - 任务分配对话框 - BatchAssignDialog - 批量分配对话框 - TaskProgressPanel - 进度面板 - AssignmentLogDrawer - 分配记录 - 实现 2 个页面: - TaskCoordination - 任务管理主页 - MyTasks - 我的任务页面 - 集成侧边栏菜单和路由 ## 问题修复 - 修复 getMyTasks 分页参数缺失 - 修复子任务 assignee 信息缺失(批量查询优化) - 修复 proportion 精度计算(余量分配) ## 技术亮点 - 零侵入设计:通过独立协调表实现,不修改现有模块 - 批量查询优化:避免 N+1 查询问题 - 4 种拆分策略:按比例/数量/文件/手动 - 进度自动聚合:子任务更新自动聚合到父任务 - 权限细粒度控制:read/write/assign 三级权限 ## 验证 - Maven 编译:✅ 零错误 - TypeScript 编译:✅ 零错误 - Vite 生产构建:✅ 成功
This commit is contained in:
@@ -48,6 +48,7 @@ public class PermissionRuleMatcher {
|
||||
addModuleRules(permissionRules, "/api/operator-market/**", "module:operator-market:read", "module:operator-market:write");
|
||||
addModuleRules(permissionRules, "/api/orchestration/**", "module:orchestration:read", "module:orchestration:write");
|
||||
addModuleRules(permissionRules, "/api/content-generation/**", "module:content-generation:use", "module:content-generation:use");
|
||||
addModuleRules(permissionRules, "/api/task-meta/**", "module:task-coordination:read", "module:task-coordination:write");
|
||||
|
||||
permissionRules.add(new PermissionRule(READ_METHODS, "/api/auth/users/**", "system:user:manage"));
|
||||
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/users/**", "system:user:manage"));
|
||||
|
||||
@@ -81,6 +81,11 @@
|
||||
<artifactId>data-evaluation-service</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.datamate</groupId>
|
||||
<artifactId>task-coordination-service</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.datamate</groupId>
|
||||
<artifactId>pipeline-orchestration-service</artifactId>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<module>data-synthesis-service</module>
|
||||
<module>data-annotation-service</module>
|
||||
<module>data-evaluation-service</module>
|
||||
<module>task-coordination-service</module>
|
||||
<module>pipeline-orchestration-service</module>
|
||||
<module>execution-engine-service</module>
|
||||
|
||||
|
||||
48
backend/services/task-coordination-service/pom.xml
Normal file
48
backend/services/task-coordination-service/pom.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.datamate</groupId>
|
||||
<artifactId>services</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>task-coordination-service</artifactId>
|
||||
<name>Task Coordination Service</name>
|
||||
<description>任务拆分与分配协调服务</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.datamate</groupId>
|
||||
<artifactId>domain-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.datamate.coordination;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
/**
|
||||
* 任务协调服务配置类
|
||||
* 提供任务拆分、分配和进度聚合功能
|
||||
*/
|
||||
@ComponentScan(basePackages = {
|
||||
"com.datamate.coordination"
|
||||
})
|
||||
public class TaskCoordinationServiceConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.datamate.coordination.application;
|
||||
|
||||
import com.datamate.coordination.common.enums.AssignmentActionEnum;
|
||||
import com.datamate.coordination.common.exception.TaskCoordinationErrorCode;
|
||||
import com.datamate.coordination.domain.repository.TaskAssignmentLogRepository;
|
||||
import com.datamate.coordination.domain.repository.TaskMetaRepository;
|
||||
import com.datamate.coordination.interfaces.dto.*;
|
||||
import com.datamate.common.auth.infrastructure.context.RequestUserContextHolder;
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TaskAssignmentService {
|
||||
|
||||
private final TaskMetaRepository taskMetaRepo;
|
||||
|
||||
private final TaskAssignmentLogRepository assignmentLogRepo;
|
||||
|
||||
@Transactional
|
||||
public void assignTask(String taskMetaId, AssignTaskRequest request) {
|
||||
TaskMetaDto dto = requireTask(taskMetaId);
|
||||
doAssign(taskMetaId, request.getUserId(), AssignmentActionEnum.ASSIGN,
|
||||
getCurrentUser(), request.getRemark());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void reassignTask(String taskMetaId, AssignTaskRequest request) {
|
||||
TaskMetaDto dto = requireTask(taskMetaId);
|
||||
Long previousUserId = dto.getAssignedTo();
|
||||
doAssign(taskMetaId, request.getUserId(), AssignmentActionEnum.REASSIGN,
|
||||
getCurrentUser(), request.getRemark());
|
||||
|
||||
// 记录日志中包含原用户
|
||||
// (doAssign 已写入日志,此处无需重复)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void revokeTask(String taskMetaId, String remark) {
|
||||
TaskMetaDto dto = requireTask(taskMetaId);
|
||||
Long previousUserId = dto.getAssignedTo();
|
||||
|
||||
dto.setAssignedTo(null);
|
||||
taskMetaRepo.update(dto);
|
||||
|
||||
TaskAssignmentLogDto logDto = new TaskAssignmentLogDto();
|
||||
logDto.setId(UUID.randomUUID().toString());
|
||||
logDto.setTaskMetaId(taskMetaId);
|
||||
logDto.setAction(AssignmentActionEnum.REVOKE);
|
||||
logDto.setFromUserId(previousUserId);
|
||||
logDto.setToUserId(null);
|
||||
logDto.setOperatedBy(getCurrentUser());
|
||||
logDto.setRemark(remark);
|
||||
assignmentLogRepo.insert(logDto);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void batchAssign(BatchAssignRequest request) {
|
||||
for (BatchAssignRequest.TaskAssignment assignment : request.getAssignments()) {
|
||||
doAssign(assignment.getTaskMetaId(), assignment.getUserId(),
|
||||
AssignmentActionEnum.ASSIGN, getCurrentUser(), assignment.getRemark());
|
||||
}
|
||||
}
|
||||
|
||||
public List<TaskAssignmentLogDto> getAssignmentLogs(String taskMetaId) {
|
||||
return assignmentLogRepo.findByTaskMetaId(taskMetaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行分配并写入日志(内部方法,也供 TaskSplitService 调用)
|
||||
*/
|
||||
void doAssign(String taskMetaId, Long userId, AssignmentActionEnum action,
|
||||
String operatedBy, String remark) {
|
||||
TaskMetaDto dto = taskMetaRepo.findById(taskMetaId);
|
||||
if (dto == null) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_NOT_FOUND);
|
||||
}
|
||||
|
||||
Long previousUserId = dto.getAssignedTo();
|
||||
dto.setAssignedTo(userId);
|
||||
taskMetaRepo.update(dto);
|
||||
|
||||
TaskAssignmentLogDto logDto = new TaskAssignmentLogDto();
|
||||
logDto.setId(UUID.randomUUID().toString());
|
||||
logDto.setTaskMetaId(taskMetaId);
|
||||
logDto.setAction(action);
|
||||
logDto.setFromUserId(previousUserId);
|
||||
logDto.setToUserId(userId);
|
||||
logDto.setOperatedBy(operatedBy);
|
||||
logDto.setRemark(remark);
|
||||
assignmentLogRepo.insert(logDto);
|
||||
}
|
||||
|
||||
private TaskMetaDto requireTask(String id) {
|
||||
TaskMetaDto dto = taskMetaRepo.findById(id);
|
||||
if (dto == null) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_NOT_FOUND);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private String getCurrentUser() {
|
||||
String userId = RequestUserContextHolder.getCurrentUserId();
|
||||
return userId != null ? userId : "system";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.datamate.coordination.application;
|
||||
|
||||
import com.datamate.coordination.common.enums.TaskMetaStatusEnum;
|
||||
import com.datamate.coordination.common.exception.TaskCoordinationErrorCode;
|
||||
import com.datamate.coordination.domain.repository.TaskMetaRepository;
|
||||
import com.datamate.coordination.interfaces.dto.AssigneeInfo;
|
||||
import com.datamate.coordination.interfaces.dto.CreateTaskMetaRequest;
|
||||
import com.datamate.coordination.interfaces.dto.TaskMetaDto;
|
||||
import com.datamate.common.auth.domain.model.AuthUserAccount;
|
||||
import com.datamate.common.auth.infrastructure.context.RequestUserContextHolder;
|
||||
import com.datamate.common.auth.infrastructure.persistence.mapper.AuthMapper;
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TaskMetaService {
|
||||
|
||||
private final TaskMetaRepository taskMetaRepo;
|
||||
|
||||
private final AuthMapper authMapper;
|
||||
|
||||
@Transactional
|
||||
public TaskMetaDto createTaskMeta(CreateTaskMetaRequest request) {
|
||||
TaskMetaDto dto = new TaskMetaDto();
|
||||
dto.setId(UUID.randomUUID().toString());
|
||||
dto.setModule(request.getModule());
|
||||
dto.setRefTaskId(request.getRefTaskId());
|
||||
dto.setTaskName(request.getTaskName());
|
||||
dto.setStatus(TaskMetaStatusEnum.PENDING);
|
||||
dto.setAssignedTo(request.getAssignedTo());
|
||||
dto.setCreatedBy(getCurrentUser());
|
||||
dto.setProgress(0);
|
||||
dto.setTotalItems(request.getTotalItems() != null ? request.getTotalItems() : 0);
|
||||
dto.setCompletedItems(0);
|
||||
dto.setFailedItems(0);
|
||||
dto.setPriority(request.getPriority() != null ? request.getPriority() : 0);
|
||||
dto.setDeadline(request.getDeadline());
|
||||
taskMetaRepo.insert(dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public TaskMetaDto getTaskMeta(String id) {
|
||||
TaskMetaDto dto = taskMetaRepo.findById(id);
|
||||
if (dto == null) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_NOT_FOUND);
|
||||
}
|
||||
dto.setChildCount(taskMetaRepo.countByParentId(id));
|
||||
populateAssigneeInfo(List.of(dto));
|
||||
return dto;
|
||||
}
|
||||
|
||||
public List<TaskMetaDto> getChildren(String parentId, Integer page, Integer size) {
|
||||
if (!taskMetaRepo.existsById(parentId)) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.PARENT_TASK_NOT_FOUND);
|
||||
}
|
||||
List<TaskMetaDto> children = taskMetaRepo.findByParentId(parentId);
|
||||
populateAssigneeInfo(children);
|
||||
return children;
|
||||
}
|
||||
|
||||
public int countChildren(String parentId) {
|
||||
return taskMetaRepo.countByParentId(parentId);
|
||||
}
|
||||
|
||||
public List<TaskMetaDto> getMyTasks(String status, String module,
|
||||
Integer page, Integer size) {
|
||||
Long currentUserId = getCurrentUserIdAsLong();
|
||||
List<TaskMetaDto> tasks = taskMetaRepo.findTasks(module, status, currentUserId, null, null, page, size);
|
||||
populateAssigneeInfo(tasks);
|
||||
return tasks;
|
||||
}
|
||||
|
||||
public int countMyTasks(String status, String module) {
|
||||
Long currentUserId = getCurrentUserIdAsLong();
|
||||
return taskMetaRepo.countTasks(module, status, currentUserId, null, null);
|
||||
}
|
||||
|
||||
public List<TaskMetaDto> findTasks(String module, String status, Long assignedTo,
|
||||
String keyword, Integer page, Integer size) {
|
||||
List<TaskMetaDto> tasks = taskMetaRepo.findTasks(module, status, assignedTo, null, keyword, page, size);
|
||||
populateAssigneeInfo(tasks);
|
||||
enrichChildCount(tasks);
|
||||
return tasks;
|
||||
}
|
||||
|
||||
public int countTasks(String module, String status, Long assignedTo, String keyword) {
|
||||
return taskMetaRepo.countTasks(module, status, assignedTo, null, keyword);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteTaskMeta(String id) {
|
||||
TaskMetaDto dto = taskMetaRepo.findById(id);
|
||||
if (dto == null) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_NOT_FOUND);
|
||||
}
|
||||
taskMetaRepo.deleteById(id);
|
||||
}
|
||||
|
||||
// ── Assignee population ──────────────────────────────
|
||||
|
||||
/**
|
||||
* 批量填充任务的 assignee 信息,避免 N+1 查询
|
||||
*/
|
||||
private void populateAssigneeInfo(List<TaskMetaDto> tasks) {
|
||||
Set<Long> userIds = tasks.stream()
|
||||
.map(TaskMetaDto::getAssignedTo)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (userIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Long, AssigneeInfo> userMap = new HashMap<>();
|
||||
for (Long userId : userIds) {
|
||||
AuthUserAccount user = authMapper.findUserById(userId);
|
||||
if (user != null) {
|
||||
AssigneeInfo info = new AssigneeInfo();
|
||||
info.setId(user.getId());
|
||||
info.setUsername(user.getUsername());
|
||||
info.setFullName(user.getFullName());
|
||||
userMap.put(userId, info);
|
||||
}
|
||||
}
|
||||
|
||||
for (TaskMetaDto task : tasks) {
|
||||
if (task.getAssignedTo() != null) {
|
||||
task.setAssignee(userMap.get(task.getAssignedTo()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充顶层任务的 childCount
|
||||
*/
|
||||
private void enrichChildCount(List<TaskMetaDto> tasks) {
|
||||
for (TaskMetaDto task : tasks) {
|
||||
if (task.getParentId() == null) {
|
||||
task.setChildCount(taskMetaRepo.countByParentId(task.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────
|
||||
|
||||
private String getCurrentUser() {
|
||||
String userId = RequestUserContextHolder.getCurrentUserId();
|
||||
return userId != null ? userId : "system";
|
||||
}
|
||||
|
||||
private Long getCurrentUserIdAsLong() {
|
||||
String userId = RequestUserContextHolder.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(userId);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("Cannot parse current user ID to Long: {}", userId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.datamate.coordination.application;
|
||||
|
||||
import com.datamate.coordination.common.enums.TaskMetaStatusEnum;
|
||||
import com.datamate.coordination.common.exception.TaskCoordinationErrorCode;
|
||||
import com.datamate.coordination.domain.repository.TaskMetaRepository;
|
||||
import com.datamate.coordination.interfaces.dto.*;
|
||||
import com.datamate.common.auth.domain.model.AuthUserAccount;
|
||||
import com.datamate.common.auth.infrastructure.persistence.mapper.AuthMapper;
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TaskProgressService {
|
||||
|
||||
private final TaskMetaRepository taskMetaRepo;
|
||||
|
||||
private final AuthMapper authMapper;
|
||||
|
||||
public TaskProgressResponse getProgress(String taskMetaId) {
|
||||
TaskMetaDto parent = taskMetaRepo.findById(taskMetaId);
|
||||
if (parent == null) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_NOT_FOUND);
|
||||
}
|
||||
|
||||
TaskProgressResponse response = new TaskProgressResponse();
|
||||
response.setTaskId(parent.getId());
|
||||
response.setTaskName(parent.getTaskName());
|
||||
response.setStatus(parent.getStatus());
|
||||
|
||||
List<TaskMetaDto> children = taskMetaRepo.findByParentId(taskMetaId);
|
||||
if (children.isEmpty()) {
|
||||
// 没有子任务,返回自身进度
|
||||
response.setOverallProgress(parent.getProgress());
|
||||
response.setTotalItems(parent.getTotalItems());
|
||||
response.setCompletedItems(parent.getCompletedItems());
|
||||
response.setFailedItems(parent.getFailedItems());
|
||||
response.setChildren(List.of());
|
||||
return response;
|
||||
}
|
||||
|
||||
// 聚合子任务进度
|
||||
int totalItems = 0;
|
||||
int completedItems = 0;
|
||||
int failedItems = 0;
|
||||
|
||||
// 批量查询 assignee 信息
|
||||
Map<Long, AssigneeInfo> userMap = buildAssigneeMap(children);
|
||||
|
||||
List<ChildTaskProgressDto> childDtos = children.stream().map(child -> {
|
||||
ChildTaskProgressDto dto = new ChildTaskProgressDto();
|
||||
dto.setTaskId(child.getId());
|
||||
dto.setTaskName(child.getTaskName());
|
||||
dto.setProgress(child.getProgress());
|
||||
dto.setTotalItems(child.getTotalItems());
|
||||
dto.setCompletedItems(child.getCompletedItems());
|
||||
dto.setFailedItems(child.getFailedItems());
|
||||
dto.setStatus(child.getStatus());
|
||||
if (child.getAssignedTo() != null) {
|
||||
dto.setAssignee(userMap.get(child.getAssignedTo()));
|
||||
}
|
||||
return dto;
|
||||
}).toList();
|
||||
|
||||
for (TaskMetaDto child : children) {
|
||||
totalItems += (child.getTotalItems() != null ? child.getTotalItems() : 0);
|
||||
completedItems += (child.getCompletedItems() != null ? child.getCompletedItems() : 0);
|
||||
failedItems += (child.getFailedItems() != null ? child.getFailedItems() : 0);
|
||||
}
|
||||
|
||||
int overallProgress = totalItems > 0
|
||||
? (int) Math.round((double) completedItems / totalItems * 100)
|
||||
: 0;
|
||||
|
||||
response.setOverallProgress(overallProgress);
|
||||
response.setTotalItems(totalItems);
|
||||
response.setCompletedItems(completedItems);
|
||||
response.setFailedItems(failedItems);
|
||||
response.setChildren(childDtos);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateProgress(String taskMetaId, UpdateProgressRequest request) {
|
||||
TaskMetaDto dto = taskMetaRepo.findById(taskMetaId);
|
||||
if (dto == null) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (request.getProgress() != null) {
|
||||
dto.setProgress(request.getProgress());
|
||||
}
|
||||
if (request.getTotalItems() != null) {
|
||||
dto.setTotalItems(request.getTotalItems());
|
||||
}
|
||||
if (request.getCompletedItems() != null) {
|
||||
dto.setCompletedItems(request.getCompletedItems());
|
||||
}
|
||||
if (request.getFailedItems() != null) {
|
||||
dto.setFailedItems(request.getFailedItems());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
TaskMetaStatusEnum newStatus = TaskMetaStatusEnum.fromValue(request.getStatus());
|
||||
dto.setStatus(newStatus);
|
||||
|
||||
if (newStatus == TaskMetaStatusEnum.IN_PROGRESS && dto.getStartedAt() == null) {
|
||||
dto.setStartedAt(LocalDateTime.now());
|
||||
}
|
||||
if (newStatus == TaskMetaStatusEnum.COMPLETED || newStatus == TaskMetaStatusEnum.FAILED) {
|
||||
dto.setCompletedAt(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
taskMetaRepo.update(dto);
|
||||
|
||||
// 如果是子任务,聚合更新父任务进度
|
||||
if (dto.getParentId() != null) {
|
||||
aggregateParentProgress(dto.getParentId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据所有子任务的状态聚合父任务进度和状态
|
||||
*/
|
||||
void aggregateParentProgress(String parentId) {
|
||||
TaskMetaDto parent = taskMetaRepo.findById(parentId);
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<TaskMetaDto> children = taskMetaRepo.findByParentId(parentId);
|
||||
if (children.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int totalItems = 0;
|
||||
int completedItems = 0;
|
||||
int failedItems = 0;
|
||||
boolean anyInProgress = false;
|
||||
boolean allCompleted = true;
|
||||
boolean allTerminated = true; // completed, failed, stopped, cancelled
|
||||
boolean anyFailed = false;
|
||||
|
||||
for (TaskMetaDto child : children) {
|
||||
totalItems += (child.getTotalItems() != null ? child.getTotalItems() : 0);
|
||||
completedItems += (child.getCompletedItems() != null ? child.getCompletedItems() : 0);
|
||||
failedItems += (child.getFailedItems() != null ? child.getFailedItems() : 0);
|
||||
|
||||
TaskMetaStatusEnum s = child.getStatus();
|
||||
if (s == TaskMetaStatusEnum.IN_PROGRESS) {
|
||||
anyInProgress = true;
|
||||
}
|
||||
if (s != TaskMetaStatusEnum.COMPLETED) {
|
||||
allCompleted = false;
|
||||
}
|
||||
if (s == TaskMetaStatusEnum.FAILED) {
|
||||
anyFailed = true;
|
||||
}
|
||||
if (s == TaskMetaStatusEnum.PENDING || s == TaskMetaStatusEnum.IN_PROGRESS) {
|
||||
allTerminated = false;
|
||||
}
|
||||
}
|
||||
|
||||
parent.setTotalItems(totalItems);
|
||||
parent.setCompletedItems(completedItems);
|
||||
parent.setFailedItems(failedItems);
|
||||
parent.setProgress(totalItems > 0
|
||||
? (int) Math.round((double) completedItems / totalItems * 100)
|
||||
: 0);
|
||||
|
||||
// 状态聚合
|
||||
if (allCompleted) {
|
||||
parent.setStatus(TaskMetaStatusEnum.COMPLETED);
|
||||
parent.setCompletedAt(LocalDateTime.now());
|
||||
} else if (anyInProgress) {
|
||||
parent.setStatus(TaskMetaStatusEnum.IN_PROGRESS);
|
||||
if (parent.getStartedAt() == null) {
|
||||
parent.setStartedAt(LocalDateTime.now());
|
||||
}
|
||||
} else if (allTerminated && anyFailed) {
|
||||
parent.setStatus(TaskMetaStatusEnum.FAILED);
|
||||
parent.setCompletedAt(LocalDateTime.now());
|
||||
} else if (allTerminated) {
|
||||
parent.setStatus(TaskMetaStatusEnum.STOPPED);
|
||||
}
|
||||
|
||||
taskMetaRepo.update(parent);
|
||||
}
|
||||
|
||||
private Map<Long, AssigneeInfo> buildAssigneeMap(List<TaskMetaDto> tasks) {
|
||||
Set<Long> userIds = tasks.stream()
|
||||
.map(TaskMetaDto::getAssignedTo)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<Long, AssigneeInfo> map = new HashMap<>();
|
||||
for (Long userId : userIds) {
|
||||
AuthUserAccount user = authMapper.findUserById(userId);
|
||||
if (user != null) {
|
||||
AssigneeInfo info = new AssigneeInfo();
|
||||
info.setId(user.getId());
|
||||
info.setUsername(user.getUsername());
|
||||
info.setFullName(user.getFullName());
|
||||
map.put(userId, info);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.datamate.coordination.application;
|
||||
|
||||
import com.datamate.coordination.common.enums.AssignmentActionEnum;
|
||||
import com.datamate.coordination.common.enums.TaskMetaStatusEnum;
|
||||
import com.datamate.coordination.common.exception.TaskCoordinationErrorCode;
|
||||
import com.datamate.coordination.domain.repository.TaskMetaRepository;
|
||||
import com.datamate.coordination.interfaces.dto.SplitTaskRequest;
|
||||
import com.datamate.coordination.interfaces.dto.TaskMetaDto;
|
||||
import com.datamate.common.infrastructure.exception.BusinessAssert;
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TaskSplitService {
|
||||
|
||||
private final TaskMetaRepository taskMetaRepo;
|
||||
|
||||
private final TaskAssignmentService assignmentService;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public List<TaskMetaDto> splitTask(String parentId, SplitTaskRequest request) {
|
||||
TaskMetaDto parent = taskMetaRepo.findById(parentId);
|
||||
if (parent == null) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_NOT_FOUND);
|
||||
}
|
||||
if (parent.getParentId() != null) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_NOT_SPLITTABLE);
|
||||
}
|
||||
if (taskMetaRepo.countByParentId(parentId) > 0) {
|
||||
throw BusinessException.of(TaskCoordinationErrorCode.TASK_ALREADY_SPLIT);
|
||||
}
|
||||
|
||||
BusinessAssert.notEmpty(request.getAssignments(), TaskCoordinationErrorCode.INVALID_SPLIT_CONFIG);
|
||||
|
||||
// 保存拆分配置到父任务
|
||||
parent.setSplitStrategy(request.getStrategy());
|
||||
try {
|
||||
parent.setSplitConfig(objectMapper.writeValueAsString(request.getSplitConfig()));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("Failed to serialize split config", e);
|
||||
}
|
||||
taskMetaRepo.update(parent);
|
||||
|
||||
// 按分配列表创建子任务
|
||||
List<TaskMetaDto> children = new ArrayList<>();
|
||||
int totalParentItems = parent.getTotalItems() != null ? parent.getTotalItems() : 0;
|
||||
int allocatedItems = 0;
|
||||
int assignmentCount = request.getAssignments().size();
|
||||
|
||||
for (int i = 0; i < assignmentCount; i++) {
|
||||
SplitTaskRequest.SplitAssignment assignment = request.getAssignments().get(i);
|
||||
|
||||
boolean isLast = (i == assignmentCount - 1);
|
||||
int childItems = calculateChildItems(assignment, totalParentItems, allocatedItems, isLast);
|
||||
allocatedItems += childItems;
|
||||
|
||||
TaskMetaDto child = new TaskMetaDto();
|
||||
child.setId(UUID.randomUUID().toString());
|
||||
child.setParentId(parentId);
|
||||
child.setModule(parent.getModule());
|
||||
child.setRefTaskId(parent.getRefTaskId());
|
||||
child.setTaskName(assignment.getTaskName() != null
|
||||
? assignment.getTaskName()
|
||||
: parent.getTaskName() + " #" + (i + 1));
|
||||
child.setStatus(TaskMetaStatusEnum.PENDING);
|
||||
child.setCreatedBy(parent.getCreatedBy());
|
||||
child.setProgress(0);
|
||||
child.setTotalItems(childItems);
|
||||
child.setCompletedItems(0);
|
||||
child.setFailedItems(0);
|
||||
child.setPriority(parent.getPriority());
|
||||
child.setDeadline(parent.getDeadline());
|
||||
|
||||
taskMetaRepo.insert(child);
|
||||
|
||||
// 如果指定了用户则分配
|
||||
if (assignment.getUserId() != null) {
|
||||
assignmentService.doAssign(child.getId(), assignment.getUserId(),
|
||||
AssignmentActionEnum.ASSIGN, parent.getCreatedBy(), null);
|
||||
}
|
||||
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
private int calculateChildItems(SplitTaskRequest.SplitAssignment assignment,
|
||||
int totalParentItems, int allocatedItems,
|
||||
boolean isLast) {
|
||||
if (assignment.getItemCount() != null && assignment.getItemCount() > 0) {
|
||||
return assignment.getItemCount();
|
||||
}
|
||||
if (assignment.getProportion() != null && assignment.getProportion() > 0) {
|
||||
if (isLast) {
|
||||
// 最后一个子任务取余量,确保总和 == 父任务总数
|
||||
return Math.max(0, totalParentItems - allocatedItems);
|
||||
}
|
||||
return (int) Math.round(totalParentItems * assignment.getProportion());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.datamate.coordination.common.enums;
|
||||
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import com.datamate.common.infrastructure.exception.SystemErrorCode;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
public enum AssignmentActionEnum {
|
||||
ASSIGN("ASSIGN"),
|
||||
REASSIGN("REASSIGN"),
|
||||
REVOKE("REVOKE");
|
||||
|
||||
private final String value;
|
||||
|
||||
AssignmentActionEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static AssignmentActionEnum fromValue(String value) {
|
||||
for (AssignmentActionEnum a : AssignmentActionEnum.values()) {
|
||||
if (a.value.equals(value)) {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
throw BusinessException.of(SystemErrorCode.INVALID_PARAMETER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.datamate.coordination.common.enums;
|
||||
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import com.datamate.common.infrastructure.exception.SystemErrorCode;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
public enum SplitStrategyEnum {
|
||||
BY_COUNT("BY_COUNT"),
|
||||
BY_FILE("BY_FILE"),
|
||||
BY_PERCENTAGE("BY_PERCENTAGE"),
|
||||
MANUAL("MANUAL");
|
||||
|
||||
private final String value;
|
||||
|
||||
SplitStrategyEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static SplitStrategyEnum fromValue(String value) {
|
||||
for (SplitStrategyEnum s : SplitStrategyEnum.values()) {
|
||||
if (s.value.equals(value)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
throw BusinessException.of(SystemErrorCode.INVALID_PARAMETER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.datamate.coordination.common.enums;
|
||||
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import com.datamate.common.infrastructure.exception.SystemErrorCode;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
public enum TaskMetaStatusEnum {
|
||||
PENDING("PENDING"),
|
||||
IN_PROGRESS("IN_PROGRESS"),
|
||||
COMPLETED("COMPLETED"),
|
||||
FAILED("FAILED"),
|
||||
STOPPED("STOPPED"),
|
||||
CANCELLED("CANCELLED");
|
||||
|
||||
private final String value;
|
||||
|
||||
TaskMetaStatusEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static TaskMetaStatusEnum fromValue(String value) {
|
||||
for (TaskMetaStatusEnum s : TaskMetaStatusEnum.values()) {
|
||||
if (s.value.equals(value)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
throw BusinessException.of(SystemErrorCode.INVALID_PARAMETER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.datamate.coordination.common.enums;
|
||||
|
||||
import com.datamate.common.infrastructure.exception.BusinessException;
|
||||
import com.datamate.common.infrastructure.exception.SystemErrorCode;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
public enum TaskModuleEnum {
|
||||
ANNOTATION("ANNOTATION"),
|
||||
CLEANING("CLEANING"),
|
||||
EVALUATION("EVALUATION"),
|
||||
SYNTHESIS("SYNTHESIS"),
|
||||
COLLECTION("COLLECTION"),
|
||||
RATIO("RATIO");
|
||||
|
||||
private final String value;
|
||||
|
||||
TaskModuleEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static TaskModuleEnum fromValue(String value) {
|
||||
for (TaskModuleEnum m : TaskModuleEnum.values()) {
|
||||
if (m.value.equals(value)) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
throw BusinessException.of(SystemErrorCode.INVALID_PARAMETER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.datamate.coordination.common.exception;
|
||||
|
||||
import com.datamate.common.infrastructure.exception.ErrorCode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum TaskCoordinationErrorCode implements ErrorCode {
|
||||
TASK_NOT_FOUND("tc.0001", "任务不存在"),
|
||||
TASK_ALREADY_SPLIT("tc.0002", "任务已拆分,不可重复拆分"),
|
||||
TASK_NOT_SPLITTABLE("tc.0003", "仅顶层任务可以拆分"),
|
||||
INVALID_SPLIT_CONFIG("tc.0004", "拆分配置无效"),
|
||||
TASK_NOT_ASSIGNABLE("tc.0005", "当前任务状态不允许分配"),
|
||||
TASK_ALREADY_ASSIGNED("tc.0006", "任务已分配给该用户"),
|
||||
USER_NOT_FOUND("tc.0007", "指定用户不存在"),
|
||||
TASK_HAS_RUNNING_CHILDREN("tc.0008", "存在进行中的子任务,无法删除"),
|
||||
PARENT_TASK_NOT_FOUND("tc.0009", "父任务不存在"),
|
||||
SPLIT_ITEMS_MISMATCH("tc.0010", "拆分条目数与总数不匹配");
|
||||
|
||||
private final String code;
|
||||
private final String message;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.datamate.coordination.domain.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.datamate.coordination.common.enums.AssignmentActionEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName(value = "t_task_assignment_log", autoResultMap = true)
|
||||
public class TaskAssignmentLog {
|
||||
|
||||
@TableId
|
||||
private String id;
|
||||
|
||||
private String taskMetaId;
|
||||
|
||||
private AssignmentActionEnum action;
|
||||
|
||||
private Long fromUserId;
|
||||
|
||||
private Long toUserId;
|
||||
|
||||
private String operatedBy;
|
||||
|
||||
private String remark;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.datamate.coordination.domain.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.datamate.coordination.common.enums.TaskMetaStatusEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName(value = "t_task_meta", autoResultMap = true)
|
||||
public class TaskMeta {
|
||||
|
||||
@TableId
|
||||
private String id;
|
||||
|
||||
private String parentId;
|
||||
|
||||
private String module;
|
||||
|
||||
private String refTaskId;
|
||||
|
||||
private String taskName;
|
||||
|
||||
private TaskMetaStatusEnum status;
|
||||
|
||||
private Long assignedTo;
|
||||
|
||||
private String createdBy;
|
||||
|
||||
private Integer progress;
|
||||
|
||||
private Integer totalItems;
|
||||
|
||||
private Integer completedItems;
|
||||
|
||||
private Integer failedItems;
|
||||
|
||||
private String splitStrategy;
|
||||
|
||||
private String splitConfig;
|
||||
|
||||
private Integer priority;
|
||||
|
||||
private LocalDateTime deadline;
|
||||
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@TableLogic(value = "NULL", delval = "NOW()")
|
||||
private LocalDateTime deletedAt;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.datamate.coordination.domain.repository;
|
||||
|
||||
import com.datamate.coordination.interfaces.dto.TaskAssignmentLogDto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TaskAssignmentLogRepository {
|
||||
|
||||
void insert(TaskAssignmentLogDto dto);
|
||||
|
||||
List<TaskAssignmentLogDto> findByTaskMetaId(String taskMetaId);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.datamate.coordination.domain.repository;
|
||||
|
||||
import com.datamate.coordination.interfaces.dto.TaskMetaDto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TaskMetaRepository {
|
||||
|
||||
TaskMetaDto findById(String id);
|
||||
|
||||
List<TaskMetaDto> findByParentId(String parentId);
|
||||
|
||||
List<TaskMetaDto> findTasks(String module, String status, Long assignedTo,
|
||||
String createdBy, String keyword,
|
||||
Integer page, Integer size);
|
||||
|
||||
int countTasks(String module, String status, Long assignedTo,
|
||||
String createdBy, String keyword);
|
||||
|
||||
void insert(TaskMetaDto dto);
|
||||
|
||||
void update(TaskMetaDto dto);
|
||||
|
||||
void deleteById(String id);
|
||||
|
||||
boolean existsById(String id);
|
||||
|
||||
int countByParentId(String parentId);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.datamate.coordination.infrastructure.converter;
|
||||
|
||||
import com.datamate.coordination.domain.model.entity.TaskAssignmentLog;
|
||||
import com.datamate.coordination.interfaces.dto.TaskAssignmentLogDto;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface TaskAssignmentLogConverter {
|
||||
TaskAssignmentLogConverter INSTANCE = Mappers.getMapper(TaskAssignmentLogConverter.class);
|
||||
|
||||
TaskAssignmentLogDto fromEntityToDto(TaskAssignmentLog entity);
|
||||
|
||||
List<TaskAssignmentLogDto> fromEntityToDto(List<TaskAssignmentLog> entities);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.datamate.coordination.infrastructure.converter;
|
||||
|
||||
import com.datamate.coordination.domain.model.entity.TaskMeta;
|
||||
import com.datamate.coordination.interfaces.dto.TaskMetaDto;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface TaskMetaConverter {
|
||||
TaskMetaConverter INSTANCE = Mappers.getMapper(TaskMetaConverter.class);
|
||||
|
||||
TaskMetaDto fromEntityToDto(TaskMeta entity);
|
||||
|
||||
List<TaskMetaDto> fromEntityToDto(List<TaskMeta> entities);
|
||||
|
||||
TaskMeta fromDtoToEntity(TaskMetaDto dto);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.datamate.coordination.infrastructure.persistence.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.datamate.coordination.domain.model.entity.TaskAssignmentLog;
|
||||
import com.datamate.coordination.domain.repository.TaskAssignmentLogRepository;
|
||||
import com.datamate.coordination.infrastructure.converter.TaskAssignmentLogConverter;
|
||||
import com.datamate.coordination.infrastructure.persistence.mapper.TaskAssignmentLogMapper;
|
||||
import com.datamate.coordination.interfaces.dto.TaskAssignmentLogDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class TaskAssignmentLogRepositoryImpl implements TaskAssignmentLogRepository {
|
||||
|
||||
private final TaskAssignmentLogMapper mapper;
|
||||
|
||||
@Override
|
||||
public void insert(TaskAssignmentLogDto dto) {
|
||||
TaskAssignmentLog entity = new TaskAssignmentLog();
|
||||
entity.setId(dto.getId());
|
||||
entity.setTaskMetaId(dto.getTaskMetaId());
|
||||
entity.setAction(dto.getAction());
|
||||
entity.setFromUserId(dto.getFromUserId());
|
||||
entity.setToUserId(dto.getToUserId());
|
||||
entity.setOperatedBy(dto.getOperatedBy());
|
||||
entity.setRemark(dto.getRemark());
|
||||
mapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TaskAssignmentLogDto> findByTaskMetaId(String taskMetaId) {
|
||||
LambdaQueryWrapper<TaskAssignmentLog> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(TaskAssignmentLog::getTaskMetaId, taskMetaId)
|
||||
.orderByDesc(TaskAssignmentLog::getCreatedAt);
|
||||
return TaskAssignmentLogConverter.INSTANCE.fromEntityToDto(mapper.selectList(wrapper));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.datamate.coordination.infrastructure.persistence.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.datamate.coordination.domain.model.entity.TaskMeta;
|
||||
import com.datamate.coordination.domain.repository.TaskMetaRepository;
|
||||
import com.datamate.coordination.infrastructure.converter.TaskMetaConverter;
|
||||
import com.datamate.coordination.infrastructure.persistence.mapper.TaskMetaMapper;
|
||||
import com.datamate.coordination.interfaces.dto.TaskMetaDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class TaskMetaRepositoryImpl implements TaskMetaRepository {
|
||||
|
||||
private final TaskMetaMapper mapper;
|
||||
|
||||
@Override
|
||||
public TaskMetaDto findById(String id) {
|
||||
TaskMeta entity = mapper.selectById(id);
|
||||
return entity == null ? null : TaskMetaConverter.INSTANCE.fromEntityToDto(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TaskMetaDto> findByParentId(String parentId) {
|
||||
LambdaQueryWrapper<TaskMeta> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(TaskMeta::getParentId, parentId)
|
||||
.isNull(TaskMeta::getDeletedAt)
|
||||
.orderByAsc(TaskMeta::getCreatedAt);
|
||||
return TaskMetaConverter.INSTANCE.fromEntityToDto(mapper.selectList(wrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TaskMetaDto> findTasks(String module, String status, Long assignedTo,
|
||||
String createdBy, String keyword,
|
||||
Integer page, Integer size) {
|
||||
LambdaQueryWrapper<TaskMeta> wrapper = buildQueryWrapper(module, status, assignedTo, createdBy, keyword);
|
||||
wrapper.orderByDesc(TaskMeta::getCreatedAt);
|
||||
|
||||
if (page != null && size != null) {
|
||||
IPage<TaskMeta> resultPage = mapper.selectPage(new Page<>(page + 1, size), wrapper);
|
||||
return TaskMetaConverter.INSTANCE.fromEntityToDto(resultPage.getRecords());
|
||||
}
|
||||
return TaskMetaConverter.INSTANCE.fromEntityToDto(mapper.selectList(wrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countTasks(String module, String status, Long assignedTo,
|
||||
String createdBy, String keyword) {
|
||||
LambdaQueryWrapper<TaskMeta> wrapper = buildQueryWrapper(module, status, assignedTo, createdBy, keyword);
|
||||
return Math.toIntExact(mapper.selectCount(wrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insert(TaskMetaDto dto) {
|
||||
mapper.insert(TaskMetaConverter.INSTANCE.fromDtoToEntity(dto));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(TaskMetaDto dto) {
|
||||
mapper.updateById(TaskMetaConverter.INSTANCE.fromDtoToEntity(dto));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String id) {
|
||||
mapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsById(String id) {
|
||||
LambdaQueryWrapper<TaskMeta> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(TaskMeta::getId, id).isNull(TaskMeta::getDeletedAt);
|
||||
return mapper.exists(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countByParentId(String parentId) {
|
||||
LambdaQueryWrapper<TaskMeta> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(TaskMeta::getParentId, parentId).isNull(TaskMeta::getDeletedAt);
|
||||
return Math.toIntExact(mapper.selectCount(wrapper));
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<TaskMeta> buildQueryWrapper(String module, String status,
|
||||
Long assignedTo, String createdBy,
|
||||
String keyword) {
|
||||
LambdaQueryWrapper<TaskMeta> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.isNull(TaskMeta::getDeletedAt);
|
||||
wrapper.eq(StringUtils.isNotBlank(module), TaskMeta::getModule, module);
|
||||
wrapper.eq(StringUtils.isNotBlank(status), TaskMeta::getStatus, status);
|
||||
wrapper.eq(assignedTo != null, TaskMeta::getAssignedTo, assignedTo);
|
||||
wrapper.eq(StringUtils.isNotBlank(createdBy), TaskMeta::getCreatedBy, createdBy);
|
||||
if (StringUtils.isNotBlank(keyword)) {
|
||||
wrapper.like(TaskMeta::getTaskName, keyword);
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.datamate.coordination.infrastructure.persistence.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.datamate.coordination.domain.model.entity.TaskAssignmentLog;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface TaskAssignmentLogMapper extends BaseMapper<TaskAssignmentLog> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.datamate.coordination.infrastructure.persistence.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.datamate.coordination.domain.model.entity.TaskMeta;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface TaskMetaMapper extends BaseMapper<TaskMeta> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class AssignTaskRequest {
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class AssigneeInfo {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
private String fullName;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class BatchAssignRequest {
|
||||
|
||||
private List<TaskAssignment> assignments;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class TaskAssignment {
|
||||
|
||||
private String taskMetaId;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import com.datamate.coordination.common.enums.TaskMetaStatusEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class ChildTaskProgressDto {
|
||||
|
||||
private String taskId;
|
||||
|
||||
private String taskName;
|
||||
|
||||
private AssigneeInfo assignee;
|
||||
|
||||
private Integer progress;
|
||||
|
||||
private Integer totalItems;
|
||||
|
||||
private Integer completedItems;
|
||||
|
||||
private Integer failedItems;
|
||||
|
||||
private TaskMetaStatusEnum status;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CreateTaskMetaRequest {
|
||||
|
||||
private String module;
|
||||
|
||||
private String refTaskId;
|
||||
|
||||
private String taskName;
|
||||
|
||||
private Long assignedTo;
|
||||
|
||||
private Integer totalItems;
|
||||
|
||||
private Integer priority;
|
||||
|
||||
private LocalDateTime deadline;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class SplitTaskRequest {
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Map<String, Object> splitConfig;
|
||||
|
||||
private List<SplitAssignment> assignments;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class SplitAssignment {
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Double proportion;
|
||||
|
||||
private Integer itemCount;
|
||||
|
||||
private String taskName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import com.datamate.coordination.common.enums.AssignmentActionEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class TaskAssignmentLogDto {
|
||||
|
||||
private String id;
|
||||
|
||||
private String taskMetaId;
|
||||
|
||||
private AssignmentActionEnum action;
|
||||
|
||||
private Long fromUserId;
|
||||
|
||||
private Long toUserId;
|
||||
|
||||
private String operatedBy;
|
||||
|
||||
private String remark;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import com.datamate.coordination.common.enums.TaskMetaStatusEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class TaskMetaDto {
|
||||
|
||||
private String id;
|
||||
|
||||
private String parentId;
|
||||
|
||||
private String module;
|
||||
|
||||
private String refTaskId;
|
||||
|
||||
private String taskName;
|
||||
|
||||
private TaskMetaStatusEnum status;
|
||||
|
||||
private Long assignedTo;
|
||||
|
||||
/** 被分配人信息(查询时填充) */
|
||||
private AssigneeInfo assignee;
|
||||
|
||||
private String createdBy;
|
||||
|
||||
private Integer progress;
|
||||
|
||||
private Integer totalItems;
|
||||
|
||||
private Integer completedItems;
|
||||
|
||||
private Integer failedItems;
|
||||
|
||||
private String splitStrategy;
|
||||
|
||||
private String splitConfig;
|
||||
|
||||
private Integer priority;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
private LocalDateTime deadline;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/** 子任务数量(查询时填充) */
|
||||
private Integer childCount;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import com.datamate.coordination.common.enums.TaskMetaStatusEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class TaskProgressResponse {
|
||||
|
||||
private String taskId;
|
||||
|
||||
private String taskName;
|
||||
|
||||
private Integer overallProgress;
|
||||
|
||||
private Integer totalItems;
|
||||
|
||||
private Integer completedItems;
|
||||
|
||||
private Integer failedItems;
|
||||
|
||||
private TaskMetaStatusEnum status;
|
||||
|
||||
private List<ChildTaskProgressDto> children;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.datamate.coordination.interfaces.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class UpdateProgressRequest {
|
||||
|
||||
private Integer progress;
|
||||
|
||||
private Integer totalItems;
|
||||
|
||||
private Integer completedItems;
|
||||
|
||||
private Integer failedItems;
|
||||
|
||||
/** 可选:同时更新状态 */
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.datamate.coordination.interfaces.rest;
|
||||
|
||||
import com.datamate.coordination.application.TaskAssignmentService;
|
||||
import com.datamate.coordination.application.TaskMetaService;
|
||||
import com.datamate.coordination.application.TaskProgressService;
|
||||
import com.datamate.coordination.application.TaskSplitService;
|
||||
import com.datamate.coordination.interfaces.dto.*;
|
||||
import com.datamate.common.interfaces.PagedResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/task-meta")
|
||||
@RequiredArgsConstructor
|
||||
public class TaskMetaController {
|
||||
|
||||
private final TaskMetaService taskMetaService;
|
||||
|
||||
private final TaskSplitService taskSplitService;
|
||||
|
||||
private final TaskAssignmentService taskAssignmentService;
|
||||
|
||||
private final TaskProgressService taskProgressService;
|
||||
|
||||
// ── CRUD ────────────────────────────────────────────────
|
||||
|
||||
@PostMapping
|
||||
public TaskMetaDto createTask(@RequestBody CreateTaskMetaRequest request) {
|
||||
return taskMetaService.createTaskMeta(request);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public TaskMetaDto getTask(@PathVariable("id") String id) {
|
||||
return taskMetaService.getTaskMeta(id);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public String deleteTask(@PathVariable("id") String id) {
|
||||
taskMetaService.deleteTaskMeta(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/children")
|
||||
public PagedResponse<TaskMetaDto> getChildren(
|
||||
@PathVariable("id") String id,
|
||||
@RequestParam(value = "page", defaultValue = "0") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "20") Integer size) {
|
||||
List<TaskMetaDto> children = taskMetaService.getChildren(id, page, size);
|
||||
int count = taskMetaService.countChildren(id);
|
||||
int totalPages = size > 0 ? (count + size - 1) / size : 1;
|
||||
return PagedResponse.of(children, page, count, totalPages);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public PagedResponse<TaskMetaDto> listTasks(
|
||||
@RequestParam(value = "page", defaultValue = "0") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "20") Integer size,
|
||||
@RequestParam(value = "module", required = false) String module,
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "assignedTo", required = false) Long assignedTo,
|
||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
List<TaskMetaDto> tasks = taskMetaService.findTasks(module, status, assignedTo, keyword, page, size);
|
||||
int count = taskMetaService.countTasks(module, status, assignedTo, keyword);
|
||||
int totalPages = size > 0 ? (count + size - 1) / size : 1;
|
||||
return PagedResponse.of(tasks, page, count, totalPages);
|
||||
}
|
||||
|
||||
@GetMapping("/my-tasks")
|
||||
public PagedResponse<TaskMetaDto> getMyTasks(
|
||||
@RequestParam(value = "page", defaultValue = "0") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "20") Integer size,
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "module", required = false) String module) {
|
||||
List<TaskMetaDto> tasks = taskMetaService.getMyTasks(status, module, page, size);
|
||||
int count = taskMetaService.countMyTasks(status, module);
|
||||
int totalPages = size > 0 ? (count + size - 1) / size : 1;
|
||||
return PagedResponse.of(tasks, page, count, totalPages);
|
||||
}
|
||||
|
||||
// ── 拆分 ───────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/{id}/split")
|
||||
public List<TaskMetaDto> splitTask(
|
||||
@PathVariable("id") String id,
|
||||
@RequestBody SplitTaskRequest request) {
|
||||
return taskSplitService.splitTask(id, request);
|
||||
}
|
||||
|
||||
// ── 分配 ───────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/{id}/assign")
|
||||
public String assignTask(
|
||||
@PathVariable("id") String id,
|
||||
@RequestBody AssignTaskRequest request) {
|
||||
taskAssignmentService.assignTask(id, request);
|
||||
return id;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reassign")
|
||||
public String reassignTask(
|
||||
@PathVariable("id") String id,
|
||||
@RequestBody AssignTaskRequest request) {
|
||||
taskAssignmentService.reassignTask(id, request);
|
||||
return id;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/revoke")
|
||||
public String revokeTask(
|
||||
@PathVariable("id") String id,
|
||||
@RequestParam(value = "remark", required = false) String remark) {
|
||||
taskAssignmentService.revokeTask(id, remark);
|
||||
return id;
|
||||
}
|
||||
|
||||
@PostMapping("/batch-assign")
|
||||
public void batchAssign(@RequestBody BatchAssignRequest request) {
|
||||
taskAssignmentService.batchAssign(request);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/assignment-logs")
|
||||
public List<TaskAssignmentLogDto> getAssignmentLogs(@PathVariable("id") String id) {
|
||||
return taskAssignmentService.getAssignmentLogs(id);
|
||||
}
|
||||
|
||||
// ── 进度 ───────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/{id}/progress")
|
||||
public TaskProgressResponse getProgress(@PathVariable("id") String id) {
|
||||
return taskProgressService.getProgress(id);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/progress")
|
||||
public String updateProgress(
|
||||
@PathVariable("id") String id,
|
||||
@RequestBody UpdateProgressRequest request) {
|
||||
taskProgressService.updateProgress(id, request);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user