diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index 0ca0844c..6d9b09b7 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -1425,6 +1425,7 @@ export async function settleOverdueWorkOrder(input: { payloadJson: JSON.stringify({ policy: input.policy, depositAmount: resolvedDepositAmount, + workerId: workerId > 0 ? workerId : null, }), now: input.now, }) @@ -1449,6 +1450,36 @@ export async function countWorkerActiveOrders(workerId: number | string): Promis return Number(result.rows[0]?.total || 0) } +export async function countTimeoutEventsByWorkerIds(workerIds: number[]): Promise> { + const counts = new Map() + const uniqueIds = [...new Set(workerIds.map((id) => Number(id)).filter((id) => id > 0))] + if (uniqueIds.length === 0) return counts + const result = await query<{ worker_id: string; total: number }>( + ` + SELECT payload_json->>'workerId' AS worker_id, COUNT(*)::int AS total + FROM work_order_events + WHERE event_type LIKE 'timeout_%' + AND payload_json->>'workerId' IS NOT NULL + AND payload_json->>'workerId' != '' + AND payload_json->>'workerId' = ANY($1::text[]) + GROUP BY payload_json->>'workerId' + `, + [uniqueIds.map(String)], + ) + for (const row of result.rows) { + const workerId = Number(row.worker_id) + if (Number.isFinite(workerId) && workerId > 0) { + counts.set(workerId, Number(row.total || 0)) + } + } + return counts +} + +export async function countWorkerTimeoutEvents(workerId: number | string): Promise { + const counts = await countTimeoutEventsByWorkerIds([Number(workerId)]) + return counts.get(Number(workerId)) || 0 +} + export async function createWorkOrderEvent(input: { workOrderId: number actorType: string diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index e81b6978..d5797286 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -12,6 +12,7 @@ import { countWorkCategoryUsages, countWorkerLevelUsages, countWorkOrderPendingSharingSubmissions, + countTimeoutEventsByWorkerIds, cancelWorkerWorkOrder, countWorkerCancellationsSince, createWorkOrder, @@ -299,8 +300,17 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) { status: String(query.status || '').trim(), keyword: String(query.keyword || '').trim(), }) + const timeoutCounts = await countTimeoutEventsByWorkerIds( + items.map((item) => Number(item.id)), + ) return { - items: items.map(mapWorkerUser), + items: items.map((item) => { + const worker = mapWorkerUser(item) + return { + ...worker, + timeoutOrderCount: timeoutCounts.get(Number(item.id)) || 0, + } + }), pagination: { page, pageSize, total }, } } diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 6ed2819e..62e1bfd4 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -9,6 +9,7 @@ import { addWorkerWalletCredit, countWorkerAcceptedOrders, countWorkerActiveOrders, + countWorkerTimeoutEvents, countWorkCategoryUsages, countWorkerLevelUsages, countWorkOrderPendingSharingSubmissions, @@ -347,9 +348,10 @@ export function requireActiveWorkerSession(session: WorkerSession | null | undef export async function getWorkerProfile(session: WorkerSession) { const worker = await getRequiredWorker(session.workerId) - const [financeSummary, acceptedOrderCount, financeConfig] = await Promise.all([ + const [financeSummary, acceptedOrderCount, timeoutOrderCount, financeConfig] = await Promise.all([ getWorkerFinanceRequestSummary(session.workerId), countWorkerAcceptedOrders(session.workerId), + countWorkerTimeoutEvents(session.workerId), Promise.resolve(getWorkerFinanceConfig()), ]) const permissions = resolveWorkerPermissions(worker) @@ -359,6 +361,7 @@ export async function getWorkerProfile(session: WorkerSession) { permissions, summary: { acceptedOrderCount, + timeoutOrderCount, pendingWithdrawAmount: financeSummary.pendingWithdrawAmount, approvedWithdrawAmount: financeSummary.approvedWithdrawAmount, pendingRechargeAmount: financeSummary.pendingRechargeAmount, diff --git a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx index dbba4bd6..b94a240a 100644 --- a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx @@ -196,6 +196,18 @@ export default function WorkersPanel() { width: 120, render: (_, row) => formatMoney(row.wallet.frozenDepositAmount), }, + { + title: '超时订单', + width: 110, + render: (_, row) => { + const count = Number(row.timeoutOrderCount || 0) + return count > 0 ? ( + {count} 单 + ) : ( + 0 + ) + }, + }, { title: '状态', width: 130, diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index b20bbc77..f3c142c7 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -1,4 +1,4 @@ -import { EyeOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons' +import { ClockCircleOutlined, EyeOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons' import { useQuery, useQueryClient } from '@tanstack/react-query' import { Alert, @@ -378,6 +378,14 @@ export default function WorkerHallPage() { ) : null} + {Number(order.timeoutMinutes || 0) > 0 ? ( +
+ }> + 限时 {order.timeoutMinutes} 分钟 + +
+ ) : null} +
{formatMoney(order.rewardAmount)} @@ -511,6 +519,15 @@ export default function WorkerHallPage() { {formatMoney(detailOrder.freezeDepositAmount)} + + {Number(detailOrder.timeoutMinutes || 0) > 0 ? ( + }> + 限时 {detailOrder.timeoutMinutes} 分钟 + + ) : ( + '不限时' + )} + {formatDateTime(resolvePublishedTime(detailOrder))} diff --git a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx index f7be84d2..e09d5825 100644 --- a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx @@ -87,6 +87,7 @@ type MetricCardItem = { prefix?: string suffix?: string precision?: number + danger?: boolean } export default function WorkerProfilePage() { @@ -519,6 +520,7 @@ export default function WorkerProfilePage() { prefix={item.prefix} suffix={item.suffix} precision={item.precision} + valueStyle={item.danger ? { color: '#cf1322' } : undefined} /> {item.note} @@ -1086,6 +1088,13 @@ function buildMetricCards(worker: WorkerUser, summary?: WorkerProfileSummary): M note: '已完成并验收通过的工单数量', suffix: '单', }, + { + label: '超时订单', + value: Number(summary?.timeoutOrderCount || 0), + note: '因任务超时被系统判定失败的工单数量', + suffix: '单', + danger: Number(summary?.timeoutOrderCount || 0) > 0, + }, ] } diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index f21709ef..31d0b46c 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -28,6 +28,7 @@ export type WorkerUser = { totalCreditedAmount: number totalSettledAmount: number } + timeoutOrderCount?: number createdAt: string updatedAt: string reviewedAt: string | null @@ -35,6 +36,7 @@ export type WorkerUser = { export type WorkerProfileSummary = { acceptedOrderCount: number + timeoutOrderCount: number pendingWithdrawAmount: number approvedWithdrawAmount: number pendingRechargeAmount: number