fix: 测试清单遗留bug批量修复(15条:商品库/消费/团队/设置/消息中心/平台套图/资产库)

- R6 商品图删除不生效: 封面asset移交+绕开prefetch缓存, 前端封面缩图补删除入口
- R48 消息中心长文本与UI框错位: 胶囊单行省略+优先级标签禁压缩
- R70 账单流水筛选: 后端加ledger_type过滤, count随筛选变化, 切筛选重置页码
- R72 月限额改后不刷新: 团队页/消费页自取最新teamSettings, 两页同源
- R83/R86 平台套图提示词框加大+模型胶囊嵌入输入框(复用Pill组件)
- R96 未读角标: 修KeyTextTransform注解过滤触发MySQL 3141致接口500
- R100 工作台记录丢失: 新增GET /api/ai/tasks/workbench/ 同源恢复, 废除localStorage方案
- R104 卖点添加按钮移出输入框, 新建抽屉+编辑态统一
- R106 商品库宽屏锁5列, 10个/页铺满两行
- R108 资产删除改软删+单张删除+回收站二次确认; trash页商品/资产双分区+restore/purge端点
- R109 生成图自动入资产库(person演员立绘除外), 迁移0008存量回填, 移除全部加入资产库入口
- R65 无需改动(22268d6已修, 测旧部署所致)

验证: tsc/build/check/makemigrations --check 通过, 后端测试210/210, perf-probe PASS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-07-02 11:30:06 +08:00
co-authored by Claude Fable 5
parent 22268d608b
commit aab4d7ea4c
28 changed files with 924 additions and 476 deletions
+50 -1
View File
@@ -5,6 +5,7 @@ import uuid
import requests
from django.db import transaction
from django.db.models import Q
from django.db.models.deletion import ProtectedError
from django.http import StreamingHttpResponse
from rest_framework import status
from rest_framework.decorators import action
@@ -99,11 +100,24 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
search_fields = ["name", "description"]
ordering_fields = ["created_at", "updated_at", "name"]
def perform_destroy(self, instance):
"""R108:资产删除 = 回收站软删(is_deleted 置位),不再物理删行(原默认 hard delete 不可恢复)。
资产库列表/批次/任务中心批次弹窗/工作台记录/对话历史引用的都是同一 Asset 查询(均过滤
is_deleted),软删后各处即联动消失(R109「生成记录与资产库数据绑定」)。TOS 文件保留,可恢复。"""
instance.is_deleted = True
instance.save(update_fields=["is_deleted", "updated_at"])
def get_queryset(self):
"""服务端过滤,支持各页按需懒加载(不再前端取全量后客户端切片)。
参数:tab(资产库分桶)/category/asset_type/source/product/q(搜索)/m_<key>(metadata 过滤)/ordering。
默认按 -created_at 排序——无 ORDER BY 时分页会重/漏。"""
qs = super().get_queryset().filter(is_deleted=False) # 软删资产不出现在资产库
qs = super().get_queryset()
# R108 垃圾桶:trash/restore/purge 只看软删资产(镜像商品垃圾桶的写法)。
# perform_destroy 软删时 update updated_at → -updated_at ≈ 删除时间倒序;不过滤 in_library
# (未入库的工作台图删了也得能恢复)。基类 queryset 的 defer(AITask 巨型 payload)保持生效。
if self.action in ("trash", "restore", "purge"):
return qs.filter(is_deleted=True).order_by("-updated_at")
qs = qs.filter(is_deleted=False) # 软删资产不出现在资产库
p = self.request.query_params
# 资产库列表/批次默认只展示「已加入资产库」的资产(in_library=True);未加入的工作台生成图不出现在这里。
# 仅对列表型 action 过滤——retrieve / set-library / submit-review 等仍要能取到未加入的资产。
@@ -244,6 +258,41 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
asset.save(update_fields=["in_library"])
return Response({"id": str(asset.id), "in_library": asset.in_library})
@action(detail=False, methods=["get"], url_path="trash")
def trash(self, request):
"""垃圾桶:列出本团队已软删(is_deleted=True)的资产 —— R108 删除弹窗承诺
「删除的图片将回收到垃圾桶」,此口兑现承诺(与商品垃圾桶 /api/products/trash/ 同构)。"""
qs = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(qs)
if page is not None:
return self.get_paginated_response(self.get_serializer(page, many=True).data)
return Response(self.get_serializer(qs, many=True).data)
@action(detail=True, methods=["post"], url_path="restore")
def restore(self, request, pk=None):
"""从垃圾桶恢复:is_deleted → False。资产库/批次/商品素材区引用的都是同一 Asset 查询
(均过滤 is_deleted),恢复后各处即联动复现;in_library 保持删除前原样,不额外置位。"""
asset = self.get_object() # trash 态 queryset:只有软删资产可恢复
asset.is_deleted = False
asset.save(update_fields=["is_deleted", "updated_at"])
return Response(self.get_serializer(asset).data, status=status.HTTP_200_OK)
@action(detail=True, methods=["delete"], url_path="purge")
def purge(self, request, pk=None):
"""彻底删除(仅垃圾桶内软删资产可彻底删,不可恢复)。与商品 purge 语义对齐:
硬删 DB 行(AssetFile 记录级联删除),TOS 对象不动(商品 purge 同样不触碰对象存储)。
分镜/视频片段/商品图等以 PROTECT 引用资产:被引用的资产硬删会炸 ProtectedError,
如实回 409(留在垃圾桶,仍可恢复),别让它 500。"""
asset = self.get_object()
try:
asset.delete()
except ProtectedError:
return Response(
{"detail": "该资产仍被商品图/分镜/视频片段等引用,无法彻底删除;可恢复后先解除引用"},
status=status.HTTP_409_CONFLICT,
)
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=False, methods=["get"], url_path="video-packs")
def video_packs(self, request):
"""视频成品按「项目素材包」打包:每个项目的已采用视频片段 = 一个包(与「导出全部」同源: