From bad0941d67019f8c4216c559299c4f351a82f259 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Fri, 28 Aug 2026 15:08:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=97=B6=E9=95=BF):=20=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=97=B6=E9=95=BF=E8=AE=A1=E7=AE=97=E6=8E=92=E9=99=A4=E9=9D=9E?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=97=A5=20+=20=E4=BF=AE=E5=A4=8D=E4=B8=A4?= =?UTF-8?q?=E5=A4=84=E6=97=B6=E5=8C=BA=E5=A4=9A=E7=AE=978=E5=B0=8F?= =?UTF-8?q?=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_wip_tasks/get_people_workload/get_people_history/product active_duration_hours/analytics 均改用 working_duration_hours - 修复 get_wip_tasks 与 active_duration_hours 的 naive 时间误标北京时间 bug(DB实存UTC,原多算8小时) - 每处读取 holidays 表排除配置的放假日期 --- backend/app/services/analytics_service.py | 14 +++++++--- backend/app/services/dashboard_service.py | 34 +++++++++++++++-------- backend/app/services/product_service.py | 14 +++++----- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/backend/app/services/analytics_service.py b/backend/app/services/analytics_service.py index 961fbb8..0184bbe 100644 --- a/backend/app/services/analytics_service.py +++ b/backend/app/services/analytics_service.py @@ -114,11 +114,12 @@ def _to_bj(dt: datetime | None) -> datetime | None: return dt -def _duration_hours(start: datetime | None, end: datetime) -> float: - """计算单台耗时(小时),无开始时间返回 0。""" +def _duration_hours(start: datetime | None, end: datetime, holidays: set = None) -> float: + """计算单台耗时(工作小时,排除周末/节假日),无开始时间返回 0。""" if not start: return 0.0 - return (end - start).total_seconds() / 3600 + from app.core.time_utils import working_duration_hours + return working_duration_hours(start, end, holidays) # ============================================================ @@ -142,10 +143,15 @@ async def get_capability_profile( Task, TASK_STATUS_WIP, TASK_STATUS_PENDING, TASK_STATUS_COMPLETED, ) from app.models.product import Product + from app.models.holiday import Holiday from app.core.time_utils import get_beijing_time now = get_beijing_time() + # 读取节假日(排除非工作日) + hres = await db.execute(select(Holiday.day)) + holidays = {r[0] for r in hres} + stmt = ( select( Task.assignee_id, Task.status, @@ -196,7 +202,7 @@ async def get_capability_profile( "hours": 0.0, "prio": 9, "first_start": None, "last_completed": None, }) - entry["hours"] += _duration_hours(start, end) + entry["hours"] += _duration_hours(start, end, holidays) if start and (entry["first_start"] is None or start < entry["first_start"]): entry["first_start"] = start completed_dt = _to_bj(row[4]) if row[4] else None diff --git a/backend/app/services/dashboard_service.py b/backend/app/services/dashboard_service.py index 31c6252..8b79631 100644 --- a/backend/app/services/dashboard_service.py +++ b/backend/app/services/dashboard_service.py @@ -209,7 +209,12 @@ async def get_dashboard_stats( async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]: from app.models.task import Task, TASK_STATUS_PENDING, TASK_STATUS_WIP from app.models.product import Product - from app.core.time_utils import get_beijing_time, BEIJING_TZ + from app.models.holiday import Holiday + from app.core.time_utils import get_beijing_time, to_beijing, working_duration_hours + + # 读取节假日(排除非工作日) + hres = await db.execute(select(Holiday.day)) + holidays = {r[0] for r in hres} stmt = ( select(Task, Product.serial_number, Product.external_serial, Product.material_name, Product.spec_model) @@ -231,12 +236,9 @@ async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]: for task, product_sn, ext_sn, mat_name, spec in rows: start = task.received_at or task.created_at if start: - if start.tzinfo is None: - start = start.replace(tzinfo=BEIJING_TZ) - else: - start = start.astimezone(BEIJING_TZ) - hours = round((now - start).total_seconds() / 3600, 1) - recv_str = start.strftime("%m-%d %H:%M") + start_bj = to_beijing(start) # 🚀 naive 按 UTC 转北京时间(修复多算8小时) + hours = working_duration_hours(start_bj, now, holidays) + recv_str = start_bj.strftime("%m-%d %H:%M") else: hours = 0 recv_str = "" @@ -683,7 +685,12 @@ async def get_people_workload(db: AsyncSession) -> list[PersonWorkload]: """上帝视角 — 按负责人聚合当前在制品设备(WIP/PENDING 任务,product 去重,含滞留时长)。""" from app.models.task import Task, TASK_STATUS_PENDING, TASK_STATUS_WIP from app.models.product import Product - from app.core.time_utils import get_beijing_time, BEIJING_TZ + from app.models.holiday import Holiday + from app.core.time_utils import get_beijing_time, to_beijing, working_duration_hours + + # 读取节假日(排除非工作日) + hres = await db.execute(select(Holiday.day)) + holidays = {r[0] for r in hres} stmt = ( select( @@ -749,7 +756,7 @@ async def get_people_workload(db: AsyncSession) -> list[PersonWorkload]: devices: list[PersonDevice] = [] for product_id, e in prods.items(): dt = e["earliest_dt"] - hours = round((now - dt).total_seconds() / 3600, 1) if dt else 0.0 + hours = working_duration_hours(dt, now, holidays) if dt else 0.0 received_str = dt.strftime("%m-%d %H:%M") if dt else None devices.append(PersonDevice( product_id=product_id, @@ -787,11 +794,16 @@ async def get_people_history( """上帝视角 — 人员效能与工时台账(平铺 Task 明细,含 WIP/PENDING/COMPLETED)。""" from app.models.task import Task, TASK_STATUS_WIP, TASK_STATUS_PENDING, TASK_STATUS_COMPLETED from app.models.product import Product - from app.core.time_utils import get_beijing_time, BEIJING_TZ + from app.core.time_utils import get_beijing_time, BEIJING_TZ, to_beijing, working_duration_hours + from app.models.holiday import Holiday from sqlalchemy import func now = get_beijing_time() + # 读取节假日(排除非工作日) + hres = await db.execute(select(Holiday.day)) + holidays = {r[0] for r in hres} + # ── 关联 TaskRecord:最新有效备注 + 记录总数 ── from app.models.task import TaskRecord from sqlalchemy import and_, or_ @@ -915,7 +927,7 @@ async def get_people_history( else: end_dt = now completed_str = None - hours = round((end_dt - received_dt).total_seconds() / 3600, 1) if received_dt else 0.0 + hours = working_duration_hours(received_dt, end_dt, holidays) if received_dt else 0.0 records.append(PersonHistoryRecord( task_id=str(row[0]), task_name=row[1] or "", diff --git a/backend/app/services/product_service.py b/backend/app/services/product_service.py index e6b44d5..c2cb662 100644 --- a/backend/app/services/product_service.py +++ b/backend/app/services/product_service.py @@ -541,11 +541,14 @@ 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)最早接手时间 → 小时 + # 🔧 当前人滞留时长:每个产品活跃任务(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 + from app.core.time_utils import get_beijing_time, to_beijing, working_duration_hours + from app.models.holiday import Holiday + hres = await db.execute(select(Holiday.day)) + holidays = {r[0] for r in hres} start_stmt = ( select( Task.product_id, @@ -563,11 +566,8 @@ async def get_all_products( 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) + start_bj = to_beijing(start) # 🚀 naive 按 UTC 转北京时间(修复多算8小时) + active_duration_map[row[0]] = working_duration_hours(start_bj, now, holidays) return [ ProductResponse(