diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 6c26dec7..7d058673 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -382,6 +382,12 @@ export type WorkOrderRow = { category_name?: string worker_username?: string worker_display_name?: string + /** 取消信息,由工单查询从取消类事件推导,仅已取消工单有值。 */ + cancel_reason?: string | null + cancel_event_type?: string | null + cancel_actor_type?: string | null + cancel_actor_id?: string | null + cancelled_at?: string | null } export type ProblemWorkOrderResolutionAction = diff --git a/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts index a2b0d9a7..5e9012f1 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts @@ -12,10 +12,43 @@ export const WORK_ORDER_SELECT = ` COALESCE(share_summary.pending_share_count, 0)::int AS pending_share_count, wc.name AS category_name, wu.username AS worker_username, - wu.display_name AS worker_display_name + wu.display_name AS worker_display_name, + cancel_info.cancel_reason, + cancel_info.cancel_event_type, + cancel_info.cancel_actor_type, + cancel_info.cancel_actor_id, + cancel_info.cancelled_at FROM work_orders wo LEFT JOIN work_categories wc ON wc.id = wo.category_id LEFT JOIN worker_users wu ON wu.id = wo.assigned_worker_id + LEFT JOIN LATERAL ( + SELECT + ev.event_type AS cancel_event_type, + ev.actor_type AS cancel_actor_type, + ev.actor_id AS cancel_actor_id, + ev.created_at AS cancelled_at, + BTRIM(COALESCE( + NULLIF(BTRIM(COALESCE(ev.payload_json->>'reason', '')), ''), + NULLIF(BTRIM(COALESCE(ev.payload_json->>'note', '')), ''), + CASE ev.event_type + WHEN 'timeout_cancel_release' THEN '任务超时未完成,系统自动取消并退还押金' + WHEN 'timeout_cancel_deduct' THEN '任务超时未完成,系统自动取消并扣除押金' + ELSE '' + END + )) AS cancel_reason + FROM work_order_events ev + WHERE ev.work_order_id = wo.id + AND wo.status = 'cancelled' + AND ev.event_type IN ( + 'cancelled_by_admin', + 'problem_cancel_release', + 'problem_cancel_deduct', + 'timeout_cancel_release', + 'timeout_cancel_deduct' + ) + ORDER BY ev.id DESC + LIMIT 1 + ) cancel_info ON TRUE LEFT JOIN LATERAL ( SELECT SUM(wos.quantity) FILTER (WHERE wos.status != 'cancelled') AS active_share_quantity, diff --git a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts index ccee2547..f28bf473 100644 --- a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts +++ b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts @@ -114,6 +114,15 @@ export function mapWorkOrderSharingWorkers(shares: WorkOrderShareRow[]) { return [...workers.values()] } +/** 取消来源:由触发取消的事件类型推导,用于列表与详情展示。 */ +function resolveWorkOrderCancelSource(cancelEventType: string | null | undefined) { + const eventType = String(cancelEventType || '').trim() + if (eventType === 'cancelled_by_admin') return '后台撤单' + if (eventType.startsWith('problem_')) return '问题单处置' + if (eventType.startsWith('timeout_')) return '超时自动取消' + return eventType || '未知来源' +} + export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderShareRow[]) { const activeShareQuantity = Math.max(0, Number(workOrder.active_share_quantity || 0)) const pendingShareCount = Math.max(0, Number(workOrder.pending_share_count || 0)) @@ -232,6 +241,16 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha acceptance: mapAcceptanceForResponse(workOrder.acceptance_json), draftAcceptance: mapAcceptanceForResponse(workOrder.draft_acceptance_json), problemNote: workOrder.problem_note || '', + cancel: + workOrder.status === WORK_ORDER_STATUS.CANCELLED + ? { + reason: String(workOrder.cancel_reason || ''), + source: resolveWorkOrderCancelSource(workOrder.cancel_event_type), + actorType: String(workOrder.cancel_actor_type || ''), + actorName: String(workOrder.cancel_actor_id || ''), + cancelledAt: workOrder.cancelled_at || null, + } + : null, worker: workOrder.assigned_worker_id ? { workerId: Number(workOrder.assigned_worker_id), diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index de4483fb..8b2675f5 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -896,21 +896,34 @@ export default function WorkOrdersPanel() { title: '状态', width: 150, render: (_, row) => { + const cancelText = + row.cancel?.reason || (row.cancel ? `${row.cancel.source}(未记录原因)` : '') return ( - - {formatStatus(row.status)} - {row.rawStatus === 'open' && row.pinnedAt ? ( - }> - 置顶 - +
+ + {formatStatus(row.status)} + {row.rawStatus === 'open' && row.pinnedAt ? ( + }> + 置顶 + + ) : null} + {row.acceptanceMode === 'friend_gift' ? 好友赠送 : null} + {row.acceptanceMode === 'friend_gift' && row.gift ? ( + + {formatGiftPhase(row.gift.phase)} + + ) : null} + + {row.cancel ? ( + + {cancelText} + ) : null} - {row.acceptanceMode === 'friend_gift' ? 好友赠送 : null} - {row.acceptanceMode === 'friend_gift' && row.gift ? ( - - {formatGiftPhase(row.gift.phase)} - - ) : null} - +
) }, }, @@ -1418,6 +1431,22 @@ export default function WorkOrdersPanel() { {String(detailOrder.problemNote || '').trim() || '-'} + {detailOrder.cancel ? ( + + + {detailOrder.cancel.source} + {detailOrder.cancel.cancelledAt + ? ` · ${formatAdminDateTime(detailOrder.cancel.cancelledAt)}` + : ''} + {detailOrder.cancel.actorType === 'admin' && detailOrder.cancel.actorName + ? ` · 操作人 ${detailOrder.cancel.actorName}` + : ''} + {detailOrder.cancel.reason + ? ` · 原因:${detailOrder.cancel.reason}` + : ' · 原因未记录'} + + + ) : null} diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index de9ccad6..ec1088bc 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -548,6 +548,14 @@ export type WorkOrder = { /** 打手本人尚未点击查看或操作过该工单。 */ isUnread?: boolean problemNote: string + /** 取消信息,仅已取消工单返回。 */ + cancel?: { + reason: string + source: string + actorType: string + actorName: string + cancelledAt: string | null + } | null worker: null | { workerId: number username: string