From b85188e62526733e6581874b4980dff28b01036d Mon Sep 17 00:00:00 2001 From: duxingchen Date: Fri, 28 Aug 2026 13:14:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E6=96=B0=E5=A2=9E=20/dashboar?= =?UTF-8?q?d/user-operations=20=E4=BA=BA=E5=91=98=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add_task_record 补 action_type=record 日志,使上传备注可统计到人 - get_user_operations 按人聚合 接收(receive)/转交(complete)/上传备注(record) 次数,时间筛选,中文名映射 - 复用 get_display_names,不新增数据库字段 --- backend/app/api/v1/endpoints/dashboard.py | 13 +++++ backend/app/services/dashboard_service.py | 71 +++++++++++++++++++++++ backend/app/services/task_service.py | 10 ++++ 3 files changed, 94 insertions(+) diff --git a/backend/app/api/v1/endpoints/dashboard.py b/backend/app/api/v1/endpoints/dashboard.py index 05de23d..1a6e73a 100644 --- a/backend/app/api/v1/endpoints/dashboard.py +++ b/backend/app/api/v1/endpoints/dashboard.py @@ -10,6 +10,7 @@ from app.services.dashboard_service import ( get_wip_tasks, WipTask, get_completed_tasks, CompletedTask, get_rejected_tasks, RejectedTask, + get_user_operations, UserOperation, get_people_workload, PersonWorkload, get_people_history, PersonHistoryRecord, search_product_messages, ProductMessageList, @@ -70,6 +71,18 @@ async def rejected_tasks( return await get_rejected_tasks(db, since=since_dt, until=until_dt, limit=limit) +@router.get("/user-operations", response_model=list[UserOperation]) +async def user_operations( + since: str | None = Query(None, description="起始日期 ISO 如 2026-08-01T00:00:00"), + until: str | None = Query(None, description="截止日期 ISO"), + db: AsyncSession = Depends(get_db), +): + """人员操作统计 — 按人聚合 接收/转交/上传备注 次数,按时段过滤""" + since_dt = datetime.fromisoformat(since) if since else None + until_dt = datetime.fromisoformat(until) if until else None + return await get_user_operations(db, since=since_dt, until=until_dt) + + @router.get("/people-workload", response_model=list[PersonWorkload]) async def people_workload( db: AsyncSession = Depends(get_db), diff --git a/backend/app/services/dashboard_service.py b/backend/app/services/dashboard_service.py index ca6daa1..e8ac73e 100644 --- a/backend/app/services/dashboard_service.py +++ b/backend/app/services/dashboard_service.py @@ -63,6 +63,15 @@ class RejectedTask(BaseModel): rejected_at: str | None # 驳回时间 ISO(BEIJING_TZ) +class UserOperation(BaseModel): + user_id: str # 登录名 username + user_name: str # 中文姓名 + receive_count: int = 0 # 接收次数 + transfer_count: int = 0 # 转交次数(action=complete) + record_count: int = 0 # 上传备注次数(action=record) + total: int = 0 # 总操作次数 + + class PersonDevice(BaseModel): product_id: str serial_number: str # 16位HEX身份证 @@ -465,6 +474,68 @@ async def get_rejected_tasks( return items +# ============================================================ +# 人员操作统计(接收/转交/上传备注 — 按人聚合,时间可筛选) +# ============================================================ + +async def get_user_operations( + db: AsyncSession, + since: datetime | None = None, + until: datetime | None = None, +) -> list[UserOperation]: + """上帝视角 — 统计每个人员的操作次数(按时段过滤 task_logs)。 + + 操作口径: + - 接收: action_type='receive' + - 转交: action_type='complete'(完工并移交下一道工序) + - 上传备注: action_type='record'(工人手动追加的进度记录) + """ + from app.models.task_log import TaskLog + + rcv = func.count().filter(TaskLog.action_type == "receive") + cpl = func.count().filter(TaskLog.action_type == "complete") + rcd = func.count().filter(TaskLog.action_type == "record") + total_expr = rcv + cpl + rcd + + stmt = ( + select(TaskLog.operator_id, rcv.label("receive"), cpl.label("complete"), rcd.label("record")) + .where( + TaskLog.action_type.in_(["receive", "complete", "record"]), + TaskLog.operator_id.isnot(None), + ) + .group_by(TaskLog.operator_id) + .order_by(total_expr.desc()) + ) + if since: + stmt = stmt.where(TaskLog.created_at >= since) + if until: + stmt = stmt.where(TaskLog.created_at <= until) + + result = await db.execute(stmt) + rows = result.all() + + ids = [r[0] for r in rows if r[0]] + name_map: dict[str, str] = {} + if ids: + from app.services.mom_cache import get_display_names + name_map = get_display_names(ids) + + items: list[UserOperation] = [] + for row in rows: + receive = row[1] or 0 + transfer = row[2] or 0 + record = row[3] or 0 + items.append(UserOperation( + user_id=row[0], + user_name=name_map.get(row[0], row[0]), + receive_count=receive, + transfer_count=transfer, + record_count=record, + total=receive + transfer + record, + )) + return items + + # ============================================================ # 人员负载(按人聚合在制品设备 — 独立「人员看板」) # ============================================================ diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py index 06d662b..51b53c9 100644 --- a/backend/app/services/task_service.py +++ b/backend/app/services/task_service.py @@ -1049,6 +1049,16 @@ async def add_task_record( db.add(record) await db.flush() + # 🔧 操作日志:记录一次「上传备注」操作(供人员操作统计) + if current_user: + from app.models.task_log import TaskLog + db.add(TaskLog( + task_id=task_id, + operator_id=current_user.get("username") or current_user.get("sub") or "", + action_type="record", + remark=f"上传备注: {(data.remark or '')[:100]}", + )) + # 🚀 留言通知:给任务当前负责人发送提醒(不给自己发) if ( current_user