diff --git a/backend/app/schemas/task.py b/backend/app/schemas/task.py
index 4e524ee..d63627a 100644
--- a/backend/app/schemas/task.py
+++ b/backend/app/schemas/task.py
@@ -116,6 +116,7 @@ class TaskSummaryResponse(BaseModel):
received_at: datetime | None = None
completed_at: datetime | None = None
created_at: datetime
+ created_by: str | None = None # 谁创建的(从task_logs追溯)
model_config = {"from_attributes": True}
@@ -140,6 +141,7 @@ class TaskResponse(BaseModel):
created_at: datetime
child_tasks: list[TaskResponse] = []
records: list[TaskRecordResponse] = []
+ created_by: str | None = None # 谁创建的(从task_logs追溯)
model_config = {"from_attributes": True}
diff --git a/backend/app/services/product_service.py b/backend/app/services/product_service.py
index 47d31cb..931cb5f 100644
--- a/backend/app/services/product_service.py
+++ b/backend/app/services/product_service.py
@@ -42,6 +42,7 @@ def _task_to_response(task: Task) -> TaskResponse:
created_at=task.created_at,
child_tasks=[_task_to_response(c) for c in task.child_tasks],
records=[TaskRecordResponse.model_validate(r) for r in (task.records or [])],
+ created_by=getattr(task, "created_by", None),
)
@@ -85,15 +86,54 @@ async def get_product_by_serial(db: AsyncSession, serial_number: str) -> Product
# 🔧 收集任务树中所有 assignee_id → 查中文姓名映射
assignee_ids: set[str] = set()
+ all_task_ids: list[uuid.UUID] = []
def _collect_ids(tasks):
for t in tasks:
+ all_task_ids.append(t.id)
if t.assignee_id: assignee_ids.add(t.assignee_id)
if t.child_tasks: _collect_ids(t.child_tasks)
for t in top_tasks:
+ all_task_ids.append(t.id)
if t.assignee_id: assignee_ids.add(t.assignee_id)
_collect_ids(task_tree)
assignee_names = _lookup_display_names(list(assignee_ids))
+ # 🔧 批量查 task_logs: 谁创建了每个任务
+ creator_map: dict[uuid.UUID, str] = {}
+ if all_task_ids:
+ from app.models.task_log import TaskLog
+ # 每个 task 取最早的 create 日志的 operator_id
+ sub = (
+ select(
+ TaskLog.task_id,
+ TaskLog.operator_id,
+ func.row_number().over(
+ partition_by=TaskLog.task_id,
+ order_by=TaskLog.created_at.asc()
+ ).label("rn")
+ )
+ .where(
+ TaskLog.task_id.in_(all_task_ids),
+ TaskLog.action_type == "create"
+ )
+ ).subquery()
+ log_result = await db.execute(
+ select(sub.c.task_id, sub.c.operator_id).where(sub.c.rn == 1)
+ )
+ for row in log_result:
+ if row[1]:
+ creator_map[row[0]] = row[1]
+
+ # 注入 created_by 到 task_tree 和 top_tasks
+ def _inject_creator(tasks):
+ for t in tasks:
+ t.created_by = creator_map.get(t.id)
+ if t.child_tasks:
+ _inject_creator(t.child_tasks)
+ _inject_creator(task_tree)
+ for t in top_tasks:
+ t.created_by = creator_map.get(t.id)
+
return ProductScanResponse(
id=product.id,
serial_number=product.serial_number,
diff --git a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue
index c1451ab..1260cde 100644
--- a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue
+++ b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue
@@ -61,7 +61,7 @@
{{ isNestedSpawn ? '⬆ 上游协助 (嵌套)' : '⬆ 上游工序' }}
- {{ parentTaskOf(lockedTask).task_name }} → {{ formatUserName(parentTaskOf(lockedTask).assignee_id) || '—' }}
+ {{ parentTaskOf(lockedTask).task_name }} → {{ formatUserName(lockedTask.created_by) || formatUserName(parentTaskOf(lockedTask).assignee_id) || '—' }}
⬇ 下游分支 ({{ lockedTask.child_tasks.length }})