from __future__ import annotations import datetime as dt from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint from sqlalchemy.dialects.mysql import MEDIUMTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db import Base def utcnow() -> dt.datetime: return dt.datetime.now(dt.UTC).replace(tzinfo=None) MediumText = Text().with_variant(MEDIUMTEXT, "mysql") class Institution(Base): __tablename__ = "institutions" id: Mapped[int] = mapped_column(Integer, primary_key=True) institution_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) name_cn: Mapped[str] = mapped_column(String(255), nullable=False) name_en: Mapped[str | None] = mapped_column(String(255)) institution_type: Mapped[str] = mapped_column(String(32), nullable=False) source_tier: Mapped[str] = mapped_column(String(16), nullable=False) website_url: Mapped[str | None] = mapped_column(String(512)) covered_topics: Mapped[str | None] = mapped_column(Text) intro_cn: Mapped[str | None] = mapped_column(Text) credibility_note: Mapped[str | None] = mapped_column(Text) report_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) latest_report_id: Mapped[str | None] = mapped_column(String(64)) latest_report_at: Mapped[dt.datetime | None] = mapped_column(DateTime) status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) updated_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow, onupdate=utcnow) reports: Mapped[list[Report]] = relationship(back_populates="institution") __table_args__ = (Index("ix_institutions_status_latest", "status", "latest_report_at"),) class Report(Base): __tablename__ = "reports" id: Mapped[int] = mapped_column(Integer, primary_key=True) report_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) report_type: Mapped[str] = mapped_column(String(16), nullable=False, default="single") title_cn: Mapped[str] = mapped_column(String(512), nullable=False) subtitle_cn: Mapped[str | None] = mapped_column(String(512)) original_title: Mapped[str | None] = mapped_column(String(512)) one_liner: Mapped[str | None] = mapped_column(String(512)) institution_id: Mapped[str] = mapped_column(String(64), ForeignKey("institutions.institution_id"), nullable=False) co_institution_ids: Mapped[str | None] = mapped_column(Text) source_tier: Mapped[str] = mapped_column(String(32), nullable=False) source_url: Mapped[str | None] = mapped_column(String(512)) source_note: Mapped[str] = mapped_column(Text, nullable=False) published_at: Mapped[dt.datetime | None] = mapped_column(DateTime) interpreted_at: Mapped[dt.datetime | None] = mapped_column(DateTime) released_at: Mapped[dt.datetime | None] = mapped_column(DateTime) topics: Mapped[str | None] = mapped_column(Text) language: Mapped[str] = mapped_column(String(8), nullable=False, default="en") has_audio: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) display_status: Mapped[str] = mapped_column(String(16), nullable=False, default="draft") display_version: Mapped[int] = mapped_column(Integer, nullable=False, default=0) cache_version: Mapped[str] = mapped_column(String(128), nullable=False) risk_disclaimer: Mapped[str | None] = mapped_column(Text) interpretation_label: Mapped[str | None] = mapped_column(String(64), default="研报解读") created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) updated_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow, onupdate=utcnow) institution: Mapped[Institution] = relationship(back_populates="reports") __table_args__ = ( Index("ix_reports_status_released", "display_status", "released_at"), Index("ix_reports_institution_released", "institution_id", "released_at"), Index("ix_reports_audio_released", "has_audio", "released_at"), ) class RawArtifact(Base): __tablename__ = "raw_artifacts" id: Mapped[int] = mapped_column(Integer, primary_key=True) raw_artifact_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) provider: Mapped[str] = mapped_column(String(32), nullable=False, default="notebooklm") artifact_type: Mapped[str] = mapped_column(String(64), nullable=False) conversation_id: Mapped[str | None] = mapped_column(String(128)) source_id: Mapped[str | None] = mapped_column(String(128)) notebook_id: Mapped[str | None] = mapped_column(String(128)) source_language: Mapped[str | None] = mapped_column(String(8)) payload_format: Mapped[str] = mapped_column(String(16), nullable=False) payload_ref: Mapped[str | None] = mapped_column(String(512)) sha256: Mapped[str | None] = mapped_column(String(128)) status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending") error: Mapped[str | None] = mapped_column(Text) size_bytes: Mapped[int | None] = mapped_column(BigInteger) generated_at: Mapped[dt.datetime | None] = mapped_column(DateTime) ingested_at: Mapped[dt.datetime | None] = mapped_column(DateTime) is_publish_blocking: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) requires_human_review: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) quality_flags: Mapped[str | None] = mapped_column(Text) retention_status: Mapped[str] = mapped_column(String(32), nullable=False, default="retained") created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) __table_args__ = ( Index("ix_raw_report_type", "report_id", "artifact_type"), Index("ix_raw_report_status", "report_id", "status"), Index("ix_raw_retention", "retention_status"), ) class DisplayArtifact(Base): __tablename__ = "display_artifacts" id: Mapped[int] = mapped_column(Integer, primary_key=True) display_artifact_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) display_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) title_cn: Mapped[str] = mapped_column(String(512), nullable=False) summary_cn: Mapped[str | None] = mapped_column(Text) source_label: Mapped[str | None] = mapped_column(String(255)) interpretation_label: Mapped[str | None] = mapped_column(String(64), default="研报解读") ai_generated_label: Mapped[str | None] = mapped_column(String(128)) synthesis_type: Mapped[str | None] = mapped_column(String(16)) source_disclosure_text: Mapped[str | None] = mapped_column(Text) review_status: Mapped[str] = mapped_column(String(16), nullable=False, default="review") reviewed_by: Mapped[str | None] = mapped_column(String(128)) reviewed_at: Mapped[dt.datetime | None] = mapped_column(DateTime) published_at: Mapped[dt.datetime | None] = mapped_column(DateTime) created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) updated_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow, onupdate=utcnow) __table_args__ = (Index("ix_display_artifacts_report_status_version", "report_id", "review_status", "display_version"),) class DisplayModule(Base): __tablename__ = "display_modules" id: Mapped[int] = mapped_column(Integer, primary_key=True) module_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) display_artifact_id: Mapped[str] = mapped_column(String(64), ForeignKey("display_artifacts.display_artifact_id"), nullable=False) type: Mapped[str] = mapped_column(String(32), nullable=False) title_cn: Mapped[str | None] = mapped_column(String(255)) content_format: Mapped[str] = mapped_column(String(16), nullable=False) content: Mapped[str | None] = mapped_column(MediumText) content_ref: Mapped[str | None] = mapped_column(String(512)) content_etag: Mapped[str | None] = mapped_column(String(64)) source_raw_artifact_ids: Mapped[str | None] = mapped_column(Text) status: Mapped[str] = mapped_column(String(16), nullable=False, default="missing") sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) updated_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow, onupdate=utcnow) __table_args__ = ( Index("ix_display_modules_report_status_sort", "report_id", "status", "sort_order"), Index("ix_display_modules_artifact_status", "display_artifact_id", "status"), ) class AudioAsset(Base): __tablename__ = "audio_assets" id: Mapped[int] = mapped_column(Integer, primary_key=True) audio_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) source_raw_artifact_id: Mapped[str | None] = mapped_column(String(64), ForeignKey("raw_artifacts.raw_artifact_id")) title_cn: Mapped[str] = mapped_column(String(512), nullable=False) duration_sec: Mapped[int | None] = mapped_column(Integer) oss_key: Mapped[str | None] = mapped_column(String(512)) waveform_ref: Mapped[str | None] = mapped_column(String(512)) chapters: Mapped[str | None] = mapped_column(Text) status: Mapped[str] = mapped_column(String(16), nullable=False, default="missing") published_at: Mapped[dt.datetime | None] = mapped_column(DateTime) created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) updated_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow, onupdate=utcnow) __table_args__ = (Index("ix_audio_report_status", "report_id", "status"),) class RelatedNews(Base): __tablename__ = "related_news" id: Mapped[int] = mapped_column(Integer, primary_key=True) related_news_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) title: Mapped[str] = mapped_column(String(512), nullable=False) source_name: Mapped[str | None] = mapped_column(String(255)) source_url: Mapped[str | None] = mapped_column(String(512)) published_at: Mapped[dt.datetime | None] = mapped_column(DateTime) language: Mapped[str | None] = mapped_column(String(8)) summary_cn: Mapped[str | None] = mapped_column(Text) match_method: Mapped[str] = mapped_column(String(32), nullable=False, default="manual_curated") match_keywords: Mapped[str | None] = mapped_column(Text) match_confidence: Mapped[str | None] = mapped_column(String(8)) status: Mapped[str] = mapped_column(String(16), nullable=False, default="candidate") created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) __table_args__ = (Index("ix_related_news_report_status", "report_id", "status"),) class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) phone_hash: Mapped[str | None] = mapped_column(String(128), unique=True) wechat_openid: Mapped[str | None] = mapped_column(String(128), unique=True) apple_user_id: Mapped[str | None] = mapped_column(String(256), unique=True) display_name: Mapped[str | None] = mapped_column(String(128)) avatar_url: Mapped[str | None] = mapped_column(String(512)) created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) last_login_at: Mapped[dt.datetime | None] = mapped_column(DateTime) status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") class Favorite(Base): __tablename__ = "favorites" id: Mapped[int] = mapped_column(Integer, primary_key=True) favorite_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) user_id: Mapped[str] = mapped_column(String(64), ForeignKey("users.user_id"), nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") __table_args__ = (UniqueConstraint("user_id", "report_id", name="uq_favorites_user_report"), Index("ix_favorites_user_report", "user_id", "report_id")) class ReadingHistory(Base): __tablename__ = "reading_history" id: Mapped[int] = mapped_column(Integer, primary_key=True) history_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) user_id: Mapped[str] = mapped_column(String(64), ForeignKey("users.user_id"), nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) event_type: Mapped[str] = mapped_column(String(32), nullable=False, default="view_detail") last_position: Mapped[str | None] = mapped_column(Text) last_seen_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) __table_args__ = (Index("ix_reading_history_user_seen", "user_id", "last_seen_at"), Index("ix_reading_history_user_report", "user_id", "report_id")) class SavedListen(Base): __tablename__ = "saved_listens" id: Mapped[int] = mapped_column(Integer, primary_key=True) saved_listen_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) user_id: Mapped[str] = mapped_column(String(64), ForeignKey("users.user_id"), nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) audio_id: Mapped[str] = mapped_column(String(64), ForeignKey("audio_assets.audio_id"), nullable=False) created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") __table_args__ = (UniqueConstraint("user_id", "audio_id", name="uq_saved_listens_user_audio"),) class PlaybackProgress(Base): __tablename__ = "playback_progress" id: Mapped[int] = mapped_column(Integer, primary_key=True) progress_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) user_id: Mapped[str] = mapped_column(String(64), ForeignKey("users.user_id"), nullable=False) audio_id: Mapped[str] = mapped_column(String(64), ForeignKey("audio_assets.audio_id"), nullable=False) report_id: Mapped[str] = mapped_column(String(64), ForeignKey("reports.report_id"), nullable=False) position_sec: Mapped[int] = mapped_column(Integer, nullable=False, default=0) duration_sec: Mapped[int | None] = mapped_column(Integer) completed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) updated_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow, onupdate=utcnow) __table_args__ = (UniqueConstraint("user_id", "audio_id", name="uq_playback_user_audio"), Index("ix_playback_user_audio", "user_id", "audio_id")) class OutboundEvent(Base): __tablename__ = "outbound_events" id: Mapped[int] = mapped_column(Integer, primary_key=True) outbound_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) click_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) tracking_id: Mapped[str] = mapped_column(String(64), nullable=False) user_id: Mapped[str | None] = mapped_column(String(64), ForeignKey("users.user_id")) device_id: Mapped[str | None] = mapped_column(String(128)) report_id: Mapped[str | None] = mapped_column(String(64), ForeignKey("reports.report_id")) institution_id: Mapped[str | None] = mapped_column(String(64)) scene: Mapped[str | None] = mapped_column(String(64)) ref: Mapped[str | None] = mapped_column(String(128)) target: Mapped[str | None] = mapped_column(String(32)) source_page: Mapped[str | None] = mapped_column(String(32)) placement: Mapped[str | None] = mapped_column(String(64)) campaign_id: Mapped[str | None] = mapped_column(String(64)) target_app: Mapped[str | None] = mapped_column(String(64)) commodity_tag: Mapped[str | None] = mapped_column(String(64)) hook_type: Mapped[str | None] = mapped_column(String(64)) user_state: Mapped[str | None] = mapped_column(String(16)) ts: Mapped[int | None] = mapped_column(BigInteger) created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=utcnow) __table_args__ = (Index("ix_outbound_tracking", "tracking_id"), Index("ix_outbound_report_created", "report_id", "created_at"))