feat(backend): 产品列表返回当前人滞留时长 active_duration_hours
- ProductResponse 增加 active_duration_hours(小时) 字段 - get_all_products 批量计算每个产品活跃任务(WIP/PENDING)最早接手时间到现在的时长 - 复用 get_people_workload 相同的北京时间口径,前端无需额外请求
This commit is contained in:
@ -61,6 +61,8 @@ class ProductResponse(BaseModel):
|
|||||||
latest_record_has_images: bool = False
|
latest_record_has_images: bool = False
|
||||||
latest_record_assignee_id: str | None = None # 🔧 最新记录操作人(消除并发张冠李戴)
|
latest_record_assignee_id: str | None = None # 🔧 最新记录操作人(消除并发张冠李戴)
|
||||||
latest_record_assignee_name: str | None = None
|
latest_record_assignee_name: str | None = None
|
||||||
|
# 🔧 当前人滞留时长 — 活跃任务(WIP/PENDING)最早接手时间到现在的时长(小时)
|
||||||
|
active_duration_hours: float | None = None
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|||||||
@ -541,6 +541,34 @@ async def get_all_products(
|
|||||||
has_img = bool(row[3] and row[3] != "[]" and row[3] != "null")
|
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])
|
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 [
|
return [
|
||||||
ProductResponse(
|
ProductResponse(
|
||||||
id=p.id,
|
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])
|
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
|
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
|
for p in products
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user