"""任务域模型(M1):item / tag / item_tag / comment / activity。 约定与 M0 一致:uuid 主键、通用列、`workspace_id NOT NULL`。 两个刻意的取舍: - **M1 只有一个负责人 `assignee_id`**,不做 `item_assignee` 多对多。M1 是"自己的待办", 多人协作在 M2 工单里才真正需要;提前建多对多表和对应 UI 是过度设计,等真要用再迁移。 - **`kind` 列现在就加上**(默认 `task`)。M2 的工单、M3 的知识都是同一条 item 流, 现在加一列只是默认值,将来加 `ticket_ext` / `knowledge_doc` 时不用改主表。 """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( DateTime, ForeignKey, Index, String, Text, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import UUID as PgUUID from sqlalchemy.orm import Mapped, mapped_column from .models import Base, Timestamps, new_uuid ITEM_STATUSES: tuple[str, ...] = ("inbox", "todo", "doing", "done", "blocked", "canceled") class Item(Base, Timestamps): """任务/工单/知识的统一载体。M1 只用 `kind='task'`。""" __tablename__ = "item" __table_args__ = ( Index("ix_item_ws_status_due", "workspace_id", "status", "due_at"), Index("ix_item_ws_assignee", "workspace_id", "assignee_id", "status"), Index("ix_item_ws_updated", "workspace_id", "updated_at"), Index("ix_item_ws_sort", "workspace_id", "status", "sort_order"), ) id: Mapped[uuid.UUID] = mapped_column(PgUUID(as_uuid=True), primary_key=True, default=new_uuid) workspace_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("workspace.id", ondelete="CASCADE"), nullable=False ) kind: Mapped[str] = mapped_column(String(16), nullable=False, default="task") title: Mapped[str] = mapped_column(String(200), nullable=False) body_md: Mapped[str] = mapped_column(Text, nullable=False, default="") status: Mapped[str] = mapped_column(String(16), nullable=False, default="inbox") priority: Mapped[int] = mapped_column(nullable=False, default=0) creator_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("app_user.id", ondelete="RESTRICT"), nullable=False ) assignee_id: Mapped[uuid.UUID | None] = mapped_column( PgUUID(as_uuid=True), ForeignKey("app_user.id", ondelete="SET NULL") ) due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) sort_order: Mapped[int] = mapped_column(nullable=False, default=0) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) class Tag(Base, Timestamps): __tablename__ = "tag" __table_args__ = ( UniqueConstraint("workspace_id", "name", name="uq_tag_ws_name"), Index("ix_tag_ws", "workspace_id", "name"), ) id: Mapped[uuid.UUID] = mapped_column(PgUUID(as_uuid=True), primary_key=True, default=new_uuid) workspace_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("workspace.id", ondelete="CASCADE"), nullable=False ) name: Mapped[str] = mapped_column(String(32), nullable=False) color: Mapped[str] = mapped_column(String(16), nullable=False, default="") class ItemTag(Base, Timestamps): __tablename__ = "item_tag" __table_args__ = ( UniqueConstraint("item_id", "tag_id", name="uq_item_tag"), Index("ix_item_tag_ws_tag", "workspace_id", "tag_id"), ) id: Mapped[uuid.UUID] = mapped_column(PgUUID(as_uuid=True), primary_key=True, default=new_uuid) workspace_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("workspace.id", ondelete="CASCADE"), nullable=False ) item_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("item.id", ondelete="CASCADE"), nullable=False ) tag_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("tag.id", ondelete="CASCADE"), nullable=False ) class Comment(Base, Timestamps): __tablename__ = "comment" __table_args__ = (Index("ix_comment_ws_item_created", "workspace_id", "item_id", "created_at"),) id: Mapped[uuid.UUID] = mapped_column(PgUUID(as_uuid=True), primary_key=True, default=new_uuid) workspace_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("workspace.id", ondelete="CASCADE"), nullable=False ) item_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("item.id", ondelete="CASCADE"), nullable=False ) author_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("app_user.id", ondelete="RESTRICT"), nullable=False ) body_md: Mapped[str] = mapped_column(Text, nullable=False, default="") deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) class Activity(Base): """事件流:item 的每一次实质变更都落一条。 这是"这条任务怎么走到今天的"唯一真相源,M4 的 LLM 沉淀、M3 的知识都从这里取料。 只增不改,所以没有 version / updated_at。 """ __tablename__ = "activity" __table_args__ = ( Index("ix_activity_ws_item_created", "workspace_id", "item_id", "created_at"), ) id: Mapped[uuid.UUID] = mapped_column(PgUUID(as_uuid=True), primary_key=True, default=new_uuid) workspace_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("workspace.id", ondelete="CASCADE"), nullable=False ) item_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("item.id", ondelete="CASCADE"), nullable=False ) actor_id: Mapped[uuid.UUID | None] = mapped_column( PgUUID(as_uuid=True), ForeignKey("app_user.id", ondelete="SET NULL") ) action: Mapped[str] = mapped_column(String(32), nullable=False) payload: Mapped[dict | None] = mapped_column(JSONB) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) TASK_TENANT_TABLES: tuple[str, ...] = ("item", "tag", "item_tag", "comment", "activity")