"""数据模型:屏幕设备、素材、节目、节目项、屏幕绑定。""" from datetime import datetime from sqlalchemy import ( Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from .database import Base class Screen(Base): """显示屏设备。device_id 由客户端首次启动时生成并注册。""" __tablename__ = "screens" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(100), default="未命名屏幕") device_id: Mapped[str] = mapped_column(String(64), unique=True, index=True) location: Mapped[str] = mapped_column(String(200), default="") status: Mapped[str] = mapped_column(String(20), default="offline") # online/offline last_seen: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) bindings: Mapped[list["ScreenBinding"]] = relationship( back_populates="screen", cascade="all, delete-orphan" ) class Asset(Base): """媒体素材:图片或视频。url 为相对于 /media 的路径。""" __tablename__ = "assets" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(200)) type: Mapped[str] = mapped_column(String(20)) # image / video url: Mapped[str] = mapped_column(String(500)) size: Mapped[int] = mapped_column(Integer, default=0) duration: Mapped[int] = mapped_column(Integer, default=0) # 图片默认展示秒数;视频为0表示按视频时长 created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) items: Mapped[list["PlaylistItem"]] = relationship(back_populates="asset") class Playlist(Base): """节目:一组素材的轮播清单。""" __tablename__ = "playlists" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(100)) description: Mapped[str] = mapped_column(Text, default="") created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) items: Mapped[list["PlaylistItem"]] = relationship( back_populates="playlist", cascade="all, delete-orphan", order_by="PlaylistItem.sort_order" ) bindings: Mapped[list["ScreenBinding"]] = relationship( back_populates="playlist", cascade="all, delete-orphan" ) class PlaylistItem(Base): """节目项:素材 + 排序 + 可选展示时长覆盖。""" __tablename__ = "playlist_items" __table_args__ = (UniqueConstraint("playlist_id", "sort_order", name="uq_playlist_sort"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id", ondelete="CASCADE"), index=True) asset_id: Mapped[int] = mapped_column(ForeignKey("assets.id", ondelete="CASCADE"), index=True) sort_order: Mapped[int] = mapped_column(Integer, default=0) duration: Mapped[int | None] = mapped_column(Integer, nullable=True) # 覆盖素材默认时长(秒) playlist: Mapped[Playlist] = relationship(back_populates="items") asset: Mapped[Asset] = relationship(back_populates="items") class ScreenBinding(Base): """屏幕与节目的绑定。一个屏幕同时只有一个 active=True 的绑定。""" __tablename__ = "screen_bindings" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) screen_id: Mapped[int] = mapped_column(ForeignKey("screens.id", ondelete="CASCADE"), index=True) playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id", ondelete="CASCADE"), index=True) active: Mapped[bool] = mapped_column(Boolean, default=True) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now) screen: Mapped[Screen] = relationship(back_populates="bindings") playlist: Mapped[Playlist] = relationship(back_populates="bindings")