增加取消订单重新可以发布

This commit is contained in:
yml2213
2026-08-20 17:46:53 +08:00
parent 543bc7f789
commit 3587e44261
10 changed files with 185 additions and 0 deletions
@@ -319,6 +319,7 @@ export type WorkOrderCapabilities = {
canAccept: boolean
canMarkProblem: boolean
canDelete: boolean
canReopen: boolean
canPublish: boolean
canCancel: 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(
workerId: number | string,
sinceIso: string,
@@ -34,6 +34,7 @@ import {
markAdminWorkOrderProblem,
pinAdminWorkOrder,
publishAdminWorkOrder,
reopenAdminCancelledWorkOrder,
reprocessAdminKuaishouSendCodeWorkOrders,
reviewAdminWorkerFinanceRequest,
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(
'/worker-platform/orders/:workOrderId/publish',
requireAdminRoles(['admin', 'operator', 'support']),
@@ -49,6 +49,7 @@ import {
sumPendingUnfreezeByOrderIds,
resolveProblemWorkOrder,
reviewWorkerFinanceRequest,
reopenCancelledWorkOrder,
updateWorkOrder,
updateWorkOrderBasic,
updateWorkerUser,
@@ -1795,6 +1796,37 @@ export async function deleteAdminWorkOrder(workOrderId: number | string) {
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 = '') {
const workOrder = await getRequiredWorkOrder(workOrderId)
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.CANCELLED,
].includes(workOrder.status as never),
canReopen: workOrder.status === WORK_ORDER_STATUS.CANCELLED && !hasActiveShares,
canPublish:
workOrder.status === WORK_ORDER_STATUS.UNASSIGNED && Number(workOrder.reward_amount || 0) > 0,
canCancel: status === WORK_ORDER_STATUS.IN_PROGRESS && Boolean(workOrder.assigned_worker_id),
@@ -734,6 +734,27 @@ test('后台拼单聚合状态同时收敛筛选语义和操作能力', () => {
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('拼单打手汇总展示当前参与者并合并同一打手的份数', () => {
const workers = mapWorkOrderSharingWorkers([
buildWorkOrderShareRow({ worker_id: 1, quantity: 2, worker_username: 'alpha' }),