"""管理端:屏幕 CRUD、绑定节目。""" import uuid from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.orm import Session, selectinload from ..auth import get_current_user from ..database import get_db from ..models import Playlist, Screen, ScreenBinding from ..schemas import ( BindRequest, PlaylistOut, ScreenCreate, ScreenDetail, ScreenOut, ScreenUpdate, ) router = APIRouter(prefix="/api/screens", tags=["screens"], dependencies=[Depends(get_current_user)]) ONLINE_WINDOW = timedelta(seconds=90) # 与客户端心跳间隔保持一致 def _apply_online_status(screen: Screen) -> Screen: """根据最后心跳时间动态刷新在线状态(不落库,仅展示层)。""" if screen.last_seen and datetime.now() - screen.last_seen <= ONLINE_WINDOW: screen.status = "online" else: screen.status = "offline" return screen def _get_screen_or_404(db: Session, screen_id: int) -> Screen: screen = db.get(Screen, screen_id) if not screen: raise HTTPException(status_code=404, detail="屏幕不存在") return screen @router.get("", response_model=list[ScreenDetail]) def list_screens(db: Session = Depends(get_db)): screens = db.scalars( select(Screen) .options(selectinload(Screen.bindings).selectinload(ScreenBinding.playlist)) .order_by(Screen.id) ).all() result = [] for s in screens: _apply_online_status(s) d = ScreenDetail.model_validate(s) active = next((b for b in s.bindings if b.active), None) if active and active.playlist: d.playlist = PlaylistOut.model_validate(active.playlist) result.append(d) return result @router.get("/{screen_id}", response_model=ScreenDetail) def get_screen(screen_id: int, db: Session = Depends(get_db)): screen = db.get( Screen, screen_id, options=[selectinload(Screen.bindings).selectinload(ScreenBinding.playlist).selectinload(Playlist.items)], ) if not screen: raise HTTPException(status_code=404, detail="屏幕不存在") _apply_online_status(screen) detail = ScreenDetail.model_validate(screen) active = next((b for b in screen.bindings if b.active), None) if active and active.playlist: detail.playlist = PlaylistOut.model_validate(active.playlist) return detail @router.post("", response_model=ScreenOut) def create_screen(body: ScreenCreate, db: Session = Depends(get_db)): """手动创建屏幕(可选指定 device_id 用于客户端注册匹配)。""" device_id = body.device_id if not device_id: # 未指定设备 ID 时:先写临时 UUID 满足 NOT NULL 约束,flush 拿到 id 后改为 pre-{id} screen = Screen(name=body.name, location=body.location, device_id=f"tmp-{uuid.uuid4().hex}") db.add(screen) db.flush() screen.device_id = f"pre-{screen.id}" else: exists = db.scalars(select(Screen).where(Screen.device_id == device_id)).first() if exists: raise HTTPException(status_code=409, detail=f"device_id 已存在(屏幕: {exists.name})") screen = Screen(name=body.name, location=body.location, device_id=device_id) db.add(screen) db.commit() db.refresh(screen) return screen @router.put("/{screen_id}", response_model=ScreenOut) def update_screen(screen_id: int, body: ScreenUpdate, db: Session = Depends(get_db)): screen = _get_screen_or_404(db, screen_id) if body.name is not None: screen.name = body.name if body.location is not None: screen.location = body.location db.commit() db.refresh(screen) return screen @router.delete("/{screen_id}") def delete_screen(screen_id: int, db: Session = Depends(get_db)): screen = _get_screen_or_404(db, screen_id) db.delete(screen) db.commit() return {"ok": True} @router.post("/{screen_id}/bind", response_model=ScreenDetail) def bind_playlist(screen_id: int, body: BindRequest, db: Session = Depends(get_db)): """绑定节目到屏幕(置为 active,其它绑定取消 active)。""" screen = _get_screen_or_404(db, screen_id) playlist = db.get(Playlist, body.playlist_id) if not playlist: raise HTTPException(status_code=404, detail="节目不存在") for b in screen.bindings: b.active = False existing = next((b for b in screen.bindings if b.playlist_id == body.playlist_id), None) if existing: existing.active = True else: db.add(ScreenBinding(screen_id=screen.id, playlist_id=body.playlist_id, active=True)) db.commit() detail = get_screen(screen_id, db) return detail @router.delete("/{screen_id}/bind") def unbind_playlist(screen_id: int, db: Session = Depends(get_db)): """解除屏幕当前绑定。""" screen = _get_screen_or_404(db, screen_id) for b in screen.bindings: if b.active: db.delete(b) db.commit() return {"ok": True}