增加取消订单重新可以发布
This commit is contained in:
@@ -319,6 +319,7 @@ export type WorkOrderCapabilities = {
|
|||||||
canAccept: boolean
|
canAccept: boolean
|
||||||
canMarkProblem: boolean
|
canMarkProblem: boolean
|
||||||
canDelete: boolean
|
canDelete: boolean
|
||||||
|
canReopen: boolean
|
||||||
canPublish: boolean
|
canPublish: boolean
|
||||||
canCancel: boolean
|
canCancel: boolean
|
||||||
canResolveProblem: boolean
|
canResolveProblem: boolean
|
||||||
|
|||||||
@@ -1981,6 +1981,84 @@ export async function cancelAssignedWorkOrder(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将已取消工单恢复为未分配状态,不变更历史押金流水。 */
|
||||||
|
export async function reopenCancelledWorkOrder(input: {
|
||||||
|
workOrderId: number
|
||||||
|
now: string
|
||||||
|
actorName?: string
|
||||||
|
}): Promise<{
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
failureReason: 'work_order_not_cancelled' | 'work_order_has_active_shares' | 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 !== 'cancelled') {
|
||||||
|
return { order: null, failureReason: 'work_order_not_cancelled' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeShareResult = await client.query<{ total: number }>(
|
||||||
|
`
|
||||||
|
SELECT COUNT(*)::int AS total
|
||||||
|
FROM work_order_shares
|
||||||
|
WHERE work_order_id = $1
|
||||||
|
AND status != 'cancelled'
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
if (Number(activeShareResult.rows[0]?.total || 0) > 0) {
|
||||||
|
return { order: null, failureReason: 'work_order_has_active_shares' }
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = 'unassigned',
|
||||||
|
assigned_worker_id = NULL,
|
||||||
|
assigned_at = NULL,
|
||||||
|
deadline_at = NULL,
|
||||||
|
acceptance_json = '{}'::jsonb,
|
||||||
|
draft_acceptance_json = '{}'::jsonb,
|
||||||
|
submitted_at = NULL,
|
||||||
|
accepted_at = NULL,
|
||||||
|
problem_note = '',
|
||||||
|
published_at = NULL,
|
||||||
|
pinned_at = NULL,
|
||||||
|
updated_at = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`,
|
||||||
|
[input.now, input.workOrderId],
|
||||||
|
)
|
||||||
|
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName || '',
|
||||||
|
eventType: 'reopened_from_cancelled',
|
||||||
|
fromStatus: 'cancelled',
|
||||||
|
toStatus: 'unassigned',
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
previousAssignedWorkerId: Number(workOrder.last_assigned_worker_id || 0) || null,
|
||||||
|
}),
|
||||||
|
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,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
markAdminWorkOrderProblem,
|
markAdminWorkOrderProblem,
|
||||||
pinAdminWorkOrder,
|
pinAdminWorkOrder,
|
||||||
publishAdminWorkOrder,
|
publishAdminWorkOrder,
|
||||||
|
reopenAdminCancelledWorkOrder,
|
||||||
reprocessAdminKuaishouSendCodeWorkOrders,
|
reprocessAdminKuaishouSendCodeWorkOrders,
|
||||||
reviewAdminWorkerFinanceRequest,
|
reviewAdminWorkerFinanceRequest,
|
||||||
resolveAdminProblemWorkOrder,
|
resolveAdminProblemWorkOrder,
|
||||||
@@ -651,6 +652,29 @@ router.delete(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/worker-platform/orders/:workOrderId/reopen',
|
||||||
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
reopenAdminCancelledWorkOrder(
|
||||||
|
String(req.params.workOrderId || ''),
|
||||||
|
req.adminSession?.username || '',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '订单已恢复为未分配状态',
|
||||||
|
errorMessage: '重新启用订单失败',
|
||||||
|
scope: '[admin/worker-platform/orders/:workOrderId/reopen]',
|
||||||
|
audit: (req, data) => ({
|
||||||
|
action: 'work_order_reopened',
|
||||||
|
targetType: 'work_order',
|
||||||
|
targetId: String(req.params.workOrderId || ''),
|
||||||
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/publish',
|
'/worker-platform/orders/:workOrderId/publish',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
sumPendingUnfreezeByOrderIds,
|
sumPendingUnfreezeByOrderIds,
|
||||||
resolveProblemWorkOrder,
|
resolveProblemWorkOrder,
|
||||||
reviewWorkerFinanceRequest,
|
reviewWorkerFinanceRequest,
|
||||||
|
reopenCancelledWorkOrder,
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
updateWorkOrderBasic,
|
updateWorkOrderBasic,
|
||||||
updateWorkerUser,
|
updateWorkerUser,
|
||||||
@@ -1795,6 +1796,37 @@ export async function deleteAdminWorkOrder(workOrderId: number | string) {
|
|||||||
return { deleted: true }
|
return { deleted: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function reopenAdminCancelledWorkOrder(workOrderId: number | string, actorName = '') {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (!mapWorkOrderAdmin(workOrder).capabilities.canReopen) {
|
||||||
|
throw createHttpError('只有没有有效拼单份额的已取消订单可以重新启用', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_reopen_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { order: updated, failureReason } = await reopenCancelledWorkOrder({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
now: nowIso(),
|
||||||
|
actorName,
|
||||||
|
})
|
||||||
|
if (failureReason === 'work_order_has_active_shares') {
|
||||||
|
throw createHttpError('该订单仍有有效拼单份额,不能重新启用', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_reopen_has_active_shares',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError('订单状态已变化,无法重新启用', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_reopen_conflict',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
||||||
|
return { order: mapWorkOrderAdmin(updated) }
|
||||||
|
}
|
||||||
|
|
||||||
export async function publishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
export async function publishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
if (workOrder.status !== WORK_ORDER_STATUS.UNASSIGNED) {
|
if (workOrder.status !== WORK_ORDER_STATUS.UNASSIGNED) {
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha
|
|||||||
WORK_ORDER_STATUS.UNASSIGNED,
|
WORK_ORDER_STATUS.UNASSIGNED,
|
||||||
WORK_ORDER_STATUS.CANCELLED,
|
WORK_ORDER_STATUS.CANCELLED,
|
||||||
].includes(workOrder.status as never),
|
].includes(workOrder.status as never),
|
||||||
|
canReopen: workOrder.status === WORK_ORDER_STATUS.CANCELLED && !hasActiveShares,
|
||||||
canPublish:
|
canPublish:
|
||||||
workOrder.status === WORK_ORDER_STATUS.UNASSIGNED && Number(workOrder.reward_amount || 0) > 0,
|
workOrder.status === WORK_ORDER_STATUS.UNASSIGNED && Number(workOrder.reward_amount || 0) > 0,
|
||||||
canCancel: status === WORK_ORDER_STATUS.IN_PROGRESS && Boolean(workOrder.assigned_worker_id),
|
canCancel: status === WORK_ORDER_STATUS.IN_PROGRESS && Boolean(workOrder.assigned_worker_id),
|
||||||
|
|||||||
@@ -734,6 +734,27 @@ test('后台拼单聚合状态同时收敛筛选语义和操作能力', () => {
|
|||||||
assert.equal(filled.capabilities.canUnpublish, false)
|
assert.equal(filled.capabilities.canUnpublish, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('已取消工单可重新启用,恢复未分配后可编辑和发布', () => {
|
||||||
|
const cancelled = mapWorkOrderAdmin(buildWorkOrderRow({ status: 'cancelled' }))
|
||||||
|
assert.equal(cancelled.capabilities.canReopen, true)
|
||||||
|
assert.equal(cancelled.capabilities.canEdit, false)
|
||||||
|
assert.equal(cancelled.capabilities.canPublish, false)
|
||||||
|
|
||||||
|
const restored = mapWorkOrderAdmin(buildWorkOrderRow({ status: 'unassigned' }))
|
||||||
|
assert.equal(restored.capabilities.canReopen, false)
|
||||||
|
assert.equal(restored.capabilities.canEdit, true)
|
||||||
|
assert.equal(restored.capabilities.canPublish, true)
|
||||||
|
|
||||||
|
const cancelledSharing = mapWorkOrderAdmin(
|
||||||
|
buildWorkOrderRow({
|
||||||
|
status: 'cancelled',
|
||||||
|
sharing_enabled: true,
|
||||||
|
active_share_quantity: 1,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.equal(cancelledSharing.capabilities.canReopen, false)
|
||||||
|
})
|
||||||
|
|
||||||
test('拼单打手汇总展示当前参与者并合并同一打手的份数', () => {
|
test('拼单打手汇总展示当前参与者并合并同一打手的份数', () => {
|
||||||
const workers = mapWorkOrderSharingWorkers([
|
const workers = mapWorkOrderSharingWorkers([
|
||||||
buildWorkOrderShareRow({ worker_id: 1, quantity: 2, worker_username: 'alpha' }),
|
buildWorkOrderShareRow({ worker_id: 1, quantity: 2, worker_username: 'alpha' }),
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
|
|||||||
return { title: '问题单已处置:取消订单并扣除押金', color: 'orange' }
|
return { title: '问题单已处置:取消订单并扣除押金', color: 'orange' }
|
||||||
|
|
||||||
// —— 取消节点 ——
|
// —— 取消节点 ——
|
||||||
|
case 'reopened_from_cancelled':
|
||||||
|
return { title: '重新启用已取消订单,恢复为未分配状态', color: 'orange' }
|
||||||
case 'cancelled_by_admin':
|
case 'cancelled_by_admin':
|
||||||
return {
|
return {
|
||||||
title: note ? `取消订单(${note})` : '取消订单',
|
title: note ? `取消订单(${note})` : '取消订单',
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import {
|
|||||||
markAdminWorkOrderProblem,
|
markAdminWorkOrderProblem,
|
||||||
pinAdminWorkOrder,
|
pinAdminWorkOrder,
|
||||||
publishAdminWorkOrder,
|
publishAdminWorkOrder,
|
||||||
|
reopenAdminCancelledWorkOrder,
|
||||||
resolveAdminProblemWorkOrder,
|
resolveAdminProblemWorkOrder,
|
||||||
submitAdminWorkOrderMaterial,
|
submitAdminWorkOrderMaterial,
|
||||||
unpublishAdminWorkOrder,
|
unpublishAdminWorkOrder,
|
||||||
@@ -863,6 +864,26 @@ export default function WorkOrdersPanel() {
|
|||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{row.capabilities?.canReopen ? (
|
||||||
|
<Button
|
||||||
|
icon={<UndoOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
modal.confirm({
|
||||||
|
title: '确认重新启用该订单?',
|
||||||
|
content: '订单将恢复为未分配状态。请确认资料与金额后,再发布到抢单大厅。',
|
||||||
|
okText: '重新启用',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () =>
|
||||||
|
runAction(
|
||||||
|
() => reopenAdminCancelledWorkOrder(row.workOrderId),
|
||||||
|
'订单已恢复为未分配状态,可编辑后发布',
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重新启用
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
{row.sharing?.enabled || row.capabilities?.canConfigureSharing ? (
|
{row.sharing?.enabled || row.capabilities?.canConfigureSharing ? (
|
||||||
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
|
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
|
||||||
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing
|
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing
|
||||||
|
|||||||
@@ -391,6 +391,10 @@ export function publishAdminWorkOrder(workOrderId: number) {
|
|||||||
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/publish`)
|
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/publish`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reopenAdminCancelledWorkOrder(workOrderId: number) {
|
||||||
|
return apiPost<{ order: WorkOrder }>(`/api/v1/admin/worker-platform/orders/${workOrderId}/reopen`)
|
||||||
|
}
|
||||||
|
|
||||||
export function updateAdminWorkOrder(
|
export function updateAdminWorkOrder(
|
||||||
workOrderId: number,
|
workOrderId: number,
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ export type WorkOrder = {
|
|||||||
canAccept: boolean
|
canAccept: boolean
|
||||||
canMarkProblem: boolean
|
canMarkProblem: boolean
|
||||||
canDelete: boolean
|
canDelete: boolean
|
||||||
|
canReopen: boolean
|
||||||
canPublish: boolean
|
canPublish: boolean
|
||||||
canCancel: boolean
|
canCancel: boolean
|
||||||
canResolveProblem: boolean
|
canResolveProblem: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user