From b09187ac6e2a33e4bf65eba4a13268be24f4ac0a Mon Sep 17 00:00:00 2001 From: duxingchen Date: Fri, 28 Aug 2026 15:28:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E7=94=9F=E4=BA=A7=E6=80=BB?= =?UTF-8?q?=E5=A4=A9=E6=95=B0=E8=BF=94=E5=9B=9E=E8=87=AA=E7=84=B6=E5=A4=A9?= =?UTF-8?q?+=E5=B7=A5=E4=BD=9C=E6=97=A5=E4=B8=A4=E4=B8=AA=E5=8F=A3?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProductResponse 加 production_days(自然天)/production_days_workdays(工作日) - get_all_products 计算:自然天自创建至今,工作日用 working_duration_hours 排除周末/节假日 - FlowDevice 加 total_days/total_workdays(设备生产总天数,自最早介入至今) --- backend/app/schemas/product.py | 3 +++ backend/app/services/analytics_service.py | 20 ++++++++++++++++++++ backend/app/services/product_service.py | 23 +++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py index ac90336..020dbcd 100644 --- a/backend/app/schemas/product.py +++ b/backend/app/schemas/product.py @@ -63,6 +63,9 @@ class ProductResponse(BaseModel): latest_record_assignee_name: str | None = None # 🔧 当前人滞留时长 — 活跃任务(WIP/PENDING)最早接手时间到现在的时长(小时) active_duration_hours: float | None = None + # 🔧 生产总天数(自创建至今) + production_days: int = 0 # 自然天 + production_days_workdays: int = 0 # 工作日(排除周末/节假日) model_config = {"from_attributes": True} diff --git a/backend/app/services/analytics_service.py b/backend/app/services/analytics_service.py index 0184bbe..a64a45e 100644 --- a/backend/app/services/analytics_service.py +++ b/backend/app/services/analytics_service.py @@ -54,6 +54,8 @@ class FlowDevice(BaseModel): spec_model: str lead_time: float # 设备生命周期总时长(小时)= max_end - min_start started_at: str # 设备最早介入时间(T0),格式 MM-DD HH:mm + total_days: int = 0 # 设备生产总天数(自然天,自最早介入至今) + total_workdays: int = 0 # 设备生产总天数(工作日,排除周末/节假日) class FlowSeries(BaseModel): @@ -394,12 +396,30 @@ async def get_flow_compare( idx: round((max_end_by_device[idx] - t0_by_device[idx]).total_seconds() / 3600, 1) for idx in t0_by_device } + # 🔧 设备生产总天数(自然天 + 工作日,自最早介入至今) + import math + from app.core.time_utils import to_beijing as _tb, working_duration_hours as _wdh + from app.models.holiday import Holiday as _Holiday + hres = await db.execute(select(_Holiday.day)) + _holidays = {r[0] for r in hres} + + def _total_days(t0): + t0_bj = _tb(t0) + if not t0_bj: + return 1, 1 + natural = max(1, math.ceil((now - t0_bj).total_seconds() / 86400)) + workdays = max(1, math.ceil(_wdh(t0_bj, now, _holidays) / 24)) + return natural, workdays + + days_map = {i: _total_days(t0_by_device[i]) for i in t0_by_device} devices = [ FlowDevice( product_sn=d.product_sn, external_serial=d.external_serial, material_name=d.material_name, spec_model=d.spec_model, lead_time=lead_time_by_index.get(i, 0.0), started_at=t0_by_device[i].strftime("%m-%d %H:%M") if i in t0_by_device else "", + total_days=days_map.get(i, (1, 1))[0], + total_workdays=days_map.get(i, (1, 1))[1], ) for i, d in enumerate(devices) ] diff --git a/backend/app/services/product_service.py b/backend/app/services/product_service.py index c2cb662..d0aa329 100644 --- a/backend/app/services/product_service.py +++ b/backend/app/services/product_service.py @@ -569,6 +569,27 @@ async def get_all_products( start_bj = to_beijing(start) # 🚀 naive 按 UTC 转北京时间(修复多算8小时) active_duration_map[row[0]] = working_duration_hours(start_bj, now, holidays) + # 🔧 生产总天数(自然天 + 工作日):自创建至今 + import math + from app.core.time_utils import to_beijing as _tb, working_duration_hours as _wdh + from app.models.holiday import Holiday as _Holiday + hres2 = await db.execute(select(_Holiday.day)) + holidays2 = {r[0] for r in hres2} + now2 = get_beijing_time() + + def _prod_days(created_at): + created = _tb(created_at) + if not created: + return 1, 1 + natural = max(1, math.ceil((now2 - created).total_seconds() / 86400)) + work_hours = _wdh(created, now2, holidays2) + workdays = max(1, math.ceil(work_hours / 24)) + return natural, workdays + + production_days_map: dict = {} + for _p in products: + production_days_map[_p.id] = _prod_days(_p.created_at) + return [ ProductResponse( id=p.id, @@ -604,6 +625,8 @@ async def get_all_products( if latest_record_map.get(p.id, (None, None, False, None))[3] else None ), active_duration_hours=active_duration_map.get(p.id), + production_days=production_days_map.get(p.id, (1, 1))[0], + production_days_workdays=production_days_map.get(p.id, (1, 1))[1], ) for p in products ]