feat(backend): 新增 /dashboard/user-operations/detail 操作明细下钻接口

- 按人+操作类型(receive/transfer/record)查询明细:任务/产品/备注/时间
- record 明细与该人统计口径一致(名下任务手动备注,排除系统自动)
This commit is contained in:
2026-08-28 13:36:23 +08:00
parent 554608b948
commit 7205de369a
2 changed files with 109 additions and 0 deletions

View File

@ -72,6 +72,14 @@ class UserOperation(BaseModel):
total: int = 0 # 总操作次数
class OperationDetail(BaseModel):
task_name: str # 任务/工序名
product_sn: str # 产品身份证
material_name: str # 设备名称
remark: str | None # 备注/说明
time: str # 操作时间 ISOBEIJING_TZ
class PersonDevice(BaseModel):
product_id: str
serial_number: str # 16位HEX身份证
@ -583,6 +591,90 @@ async def get_user_operations(
return items
# ============================================================
# 人员操作明细(点击数字下钻 — 接收/转交/上传备注)
# ============================================================
async def get_user_operation_detail(
db: AsyncSession,
user_id: str,
action_type: str,
since: datetime | None = None,
until: datetime | None = None,
) -> list[OperationDetail]:
"""查询某人在指定时段内的某类操作明细。
action_type:
- receive: 接收task_logs.action_type='receive'
- transfer: 转交task_logs.action_type='complete'
- record: 上传备注(该人名下任务的手动备注,排除系统自动生成的)
"""
from app.models.task_log import TaskLog
from app.models.task import Task, TaskRecord
from app.models.product import Product
from app.core.time_utils import BEIJING_TZ
def _to_iso(dt) -> str:
if not dt:
return ""
if dt.tzinfo is None:
from datetime import timezone as dt_timezone
dt = dt.replace(tzinfo=dt_timezone.utc).astimezone(BEIJING_TZ)
else:
dt = dt.astimezone(BEIJING_TZ)
return dt.isoformat()
items: list[OperationDetail] = []
if action_type in ("receive", "transfer"):
act = "receive" if action_type == "receive" else "complete"
stmt = (
select(TaskLog.created_at, Task.task_name, Product.serial_number, Product.material_name, TaskLog.remark)
.join(Task, TaskLog.task_id == Task.id)
.join(Product, Task.product_id == Product.id)
.where(TaskLog.operator_id == user_id, TaskLog.action_type == act)
.order_by(TaskLog.created_at.desc())
)
if since:
stmt = stmt.where(TaskLog.created_at >= since)
if until:
stmt = stmt.where(TaskLog.created_at <= until)
rows = (await db.execute(stmt)).all()
for r in rows:
items.append(OperationDetail(
task_name=r[1] or "",
product_sn=r[2] or "",
material_name=r[3] or "",
remark=r[4] or "",
time=_to_iso(r[0]),
))
elif action_type == "record":
stmt = (
select(TaskRecord.created_at, Task.task_name, Product.serial_number, Product.material_name, TaskRecord.remark)
.join(Task, TaskRecord.task_id == Task.id)
.join(Product, Task.product_id == Product.id)
.where(
Task.assignee_id == user_id,
or_(TaskRecord.remark.is_(None), ~TaskRecord.remark.like("[%")),
)
.order_by(TaskRecord.created_at.desc())
)
if since:
stmt = stmt.where(TaskRecord.created_at >= since)
if until:
stmt = stmt.where(TaskRecord.created_at <= until)
rows = (await db.execute(stmt)).all()
for r in rows:
items.append(OperationDetail(
task_name=r[1] or "",
product_sn=r[2] or "",
material_name=r[3] or "",
remark=r[4] or "",
time=_to_iso(r[0]),
))
return items
# ============================================================
# 人员负载(按人聚合在制品设备 — 独立「人员看板」)
# ============================================================