完善视频提炼和视频复刻完善提交测试
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user