feat:新增大屏存取发布接口
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
"""大屏存储:协议和进程内存实现;MongoDB 实现见 screen_mongo
|
||||
|
||||
内存实现只给测试用——大屏数据不能丢,没配 MongoDB 时 screens 接口直接返回 503
|
||||
所有查询都带 owner_id:别人的大屏与不存在同样返回 None
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Protocol
|
||||
|
||||
from app.schemas.saved_screen import (
|
||||
ListStatus,
|
||||
PublishEntry,
|
||||
PublishSnapshot,
|
||||
ScreenRecord,
|
||||
ScreenSummary,
|
||||
SortField,
|
||||
)
|
||||
|
||||
|
||||
def canvas_size(canvas: Any) -> dict[str, Any] | None:
|
||||
"""列表卡片只需要画布宽高"""
|
||||
if not isinstance(canvas, dict):
|
||||
return None
|
||||
return {key: canvas[key] for key in ("width", "height") if key in canvas}
|
||||
|
||||
|
||||
def summarize(record: ScreenRecord) -> ScreenSummary:
|
||||
"""去掉页面,只保留画布宽高"""
|
||||
data = record.model_dump(exclude={"page"})
|
||||
data["canvas"] = canvas_size(record.page.get("canvas"))
|
||||
return ScreenSummary.model_validate(data)
|
||||
|
||||
|
||||
class ScreenStore(Protocol):
|
||||
async def insert(self, record: ScreenRecord) -> None: ...
|
||||
|
||||
async def get(
|
||||
self, screen_id: str, owner_id: int, *, deleted: bool = False
|
||||
) -> ScreenRecord | None:
|
||||
"""带页面的记录;deleted 为 True 时只找回收站里的"""
|
||||
...
|
||||
|
||||
async def get_summary(
|
||||
self, screen_id: str, owner_id: int, *, deleted: bool = False
|
||||
) -> ScreenSummary | None: ...
|
||||
|
||||
async def update(
|
||||
self,
|
||||
screen_id: str,
|
||||
owner_id: int,
|
||||
changes: dict[str, Any],
|
||||
*,
|
||||
revision: int | None = None,
|
||||
where: dict[str, Any] | None = None,
|
||||
deleted: bool = False,
|
||||
) -> ScreenSummary | None:
|
||||
"""一步完成"匹配并更新";不匹配返回 None
|
||||
|
||||
给了 revision 就要求库里的 revision 与之相等;where 是额外的等值条件,键可以带点,
|
||||
如 {"publish.version": 3}、{"publish": None}——更新是基于先前读到的值做的,值变了就不能盖上去
|
||||
"""
|
||||
...
|
||||
|
||||
async def search(
|
||||
self,
|
||||
owner_id: int,
|
||||
*,
|
||||
keyword: str | None,
|
||||
status: ListStatus | None,
|
||||
sort: SortField,
|
||||
descending: bool,
|
||||
limit: int,
|
||||
offset: int,
|
||||
deleted: bool,
|
||||
) -> tuple[list[ScreenSummary], int]: ...
|
||||
|
||||
async def get_by_share_id(self, share_id: str) -> ScreenSummary | None:
|
||||
"""未删除的大屏;是否已下线由调用方看 publish.status"""
|
||||
...
|
||||
|
||||
async def list_expired_trash(
|
||||
self, deleted_before: datetime, limit: int
|
||||
) -> list[ScreenSummary]:
|
||||
"""所有人的回收站里 deleted_at 早于 deleted_before 的大屏"""
|
||||
...
|
||||
|
||||
async def purge(self, screen_id: str, owner_id: int) -> bool:
|
||||
"""彻底删除回收站里的大屏;已经被还原(不在回收站)或不存在时返回 False"""
|
||||
...
|
||||
|
||||
async def delete_publishes(self, screen_id: str) -> None: ...
|
||||
|
||||
async def last_publish_version(self, screen_id: str) -> int: ...
|
||||
|
||||
async def insert_publish(self, snapshot: PublishSnapshot) -> bool:
|
||||
"""同一大屏同一版本号已存在时返回 False"""
|
||||
...
|
||||
|
||||
async def get_publish(
|
||||
self, screen_id: str, version: int
|
||||
) -> PublishSnapshot | None: ...
|
||||
|
||||
async def list_publishes(
|
||||
self, screen_id: str, limit: int, offset: int
|
||||
) -> tuple[list[PublishEntry], int]:
|
||||
"""版本号倒序"""
|
||||
...
|
||||
|
||||
async def delete_publish(self, screen_id: str, version: int) -> None: ...
|
||||
|
||||
|
||||
class InMemoryScreenStore:
|
||||
def __init__(self) -> None:
|
||||
self._screens: dict[str, ScreenRecord] = {}
|
||||
self._publishes: dict[tuple[str, int], PublishSnapshot] = {}
|
||||
|
||||
def _find(
|
||||
self, screen_id: str, owner_id: int, deleted: bool
|
||||
) -> ScreenRecord | None:
|
||||
record = self._screens.get(screen_id)
|
||||
if record is None or record.owner_id != owner_id:
|
||||
return None
|
||||
return record if (record.deleted_at is not None) == deleted else None
|
||||
|
||||
async def insert(self, record: ScreenRecord) -> None:
|
||||
self._screens[record.screen_id] = record.model_copy(deep=True)
|
||||
|
||||
async def get(self, screen_id, owner_id, *, deleted=False):
|
||||
record = self._find(screen_id, owner_id, deleted)
|
||||
return None if record is None else record.model_copy(deep=True)
|
||||
|
||||
async def get_summary(self, screen_id, owner_id, *, deleted=False):
|
||||
record = self._find(screen_id, owner_id, deleted)
|
||||
return None if record is None else summarize(record)
|
||||
|
||||
async def update(
|
||||
self, screen_id, owner_id, changes, *, revision=None, where=None, deleted=False
|
||||
):
|
||||
record = self._find(screen_id, owner_id, deleted)
|
||||
if record is None or (revision is not None and record.revision != revision):
|
||||
return None
|
||||
if where is not None and not _matches(record, where):
|
||||
return None
|
||||
updated = record.model_copy(update=changes, deep=True)
|
||||
self._screens[screen_id] = updated
|
||||
return summarize(updated)
|
||||
|
||||
async def search(
|
||||
self, owner_id, *, keyword, status, sort, descending, limit, offset, deleted
|
||||
):
|
||||
records = [
|
||||
record
|
||||
for record in self._screens.values()
|
||||
if record.owner_id == owner_id
|
||||
and (record.deleted_at is not None) == deleted
|
||||
and (keyword is None or keyword.lower() in record.name.lower())
|
||||
and _status_matches(record, status)
|
||||
]
|
||||
# 排序字段相同时按 ID 兜底,翻页才稳定
|
||||
records.sort(key=lambda r: (getattr(r, sort), r.screen_id), reverse=descending)
|
||||
return [summarize(r) for r in records[offset : offset + limit]], len(records)
|
||||
|
||||
async def get_by_share_id(self, share_id):
|
||||
for record in self._screens.values():
|
||||
if (
|
||||
record.deleted_at is None
|
||||
and record.publish is not None
|
||||
and record.publish.share_id == share_id
|
||||
):
|
||||
return summarize(record)
|
||||
return None
|
||||
|
||||
async def list_expired_trash(self, deleted_before, limit):
|
||||
expired = sorted(
|
||||
(
|
||||
r
|
||||
for r in self._screens.values()
|
||||
if r.deleted_at is not None and r.deleted_at < deleted_before
|
||||
),
|
||||
key=lambda r: r.deleted_at,
|
||||
)
|
||||
return [summarize(r) for r in expired[:limit]]
|
||||
|
||||
async def purge(self, screen_id, owner_id):
|
||||
if self._find(screen_id, owner_id, True) is None:
|
||||
return False
|
||||
del self._screens[screen_id]
|
||||
return True
|
||||
|
||||
async def delete_publishes(self, screen_id):
|
||||
for key in [key for key in self._publishes if key[0] == screen_id]:
|
||||
del self._publishes[key]
|
||||
|
||||
async def last_publish_version(self, screen_id):
|
||||
return max(
|
||||
(version for sid, version in self._publishes if sid == screen_id), default=0
|
||||
)
|
||||
|
||||
async def insert_publish(self, snapshot):
|
||||
key = (snapshot.screen_id, snapshot.version)
|
||||
if key in self._publishes:
|
||||
return False
|
||||
self._publishes[key] = snapshot.model_copy(deep=True)
|
||||
return True
|
||||
|
||||
async def get_publish(self, screen_id, version):
|
||||
snapshot = self._publishes.get((screen_id, version))
|
||||
return None if snapshot is None else snapshot.model_copy(deep=True)
|
||||
|
||||
async def list_publishes(self, screen_id, limit, offset):
|
||||
snapshots = sorted(
|
||||
(s for (sid, _), s in self._publishes.items() if sid == screen_id),
|
||||
key=lambda s: s.version,
|
||||
reverse=True,
|
||||
)
|
||||
entries = [
|
||||
PublishEntry.model_validate(s.model_dump(exclude={"page"}))
|
||||
for s in snapshots[offset : offset + limit]
|
||||
]
|
||||
return entries, len(snapshots)
|
||||
|
||||
async def delete_publish(self, screen_id, version):
|
||||
self._publishes.pop((screen_id, version), None)
|
||||
|
||||
|
||||
def _matches(record: ScreenRecord, where: dict[str, Any]) -> bool:
|
||||
for path, expected in where.items():
|
||||
value: Any = record
|
||||
for part in path.split("."):
|
||||
value = None if value is None else getattr(value, part, None)
|
||||
if value != expected:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _status_matches(record: ScreenSummary, status: ListStatus | None) -> bool:
|
||||
if status is None:
|
||||
return True
|
||||
if status == "draft":
|
||||
return record.publish is None
|
||||
return record.publish is not None and record.publish.status == status
|
||||
Reference in New Issue
Block a user