From 57884d7baacd4b936758aeb2945d6d0ccc8d74e9 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Thu, 6 Aug 2026 11:39:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=92=A4=E5=9B=9E=E8=BD=AC=E4=BA=A4:=20recall?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=88=A0=E9=99=A4=E5=AD=90=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E7=88=B6WIP=20+=20=E5=89=8D=E7=AB=AFcanRecal?= =?UTF-8?q?l=E5=88=A4=E6=96=AD=20+=20=E7=BA=A2=E8=89=B2=E6=92=A4=E5=9B=9E?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/endpoints/tasks.py | 17 ++++++++ backend/app/services/task_service.py | 42 +++++++++++++++++++ .../pages/scan/components/WorkspaceArea.vue | 6 +++ track-uniapp/src/pages/scan/detail.vue | 3 ++ 4 files changed, 68 insertions(+) diff --git a/backend/app/api/v1/endpoints/tasks.py b/backend/app/api/v1/endpoints/tasks.py index e5065fc..8394f35 100644 --- a/backend/app/api/v1/endpoints/tasks.py +++ b/backend/app/api/v1/endpoints/tasks.py @@ -121,6 +121,23 @@ async def end_task_endpoint( return await task_service.end_task(db, uuid.UUID(task_id), operator_id) +# ============================================================ +# 核心业务 0.3:撤回转交 (PENDING → 删除 + 恢复父任务) +# ============================================================ + +@router.post("/{task_id}/recall", response_model=TaskResponse) +async def recall_task_endpoint( + task_id: str, + operator_id: str | None = Query(None), + db: AsyncSession = Depends(get_db), +): + """ + **撤回转交:删除 PENDING 子任务,恢复父任务为 WIP。** + 适用场景:转交后发现选错人,在对方接收前撤回。 + """ + return await task_service.recall_task(db, uuid.UUID(task_id), operator_id) + + # ============================================================ # 核心业务 0.5:并发派发协助分支 (WIP → 不改变状态,创建子任务) # ============================================================ diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py index 8c1d857..5e29e39 100644 --- a/backend/app/services/task_service.py +++ b/backend/app/services/task_service.py @@ -306,6 +306,48 @@ async def end_task( return _to_response(task) +# ============================================================ +# 核心业务 0.3:撤回转交 +# ============================================================ + +async def recall_task( + db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None +) -> TaskResponse: + """撤回 PENDING 转交任务,恢复父任务为 WIP。""" + task = await _get_task_or_404(db, task_id) + + if task.status != TASK_STATUS_PENDING: + raise HTTPException(status_code=409, detail="只有待接收(PENDING)的任务可以撤回") + + if not task.parent_task_id: + raise HTTPException(status_code=400, detail="顶层任务无法撤回") + + parent = await _get_task_or_404(db, task.parent_task_id) + if parent.status != TASK_STATUS_COMPLETED: + raise HTTPException(status_code=409, detail="父任务状态异常,无法撤回") + + # 恢复父任务 + parent.status = TASK_STATUS_WIP + parent.completed_at = None + + # 更新产品位置到父任务负责人 + product_result = await db.execute(select(Product).where(Product.id == parent.product_id)) + product = product_result.scalar_one_or_none() + if product and parent.assignee_id: + product.current_location_id = parent.assignee_id + product.overall_status = parent.task_name + + # 删除 PENDING 子任务 + await db.delete(task) + + await _create_task_log(db, parent.id, action_type="recall", operator_id=operator_id, + remark=f"撤回转交「{task.task_name}」→ {task.assignee_id},恢复父任务为 WIP") + + await db.commit() + await db.refresh(parent) + return _to_response(parent) + + # ============================================================ # 核心业务 0.5:派发协助分支(不改变父任务状态) # ============================================================ diff --git a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue index a7faf91..53fabc4 100644 --- a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue +++ b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue @@ -90,6 +90,10 @@ + + + 🔒 非当前任务指派人,仅可查看 @@ -132,6 +136,7 @@ export default { }, lockedTask() { return this.focusTasks.find(t => t.id === this.lockedTaskId) || null; }, isAssignee() { if (!this.lockedTask) return false; return this.lockedTask.assignee_id == this.currentUserId || this.lockedTask.assignee_id == this.currentUsername; }, + canRecall() { if (!this.lockedTask || !this.lockedTask.parent_task_id) return false; const parent = this.taskMap[this.lockedTask.parent_task_id]; return parent && (parent.assignee_id == this.currentUserId || parent.assignee_id == this.currentUsername); }, branchLabelsMap() { const map = {}; if (!this.product || !this.product.task_tree) return map; @@ -223,4 +228,5 @@ export default { .footer-reject { background: #fce4ec; color: #dc2626; } .footer-end { background: #fef3c7; color: #b45309; } .footer-spawn { background: #ede9fe; color: #7c3aed; } +.footer-recall { background: #fef2f2; color: #dc2626; } diff --git a/track-uniapp/src/pages/scan/detail.vue b/track-uniapp/src/pages/scan/detail.vue index 78a96c6..1ac5199 100644 --- a/track-uniapp/src/pages/scan/detail.vue +++ b/track-uniapp/src/pages/scan/detail.vue @@ -223,6 +223,7 @@ export default { if (type === "record") { this.openRecordPopup(task); return; } if (type === "deleteRecord") { this.doDeleteRecord(record); return; } if (type === "end") { this.confirmEndBranch(task); return; } + if (type === "recall") { this.confirmRecall(task); return; } if (type === "transfer" || type === "spawn") { uni.showLoading({ title: "加载数据..." }); try { if (!this.userOptions.length) await this.loadUsers(); if (!this.processOptions.length) this.processOptions = ["🏭 入库 (virtual_warehouse)", ...TASK_NAME_OPTIONS]; } finally { uni.hideLoading(); } } this.actionPopup = { visible: true, type, task }; this.rejectReason = ""; this.receiveRemark = ""; this.receiveTaskName = ""; this.transferForm = { selectedUserId: "", isWarehouse: false, note: "" }; @@ -231,6 +232,8 @@ export default { handleViewRecords(task) { uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` }); }, async doDeleteRecord(record) { try { await request({ url: `/records/${record.id}`, method: "DELETE" }); uni.showToast({ title: "记录已删除", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} }, confirmEndBranch(task) { uni.showModal({ title: "结束分支", content: `确定结束「${task.task_name}」吗?`, success: (res) => { if (res.confirm) this.doEndBranch(task); } }); }, + confirmRecall(task) { uni.showModal({ title: "撤回转交", content: `确定撤回「${task.task_name}」吗?撤回后任务将回到您的手中。`, success: (res) => { if (res.confirm) this.doRecall(task); } }); }, + async doRecall(task) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/recall?operator_id=${encodeURIComponent(opId)}`); uni.showToast({ title: "已撤回", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} }, async doEndBranch(task) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/end?operator_id=${encodeURIComponent(opId)}`); uni.showToast({ title: "分支已结束", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} }, closeActionPopup() { this.actionPopup = { visible: false, type: "", task: null }; }, async doReceive() { this.actionLoading = true; try { const remark = this.receiveRemark.trim() || undefined; const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/receive?operator_id=${encodeURIComponent(opId)}`, { remark, task_name: this.receiveTaskName }); uni.showToast({ title: "已接收", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },