文件版本管理-初步优化

This commit is contained in:
Cheng Zhou
2026-01-14 11:35:37 +08:00
parent ef1e67218c
commit f9ef5c109f
46 changed files with 4603 additions and 30 deletions
+37
View File
@@ -0,0 +1,37 @@
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class AcknowledgementType(str, enum.Enum):
RECEIVED = "RECEIVED"
READ = "READ"
TRAINED = "TRAINED"
class Acknowledgement(Base):
__tablename__ = "acknowledgements"
__table_args__ = (
UniqueConstraint("distribution_id", "user_id", "ack_type", name="uq_ack_distribution_user_type"),
)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
distribution_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("distributions.id"), nullable=False)
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
site_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), nullable=True)
ack_type: Mapped[AcknowledgementType] = mapped_column(
Enum(AcknowledgementType, name="acknowledgement_type"),
nullable=False,
)
due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
acked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
distribution = relationship("Distribution", back_populates="acknowledgements")
+2 -1
View File
@@ -1,7 +1,7 @@
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy import DateTime, ForeignKey, Index, String, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
@@ -10,6 +10,7 @@ from app.db.base_class import Base
class AuditLog(Base):
__tablename__ = "audit_logs"
__table_args__ = (Index("ix_audit_logs_entity_at", "entity_type", "entity_id", "created_at"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
study_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=True)
+50
View File
@@ -0,0 +1,50 @@
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, String, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class DistributionTargetType(str, enum.Enum):
SITE = "SITE"
ROLE = "ROLE"
USER = "USER"
class DistributionStatus(str, enum.Enum):
ACTIVE = "ACTIVE"
CLOSED = "CLOSED"
class Distribution(Base):
__tablename__ = "distributions"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
document_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("documents.id"), nullable=False)
version_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("document_versions.id"), nullable=False)
target_type: Mapped[DistributionTargetType] = mapped_column(
Enum(DistributionTargetType, name="distribution_target_type"),
nullable=False,
)
target_id: Mapped[str] = mapped_column(String(50), nullable=False)
status: Mapped[DistributionStatus] = mapped_column(
Enum(DistributionStatus, name="distribution_status"),
nullable=False,
server_default=DistributionStatus.ACTIVE.value,
)
due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
document = relationship("Document")
version = relationship("DocumentVersion", back_populates="distributions")
acknowledgements = relationship(
"Acknowledgement",
back_populates="distribution",
cascade="all, delete-orphan",
)
+69
View File
@@ -0,0 +1,69 @@
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class DocumentScopeType(str, enum.Enum):
GLOBAL = "GLOBAL"
SITE = "SITE"
DERIVED = "DERIVED"
class DocumentStatus(str, enum.Enum):
DRAFT = "DRAFT"
ACTIVE = "ACTIVE"
ARCHIVED = "ARCHIVED"
class Document(Base):
__tablename__ = "documents"
__table_args__ = (UniqueConstraint("trial_id", "doc_no", name="uq_documents_trial_doc_no"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
trial_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=False)
site_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), nullable=True)
doc_no: Mapped[str] = mapped_column(String(50), nullable=False)
doc_type: Mapped[str] = mapped_column(String(50), nullable=False)
title: Mapped[str] = mapped_column(String(200), nullable=False)
scope_type: Mapped[DocumentScopeType] = mapped_column(
Enum(DocumentScopeType, name="document_scope_type"),
nullable=False,
server_default=DocumentScopeType.GLOBAL.value,
)
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
status: Mapped[DocumentStatus] = mapped_column(
Enum(DocumentStatus, name="document_status"),
nullable=False,
server_default=DocumentStatus.ACTIVE.value,
)
current_effective_version_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("document_versions.id"),
nullable=True,
)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
onupdate=func.now(),
)
versions = relationship(
"DocumentVersion",
back_populates="document",
cascade="all, delete-orphan",
foreign_keys="DocumentVersion.document_id",
)
current_effective_version = relationship(
"DocumentVersion",
foreign_keys=[current_effective_version_id],
post_update=True,
)
+61
View File
@@ -0,0 +1,61 @@
import enum
import uuid
from datetime import datetime
from sqlalchemy import BigInteger, DateTime, Enum, ForeignKey, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class DocumentVersionStatus(str, enum.Enum):
DRAFT = "DRAFT"
SUBMITTED = "SUBMITTED"
APPROVED = "APPROVED"
EFFECTIVE = "EFFECTIVE"
SUPERSEDED = "SUPERSEDED"
ARCHIVED = "ARCHIVED"
WITHDRAWN = "WITHDRAWN"
class DocumentVersion(Base):
__tablename__ = "document_versions"
__table_args__ = (UniqueConstraint("document_id", "version_no", name="uq_document_versions_doc_no"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
document_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("documents.id"), nullable=False)
version_no: Mapped[str] = mapped_column(String(50), nullable=False)
parent_version_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("document_versions.id"),
nullable=True,
)
derived_from_version_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("document_versions.id"),
nullable=True,
)
status: Mapped[DocumentVersionStatus] = mapped_column(
Enum(DocumentVersionStatus, name="document_version_status"),
nullable=False,
server_default=DocumentVersionStatus.DRAFT.value,
)
effective_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
superseded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
file_uri: Mapped[str] = mapped_column(String(500), nullable=False)
file_hash: Mapped[str] = mapped_column(String(128), nullable=False)
file_size: Mapped[int] = mapped_column(BigInteger, nullable=False)
mime_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
change_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
submitted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
withdrawn_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
document = relationship("Document", back_populates="versions", foreign_keys=[document_id])
parent_version = relationship("DocumentVersion", remote_side=[id], foreign_keys=[parent_version_id])
derived_from_version = relationship("DocumentVersion", remote_side=[id], foreign_keys=[derived_from_version_id])
workflow_instance = relationship("VersionWorkflow", back_populates="version", uselist=False)
distributions = relationship("Distribution", back_populates="version")
+1
View File
@@ -15,6 +15,7 @@ class UserRole(str, enum.Enum):
CRA = "CRA"
PV = "PV"
IMP = "IMP"
QA = "QA"
class UserStatus(str, enum.Enum):
+47
View File
@@ -0,0 +1,47 @@
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class WorkflowStatus(str, enum.Enum):
PENDING = "PENDING"
APPROVED = "APPROVED"
REJECTED = "REJECTED"
CANCELED = "CANCELED"
class VersionWorkflow(Base):
__tablename__ = "version_workflows"
__table_args__ = (UniqueConstraint("version_id", name="uq_version_workflow_version"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
document_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("documents.id"), nullable=False)
version_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("document_versions.id"), nullable=False)
template_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("workflow_templates.id"), nullable=False)
status: Mapped[WorkflowStatus] = mapped_column(
Enum(WorkflowStatus, name="workflow_status"),
nullable=False,
server_default=WorkflowStatus.PENDING.value,
)
current_node: Mapped[int | None] = mapped_column(Integer, nullable=True)
submitted_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
submitted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
onupdate=func.now(),
)
template = relationship("WorkflowTemplate")
version = relationship("DocumentVersion", back_populates="workflow_instance")
document = relationship("Document")
actions = relationship("WorkflowAction", back_populates="workflow", cascade="all, delete-orphan")
+32
View File
@@ -0,0 +1,32 @@
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class WorkflowActionType(str, enum.Enum):
SUBMIT = "SUBMIT"
APPROVE = "APPROVE"
REJECT = "REJECT"
class WorkflowAction(Base):
__tablename__ = "workflow_actions"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
workflow_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("version_workflows.id"), nullable=False)
node_order: Mapped[int | None] = mapped_column(Integer, nullable=True)
action: Mapped[WorkflowActionType] = mapped_column(
Enum(WorkflowActionType, name="workflow_action_type"),
nullable=False,
)
actor_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
comment: Mapped[str | None] = mapped_column(Text, nullable=True)
acted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
workflow = relationship("VersionWorkflow", back_populates="actions")
+23
View File
@@ -0,0 +1,23 @@
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class WorkflowNode(Base):
__tablename__ = "workflow_nodes"
__table_args__ = (UniqueConstraint("template_id", "node_order", name="uq_workflow_nodes_order"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
template_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("workflow_templates.id"), nullable=False)
node_order: Mapped[int] = mapped_column(Integer, nullable=False)
role: Mapped[str] = mapped_column(String(50), nullable=False)
name: Mapped[str | None] = mapped_column(String(100), nullable=True)
is_required: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="true")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
template = relationship("WorkflowTemplate", back_populates="nodes")
+33
View File
@@ -0,0 +1,33 @@
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class WorkflowTemplate(Base):
__tablename__ = "workflow_templates"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
trial_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="true")
created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
onupdate=func.now(),
)
nodes = relationship(
"WorkflowNode",
back_populates="template",
cascade="all, delete-orphan",
order_by="WorkflowNode.node_order",
)