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/operator-market/**", "module:operator-market:read", "module:operator-market:write");
|
||||||
addModuleRules(permissionRules, "/api/orchestration/**", "module:orchestration:read", "module:orchestration: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/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(READ_METHODS, "/api/auth/users/**", "system:user:manage"));
|
||||||
permissionRules.add(new PermissionRule(WRITE_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>
|
<artifactId>data-evaluation-service</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.datamate</groupId>
|
||||||
|
<artifactId>task-coordination-service</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.datamate</groupId>
|
<groupId>com.datamate</groupId>
|
||||||
<artifactId>pipeline-orchestration-service</artifactId>
|
<artifactId>pipeline-orchestration-service</artifactId>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<module>data-synthesis-service</module>
|
<module>data-synthesis-service</module>
|
||||||
<module>data-annotation-service</module>
|
<module>data-annotation-service</module>
|
||||||
<module>data-evaluation-service</module>
|
<module>data-evaluation-service</module>
|
||||||
|
<module>task-coordination-service</module>
|
||||||
<module>pipeline-orchestration-service</module>
|
<module>pipeline-orchestration-service</module>
|
||||||
<module>execution-engine-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ export const PermissionCodes = {
|
|||||||
operatorMarketWrite: "module:operator-market:write",
|
operatorMarketWrite: "module:operator-market:write",
|
||||||
orchestrationRead: "module:orchestration:read",
|
orchestrationRead: "module:orchestration:read",
|
||||||
orchestrationWrite: "module:orchestration:write",
|
orchestrationWrite: "module:orchestration:write",
|
||||||
|
taskCoordinationRead: "module:task-coordination:read",
|
||||||
|
taskCoordinationWrite: "module:task-coordination:write",
|
||||||
|
taskCoordinationAssign: "module:task-coordination:assign",
|
||||||
contentGenerationUse: "module:content-generation:use",
|
contentGenerationUse: "module:content-generation:use",
|
||||||
agentUse: "module:agent:use",
|
agentUse: "module:agent:use",
|
||||||
userManage: "system:user:manage",
|
userManage: "system:user:manage",
|
||||||
@@ -34,6 +37,7 @@ const routePermissionRules: Array<{ prefix: string; permission: string }> = [
|
|||||||
{ prefix: "/data/knowledge-base", permission: PermissionCodes.knowledgeBaseRead },
|
{ prefix: "/data/knowledge-base", permission: PermissionCodes.knowledgeBaseRead },
|
||||||
{ prefix: "/data/operator-market", permission: PermissionCodes.operatorMarketRead },
|
{ prefix: "/data/operator-market", permission: PermissionCodes.operatorMarketRead },
|
||||||
{ prefix: "/data/orchestration", permission: PermissionCodes.orchestrationRead },
|
{ prefix: "/data/orchestration", permission: PermissionCodes.orchestrationRead },
|
||||||
|
{ prefix: "/data/task-coordination", permission: PermissionCodes.taskCoordinationRead },
|
||||||
{ prefix: "/data/content-generation", permission: PermissionCodes.contentGenerationUse },
|
{ prefix: "/data/content-generation", permission: PermissionCodes.contentGenerationUse },
|
||||||
{ prefix: "/chat", permission: PermissionCodes.agentUse },
|
{ prefix: "/chat", permission: PermissionCodes.agentUse },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
Shield,
|
Shield,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
ListChecks,
|
||||||
// Database,
|
// Database,
|
||||||
// Store,
|
// Store,
|
||||||
// Merge,
|
// Merge,
|
||||||
@@ -55,6 +56,24 @@ export const menuItems = [
|
|||||||
description: "管理知识集与知识条目",
|
description: "管理知识集与知识条目",
|
||||||
color: "bg-indigo-500",
|
color: "bg-indigo-500",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "task-coordination",
|
||||||
|
title: "任务协调",
|
||||||
|
icon: ListChecks,
|
||||||
|
permissionCode: PermissionCodes.taskCoordinationRead,
|
||||||
|
description: "任务拆分、分配与进度跟踪",
|
||||||
|
color: "bg-amber-500",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "task-coordination",
|
||||||
|
title: "任务管理",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "task-coordination/my-tasks",
|
||||||
|
title: "我的任务",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// id: "cleansing",
|
// id: "cleansing",
|
||||||
// title: "数据清洗",
|
// title: "数据清洗",
|
||||||
|
|||||||
482
frontend/src/pages/TaskCoordination/Home/TaskCoordination.tsx
Normal file
482
frontend/src/pages/TaskCoordination/Home/TaskCoordination.tsx
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Progress,
|
||||||
|
Modal,
|
||||||
|
App,
|
||||||
|
} from "antd";
|
||||||
|
import type { ColumnsType } from "antd/es/table";
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
ScissorOutlined,
|
||||||
|
UserAddOutlined,
|
||||||
|
SwapOutlined,
|
||||||
|
UndoOutlined,
|
||||||
|
BarChartOutlined,
|
||||||
|
HistoryOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import useFetchData from "@/hooks/useFetchData";
|
||||||
|
import { SearchControls } from "@/components/SearchControls";
|
||||||
|
import {
|
||||||
|
queryTaskMetasUsingGet,
|
||||||
|
getChildrenUsingGet,
|
||||||
|
deleteTaskMetaByIdUsingDelete,
|
||||||
|
revokeTaskUsingPost,
|
||||||
|
} from "../taskCoordination.api";
|
||||||
|
import {
|
||||||
|
TaskMetaDto,
|
||||||
|
TaskMetaStatus,
|
||||||
|
} from "../taskCoordination.model";
|
||||||
|
import {
|
||||||
|
renderTaskStatus,
|
||||||
|
getModuleLabel,
|
||||||
|
statusFilterOptions,
|
||||||
|
moduleFilterOptions,
|
||||||
|
} from "../taskCoordination.const";
|
||||||
|
import SplitTaskDialog from "../components/SplitTaskDialog";
|
||||||
|
import AssignTaskDialog from "../components/AssignTaskDialog";
|
||||||
|
import BatchAssignDialog from "../components/BatchAssignDialog";
|
||||||
|
import TaskProgressPanel from "../components/TaskProgressPanel";
|
||||||
|
import AssignmentLogDrawer from "../components/AssignmentLogDrawer";
|
||||||
|
|
||||||
|
function mapTaskMeta(data: Partial<TaskMetaDto>): TaskMetaDto {
|
||||||
|
return {
|
||||||
|
id: data.id || "",
|
||||||
|
parentId: data.parentId,
|
||||||
|
module: data.module || "",
|
||||||
|
refTaskId: data.refTaskId || "",
|
||||||
|
taskName: data.taskName || "-",
|
||||||
|
status: (data.status as TaskMetaStatus) || TaskMetaStatus.PENDING,
|
||||||
|
assignedTo: data.assignedTo,
|
||||||
|
assigneeName:
|
||||||
|
data.assignee?.fullName || data.assignee?.username || data.assigneeName,
|
||||||
|
progress: data.progress ?? 0,
|
||||||
|
totalItems: data.totalItems ?? 0,
|
||||||
|
completedItems: data.completedItems ?? 0,
|
||||||
|
failedItems: data.failedItems ?? 0,
|
||||||
|
splitStrategy: data.splitStrategy,
|
||||||
|
priority: data.priority ?? 0,
|
||||||
|
deadline: data.deadline,
|
||||||
|
remark: data.remark,
|
||||||
|
createdBy: data.createdBy,
|
||||||
|
createdAt: data.createdAt || "",
|
||||||
|
updatedAt: data.updatedAt || "",
|
||||||
|
childCount: data.childCount ?? 0,
|
||||||
|
assignee: data.assignee,
|
||||||
|
} as TaskMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TaskCoordinationPage() {
|
||||||
|
const { message, modal } = App.useApp();
|
||||||
|
|
||||||
|
// Dialog states
|
||||||
|
const [splitTask, setSplitTask] = useState<TaskMetaDto | null>(null);
|
||||||
|
const [assignTask, setAssignTask] = useState<TaskMetaDto | null>(null);
|
||||||
|
const [assignMode, setAssignMode] = useState<"assign" | "reassign">("assign");
|
||||||
|
const [batchAssignTasks, setBatchAssignTasks] = useState<TaskMetaDto[]>([]);
|
||||||
|
const [progressTaskId, setProgressTaskId] = useState<string | null>(null);
|
||||||
|
const [logTask, setLogTask] = useState<{ id: string; name: string } | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||||
|
const [selectedRows, setSelectedRows] = useState<TaskMetaDto[]>([]);
|
||||||
|
|
||||||
|
// Expanded rows and children cache
|
||||||
|
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||||
|
const [childrenMap, setChildrenMap] = useState<
|
||||||
|
Record<string, TaskMetaDto[]>
|
||||||
|
>({});
|
||||||
|
const [childrenLoading, setChildrenLoading] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
tableData,
|
||||||
|
pagination,
|
||||||
|
searchParams,
|
||||||
|
fetchData,
|
||||||
|
handleFiltersChange,
|
||||||
|
handleKeywordChange,
|
||||||
|
} = useFetchData<TaskMetaDto>(
|
||||||
|
queryTaskMetasUsingGet,
|
||||||
|
mapTaskMeta,
|
||||||
|
30000,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load children when expanding
|
||||||
|
const handleExpand = useCallback(
|
||||||
|
async (expanded: boolean, record: TaskMetaDto) => {
|
||||||
|
if (!expanded) {
|
||||||
|
setExpandedRowKeys((keys) => keys.filter((k) => k !== record.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExpandedRowKeys((keys) => [...keys, record.id]);
|
||||||
|
|
||||||
|
if (childrenMap[record.id]) return; // already loaded
|
||||||
|
|
||||||
|
setChildrenLoading((prev) => ({ ...prev, [record.id]: true }));
|
||||||
|
try {
|
||||||
|
const res: any = await getChildrenUsingGet(record.id, {
|
||||||
|
page: 0,
|
||||||
|
size: 100,
|
||||||
|
});
|
||||||
|
const children = (res?.data?.content ?? []).map(mapTaskMeta);
|
||||||
|
setChildrenMap((prev) => ({ ...prev, [record.id]: children }));
|
||||||
|
} catch {
|
||||||
|
message.error("加载子任务失败");
|
||||||
|
} finally {
|
||||||
|
setChildrenLoading((prev) => ({ ...prev, [record.id]: false }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[childrenMap, message]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
setChildrenMap({});
|
||||||
|
setExpandedRowKeys([]);
|
||||||
|
fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (task: TaskMetaDto) => {
|
||||||
|
modal.confirm({
|
||||||
|
title: `确认删除任务「${task.taskName}」?`,
|
||||||
|
content: "删除后不可恢复,子任务也将一并删除。",
|
||||||
|
okText: "删除",
|
||||||
|
okType: "danger",
|
||||||
|
cancelText: "取消",
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteTaskMetaByIdUsingDelete(task.id);
|
||||||
|
message.success("删除成功");
|
||||||
|
handleRefresh();
|
||||||
|
} catch {
|
||||||
|
message.error("删除失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevoke = (task: TaskMetaDto) => {
|
||||||
|
modal.confirm({
|
||||||
|
title: `确认撤回任务「${task.taskName}」的分配?`,
|
||||||
|
okText: "撤回",
|
||||||
|
cancelText: "取消",
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await revokeTaskUsingPost(task.id);
|
||||||
|
message.success("撤回成功");
|
||||||
|
handleRefresh();
|
||||||
|
} catch {
|
||||||
|
message.error("撤回失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<TaskMetaDto> = [
|
||||||
|
{
|
||||||
|
title: "任务名称",
|
||||||
|
dataIndex: "taskName",
|
||||||
|
key: "taskName",
|
||||||
|
fixed: "left",
|
||||||
|
width: 240,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "模块",
|
||||||
|
dataIndex: "module",
|
||||||
|
key: "module",
|
||||||
|
width: 100,
|
||||||
|
render: (v) => <Tag>{getModuleLabel(v)}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
width: 100,
|
||||||
|
render: (v) => renderTaskStatus(v),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "负责人",
|
||||||
|
key: "assignee",
|
||||||
|
width: 120,
|
||||||
|
render: (_, record) => record.assigneeName || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "进度",
|
||||||
|
key: "progress",
|
||||||
|
width: 180,
|
||||||
|
render: (_, record) => {
|
||||||
|
const pct = record.totalItems
|
||||||
|
? Math.round((record.completedItems / record.totalItems) * 100)
|
||||||
|
: record.progress || 0;
|
||||||
|
const status =
|
||||||
|
record.status === "COMPLETED"
|
||||||
|
? ("success" as const)
|
||||||
|
: record.status === "FAILED"
|
||||||
|
? ("exception" as const)
|
||||||
|
: ("active" as const);
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Progress
|
||||||
|
percent={pct}
|
||||||
|
size="small"
|
||||||
|
status={status}
|
||||||
|
className="flex-1 mb-0"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-400 shrink-0">
|
||||||
|
{record.completedItems}/{record.totalItems}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "子任务",
|
||||||
|
dataIndex: "childCount",
|
||||||
|
key: "childCount",
|
||||||
|
width: 80,
|
||||||
|
align: "center",
|
||||||
|
render: (v) =>
|
||||||
|
v > 0 ? <Tag color="blue">{v}</Tag> : <span className="text-gray-400">-</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "创建时间",
|
||||||
|
dataIndex: "createdAt",
|
||||||
|
key: "createdAt",
|
||||||
|
width: 160,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "actions",
|
||||||
|
fixed: "right",
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => {
|
||||||
|
const isParent = !record.parentId;
|
||||||
|
const canSplit = isParent && record.childCount === 0;
|
||||||
|
const canAssign =
|
||||||
|
!record.assignedTo &&
|
||||||
|
record.status === TaskMetaStatus.PENDING;
|
||||||
|
const canReassign = !!record.assignedTo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{canSplit && (
|
||||||
|
<Tooltip title="拆分">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<ScissorOutlined />}
|
||||||
|
onClick={() => setSplitTask(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{canAssign && (
|
||||||
|
<Tooltip title="分配">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<UserAddOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setAssignMode("assign");
|
||||||
|
setAssignTask(record);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{canReassign && (
|
||||||
|
<>
|
||||||
|
<Tooltip title="重新分配">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<SwapOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setAssignMode("reassign");
|
||||||
|
setAssignTask(record);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="撤回分配">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<UndoOutlined />}
|
||||||
|
onClick={() => handleRevoke(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Tooltip title="进度详情">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<BarChartOutlined />}
|
||||||
|
onClick={() => setProgressTaskId(record.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="分配记录">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<HistoryOutlined />}
|
||||||
|
onClick={() =>
|
||||||
|
setLogTask({ id: record.id, name: record.taskName })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{isParent && (
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Expanded row renders child tasks
|
||||||
|
const expandedRowRender = (record: TaskMetaDto) => {
|
||||||
|
if (childrenLoading[record.id]) {
|
||||||
|
return <div className="py-4 text-center text-gray-400">加载中...</div>;
|
||||||
|
}
|
||||||
|
const children = childrenMap[record.id];
|
||||||
|
if (!children || children.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="py-4 text-center text-gray-400">暂无子任务</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
dataSource={children}
|
||||||
|
columns={columns.filter((c) => c.key !== "childCount")}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterOptions = [
|
||||||
|
{ key: "status", label: "状态", options: statusFilterOptions },
|
||||||
|
{ key: "module", label: "模块", options: moduleFilterOptions },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full gap-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold">任务协调</h1>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedRowKeys.length > 0 && (
|
||||||
|
<Button
|
||||||
|
icon={<UserAddOutlined />}
|
||||||
|
onClick={() => setBatchAssignTasks(selectedRows)}
|
||||||
|
>
|
||||||
|
批量分配 ({selectedRowKeys.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchControls
|
||||||
|
searchTerm={searchParams.keyword}
|
||||||
|
onSearchChange={handleKeywordChange}
|
||||||
|
searchPlaceholder="搜索任务名称"
|
||||||
|
filters={filterOptions}
|
||||||
|
onFiltersChange={handleFiltersChange}
|
||||||
|
showViewToggle={false}
|
||||||
|
onReload={handleRefresh}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={tableData}
|
||||||
|
pagination={pagination}
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (keys, rows) => {
|
||||||
|
setSelectedRowKeys(keys as string[]);
|
||||||
|
setSelectedRows(rows);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
expandable={{
|
||||||
|
expandedRowKeys,
|
||||||
|
expandedRowRender,
|
||||||
|
onExpand: handleExpand,
|
||||||
|
expandIcon: ({ expanded, onExpand, record }) =>
|
||||||
|
record.childCount > 0 ? (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={expanded ? <DownOutlined /> : <RightOutlined />}
|
||||||
|
onClick={(e) => onExpand(record, e)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span style={{ width: 32, display: "inline-block" }} />
|
||||||
|
),
|
||||||
|
rowExpandable: (record) => record.childCount > 0,
|
||||||
|
}}
|
||||||
|
scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Dialogs */}
|
||||||
|
<SplitTaskDialog
|
||||||
|
open={!!splitTask}
|
||||||
|
task={splitTask}
|
||||||
|
onClose={() => setSplitTask(null)}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AssignTaskDialog
|
||||||
|
open={!!assignTask}
|
||||||
|
task={assignTask}
|
||||||
|
mode={assignMode}
|
||||||
|
onClose={() => setAssignTask(null)}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BatchAssignDialog
|
||||||
|
open={batchAssignTasks.length > 0}
|
||||||
|
tasks={batchAssignTasks}
|
||||||
|
onClose={() => setBatchAssignTasks([])}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TaskProgressPanel
|
||||||
|
open={!!progressTaskId}
|
||||||
|
taskId={progressTaskId}
|
||||||
|
onClose={() => setProgressTaskId(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AssignmentLogDrawer
|
||||||
|
open={!!logTask}
|
||||||
|
taskId={logTask?.id ?? null}
|
||||||
|
taskName={logTask?.name}
|
||||||
|
onClose={() => setLogTask(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
188
frontend/src/pages/TaskCoordination/MyTasks/MyTasks.tsx
Normal file
188
frontend/src/pages/TaskCoordination/MyTasks/MyTasks.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Card, Table, Button, Progress, Tooltip, App } from "antd";
|
||||||
|
import type { ColumnsType } from "antd/es/table";
|
||||||
|
import { BarChartOutlined } from "@ant-design/icons";
|
||||||
|
import useFetchData from "@/hooks/useFetchData";
|
||||||
|
import { SearchControls } from "@/components/SearchControls";
|
||||||
|
import { getMyTasksUsingGet } from "../taskCoordination.api";
|
||||||
|
import { TaskMetaDto, TaskMetaStatus } from "../taskCoordination.model";
|
||||||
|
import {
|
||||||
|
renderTaskStatus,
|
||||||
|
getModuleLabel,
|
||||||
|
statusFilterOptions,
|
||||||
|
moduleFilterOptions,
|
||||||
|
} from "../taskCoordination.const";
|
||||||
|
import TaskProgressPanel from "../components/TaskProgressPanel";
|
||||||
|
|
||||||
|
function mapTaskMeta(data: Partial<TaskMetaDto>): TaskMetaDto {
|
||||||
|
return {
|
||||||
|
id: data.id || "",
|
||||||
|
parentId: data.parentId,
|
||||||
|
module: data.module || "",
|
||||||
|
refTaskId: data.refTaskId || "",
|
||||||
|
taskName: data.taskName || "-",
|
||||||
|
status: (data.status as TaskMetaStatus) || TaskMetaStatus.PENDING,
|
||||||
|
assignedTo: data.assignedTo,
|
||||||
|
assigneeName:
|
||||||
|
data.assignee?.fullName || data.assignee?.username || data.assigneeName,
|
||||||
|
progress: data.progress ?? 0,
|
||||||
|
totalItems: data.totalItems ?? 0,
|
||||||
|
completedItems: data.completedItems ?? 0,
|
||||||
|
failedItems: data.failedItems ?? 0,
|
||||||
|
priority: data.priority ?? 0,
|
||||||
|
deadline: data.deadline,
|
||||||
|
remark: data.remark,
|
||||||
|
createdBy: data.createdBy,
|
||||||
|
createdAt: data.createdAt || "",
|
||||||
|
updatedAt: data.updatedAt || "",
|
||||||
|
childCount: data.childCount ?? 0,
|
||||||
|
assignee: data.assignee,
|
||||||
|
} as TaskMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MyTasksPage() {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [progressTaskId, setProgressTaskId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
tableData,
|
||||||
|
pagination,
|
||||||
|
searchParams,
|
||||||
|
fetchData,
|
||||||
|
handleFiltersChange,
|
||||||
|
handleKeywordChange,
|
||||||
|
} = useFetchData<TaskMetaDto>(
|
||||||
|
getMyTasksUsingGet,
|
||||||
|
mapTaskMeta,
|
||||||
|
30000,
|
||||||
|
true,
|
||||||
|
[],
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: ColumnsType<TaskMetaDto> = [
|
||||||
|
{
|
||||||
|
title: "任务名称",
|
||||||
|
dataIndex: "taskName",
|
||||||
|
key: "taskName",
|
||||||
|
fixed: "left",
|
||||||
|
width: 240,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "模块",
|
||||||
|
dataIndex: "module",
|
||||||
|
key: "module",
|
||||||
|
width: 100,
|
||||||
|
render: (v) => getModuleLabel(v),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
width: 100,
|
||||||
|
render: (v) => renderTaskStatus(v),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "进度",
|
||||||
|
key: "progress",
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => {
|
||||||
|
const pct = record.totalItems
|
||||||
|
? Math.round((record.completedItems / record.totalItems) * 100)
|
||||||
|
: record.progress || 0;
|
||||||
|
const status =
|
||||||
|
record.status === "COMPLETED"
|
||||||
|
? ("success" as const)
|
||||||
|
: record.status === "FAILED"
|
||||||
|
? ("exception" as const)
|
||||||
|
: ("active" as const);
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Progress
|
||||||
|
percent={pct}
|
||||||
|
size="small"
|
||||||
|
status={status}
|
||||||
|
className="flex-1 mb-0"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-400 shrink-0">
|
||||||
|
{record.completedItems}/{record.totalItems}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "截止时间",
|
||||||
|
dataIndex: "deadline",
|
||||||
|
key: "deadline",
|
||||||
|
width: 160,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (v) => v || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "创建时间",
|
||||||
|
dataIndex: "createdAt",
|
||||||
|
key: "createdAt",
|
||||||
|
width: 160,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "actions",
|
||||||
|
fixed: "right",
|
||||||
|
width: 80,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Tooltip title="进度详情">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<BarChartOutlined />}
|
||||||
|
onClick={() => setProgressTaskId(record.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filterOptions = [
|
||||||
|
{ key: "status", label: "状态", options: statusFilterOptions },
|
||||||
|
{ key: "module", label: "模块", options: moduleFilterOptions },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full gap-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold">我的任务</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchControls
|
||||||
|
searchTerm={searchParams.keyword}
|
||||||
|
onSearchChange={handleKeywordChange}
|
||||||
|
searchPlaceholder="搜索任务名称"
|
||||||
|
filters={filterOptions}
|
||||||
|
onFiltersChange={handleFiltersChange}
|
||||||
|
showViewToggle={false}
|
||||||
|
onReload={() => fetchData()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={tableData}
|
||||||
|
pagination={pagination}
|
||||||
|
scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<TaskProgressPanel
|
||||||
|
open={!!progressTaskId}
|
||||||
|
taskId={progressTaskId}
|
||||||
|
onClose={() => setProgressTaskId(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Modal, Form, Select, Input, App } from "antd";
|
||||||
|
import { UserOption, TaskMetaDto } from "../taskCoordination.model";
|
||||||
|
import {
|
||||||
|
assignTaskUsingPost,
|
||||||
|
reassignTaskUsingPost,
|
||||||
|
listUsersUsingGet,
|
||||||
|
} from "../taskCoordination.api";
|
||||||
|
|
||||||
|
interface AssignTaskDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
task: TaskMetaDto | null;
|
||||||
|
mode: "assign" | "reassign";
|
||||||
|
onClose: () => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AssignTaskDialog({
|
||||||
|
open,
|
||||||
|
task,
|
||||||
|
mode,
|
||||||
|
onClose,
|
||||||
|
onRefresh,
|
||||||
|
}: AssignTaskDialogProps) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [users, setUsers] = useState<UserOption[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
form.resetFields();
|
||||||
|
listUsersUsingGet()
|
||||||
|
.then((res: any) => {
|
||||||
|
setUsers(
|
||||||
|
(res?.data ?? []).map((u: any) => ({
|
||||||
|
id: u.id,
|
||||||
|
username: u.username,
|
||||||
|
fullName: u.fullName,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [open, form]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!task) return;
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
const fn =
|
||||||
|
mode === "reassign" ? reassignTaskUsingPost : assignTaskUsingPost;
|
||||||
|
await fn(task.id, {
|
||||||
|
userId: values.userId,
|
||||||
|
remark: values.remark,
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success(mode === "reassign" ? "重新分配成功" : "分配成功");
|
||||||
|
onRefresh();
|
||||||
|
onClose();
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.errorFields) return;
|
||||||
|
message.error(err?.message || "操作失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={`${mode === "reassign" ? "重新分配" : "分配"}任务:${task?.taskName || ""}`}
|
||||||
|
open={open}
|
||||||
|
onCancel={onClose}
|
||||||
|
onOk={handleSubmit}
|
||||||
|
confirmLoading={submitting}
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
width={480}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" className="mt-4">
|
||||||
|
<Form.Item
|
||||||
|
label="分配给"
|
||||||
|
name="userId"
|
||||||
|
rules={[{ required: true, message: "请选择用户" }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
placeholder="搜索并选择用户"
|
||||||
|
optionFilterProp="label"
|
||||||
|
options={users.map((u) => ({
|
||||||
|
label: u.fullName ? `${u.fullName} (${u.username})` : u.username,
|
||||||
|
value: u.id,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="备注" name="remark">
|
||||||
|
<Input.TextArea rows={3} placeholder="可选,添加备注说明" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Drawer, Table, Tag, Spin, Timeline, App } from "antd";
|
||||||
|
import { TaskAssignmentLogDto } from "../taskCoordination.model";
|
||||||
|
import { getAssignmentLogsUsingGet } from "../taskCoordination.api";
|
||||||
|
import { AssignmentActionMap } from "../taskCoordination.const";
|
||||||
|
|
||||||
|
interface AssignmentLogDrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
taskId: string | null;
|
||||||
|
taskName?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionColorMap: Record<string, string> = {
|
||||||
|
ASSIGN: "blue",
|
||||||
|
REASSIGN: "orange",
|
||||||
|
REVOKE: "red",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AssignmentLogDrawer({
|
||||||
|
open,
|
||||||
|
taskId,
|
||||||
|
taskName,
|
||||||
|
onClose,
|
||||||
|
}: AssignmentLogDrawerProps) {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [logs, setLogs] = useState<TaskAssignmentLogDto[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !taskId) return;
|
||||||
|
setLoading(true);
|
||||||
|
getAssignmentLogsUsingGet(taskId)
|
||||||
|
.then((res: any) => {
|
||||||
|
setLogs(res?.data ?? []);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error("加载分配记录失败");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [open, taskId, message]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={`分配记录:${taskName || ""}`}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
width={520}
|
||||||
|
>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
{logs.length === 0 && !loading ? (
|
||||||
|
<div className="text-center text-gray-400 py-8">暂无分配记录</div>
|
||||||
|
) : (
|
||||||
|
<Timeline
|
||||||
|
items={logs.map((log) => ({
|
||||||
|
color: actionColorMap[log.action] || "gray",
|
||||||
|
children: (
|
||||||
|
<div className="pb-2">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Tag
|
||||||
|
color={actionColorMap[log.action] || "default"}
|
||||||
|
>
|
||||||
|
{AssignmentActionMap[log.action] || log.action}
|
||||||
|
</Tag>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{log.createdAt}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-gray-500">操作人:</span>
|
||||||
|
{log.operatorName || "-"}
|
||||||
|
{log.userName && (
|
||||||
|
<>
|
||||||
|
<span className="text-gray-500 ml-3">目标用户:</span>
|
||||||
|
{log.userName}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{log.remark && (
|
||||||
|
<div className="text-sm text-gray-500 mt-1">
|
||||||
|
备注:{log.remark}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Modal, Form, Select, Input, Table, App } from "antd";
|
||||||
|
import { UserOption, TaskMetaDto } from "../taskCoordination.model";
|
||||||
|
import { batchAssignUsingPost, listUsersUsingGet } from "../taskCoordination.api";
|
||||||
|
|
||||||
|
interface BatchAssignDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
tasks: TaskMetaDto[];
|
||||||
|
onClose: () => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RowAssignment {
|
||||||
|
taskMetaId: string;
|
||||||
|
taskName: string;
|
||||||
|
userId?: number;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BatchAssignDialog({
|
||||||
|
open,
|
||||||
|
tasks,
|
||||||
|
onClose,
|
||||||
|
onRefresh,
|
||||||
|
}: BatchAssignDialogProps) {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [users, setUsers] = useState<UserOption[]>([]);
|
||||||
|
const [rows, setRows] = useState<RowAssignment[]>([]);
|
||||||
|
const [globalUserId, setGlobalUserId] = useState<number | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setGlobalUserId(undefined);
|
||||||
|
setRows(
|
||||||
|
tasks.map((t) => ({
|
||||||
|
taskMetaId: t.id,
|
||||||
|
taskName: t.taskName,
|
||||||
|
userId: undefined,
|
||||||
|
remark: undefined,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
listUsersUsingGet()
|
||||||
|
.then((res: any) => {
|
||||||
|
setUsers(
|
||||||
|
(res?.data ?? []).map((u: any) => ({
|
||||||
|
id: u.id,
|
||||||
|
username: u.username,
|
||||||
|
fullName: u.fullName,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [open, tasks]);
|
||||||
|
|
||||||
|
const handleRowChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof RowAssignment,
|
||||||
|
value: unknown
|
||||||
|
) => {
|
||||||
|
const updated = [...rows];
|
||||||
|
updated[index] = { ...updated[index], [field]: value };
|
||||||
|
setRows(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGlobalUserChange = (userId: number) => {
|
||||||
|
setGlobalUserId(userId);
|
||||||
|
setRows(rows.map((r) => ({ ...r, userId })));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const validRows = rows.filter((r) => r.userId);
|
||||||
|
if (validRows.length === 0) {
|
||||||
|
message.warning("请至少为一个任务选择分配用户");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
await batchAssignUsingPost({
|
||||||
|
assignments: validRows.map((r) => ({
|
||||||
|
taskMetaId: r.taskMetaId,
|
||||||
|
userId: r.userId!,
|
||||||
|
remark: r.remark,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
message.success(`成功分配 ${validRows.length} 个任务`);
|
||||||
|
onRefresh();
|
||||||
|
onClose();
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err?.message || "批量分配失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const userOptions = users.map((u) => ({
|
||||||
|
label: u.fullName ? `${u.fullName} (${u.username})` : u.username,
|
||||||
|
value: u.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "任务名称",
|
||||||
|
dataIndex: "taskName",
|
||||||
|
key: "taskName",
|
||||||
|
width: 200,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "分配给",
|
||||||
|
key: "userId",
|
||||||
|
width: 200,
|
||||||
|
render: (_: unknown, _record: RowAssignment, index: number) => (
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
allowClear
|
||||||
|
placeholder="选择用户"
|
||||||
|
optionFilterProp="label"
|
||||||
|
value={rows[index]?.userId}
|
||||||
|
onChange={(v) => handleRowChange(index, "userId", v)}
|
||||||
|
options={userOptions}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "备注",
|
||||||
|
key: "remark",
|
||||||
|
render: (_: unknown, _record: RowAssignment, index: number) => (
|
||||||
|
<Input
|
||||||
|
placeholder="可选备注"
|
||||||
|
value={rows[index]?.remark}
|
||||||
|
onChange={(e) => handleRowChange(index, "remark", e.target.value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={`批量分配任务(${tasks.length} 个)`}
|
||||||
|
open={open}
|
||||||
|
onCancel={onClose}
|
||||||
|
onOk={handleSubmit}
|
||||||
|
confirmLoading={submitting}
|
||||||
|
okText="确认分配"
|
||||||
|
cancelText="取消"
|
||||||
|
width={720}
|
||||||
|
>
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<span className="text-sm text-gray-600 shrink-0">统一分配给:</span>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
allowClear
|
||||||
|
placeholder="选择用户(可选)"
|
||||||
|
optionFilterProp="label"
|
||||||
|
value={globalUserId}
|
||||||
|
onChange={handleGlobalUserChange}
|
||||||
|
options={userOptions}
|
||||||
|
style={{ width: 240 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
dataSource={rows}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="taskMetaId"
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
scroll={{ y: 400 }}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Select,
|
||||||
|
InputNumber,
|
||||||
|
Input,
|
||||||
|
Radio,
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
App,
|
||||||
|
} from "antd";
|
||||||
|
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
SplitStrategy,
|
||||||
|
SplitAssignment,
|
||||||
|
UserOption,
|
||||||
|
TaskMetaDto,
|
||||||
|
} from "../taskCoordination.model";
|
||||||
|
import { SplitStrategyMap } from "../taskCoordination.const";
|
||||||
|
import { splitTaskUsingPost, listUsersUsingGet } from "../taskCoordination.api";
|
||||||
|
|
||||||
|
interface SplitTaskDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
task: TaskMetaDto | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SplitTaskDialog({
|
||||||
|
open,
|
||||||
|
task,
|
||||||
|
onClose,
|
||||||
|
onRefresh,
|
||||||
|
}: SplitTaskDialogProps) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [users, setUsers] = useState<UserOption[]>([]);
|
||||||
|
const [assignments, setAssignments] = useState<SplitAssignment[]>([
|
||||||
|
{ taskName: "", proportion: undefined, itemCount: undefined },
|
||||||
|
{ taskName: "", proportion: undefined, itemCount: undefined },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const strategy: SplitStrategy = Form.useWatch("strategy", form);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
listUsersUsingGet()
|
||||||
|
.then((res: any) => {
|
||||||
|
setUsers(
|
||||||
|
(res?.data ?? []).map((u: any) => ({
|
||||||
|
id: u.id,
|
||||||
|
username: u.username,
|
||||||
|
fullName: u.fullName,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
form.setFieldsValue({ strategy: SplitStrategy.BY_PERCENTAGE });
|
||||||
|
setAssignments([
|
||||||
|
{ taskName: "", proportion: undefined, itemCount: undefined },
|
||||||
|
{ taskName: "", proportion: undefined, itemCount: undefined },
|
||||||
|
]);
|
||||||
|
}, [open, form]);
|
||||||
|
|
||||||
|
const handleAddRow = () => {
|
||||||
|
setAssignments([
|
||||||
|
...assignments,
|
||||||
|
{ taskName: "", proportion: undefined, itemCount: undefined },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveRow = (index: number) => {
|
||||||
|
if (assignments.length <= 2) return;
|
||||||
|
setAssignments(assignments.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignmentChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof SplitAssignment,
|
||||||
|
value: unknown
|
||||||
|
) => {
|
||||||
|
const updated = [...assignments];
|
||||||
|
updated[index] = { ...updated[index], [field]: value };
|
||||||
|
setAssignments(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!task) return;
|
||||||
|
try {
|
||||||
|
await form.validateFields();
|
||||||
|
|
||||||
|
const validAssignments = assignments.filter(
|
||||||
|
(a) => a.userId || a.itemCount || a.proportion
|
||||||
|
);
|
||||||
|
if (validAssignments.length < 2) {
|
||||||
|
message.warning("至少需要拆分为 2 个子任务");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strategy === SplitStrategy.BY_PERCENTAGE) {
|
||||||
|
const totalProportion = validAssignments.reduce(
|
||||||
|
(sum, a) => sum + (a.proportion || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (Math.abs(totalProportion - 100) > 0.01) {
|
||||||
|
message.warning("比例之和必须为 100%");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
await splitTaskUsingPost(task.id, {
|
||||||
|
strategy,
|
||||||
|
assignments: validAssignments.map((a, i) => ({
|
||||||
|
...a,
|
||||||
|
taskName: a.taskName || `${task.taskName} - 子任务 ${i + 1}`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
message.success("任务拆分成功");
|
||||||
|
onRefresh();
|
||||||
|
onClose();
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.errorFields) return;
|
||||||
|
message.error(err?.message || "拆分失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "子任务名称",
|
||||||
|
key: "taskName",
|
||||||
|
width: 200,
|
||||||
|
render: (_: unknown, _record: SplitAssignment, index: number) => (
|
||||||
|
<Input
|
||||||
|
placeholder={`${task?.taskName || "任务"} - 子任务 ${index + 1}`}
|
||||||
|
value={assignments[index]?.taskName}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleAssignmentChange(index, "taskName", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "分配用户",
|
||||||
|
key: "userId",
|
||||||
|
width: 180,
|
||||||
|
render: (_: unknown, _record: SplitAssignment, index: number) => (
|
||||||
|
<Select
|
||||||
|
placeholder="选择用户"
|
||||||
|
allowClear
|
||||||
|
value={assignments[index]?.userId}
|
||||||
|
onChange={(v) => handleAssignmentChange(index, "userId", v)}
|
||||||
|
options={users.map((u) => ({
|
||||||
|
label: u.fullName || u.username,
|
||||||
|
value: u.id,
|
||||||
|
}))}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...(strategy === SplitStrategy.BY_PERCENTAGE
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "比例 (%)",
|
||||||
|
key: "proportion",
|
||||||
|
width: 120,
|
||||||
|
render: (_: unknown, _record: SplitAssignment, index: number) => (
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
placeholder="比例"
|
||||||
|
value={assignments[index]?.proportion}
|
||||||
|
onChange={(v) =>
|
||||||
|
handleAssignmentChange(index, "proportion", v)
|
||||||
|
}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(strategy === SplitStrategy.BY_COUNT || strategy === SplitStrategy.MANUAL
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "数量",
|
||||||
|
key: "itemCount",
|
||||||
|
width: 120,
|
||||||
|
render: (_: unknown, _record: SplitAssignment, index: number) => (
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
placeholder="数量"
|
||||||
|
value={assignments[index]?.itemCount}
|
||||||
|
onChange={(v) =>
|
||||||
|
handleAssignmentChange(index, "itemCount", v)
|
||||||
|
}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
title: "",
|
||||||
|
key: "actions",
|
||||||
|
width: 50,
|
||||||
|
render: (_: unknown, _record: SplitAssignment, index: number) => (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
disabled={assignments.length <= 2}
|
||||||
|
onClick={() => handleRemoveRow(index)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={`拆分任务:${task?.taskName || ""}`}
|
||||||
|
open={open}
|
||||||
|
onCancel={onClose}
|
||||||
|
width={720}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button onClick={onClose} disabled={submitting}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" onClick={handleSubmit} loading={submitting}>
|
||||||
|
确认拆分
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
label="拆分策略"
|
||||||
|
name="strategy"
|
||||||
|
rules={[{ required: true, message: "请选择拆分策略" }]}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
{Object.entries(SplitStrategyMap).map(([value, label]) => (
|
||||||
|
<Radio.Button key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</Radio.Button>
|
||||||
|
))}
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{task && (
|
||||||
|
<div className="mb-4 p-3 bg-gray-50 rounded text-sm text-gray-600">
|
||||||
|
总数据量:<strong>{task.totalItems ?? "-"}</strong> 条
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
dataSource={assignments}
|
||||||
|
columns={columns}
|
||||||
|
rowKey={(_, index) => String(index)}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
footer={() => (
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleAddRow}
|
||||||
|
>
|
||||||
|
添加子任务
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Drawer, Progress, Spin, Table, Tag, App } from "antd";
|
||||||
|
import type { ColumnsType } from "antd/es/table";
|
||||||
|
import {
|
||||||
|
TaskProgressResponse,
|
||||||
|
ChildTaskProgressDto,
|
||||||
|
} from "../taskCoordination.model";
|
||||||
|
import { getProgressUsingGet } from "../taskCoordination.api";
|
||||||
|
import { renderTaskStatus } from "../taskCoordination.const";
|
||||||
|
|
||||||
|
interface TaskProgressPanelProps {
|
||||||
|
open: boolean;
|
||||||
|
taskId: string | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TaskProgressPanel({
|
||||||
|
open,
|
||||||
|
taskId,
|
||||||
|
onClose,
|
||||||
|
}: TaskProgressPanelProps) {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<TaskProgressResponse | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !taskId) return;
|
||||||
|
setLoading(true);
|
||||||
|
getProgressUsingGet(taskId)
|
||||||
|
.then((res: any) => {
|
||||||
|
setData(res?.data ?? null);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error("加载进度信息失败");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [open, taskId, message]);
|
||||||
|
|
||||||
|
const progressStatus = (() => {
|
||||||
|
if (!data) return "normal" as const;
|
||||||
|
switch (data.status) {
|
||||||
|
case "COMPLETED":
|
||||||
|
return "success" as const;
|
||||||
|
case "FAILED":
|
||||||
|
return "exception" as const;
|
||||||
|
case "IN_PROGRESS":
|
||||||
|
return "active" as const;
|
||||||
|
default:
|
||||||
|
return "normal" as const;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const childColumns: ColumnsType<ChildTaskProgressDto> = [
|
||||||
|
{
|
||||||
|
title: "子任务",
|
||||||
|
dataIndex: "taskName",
|
||||||
|
key: "taskName",
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "负责人",
|
||||||
|
key: "assignee",
|
||||||
|
width: 100,
|
||||||
|
render: (_, record) => record.assignee?.fullName || record.assignee?.username || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
width: 100,
|
||||||
|
render: (status) => renderTaskStatus(status),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "进度",
|
||||||
|
key: "progress",
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => {
|
||||||
|
const pct = record.totalItems
|
||||||
|
? Math.round((record.completedItems / record.totalItems) * 100)
|
||||||
|
: record.progress || 0;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Progress percent={pct} size="small" className="flex-1 mb-0" />
|
||||||
|
<span className="text-xs text-gray-500 shrink-0">
|
||||||
|
{record.completedItems}/{record.totalItems}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "失败",
|
||||||
|
dataIndex: "failedItems",
|
||||||
|
key: "failedItems",
|
||||||
|
width: 60,
|
||||||
|
align: "center",
|
||||||
|
render: (v) =>
|
||||||
|
v > 0 ? <Tag color="error">{v}</Tag> : <span className="text-gray-400">0</span>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={`任务进度:${data?.taskName || ""}`}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
width={680}
|
||||||
|
>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
{data && (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{/* Overall progress */}
|
||||||
|
<div className="p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-sm font-medium text-gray-700 mb-3">
|
||||||
|
整体进度
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={data.overallProgress}
|
||||||
|
status={progressStatus}
|
||||||
|
strokeWidth={12}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-6 mt-3 text-sm">
|
||||||
|
<span>
|
||||||
|
总计:<strong>{data.totalItems}</strong>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
已完成:
|
||||||
|
<strong className="text-green-600">
|
||||||
|
{data.completedItems}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
失败:
|
||||||
|
<strong className="text-red-500">{data.failedItems}</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Child tasks */}
|
||||||
|
{data.children && data.children.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-700 mb-3">
|
||||||
|
子任务进度({data.children.length} 个)
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
dataSource={data.children}
|
||||||
|
columns={childColumns}
|
||||||
|
rowKey="taskId"
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
frontend/src/pages/TaskCoordination/taskCoordination.api.ts
Normal file
76
frontend/src/pages/TaskCoordination/taskCoordination.api.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { get, post, put, del } from "@/utils/request";
|
||||||
|
|
||||||
|
type RequestParams = Record<string, unknown>;
|
||||||
|
type RequestPayload = Record<string, unknown>;
|
||||||
|
|
||||||
|
// ── CRUD ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function createTaskMetaUsingPost(data: RequestPayload) {
|
||||||
|
return post("/api/task-meta", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaskMetaByIdUsingGet(id: string) {
|
||||||
|
return get(`/api/task-meta/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTaskMetaByIdUsingDelete(id: string) {
|
||||||
|
return del(`/api/task-meta/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryTaskMetasUsingGet(params?: RequestParams) {
|
||||||
|
return get("/api/task-meta", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChildrenUsingGet(id: string, params?: RequestParams) {
|
||||||
|
return get(`/api/task-meta/${id}/children`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMyTasksUsingGet(params?: RequestParams) {
|
||||||
|
return get("/api/task-meta/my-tasks", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Split ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function splitTaskUsingPost(id: string, data: RequestPayload) {
|
||||||
|
return post(`/api/task-meta/${id}/split`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Assignment ───────────────────────────────────────────
|
||||||
|
|
||||||
|
export function assignTaskUsingPost(id: string, data: RequestPayload) {
|
||||||
|
return post(`/api/task-meta/${id}/assign`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reassignTaskUsingPost(id: string, data: RequestPayload) {
|
||||||
|
return post(`/api/task-meta/${id}/reassign`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function revokeTaskUsingPost(id: string, remark?: string) {
|
||||||
|
return post(`/api/task-meta/${id}/revoke`, null, {
|
||||||
|
params: remark ? { remark } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchAssignUsingPost(data: RequestPayload) {
|
||||||
|
return post("/api/task-meta/batch-assign", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAssignmentLogsUsingGet(id: string) {
|
||||||
|
return get(`/api/task-meta/${id}/assignment-logs`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Progress ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getProgressUsingGet(id: string) {
|
||||||
|
return get(`/api/task-meta/${id}/progress`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProgressUsingPut(id: string, data: RequestPayload) {
|
||||||
|
return put(`/api/task-meta/${id}/progress`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Users (for assignment selectors) ─────────────────────
|
||||||
|
|
||||||
|
export function listUsersUsingGet() {
|
||||||
|
return get("/api/auth/users");
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Tag } from "antd";
|
||||||
|
import {
|
||||||
|
TaskMetaStatus,
|
||||||
|
SplitStrategy,
|
||||||
|
TaskModule,
|
||||||
|
AssignmentAction,
|
||||||
|
} from "./taskCoordination.model";
|
||||||
|
|
||||||
|
// ── Status ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface StatusMeta {
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TaskStatusMap: Record<TaskMetaStatus, StatusMeta> = {
|
||||||
|
[TaskMetaStatus.PENDING]: { label: "待处理", color: "default" },
|
||||||
|
[TaskMetaStatus.IN_PROGRESS]: { label: "进行中", color: "processing" },
|
||||||
|
[TaskMetaStatus.COMPLETED]: { label: "已完成", color: "success" },
|
||||||
|
[TaskMetaStatus.FAILED]: { label: "失败", color: "error" },
|
||||||
|
[TaskMetaStatus.STOPPED]: { label: "已停止", color: "warning" },
|
||||||
|
[TaskMetaStatus.CANCELLED]: { label: "已取消", color: "default" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderTaskStatus(status?: string) {
|
||||||
|
if (!status) return "-";
|
||||||
|
const meta = TaskStatusMap[status as TaskMetaStatus];
|
||||||
|
if (!meta) return <Tag>{status}</Tag>;
|
||||||
|
return <Tag color={meta.color}>{meta.label}</Tag>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Module ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const TaskModuleMap: Record<TaskModule, string> = {
|
||||||
|
[TaskModule.ANNOTATION]: "数据标注",
|
||||||
|
[TaskModule.CLEANING]: "数据清洗",
|
||||||
|
[TaskModule.EVALUATION]: "数据评估",
|
||||||
|
[TaskModule.SYNTHESIS]: "数据合成",
|
||||||
|
[TaskModule.COLLECTION]: "数据归集",
|
||||||
|
[TaskModule.RATIO]: "数据配比",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getModuleLabel(module?: string): string {
|
||||||
|
if (!module) return "-";
|
||||||
|
return TaskModuleMap[module as TaskModule] || module;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Split Strategy ───────────────────────────────────────
|
||||||
|
|
||||||
|
export const SplitStrategyMap: Record<SplitStrategy, string> = {
|
||||||
|
[SplitStrategy.BY_COUNT]: "按数量拆分",
|
||||||
|
[SplitStrategy.BY_FILE]: "按文件拆分",
|
||||||
|
[SplitStrategy.BY_PERCENTAGE]: "按比例拆分",
|
||||||
|
[SplitStrategy.MANUAL]: "手动拆分",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Assignment Action ────────────────────────────────────
|
||||||
|
|
||||||
|
export const AssignmentActionMap: Record<AssignmentAction, string> = {
|
||||||
|
[AssignmentAction.ASSIGN]: "分配",
|
||||||
|
[AssignmentAction.REASSIGN]: "重新分配",
|
||||||
|
[AssignmentAction.REVOKE]: "撤回",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Filter Options ───────────────────────────────────────
|
||||||
|
|
||||||
|
export const statusFilterOptions = Object.entries(TaskStatusMap).map(
|
||||||
|
([value, meta]) => ({ label: meta.label, value })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const moduleFilterOptions = Object.entries(TaskModuleMap).map(
|
||||||
|
([value, label]) => ({ label, value })
|
||||||
|
);
|
||||||
155
frontend/src/pages/TaskCoordination/taskCoordination.model.ts
Normal file
155
frontend/src/pages/TaskCoordination/taskCoordination.model.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
// ── Enums ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export enum TaskMetaStatus {
|
||||||
|
PENDING = "PENDING",
|
||||||
|
IN_PROGRESS = "IN_PROGRESS",
|
||||||
|
COMPLETED = "COMPLETED",
|
||||||
|
FAILED = "FAILED",
|
||||||
|
STOPPED = "STOPPED",
|
||||||
|
CANCELLED = "CANCELLED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SplitStrategy {
|
||||||
|
BY_COUNT = "BY_COUNT",
|
||||||
|
BY_FILE = "BY_FILE",
|
||||||
|
BY_PERCENTAGE = "BY_PERCENTAGE",
|
||||||
|
MANUAL = "MANUAL",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TaskModule {
|
||||||
|
ANNOTATION = "ANNOTATION",
|
||||||
|
CLEANING = "CLEANING",
|
||||||
|
EVALUATION = "EVALUATION",
|
||||||
|
SYNTHESIS = "SYNTHESIS",
|
||||||
|
COLLECTION = "COLLECTION",
|
||||||
|
RATIO = "RATIO",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AssignmentAction {
|
||||||
|
ASSIGN = "ASSIGN",
|
||||||
|
REASSIGN = "REASSIGN",
|
||||||
|
REVOKE = "REVOKE",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Response Interfaces ──────────────────────────────────
|
||||||
|
|
||||||
|
export interface AssigneeInfo {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
fullName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskMetaDto {
|
||||||
|
id: string;
|
||||||
|
parentId?: string;
|
||||||
|
module: string;
|
||||||
|
refTaskId: string;
|
||||||
|
taskName: string;
|
||||||
|
status: TaskMetaStatus;
|
||||||
|
assignedTo?: number;
|
||||||
|
assigneeName?: string;
|
||||||
|
progress: number;
|
||||||
|
totalItems: number;
|
||||||
|
completedItems: number;
|
||||||
|
failedItems: number;
|
||||||
|
splitStrategy?: string;
|
||||||
|
splitConfig?: string;
|
||||||
|
priority: number;
|
||||||
|
deadline?: string;
|
||||||
|
remark?: string;
|
||||||
|
createdBy?: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
childCount: number;
|
||||||
|
assignee?: AssigneeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskAssignmentLogDto {
|
||||||
|
id: number;
|
||||||
|
taskMetaId: string;
|
||||||
|
action: AssignmentAction;
|
||||||
|
userId?: number;
|
||||||
|
userName?: string;
|
||||||
|
remark?: string;
|
||||||
|
operatorId?: number;
|
||||||
|
operatorName?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChildTaskProgressDto {
|
||||||
|
taskId: string;
|
||||||
|
taskName: string;
|
||||||
|
assignee?: AssigneeInfo;
|
||||||
|
progress: number;
|
||||||
|
totalItems: number;
|
||||||
|
completedItems: number;
|
||||||
|
failedItems: number;
|
||||||
|
status: TaskMetaStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskProgressResponse {
|
||||||
|
taskId: string;
|
||||||
|
taskName: string;
|
||||||
|
overallProgress: number;
|
||||||
|
totalItems: number;
|
||||||
|
completedItems: number;
|
||||||
|
failedItems: number;
|
||||||
|
status: TaskMetaStatus;
|
||||||
|
children: ChildTaskProgressDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Request Interfaces ───────────────────────────────────
|
||||||
|
|
||||||
|
export interface CreateTaskMetaRequest {
|
||||||
|
module: string;
|
||||||
|
refTaskId: string;
|
||||||
|
taskName: string;
|
||||||
|
assignedTo?: number;
|
||||||
|
totalItems?: number;
|
||||||
|
priority?: number;
|
||||||
|
deadline?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitAssignment {
|
||||||
|
userId?: number;
|
||||||
|
proportion?: number;
|
||||||
|
itemCount?: number;
|
||||||
|
taskName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitTaskRequest {
|
||||||
|
strategy: SplitStrategy;
|
||||||
|
splitConfig?: Record<string, unknown>;
|
||||||
|
assignments: SplitAssignment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssignTaskRequest {
|
||||||
|
userId: number;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchTaskAssignment {
|
||||||
|
taskMetaId: string;
|
||||||
|
userId: number;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchAssignRequest {
|
||||||
|
assignments: BatchTaskAssignment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProgressRequest {
|
||||||
|
progress?: number;
|
||||||
|
totalItems?: number;
|
||||||
|
completedItems?: number;
|
||||||
|
failedItems?: number;
|
||||||
|
status?: TaskMetaStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── User (for assignment selectors) ──────────────────────
|
||||||
|
|
||||||
|
export interface UserOption {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
fullName?: string;
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ import OperatorPluginCreate from "@/pages/OperatorMarket/Create/OperatorPluginCr
|
|||||||
import OperatorPluginDetail from "@/pages/OperatorMarket/Detail/OperatorPluginDetail";
|
import OperatorPluginDetail from "@/pages/OperatorMarket/Detail/OperatorPluginDetail";
|
||||||
import RatioTasksPage from "@/pages/RatioTask/Home/RatioTask.tsx";
|
import RatioTasksPage from "@/pages/RatioTask/Home/RatioTask.tsx";
|
||||||
import CreateRatioTask from "@/pages/RatioTask/Create/CreateRatioTask.tsx";
|
import CreateRatioTask from "@/pages/RatioTask/Create/CreateRatioTask.tsx";
|
||||||
|
import TaskCoordinationPage from "@/pages/TaskCoordination/Home/TaskCoordination";
|
||||||
|
import MyTasksPage from "@/pages/TaskCoordination/MyTasks/MyTasks";
|
||||||
import OrchestrationPage from "@/pages/Orchestration/Orchestration";
|
import OrchestrationPage from "@/pages/Orchestration/Orchestration";
|
||||||
import WorkflowEditor from "@/pages/Orchestration/WorkflowEditor";
|
import WorkflowEditor from "@/pages/Orchestration/WorkflowEditor";
|
||||||
import { withErrorBoundary } from "@/components/ErrorBoundary";
|
import { withErrorBoundary } from "@/components/ErrorBoundary";
|
||||||
@@ -285,6 +287,20 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "task-coordination",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
index: true,
|
||||||
|
Component: TaskCoordinationPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "my-tasks",
|
||||||
|
Component: MyTasksPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "operator-market",
|
path: "operator-market",
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ from .data_evaluation import (
|
|||||||
EvaluationItem
|
EvaluationItem
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .task_coordination import (
|
||||||
|
TaskMeta,
|
||||||
|
TaskAssignmentLog
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Dataset",
|
"Dataset",
|
||||||
"DatasetTag",
|
"DatasetTag",
|
||||||
@@ -36,4 +41,6 @@ __all__ = [
|
|||||||
"LabelingProjectFile",
|
"LabelingProjectFile",
|
||||||
"EvaluationTask",
|
"EvaluationTask",
|
||||||
"EvaluationItem",
|
"EvaluationItem",
|
||||||
|
"TaskMeta",
|
||||||
|
"TaskAssignmentLog",
|
||||||
]
|
]
|
||||||
|
|||||||
220
runtime/datamate-python/app/db/models/task_coordination.py
Normal file
220
runtime/datamate-python/app/db/models/task_coordination.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
"""Tables of Task Coordination Module (任务拆分与分配)"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
BigInteger,
|
||||||
|
TIMESTAMP,
|
||||||
|
Text,
|
||||||
|
JSON,
|
||||||
|
ForeignKey,
|
||||||
|
Index,
|
||||||
|
)
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from app.db.session import Base
|
||||||
|
|
||||||
|
|
||||||
|
# ── 任务元状态常量 ──────────────────────────────────────────
|
||||||
|
TASK_META_STATUS_PENDING = "PENDING"
|
||||||
|
TASK_META_STATUS_IN_PROGRESS = "IN_PROGRESS"
|
||||||
|
TASK_META_STATUS_COMPLETED = "COMPLETED"
|
||||||
|
TASK_META_STATUS_FAILED = "FAILED"
|
||||||
|
TASK_META_STATUS_STOPPED = "STOPPED"
|
||||||
|
TASK_META_STATUS_CANCELLED = "CANCELLED"
|
||||||
|
|
||||||
|
TASK_META_STATUS_VALUES = {
|
||||||
|
TASK_META_STATUS_PENDING,
|
||||||
|
TASK_META_STATUS_IN_PROGRESS,
|
||||||
|
TASK_META_STATUS_COMPLETED,
|
||||||
|
TASK_META_STATUS_FAILED,
|
||||||
|
TASK_META_STATUS_STOPPED,
|
||||||
|
TASK_META_STATUS_CANCELLED,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 所属模块常量 ────────────────────────────────────────────
|
||||||
|
MODULE_ANNOTATION = "ANNOTATION"
|
||||||
|
MODULE_CLEANING = "CLEANING"
|
||||||
|
MODULE_EVALUATION = "EVALUATION"
|
||||||
|
MODULE_SYNTHESIS = "SYNTHESIS"
|
||||||
|
MODULE_COLLECTION = "COLLECTION"
|
||||||
|
MODULE_RATIO = "RATIO"
|
||||||
|
|
||||||
|
MODULE_VALUES = {
|
||||||
|
MODULE_ANNOTATION,
|
||||||
|
MODULE_CLEANING,
|
||||||
|
MODULE_EVALUATION,
|
||||||
|
MODULE_SYNTHESIS,
|
||||||
|
MODULE_COLLECTION,
|
||||||
|
MODULE_RATIO,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 拆分策略常量 ────────────────────────────────────────────
|
||||||
|
SPLIT_STRATEGY_BY_COUNT = "BY_COUNT"
|
||||||
|
SPLIT_STRATEGY_BY_FILE = "BY_FILE"
|
||||||
|
SPLIT_STRATEGY_BY_PERCENTAGE = "BY_PERCENTAGE"
|
||||||
|
SPLIT_STRATEGY_MANUAL = "MANUAL"
|
||||||
|
|
||||||
|
# ── 分配操作常量 ────────────────────────────────────────────
|
||||||
|
ASSIGNMENT_ACTION_ASSIGN = "ASSIGN"
|
||||||
|
ASSIGNMENT_ACTION_REASSIGN = "REASSIGN"
|
||||||
|
ASSIGNMENT_ACTION_REVOKE = "REVOKE"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskMeta(Base):
|
||||||
|
"""任务拆分与分配元数据表"""
|
||||||
|
|
||||||
|
__tablename__ = "t_task_meta"
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
String(36),
|
||||||
|
primary_key=True,
|
||||||
|
default=lambda: str(uuid.uuid4()),
|
||||||
|
comment="任务元ID (UUID)",
|
||||||
|
)
|
||||||
|
parent_id = Column(
|
||||||
|
String(36),
|
||||||
|
ForeignKey("t_task_meta.id", ondelete="CASCADE"),
|
||||||
|
nullable=True,
|
||||||
|
comment="父任务ID,顶层任务为NULL",
|
||||||
|
)
|
||||||
|
module = Column(
|
||||||
|
String(50),
|
||||||
|
nullable=False,
|
||||||
|
comment="所属模块: ANNOTATION/CLEANING/EVALUATION/SYNTHESIS/COLLECTION/RATIO",
|
||||||
|
)
|
||||||
|
ref_task_id = Column(
|
||||||
|
String(64),
|
||||||
|
nullable=False,
|
||||||
|
comment="关联的业务任务ID(各模块自己的task表ID)",
|
||||||
|
)
|
||||||
|
task_name = Column(String(255), nullable=False, comment="任务名称")
|
||||||
|
status = Column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=TASK_META_STATUS_PENDING,
|
||||||
|
comment="状态: PENDING/IN_PROGRESS/COMPLETED/FAILED/STOPPED/CANCELLED",
|
||||||
|
)
|
||||||
|
assigned_to = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey("users.id", ondelete="SET NULL"),
|
||||||
|
nullable=True,
|
||||||
|
comment="分配给的用户ID (users.id)",
|
||||||
|
)
|
||||||
|
created_by = Column(String(255), nullable=False, comment="创建者用户名")
|
||||||
|
|
||||||
|
# ── 进度字段 ──
|
||||||
|
progress = Column(
|
||||||
|
Integer, nullable=False, default=0, comment="进度百分比 0-100"
|
||||||
|
)
|
||||||
|
total_items = Column(
|
||||||
|
Integer, nullable=False, default=0, comment="总条目数"
|
||||||
|
)
|
||||||
|
completed_items = Column(
|
||||||
|
Integer, nullable=False, default=0, comment="已完成条目数"
|
||||||
|
)
|
||||||
|
failed_items = Column(
|
||||||
|
Integer, nullable=False, default=0, comment="失败条目数"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 拆分配置 ──
|
||||||
|
split_strategy = Column(
|
||||||
|
String(50),
|
||||||
|
nullable=True,
|
||||||
|
comment="拆分策略: BY_COUNT/BY_FILE/BY_PERCENTAGE/MANUAL",
|
||||||
|
)
|
||||||
|
split_config = Column(JSON, nullable=True, comment="拆分配置参数")
|
||||||
|
|
||||||
|
# ── 调度 ──
|
||||||
|
priority = Column(
|
||||||
|
Integer, nullable=False, default=0, comment="优先级 0=普通 1=高 2=紧急"
|
||||||
|
)
|
||||||
|
deadline = Column(TIMESTAMP, nullable=True, comment="截止时间")
|
||||||
|
|
||||||
|
# ── 时间戳 ──
|
||||||
|
started_at = Column(TIMESTAMP, nullable=True, comment="开始时间")
|
||||||
|
completed_at = Column(TIMESTAMP, nullable=True, comment="完成时间")
|
||||||
|
created_at = Column(
|
||||||
|
TIMESTAMP,
|
||||||
|
server_default=func.current_timestamp(),
|
||||||
|
comment="创建时间",
|
||||||
|
)
|
||||||
|
updated_at = Column(
|
||||||
|
TIMESTAMP,
|
||||||
|
server_default=func.current_timestamp(),
|
||||||
|
onupdate=func.current_timestamp(),
|
||||||
|
comment="更新时间",
|
||||||
|
)
|
||||||
|
deleted_at = Column(TIMESTAMP, nullable=True, comment="删除时间(软删除)")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_task_meta_parent_id", "parent_id"),
|
||||||
|
Index("idx_task_meta_module_ref", "module", "ref_task_id"),
|
||||||
|
Index("idx_task_meta_assigned_to", "assigned_to"),
|
||||||
|
Index("idx_task_meta_status", "status"),
|
||||||
|
Index("idx_task_meta_created_by", "created_by"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<TaskMeta(id={self.id}, task_name={self.task_name}, status={self.status})>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_deleted(self) -> bool:
|
||||||
|
"""检查是否已被软删除"""
|
||||||
|
return self.deleted_at is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_parent(self) -> bool:
|
||||||
|
"""是否为父任务(顶层任务)"""
|
||||||
|
return self.parent_id is None
|
||||||
|
|
||||||
|
|
||||||
|
class TaskAssignmentLog(Base):
|
||||||
|
"""任务分配操作日志表"""
|
||||||
|
|
||||||
|
__tablename__ = "t_task_assignment_log"
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
String(36),
|
||||||
|
primary_key=True,
|
||||||
|
default=lambda: str(uuid.uuid4()),
|
||||||
|
comment="日志ID (UUID)",
|
||||||
|
)
|
||||||
|
task_meta_id = Column(
|
||||||
|
String(36),
|
||||||
|
ForeignKey("t_task_meta.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
comment="任务元ID",
|
||||||
|
)
|
||||||
|
action = Column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
comment="操作类型: ASSIGN/REASSIGN/REVOKE",
|
||||||
|
)
|
||||||
|
from_user_id = Column(
|
||||||
|
BigInteger, nullable=True, comment="原分配用户ID"
|
||||||
|
)
|
||||||
|
to_user_id = Column(
|
||||||
|
BigInteger, nullable=True, comment="新分配用户ID"
|
||||||
|
)
|
||||||
|
operated_by = Column(
|
||||||
|
String(255), nullable=False, comment="操作者用户名"
|
||||||
|
)
|
||||||
|
remark = Column(String(500), nullable=True, comment="备注")
|
||||||
|
created_at = Column(
|
||||||
|
TIMESTAMP,
|
||||||
|
server_default=func.current_timestamp(),
|
||||||
|
comment="创建时间",
|
||||||
|
)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_assignment_log_task_meta_id", "task_meta_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<TaskAssignmentLog(id={self.id}, task_meta_id={self.task_meta_id}, "
|
||||||
|
f"action={self.action})>"
|
||||||
|
)
|
||||||
119
scripts/db/task-coordination-init.sql
Normal file
119
scripts/db/task-coordination-init.sql
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
-- =============================================
|
||||||
|
-- DataMate 任务协调模块 - 初始化脚本
|
||||||
|
-- 说明:
|
||||||
|
-- 1) 创建 t_task_meta 表(任务拆分与分配元数据)
|
||||||
|
-- 2) 创建 t_task_assignment_log 表(分配操作日志)
|
||||||
|
-- 3) 在 t_auth_permissions 中插入任务协调权限
|
||||||
|
-- 4) 将权限绑定到 ROLE_ADMIN 和 ROLE_DATA_EDITOR
|
||||||
|
-- 5) 本脚本按"幂等"方式编写,可重复执行
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
USE datamate;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 1) t_task_meta - 任务拆分与分配元数据表
|
||||||
|
-- =============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS t_task_meta
|
||||||
|
(
|
||||||
|
id VARCHAR(36) PRIMARY KEY COMMENT '任务元ID (UUID)',
|
||||||
|
parent_id VARCHAR(36) NULL COMMENT '父任务ID,顶层任务为NULL',
|
||||||
|
module VARCHAR(50) NOT NULL COMMENT '所属模块: ANNOTATION/CLEANING/EVALUATION/SYNTHESIS/COLLECTION/RATIO',
|
||||||
|
ref_task_id VARCHAR(64) NOT NULL COMMENT '关联的业务任务ID(各模块自己的task表ID)',
|
||||||
|
task_name VARCHAR(255) NOT NULL COMMENT '任务名称',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '状态: PENDING/IN_PROGRESS/COMPLETED/FAILED/STOPPED/CANCELLED',
|
||||||
|
assigned_to BIGINT NULL COMMENT '分配给的用户ID (users.id)',
|
||||||
|
created_by VARCHAR(255) NOT NULL COMMENT '创建者用户名',
|
||||||
|
|
||||||
|
-- 进度字段
|
||||||
|
progress INT NOT NULL DEFAULT 0 COMMENT '进度百分比 0-100',
|
||||||
|
total_items INT NOT NULL DEFAULT 0 COMMENT '总条目数',
|
||||||
|
completed_items INT NOT NULL DEFAULT 0 COMMENT '已完成条目数',
|
||||||
|
failed_items INT NOT NULL DEFAULT 0 COMMENT '失败条目数',
|
||||||
|
|
||||||
|
-- 拆分配置
|
||||||
|
split_strategy VARCHAR(50) NULL COMMENT '拆分策略: BY_COUNT/BY_FILE/BY_PERCENTAGE/MANUAL',
|
||||||
|
split_config JSON NULL COMMENT '拆分配置参数',
|
||||||
|
|
||||||
|
-- 调度
|
||||||
|
priority INT NOT NULL DEFAULT 0 COMMENT '优先级 0=普通 1=高 2=紧急',
|
||||||
|
deadline TIMESTAMP NULL COMMENT '截止时间',
|
||||||
|
|
||||||
|
-- 时间戳
|
||||||
|
started_at TIMESTAMP NULL COMMENT '开始时间',
|
||||||
|
completed_at TIMESTAMP NULL COMMENT '完成时间',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at TIMESTAMP NULL COMMENT '删除时间(软删除)',
|
||||||
|
|
||||||
|
-- 外键
|
||||||
|
CONSTRAINT fk_task_meta_parent FOREIGN KEY (parent_id) REFERENCES t_task_meta (id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_task_meta_assignee FOREIGN KEY (assigned_to) REFERENCES users (id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
INDEX idx_task_meta_parent_id (parent_id),
|
||||||
|
INDEX idx_task_meta_module_ref (module, ref_task_id),
|
||||||
|
INDEX idx_task_meta_assigned_to (assigned_to),
|
||||||
|
INDEX idx_task_meta_status (status),
|
||||||
|
INDEX idx_task_meta_created_by (created_by)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COMMENT = '任务拆分与分配元数据表';
|
||||||
|
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 2) t_task_assignment_log - 分配操作日志表
|
||||||
|
-- =============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS t_task_assignment_log
|
||||||
|
(
|
||||||
|
id VARCHAR(36) PRIMARY KEY COMMENT '日志ID (UUID)',
|
||||||
|
task_meta_id VARCHAR(36) NOT NULL COMMENT '任务元ID',
|
||||||
|
action VARCHAR(20) NOT NULL COMMENT '操作类型: ASSIGN/REASSIGN/REVOKE',
|
||||||
|
from_user_id BIGINT NULL COMMENT '原分配用户ID',
|
||||||
|
to_user_id BIGINT NULL COMMENT '新分配用户ID',
|
||||||
|
operated_by VARCHAR(255) NOT NULL COMMENT '操作者用户名',
|
||||||
|
remark VARCHAR(500) NULL COMMENT '备注',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
|
||||||
|
-- 外键
|
||||||
|
CONSTRAINT fk_assignment_log_task FOREIGN KEY (task_meta_id) REFERENCES t_task_meta (id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
INDEX idx_assignment_log_task_meta_id (task_meta_id)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COMMENT = '任务分配操作日志';
|
||||||
|
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 3) 任务协调权限初始化
|
||||||
|
-- =============================================
|
||||||
|
INSERT IGNORE INTO t_auth_permissions (id, permission_code, permission_name, module, action, path_pattern, method, enabled, is_built_in)
|
||||||
|
VALUES ('perm-tc-read', 'module:task-coordination:read', '任务协调读取', 'task-coordination', 'read', '/api/task-meta/**', 'GET', 1, 1),
|
||||||
|
('perm-tc-write', 'module:task-coordination:write', '任务协调写入', 'task-coordination', 'write', '/api/task-meta/**', 'POST,PUT,PATCH,DELETE', 1, 1),
|
||||||
|
('perm-tc-assign', 'module:task-coordination:assign', '任务分配', 'task-coordination', 'assign', '/api/task-meta/*/assign', 'POST', 1, 1);
|
||||||
|
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 4) 绑定权限到角色
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- 管理员获得全部任务协调权限
|
||||||
|
INSERT IGNORE INTO t_auth_role_permissions (role_id, permission_id)
|
||||||
|
SELECT 'role-admin', p.id
|
||||||
|
FROM t_auth_permissions p
|
||||||
|
WHERE p.permission_code IN (
|
||||||
|
'module:task-coordination:read',
|
||||||
|
'module:task-coordination:write',
|
||||||
|
'module:task-coordination:assign'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 数据运营获得任务协调读写和分配权限
|
||||||
|
INSERT IGNORE INTO t_auth_role_permissions (role_id, permission_id)
|
||||||
|
SELECT 'role-data-editor', p.id
|
||||||
|
FROM t_auth_permissions p
|
||||||
|
WHERE p.permission_code IN (
|
||||||
|
'module:task-coordination:read',
|
||||||
|
'module:task-coordination:write',
|
||||||
|
'module:task-coordination:assign'
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user