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:
2026-02-09 00:42:34 +08:00
parent 78624915b7
commit 71f8f7d1c3
51 changed files with 3765 additions and 0 deletions

View File

@@ -23,6 +23,11 @@ from .data_evaluation import (
EvaluationItem
)
from .task_coordination import (
TaskMeta,
TaskAssignmentLog
)
__all__ = [
"Dataset",
"DatasetTag",
@@ -36,4 +41,6 @@ __all__ = [
"LabelingProjectFile",
"EvaluationTask",
"EvaluationItem",
"TaskMeta",
"TaskAssignmentLog",
]

View 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})>"
)