优化下架和撤单功能

This commit is contained in:
yml2213
2026-08-15 15:25:56 +08:00
parent 3b562b4824
commit cd3b48eef5
5 changed files with 160 additions and 9 deletions
@@ -1295,6 +1295,97 @@ export async function unassignWorkOrder(input: {
}) })
} }
export async function cancelAssignedWorkOrder(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<WorkOrderRow>(
`
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 = 'cancelled',
assigned_worker_id = NULL,
assigned_at = NULL,
deadline_at = NULL,
published_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: 'cancelled_by_admin',
fromStatus: 'in_progress',
toStatus: 'cancelled',
payloadJson: JSON.stringify({ workerId, releaseAmount }),
now: input.now,
})
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
export async function countWorkerCancellationsSince( export async function countWorkerCancellationsSince(
workerId: number | string, workerId: number | string,
sinceIso: string, sinceIso: string,
@@ -4,6 +4,7 @@ import {
acceptAdminWorkOrder, acceptAdminWorkOrder,
acceptAdminWorkOrders, acceptAdminWorkOrders,
assignAdminWorkOrderToWorker, assignAdminWorkOrderToWorker,
cancelAdminWorkOrder,
unassignAdminWorkOrder, unassignAdminWorkOrder,
creditAdminWorkerWallet, creditAdminWorkerWallet,
createAdminMockWorkOrder, createAdminMockWorkOrder,
@@ -382,6 +383,22 @@ router.post(
}), }),
) )
router.post(
'/worker-platform/orders/:workOrderId/cancel',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => cancelAdminWorkOrder(String(req.params.workOrderId || '')), {
successMessage: '订单已撤单,打手额度已恢复',
errorMessage: '撤单失败',
scope: '[admin/worker-platform/orders/:workOrderId/cancel]',
audit: (req, data) => ({
action: 'work_order_cancelled',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
)
router.delete( router.delete(
'/worker-platform/orders/:workOrderId', '/worker-platform/orders/:workOrderId',
requireAdminRoles(['admin', 'operator']), requireAdminRoles(['admin', 'operator']),
@@ -416,7 +433,7 @@ router.post(
router.post( router.post(
'/worker-platform/orders/:workOrderId/unpublish', '/worker-platform/orders/:workOrderId/unpublish',
requireAdminRoles(['admin', 'operator']), requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => unpublishAdminWorkOrder(String(req.params.workOrderId || '')), { createJsonHandler((req) => unpublishAdminWorkOrder(String(req.params.workOrderId || '')), {
successMessage: '订单已撤回到未分配', successMessage: '订单已撤回到未分配',
errorMessage: '下架接单订单失败', errorMessage: '下架接单订单失败',
@@ -6,6 +6,7 @@ import {
acceptWorkOrderAndSettle, acceptWorkOrderAndSettle,
addWorkerWalletCredit, addWorkerWalletCredit,
assignWorkOrderToWorker, assignWorkOrderToWorker,
cancelAssignedWorkOrder,
unassignWorkOrder, unassignWorkOrder,
countWorkerAcceptedOrders, countWorkerAcceptedOrders,
countWorkerActiveOrders, countWorkerActiveOrders,
@@ -1240,6 +1241,42 @@ export async function unassignAdminWorkOrder(workOrderId: number | string) {
return { order: mapWorkOrderAdmin(updated) } return { order: mapWorkOrderAdmin(updated) }
} }
export async function cancelAdminWorkOrder(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_cancel_status_invalid',
})
}
const { order: updated, failureReason } = await cancelAssignedWorkOrder({
workOrderId: workOrder.id,
now: nowIso(),
})
if (failureReason === 'work_order_not_in_progress') {
throw createHttpError('订单状态已变化,无法撤单', {
statusCode: 409,
errorCode: 'work_order_cancel_status_invalid',
})
}
if (failureReason === 'work_order_no_worker') {
throw createHttpError('该订单尚未被打手接取', {
statusCode: 409,
errorCode: 'work_order_cancel_no_worker',
})
}
if (!updated) {
throw createHttpError('撤单失败,订单状态可能已变化', {
statusCode: 409,
errorCode: 'work_order_cancel_conflict',
})
}
return { order: mapWorkOrderAdmin(updated) }
}
export async function getAdminWorkerPlatformSummary() { export async function getAdminWorkerPlatformSummary() {
const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([ const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }), listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
@@ -46,8 +46,8 @@ import {
acceptAdminWorkOrder, acceptAdminWorkOrder,
acceptAdminWorkOrders, acceptAdminWorkOrders,
assignAdminWorkOrderToWorker, assignAdminWorkOrderToWorker,
cancelAdminWorkOrder,
createAdminMockWorkOrder, createAdminMockWorkOrder,
unassignAdminWorkOrder,
creditAdminWorkerWallet, creditAdminWorkerWallet,
deductAdminWorkOrderPendingDeposit, deductAdminWorkOrderPendingDeposit,
deleteAdminWorkCategory, deleteAdminWorkCategory,
@@ -692,25 +692,25 @@ export default function WorkOrdersPanel() {
const assignedWorker = row.worker const assignedWorker = row.worker
if (!assignedWorker) return if (!assignedWorker) return
modal.confirm({ modal.confirm({
title: '取消指派该打手', title: '确认撤单',
content: ( content: (
<span> <span>
{assignedWorker.displayName || assignedWorker.username} {assignedWorker.displayName || assignedWorker.username}
退
</span> </span>
), ),
okText: '确认取消指派', okText: '确认撤单',
cancelText: '取消', cancelText: '取消',
okButtonProps: { danger: true }, okButtonProps: { danger: true },
onOk: () => onOk: () =>
runAction( runAction(
() => unassignAdminWorkOrder(row.workOrderId), () => cancelAdminWorkOrder(row.workOrderId),
'已取消指派', '已撤单并恢复打手额度',
), ),
}) })
}} }}
> >
</Button> </Button>
) : null} ) : null}
{['pending_material', 'in_progress'].includes(row.status) ? ( {['pending_material', 'in_progress'].includes(row.status) ? (
@@ -149,6 +149,12 @@ export function unassignAdminWorkOrder(workOrderId: number) {
) )
} }
export function cancelAdminWorkOrder(workOrderId: number) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/cancel`,
)
}
export function creditAdminWorkerWallet( export function creditAdminWorkerWallet(
workerId: number, workerId: number,
payload: { amount: number; note?: string }, payload: { amount: number; note?: string },