From 90d615cbb0e23134ff843a8d3aa9b1c48445a283 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Fri, 28 Aug 2026 10:46:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E4=BA=A7=E5=93=81=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E8=BF=94=E5=9B=9E=E5=BD=93=E5=89=8D=E4=BA=BA=E6=BB=9E?= =?UTF-8?q?=E7=95=99=E6=97=B6=E9=95=BF=20active=5Fduration=5Fhours?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProductResponse 增加 active_duration_hours(小时) 字段 - get_all_products 批量计算每个产品活跃任务(WIP/PENDING)最早接手时间到现在的时长 - 复用 get_people_workload 相同的北京时间口径,前端无需额外请求 --- backend/app/schemas/product.py | 2 ++ backend/app/services/product_service.py | 29 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+) 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 ]