fix: 优化垃圾桶异常批次缩略图

This commit is contained in:
hh
2026-07-16 18:24:00 +08:00
parent 538c5024e2
commit 5fc316112c
2 changed files with 116 additions and 8 deletions
+80 -1
View File
@@ -1903,6 +1903,9 @@ class WorkbenchAndUnreadTests(TestCase):
failed.status = AITask.Status.FAILED
failed.save(update_fields=["status"])
succeeded_asset = Asset.objects.get(origin_task=succeeded)
succeeded_file = succeeded_asset.files.get(is_primary=True)
succeeded_file.preview_url = "http://x/generated-success.png"
succeeded_file.save(update_fields=["preview_url"])
deleted = self.client.delete(f"/api/ai/tasks/{failed.id}/workbench-batch/")
self.assertEqual(deleted.status_code, 200, deleted.content)
@@ -1922,7 +1925,8 @@ class WorkbenchAndUnreadTests(TestCase):
rows = trash.json()["results"]
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["id"], str(failed.id))
self.assertEqual(rows[0]["cover_preview_url"], "http://x/cover.png")
self.assertEqual(rows[0]["cover_preview_url"], "http://x/generated-success.png")
self.assertEqual(rows[0]["product_title"], self.product.title)
asset_trash_ids = {row["id"] for row in self.client.get("/api/assets/trash/").json()["results"]}
self.assertNotIn(str(succeeded_asset.id), asset_trash_ids)
@@ -1948,6 +1952,81 @@ class WorkbenchAndUnreadTests(TestCase):
self.assertIsNotNone(succeeded_asset.purged_at)
self.assertEqual(self.client.get("/api/ai/tasks/workbench-exception-batches-trash/").json()["results"], [])
def test_exception_batch_without_successful_image_uses_no_image_even_with_product_cover(self):
"""纯失败批次即使商品有封面也必须返回空缩略图,由前端显示“无图”。"""
task = self._generate(mode="cover", count=1, platform_id="douyin")[0]
Asset.objects.filter(origin_task=task).delete()
task.status = AITask.Status.FAILED
task.save(update_fields=["status"])
deleted = self.client.delete(f"/api/ai/tasks/{task.id}/workbench-batch/")
self.assertEqual(deleted.status_code, 200, deleted.content)
rows = self.client.get("/api/ai/tasks/workbench-exception-batches-trash/").json()["results"]
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["cover_preview_url"], "")
self.assertEqual(rows[0]["product_title"], self.product.title)
def test_exception_batch_uses_first_successful_image_in_original_task_order(self):
"""多张成功图时跳过前序失败任务,稳定选择原始批次中第一张成功图。"""
tasks = self._generate(mode="cover", count=3, platform_id="douyin")
failed, first_succeeded, later_succeeded = tasks
Asset.objects.filter(origin_task=failed).delete()
failed.status = AITask.Status.FAILED
failed.save(update_fields=["status"])
AssetFile.objects.filter(asset__origin_task=first_succeeded).update(preview_url="http://x/first-success.png")
AssetFile.objects.filter(asset__origin_task=later_succeeded).update(preview_url="http://x/later-success.png")
deleted = self.client.delete(f"/api/ai/tasks/{failed.id}/workbench-batch/")
self.assertEqual(deleted.status_code, 200, deleted.content)
rows = self.client.get("/api/ai/tasks/workbench-exception-batches-trash/").json()["results"]
self.assertEqual(rows[0]["cover_preview_url"], "http://x/first-success.png")
def test_exception_batch_uses_successful_append_when_original_tasks_all_failed(self):
"""原始任务均失败但同批补图成功时,使用最先成功的补图作为缩略图。"""
original_tasks = self._generate(mode="cover", count=2, platform_id="douyin")
for task in original_tasks:
Asset.objects.filter(origin_task=task).delete()
task.status = AITask.Status.FAILED
task.save(update_fields=["status"])
payload = dict(original_tasks[0].request_payload)
payload.update({
"index": 0,
"batch_append": True,
"retry_of_task_id": str(original_tasks[0].id),
})
appended = AITask.objects.create(
team=self.team,
created_by=self.user,
task_type=original_tasks[0].task_type,
status=AITask.Status.SUCCEEDED,
model_config=original_tasks[0].model_config,
idempotency_key="exception-batch-successful-append",
request_payload=payload,
)
appended_asset = Asset.objects.create(
team=self.team,
created_by=self.user,
name="成功补图",
asset_type=Asset.Type.IMAGE,
source=Asset.Source.AI_GENERATED,
category=Asset.Category.PLATFORM_KIT,
origin_task=appended,
)
AssetFile.objects.create(
asset=appended_asset,
object_key="append.png",
bucket="b",
content_type="image/png",
preview_url="http://x/successful-append.png",
is_primary=True,
)
deleted = self.client.delete(f"/api/ai/tasks/{original_tasks[0].id}/workbench-batch/")
self.assertEqual(deleted.status_code, 200, deleted.content)
rows = self.client.get("/api/ai/tasks/workbench-exception-batches-trash/").json()["results"]
self.assertEqual(rows[0]["cover_preview_url"], "http://x/successful-append.png")
def test_successful_batch_keeps_existing_asset_trash_behavior(self):
tasks = self._generate(mode="cover", count=2, platform_id="douyin")
assets = list(Asset.objects.filter(origin_task__in=tasks))
+36 -7
View File
@@ -295,9 +295,36 @@ class AITaskViewSet(TeamScopedViewSetMixin, ReadOnlyModelViewSet):
if product_id:
product_ids.add(product_id)
# 异常批次缩略图只来自该批任务自己的成功成图。批次删除时这些资产会一起软删,
# 这里批量取回并按任务映射,避免逐批查询;商品封面不再参与缩略图回退。
preview_by_task_id = {}
task_ids = [task.id for task in tasks]
generated_assets = (
Asset.objects.filter(
team=self.get_team(),
origin_task_id__in=task_ids,
asset_type=Asset.Type.IMAGE,
is_deleted=True,
purged_at__isnull=True,
)
.prefetch_related("files")
.order_by("created_at", "id")
)
for asset in generated_assets:
files = sorted(
asset.files.all(),
key=lambda item: (not item.is_primary, item.created_at, str(item.id)),
)
for file in files:
preview_url = AssetFileSerializer(file).data.get("preview_url", "")
if preview_url:
preview_by_task_id.setdefault(asset.origin_task_id, preview_url)
break
# 商品仍只用于异常批次标题文字回退,不再关联或预取商品封面资源。
products = Product.objects.filter(
team=self.get_team(), id__in=product_ids, status=Product.Status.ACTIVE, purged_at__isnull=True
).select_related("cover_asset").prefetch_related("cover_asset__files")
).only("id", "title")
product_by_id = {str(product.id): product for product in products}
rows = []
@@ -307,12 +334,14 @@ class AITaskViewSet(TeamScopedViewSetMixin, ReadOnlyModelViewSet):
first = group[0]
payload = first.request_payload or {}
product = product_by_id.get(str(payload.get("product_id") or ""))
cover = ""
if product and product.cover_asset and not product.cover_asset.is_deleted and product.cover_asset.purged_at is None:
files = list(product.cover_asset.files.all())
primary = next((item for item in files if item.is_primary), files[0] if files else None)
if primary:
cover = AssetFileSerializer(primary).data.get("preview_url", "")
cover = next(
(
preview_by_task_id[task.id]
for task in group
if task.status == AITask.Status.SUCCEEDED and task.id in preview_by_task_id
),
"",
)
intended = sum(1 for task in group if not (task.request_payload or {}).get("batch_append"))
rows.append({
"id": str(first.id),