diff --git a/backend/app/api/v1/endpoints/tasks.py b/backend/app/api/v1/endpoints/tasks.py
index 88c3211..a788b4c 100644
--- a/backend/app/api/v1/endpoints/tasks.py
+++ b/backend/app/api/v1/endpoints/tasks.py
@@ -2,6 +2,7 @@
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, Query, Body
+from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
@@ -120,6 +121,30 @@ async def end_task_endpoint(
return await task_service.end_task(db, uuid.UUID(task_id), operator_id)
+# ============================================================
+# 核心业务 0.5:并发派发协助分支 (WIP → 不改变状态,创建子任务)
+# ============================================================
+
+class SpawnRequest(BaseModel):
+ task_name: str = Field(..., max_length=200, description="工序名称")
+ assignee_id: str | None = Field(None, max_length=64, description="负责人ID")
+ remark: str | None = Field(None, max_length=2000, description="派发备注")
+
+
+@router.post("/{task_id}/spawn", response_model=TaskResponse, status_code=201)
+async def spawn_subtask_endpoint(
+ task_id: str,
+ data: SpawnRequest,
+ operator_id: str | None = Query(None),
+ db: AsyncSession = Depends(get_db),
+):
+ """
+ **派发协助分支:在当前任务下创建并行子任务,父任务状态保持不变。**
+ 用于 WIP 期间工人需要其他人协助协同的场景。
+ """
+ return await task_service.spawn_subtask(db, uuid.UUID(task_id), data, operator_id)
+
+
# ============================================================
# 核心业务 1:确认接收 (PENDING → WIP)
# ============================================================
diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py
index 32f1eba..2cbb158 100644
--- a/backend/app/services/task_service.py
+++ b/backend/app/services/task_service.py
@@ -236,6 +236,9 @@ async def end_task(
"""
task = await _get_task_or_404(db, task_id)
+ # 校验:必须等待所有协助分支完成
+ await _check_children_done(db, task_id)
+
if task.status == TASK_STATUS_COMPLETED:
raise HTTPException(status_code=409, detail="此分支已经结束")
if task.status == TASK_STATUS_PENDING:
@@ -253,6 +256,62 @@ async def end_task(
return _to_response(task)
+# ============================================================
+# 核心业务 0.5:派发协助分支(不改变父任务状态)
+# ============================================================
+
+async def spawn_subtask(
+ db: AsyncSession, task_id: uuid.UUID, data, operator_id: str | None = None
+) -> TaskResponse:
+ """在当前任务下创建并行子任务,父任务状态保持不变。"""
+ task = await _get_task_or_404(db, task_id)
+
+ if task.status == TASK_STATUS_COMPLETED:
+ raise HTTPException(status_code=409, detail="任务已完成,无法派发协助分支")
+ if task.status == TASK_STATUS_REJECTED:
+ raise HTTPException(status_code=409, detail="任务已驳回,无法派发协助分支")
+
+ child = Task(
+ product_id=task.product_id,
+ parent_task_id=task.id,
+ task_name=data.task_name,
+ assignee_id=data.assignee_id,
+ status=TASK_STATUS_PENDING,
+ notify_parent_on_complete=False,
+ is_rework=False,
+ remark=data.remark or None,
+ )
+ db.add(child)
+ await db.flush()
+
+ await _create_task_log(db, child.id, action_type="create", operator_id=operator_id,
+ remark=f"协助分支(由「{task.task_name}」派发,分配给 {data.assignee_id})")
+ await db.commit()
+ await db.refresh(child)
+ return _to_response(child)
+
+
+async def _check_children_done(db: AsyncSession, task_id: uuid.UUID):
+ """检查当前任务的所有子任务是否都已完结。未完结则抛出 409。"""
+ result = await db.execute(
+ select(Task).where(Task.parent_task_id == task_id)
+ )
+ children = result.scalars().all()
+ incomplete = [c for c in children if c.status not in (
+ TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED, ARCHIVED_STATUS
+ )]
+ if incomplete:
+ names = "、".join(c.task_name for c in incomplete)
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail=f"当前工序还有未完成的协助分支({names}),必须等待分支结束才能转交或完工!",
+ )
+ return True
+
+
+ARCHIVED_STATUS = "ARCHIVED"
+
+
# ============================================================
# 核心业务 1:确认接收 (PENDING → WIP)
# ============================================================
@@ -429,6 +488,9 @@ async def transfer_task(
"""
task = await _get_task_or_404(db, task_id)
+ # 校验:必须等待所有协助分支完成
+ await _check_children_done(db, task_id)
+
# 校验:不能重复完成
if task.status == TASK_STATUS_COMPLETED:
raise HTTPException(
diff --git a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue
index 8e5674f..9f58fee 100644
--- a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue
+++ b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue
@@ -78,6 +78,8 @@
@tap="$emit('action', { task: lockedTask, type: 'transfer' })">🔄 完工转交
+