init: EasyScreen 电子菜单系统
- server: FastAPI 后端(多屏管理、素材上传、节目编排、客户端注册/配置/心跳/崩溃上报、管理台托管) - android: Java 客户端(minSdk 23,全屏图片/视频轮播、远程配置、开机自启、崩溃上报) - web: React + Vite + antd 管理台(屏幕/素材/节目管理) - 屏幕设备 ID 关联机制、gunicorn 生产部署脚本
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""管理端:素材上传与管理。"""
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..auth import get_current_user
|
||||
from ..config import get_server_config
|
||||
from ..database import get_db
|
||||
from ..models import Asset, PlaylistItem
|
||||
from ..schemas import AssetOut
|
||||
|
||||
router = APIRouter(prefix="/api/assets", tags=["assets"], dependencies=[Depends(get_current_user)])
|
||||
|
||||
_EXT_MAP = {
|
||||
"image/jpeg": ".jpg",
|
||||
"image/png": ".png",
|
||||
"image/gif": ".gif",
|
||||
"image/webp": ".webp",
|
||||
"video/mp4": ".mp4",
|
||||
"video/webm": ".webm",
|
||||
"video/x-matroska": ".mkv",
|
||||
}
|
||||
|
||||
|
||||
@router.get("", response_model=list[AssetOut])
|
||||
def list_assets(db: Session = Depends(get_db)):
|
||||
return db.scalars(select(Asset).order_by(Asset.created_at.desc())).all()
|
||||
|
||||
|
||||
@router.post("", response_model=AssetOut)
|
||||
async def upload_asset(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
||||
"""上传图片/视频素材。"""
|
||||
cfg = get_server_config()
|
||||
content_type = file.content_type or ""
|
||||
ext = _EXT_MAP.get(content_type)
|
||||
if not ext:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的素材类型: {content_type or '未知'}")
|
||||
|
||||
max_bytes = cfg.get("max_upload_mb", 500) * 1024 * 1024
|
||||
# 流式读取并限制大小
|
||||
data = b""
|
||||
while chunk := await file.read(1024 * 1024):
|
||||
data += chunk
|
||||
if len(data) > max_bytes:
|
||||
raise HTTPException(status_code=413, detail=f"文件超过大小限制 {cfg['max_upload_mb']}MB")
|
||||
|
||||
filename = f"{uuid.uuid4().hex}{ext}"
|
||||
upload_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), cfg.get("upload_dir", "./uploads")))
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
dest = os.path.join(upload_dir, filename)
|
||||
with open(dest, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
is_video = content_type.startswith("video/")
|
||||
asset = Asset(
|
||||
name=file.filename or filename,
|
||||
type="video" if is_video else "image",
|
||||
url=f"/media/{filename}",
|
||||
size=len(data),
|
||||
duration=0 if is_video else 10, # 图片默认展示 10 秒
|
||||
)
|
||||
db.add(asset)
|
||||
db.commit()
|
||||
db.refresh(asset)
|
||||
return asset
|
||||
|
||||
|
||||
@router.delete("/{asset_id}")
|
||||
def delete_asset(asset_id: int, db: Session = Depends(get_db)):
|
||||
asset = db.get(Asset, asset_id)
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="素材不存在")
|
||||
|
||||
used = db.scalar(select(func.count(PlaylistItem.id)).where(PlaylistItem.asset_id == asset_id))
|
||||
if used:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"该素材正被 {used} 个节目引用,请先从节目中移除再删除",
|
||||
)
|
||||
|
||||
cfg = get_server_config()
|
||||
upload_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), cfg.get("upload_dir", "./uploads")))
|
||||
file_path = os.path.join(upload_dir, os.path.basename(asset.url))
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
db.delete(asset)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
Reference in New Issue
Block a user