From 27bcb7dd68d40f2abdca5d86357bc49de6882b78 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 16 Aug 2026 11:14:33 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=E6=88=91=E7=9A=84=E8=AE=A2=E5=8D=95=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E4=B8=8E=E6=90=9C=E7=B4=A2=E6=A0=8FFlex=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker-platform/worker-repo.ts | 77 +++++ apps/backend/src/routes/worker.ts | 11 + .../worker-platform/worker-service.ts | 17 ++ .../src/pages/worker/WorkerHallPage.tsx | 101 ++++++- apps/frontend/src/services/worker.ts | 5 + apps/frontend/src/styles/worker.css | 280 +++++++++++++----- apps/frontend/src/types/worker-platform.ts | 16 + 7 files changed, 430 insertions(+), 77 deletions(-) diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index 979a32a5..03006011 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -680,6 +680,83 @@ export async function countWorkerAcceptedOrders(workerId: number | string): Prom return Number(result.rows[0]?.total || 0) } +export async function listWorkerCompletionLeaderboard(input: { + limit: number + currentWorkerId: number +}): Promise< + Array<{ + rank: number + workerId: number + displayName: string + levelName: string + acceptedOrderCount: number + }> +> { + const limit = Math.min(50, Math.max(1, Math.floor(Number(input.limit || 10)))) + const result = await query<{ + rank: number + worker_id: number + display_name: string + level_name: string | null + accepted_order_count: number + }>( + ` + WITH accepted_totals AS ( + SELECT + worker_id, + SUM(accepted_order_count)::int AS accepted_order_count, + MAX(last_accepted_at) AS last_accepted_at + FROM ( + SELECT + assigned_worker_id AS worker_id, + COUNT(*)::int AS accepted_order_count, + MAX(accepted_at) AS last_accepted_at + FROM work_orders + WHERE assigned_worker_id IS NOT NULL AND status = 'accepted' + GROUP BY assigned_worker_id + + UNION ALL + + SELECT + worker_id, + COALESCE(SUM(quantity), 0)::int AS accepted_order_count, + MAX(accepted_at) AS last_accepted_at + FROM work_order_shares + WHERE status = 'accepted' + GROUP BY worker_id + ) AS accepted_records + GROUP BY worker_id + ), + ranked_workers AS ( + SELECT + ROW_NUMBER() OVER ( + ORDER BY totals.accepted_order_count DESC, totals.last_accepted_at DESC NULLS LAST, wu.id ASC + )::int AS rank, + wu.id AS worker_id, + COALESCE(NULLIF(wu.display_name, ''), wu.username) AS display_name, + wl.name AS level_name, + totals.accepted_order_count + FROM worker_users wu + INNER JOIN accepted_totals totals ON totals.worker_id = wu.id + LEFT JOIN worker_levels wl ON wl.id = wu.level_id + WHERE wu.status = 'active' + ) + SELECT rank, worker_id, display_name, level_name, accepted_order_count + FROM ranked_workers + WHERE rank <= $1 OR worker_id = $2 + ORDER BY rank ASC + `, + [limit, Number(input.currentWorkerId)], + ) + return result.rows.map((row) => ({ + rank: Number(row.rank || 0), + workerId: Number(row.worker_id || 0), + displayName: String(row.display_name || '').trim(), + levelName: String(row.level_name || '').trim(), + acceptedOrderCount: Number(row.accepted_order_count || 0), + })) +} + export async function maybeUpgradeWorkerLevelWithClient( client: PoolClient, workerId: number, diff --git a/apps/backend/src/routes/worker.ts b/apps/backend/src/routes/worker.ts index 66927df9..4d4d0fd3 100644 --- a/apps/backend/src/routes/worker.ts +++ b/apps/backend/src/routes/worker.ts @@ -9,6 +9,7 @@ import { createWorkerWithdrawRequest, getWorkerProfile, getWorkerSessionSummary, + getWorkerHallLeaderboard, grabWorkerHallOrder, joinWorkerSharingOrder, listWorkerProfileFinanceRequests, @@ -187,6 +188,16 @@ router.get( }), ) +router.get( + '/hall/leaderboard', + requireActiveWorker, + createRouteHandler((req) => getWorkerHallLeaderboard(getRequiredWorkerSession(req)), { + successMessage: 'ok', + errorMessage: '读取接单排行榜失败', + scope: '[worker/hall/leaderboard]', + }), +) + router.post( '/hall/orders/:workOrderId/grab', requireActiveWorker, diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index e0dc13e6..a852a9ad 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -44,6 +44,7 @@ import { listAllWorkCategories, listPendingMaterialWorkOrdersByPlatformOrderId, listWorkerFinanceRequests, + listWorkerCompletionLeaderboard, listWorkerSharesByWorker, listWorkerWalletLedgers, listWorkerWorkOrderNotes, @@ -841,6 +842,22 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work } } +export async function getWorkerHallLeaderboard(session: WorkerSession) { + requireActiveWorkerSession(session) + const entries = await listWorkerCompletionLeaderboard({ + limit: 10, + currentWorkerId: session.workerId, + }) + const current = entries.find((entry) => entry.workerId === session.workerId) + return { + items: entries.filter((entry) => entry.rank <= 10), + current: { + rank: current?.rank || null, + acceptedOrderCount: current?.acceptedOrderCount || 0, + }, + } +} + export async function grabWorkerHallOrder(workOrderId: number | string, session: WorkerSession) { requireActiveWorkerSession(session) const worker = await getRequiredWorker(session.workerId) diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index a92890c7..847159a9 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -1,4 +1,4 @@ -import { ClockCircleOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons' +import { ClockCircleOutlined, ReloadOutlined, SearchOutlined, TrophyOutlined } from '@ant-design/icons' import { useQuery, useQueryClient } from '@tanstack/react-query' import { Alert, @@ -6,6 +6,7 @@ import { Button, Card, Descriptions, + Drawer, Empty, Input, InputNumber, @@ -23,6 +24,7 @@ import { useEffect, useState } from 'react' import { fetchWorkerHallOrders, + fetchWorkerHallLeaderboard, fetchWorkerMyOrders, fetchWorkerProfile, grabWorkerOrder, @@ -47,6 +49,7 @@ export default function WorkerHallPage() { const [pageSize, setPageSize] = useState(20) const [grabbingId, setGrabbingId] = useState(0) const [sharingOrder, setSharingOrder] = useState(null) + const [leaderboardOpen, setLeaderboardOpen] = useState(false) const [sharingQuantity, setSharingQuantity] = useState(1) const [submittingSharing, setSubmittingSharing] = useState(false) const [autoRefresh, setAutoRefresh] = useState(loadAutoRefreshPreference) @@ -105,6 +108,14 @@ export default function WorkerHallPage() { retry: false, }) + const leaderboardQuery = useQuery({ + queryKey: ['worker-hall-leaderboard'], + queryFn: () => fetchWorkerHallLeaderboard(), + enabled: leaderboardOpen, + staleTime: 30_000, + retry: false, + }) + const orders = ordersQuery.data?.data.items || [] const pagination = ordersQuery.data?.data.pagination const categories = ordersQuery.data?.data.categories || [] @@ -125,6 +136,7 @@ export default function WorkerHallPage() { queryClient.invalidateQueries({ queryKey: ['worker-profile'] }), queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }), queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }), + queryClient.invalidateQueries({ queryKey: ['worker-hall-leaderboard'] }), ]) } @@ -219,6 +231,15 @@ export default function WorkerHallPage() {
+ +