Files
EasyScreen/server/app/routers/admin_screens.py
T
Tatta 4e2775c8c3 init: EasyScreen 电子菜单系统
- server: FastAPI 后端(多屏管理、素材上传、节目编排、客户端注册/配置/心跳/崩溃上报、管理台托管)
- android: Java 客户端(minSdk 23,全屏图片/视频轮播、远程配置、开机自启、崩溃上报)
- web: React + Vite + antd 管理台(屏幕/素材/节目管理)
- 屏幕设备 ID 关联机制、gunicorn 生产部署脚本
2026-08-12 20:57:55 +08:00

148 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""管理端:屏幕 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}