diff --git a/core/backend/apps/assets/views.py b/core/backend/apps/assets/views.py index e72841d..fa087ed 100644 --- a/core/backend/apps/assets/views.py +++ b/core/backend/apps/assets/views.py @@ -3,6 +3,7 @@ import uuid import requests from django.db import transaction +from django.db.models import Q from django.http import StreamingHttpResponse from rest_framework import status from rest_framework.decorators import action @@ -27,6 +28,26 @@ class AssetPagination(PageNumberPagination): max_page_size = 200 +# 资产库 tab → 查询条件。与前端 assetTab(library.tsx)完全一致,供服务端按页过滤/计数。 +_KNOWN_CATS = ["person", "scene", "product_image", "final_video", "upload"] + + +def _tab_q(tab: str) -> Q: + if tab == "people": + return Q(category="person") + if tab == "scenes": + return Q(category="scene") + if tab == "products": + return Q(category="product_image") + if tab == "uploads": + return Q(category="upload") + if tab == "finals": # final_video,或「未归类但是视频」 + return Q(category="final_video") | (~Q(category__in=_KNOWN_CATS) & Q(asset_type="video")) + if tab == "unclassified": # 未归类且非视频 + return ~Q(category__in=_KNOWN_CATS) & ~Q(asset_type="video") + return Q() + + class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet): # select_related 回溯链:asset → origin_task → project,供序列化器解析资产归属商品(避免 N+1)。 # ★ defer 掉 AITask 的两个巨型 JSON 列(request/response payload):列表序列化只需 project.product_id, @@ -43,6 +64,53 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet): search_fields = ["name", "description"] ordering_fields = ["created_at", "updated_at", "name"] + def get_queryset(self): + """服务端过滤,支持各页按需懒加载(不再前端取全量后客户端切片)。 + 参数:tab(资产库分桶)/category/asset_type/source/product/q(搜索)/m_(metadata 过滤)/ordering。 + 默认按 -created_at 排序——无 ORDER BY 时分页会重/漏。""" + qs = super().get_queryset() + p = self.request.query_params + if p.get("tab"): + qs = qs.filter(_tab_q(p["tab"])) + if p.get("category"): + qs = qs.filter(category=p["category"]) + if p.get("asset_type"): + qs = qs.filter(asset_type=p["asset_type"]) + if p.get("source"): + qs = qs.filter(source=p["source"]) + if p.get("product"): + # 资产归属商品:独立生图写在 metadata.product_id;项目内生成回溯 origin_task→project→product + qs = qs.filter(Q(metadata__product_id=p["product"]) | Q(origin_task__project__product_id=p["product"])) + if p.get("q"): + qs = qs.filter(Q(name__icontains=p["q"]) | Q(description__icontains=p["q"])) + for key, val in p.items(): + if key.startswith("m_") and val: + qs = qs.filter(**{f"metadata__{key[2:]}": val}) + ordering = p.get("ordering") or "-created_at" + return qs.order_by(ordering) + + @action(detail=False, methods=["get"]) + def summary(self, request): + """资产库 tab 计数(人物/场景/商品图/成片/我的上传/未分类),供 tab 徽标——不必取全量。""" + base = Asset.objects.filter(team=self.get_team()) + tabs = ["people", "scenes", "products", "finals", "uploads", "unclassified"] + return Response({t: base.filter(_tab_q(t)).count() for t in tabs}) + + @action(detail=False, methods=["get"]) + def facets(self, request): + """某 tab 下真实存在的筛选项(来源/类型/指定 metadata 键的取值),供下拉「只列真有的」。 + 参数:tab、meta_keys=gender,age,role,...(逗号分隔)。""" + base = Asset.objects.filter(team=self.get_team()) + if request.query_params.get("tab"): + base = base.filter(_tab_q(request.query_params["tab"])) + sources = sorted(s for s in base.values_list("source", flat=True).distinct() if s) + kinds = sorted(k for k in base.values_list("asset_type", flat=True).distinct() if k) + meta = {} + for key in (k for k in request.query_params.get("meta_keys", "").split(",") if k): + vals = base.values_list(f"metadata__{key}", flat=True) + meta[key] = sorted({str(v) for v in vals if v not in (None, "")}) + return Response({"sources": sources, "kinds": kinds, "metadata": meta}) + @action(detail=True, methods=["get"], url_path="raw") def raw(self, request, pk=None): """同源流式代理资产主文件。TOS 桶未配 CORS,浏览器 JS 读不到跨域媒体数据—— diff --git a/core/backend/apps/products/views.py b/core/backend/apps/products/views.py index 9878752..65eaf4b 100644 --- a/core/backend/apps/products/views.py +++ b/core/backend/apps/products/views.py @@ -17,7 +17,12 @@ from .serializers import ProductSerializer class ProductViewSet(TeamScopedViewSetMixin, ModelViewSet): - queryset = Product.objects.prefetch_related("images", "selling_points").all() + # 预取 图片资产文件 + 封面资产文件:序列化器内嵌 preview_url 时不再 N+1(也让前端无需全量 assets 反查) + queryset = ( + Product.objects.select_related("cover_asset") + .prefetch_related("images__asset__files", "cover_asset__files", "selling_points") + .all() + ) serializer_class = ProductSerializer search_fields = ["title", "brand", "category"] ordering_fields = ["created_at", "updated_at", "title"]