fix(P1): 素材列表status默认过滤 + page/page_size分页支持 #532

Merged
auto-approve-bot merged 2 commits from fix/p1-asset-list-status-and-pagination into develop 2026-07-18 18:47:38 +08:00
4 changed files with 106 additions and 52 deletions
+40 -14
View File
@@ -95,6 +95,12 @@ def list_assets(
None,
description="按内容分类筛选:scenic=风景、product=产品、person=人物、animal=动物、food=美食、tech=科技、sport=运动、music=音乐、other=其他",
),
status: Optional[str] = Query(
"ready",
description="按状态筛选,逗号分隔多值;默认仅返回 ready;传 all 返回所有状态(含 deleted",
),
page: Optional[int] = Query(None, ge=1, description="页码,从1开始;与 page_size 配对使用,优先于 skip/limit"),
page_size: Optional[int] = Query(None, ge=1, le=500, description="每页数量;与 page 配对使用"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -104,6 +110,22 @@ def list_assets(
) -> ListAssetsResponse:
user_id = authenticated_user.user.id
# ── 分页:page/page_size 优先于 skip/limit
if page is not None and page_size is not None:
skip = (page - 1) * page_size
limit = page_size
# ── 解析 status 过滤
status_list: list[str] | None
if status and status.lower() == "all":
status_list = None # None = 不过滤,返回所有状态
elif status:
status_list = [s.strip() for s in status.split(",") if s.strip()]
if not status_list:
status_list = ["ready"]
else:
status_list = ["ready"]
# kind → file_type 映射(voice 对应 audio
kind_to_file_type = {"video": "video", "voice": "audio", "image": "image"}
@@ -172,11 +194,13 @@ def list_assets(
raise HTTPException(status_code=404, detail=f"AssetLibrary {library_id} not found")
check_project_access(library.project_id, user_id, project_repository)
if ft:
items = asset_repository.find_by_library_and_file_type(library_id, ft, skip=skip, limit=limit)
total = asset_repository.count_by_project(library.project_id) if not kind else len(items)
items = asset_repository.find_by_library_and_file_type(
library_id, ft, skip=skip, limit=limit, status=status_list
)
total = len(items)
else:
items = asset_repository.find_by_library(library_id, skip=skip, limit=limit)
total = asset_repository.count_by_project(library.project_id)
items = asset_repository.find_by_library(library_id, skip=skip, limit=limit, status=status_list)
total = asset_repository.count_by_project(library.project_id, status=status_list)
return ListAssetsResponse(
items=[_to_asset_response(item) for item in items],
total=total,
@@ -189,13 +213,13 @@ def list_assets(
check_project_access(project_id, user_id, project_repository)
if ft:
# 无直接方法,加载后按 file_type 过滤(仍比全量加载好)
all_items = asset_repository.find_by_project(project_id)
all_items = asset_repository.find_by_project(project_id, status=status_list)
items = [i for i in all_items if i.mime_type and i.mime_type.startswith(ft)]
total = len(items)
paged = items[skip : skip + limit]
else:
items = asset_repository.find_by_project(project_id, skip=skip, limit=limit)
total = asset_repository.count_by_project(project_id)
items = asset_repository.find_by_project(project_id, skip=skip, limit=limit, status=status_list)
total = asset_repository.count_by_project(project_id, status=status_list)
paged = items
return ListAssetsResponse(
items=[_to_asset_response(item) for item in paged],
@@ -215,17 +239,17 @@ def list_assets(
if not project_ids:
return ListAssetsResponse(items=[], total=0, skip=skip, limit=limit)
total = asset_repository.count_by_project_ids(project_ids)
total = asset_repository.count_by_project_ids(project_ids, status=status_list)
# 跨项目分页:逐项目累积直到凑够一页
paged_items: list = []
offset = skip
remaining = limit
for pid in project_ids:
proj_total = asset_repository.count_by_project(pid)
proj_total = asset_repository.count_by_project(pid, status=status_list)
if offset >= proj_total:
offset -= proj_total
continue
proj_items = asset_repository.find_by_project(pid, skip=offset, limit=remaining)
proj_items = asset_repository.find_by_project(pid, skip=offset, limit=remaining, status=status_list)
paged_items.extend(proj_items)
remaining -= len(proj_items)
offset = 0
@@ -246,12 +270,14 @@ def list_assets(
raise HTTPException(status_code=404, detail=f"AssetLibrary {library_id} not found")
check_project_access(library.project_id, user_id, project_repository)
if kind:
all_items = asset_repository.find_by_library_and_file_type(library_id, kind_to_file_type[kind])
all_items = asset_repository.find_by_library_and_file_type(
library_id, kind_to_file_type[kind], status=status_list
)
else:
all_items = asset_repository.find_by_library(library_id)
all_items = asset_repository.find_by_library(library_id, status=status_list)
elif project_id:
check_project_access(project_id, user_id, project_repository)
all_items = asset_repository.find_by_project(project_id)
all_items = asset_repository.find_by_project(project_id, status=status_list)
else:
try:
projects = project_repository.find_accessible_projects(user_id)
@@ -260,7 +286,7 @@ def list_assets(
return ListAssetsResponse(items=[], total=0, skip=skip, limit=limit)
all_items = []
for proj in projects:
all_items.extend(asset_repository.find_by_project(proj.id))
all_items.extend(asset_repository.find_by_project(proj.id, status=status_list))
# 应用 kind 过滤(如果有)+ keyword/gender/style
if kind:
@@ -16,15 +16,12 @@ class SQLAlchemyAssetRepository:
library_id: str,
skip: int = 0,
limit: int = 100,
status: list[str] | None = None,
) -> list[Asset]:
models = (
self.session.query(AssetModel)
.filter(AssetModel.asset_library_id == library_id)
.order_by(AssetModel.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
query = self.session.query(AssetModel).filter(AssetModel.asset_library_id == library_id)
if status:
query = query.filter(AssetModel.status.in_(status))
models = query.order_by(AssetModel.created_at.desc()).offset(skip).limit(limit).all()
return [self._to_domain(model) for model in models]
def find_by_project(
@@ -32,15 +29,12 @@ class SQLAlchemyAssetRepository:
project_id: str,
skip: int = 0,
limit: int = 100,
status: list[str] | None = None,
) -> list[Asset]:
models = (
self.session.query(AssetModel)
.filter(AssetModel.project_id == project_id)
.order_by(AssetModel.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
query = self.session.query(AssetModel).filter(AssetModel.project_id == project_id)
if status:
query = query.filter(AssetModel.status.in_(status))
models = query.order_by(AssetModel.created_at.desc()).offset(skip).limit(limit).all()
return [self._to_domain(model) for model in models]
def find_by_library_and_file_type(
@@ -49,15 +43,14 @@ class SQLAlchemyAssetRepository:
file_type: str,
skip: int = 0,
limit: int = 100,
status: list[str] | None = None,
) -> list[Asset]:
models = (
self.session.query(AssetModel)
.filter(AssetModel.asset_library_id == library_id, AssetModel.file_type == file_type)
.order_by(AssetModel.created_at.desc())
.offset(skip)
.limit(limit)
.all()
query = self.session.query(AssetModel).filter(
AssetModel.asset_library_id == library_id, AssetModel.file_type == file_type
)
if status:
query = query.filter(AssetModel.status.in_(status))
models = query.order_by(AssetModel.created_at.desc()).offset(skip).limit(limit).all()
return [self._to_domain(model) for model in models]
def find_by_id(self, asset_id: str) -> Asset | None:
@@ -223,13 +216,19 @@ class SQLAlchemyAssetRepository:
self.session.commit()
return count
def count_by_project(self, project_id: str) -> int:
return self.session.query(AssetModel).filter(AssetModel.project_id == project_id).count()
def count_by_project(self, project_id: str, status: list[str] | None = None) -> int:
query = self.session.query(AssetModel).filter(AssetModel.project_id == project_id)
if status:
query = query.filter(AssetModel.status.in_(status))
return query.count()
def count_by_project_ids(self, project_ids: list[str]) -> int:
def count_by_project_ids(self, project_ids: list[str], status: list[str] | None = None) -> int:
if not project_ids:
return 0
return self.session.query(AssetModel).filter(AssetModel.project_id.in_(project_ids)).count()
query = self.session.query(AssetModel).filter(AssetModel.project_id.in_(project_ids))
if status:
query = query.filter(AssetModel.status.in_(status))
return query.count()
def sum_storage_by_project_ids(self, project_ids: list[str]) -> int:
if not project_ids:
+5 -2
View File
@@ -20,6 +20,7 @@ class AssetRepository(ABC):
project_id: str,
skip: int = 0,
limit: int = 100,
status: list[str] | None = None,
) -> list[Asset]:
pass
@@ -29,6 +30,7 @@ class AssetRepository(ABC):
library_id: str,
skip: int = 0,
limit: int = 100,
status: list[str] | None = None,
) -> list[Asset]:
pass
@@ -39,6 +41,7 @@ class AssetRepository(ABC):
file_type: str,
skip: int = 0,
limit: int = 100,
status: list[str] | None = None,
) -> list[Asset]:
pass
@@ -71,11 +74,11 @@ class AssetRepository(ABC):
pass
@abstractmethod
def count_by_project(self, project_id: str) -> int:
def count_by_project(self, project_id: str, status: list[str] | None = None) -> int:
pass
@abstractmethod
def count_by_project_ids(self, project_ids: list[str]) -> int:
def count_by_project_ids(self, project_ids: list[str], status: list[str] | None = None) -> int:
pass
@abstractmethod
+35 -9
View File
@@ -103,19 +103,35 @@ class StubAssetRepository:
def find_by_id(self, asset_id: str) -> Asset | None:
return self._assets.get(asset_id)
def find_by_library(self, library_id: str, skip: int = 0, limit: int = 100) -> list[Asset]:
def find_by_library(
self, library_id: str, skip: int = 0, limit: int = 100, status: list[str] | None = None
) -> list[Asset]:
items = [a for a in self._assets.values() if a.library_id == library_id]
if status:
items = [a for a in items if a.status.value in status]
items.sort(key=lambda a: a.created_at, reverse=True)
return items[skip : skip + limit]
def find_by_library_and_file_type(self, library_id: str, file_type: str) -> list[Asset]:
return [
def find_by_library_and_file_type(
self, library_id: str, file_type: str, skip: int = 0, limit: int = 100, status: list[str] | None = None
) -> list[Asset]:
items = [
a
for a in self._assets.values()
if a.library_id == library_id and a.mime_type and a.mime_type.startswith(file_type)
]
if status:
items = [a for a in items if a.status.value in status]
items.sort(key=lambda a: a.created_at, reverse=True)
return items[skip : skip + limit]
def find_by_project(self, project_id: str, skip: int = 0, limit: int = 100) -> list[Asset]:
def find_by_project(
self, project_id: str, skip: int = 0, limit: int = 100, status: list[str] | None = None
) -> list[Asset]:
items = [a for a in self._assets.values() if a.project_id == project_id]
if status:
items = [a for a in items if a.status.value in status]
items.sort(key=lambda a: a.created_at, reverse=True)
return items[skip : skip + limit]
def update(self, asset: Asset) -> Asset:
@@ -184,11 +200,17 @@ class StubAssetRepository:
count += 1
return count
def count_by_project(self, project_id: str) -> int:
return len([a for a in self._assets.values() if a.project_id == project_id])
def count_by_project(self, project_id: str, status: list[str] | None = None) -> int:
items = [a for a in self._assets.values() if a.project_id == project_id]
if status:
items = [a for a in items if a.status.value in status]
return len(items)
def count_by_project_ids(self, project_ids: list[str]) -> int:
return len([a for a in self._assets.values() if a.project_id in project_ids])
def count_by_project_ids(self, project_ids: list[str], status: list[str] | None = None) -> int:
items = [a for a in self._assets.values() if a.project_id in project_ids]
if status:
items = [a for a in items if a.status.value in status]
return len(items)
def find_by_library_and_file_hash(self, library_id: str, file_hash: str) -> Asset | None:
if not file_hash:
@@ -395,7 +417,7 @@ class TestListAssets:
"""获取素材列表端点测试。"""
def _create_test_assets(self, client, count: int = 3):
"""辅助方法:创建测试素材。"""
"""辅助方法:创建测试素材status=ready"""
for i in range(count):
client.post(
"/api/v1/assets",
@@ -406,6 +428,7 @@ class TestListAssets:
"storage_key": f"uploads/video-{i}.mp4",
"mime_type": "video/mp4",
"file_size": 1024 * (i + 1),
"status": "ready",
},
)
@@ -457,6 +480,7 @@ class TestListAssets:
"name": "hello-world.mp4",
"storage_key": "uploads/hello.mp4",
"mime_type": "video/mp4",
"status": "ready",
},
)
client.post(
@@ -467,6 +491,7 @@ class TestListAssets:
"name": "goodbye.mp4",
"storage_key": "uploads/goodbye.mp4",
"mime_type": "video/mp4",
"status": "ready",
},
)
@@ -768,6 +793,7 @@ class TestAssetsCRUDFlow:
"mime_type": "video/mp4",
"file_size": 8192,
"metadata": {"source": "test"},
"status": "ready",
},
)
assert create_resp.status_code == 200