fix: 完善商品—模特库-视频项目-自由创作等删除先进垃圾桶及可恢复等流程
This commit is contained in:
@@ -5,8 +5,8 @@ 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 django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
@@ -120,8 +120,15 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
# 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) # 软删资产不出现在资产库
|
||||
trash_qs = qs.filter(is_deleted=True, purged_at__isnull=True)
|
||||
# 自由创作父记录已进垃圾桶时,生成 Asset 由父记录统一恢复/彻底隐藏,
|
||||
# 不在“资产”分类重复展示。单独从资产库删除时父记录仍为正常态,仍会出现在这里。
|
||||
owned_by_deleted_free_creation = (
|
||||
Q(origin_task__task_type="free_video", origin_task__is_deleted=True)
|
||||
| Q(origin_task__conversation__is_deleted=True)
|
||||
)
|
||||
return trash_qs.exclude(owned_by_deleted_free_creation).order_by("-updated_at")
|
||||
qs = qs.filter(is_deleted=False, purged_at__isnull=True) # 软删资产不出现在资产库
|
||||
p = self.request.query_params
|
||||
# 资产库列表/批次默认只展示「已加入资产库」的资产(in_library=True);未加入的工作台生成图不出现在这里。
|
||||
# 仅对列表型 action 过滤——retrieve / set-library / submit-review 等仍要能取到未加入的资产。
|
||||
@@ -176,7 +183,7 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
"""资产库 tab 计数,供 tab 徽标——不必取全量。
|
||||
图片趴三类(模特上身图/平台套图/自由创作)按「生成批次」计数(与卡片成组展示一致);
|
||||
其他(我的上传 + 兜底)仍按资产张数计。"""
|
||||
base = Asset.objects.filter(team=self.get_team(), is_deleted=False, in_library=True)
|
||||
base = Asset.objects.filter(team=self.get_team(), is_deleted=False, purged_at__isnull=True, in_library=True)
|
||||
out = {}
|
||||
for t in BATCH_TABS: # 图片趴:数批次
|
||||
seen = set()
|
||||
@@ -279,23 +286,16 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
(均过滤 is_deleted),恢复后各处即联动复现;in_library 保持删除前原样,不额外置位。"""
|
||||
asset = self.get_object() # trash 态 queryset:只有软删资产可恢复
|
||||
asset.is_deleted = False
|
||||
asset.save(update_fields=["is_deleted", "updated_at"])
|
||||
asset.purged_at = None
|
||||
asset.save(update_fields=["is_deleted", "purged_at", "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。"""
|
||||
"""彻底删除 = 二级软删除:从垃圾桶隐藏,DB 记录和 TOS 文件保留。"""
|
||||
asset = self.get_object()
|
||||
try:
|
||||
asset.delete()
|
||||
except ProtectedError:
|
||||
return Response(
|
||||
{"detail": "该资产仍被商品图/分镜/视频片段等引用,无法彻底删除;可恢复后先解除引用"},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
asset.purged_at = timezone.now()
|
||||
asset.save(update_fields=["purged_at", "updated_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="video-packs")
|
||||
@@ -323,7 +323,7 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
clips = []
|
||||
for seg in segs:
|
||||
a = getattr(seg.adopted_version, "asset", None)
|
||||
if a is None or a.is_deleted:
|
||||
if a is None or a.is_deleted or a.purged_at is not None:
|
||||
continue
|
||||
clips.append({"id": str(a.id), "name": a.name, "url": _asset_preview(a)})
|
||||
if not clips:
|
||||
@@ -342,7 +342,7 @@ class AssetViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
def facets(self, request):
|
||||
"""某 tab 下真实存在的筛选项(来源/类型/关联商品/指定 metadata 键的取值),供下拉「只列真有的」。
|
||||
参数:tab、meta_keys=gender,age,role,...(逗号分隔)。"""
|
||||
base = Asset.objects.filter(team=self.get_team(), is_deleted=False, in_library=True)
|
||||
base = Asset.objects.filter(team=self.get_team(), is_deleted=False, purged_at__isnull=True, in_library=True)
|
||||
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)
|
||||
@@ -430,8 +430,14 @@ class ModelLibraryViewSet(ModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
team = self.get_team()
|
||||
if self.action in ("trash", "restore", "purge"):
|
||||
return (
|
||||
Model.objects.filter(team=team, is_official=False, is_deleted=True, purged_at__isnull=True)
|
||||
.prefetch_related("portrait_asset__files", "triview_asset__files")
|
||||
.order_by("-updated_at")
|
||||
)
|
||||
qs = (
|
||||
Model.objects.filter(Q(team=team) | Q(is_official=True), is_deleted=False)
|
||||
Model.objects.filter(Q(team=team) | Q(is_official=True), is_deleted=False, purged_at__isnull=True)
|
||||
.prefetch_related("portrait_asset__files", "triview_asset__files")
|
||||
)
|
||||
tab = self.request.query_params.get("tab")
|
||||
@@ -456,6 +462,32 @@ class ModelLibraryViewSet(ModelViewSet):
|
||||
instance.is_deleted = True
|
||||
instance.save(update_fields=["is_deleted", "updated_at"])
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="trash")
|
||||
def trash(self, request):
|
||||
"""垃圾桶:列出本团队已软删且未彻底隐藏的自建模特。"""
|
||||
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):
|
||||
"""从垃圾桶恢复模特。"""
|
||||
model = self.get_object()
|
||||
model.is_deleted = False
|
||||
model.purged_at = None
|
||||
model.save(update_fields=["is_deleted", "purged_at", "updated_at"])
|
||||
return Response(self.get_serializer(model).data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["delete"], url_path="purge")
|
||||
def purge(self, request, pk=None):
|
||||
"""彻底删除 = 二级软删除:从垃圾桶隐藏,DB 记录和关联资产保留。"""
|
||||
model = self.get_object()
|
||||
model.purged_at = timezone.now()
|
||||
model.save(update_fields=["purged_at", "updated_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(detail=False, methods=["post"], url_path="upload", parser_classes=[MultiPartParser, FormParser])
|
||||
def upload(self, request):
|
||||
"""真人上传:一张人像图 → 建 model_portrait 资产 + Model(source=upload)。三视图/声线后续补。"""
|
||||
@@ -507,10 +539,10 @@ class ModelLibraryViewSet(ModelViewSet):
|
||||
资产生成时已落库扣过费,此处只引用、不重新上传/不二次扣费。同一资产已建过则幂等返回。"""
|
||||
team = self.get_team()
|
||||
asset_id = request.data.get("asset_id")
|
||||
asset = Asset.objects.filter(id=asset_id, team=team, is_deleted=False).first() if asset_id else None
|
||||
asset = Asset.objects.filter(id=asset_id, team=team, is_deleted=False, purged_at__isnull=True).first() if asset_id else None
|
||||
if asset is None:
|
||||
raise ValidationError({"asset_id": "资产不存在或不属于本团队"})
|
||||
existing = Model.objects.filter(team=team, portrait_asset=asset, is_deleted=False).first()
|
||||
existing = Model.objects.filter(team=team, portrait_asset=asset, is_deleted=False, purged_at__isnull=True).first()
|
||||
if existing is not None:
|
||||
return Response(ModelLibrarySerializer(existing).data, status=status.HTTP_200_OK)
|
||||
name = (request.data.get("name") or (asset.name or "模特").split("·")[0].strip() or "模特")[:255]
|
||||
|
||||
Reference in New Issue
Block a user