fix: 优化垃圾桶异常批次缩略图
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user