diff --git a/backend/app/api/v1/endpoints/tasks.py b/backend/app/api/v1/endpoints/tasks.py index 8394f35..981b251 100644 --- a/backend/app/api/v1/endpoints/tasks.py +++ b/backend/app/api/v1/endpoints/tasks.py @@ -3,6 +3,7 @@ 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 @@ -173,6 +174,7 @@ async def receive_task_endpoint( 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), ): """ **确认接收任务。工人选定工序名称后接收。** @@ -181,7 +183,8 @@ async def receive_task_endpoint( 动作:状态改为 WIP,记录 received_at,更新 task_name,同步产品宏观状态。 """ return await task_service.receive_task( - db, uuid.UUID(task_id), operator_id, remark, task_name + db, uuid.UUID(task_id), operator_id, remark, task_name, + operator_role=current_user.get("role"), ) @@ -195,6 +198,7 @@ async def reject_task_endpoint( request: TaskRejectRequest, operator_id: str | None = Query(None, description="操作人ID"), db: AsyncSession = Depends(get_db), + current_user: dict = Depends(get_current_user), ): """ **品质驳回:将任务标记为 REJECTED,自动创建返工任务。** @@ -205,7 +209,8 @@ async def reject_task_endpoint( 3. 为该负责人新建返工任务(is_rework=True, status=PENDING)。 """ return await task_service.reject_task( - db, uuid.UUID(task_id), request, operator_id + db, uuid.UUID(task_id), request, operator_id, + operator_role=current_user.get("role"), ) @@ -219,6 +224,7 @@ async def transfer_task_endpoint( request: TaskTransferRequest, operator_id: str | None = Query(None, description="操作人ID"), db: AsyncSession = Depends(get_db), + current_user: dict = Depends(get_current_user), ): """ **完工并裂变转交:完成当前任务,批量创建下一道工序任务。** @@ -237,7 +243,8 @@ async def transfer_task_endpoint( - 否则 → 顶层同级转交。 """ return await task_service.transfer_task( - db, uuid.UUID(task_id), request, operator_id + db, uuid.UUID(task_id), request, operator_id, + operator_role=current_user.get("role"), ) diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py index dfe428a..6c1f2be 100644 --- a/backend/app/services/task_service.py +++ b/backend/app/services/task_service.py @@ -30,6 +30,20 @@ from app.schemas.task import ( # 特殊位置常量 VIRTUAL_WAREHOUSE = "virtual_warehouse" +# 管理员/主管角色白名单 — 拥有上帝视角操作权限 +ADMIN_ROLES = {"SUPER_ADMIN", "SUPERVISOR"} + + +def _check_permission(task_assignee_id: str | None, operator_id: str | None, operator_role: str | None = None) -> None: + """权限校验:本人 或 管理员/主管 可操作""" + if operator_role and operator_role in ADMIN_ROLES: + return # 上帝视角,直接放行 + if operator_id and task_assignee_id and operator_id != task_assignee_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"您无权操作此任务,当前任务负责人为 {task_assignee_id}", + ) + # ============================================================ # 内部辅助函数 @@ -433,6 +447,7 @@ ARCHIVED_STATUS = "ARCHIVED" async def receive_task( db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None, remark: str | None = None, task_name: str | None = None, + operator_role: str | None = None, ) -> TaskResponse: """ 操作员确认接收任务。工人选定工序名称后接收。 @@ -442,12 +457,8 @@ async def receive_task( """ task = await _get_task_or_404(db, task_id) - # 权限校验:只有负责人本人可接收 - if operator_id and task.assignee_id and operator_id != task.assignee_id: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=f"您无权操作此任务,当前任务负责人为 {task.assignee_id}", - ) + # 权限校验:本人 或 管理员/主管 可操作 + _check_permission(task.assignee_id, operator_id, operator_role) # 校验:只有 PENDING 状态可接收 if task.status != TASK_STATUS_PENDING: @@ -492,7 +503,8 @@ async def receive_task( # ============================================================ async def reject_task( - db: AsyncSession, task_id: uuid.UUID, request: TaskRejectRequest, operator_id: str | None = None + db: AsyncSession, task_id: uuid.UUID, request: TaskRejectRequest, operator_id: str | None = None, + operator_role: str | None = None, ) -> TaskResponse: """ 品质驳回:将当前任务标记为 REJECTED,并自动创建返工任务给上一道工序负责人。 @@ -507,12 +519,8 @@ async def reject_task( """ task = await _get_task_or_404(db, task_id) - # 权限校验:只有负责人本人可驳回 - if operator_id and task.assignee_id and operator_id != task.assignee_id: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=f"您无权操作此任务,当前任务负责人为 {task.assignee_id}", - ) + # 权限校验:本人 或 管理员/主管 可驳回 + _check_permission(task.assignee_id, operator_id, operator_role) # 校验:不能重复驳回已完成/已驳回的任务 if task.status in (TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED): @@ -612,7 +620,8 @@ async def reject_task( # ============================================================ async def transfer_task( - db: AsyncSession, task_id: uuid.UUID, request: TaskTransferRequest, operator_id: str | None = None + db: AsyncSession, task_id: uuid.UUID, request: TaskTransferRequest, operator_id: str | None = None, + operator_role: str | None = None, ) -> TaskTransferResponse: """ 完工并裂变转交: @@ -633,6 +642,9 @@ async def transfer_task( """ task = await _get_task_or_404(db, task_id) + # 权限校验:本人 或 管理员/主管 可转交 + _check_permission(task.assignee_id, operator_id, operator_role) + # 校验:不能重复完成 if task.status == TASK_STATUS_COMPLETED: raise HTTPException( diff --git a/frontend/src/components/TaskTree/TaskFlowView.tsx b/frontend/src/components/TaskTree/TaskFlowView.tsx index 3591259..b01473b 100644 --- a/frontend/src/components/TaskTree/TaskFlowView.tsx +++ b/frontend/src/components/TaskTree/TaskFlowView.tsx @@ -92,15 +92,23 @@ const FlowCard = memo(function FlowCard({ task, isActive, onAction, + currentUser, + assigneeName, }: { task: TaskResponse; isActive: boolean; onAction: (target: ModalTarget) => void; + currentUser?: { username?: string; role?: string } | null; + assigneeName?: string; }) { const cfg = getStatusConfig(task.status); const dwell = calcDwell(task.received_at, task.completed_at, task.status); const isCompleted = task.status?.toUpperCase() === TASK_STATUS.COMPLETED; const isArchived = task.status?.toUpperCase() === TASK_STATUS.ARCHIVED; + // 权限:本人或管理员/主管可操作 + const isOwner = !!(currentUser?.username && task.assignee_id === currentUser.username); + const isManager = currentUser?.role === "SUPER_ADMIN" || currentUser?.role === "SUPERVISOR"; + const canOperate = isOwner || isManager; return (
⚠ 运维干预模式
+ {task.status?.toUpperCase() === TASK_STATUS.PENDING && ( +非当前任务责任人,无法操作
+