From 21211bd76090dc5aaa6b45ea654735ddf32da418 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 15 Aug 2026 13:48:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E6=8C=87=E6=B4=BE=E6=89=93=E6=89=8B=EF=BC=8C=E5=8F=AF=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E6=8C=87=E6=B4=BE=E4=BB=96=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后台可取消进行中订单的指派:订单回未分配,冻结押金全额退还,记录事件与审计 - 工单列表新增「取消指派」按钮(带确认弹窗),客服/管理员/运营可用 - 取消后可重新指派给其他打手 --- .../worker-platform/work-order-repo.ts | 90 +++++++++++++++++++ .../src/routes/admin/worker-platform.ts | 17 ++++ .../services/worker-platform/admin-service.ts | 38 ++++++++ .../pages/admin/panels/WorkOrdersPanel.tsx | 31 +++++++ .../src/services/admin/worker-platform.ts | 6 ++ 5 files changed, 182 insertions(+) diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index ab30ae4a..bdc6f752 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -1063,6 +1063,96 @@ export async function cancelWorkerWorkOrder(input: { }) } +export async function unassignWorkOrder(input: { + workOrderId: number + now: string +}): Promise<{ + order: WorkOrderRow | null + failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null +}> { + return withTransaction(async (client) => { + const currentResult = await client.query( + ` + SELECT * + FROM work_orders + WHERE id = $1 + FOR UPDATE + `, + [input.workOrderId], + ) + const workOrder = currentResult.rows[0] || null + if (!workOrder || workOrder.status !== 'in_progress') { + return { order: null, failureReason: 'work_order_not_in_progress' } + } + const workerId = Number(workOrder.assigned_worker_id || 0) + if (!workerId) { + return { order: null, failureReason: 'work_order_no_worker' } + } + + await ensureWorkerWalletWithClient(client, workerId, input.now) + const wallet = await getWorkerWalletWithClient(client, workerId) + const releaseAmount = Math.min( + await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId), + Number(wallet?.frozen_deposit_amount || 0), + ) + + await client.query( + ` + UPDATE work_orders + SET + status = 'unassigned', + assigned_worker_id = NULL, + assigned_at = NULL, + deadline_at = NULL, + updated_at = $1 + WHERE id = $2 + `, + [input.now, input.workOrderId], + ) + + if (releaseAmount > 0) { + const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount + const nextFrozen = Math.max( + 0, + Number(wallet?.frozen_deposit_amount || 0) - releaseAmount, + ) + await client.query( + ` + UPDATE worker_wallets + SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3 + WHERE worker_id = $4 + `, + [nextAvailable, nextFrozen, input.now, workerId], + ) + await client.query( + ` + INSERT INTO worker_wallet_ledgers ( + worker_id, ledger_type, amount, balance_after, frozen_after, + related_work_order_id, note, payload_json, created_at + ) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '后台取消指派退还押金', '{}'::jsonb, $6) + `, + [workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now], + ) + } + + await createWorkOrderEventWithClient(client, { + workOrderId: input.workOrderId, + actorType: 'admin', + actorId: '', + eventType: 'unassigned_by_admin', + fromStatus: 'in_progress', + toStatus: 'unassigned', + payloadJson: JSON.stringify({ workerId, releaseAmount }), + now: input.now, + }) + + return { + order: await getWorkOrderByIdWithClient(client, input.workOrderId), + failureReason: null, + } + }) +} + export async function countWorkerCancellationsSince( workerId: number | string, sinceIso: string, diff --git a/apps/backend/src/routes/admin/worker-platform.ts b/apps/backend/src/routes/admin/worker-platform.ts index 54e9b877..235dc59f 100644 --- a/apps/backend/src/routes/admin/worker-platform.ts +++ b/apps/backend/src/routes/admin/worker-platform.ts @@ -3,6 +3,7 @@ import { Router } from 'express' import { acceptAdminWorkOrder, assignAdminWorkOrderToWorker, + unassignAdminWorkOrder, creditAdminWorkerWallet, createAdminMockWorkOrder, deductAdminWorkOrderPendingDeposit, @@ -361,6 +362,22 @@ router.post( ), ) +router.post( + '/worker-platform/orders/:workOrderId/unassign', + requireAdminRoles(['admin', 'operator', 'support']), + createJsonHandler((req) => unassignAdminWorkOrder(String(req.params.workOrderId || '')), { + successMessage: '已取消指派', + errorMessage: '取消指派失败', + scope: '[admin/worker-platform/orders/:workOrderId/unassign]', + audit: (req, data) => ({ + action: 'work_order_worker_unassigned', + targetType: 'work_order', + targetId: String(req.params.workOrderId || ''), + data: data && typeof data === 'object' ? (data as Record) : {}, + }), + }), +) + router.delete( '/worker-platform/orders/:workOrderId', requireAdminRoles(['admin', 'operator']), diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 1a51f999..30ad23af 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -6,6 +6,7 @@ import { acceptWorkOrderAndSettle, addWorkerWalletCredit, assignWorkOrderToWorker, + unassignWorkOrder, countWorkerAcceptedOrders, countWorkerActiveOrders, countWorkCategoryUsages, @@ -1146,6 +1147,43 @@ export async function acceptAdminWorkOrder(workOrderId: number | string) { return { order: mapWorkOrderAdmin(updated || workOrder) } } +export async function unassignAdminWorkOrder(workOrderId: number | string) { + const workOrder = await getRequiredWorkOrder(workOrderId) + if ( + workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || + !workOrder.assigned_worker_id + ) { + throw createHttpError('只有进行中且已指派打手的订单可以取消指派', { + statusCode: 409, + errorCode: 'work_order_unassign_status_invalid', + }) + } + const now = nowIso() + const { order: updated, failureReason } = await unassignWorkOrder({ + workOrderId: workOrder.id, + now, + }) + if (failureReason === 'work_order_not_in_progress') { + throw createHttpError('订单状态已变化,无法取消指派', { + statusCode: 409, + errorCode: 'work_order_unassign_status_invalid', + }) + } + if (failureReason === 'work_order_no_worker') { + throw createHttpError('该订单未指派打手', { + statusCode: 409, + errorCode: 'work_order_unassign_no_worker', + }) + } + if (!updated) { + throw createHttpError('取消指派失败,订单状态可能已变化', { + statusCode: 409, + errorCode: 'work_order_unassign_conflict', + }) + } + return { order: mapWorkOrderAdmin(updated) } +} + export async function getAdminWorkerPlatformSummary() { const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([ listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }), diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 52755294..67e686c3 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -8,6 +8,7 @@ import { SearchOutlined, SendOutlined, TeamOutlined, + UndoOutlined, UserAddOutlined, WarningOutlined, } from '@ant-design/icons' @@ -45,6 +46,7 @@ import { acceptAdminWorkOrder, assignAdminWorkOrderToWorker, createAdminMockWorkOrder, + unassignAdminWorkOrder, creditAdminWorkerWallet, deductAdminWorkOrderPendingDeposit, deleteAdminWorkCategory, @@ -650,6 +652,35 @@ export default function WorkOrdersPanel() { 指派 ) : null} + {row.status === 'in_progress' && row.worker ? ( + + ) : null} {row.status === 'pending_material' ? (