feat:新增大屏存取发布接口
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
"""大屏存取、发布和管理页接口
|
||||
|
||||
登录用户只能操作自己的大屏;别人的大屏与不存在同样返回 404
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Query, Request, UploadFile
|
||||
|
||||
from app.common.response import ApiResponse, success_response
|
||||
from app.core.auth import get_current_user
|
||||
from app.core.exceptions import BizException
|
||||
from app.schemas.saved_screen import (
|
||||
ListStatus,
|
||||
PublishBrief,
|
||||
PublishHistory,
|
||||
PublishHistoryItem,
|
||||
ScreenCopied,
|
||||
ScreenCopyRequest,
|
||||
ScreenCreate,
|
||||
ScreenCreated,
|
||||
ScreenDetail,
|
||||
ScreenList,
|
||||
ScreenListItem,
|
||||
ScreenPublished,
|
||||
ScreenPublishRequest,
|
||||
ScreenRename,
|
||||
ScreenRestored,
|
||||
ScreenSave,
|
||||
ScreenSaved,
|
||||
ScreenSummary,
|
||||
SortField,
|
||||
ThumbnailResult,
|
||||
TrashEmptied,
|
||||
)
|
||||
from app.schemas.user import CurrentUser
|
||||
from app.services import thumbnails
|
||||
from app.services.screens import ScreenService
|
||||
|
||||
router = APIRouter(prefix="/screens", tags=["Screen"])
|
||||
|
||||
# 深翻页的 skip 会让 MongoDB 空转,管理页一屏 20 条,这个上限足够
|
||||
MAX_OFFSET = 10000
|
||||
THUMBNAIL_URL = "/api/public/thumbnails/{}"
|
||||
|
||||
User = Annotated[CurrentUser, Depends(get_current_user)]
|
||||
|
||||
|
||||
def get_screen_service(request: Request) -> ScreenService:
|
||||
"""没配 MongoDB 时大屏数据只会存在进程内存里,重启就丢,所以直接 503"""
|
||||
service = getattr(request.app.state, "screen_service", None)
|
||||
if service is None:
|
||||
raise BizException(503, "未配置 MongoDB,大屏存取不可用")
|
||||
return service
|
||||
|
||||
|
||||
Service = Annotated[ScreenService, Depends(get_screen_service)]
|
||||
|
||||
|
||||
def thumbnail_url(thumbnail_id: str | None) -> str | None:
|
||||
return None if thumbnail_id is None else THUMBNAIL_URL.format(thumbnail_id)
|
||||
|
||||
|
||||
def _list_item(summary: ScreenSummary, trash_days: int) -> ScreenListItem:
|
||||
publish = summary.publish
|
||||
return ScreenListItem(
|
||||
screen_id=summary.screen_id,
|
||||
name=summary.name,
|
||||
thumbnail_url=thumbnail_url(summary.thumbnail),
|
||||
canvas=summary.canvas,
|
||||
status="draft" if publish is None else publish.status,
|
||||
has_unpublished_changes=publish is not None
|
||||
and summary.revision > publish.revision,
|
||||
share_id=None if publish is None else publish.share_id,
|
||||
updated_at=summary.updated_at,
|
||||
published_at=None if publish is None else publish.published_at,
|
||||
deleted_at=summary.deleted_at,
|
||||
purge_at=None
|
||||
if summary.deleted_at is None
|
||||
else summary.deleted_at + timedelta(days=trash_days),
|
||||
)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_screen(
|
||||
body: ScreenCreate, user: User, service: Service
|
||||
) -> ApiResponse[ScreenCreated]:
|
||||
record = await service.create(user.id, body.name, body.page)
|
||||
return success_response(
|
||||
ScreenCreated(screen_id=record.screen_id, revision=record.revision)
|
||||
)
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_screens(
|
||||
user: User,
|
||||
service: Service,
|
||||
keyword: Annotated[str | None, Query(max_length=100)] = None,
|
||||
status: ListStatus | None = None,
|
||||
sort: SortField = "updated_at",
|
||||
order: Literal["asc", "desc"] = "desc",
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 20,
|
||||
offset: Annotated[int, Query(ge=0, le=MAX_OFFSET)] = 0,
|
||||
deleted: bool = False,
|
||||
) -> ApiResponse[ScreenList]:
|
||||
items, total = await service.list_screens(
|
||||
user.id,
|
||||
keyword=keyword.strip() or None if keyword else None,
|
||||
status=status,
|
||||
sort=sort,
|
||||
descending=order == "desc",
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
deleted=deleted,
|
||||
)
|
||||
return success_response(
|
||||
ScreenList(
|
||||
items=[_list_item(i, service.trash_days) for i in items], total=total
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/trash")
|
||||
async def empty_trash(user: User, service: Service) -> ApiResponse[TrashEmptied]:
|
||||
"""清空回收站:彻底删除,不可恢复。要先于 /{screen_id} 注册"""
|
||||
return success_response(TrashEmptied(count=await service.empty_trash(user.id)))
|
||||
|
||||
|
||||
@router.get("/{screen_id}")
|
||||
async def get_screen(
|
||||
screen_id: str, user: User, service: Service
|
||||
) -> ApiResponse[ScreenDetail]:
|
||||
record = await service.get(screen_id, user.id)
|
||||
publish = record.publish
|
||||
return success_response(
|
||||
ScreenDetail(
|
||||
screen_id=record.screen_id,
|
||||
name=record.name,
|
||||
page=record.page,
|
||||
revision=record.revision,
|
||||
updated_at=record.updated_at,
|
||||
publish=None
|
||||
if publish is None
|
||||
else PublishBrief.model_validate(publish.model_dump(exclude={"revision"})),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{screen_id}")
|
||||
async def save_screen(
|
||||
screen_id: str, body: ScreenSave, user: User, service: Service
|
||||
) -> ApiResponse[ScreenSaved]:
|
||||
saved = await service.save(screen_id, user.id, body.page, body.revision)
|
||||
return success_response(
|
||||
ScreenSaved(revision=saved.revision, updated_at=saved.updated_at)
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{screen_id}")
|
||||
async def rename_screen(
|
||||
screen_id: str, body: ScreenRename, user: User, service: Service
|
||||
) -> ApiResponse[dict]:
|
||||
await service.rename(screen_id, user.id, body.name)
|
||||
return success_response()
|
||||
|
||||
|
||||
@router.delete("/{screen_id}")
|
||||
async def delete_screen(
|
||||
screen_id: str, user: User, service: Service
|
||||
) -> ApiResponse[dict]:
|
||||
await service.delete(screen_id, user.id)
|
||||
return success_response()
|
||||
|
||||
|
||||
@router.post("/{screen_id}/restore")
|
||||
async def restore_deleted_screen(
|
||||
screen_id: str, user: User, service: Service
|
||||
) -> ApiResponse[dict]:
|
||||
await service.restore_deleted(screen_id, user.id)
|
||||
return success_response()
|
||||
|
||||
|
||||
@router.post("/{screen_id}/publish")
|
||||
async def publish_screen(
|
||||
screen_id: str, body: ScreenPublishRequest, user: User, service: Service
|
||||
) -> ApiResponse[ScreenPublished]:
|
||||
info = await service.publish(screen_id, user.id, body.revision)
|
||||
return success_response(
|
||||
ScreenPublished(
|
||||
version=info.version, share_id=info.share_id, published_at=info.published_at
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{screen_id}/unpublish")
|
||||
async def unpublish_screen(
|
||||
screen_id: str, user: User, service: Service
|
||||
) -> ApiResponse[dict]:
|
||||
await service.unpublish(screen_id, user.id)
|
||||
return success_response()
|
||||
|
||||
|
||||
@router.post("/{screen_id}/copy")
|
||||
async def copy_screen(
|
||||
screen_id: str,
|
||||
user: User,
|
||||
service: Service,
|
||||
body: ScreenCopyRequest | None = None,
|
||||
) -> ApiResponse[ScreenCopied]:
|
||||
new_id = await service.copy_screen(screen_id, user.id, body.name if body else None)
|
||||
return success_response(ScreenCopied(screen_id=new_id))
|
||||
|
||||
|
||||
@router.put("/{screen_id}/thumbnail")
|
||||
async def upload_thumbnail(
|
||||
screen_id: str,
|
||||
file: Annotated[UploadFile, File()],
|
||||
user: User,
|
||||
service: Service,
|
||||
) -> ApiResponse[ThumbnailResult]:
|
||||
data = await file.read(thumbnails.MAX_BYTES + 1)
|
||||
thumbnail_id = await service.set_thumbnail(screen_id, user.id, data)
|
||||
return success_response(ThumbnailResult(thumbnail_url=thumbnail_url(thumbnail_id)))
|
||||
|
||||
|
||||
@router.get("/{screen_id}/publishes")
|
||||
async def list_publishes(
|
||||
screen_id: str,
|
||||
user: User,
|
||||
service: Service,
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 20,
|
||||
offset: Annotated[int, Query(ge=0, le=MAX_OFFSET)] = 0,
|
||||
) -> ApiResponse[PublishHistory]:
|
||||
entries, total = await service.list_publishes(screen_id, user.id, limit, offset)
|
||||
return success_response(
|
||||
PublishHistory(
|
||||
items=[PublishHistoryItem.model_validate(e.model_dump()) for e in entries],
|
||||
total=total,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{screen_id}/publishes/{version}/restore")
|
||||
async def restore_publish(
|
||||
screen_id: str, version: int, user: User, service: Service
|
||||
) -> ApiResponse[ScreenRestored]:
|
||||
revision = await service.restore_publish(screen_id, user.id, version)
|
||||
return success_response(ScreenRestored(revision=revision))
|
||||
Reference in New Issue
Block a user