fix: YYX测试清单16条收口——资产库弹窗层级/批次多选删/商品筛选/模特卡单卡化/垃圾桶清空等
- 资产库:批次弹窗降层(under-confirm 990<999)修单图删除确认弹窗被压;编辑态批次卡多选批删;facets 新增 products 关联商品筛选(前后端,+2 测试);视频成品 tab 补搜索/排序/分页 - 平台套图工作台:「更多」气泡与 icon 左对齐+鼠标移出自动收起;模特卡改单卡(空态「+」占位,点卡进模特库单选回填) - 工作台:删余额条,冻结/共计移至小标题行右侧(.dash-funds-mono) - 模特库:点模特卡弹详情(形象图/标签/三视图+灯箱放大) - 垃圾桶:新增「全部恢复」/「清空垃圾桶」(清空二次确认) - 视频项目页:字重/阴影等轻量对齐设计规范 - api.assetFacets 返回体逐键兜底:旧后端缺 products 键会致资产库整页 undefined.find 白屏(前后端部署时序防护,浏览器已复现验证) 验证:tsc --noEmit / vite build / apps.assets 测试全过;暂存树已独立编译验证,不依赖工作区其余在途改动 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -220,6 +220,43 @@ class AssetBatchesTests(TestCase):
|
||||
self.assertEqual(self.client.get("/api/assets/summary/").json()["tryon"], 2)
|
||||
|
||||
|
||||
class FacetsProductsTests(TestCase):
|
||||
"""facets 返回本 tab 下真实关联的商品(?product= 三路之一:metadata.product_id 等),
|
||||
并按 tab 隔离、team 隔离、只回 id/title。"""
|
||||
|
||||
def setUp(self):
|
||||
from apps.products.models import Product
|
||||
|
||||
self.user, self.team = _mk_team("ufp", "TeamFP")
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
self.p1 = Product.objects.create(team=self.team, created_by=self.user, title="商品甲")
|
||||
self.p2 = Product.objects.create(team=self.team, created_by=self.user, title="商品乙")
|
||||
# tryon 图经 metadata.product_id 关联 p1
|
||||
Asset.objects.create(
|
||||
team=self.team, name="上身图", asset_type="image", source="ai_generated",
|
||||
category=Asset.Category.MODEL_TRYON, metadata={"product_id": str(self.p1.id)},
|
||||
)
|
||||
# kits 图关联 p2(验证 tab 隔离:tryon 下不应出现 p2)
|
||||
Asset.objects.create(
|
||||
team=self.team, name="套图", asset_type="image", source="ai_generated",
|
||||
category=Asset.Category.PLATFORM_KIT, metadata={"product_id": str(self.p2.id)},
|
||||
)
|
||||
|
||||
def test_facets_products_scoped_by_tab(self):
|
||||
data = self.client.get("/api/assets/facets/?tab=tryon").json()
|
||||
self.assertIn("products", data)
|
||||
self.assertEqual({p["id"] for p in data["products"]}, {str(self.p1.id)})
|
||||
self.assertEqual({p["title"] for p in data["products"]}, {"商品甲"})
|
||||
# kits tab 只见 p2
|
||||
kits = self.client.get("/api/assets/facets/?tab=kits").json()
|
||||
self.assertEqual({p["id"] for p in kits["products"]}, {str(self.p2.id)})
|
||||
|
||||
def test_facets_products_empty_when_none(self):
|
||||
data = self.client.get("/api/assets/facets/?tab=others").json()
|
||||
self.assertEqual(data["products"], [])
|
||||
|
||||
|
||||
class SubmitReviewTests(TestCase):
|
||||
"""手动兜底:灰盾点击 → 提交审核;只对送审类放行,非送审类 400。"""
|
||||
|
||||
|
||||
@@ -335,7 +335,7 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"])
|
||||
def facets(self, request):
|
||||
"""某 tab 下真实存在的筛选项(来源/类型/指定 metadata 键的取值),供下拉「只列真有的」。
|
||||
"""某 tab 下真实存在的筛选项(来源/类型/关联商品/指定 metadata 键的取值),供下拉「只列真有的」。
|
||||
参数:tab、meta_keys=gender,age,role,...(逗号分隔)。"""
|
||||
base = Asset.objects.filter(team=self.get_team(), is_deleted=False, in_library=True)
|
||||
if request.query_params.get("tab"):
|
||||
@@ -346,7 +346,45 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
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})
|
||||
# 关联商品:本 tab 下真实关联到的商品(供「关联商品」下拉只列真有的),只回 id/title
|
||||
products = self._facet_products(base)
|
||||
return Response({"sources": sources, "kinds": kinds, "metadata": meta, "products": products})
|
||||
|
||||
def _facet_products(self, base):
|
||||
"""收集 base(已 team+tab 过滤)下真实关联的商品 id → 解析成 [{id, title}]。
|
||||
三路与 get_queryset(?product=)完全对齐:① metadata.product_id ② origin_task→project→product
|
||||
(仅「确属商品」的类目,不误纳角色/场景/分镜)③ ProductImage 关联。
|
||||
只 values 取需要的列(product_id / title),不 select_related AITask,不拖巨型 payload。"""
|
||||
from apps.products.models import Product
|
||||
|
||||
product_ids: set[str] = set()
|
||||
# 路① 独立生图写 metadata.product_id:JSON 键在 Python 侧取,避开 MySQL JSON 抽取比较坑
|
||||
for md in base.values_list("metadata", flat=True):
|
||||
if isinstance(md, dict):
|
||||
pid = md.get("product_id")
|
||||
if pid:
|
||||
product_ids.add(str(pid))
|
||||
# 路② 项目内生成回溯,仅「确属商品」的类目(与 ZWQ#5 一致)
|
||||
product_categories = (
|
||||
Asset.Category.PRODUCT_IMAGE,
|
||||
Asset.Category.MODEL_TRYON,
|
||||
Asset.Category.PLATFORM_KIT,
|
||||
Asset.Category.FREE_CREATE,
|
||||
)
|
||||
pid2 = base.filter(
|
||||
category__in=product_categories, origin_task__project__product__isnull=False
|
||||
).values_list("origin_task__project__product_id", flat=True)
|
||||
product_ids.update(str(x) for x in pid2 if x)
|
||||
# 路③ 上传商品图经 ProductImage 关联
|
||||
pid3 = base.filter(product_images__isnull=False).values_list("product_images__product_id", flat=True)
|
||||
product_ids.update(str(x) for x in pid3 if x)
|
||||
if not product_ids:
|
||||
return []
|
||||
# 团队隔离(即便某 product_id 越权也过滤掉);只取 id/title
|
||||
rows = Product.objects.filter(team=self.get_team(), id__in=product_ids).values("id", "title")
|
||||
products = [{"id": str(r["id"]), "title": r["title"]} for r in rows]
|
||||
products.sort(key=lambda x: x["title"] or "")
|
||||
return products
|
||||
|
||||
@action(detail=True, methods=["get"], url_path="raw")
|
||||
def raw(self, request, pk=None):
|
||||
|
||||
Reference in New Issue
Block a user