Files
xiaoxia-saas/packages/domain/project_management.py
T
2026-06-21 06:52:19 +08:00

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)