diff --git a/backend/app/api/v1/endpoints/analytics.py b/backend/app/api/v1/endpoints/analytics.py index 24a917c..b51506d 100644 --- a/backend/app/api/v1/endpoints/analytics.py +++ b/backend/app/api/v1/endpoints/analytics.py @@ -40,12 +40,16 @@ async def flow_compare( product_sns: str | None = Query(None, description="身份证,逗号分隔"), spec_models: str | None = Query(None, description="规格型号,逗号分隔(无 product_sns 时按型号取最近设备)"), mode: str = Query("natural", description="时间口径: natural(自然小时) / workdays(工作小时,排除周末节假日)"), + since: str | None = Query(None, description="起始日期 ISO"), + until: str | None = Query(None, description="截止日期 ISO"), db: AsyncSession = Depends(get_db), ): """设备流转对比 — 每台设备各操作人耗时(堆叠柱状,按人堆叠,识别瓶颈)。""" sns = [s.strip() for s in product_sns.split(",") if s.strip()] if product_sns else None specs = [s.strip() for s in spec_models.split(",") if s.strip()] if spec_models else None - return await get_flow_compare(db, product_sns=sns, spec_models=specs, mode=mode) + since_dt = datetime.fromisoformat(since) if since else None + until_dt = datetime.fromisoformat(until) if until else None + return await get_flow_compare(db, product_sns=sns, spec_models=specs, mode=mode, since=since_dt, until=until_dt) @router.get("/options", response_model=AnalyticsOptions) diff --git a/backend/app/services/analytics_service.py b/backend/app/services/analytics_service.py index c7e218f..dbcb53a 100644 --- a/backend/app/services/analytics_service.py +++ b/backend/app/services/analytics_service.py @@ -285,6 +285,8 @@ async def get_flow_compare( product_sns: list[str] | None = None, spec_models: list[str] | None = None, mode: str = "natural", + since: datetime | None = None, + until: datetime | None = None, ) -> FlowResponse: """ 查询每台设备上各操作人的任务时间区间,拼装为「生命周期时间轴」区间图: @@ -296,7 +298,11 @@ async def get_flow_compare( - 仅传 spec_models:这些型号下最近有流转的 20 台设备; - 都未传:返回空。 - data 每项 = [device_index, start_offset, end_offset, task_name, duration](单位小时)。 + data 每项 = [device_index, task_name, is_main, + start_nat, end_nat, start_work, end_work, + duration_total(自然), duration_work(工作日)](单位小时)。 + 前端按 isWorkday 切换选取自然/工作日偏移,即时重绘无需重新请求。 + mode 参数保留兼容(历史调用),不再影响返回内容。 """ from app.models.task import Task, TASK_STATUS_WIP, TASK_STATUS_PENDING, TASK_STATUS_COMPLETED from app.models.product import Product @@ -382,6 +388,11 @@ async def get_flow_compare( end = _to_bj(completed_at) if completed_at else now if start is None: continue + # 时间筛选:任务区间与 [since, until] 有交集 + if since is not None and end is not None and end < since: + continue + if until is not None and start is not None and start > until: + continue is_main = 0 if task_type == "SPAWN" else 1 intervals.append({ "idx": idx, @@ -430,24 +441,23 @@ async def get_flow_compare( ] # ── 按人分组,转为相对 T0 的小时偏移区间 ── + # 每项同时携带自然天与工作日两套偏移/时长,供前端按 isWorkday 即时切换: + # [idx, task_name, is_main, start_nat, end_nat, start_work, end_work, dur_total, dur_work] + from app.core.time_utils import 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} by_assignee: dict[str, list[list]] = {} - # 工作日模式:把时间偏移换算为「排除周末/节假日的工作小时」,压缩休息日 - if mode == "workdays": - from app.core.time_utils import 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} for it in intervals: t0 = t0_by_device[it["idx"]] - if mode == "workdays": - start_offset = _wdh(t0, it["start"], _holidays) - end_offset = _wdh(t0, it["end"], _holidays) - else: - start_offset = round((it["start"] - t0).total_seconds() / 3600, 1) - end_offset = round((it["end"] - t0).total_seconds() / 3600, 1) - duration = round(end_offset - start_offset, 1) + start_nat = round((it["start"] - t0).total_seconds() / 3600, 1) + end_nat = round((it["end"] - t0).total_seconds() / 3600, 1) + start_work = round(_wdh(t0, it["start"], _holidays), 1) + end_work = round(_wdh(t0, it["end"], _holidays), 1) by_assignee.setdefault(it["assignee_id"], []).append( - [it["idx"], start_offset, end_offset, it["task_name"], duration, it["is_main"]] + [it["idx"], it["task_name"], it["is_main"], + start_nat, end_nat, start_work, end_work, + round(end_nat - start_nat, 1), round(end_work - start_work, 1)] ) # 翻译人员姓名