fix(驳回/返工): 抽屉与卡片数字口径一致,同时展示已驳回+返工任务

- 根因:卡片数字含返工任务(tasks_rework 实时快照),但抽屉只列已驳回 → 点开为空
- get_rejected_tasks 扩展返回两类(kind=rejected 按时间过滤 / kind=rework 实时不过滤)
- 前端抽屉按「已驳回」「返工任务」分组展示,数字与明细一一对应
This commit is contained in:
2026-08-28 11:33:54 +08:00
parent 1e6adeb576
commit 90f5718e68
3 changed files with 147 additions and 68 deletions

View File

@ -50,15 +50,17 @@ class CompletedTask(BaseModel):
class RejectedTask(BaseModel):
task_id: str
task_name: str # 被驳回的工序
kind: str = "rejected" # "rejected"(已驳回) | "rework"(返工任务)
task_name: str # 工序
product_sn: str # 16位HEX身份证
external_serial: str | None # 业务序列号
material_name: str # 产品名称(物料名称)
spec_model: str # 规格型号
rejected_by: str # 驳回人中文姓名
rejected_by: str = "" # 驳回人中文姓名rework 可能无)
rework_assignee: str # 返工任务负责人中文姓名(转交给谁返工)
reject_reason: str | None # 驳回原因
rejected_at: str # 驳回时间 ISOBEIJING_TZ
status: str | None = None # 当前状态rework 任务用PENDING/WIP/COMPLETED
rejected_at: str | None # 驳回时间 ISOBEIJING_TZ
class PersonDevice(BaseModel):
@ -306,10 +308,13 @@ async def get_rejected_tasks(
until: datetime | None = None,
limit: int = 200,
) -> list[RejectedTask]:
"""按时段查询驳回任务明细(上帝视角),用于「驳回/返工」卡片下钻。
"""查询驳回/返工任务明细(上帝视角),用于「驳回/返工」卡片下钻。
每条返回:设备信息、被驳回工序、驳回人、返工任务负责人(转交给谁)、
驳回原因与时间。返工负责人追溯逻辑与 task_service.reject_task 一致:
返回两类(与卡片数字 tasks_rejected + tasks_rework 口径一致):
- kind="rejected":被驳回任务,按 completed_at 时间过滤
- kind="rework"返工任务is_rework=True 且非驳回状态),实时快照不过滤时间
返工负责人追溯逻辑与 task_service.reject_task 一致:
优先最早 create log 的 operator → 兜底父任务负责人 → 兜底自身。
"""
from app.models.task import Task, TASK_STATUS_REJECTED
@ -317,6 +322,17 @@ async def get_rejected_tasks(
from app.models.product import Product
from app.core.time_utils import BEIJING_TZ
def _to_bj_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()
# ── 1. 已驳回任务(按时间过滤)──
stmt = (
select(Task, Product.serial_number, Product.external_serial, Product.material_name, Product.spec_model)
.join(Product, Task.product_id == Product.id)
@ -327,14 +343,28 @@ async def get_rejected_tasks(
if until:
stmt = stmt.where(Task.completed_at <= until)
stmt = stmt.order_by(Task.completed_at.desc()).limit(limit)
result = await db.execute(stmt)
rows = result.all()
rejected_rows = (await db.execute(stmt)).all()
task_ids = [t.id for t, *_ in rows]
# ── 2. 返工任务is_rework=True 且当前非驳回状态,实时不过滤时间)──
rework_stmt = (
select(Task, Product.serial_number, Product.external_serial, Product.material_name, Product.spec_model)
.join(Product, Task.product_id == Product.id)
.where(Task.is_rework.is_(True), Task.status != TASK_STATUS_REJECTED)
.order_by(Task.created_at.desc())
.limit(limit)
)
rework_rows = (await db.execute(rework_stmt)).all()
# ── 批量取每个任务的驳回人(最新一条 reject log 的 operator_id──
# 合并rejected 在前rework 在后
all_rows: list[tuple[str, object]] = [
*[("rejected", t) for t in rejected_rows],
*[("rework", t) for t in rework_rows],
]
# ── 已驳回任务:批量取驳回人 / 最早 create log / 父任务负责人 ──
rejected_ids = [t.id for t, *_ in rejected_rows]
reject_op: dict = {}
if task_ids:
if rejected_ids:
sub = (
select(
TaskLog.task_id, TaskLog.operator_id,
@ -343,16 +373,15 @@ async def get_rejected_tasks(
order_by=TaskLog.created_at.desc(),
).label("rn"),
)
.where(TaskLog.task_id.in_(task_ids), TaskLog.action_type == "reject")
.where(TaskLog.task_id.in_(rejected_ids), TaskLog.action_type == "reject")
).subquery()
r = await db.execute(select(sub.c.task_id, sub.c.operator_id).where(sub.c.rn == 1))
for row in r:
if row[1]:
reject_op[row[0]] = row[1]
# ── 批量取每个任务最早 create log 的 operator返工负责人的第一优先──
create_op: dict = {}
if task_ids:
if rejected_ids:
sub = (
select(
TaskLog.task_id, TaskLog.operator_id,
@ -361,16 +390,15 @@ async def get_rejected_tasks(
order_by=TaskLog.created_at.asc(),
).label("rn"),
)
.where(TaskLog.task_id.in_(task_ids), TaskLog.action_type == "create")
.where(TaskLog.task_id.in_(rejected_ids), TaskLog.action_type == "create")
).subquery()
r = await db.execute(select(sub.c.task_id, sub.c.operator_id).where(sub.c.rn == 1))
for row in r:
if row[1]:
create_op[row[0]] = row[1]
# ── 兜底:父任务负责人(对没有 create log 的任务)──
parent_assignee: dict = {}
parent_ids = [t.parent_task_id for t, *_ in rows if t.parent_task_id and t.id not in create_op]
parent_ids = [t.parent_task_id for t, *_ in rejected_rows if t.parent_task_id and t.id not in create_op]
if parent_ids:
r = await db.execute(
select(Task.id, Task.assignee_id).where(Task.id.in_(parent_ids))
@ -379,13 +407,14 @@ async def get_rejected_tasks(
if row[1]:
parent_assignee[row[0]] = row[1]
# ── 中文名映射(驳回人 + 返工负责人 + 任务负责人 一次批量查)──
# ── 中文名映射(驳回人 + 返工负责人 一次批量查)──
raw_ids: set[str] = set()
for t, *_ in rows:
raw_ids.add(reject_op.get(t.id) or "")
raw_ids.add(create_op.get(t.id) or "")
if t.id not in create_op and t.parent_task_id:
raw_ids.add(parent_assignee.get(t.parent_task_id) or "")
for kind, (t, *_row) in all_rows:
if kind == "rejected":
raw_ids.add(reject_op.get(t.id) or "")
raw_ids.add(create_op.get(t.id) or "")
if t.id not in create_op and t.parent_task_id:
raw_ids.add(parent_assignee.get(t.parent_task_id) or "")
raw_ids.add(t.assignee_id or "")
raw_ids.discard("")
name_map: dict[str, str] = {}
@ -394,36 +423,45 @@ async def get_rejected_tasks(
name_map = get_display_names(list(raw_ids))
items: list[RejectedTask] = []
for task, sn, ext, mat, spec in rows:
# 复刻 reject_task 追溯逻辑create op → 父任务负责人 → 自身
rework_id = create_op.get(task.id)
if not rework_id and task.parent_task_id:
rework_id = parent_assignee.get(task.parent_task_id)
if not rework_id:
rework_id = task.assignee_id
rejected_by_id = reject_op.get(task.id) or task.assignee_id
t = task.completed_at
if t:
if t.tzinfo is None:
from datetime import timezone as dt_timezone
t = t.replace(tzinfo=dt_timezone.utc).astimezone(BEIJING_TZ)
else:
t = t.astimezone(BEIJING_TZ)
time_str = t.isoformat() if t else ""
items.append(RejectedTask(
task_id=str(task.id),
task_name=task.task_name,
product_sn=sn or "",
external_serial=ext or None,
material_name=mat or "",
spec_model=spec or "",
rejected_by=name_map.get(rejected_by_id or "", rejected_by_id or ""),
rework_assignee=name_map.get(rework_id or "", rework_id or ""),
reject_reason=task.reject_reason,
rejected_at=time_str,
))
for kind, (task, sn, ext, mat, spec) in all_rows:
if kind == "rejected":
# 复刻 reject_task 追溯逻辑:create op → 父任务负责人 → 自身
rework_id = create_op.get(task.id)
if not rework_id and task.parent_task_id:
rework_id = parent_assignee.get(task.parent_task_id)
if not rework_id:
rework_id = task.assignee_id
rejected_by_id = reject_op.get(task.id) or task.assignee_id
items.append(RejectedTask(
task_id=str(task.id),
kind="rejected",
task_name=task.task_name,
product_sn=sn or "",
external_serial=ext or None,
material_name=mat or "",
spec_model=spec or "",
rejected_by=name_map.get(rejected_by_id or "", rejected_by_id or ""),
rework_assignee=name_map.get(rework_id or "", rework_id or ""),
reject_reason=task.reject_reason,
status=task.status,
rejected_at=_to_bj_iso(task.completed_at),
))
else:
# 返工任务:负责人即其 assignee
items.append(RejectedTask(
task_id=str(task.id),
kind="rework",
task_name=task.task_name,
product_sn=sn or "",
external_serial=ext or None,
material_name=mat or "",
spec_model=spec or "",
rejected_by="",
rework_assignee=name_map.get(task.assignee_id or "", task.assignee_id or ""),
reject_reason=None,
status=task.status,
rejected_at=_to_bj_iso(task.received_at or task.created_at),
))
return items