Files
track/backend/app/api/v1/endpoints/tasks.py

290 lines
10 KiB
Python
Raw Normal View History

"""任务 API 端点 — 核心业务:接收、驳回返工、裂变转交、无限嵌套子任务"""
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
from app.schemas.task import (
TaskCreate,
TaskUpdate,
TaskCompleteRequest,
TaskRejectRequest,
TaskTransferRequest,
TaskTransferBranch,
SubtaskCreate,
TaskRecordCreate,
TaskResponse,
TaskCompleteResponse,
TaskTransferResponse,
TaskSummaryResponse,
TaskListResponse,
)
from app.services import task_service
router = APIRouter(prefix="/tasks", tags=["任务管理"])
# ============================================================
# 任务 CRUD
# ============================================================
@router.get("/", response_model=TaskListResponse)
async def list_tasks(
product_id: str | None = Query(None, description="按产品ID筛选"),
assignee_id: str | None = Query(None, description="按负责人ID筛选逻辑外键→老系统"),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
db: AsyncSession = Depends(get_db),
):
"""获取任务列表,可按产品/负责人筛选(只返回顶层任务)"""
pid = uuid.UUID(product_id) if product_id else None
return await task_service.get_all_tasks(db, product_id=pid, assignee_id=assignee_id, skip=skip, limit=limit)
@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(
task_id: str,
db: AsyncSession = Depends(get_db),
):
"""
获取任务详情 递归包含所有层级的子任务
前端可根据此结果渲染完整的任务树
"""
return await task_service.get_task(db, uuid.UUID(task_id))
@router.post("/", response_model=TaskResponse, status_code=201)
async def create_task_endpoint(
data: TaskCreate,
db: AsyncSession = Depends(get_db),
):
"""创建任务"""
return await task_service.create_task(db, data)
@router.patch("/{task_id}", response_model=TaskResponse)
async def update_task_endpoint(
task_id: str,
data: TaskUpdate,
db: AsyncSession = Depends(get_db),
):
"""更新任务"""
return await task_service.update_task(db, uuid.UUID(task_id), data)
# ============================================================
# 核心卡点逻辑:任务完成 / 转交
# ============================================================
@router.post("/{task_id}/complete", response_model=TaskCompleteResponse)
async def complete_task_endpoint(
task_id: str,
request: TaskCompleteRequest,
db: AsyncSession = Depends(get_db),
):
"""
**核心接口完成任务 + 可选创建下一步任务转交**
卡点逻辑
1. 检查当前任务是否已完成幂等保护
2. 查询所有 `notify_parent_on_complete=True` 的子任务
如果存在未完成的返回 HTTP 400请等待相关子任务完成
3. 全部通过后标记任务为 completed写入操作日志
4. 若提供了 `next_task_name` + `next_assignee_id`自动创建下一步任务
典型场景
- 某个加工步骤完成需要检查所有必须的前置工序子任务是否已完成
- 完成后自动创建下一步任务并指定负责人
"""
return await task_service.complete_task(
db, uuid.UUID(task_id), request
)
# ============================================================
# 核心业务 0结束分支终止当前节点不创建下游
# ============================================================
@router.post("/{task_id}/end", response_model=TaskResponse)
async def end_task_endpoint(
task_id: str,
operator_id: str | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""
**结束当前分支标记任务为 COMPLETED不创建下游任务**
用于工人认为工序已完结无需转交下一人的场景
"""
return await task_service.end_task(db, uuid.UUID(task_id), operator_id)
# ============================================================
# 核心业务 0.3:撤回转交 (PENDING → 删除 + 恢复父任务)
# ============================================================
@router.post("/{task_id}/recall", response_model=TaskResponse)
async def recall_task_endpoint(
task_id: str,
operator_id: str | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""
**撤回转交删除 PENDING 子任务恢复父任务为 WIP**
适用场景转交后发现选错人在对方接收前撤回
"""
return await task_service.recall_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)
# ============================================================
@router.post("/{task_id}/receive", response_model=TaskResponse)
async def receive_task_endpoint(
task_id: str,
operator_id: str | None = Query(None, description="操作人ID"),
remark: str | None = Body(None, description="接收备注", embed=True),
task_name: str | None = Body(None, description="接收人选定的工序名称", embed=True),
db: AsyncSession = Depends(get_db),
):
"""
**确认接收任务工人选定工序名称后接收**
校验只有状态为 PENDING 的任务可接收
动作状态改为 WIP记录 received_at更新 task_name同步产品宏观状态
"""
return await task_service.receive_task(
db, uuid.UUID(task_id), operator_id, remark, task_name
)
# ============================================================
# 核心业务 2品质驳回 (→ REJECTED + 返工闭环)
# ============================================================
@router.post("/{task_id}/reject", response_model=TaskResponse)
async def reject_task_endpoint(
task_id: str,
request: TaskRejectRequest,
operator_id: str | None = Query(None, description="操作人ID"),
db: AsyncSession = Depends(get_db),
):
"""
**品质驳回将任务标记为 REJECTED自动创建返工任务**
防呆闭环逻辑
1. 将当前任务状态改为 REJECTED记录 reject_reason completed_at
2. 查找上一道工序的负责人父任务的 assignee_id
3. 为该负责人新建返工任务is_rework=True, status=PENDING
"""
return await task_service.reject_task(
db, uuid.UUID(task_id), request, operator_id
)
# ============================================================
# 核心业务 3完工并裂变转交 (→ COMPLETED + 多路裂变)
# ============================================================
@router.post("/{task_id}/transfer", response_model=TaskTransferResponse)
async def transfer_task_endpoint(
task_id: str,
request: TaskTransferRequest,
operator_id: str | None = Query(None, description="操作人ID"),
db: AsyncSession = Depends(get_db),
):
"""
**完工并裂变转交完成当前任务批量创建下一道工序任务**
动作 1闭环当前节点
- 将当前任务状态改为 COMPLETED记录 completed_at
动作 2解析下家
- 遍历 next_assignees 列表
- 如果包含 'virtual_warehouse'则将 Product current_location_id 设为仓库
- 为每一个 assignee_id 新建 PENDING 任务
裂变逻辑
- next_assignees > 1 多路裂变新任务挂在当前任务下形成树状分支
- 当前任务是子任务 单路转交也保持在同一父任务下
- 否则 顶层同级转交
"""
return await task_service.transfer_task(
db, uuid.UUID(task_id), request, operator_id
)
# ============================================================
# 无限层级子任务
# ============================================================
@router.post("/{task_id}/subtasks", response_model=TaskResponse, status_code=201)
async def create_subtask_endpoint(
task_id: str,
data: SubtaskCreate,
db: AsyncSession = Depends(get_db),
):
"""
**创建子任务支持无限层级嵌套**
新子任务将自动继承父任务的 product_id
若父任务已完成拒绝创建
"""
return await task_service.create_subtask(
db, uuid.UUID(task_id), data
)
# ============================================================
# 查询产品顶层任务(便捷接口)
# ============================================================
@router.get("/by-product/{product_id}", response_model=list[TaskSummaryResponse])
async def get_tasks_by_product(
product_id: str,
db: AsyncSession = Depends(get_db),
):
"""获取指定产品的顶层任务列表(不含子任务嵌套)"""
return await task_service.get_top_level_tasks(db, uuid.UUID(product_id))
# ============================================================
# 任务进度记录 — 备注/传图
# ============================================================
@router.patch("/{task_id}/records", response_model=TaskResponse)
async def add_task_record_endpoint(
task_id: str,
data: TaskRecordCreate,
db: AsyncSession = Depends(get_db),
):
"""追加进度记录(备注+图片),不改变任务状态"""
return await task_service.add_task_record(db, uuid.UUID(task_id), data)