2026-08-04 17:03:23 +08:00
|
|
|
|
"""任务模型 — 支持无限嵌套、任务裂变分支、返工闭环"""
|
2026-08-04 10:05:59 +08:00
|
|
|
|
import uuid
|
2026-08-05 14:00:23 +08:00
|
|
|
|
from datetime import datetime
|
2026-08-04 10:05:59 +08:00
|
|
|
|
from sqlalchemy import String, DateTime, Boolean, ForeignKey
|
|
|
|
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
|
|
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.base import Base
|
2026-08-05 14:00:23 +08:00
|
|
|
|
from app.core.time_utils import get_beijing_time
|
2026-08-04 10:05:59 +08:00
|
|
|
|
|
2026-08-04 17:03:23 +08:00
|
|
|
|
# 任务状态常量
|
|
|
|
|
|
TASK_STATUS_PENDING = "PENDING" # 待接收
|
|
|
|
|
|
TASK_STATUS_WIP = "WIP" # 进行中
|
|
|
|
|
|
TASK_STATUS_COMPLETED = "COMPLETED" # 已完成
|
|
|
|
|
|
TASK_STATUS_REJECTED = "REJECTED" # 已驳回
|
|
|
|
|
|
TASK_STATUS_ARCHIVED = "ARCHIVED" # 已入库
|
|
|
|
|
|
|
2026-08-04 10:05:59 +08:00
|
|
|
|
|
|
|
|
|
|
class Task(Base):
|
|
|
|
|
|
__tablename__ = "tasks"
|
|
|
|
|
|
|
|
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 物理外键(关联本库 products) ----
|
|
|
|
|
|
product_id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
|
UUID(as_uuid=True), ForeignKey("products.id"), nullable=False, comment="所属产品ID",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-04 17:03:23 +08:00
|
|
|
|
# ---- 物理外键(自引用:无限嵌套父子任务 / 裂变分支) ----
|
2026-08-04 10:05:59 +08:00
|
|
|
|
parent_task_id: Mapped[uuid.UUID | None] = mapped_column(
|
2026-08-04 17:03:23 +08:00
|
|
|
|
UUID(as_uuid=True), ForeignKey("tasks.id"), nullable=True, index=True, comment="父任务ID(用于任务裂变树)",
|
2026-08-04 10:05:59 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
task_name: Mapped[str] = mapped_column(
|
|
|
|
|
|
String(200), nullable=False, comment="任务名称",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 逻辑外键(关联老系统用户表,仅存储 ID,无物理约束) ----
|
|
|
|
|
|
assignee_id: Mapped[str | None] = mapped_column(
|
|
|
|
|
|
String(64), nullable=True, comment="负责人ID(逻辑外键→老系统)",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
status: Mapped[str] = mapped_column(
|
2026-08-04 17:03:23 +08:00
|
|
|
|
String(50), nullable=False, default=TASK_STATUS_PENDING,
|
|
|
|
|
|
comment="任务状态: PENDING(待接收) | WIP(进行中) | COMPLETED(已完成) | REJECTED(已驳回) | ARCHIVED(已入库)",
|
2026-08-04 10:05:59 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
notify_parent_on_complete: Mapped[bool] = mapped_column(
|
|
|
|
|
|
Boolean, default=False, comment="完成后是否通知父任务",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-04 17:03:23 +08:00
|
|
|
|
# ---- 时间追踪 ----
|
2026-08-04 10:05:59 +08:00
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
2026-08-05 14:00:23 +08:00
|
|
|
|
DateTime(timezone=True), default=get_beijing_time, comment="创建时间",
|
2026-08-04 10:05:59 +08:00
|
|
|
|
)
|
2026-08-04 17:03:23 +08:00
|
|
|
|
received_at: Mapped[datetime | None] = mapped_column(
|
|
|
|
|
|
DateTime(timezone=True), nullable=True, comment="操作员确认接收时间",
|
|
|
|
|
|
)
|
|
|
|
|
|
completed_at: Mapped[datetime | None] = mapped_column(
|
|
|
|
|
|
DateTime(timezone=True), nullable=True, comment="任务完工转交时间",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 驳回/返工 ----
|
|
|
|
|
|
reject_reason: Mapped[str | None] = mapped_column(
|
|
|
|
|
|
String(500), nullable=True, comment="驳回原因",
|
|
|
|
|
|
)
|
|
|
|
|
|
is_rework: Mapped[bool] = mapped_column(
|
|
|
|
|
|
Boolean, default=False, comment="是否为返工任务",
|
|
|
|
|
|
)
|
2026-08-05 15:22:50 +08:00
|
|
|
|
remark: Mapped[str | None] = mapped_column(
|
|
|
|
|
|
String(2000), nullable=True, comment="任务初始描述/交接备注",
|
|
|
|
|
|
)
|
2026-08-04 10:05:59 +08:00
|
|
|
|
|
|
|
|
|
|
# ---- 关系 ----
|
|
|
|
|
|
product: Mapped["Product"] = relationship("Product", lazy="selectin")
|
|
|
|
|
|
parent_task: Mapped["Task | None"] = relationship(
|
|
|
|
|
|
"Task", remote_side="Task.id", back_populates="child_tasks", lazy="selectin",
|
|
|
|
|
|
)
|
|
|
|
|
|
child_tasks: Mapped[list["Task"]] = relationship(
|
|
|
|
|
|
"Task", back_populates="parent_task", lazy="selectin",
|
|
|
|
|
|
)
|
2026-08-05 14:00:23 +08:00
|
|
|
|
records: Mapped[list["TaskRecord"]] = relationship(
|
|
|
|
|
|
"TaskRecord", back_populates="task", lazy="selectin", cascade="all, delete-orphan",
|
|
|
|
|
|
)
|
2026-08-04 10:05:59 +08:00
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
|
return f"<Task {self.task_name}>"
|
2026-08-05 14:00:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 任务进度记录 — 随时备注/传图
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
class TaskRecord(Base):
|
|
|
|
|
|
__tablename__ = "task_records"
|
|
|
|
|
|
|
|
|
|
|
|
id: Mapped[int] = mapped_column(
|
|
|
|
|
|
primary_key=True, autoincrement=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
task_id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
|
UUID(as_uuid=True), ForeignKey("tasks.id"), nullable=False, index=True, comment="所属任务ID",
|
|
|
|
|
|
)
|
|
|
|
|
|
remark: Mapped[str | None] = mapped_column(
|
|
|
|
|
|
String(2000), nullable=True, comment="备注文本",
|
|
|
|
|
|
)
|
|
|
|
|
|
images: Mapped[str | None] = mapped_column(
|
|
|
|
|
|
String(4000), nullable=True, comment="图片URL列表(JSON字符串)",
|
|
|
|
|
|
)
|
|
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
|
DateTime(timezone=True), default=get_beijing_time, comment="记录时间",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 关系 ----
|
|
|
|
|
|
task: Mapped["Task"] = relationship("Task", back_populates="records")
|
|
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
|
return f"<TaskRecord {self.id} @ {self.created_at}>"
|