From b9e4d007aacbe34c70085f831d4e0e7b8eb45bda Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 15 Aug 2026 17:41:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=92=A4=E5=8D=95=E5=A4=84?= =?UTF-8?q?=E7=90=86=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker-platform/work-order-repo.ts | 103 +++++++++++++++- .../src/routes/admin/worker-platform.ts | 9 +- .../services/worker-platform/admin-service.ts | 31 ++++- .../pages/admin/panels/WorkOrdersPanel.tsx | 114 ++++++++++++++---- .../src/services/admin/worker-platform.ts | 9 +- 5 files changed, 236 insertions(+), 30 deletions(-) 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 84007b55..673843aa 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -1295,9 +1295,106 @@ export async function unassignWorkOrder(input: { }) } +export async function returnAssignedWorkOrderToHall(input: { + workOrderId: number + now: string + reason?: 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 = 'open', + assigned_worker_id = NULL, + assigned_at = NULL, + deadline_at = NULL, + published_at = $1, + 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: 'returned_to_hall_by_admin', + fromStatus: 'in_progress', + toStatus: 'open', + payloadJson: JSON.stringify({ + workerId, + releaseAmount, + reason: String(input.reason || '').trim(), + }), + now: input.now, + }) + + return { + order: await getWorkOrderByIdWithClient(client, input.workOrderId), + failureReason: null, + } + }) +} + export async function cancelAssignedWorkOrder(input: { workOrderId: number now: string + reason?: string }): Promise<{ order: WorkOrderRow | null failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null @@ -1375,7 +1472,11 @@ export async function cancelAssignedWorkOrder(input: { eventType: 'cancelled_by_admin', fromStatus: 'in_progress', toStatus: 'cancelled', - payloadJson: JSON.stringify({ workerId, releaseAmount }), + payloadJson: JSON.stringify({ + workerId, + releaseAmount, + reason: String(input.reason || '').trim(), + }), now: input.now, }) diff --git a/apps/backend/src/routes/admin/worker-platform.ts b/apps/backend/src/routes/admin/worker-platform.ts index 03155d80..d2f8528b 100644 --- a/apps/backend/src/routes/admin/worker-platform.ts +++ b/apps/backend/src/routes/admin/worker-platform.ts @@ -386,12 +386,15 @@ router.post( router.post( '/worker-platform/orders/:workOrderId/cancel', requireAdminRoles(['admin', 'operator', 'support']), - createJsonHandler((req) => cancelAdminWorkOrder(String(req.params.workOrderId || '')), { - successMessage: '订单已撤单,打手额度已恢复', + createJsonHandler((req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}), { + successMessage: '撤单处理完成,打手额度已恢复', errorMessage: '撤单失败', scope: '[admin/worker-platform/orders/:workOrderId/cancel]', audit: (req, data) => ({ - action: 'work_order_cancelled', + action: + String(req.body?.action || '').trim() === 'return_to_hall' + ? 'work_order_returned_to_hall' + : 'work_order_cancelled', targetType: 'work_order', targetId: String(req.params.workOrderId || ''), data: data && typeof data === 'object' ? (data as Record) : {}, diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 3c119796..8002f3fd 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -7,6 +7,7 @@ import { addWorkerWalletCredit, assignWorkOrderToWorker, cancelAssignedWorkOrder, + returnAssignedWorkOrderToHall, unassignWorkOrder, countWorkerAcceptedOrders, countWorkerActiveOrders, @@ -1258,7 +1259,10 @@ export async function unassignAdminWorkOrder(workOrderId: number | string) { return { order: mapWorkOrderAdmin(updated) } } -export async function cancelAdminWorkOrder(workOrderId: number | string) { +export async function cancelAdminWorkOrder( + workOrderId: number | string, + payload: JsonObject = {}, +) { const workOrder = await getRequiredWorkOrder(workOrderId) if ( workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || @@ -1269,10 +1273,29 @@ export async function cancelAdminWorkOrder(workOrderId: number | string) { errorCode: 'work_order_cancel_status_invalid', }) } - const { order: updated, failureReason } = await cancelAssignedWorkOrder({ + const action = String(payload.action || '').trim() + if (!['return_to_hall', 'cancel_order'].includes(action)) { + throw createHttpError('请选择撤单处理方式', { + statusCode: 400, + errorCode: 'work_order_cancel_action_required', + }) + } + const reason = String(payload.reason || '').trim() + if (action === 'cancel_order' && !reason) { + throw createHttpError('取消订单时请填写退款或撤单原因', { + statusCode: 400, + errorCode: 'work_order_cancel_reason_required', + }) + } + const input = { workOrderId: workOrder.id, now: nowIso(), - }) + reason, + } + const { order: updated, failureReason } = + action === 'return_to_hall' + ? await returnAssignedWorkOrderToHall(input) + : await cancelAssignedWorkOrder(input) if (failureReason === 'work_order_not_in_progress') { throw createHttpError('订单状态已变化,无法撤单', { statusCode: 409, @@ -1291,7 +1314,7 @@ export async function cancelAdminWorkOrder(workOrderId: number | string) { errorCode: 'work_order_cancel_conflict', }) } - return { order: mapWorkOrderAdmin(updated) } + return { order: mapWorkOrderAdmin(updated), action } } export async function getAdminWorkerPlatformSummary() { diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 03e0e5c2..2491b353 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -131,6 +131,11 @@ type MaterialFormValues = { rewardAmount?: number } +type CancelOrderFormValues = { + action?: 'return_to_hall' | 'cancel_order' + reason?: string +} + export default function WorkOrdersPanel() { const { message, modal } = App.useApp() const navigate = useNavigate() @@ -154,9 +159,11 @@ export default function WorkOrdersPanel() { const [assignWorkerId, setAssignWorkerId] = useState(undefined) const [editOrder, setEditOrder] = useState(null) const [deductOrder, setDeductOrder] = useState(null) + const [cancelOrder, setCancelOrder] = useState(null) const [problemForm] = Form.useForm() const [resolutionForm] = Form.useForm() const [materialForm] = Form.useForm() + const [cancelForm] = Form.useForm() const [deductForm] = Form.useForm<{ amount?: number; note?: string }>() const [editForm] = Form.useForm<{ productName?: string @@ -208,6 +215,11 @@ export default function WorkOrdersPanel() { setAssignWorkerId(undefined) } + function openCancelModal(row: WorkOrder) { + setCancelOrder(row) + cancelForm.setFieldsValue({ action: 'return_to_hall', reason: '' }) + } + function applyMaterialPaste(text: string) { if (!materialOrder) return const recognized = parseWorkerMaterialText(text) @@ -383,6 +395,24 @@ export default function WorkOrdersPanel() { resolutionForm.resetFields() } + async function submitCancelOrder(values: CancelOrderFormValues) { + if (!cancelOrder || !values.action) return + const action = values.action + const succeeded = await runAction( + () => + cancelAdminWorkOrder(cancelOrder.workOrderId, { + action, + reason: String(values.reason || '').trim(), + }), + action === 'return_to_hall' + ? '订单已退回大厅,打手额度已恢复' + : '订单已取消,打手额度已恢复', + ) + if (!succeeded) return + setCancelOrder(null) + cancelForm.resetFields() + } + async function submitMaterial(values: MaterialFormValues) { if (!materialOrder) return const succeeded = await runAction( @@ -716,27 +746,7 @@ export default function WorkOrdersPanel() { @@ -1318,6 +1328,68 @@ export default function WorkOrdersPanel() { + { + setCancelOrder(null) + cancelForm.resetFields() + }} + onOk={() => cancelForm.submit()} + destroyOnHidden + > + {cancelOrder ? ( +
+ + + + + 退回大厅:订单重新开放给其他打手接单 + 取消订单:用户退款后终止订单,不再回大厅 + + + + ({ + validator(_, value) { + if ( + getFieldValue('action') !== 'cancel_order' || + String(value || '').trim() + ) { + return Promise.resolve() + } + return Promise.reject(new Error('取消订单时请填写退款或撤单原因')) + }, + }), + ]} + > + + + + ) : null} +
+ ( `/api/v1/admin/worker-platform/orders/${workOrderId}/cancel`, + payload, ) }