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
+69 -4
View File
@@ -127,8 +127,11 @@ class AITaskViewSet(TeamScopedViewSetMixin, ReadOnlyModelViewSet):
_GEN_MODES = ("model", "cover", "image")
def _unread_base(self):
"""本团队、属于图片生成、且未读(read_at is null)的任务集合(已 annotate rp_mode/rp_product_id)。"""
return self.get_queryset().filter(rp_mode__in=self._GEN_MODES, read_at__isnull=True)
"""本团队、属于图片生成、且未读(read_at is null)的任务集合(已 annotate rp_mode/rp_product_id)。
过滤必须走 request_payload__mode 路径查找而非 rp_mode 注解:KeyTextTransform 注解上的
exact/in 查找在 MySQL 会把裸字符串塞进 JSON_EXTRACT 当文档解析 → 3141 全查询炸
(sqlite 编译路径不同,单测测不出来);路径查找 RHS 会被正确 JSON 编码。"""
return self.get_queryset().filter(request_payload__mode__in=self._GEN_MODES, read_at__isnull=True)
@action(detail=False, methods=["get"], url_path="unread")
def unread(self, request):
@@ -142,6 +145,68 @@ class AITaskViewSet(TeamScopedViewSetMixin, ReadOnlyModelViewSet):
by_product[str(pid)] = by_product.get(str(pid), 0) + 1
return Response({"total": total, "by_product": by_product})
@action(detail=False, methods=["get"], url_path="workbench")
def workbench(self, request):
"""R100:工作台(模特上身图/平台套图/图片创作)的生成记录改从后端持久数据拉取 ——
原先只存前端 localStorage(1 小时过期、换浏览器/清缓存即空),任务中心有记录而工作台丢。
与任务中心同源 = 同一批 AITask;按 request_payload.mode(白名单)+ product 过滤,
附带每个任务的存活成图资产,前端按 batch_id 归批还原批次流。
性能:仍只用 KeyTextTransform 在 SQL 层抽 payload 标量、defer 两个巨型 payload 列,不整列拖出。"""
from django.db.models.fields.json import KeyTextTransform
mode = str(request.query_params.get("mode") or "").strip()
if mode not in self._GEN_MODES:
return Response({"detail": "mode 仅支持 model / cover / image"}, status=status.HTTP_400_BAD_REQUEST)
try:
limit = min(500, max(1, int(request.query_params.get("limit") or 200)))
except (TypeError, ValueError):
limit = 200
qs = (
AITask.objects.filter(
team=self.get_team(),
project__isnull=True, # 工作台独立生图不挂项目;排除流水线内部任务
task_type__in=[AITask.Type.PERSON_IMAGE, AITask.Type.PRODUCT_IMAGE],
)
.defer("request_payload", "response_payload")
.annotate(
rp_mode=KeyTextTransform("mode", "request_payload"),
rp_batch_id=KeyTextTransform("batch_id", "request_payload"),
rp_product_id=KeyTextTransform("product_id", "request_payload"),
rp_prompt=KeyTextTransform("prompt", "request_payload"),
rp_ratio=KeyTextTransform("ratio", "request_payload"),
rp_platform_id=KeyTextTransform("platform_id", "request_payload"),
rp_model_id=KeyTextTransform("model_id", "request_payload"),
rp_model_entity_id=KeyTextTransform("model_entity_id", "request_payload"),
)
.filter(request_payload__mode=mode) # 路径查找,不能用 rp_mode 注解比较(MySQL 3141,见 _unread_base)
)
product_id = str(request.query_params.get("product") or "").strip()
if product_id:
qs = qs.filter(request_payload__product_id=product_id)
tasks = list(qs.order_by("-created_at").prefetch_related("generated_assets", "generated_assets__files")[:limit])
tasks.reverse() # 旧 → 新,与对话流/工作台批次流的时间序一致
data = [
{
"id": str(t.id),
"status": t.status,
"error_message": t.error_message,
"prompt": t.rp_prompt or "",
"batch_id": t.rp_batch_id or "",
"ratio": t.rp_ratio or "",
"product_id": t.rp_product_id or "",
"model_id": t.rp_model_id or "",
"model_entity_id": t.rp_model_entity_id or "",
"platform_id": t.rp_platform_id or "",
"created_at": t.created_at,
# 软删的图不再出现在工作台记录里(R109:删除资产库图片 → 任务记录联动)
"assets": AssetSerializer(
[a for a in t.generated_assets.all() if not a.is_deleted], many=True
).data,
}
for t in tasks
]
return Response({"tasks": data})
@action(detail=False, methods=["post"], url_path="mark-read")
def mark_read(self, request):
"""标记已读 → 清零未读胶囊。
@@ -154,9 +219,9 @@ class AITaskViewSet(TeamScopedViewSetMixin, ReadOnlyModelViewSet):
if isinstance(ids, str):
ids = [s for s in ids.split(",") if s.strip()]
if product_id:
qs = qs.filter(rp_product_id=product_id)
qs = qs.filter(request_payload__product_id=product_id)
if batch_id:
qs = qs.filter(rp_batch_id=batch_id)
qs = qs.filter(request_payload__batch_id=batch_id)
if ids:
qs = qs.filter(id__in=[str(i).strip() for i in ids if str(i).strip()])
updated = qs.update(read_at=timezone.now())