init: EasyScreen 电子菜单系统
- server: FastAPI 后端(多屏管理、素材上传、节目编排、客户端注册/配置/心跳/崩溃上报、管理台托管) - android: Java 客户端(minSdk 23,全屏图片/视频轮播、远程配置、开机自启、崩溃上报) - web: React + Vite + antd 管理台(屏幕/素材/节目管理) - 屏幕设备 ID 关联机制、gunicorn 生产部署脚本
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""管理端:节目(播放清单)CRUD。"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from ..auth import get_current_user
|
||||
from ..database import get_db
|
||||
from ..models import Asset, Playlist, PlaylistItem, ScreenBinding
|
||||
from ..schemas import PlaylistCreate, PlaylistItemIn, PlaylistOut, PlaylistUpdate
|
||||
|
||||
router = APIRouter(prefix="/api/playlists", tags=["playlists"], dependencies=[Depends(get_current_user)])
|
||||
|
||||
|
||||
def _get_playlist_or_404(db: Session, playlist_id: int) -> Playlist:
|
||||
playlist = db.get(Playlist, playlist_id, options=[selectinload(Playlist.items).selectinload(PlaylistItem.asset)])
|
||||
if not playlist:
|
||||
raise HTTPException(status_code=404, detail="节目不存在")
|
||||
return playlist
|
||||
|
||||
|
||||
def _replace_items(db: Session, playlist_id: int, items: list[PlaylistItemIn]):
|
||||
"""全量替换节目项。"""
|
||||
asset_ids = {item.asset_id for item in items}
|
||||
if asset_ids:
|
||||
found = set(db.scalars(select(Asset.id).where(Asset.id.in_(asset_ids))).all())
|
||||
missing = asset_ids - found
|
||||
if missing:
|
||||
raise HTTPException(status_code=400, detail=f"素材不存在: {sorted(missing)}")
|
||||
|
||||
db.execute(delete(PlaylistItem).where(PlaylistItem.playlist_id == playlist_id))
|
||||
for idx, item in enumerate(items):
|
||||
db.add(
|
||||
PlaylistItem(
|
||||
playlist_id=playlist_id,
|
||||
asset_id=item.asset_id,
|
||||
sort_order=idx,
|
||||
duration=item.duration,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[PlaylistOut])
|
||||
def list_playlists(db: Session = Depends(get_db)):
|
||||
return db.scalars(
|
||||
select(Playlist)
|
||||
.options(selectinload(Playlist.items).selectinload(PlaylistItem.asset))
|
||||
.order_by(Playlist.created_at.desc())
|
||||
).all()
|
||||
|
||||
|
||||
@router.get("/{playlist_id}", response_model=PlaylistOut)
|
||||
def get_playlist(playlist_id: int, db: Session = Depends(get_db)):
|
||||
return _get_playlist_or_404(db, playlist_id)
|
||||
|
||||
|
||||
@router.post("", response_model=PlaylistOut)
|
||||
def create_playlist(body: PlaylistCreate, db: Session = Depends(get_db)):
|
||||
playlist = Playlist(name=body.name, description=body.description)
|
||||
db.add(playlist)
|
||||
db.flush()
|
||||
_replace_items(db, playlist.id, body.items)
|
||||
db.commit()
|
||||
return _get_playlist_or_404(db, playlist.id)
|
||||
|
||||
|
||||
@router.put("/{playlist_id}", response_model=PlaylistOut)
|
||||
def update_playlist(playlist_id: int, body: PlaylistUpdate, db: Session = Depends(get_db)):
|
||||
playlist = _get_playlist_or_404(db, playlist_id)
|
||||
if body.name is not None:
|
||||
playlist.name = body.name
|
||||
if body.description is not None:
|
||||
playlist.description = body.description
|
||||
if body.items is not None:
|
||||
_replace_items(db, playlist.id, body.items)
|
||||
db.commit()
|
||||
return _get_playlist_or_404(db, playlist_id)
|
||||
|
||||
|
||||
@router.delete("/{playlist_id}")
|
||||
def delete_playlist(playlist_id: int, db: Session = Depends(get_db)):
|
||||
playlist = _get_playlist_or_404(db, playlist_id)
|
||||
has_active_binding = (
|
||||
db.scalar(select(ScreenBinding.id).where(ScreenBinding.playlist_id == playlist_id, ScreenBinding.active == True)) # noqa: E712
|
||||
is not None
|
||||
)
|
||||
db.execute(delete(ScreenBinding).where(ScreenBinding.playlist_id == playlist_id))
|
||||
db.delete(playlist)
|
||||
db.commit()
|
||||
return {"ok": True, "unbound_screens": has_active_binding}
|
||||
Reference in New Issue
Block a user