From 65ff05fb2fc6012732c97ebf286468e6ae8a7daa Mon Sep 17 00:00:00 2001 From: duxingchen Date: Fri, 7 Aug 2026 17:26:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=8A=B6=E6=80=81=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D+=E4=B8=AD=E6=96=87=E5=A7=93=E5=90=8D?= =?UTF-8?q?=E6=98=A0=E5=B0=84+SUPERVISOR/SUPER=5FADMIN=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=8E=A7=E5=88=B6(=E5=89=8D=E7=AB=AF+=E5=90=8E=E7=AB=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/endpoints/tasks.py | 13 ++- backend/app/services/task_service.py | 40 ++++++--- .../src/components/TaskTree/TaskFlowView.tsx | 86 +++++++++++++------ frontend/src/pages/admin/AdminTasksPage.tsx | 14 ++- 4 files changed, 110 insertions(+), 43 deletions(-) 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.assignee_id && ( - {task.assignee_id} + {assigneeName || task.assignee_id} )}
@@ -160,49 +168,75 @@ const FlowCard = memo(function FlowCard({ {task.received_at && `接收: ${new Date(task.received_at).toLocaleDateString("zh-CN")}`}

- {/* 操作按钮 — 仅激活态显示 */} - {isActive && ( + {/* 操作按钮 — 仅激活态 + 有权限时显示 */} + {isActive && canOperate && (
{task.status?.toUpperCase() === TASK_STATUS.PENDING && ( <> - - - )} {task.status?.toUpperCase() === TASK_STATUS.WIP && ( <> - - )}
)} + {/* 非本人但管理员可见:干预提示 */} + {isActive && !isOwner && isManager && ( +
+

⚠ 运维干预模式

+ {task.status?.toUpperCase() === TASK_STATUS.PENDING && ( +
+ + +
+ )} + {task.status?.toUpperCase() === TASK_STATUS.WIP && ( +
+ + +
+ )} +
+ )} + {/* 非本人非管理员:只读提示 */} + {isActive && !canOperate && ( +
+

非当前任务责任人,无法操作

+
+ )} ); }); @@ -212,9 +246,11 @@ const FlowCard = memo(function FlowCard({ interface TaskFlowViewProps { tasks: TaskResponse[]; onAction: (target: ModalTarget) => void; + currentUser?: { username?: string; role?: string } | null; + assigneeNames?: Record; } -export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction }: TaskFlowViewProps) { +export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, currentUser, assigneeNames }: TaskFlowViewProps) { // 按 depth 分组的层级数据 const depthMap = useMemo(() => { if (!tasks || tasks.length === 0) return new Map(); @@ -266,6 +302,8 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction }: Task task={task} isActive={isActive} onAction={onAction} + currentUser={currentUser} + assigneeName={assigneeNames?.[task.assignee_id || ""]} /> ); diff --git a/frontend/src/pages/admin/AdminTasksPage.tsx b/frontend/src/pages/admin/AdminTasksPage.tsx index a5fa5b8..c2305be 100644 --- a/frontend/src/pages/admin/AdminTasksPage.tsx +++ b/frontend/src/pages/admin/AdminTasksPage.tsx @@ -16,6 +16,7 @@ import type { ProductResponse } from "../../types/admin"; import type { ProductScanResponse, TaskResponse } from "../../types/api"; import { useToast } from "../../components/ui/Toast"; import { getStatusConfig } from "../../constants/task"; +import { useAuth } from "../../contexts/AuthContext"; const STATUS_TABS = [ { key: "", label: "全部" }, @@ -33,6 +34,7 @@ interface OrderGroup { export default function AdminTasksPage() { const { toast } = useToast(); + const { user: currentUser } = useAuth(); // 搜索 & 筛选 const [keyword, setKeyword] = useState(""); @@ -357,7 +359,14 @@ export default function AdminTasksPage() { const productExpanded = expandedProducts.has(p.serial_number); const isTreeLoading = treeLoading[p.serial_number]; const tree = taskTrees[p.serial_number]; - const statusCfg = getStatusConfig(p.status); + // 综合状态:优先流转树状态,兜底产品状态 + const treeStatus = tree + ? (tree.task_tree?.some(t => t.status === "WIP") ? "WIP" + : tree.task_tree?.every(t => t.status === "COMPLETED" || t.status === "ARCHIVED") ? "COMPLETED" + : tree.task_tree?.[0]?.status) + : null; + const displayStatus = treeStatus || p.status; + const statusCfg = getStatusConfig(displayStatus); return (
@@ -386,7 +395,7 @@ export default function AdminTasksPage() { 🏭 仓库 ) : ( - p.current_location_id ?? "—" + p.current_location_name || p.current_location_id || "—" )}
@@ -420,6 +429,7 @@ export default function AdminTasksPage() { ) : (