fix(P1): 素材列表status默认过滤 + page/page_size分页支持 #532
Regular → Executable
+40
-14
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user