优化撤单处理方式
This commit is contained in:
@@ -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: {
|
export async function cancelAssignedWorkOrder(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
now: string
|
now: string
|
||||||
|
reason?: string
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
order: WorkOrderRow | null
|
order: WorkOrderRow | null
|
||||||
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | 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',
|
eventType: 'cancelled_by_admin',
|
||||||
fromStatus: 'in_progress',
|
fromStatus: 'in_progress',
|
||||||
toStatus: 'cancelled',
|
toStatus: 'cancelled',
|
||||||
payloadJson: JSON.stringify({ workerId, releaseAmount }),
|
payloadJson: JSON.stringify({
|
||||||
|
workerId,
|
||||||
|
releaseAmount,
|
||||||
|
reason: String(input.reason || '').trim(),
|
||||||
|
}),
|
||||||
now: input.now,
|
now: input.now,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -386,12 +386,15 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/cancel',
|
'/worker-platform/orders/:workOrderId/cancel',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler((req) => cancelAdminWorkOrder(String(req.params.workOrderId || '')), {
|
createJsonHandler((req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}), {
|
||||||
successMessage: '订单已撤单,打手额度已恢复',
|
successMessage: '撤单处理完成,打手额度已恢复',
|
||||||
errorMessage: '撤单失败',
|
errorMessage: '撤单失败',
|
||||||
scope: '[admin/worker-platform/orders/:workOrderId/cancel]',
|
scope: '[admin/worker-platform/orders/:workOrderId/cancel]',
|
||||||
audit: (req, data) => ({
|
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',
|
targetType: 'work_order',
|
||||||
targetId: String(req.params.workOrderId || ''),
|
targetId: String(req.params.workOrderId || ''),
|
||||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
addWorkerWalletCredit,
|
addWorkerWalletCredit,
|
||||||
assignWorkOrderToWorker,
|
assignWorkOrderToWorker,
|
||||||
cancelAssignedWorkOrder,
|
cancelAssignedWorkOrder,
|
||||||
|
returnAssignedWorkOrderToHall,
|
||||||
unassignWorkOrder,
|
unassignWorkOrder,
|
||||||
countWorkerAcceptedOrders,
|
countWorkerAcceptedOrders,
|
||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
@@ -1258,7 +1259,10 @@ export async function unassignAdminWorkOrder(workOrderId: number | string) {
|
|||||||
return { order: mapWorkOrderAdmin(updated) }
|
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)
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
if (
|
if (
|
||||||
workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS ||
|
workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS ||
|
||||||
@@ -1269,10 +1273,29 @@ export async function cancelAdminWorkOrder(workOrderId: number | string) {
|
|||||||
errorCode: 'work_order_cancel_status_invalid',
|
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,
|
workOrderId: workOrder.id,
|
||||||
now: nowIso(),
|
now: nowIso(),
|
||||||
})
|
reason,
|
||||||
|
}
|
||||||
|
const { order: updated, failureReason } =
|
||||||
|
action === 'return_to_hall'
|
||||||
|
? await returnAssignedWorkOrderToHall(input)
|
||||||
|
: await cancelAssignedWorkOrder(input)
|
||||||
if (failureReason === 'work_order_not_in_progress') {
|
if (failureReason === 'work_order_not_in_progress') {
|
||||||
throw createHttpError('订单状态已变化,无法撤单', {
|
throw createHttpError('订单状态已变化,无法撤单', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
@@ -1291,7 +1314,7 @@ export async function cancelAdminWorkOrder(workOrderId: number | string) {
|
|||||||
errorCode: 'work_order_cancel_conflict',
|
errorCode: 'work_order_cancel_conflict',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return { order: mapWorkOrderAdmin(updated) }
|
return { order: mapWorkOrderAdmin(updated), action }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminWorkerPlatformSummary() {
|
export async function getAdminWorkerPlatformSummary() {
|
||||||
|
|||||||
@@ -131,6 +131,11 @@ type MaterialFormValues = {
|
|||||||
rewardAmount?: number
|
rewardAmount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CancelOrderFormValues = {
|
||||||
|
action?: 'return_to_hall' | 'cancel_order'
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function WorkOrdersPanel() {
|
export default function WorkOrdersPanel() {
|
||||||
const { message, modal } = App.useApp()
|
const { message, modal } = App.useApp()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -154,9 +159,11 @@ export default function WorkOrdersPanel() {
|
|||||||
const [assignWorkerId, setAssignWorkerId] = useState<number | undefined>(undefined)
|
const [assignWorkerId, setAssignWorkerId] = useState<number | undefined>(undefined)
|
||||||
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
|
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
|
||||||
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
||||||
|
const [cancelOrder, setCancelOrder] = useState<WorkOrder | null>(null)
|
||||||
const [problemForm] = Form.useForm()
|
const [problemForm] = Form.useForm()
|
||||||
const [resolutionForm] = Form.useForm()
|
const [resolutionForm] = Form.useForm()
|
||||||
const [materialForm] = Form.useForm<MaterialFormValues>()
|
const [materialForm] = Form.useForm<MaterialFormValues>()
|
||||||
|
const [cancelForm] = Form.useForm<CancelOrderFormValues>()
|
||||||
const [deductForm] = Form.useForm<{ amount?: number; note?: string }>()
|
const [deductForm] = Form.useForm<{ amount?: number; note?: string }>()
|
||||||
const [editForm] = Form.useForm<{
|
const [editForm] = Form.useForm<{
|
||||||
productName?: string
|
productName?: string
|
||||||
@@ -208,6 +215,11 @@ export default function WorkOrdersPanel() {
|
|||||||
setAssignWorkerId(undefined)
|
setAssignWorkerId(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCancelModal(row: WorkOrder) {
|
||||||
|
setCancelOrder(row)
|
||||||
|
cancelForm.setFieldsValue({ action: 'return_to_hall', reason: '' })
|
||||||
|
}
|
||||||
|
|
||||||
function applyMaterialPaste(text: string) {
|
function applyMaterialPaste(text: string) {
|
||||||
if (!materialOrder) return
|
if (!materialOrder) return
|
||||||
const recognized = parseWorkerMaterialText(text)
|
const recognized = parseWorkerMaterialText(text)
|
||||||
@@ -383,6 +395,24 @@ export default function WorkOrdersPanel() {
|
|||||||
resolutionForm.resetFields()
|
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) {
|
async function submitMaterial(values: MaterialFormValues) {
|
||||||
if (!materialOrder) return
|
if (!materialOrder) return
|
||||||
const succeeded = await runAction(
|
const succeeded = await runAction(
|
||||||
@@ -716,27 +746,7 @@ export default function WorkOrdersPanel() {
|
|||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
icon={<UndoOutlined />}
|
icon={<UndoOutlined />}
|
||||||
onClick={() => {
|
onClick={() => openCancelModal(row)}
|
||||||
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),
|
|
||||||
'已撤单并恢复打手额度',
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
撤单
|
撤单
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1318,6 +1328,68 @@ export default function WorkOrdersPanel() {
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</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
|
<Modal
|
||||||
title={materialOrder?.status === 'in_progress' ? '更新订单资料' : '人工补充资料'}
|
title={materialOrder?.status === 'in_progress' ? '更新订单资料' : '人工补充资料'}
|
||||||
open={Boolean(materialOrder)}
|
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 }>(
|
return apiPost<{ order: WorkOrder }>(
|
||||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/cancel`,
|
`/api/v1/admin/worker-platform/orders/${workOrderId}/cancel`,
|
||||||
|
payload,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user