From 97cf565946d5fcbef82d9b22eea9b7523bb6b032 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 19 Jul 2026 07:28:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(P1):=20=E7=B4=A0=E6=9D=90=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=94=AF=E6=8C=81=E6=8C=89=E7=B1=BB=E5=9E=8B=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=20-=20=E8=A7=86=E9=A2=91=E5=BA=93/=E9=85=8D=E9=9F=B3?= =?UTF-8?q?=E5=BA=93=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增/修复: - Asset domain 增加 file_type property(从 mime_type 推导) - AssetRepository 新增 find_by_project_and_file_type / count_by_project_and_file_type / count_by_library_and_file_type - 素材列表接口 kind 过滤全路径覆盖(DB层分页更高效): - library_id模式:total改用count查询(之前用len(items)不准) - project_id模式:改用DB层file_type查询(之前内存过滤性能差) - 跨项目模式:新增kind过滤(之前完全没有) - 内存过滤路径:project_id和跨项目增加DB层预过滤+改用file_type判断 - 向后兼容:kind参数已存在,只是之前不完整 --- apps/api/app/api/routes/assets.py | 84 +++++++++++++------ .../sqlalchemy_impl/asset_repository.py | 42 ++++++++++ packages/domain/entities.py | 7 ++ 3 files changed, 108 insertions(+), 25 deletions(-) diff --git a/apps/api/app/api/routes/assets.py b/apps/api/app/api/routes/assets.py index 73e029448..fc671d992 100755 --- a/apps/api/app/api/routes/assets.py +++ b/apps/api/app/api/routes/assets.py @@ -201,7 +201,7 @@ def list_assets( items = asset_repository.find_by_library_and_file_type( library_id, ft, skip=skip, limit=limit, status=status_list ) - total = len(items) + total = asset_repository.count_by_library_and_file_type(library_id, ft, status=status_list) else: 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) @@ -216,11 +216,11 @@ def list_assets( if project_id: check_project_access(project_id, user_id, project_repository) if ft: - # 无直接方法,加载后按 file_type 过滤(仍比全量加载好) - 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] + items = asset_repository.find_by_project_and_file_type( + project_id, ft, skip=skip, limit=limit, status=status_list + ) + total = asset_repository.count_by_project_and_file_type(project_id, ft, status=status_list) + paged = items else: 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) @@ -243,22 +243,43 @@ 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, status=status_list) - # 跨项目分页:逐项目累积直到凑够一页 - paged_items: list = [] - offset = skip - remaining = limit - for pid in project_ids: - 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, status=status_list) - paged_items.extend(proj_items) - remaining -= len(proj_items) - offset = 0 - if remaining <= 0: - break + if ft: + # 有 kind 过滤:逐项目查 file_type,凑够一页 + total = 0 + paged_items: list = [] + offset = skip + remaining = limit + for pid in project_ids: + proj_total = asset_repository.count_by_project_and_file_type(pid, ft, status=status_list) + total += proj_total + if offset >= proj_total: + offset -= proj_total + continue + proj_items = asset_repository.find_by_project_and_file_type( + pid, ft, skip=offset, limit=remaining, status=status_list + ) + paged_items.extend(proj_items) + remaining -= len(proj_items) + offset = 0 + if remaining <= 0: + break + else: + 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, status=status_list) + if offset >= proj_total: + offset -= proj_total + continue + 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 + if remaining <= 0: + break return ListAssetsResponse( items=[_to_asset_response(item) for item in paged_items], @@ -281,7 +302,14 @@ def list_assets( 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, status=status_list) + if kind: + ft = kind_to_file_type.get(kind) + if ft: + all_items = asset_repository.find_by_project_and_file_type(project_id, ft, status=status_list) + else: + all_items = asset_repository.find_by_project(project_id, status=status_list) + else: + all_items = asset_repository.find_by_project(project_id, status=status_list) else: try: projects = project_repository.find_accessible_projects(user_id) @@ -290,12 +318,18 @@ 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, status=status_list)) + if kind and kind_to_file_type.get(kind): + all_items.extend( + asset_repository.find_by_project_and_file_type(proj.id, kind_to_file_type[kind], status=status_list) + ) + else: + all_items.extend(asset_repository.find_by_project(proj.id, status=status_list)) # 应用 kind 过滤(如果有)+ keyword/gender/style if kind: ft = kind_to_file_type.get(kind) - all_items = [i for i in all_items if i.mime_type and i.mime_type.startswith(ft or "")] + if ft: + all_items = [i for i in all_items if i.file_type == ft] filtered = _apply_memory_filters(all_items) total = len(filtered) paged = filtered[skip : skip + limit] diff --git a/packages/adapters/sqlalchemy_impl/asset_repository.py b/packages/adapters/sqlalchemy_impl/asset_repository.py index 5be36cf33..a5abe8bed 100755 --- a/packages/adapters/sqlalchemy_impl/asset_repository.py +++ b/packages/adapters/sqlalchemy_impl/asset_repository.py @@ -53,6 +53,48 @@ class SQLAlchemyAssetRepository: models = query.order_by(AssetModel.created_at.desc()).offset(skip).limit(limit).all() return [self._to_domain(model) for model in models] + def count_by_library_and_file_type( + self, + library_id: str, + file_type: str, + status: list[str] | None = None, + ) -> int: + 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)) + return query.count() + + def find_by_project_and_file_type( + self, + project_id: str, + file_type: str, + skip: int = 0, + limit: int = 100, + status: list[str] | None = None, + ) -> list[Asset]: + query = self.session.query(AssetModel).filter( + AssetModel.project_id == project_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 count_by_project_and_file_type( + self, + project_id: str, + file_type: str, + status: list[str] | None = None, + ) -> int: + query = self.session.query(AssetModel).filter( + AssetModel.project_id == project_id, AssetModel.file_type == file_type + ) + if status: + query = query.filter(AssetModel.status.in_(status)) + return query.count() + def find_by_id(self, asset_id: str) -> Asset | None: model = self.session.query(AssetModel).filter(AssetModel.id == asset_id).first() if model is None: diff --git a/packages/domain/entities.py b/packages/domain/entities.py index 0280e36e2..027e4a854 100755 --- a/packages/domain/entities.py +++ b/packages/domain/entities.py @@ -204,6 +204,13 @@ class Asset: created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + @property + def file_type(self) -> str: + """文件类型(从 mime_type 推导,如 video/audio/image).""" + if "/" in self.mime_type: + return self.mime_type.split("/")[0] + return self.mime_type + @classmethod def create( cls, -- 2.54.0