feat(backend): 新增 /dashboard/user-operations 人员操作统计接口

- add_task_record 补 action_type=record 日志,使上传备注可统计到人
- get_user_operations 按人聚合 接收(receive)/转交(complete)/上传备注(record) 次数,时间筛选,中文名映射
- 复用 get_display_names,不新增数据库字段
This commit is contained in:
2026-08-28 13:14:09 +08:00
parent dd14185ab4
commit b85188e625
3 changed files with 94 additions and 0 deletions

View File

@ -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),

View File

@ -63,6 +63,15 @@ class RejectedTask(BaseModel):
rejected_at: str | None # 驳回时间 ISOBEIJING_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
# ============================================================
# 人员负载(按人聚合在制品设备 — 独立「人员看板」)
# ============================================================

View File

@ -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