diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py index 1bdda1b..ac90336 100644 --- a/backend/app/schemas/product.py +++ b/backend/app/schemas/product.py @@ -61,6 +61,8 @@ class ProductResponse(BaseModel): latest_record_has_images: bool = False latest_record_assignee_id: str | None = None # 🔧 最新记录操作人(消除并发张冠李戴) latest_record_assignee_name: str | None = None + # 🔧 当前人滞留时长 — 活跃任务(WIP/PENDING)最早接手时间到现在的时长(小时) + active_duration_hours: float | None = None model_config = {"from_attributes": True} diff --git a/backend/app/services/product_service.py b/backend/app/services/product_service.py index 8205904..e6b44d5 100644 --- a/backend/app/services/product_service.py +++ b/backend/app/services/product_service.py @@ -541,6 +541,34 @@ async def get_all_products( has_img = bool(row[3] and row[3] != "[]" and row[3] != "null") latest_record_map[row[0]] = (row[1], row[2], has_img, row[4]) + # 🔧 当前人滞留时长:每个产品活跃任务(WIP/PENDING)最早接手时间 → 小时 + active_duration_map: dict[uuid.UUID, float] = {} + if product_ids: + from sqlalchemy import func as sa_func + from app.core.time_utils import get_beijing_time, BEIJING_TZ + start_stmt = ( + select( + Task.product_id, + sa_func.min(sa_func.coalesce(Task.received_at, Task.created_at)), + ) + .where( + Task.product_id.in_(product_ids), + Task.status.in_(["WIP", "PENDING"]), + ) + .group_by(Task.product_id) + ) + start_result = await db.execute(start_stmt) + now = get_beijing_time() + for row in start_result: + start = row[1] + if start is None: + continue + if start.tzinfo is None: + start = start.replace(tzinfo=BEIJING_TZ) + else: + start = start.astimezone(BEIJING_TZ) + active_duration_map[row[0]] = round((now - start).total_seconds() / 3600, 1) + return [ ProductResponse( id=p.id, @@ -575,6 +603,7 @@ async def get_all_products( merged_name_map.get(latest_record_map.get(p.id, (None, None, False, None))[3]) if latest_record_map.get(p.id, (None, None, False, None))[3] else None ), + active_duration_hours=active_duration_map.get(p.id), ) for p in products ]