248 lines
7.8 KiB
Python
248 lines
7.8 KiB
Python
"""项目管理领域对象:任务、里程碑、项目阶段"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from enum import StrEnum
|
|
from uuid import uuid4
|
|
|
|
|
|
class TaskStatus(StrEnum):
|
|
"""任务状态"""
|
|
|
|
PENDING = "pending" # 待开始
|
|
IN_PROGRESS = "in_progress" # 进行中
|
|
BLOCKED = "blocked" # 阻塞
|
|
COMPLETED = "completed" # 已完成
|
|
CANCELLED = "cancelled" # 已取消
|
|
|
|
|
|
class TaskPriority(StrEnum):
|
|
"""任务优先级"""
|
|
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
URGENT = "urgent"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Task:
|
|
"""任务实体"""
|
|
|
|
id: str
|
|
project_id: str
|
|
workspace_id: str
|
|
name: str
|
|
description: str = ""
|
|
status: TaskStatus = TaskStatus.PENDING
|
|
priority: TaskPriority = TaskPriority.MEDIUM
|
|
parent_task_id: str = "" # 父任务ID(支持子任务层级)
|
|
assignee_user_id: str = "" # 负责人
|
|
progress: float = 0.0 # 进度 0-100
|
|
planned_start_date: datetime | None = None
|
|
planned_end_date: datetime | None = None
|
|
actual_start_date: datetime | None = None
|
|
actual_end_date: datetime | None = None
|
|
tags: list[str] = field(default_factory=list)
|
|
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
project_id: str,
|
|
workspace_id: str,
|
|
name: str,
|
|
description: str = "",
|
|
priority: TaskPriority = TaskPriority.MEDIUM,
|
|
parent_task_id: str = "",
|
|
assignee_user_id: str = "",
|
|
planned_start_date: datetime | None = None,
|
|
planned_end_date: datetime | None = None,
|
|
) -> "Task":
|
|
"""创建任务"""
|
|
clean_name = name.strip()
|
|
if not clean_name:
|
|
raise ValueError("任务名称不能为空")
|
|
if not project_id.strip():
|
|
raise ValueError("project_id 不能为空")
|
|
if not workspace_id.strip():
|
|
raise ValueError("workspace_id 不能为空")
|
|
|
|
return cls(
|
|
id=uuid4().hex,
|
|
project_id=project_id.strip(),
|
|
workspace_id=workspace_id.strip(),
|
|
name=clean_name,
|
|
description=description.strip(),
|
|
priority=priority,
|
|
parent_task_id=parent_task_id.strip(),
|
|
assignee_user_id=assignee_user_id.strip(),
|
|
planned_start_date=planned_start_date,
|
|
planned_end_date=planned_end_date,
|
|
)
|
|
|
|
def update_status(self, new_status: TaskStatus) -> None:
|
|
"""更新任务状态"""
|
|
self.status = new_status
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
# 自动设置实际开始/结束时间
|
|
if new_status == TaskStatus.IN_PROGRESS and self.actual_start_date is None:
|
|
self.actual_start_date = datetime.now(timezone.utc)
|
|
elif new_status == TaskStatus.COMPLETED and self.actual_end_date is None:
|
|
self.actual_end_date = datetime.now(timezone.utc)
|
|
self.progress = 100.0
|
|
|
|
def update_progress(self, progress: float) -> None:
|
|
"""更新任务进度"""
|
|
if not 0 <= progress <= 100:
|
|
raise ValueError("进度必须在 0-100 之间")
|
|
self.progress = progress
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
# 自动更新状态
|
|
if progress > 0 and self.status == TaskStatus.PENDING:
|
|
self.status = TaskStatus.IN_PROGRESS
|
|
if progress == 100 and self.status != TaskStatus.COMPLETED:
|
|
self.status = TaskStatus.COMPLETED
|
|
if self.actual_end_date is None:
|
|
self.actual_end_date = datetime.now(timezone.utc)
|
|
|
|
def add_tag(self, tag: str) -> None:
|
|
"""添加标签"""
|
|
clean_tag = tag.strip()
|
|
if not clean_tag:
|
|
raise ValueError("标签不能为空")
|
|
if clean_tag not in self.tags:
|
|
self.tags.append(clean_tag)
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
def remove_tag(self, tag: str) -> None:
|
|
"""删除标签"""
|
|
clean_tag = tag.strip()
|
|
if clean_tag in self.tags:
|
|
self.tags.remove(clean_tag)
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Milestone:
|
|
"""里程碑实体"""
|
|
|
|
id: str
|
|
project_id: str
|
|
workspace_id: str
|
|
name: str
|
|
description: str = ""
|
|
target_date: datetime | None = None
|
|
completed: bool = False
|
|
completed_at: datetime | None = None
|
|
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
project_id: str,
|
|
workspace_id: str,
|
|
name: str,
|
|
description: str = "",
|
|
target_date: datetime | None = None,
|
|
) -> "Milestone":
|
|
"""创建里程碑"""
|
|
clean_name = name.strip()
|
|
if not clean_name:
|
|
raise ValueError("里程碑名称不能为空")
|
|
if not project_id.strip():
|
|
raise ValueError("project_id 不能为空")
|
|
if not workspace_id.strip():
|
|
raise ValueError("workspace_id 不能为空")
|
|
|
|
return cls(
|
|
id=uuid4().hex,
|
|
project_id=project_id.strip(),
|
|
workspace_id=workspace_id.strip(),
|
|
name=clean_name,
|
|
description=description.strip(),
|
|
target_date=target_date,
|
|
)
|
|
|
|
def mark_completed(self) -> None:
|
|
"""标记为已完成"""
|
|
if not self.completed:
|
|
self.completed = True
|
|
self.completed_at = datetime.now(timezone.utc)
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
def reopen(self) -> None:
|
|
"""重新打开里程碑"""
|
|
if self.completed:
|
|
self.completed = False
|
|
self.completed_at = None
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class TaskIssue:
|
|
"""任务问题/卡点实体"""
|
|
|
|
id: str
|
|
task_id: str
|
|
project_id: str
|
|
workspace_id: str
|
|
title: str
|
|
description: str = ""
|
|
resolved: bool = False
|
|
resolved_at: datetime | None = None
|
|
created_by_user_id: str = ""
|
|
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
task_id: str,
|
|
project_id: str,
|
|
workspace_id: str,
|
|
title: str,
|
|
description: str = "",
|
|
created_by_user_id: str = "",
|
|
) -> "TaskIssue":
|
|
"""创建任务问题"""
|
|
clean_title = title.strip()
|
|
if not clean_title:
|
|
raise ValueError("问题标题不能为空")
|
|
if not task_id.strip():
|
|
raise ValueError("task_id 不能为空")
|
|
if not project_id.strip():
|
|
raise ValueError("project_id 不能为空")
|
|
if not workspace_id.strip():
|
|
raise ValueError("workspace_id 不能为空")
|
|
|
|
return cls(
|
|
id=uuid4().hex,
|
|
task_id=task_id.strip(),
|
|
project_id=project_id.strip(),
|
|
workspace_id=workspace_id.strip(),
|
|
title=clean_title,
|
|
description=description.strip(),
|
|
created_by_user_id=created_by_user_id.strip(),
|
|
)
|
|
|
|
def mark_resolved(self) -> None:
|
|
"""标记为已解决"""
|
|
if not self.resolved:
|
|
self.resolved = True
|
|
self.resolved_at = datetime.now(timezone.utc)
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
def reopen(self) -> None:
|
|
"""重新打开问题"""
|
|
if self.resolved:
|
|
self.resolved = False
|
|
self.resolved_at = None
|
|
self.updated_at = datetime.now(timezone.utc)
|