Files
track/backend/app/api/v1/endpoints/tasks.py
duxingchen d40a8d480e fix(backend): end_task 和 complete_task 补齐权限校验
- end_task 新增 operator_role 参数 + _check_permission 调用
- complete_task 新增 operator_role 参数 + _check_permission 调用
- 两个端点均注入 current_user Depends(get_current_user)
- 非任务负责人且非管理员/主管调用时返回 403
2026-08-11 10:13:40 +08:00

303 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""任务 API 端点 — 核心业务:接收、驳回返工、裂变转交、无限嵌套子任务"""
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, Query, Body
from pydantic import BaseModel, Field
from app.services.auth_service import get_current_user
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),
current_user: dict = Depends(get_current_user),
):
"""
**核心接口:完成任务 + 可选创建下一步任务(转交)**
卡点逻辑:
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,
operator_role=current_user.get("role"),
)
# ============================================================
# 核心业务 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),
current_user: dict = Depends(get_current_user),
):
"""
**结束当前分支:标记任务为 COMPLETED不创建下游任务。**
用于工人认为工序已完结、无需转交下一人的场景。
"""
return await task_service.end_task(
db, uuid.UUID(task_id), operator_id,
operator_role=current_user.get("role"),
)
# ============================================================
# 核心业务 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),
current_user: dict = Depends(get_current_user),
):
"""
**确认接收任务。工人选定工序名称后接收。**
校验:只有状态为 PENDING 的任务可接收。
动作:状态改为 WIP记录 received_at更新 task_name同步产品宏观状态。
"""
return await task_service.receive_task(
db, uuid.UUID(task_id), operator_id, remark, task_name,
operator_role=current_user.get("role"),
)
# ============================================================
# 核心业务 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),
current_user: dict = Depends(get_current_user),
):
"""
**品质驳回:将任务标记为 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,
operator_role=current_user.get("role"),
)
# ============================================================
# 核心业务 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),
current_user: dict = Depends(get_current_user),
):
"""
**完工并裂变转交:完成当前任务,批量创建下一道工序任务。**
动作 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,
operator_role=current_user.get("role"),
)
# ============================================================
# 无限层级子任务
# ============================================================
@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)