import uuid from datetime import UTC, datetime from sqlalchemy import CheckConstraint, Float, ForeignKey, Index, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db import Base def new_uuid() -> str: return str(uuid.uuid4()) def utc_now() -> datetime: return datetime.now(UTC) class Task(Base): __tablename__ = "tasks" __table_args__ = ( CheckConstraint("status IN ('running', 'success', 'failed')", name="ck_tasks_status"), CheckConstraint( "analysis_status IN ('normal', 'insufficient')", name="ck_tasks_analysis_status", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) platform: Mapped[str] = mapped_column(String(32), nullable=False) status: Mapped[str] = mapped_column(String(16), nullable=False, default="running") analysis_status: Mapped[str] = mapped_column(String(16), nullable=False, default="normal") analysis_success_rate: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) hotspot_limit: Mapped[int] = mapped_column(Integer, nullable=False, default=5) item_limit_per_hotspot: Mapped[int] = mapped_column(Integer, nullable=False, default=5) comment_limit_per_item: Mapped[int] = mapped_column(Integer, nullable=False, default=50) total_items_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) processed_items_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) successful_items_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) failed_items_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) error_stage: Mapped[str | None] = mapped_column(String(64)) error_type: Mapped[str | None] = mapped_column(String(64)) error_message: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(nullable=False, default=utc_now) started_at: Mapped[datetime | None] = mapped_column() finished_at: Mapped[datetime | None] = mapped_column() hotspots: Mapped[list["Hotspot"]] = relationship(back_populates="task") content_items: Mapped[list["ContentItem"]] = relationship(back_populates="task") comments: Mapped[list["Comment"]] = relationship(back_populates="task") reports: Mapped[list["Report"]] = relationship(back_populates="task") class Hotspot(Base): __tablename__ = "hotspots" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) task_id: Mapped[str] = mapped_column(ForeignKey("tasks.id"), nullable=False) platform: Mapped[str] = mapped_column(String(32), nullable=False) source_hot_id: Mapped[str | None] = mapped_column(String(128)) rank: Mapped[int | None] = mapped_column(Integer) title: Mapped[str] = mapped_column(Text, nullable=False) heat_value: Mapped[str | None] = mapped_column(String(128)) raw_data: Mapped[str] = mapped_column(Text, nullable=False, default="{}") created_at: Mapped[datetime] = mapped_column(nullable=False, default=utc_now) task: Mapped[Task] = relationship(back_populates="hotspots") content_items: Mapped[list["ContentItem"]] = relationship(back_populates="hotspot") comments: Mapped[list["Comment"]] = relationship(back_populates="hotspot") reports: Mapped[list["Report"]] = relationship(back_populates="hotspot") class ContentItem(Base): __tablename__ = "content_items" __table_args__ = ( Index("ix_content_items_task_hotspot", "task_id", "hotspot_id"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) task_id: Mapped[str] = mapped_column(ForeignKey("tasks.id"), nullable=False) hotspot_id: Mapped[str] = mapped_column(ForeignKey("hotspots.id"), nullable=False) platform: Mapped[str] = mapped_column(String(32), nullable=False) source_item_id: Mapped[str] = mapped_column(String(128), nullable=False) item_type: Mapped[str] = mapped_column(String(32), nullable=False) title: Mapped[str | None] = mapped_column(Text) summary: Mapped[str | None] = mapped_column(Text) url: Mapped[str | None] = mapped_column(Text) status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending") error_stage: Mapped[str | None] = mapped_column(String(64)) error_type: Mapped[str | None] = mapped_column(String(64)) error_message: Mapped[str | None] = mapped_column(Text) raw_data: Mapped[str] = mapped_column(Text, nullable=False, default="{}") created_at: Mapped[datetime] = mapped_column(nullable=False, default=utc_now) task: Mapped[Task] = relationship(back_populates="content_items") hotspot: Mapped[Hotspot] = relationship(back_populates="content_items") comments: Mapped[list["Comment"]] = relationship(back_populates="content_item") reports: Mapped[list["Report"]] = relationship(back_populates="content_item") class Comment(Base): __tablename__ = "comments" __table_args__ = ( Index("ix_comments_task_content_item", "task_id", "content_item_id"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) task_id: Mapped[str] = mapped_column(ForeignKey("tasks.id"), nullable=False) hotspot_id: Mapped[str] = mapped_column(ForeignKey("hotspots.id"), nullable=False) content_item_id: Mapped[str] = mapped_column(ForeignKey("content_items.id"), nullable=False) platform: Mapped[str] = mapped_column(String(32), nullable=False) source_comment_id: Mapped[str | None] = mapped_column(String(128)) content: Mapped[str] = mapped_column(Text, nullable=False) author: Mapped[str | None] = mapped_column(Text) like_count: Mapped[int | None] = mapped_column(Integer) comment_time: Mapped[datetime | None] = mapped_column() sentiment: Mapped[str] = mapped_column(String(32), nullable=False, default="unknown") labels: Mapped[str] = mapped_column(Text, nullable=False, default="[]") reason: Mapped[str | None] = mapped_column(Text) ai_analysis_status: Mapped[str] = mapped_column(String(32), nullable=False, default="skipped") raw_data: Mapped[str] = mapped_column(Text, nullable=False, default="{}") ai_raw_response: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(nullable=False, default=utc_now) task: Mapped[Task] = relationship(back_populates="comments") hotspot: Mapped[Hotspot] = relationship(back_populates="comments") content_item: Mapped[ContentItem] = relationship(back_populates="comments") class Report(Base): __tablename__ = "reports" __table_args__ = ( Index("ix_reports_task_report_type", "task_id", "report_type"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) task_id: Mapped[str] = mapped_column(ForeignKey("tasks.id"), nullable=False) hotspot_id: Mapped[str | None] = mapped_column(ForeignKey("hotspots.id")) content_item_id: Mapped[str | None] = mapped_column(ForeignKey("content_items.id")) report_type: Mapped[str] = mapped_column(String(32), nullable=False) title: Mapped[str] = mapped_column(Text, nullable=False) data: Mapped[str] = mapped_column(Text, nullable=False, default="{}") markdown: Mapped[str] = mapped_column(Text, nullable=False, default="") metrics_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}") typical_comments_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]") summary: Mapped[str] = mapped_column(Text, nullable=False, default="") markdown_content: Mapped[str] = mapped_column(Text, nullable=False, default="") created_at: Mapped[datetime] = mapped_column(nullable=False, default=utc_now) task: Mapped[Task] = relationship(back_populates="reports") hotspot: Mapped[Hotspot | None] = relationship(back_populates="reports") content_item: Mapped[ContentItem | None] = relationship(back_populates="reports") def create_report_record( *, task_id: str, report_type: str, title: str, hotspot_id: str | None = None, content_item_id: str | None = None, data: str = "{}", markdown: str = "", ) -> Report: if report_type == "hotspot" and not hotspot_id: raise ValueError("hotspot report requires hotspot_id") if report_type == "content_item" and not content_item_id: raise ValueError("content item report requires content_item_id") return Report( task_id=task_id, report_type=report_type, title=title, hotspot_id=hotspot_id, content_item_id=content_item_id, data=data, markdown=markdown, )