feat(backend): 产品列表返回当前人滞留时长 active_duration_hours

- ProductResponse 增加 active_duration_hours(小时) 字段
- get_all_products 批量计算每个产品活跃任务(WIP/PENDING)最早接手时间到现在的时长
- 复用 get_people_workload 相同的北京时间口径,前端无需额外请求
This commit is contained in:
2026-08-28 10:46:10 +08:00
parent 620df5ce11
commit 90d615cbb0
2 changed files with 31 additions and 0 deletions

View File

@ -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}

View File

@ -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
]