From 807df56c3a80fb937e516dc126caa5361bd1052e Mon Sep 17 00:00:00 2001 From: duxingchen Date: Fri, 14 Aug 2026 14:30:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(analytics):=20=E8=A7=84=E6=A0=BC=E5=9E=8B?= =?UTF-8?q?=E5=8F=B7=E5=B8=A6=E7=89=A9=E6=96=99=E5=90=8D=20+=20=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=AD=97=E5=85=B8=EF=BC=88/options=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=A5=91=E7=BA=A6=E6=89=A9=E5=B1=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 SpecModelOption/DeviceOption schema - get_analytics_options 返回规格型号的物料名(同型号取首个非空名)+ 关联设备列表 - 前端同步 SpecModelOption/DeviceOption 类型 --- backend/app/services/analytics_service.py | 87 ++++++++++++++++++++--- frontend/src/services/analyticsApi.ts | 15 +++- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/backend/app/services/analytics_service.py b/backend/app/services/analytics_service.py index e9a35a3..1f31262 100644 --- a/backend/app/services/analytics_service.py +++ b/backend/app/services/analytics_service.py @@ -70,9 +70,22 @@ class AssigneeOption(BaseModel): name: str +class SpecModelOption(BaseModel): + spec_model: str + material_name: str + + +class DeviceOption(BaseModel): + product_sn: str + external_serial: str | None + material_name: str + spec_model: str + + class AnalyticsOptions(BaseModel): assignees: list[AssigneeOption] - spec_models: list[str] + spec_models: list[SpecModelOption] + devices: list[DeviceOption] class DeviceRecord(BaseModel): @@ -478,11 +491,12 @@ async def get_analytics_options( ) -> AnalyticsOptions: """返回筛选栏选项,支持动态联动: - 传入 spec_models:只返回碰过这些型号的人; - - 传入 assignee_ids:只返回这些人处理过的型号。""" + - 传入 assignee_ids:只返回这些人处理过的型号; + - devices:根据筛选条件返回关联设备(无筛选则返回最近流转的设备)。""" from app.models.task import Task from app.models.product import Product - # 负责人:从 tasks 去重(可选按 spec_models 精确过滤) + # ── 负责人:从 tasks 去重(可选按 spec_models 过滤) ── if spec_models: assignee_stmt = ( select(Task.assignee_id) @@ -512,23 +526,78 @@ async def get_analytics_options( for aid in distinct_assignees ] - # 规格型号:从 products 去重(可选按 assignee_ids 过滤) + # ── 规格型号 + 物料名(可选按 assignee_ids 过滤;同型号取首个非空物料名) ── if assignee_ids: spec_stmt = ( - select(Product.spec_model) + select(Product.spec_model, Product.material_name) .join(Task, Task.product_id == Product.id) .where( Product.spec_model.isnot(None), func.trim(Product.spec_model) != "", Task.assignee_id.in_(assignee_ids), ) + .distinct() ) else: spec_stmt = ( - select(Product.spec_model) + select(Product.spec_model, Product.material_name) .where(Product.spec_model.isnot(None), func.trim(Product.spec_model) != "") + .distinct() ) - spec_rows = (await db.execute(spec_stmt.distinct())).all() - spec_models = sorted({r[0] for r in spec_rows if r[0]}) + spec_rows = (await db.execute(spec_stmt)).all() + spec_map: dict[str, str] = {} + for row in spec_rows: + sm = row[0] + mn = (row[1] or "").strip() + if sm not in spec_map: + spec_map[sm] = mn + elif mn and not spec_map[sm]: + spec_map[sm] = mn + spec_model_opts = [ + SpecModelOption(spec_model=sm, material_name=spec_map[sm]) + for sm in sorted(spec_map.keys()) + ] - return AnalyticsOptions(assignees=assignees, spec_models=spec_models) + # ── 设备字典(可选按 assignee_ids / spec_models 过滤;无筛选返回最近流转 100 台) ── + if assignee_ids or spec_models: + device_stmt = select( + Product.serial_number, Product.external_serial, + Product.material_name, Product.spec_model, + ) + if assignee_ids: + device_stmt = device_stmt.join( + Task, Task.product_id == Product.id + ).where(Task.assignee_id.in_(assignee_ids)) + if spec_models: + device_stmt = device_stmt.where(Product.spec_model.in_(spec_models)) + device_stmt = device_stmt.distinct() + else: + latest_subq = ( + select( + Task.product_id, + func.max(func.coalesce(Task.received_at, Task.created_at)).label("latest"), + ) + .group_by(Task.product_id) + .subquery() + ) + device_stmt = ( + select( + Product.serial_number, Product.external_serial, + Product.material_name, Product.spec_model, + ) + .join(latest_subq, latest_subq.c.product_id == Product.id) + .order_by(latest_subq.c.latest.desc()) + .limit(100) + ) + device_rows = (await db.execute(device_stmt)).all() + devices = [ + DeviceOption( + product_sn=r[0] or "", + external_serial=r[1] or None, + material_name=r[2] or "", + spec_model=r[3] or "", + ) + for r in device_rows + ] + + return AnalyticsOptions(assignees=assignees, spec_models=spec_model_opts, devices=devices) diff --git a/frontend/src/services/analyticsApi.ts b/frontend/src/services/analyticsApi.ts index 104a1ee..1a5b13e 100644 --- a/frontend/src/services/analyticsApi.ts +++ b/frontend/src/services/analyticsApi.ts @@ -71,9 +71,22 @@ export interface AssigneeOption { name: string; } +export interface SpecModelOption { + spec_model: string; + material_name: string; +} + +export interface DeviceOption { + product_sn: string; + external_serial: string | null; + material_name: string; + spec_model: string; +} + export interface AnalyticsOptions { assignees: AssigneeOption[]; - spec_models: string[]; + spec_models: SpecModelOption[]; + devices: DeviceOption[]; } // ─── 查询参数 ──────────────────────────────────────────────