"""项目管理领域对象:任务、里程碑、项目阶段""" 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)