diff --git a/apps/api/app/api/routes/assets.py b/apps/api/app/api/routes/assets.py old mode 100644 new mode 100755 index 7d90cf9c7..262637836 --- a/apps/api/app/api/routes/assets.py +++ b/apps/api/app/api/routes/assets.py @@ -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: diff --git a/packages/adapters/sqlalchemy_impl/asset_repository.py b/packages/adapters/sqlalchemy_impl/asset_repository.py index 06830d0dc..5be36cf33 100755 --- a/packages/adapters/sqlalchemy_impl/asset_repository.py +++ b/packages/adapters/sqlalchemy_impl/asset_repository.py @@ -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: diff --git a/packages/ports/asset_repository.py b/packages/ports/asset_repository.py index e3b2cdbb8..5122968d9 100755 --- a/packages/ports/asset_repository.py +++ b/packages/ports/asset_repository.py @@ -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 diff --git a/tests/integration/test_assets_api.py b/tests/integration/test_assets_api.py index d15ef7df1..8fe4ff33f 100755 --- a/tests/integration/test_assets_api.py +++ b/tests/integration/test_assets_api.py @@ -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