优化撤单处理方式

This commit is contained in:
yml2213
2026-08-15 17:41:01 +08:00
parent 57fda860dc
commit b9e4d007aa
5 changed files with 236 additions and 30 deletions
@@ -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<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 = '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,
})
@@ -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<string, unknown>) : {},
@@ -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() {
@@ -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<number | undefined>(undefined)
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
const [cancelOrder, setCancelOrder] = useState<WorkOrder | null>(null)
const [problemForm] = Form.useForm()
const [resolutionForm] = Form.useForm()
const [materialForm] = Form.useForm<MaterialFormValues>()
const [cancelForm] = Form.useForm<CancelOrderFormValues>()
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() {
<Button
danger
icon={<UndoOutlined />}
onClick={() => {
const assignedWorker = row.worker
if (!assignedWorker) return
modal.confirm({
title: '确认撤单?',
content: (
<span>
{assignedWorker.displayName || assignedWorker.username}
</span>
),
okText: '确认撤单',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: () =>
runAction(
() => cancelAdminWorkOrder(row.workOrderId),
'已撤单并恢复打手额度',
),
})
}}
onClick={() => openCancelModal(row)}
>
</Button>
@@ -1318,6 +1328,68 @@ export default function WorkOrdersPanel() {
</Form>
</Modal>
<Modal
title={`撤单处理 · ${getDisplayOrderNo(cancelOrder)}`}
open={Boolean(cancelOrder)}
onCancel={() => {
setCancelOrder(null)
cancelForm.resetFields()
}}
onOk={() => cancelForm.submit()}
destroyOnHidden
>
{cancelOrder ? (
<Form
form={cancelForm}
layout="vertical"
initialValues={{ action: 'return_to_hall' }}
onFinish={submitCancelOrder}
>
<Alert
showIcon
type="warning"
message="处理后都会恢复打手接单额度和冻结押金,请确认处理方式。"
style={{ marginBottom: 16 }}
/>
<Form.Item
label="处理方式"
name="action"
rules={[{ required: true, message: '请选择撤单处理方式' }]}
>
<Radio.Group>
<Space direction="vertical">
<Radio value="return_to_hall">退</Radio>
<Radio value="cancel_order">退</Radio>
</Space>
</Radio.Group>
</Form.Item>
<Form.Item
label="处理原因"
name="reason"
dependencies={['action']}
rules={[
({ getFieldValue }) => ({
validator(_, value) {
if (
getFieldValue('action') !== 'cancel_order' ||
String(value || '').trim()
) {
return Promise.resolve()
}
return Promise.reject(new Error('取消订单时请填写退款或撤单原因'))
},
}),
]}
>
<Input.TextArea
rows={3}
placeholder="取消订单时请填写退款原因;退回大厅可选填"
/>
</Form.Item>
</Form>
) : null}
</Modal>
<Modal
title={materialOrder?.status === 'in_progress' ? '更新订单资料' : '人工补充资料'}
open={Boolean(materialOrder)}
@@ -149,9 +149,16 @@ export function unassignAdminWorkOrder(workOrderId: number) {
)
}
export function cancelAdminWorkOrder(workOrderId: number) {
export function cancelAdminWorkOrder(
workOrderId: number,
payload: {
action: 'return_to_hall' | 'cancel_order'
reason?: string
},
) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/cancel`,
payload,
)
}