"""附件:任务与知识条目共用一张表。 为什么不区分 kind:知识本身就是 `item`(kind=knowledge),附件挂在 `item_id` 上 天然对任务、工单、知识一视同仁;将来要给「工单」加验收材料也不用再建表。 文件本体放磁盘,库里只存元数据与 `storage_key`。这样做的代价是备份要同时备盘, 收益是数据库不会被几十 MB 的文件撑爆、备份与恢复的粒度还能分开。 """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( BigInteger, CheckConstraint, DateTime, ForeignKey, Index, String, func, ) from sqlalchemy.dialects.postgresql import UUID as PgUUID from sqlalchemy.orm import Mapped, mapped_column from app.models import Base ATTACHMENT_TENANT_TABLES = ("attachment",) class Attachment(Base): __tablename__ = "attachment" __table_args__ = ( CheckConstraint("size_bytes >= 0", name="ck_attachment_size"), Index("ix_attachment_ws_item", "workspace_id", "item_id", "created_at"), ) id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) 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, ) uploader_id: Mapped[uuid.UUID] = mapped_column( PgUUID(as_uuid=True), ForeignKey("app_user.id", ondelete="RESTRICT"), nullable=False ) # 原始文件名:只用于展示与下载时的 Content-Disposition,绝不参与磁盘路径 filename: Mapped[str] = mapped_column(String(255), nullable=False) content_type: Mapped[str] = mapped_column( String(120), nullable=False, default="application/octet-stream" ) size_bytes: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) sha256: Mapped[str] = mapped_column(String(64), nullable=False) storage_key: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False )