diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py index fe75982..b8a6961 100644 --- a/backend/app/schemas/product.py +++ b/backend/app/schemas/product.py @@ -78,6 +78,7 @@ class ProductScanResponse(BaseModel): created_at: datetime top_level_tasks: list[TaskSummaryResponse] = [] task_tree: list[TaskResponse] = [] + assignee_names: dict[str, str] = {} # 🔧 username→中文姓名映射 model_config = {"from_attributes": True} diff --git a/backend/app/services/product_service.py b/backend/app/services/product_service.py index 3cdb00f..1037646 100644 --- a/backend/app/services/product_service.py +++ b/backend/app/services/product_service.py @@ -108,6 +108,17 @@ async def get_product_by_serial(db: AsyncSession, serial_number: str) -> Product # 获取完整任务树(递归嵌套,供前端渲染十字矩阵树状图) task_tree = await _load_task_tree(db, product.id) + # 🔧 收集任务树中所有 assignee_id → 查中文姓名映射 + assignee_ids: set[str] = set() + def _collect_ids(tasks): + for t in tasks: + if t.assignee_id: assignee_ids.add(t.assignee_id) + if t.child_tasks: _collect_ids(t.child_tasks) + for t in top_tasks: + if t.assignee_id: assignee_ids.add(t.assignee_id) + _collect_ids(task_tree) + assignee_names = _lookup_display_names(list(assignee_ids)) + return ProductScanResponse( id=product.id, serial_number=product.serial_number, @@ -128,6 +139,7 @@ async def get_product_by_serial(db: AsyncSession, serial_number: str) -> Product TaskSummaryResponse.model_validate(t) for t in top_tasks ], task_tree=task_tree, + assignee_names=assignee_names, # 🔧 username→中文姓名 ) @@ -408,6 +420,53 @@ async def get_all_products( for row in task_result: macro_map[row[0]] = prio_to_status.get(row[1], None) + # 🔧 动态宏观状态:只从主干任务中获取最高优先级任务的 task_name + overall_names: dict[uuid.UUID, str] = {} + if product_ids: + from sqlalchemy import func as sa_func2, case as sa_case2 + # 先找每个产品中优先级最高的主干任务ID + main_prio_stmt = ( + select( + Task.product_id, + sa_func2.max(sa_case2( + (Task.status == "WIP", 3), + (Task.status == "PENDING", 2), + (Task.status == "COMPLETED", 1), + else_=0, + )).label("prio"), + ) + .where( + Task.product_id.in_(product_ids), + sa_func2.or_( + Task.parent_task_id.is_(None), + Task.task_type.in_(["TRANSFER", "RECOVERY"]), + ), + ) + .group_by(Task.product_id) + ).subquery() + # 再 join 回去拿 task_name + name_stmt = ( + select(Task.product_id, Task.task_name) + .join(main_prio_stmt, sa_func2.and_( + Task.product_id == main_prio_stmt.c.product_id, + sa_case2( + (Task.status == "WIP", 3), + (Task.status == "PENDING", 2), + (Task.status == "COMPLETED", 1), + else_=0, + ) == main_prio_stmt.c.prio, + sa_func2.or_( + Task.parent_task_id.is_(None), + Task.task_type.in_(["TRANSFER", "RECOVERY"]), + ), + )) + .order_by(Task.created_at.desc()) + .limit(1) + ) + name_result = await db.execute(name_stmt) + for row in name_result: + overall_names[row[0]] = row[1] + return [ ProductResponse( id=p.id, @@ -427,8 +486,8 @@ async def get_all_products( else name_map.get(p.current_location_id) if p.current_location_id else None ), - macro_status=macro_map.get(p.id) or p.status, # 优先任务树状态,兜底产品状态 - overall_status=p.overall_status, + macro_status=macro_map.get(p.id) or p.status, + overall_status=overall_names.get(p.id) or p.overall_status, # 🔧 动态主干任务名优先 status=p.status, created_at=p.created_at, ) diff --git a/frontend/src/pages/admin/AdminTasksPage.tsx b/frontend/src/pages/admin/AdminTasksPage.tsx index 74aa112..f5c9f1b 100644 --- a/frontend/src/pages/admin/AdminTasksPage.tsx +++ b/frontend/src/pages/admin/AdminTasksPage.tsx @@ -413,6 +413,7 @@ export default function AdminTasksPage() { tasks={tree.task_tree} onAction={setModalTarget} currentUser={currentUser} + assigneeNames={tree.assignee_names} /> ) : (