完善视频提炼和视频复刻完善提交测试

This commit is contained in:
Azmat@qq.com
2026-08-28 10:15:15 +08:00
parent 52c5cdae1d
commit 849b14a0f6
33 changed files with 1475 additions and 208 deletions
@@ -259,6 +259,29 @@ def _should_mark_project_failed(job: QuickCreateJob) -> bool:
return True
# 编排器活着时会不断推进并保存 job(updated_at 跟着动)。超过这个时长没动过,
# 基本就是 worker 挂了 —— 不当它在跑,否则一次抽风会把团队永久锁在"有任务进行中"。
QUICK_CREATE_STALE_AFTER = timedelta(minutes=30)
def get_inflight_quick_create(team):
"""本团队正在跑的一键成片。没有 / 已经僵死的返回 None。"""
from django.utils import timezone
return (
QuickCreateJob.objects.select_related("project")
.filter(
team=team,
status__in=(QuickCreateJob.Status.QUEUED, QuickCreateJob.Status.RUNNING),
project__is_deleted=False,
project__purged_at__isnull=True,
updated_at__gte=timezone.now() - QUICK_CREATE_STALE_AFTER,
)
.order_by("-created_at")
.first()
)
def restore_false_failed_quick_creates(team) -> None:
jobs = (
QuickCreateJob.objects.select_related("project")
@@ -111,6 +111,55 @@ class QuickCreateApiTests(TestCase):
{str(image.asset_id) for image in product.images.order_by("sort_order")},
)
@patch("apps.projects.services.quick_create.get_quick_script_model", return_value=object())
@patch("apps.projects.views.get_default_model")
@patch("apps.projects.tasks.advance_quick_create_task.apply_async")
@patch("apps.projects.views.require_worker_task")
@patch("apps.projects.views._store_uploaded_asset")
def test_second_submit_is_blocked_while_one_is_running(self, store_asset, require_worker_task, enqueue, get_model, get_quick_model):
"""刷新页面时前端拉状态有空档,用户容易以为「没任务」再提交一次 ——
那会重复建商品、重复扣费。后端必须拦住,并把在跑的那条带回去。"""
store_asset.side_effect = self._uploaded_asset
video_model = SimpleNamespace(
id=uuid.uuid4(),
name="doubao-seedance-2-0-fast-260128",
display_name="Seedance 2.0 Fast",
metadata={"capabilities": {"resolutions": ["480p", "720p"], "aspect_ratios": ["9:16"], "durations": [15]}},
)
get_model.side_effect = [object(), video_model, object(), video_model]
def _post():
return self.client.post(
"/api/projects/quick-create/",
{
"name": "轻醒咖啡",
"images": [SimpleUploadedFile("front.png", b"png-one", content_type="image/png")],
"aspect_ratio": "9:16",
"resolution": "720p",
"total_duration": "15",
"category": "食品饮料",
},
)
first = _post()
self.assertEqual(first.status_code, 202)
job_id = str(QuickCreateJob.objects.get(project__product__title="轻醒咖啡").id)
again = _post()
self.assertEqual(again.status_code, 409)
self.assertIn("完成或取消后", again.json()["detail"])
self.assertEqual(again.json()["inflight"]["id"], job_id)
# 历史接口也要把在跑的那条带出来,前端刷新后才能恢复进度
listed = self.client.get("/api/projects/quick-create-history/").json()
self.assertEqual(listed["inflight"]["id"], job_id)
# 收成终态后放行
QuickCreateJob.objects.filter(id=job_id).update(status=QuickCreateJob.Status.CANCELLED)
from apps.projects.services.quick_create import get_inflight_quick_create
self.assertIsNone(get_inflight_quick_create(self.team))
@patch("apps.projects.services.quick_create.get_quick_script_model", return_value=object())
@patch("apps.projects.views.get_default_model")
@patch("apps.projects.tasks.advance_quick_create_task.apply_async")
+19
View File
@@ -485,6 +485,20 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
def quick_create(self, request):
"""商品名称 + 1–9 张图 → 新建商品与项目,并启动完整自动生产流水线。"""
require_worker_task("apps.projects.tasks.advance_quick_create_task")
# 单飞闸:一个团队同时只允许一条一键成片。刷新页面时前端拉状态有空档,
# 用户容易以为「没任务」再提交一次 —— 那会重复建商品、重复扣费。
# get_inflight_quick_create 会跳过 30 分钟没动过的僵尸 job,不至于把人永久锁住。
from .services.quick_create import get_inflight_quick_create
running_job = get_inflight_quick_create(self.get_team())
if running_job is not None:
return Response(
{
"detail": "已有一个一键成片正在进行中,完成或取消后才能再提交",
"inflight": QuickCreateJobSerializer(running_job).data,
},
status=status.HTTP_409_CONFLICT,
)
name = str(request.data.get("name") or "").strip()
uploads = request.FILES.getlist("images") or request.FILES.getlist("images[]")
source_product_id = str(request.data.get("source_product_id") or "").strip()
@@ -775,9 +789,14 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
)
.order_by("-created_at")
)
from .services.quick_create import get_inflight_quick_create
running = get_inflight_quick_create(self.get_team())
return Response({
"count": jobs.count(),
"results": QuickCreateJobSerializer(jobs[:30], many=True).data,
# 进行中的那条:前端刷新/重进页面据此恢复进度,不再显示成空表单
"inflight": QuickCreateJobSerializer(running).data if running is not None else None,
})
@transaction.atomic