diff --git a/apps/backend/src/db/migrations/041_daily_worker_leaderboard.sql b/apps/backend/src/db/migrations/041_daily_worker_leaderboard.sql new file mode 100644 index 00000000..04980055 --- /dev/null +++ b/apps/backend/src/db/migrations/041_daily_worker_leaderboard.sql @@ -0,0 +1,8 @@ +-- 每日排行榜按验收时间筛选,避免全表扫描已完成工单与拼单记录。 +CREATE INDEX IF NOT EXISTS idx_work_orders_accepted_at_worker + ON work_orders(accepted_at DESC, assigned_worker_id) + WHERE status = 'accepted' AND assigned_worker_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_work_order_shares_accepted_at_worker + ON work_order_shares(accepted_at DESC, worker_id) + WHERE status = 'accepted'; diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index 2396a2c8..331265c9 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -960,6 +960,7 @@ export async function countWorkerAcceptedOrders(workerId: number | string): Prom export async function listWorkerCompletionLeaderboard(input: { limit: number currentWorkerId: number + period?: 'all' | 'daily' }): Promise< Array<{ rank: number @@ -970,6 +971,7 @@ export async function listWorkerCompletionLeaderboard(input: { }> > { const limit = Math.min(50, Math.max(1, Math.floor(Number(input.limit || 10)))) + const dailyOnly = input.period === 'daily' const result = await query<{ rank: number worker_id: number @@ -978,7 +980,12 @@ export async function listWorkerCompletionLeaderboard(input: { accepted_order_count: number }>( ` - WITH accepted_totals AS ( + WITH leaderboard_day AS ( + SELECT + date_trunc('day', CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai') + AT TIME ZONE 'Asia/Shanghai' AS starts_at + ), + accepted_totals AS ( SELECT worker_id, SUM(accepted_order_count)::int AS accepted_order_count, @@ -989,7 +996,13 @@ export async function listWorkerCompletionLeaderboard(input: { 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' + CROSS JOIN leaderboard_day + WHERE assigned_worker_id IS NOT NULL + AND status = 'accepted' + AND ( + $3::boolean = false + OR (accepted_at >= leaderboard_day.starts_at AND accepted_at < leaderboard_day.starts_at + INTERVAL '1 day') + ) GROUP BY assigned_worker_id UNION ALL @@ -999,7 +1012,12 @@ export async function listWorkerCompletionLeaderboard(input: { COALESCE(SUM(quantity), 0)::int AS accepted_order_count, MAX(accepted_at) AS last_accepted_at FROM work_order_shares + CROSS JOIN leaderboard_day WHERE status = 'accepted' + AND ( + $3::boolean = false + OR (accepted_at >= leaderboard_day.starts_at AND accepted_at < leaderboard_day.starts_at + INTERVAL '1 day') + ) GROUP BY worker_id ) AS accepted_records GROUP BY worker_id @@ -1023,7 +1041,7 @@ export async function listWorkerCompletionLeaderboard(input: { WHERE rank <= $1 OR worker_id = $2 ORDER BY rank ASC `, - [limit, Number(input.currentWorkerId)], + [limit, Number(input.currentWorkerId), dailyOnly], ) return result.rows.map((row) => ({ rank: Number(row.rank || 0), diff --git a/apps/backend/src/routes/worker.ts b/apps/backend/src/routes/worker.ts index 5d18e401..e7d7e3ce 100644 --- a/apps/backend/src/routes/worker.ts +++ b/apps/backend/src/routes/worker.ts @@ -304,11 +304,14 @@ router.get( router.get( '/hall/leaderboard', requireActiveWorker, - createRouteHandler((req) => getWorkerHallLeaderboard(getRequiredWorkerSession(req)), { - successMessage: 'ok', - errorMessage: '读取接单排行榜失败', - scope: '[worker/hall/leaderboard]', - }), + createRouteHandler( + (req) => getWorkerHallLeaderboard(getRequiredWorkerSession(req), req.query?.period), + { + successMessage: 'ok', + errorMessage: '读取接单排行榜失败', + scope: '[worker/hall/leaderboard]', + }, + ), ) router.post( diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 37154f38..bccab000 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -1202,11 +1202,13 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work } } -export async function getWorkerHallLeaderboard(session: WorkerSession) { +export async function getWorkerHallLeaderboard(session: WorkerSession, period: unknown = 'all') { requireActiveWorkerSession(session) + const leaderboardPeriod = period === 'daily' ? 'daily' : 'all' const entries = await listWorkerCompletionLeaderboard({ limit: 10, currentWorkerId: session.workerId, + period: leaderboardPeriod, }) const current = entries.find((entry) => entry.workerId === session.workerId) return { diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index b68e7a67..90a7e316 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -34,7 +34,7 @@ import { grabWorkerOrder, joinWorkerSharingOrder, } from '@/services/worker' -import type { WorkOrder, WorkerUser } from '@/types/worker-platform' +import type { WorkOrder, WorkerLeaderboardPeriod, WorkerUser } from '@/types/worker-platform' import { useIsMobile } from '@/utils/use-is-mobile' import { resolveHallDepositAmount, @@ -60,6 +60,7 @@ export default function WorkerHallPage() { const [grabbingId, setGrabbingId] = useState(0) const [sharingOrder, setSharingOrder] = useState(null) const [leaderboardOpen, setLeaderboardOpen] = useState(false) + const [leaderboardPeriod, setLeaderboardPeriod] = useState('all') const [sharingQuantity, setSharingQuantity] = useState(1) const [submittingSharing, setSubmittingSharing] = useState(false) const loadMoreRef = useRef(null) @@ -117,8 +118,8 @@ export default function WorkerHallPage() { }) const leaderboardQuery = useQuery({ - queryKey: ['worker-hall-leaderboard'], - queryFn: () => fetchWorkerHallLeaderboard(), + queryKey: ['worker-hall-leaderboard', leaderboardPeriod], + queryFn: () => fetchWorkerHallLeaderboard(leaderboardPeriod), enabled: leaderboardOpen, retry: false, }) @@ -778,8 +779,19 @@ export default function WorkerHallPage() { onClose={() => setLeaderboardOpen(false)} >
+ setLeaderboardPeriod(key === 'daily' ? 'daily' : 'all')} + /> - 按累计验收完成数量排行 + {leaderboardPeriod === 'daily' + ? '按今日验收完成数量排行(北京时间)' + : '按累计验收完成数量排行'} {leaderboardQuery.isLoading ? (
diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index 9969aed4..7a2ef1fd 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -8,6 +8,7 @@ import type { WorkerFinanceRequest, WorkerHallOrdersResponse, WorkerLeaderboardResponse, + WorkerLeaderboardPeriod, WorkerListResponse, WorkerLoginResponse, WorkerOrderOverviewResponse, @@ -145,8 +146,8 @@ export function fetchWorkerHallOrders(params?: Record) { return apiGet('/api/v1/worker/hall/orders', params) } -export function fetchWorkerHallLeaderboard() { - return apiGet('/api/v1/worker/hall/leaderboard') +export function fetchWorkerHallLeaderboard(period: WorkerLeaderboardPeriod = 'all') { + return apiGet('/api/v1/worker/hall/leaderboard', { period }) } export function grabWorkerOrder(workOrderId: number) { diff --git a/apps/frontend/src/styles/worker.css b/apps/frontend/src/styles/worker.css index e4587c6b..4e863131 100644 --- a/apps/frontend/src/styles/worker.css +++ b/apps/frontend/src/styles/worker.css @@ -1915,6 +1915,10 @@ body { font-size: 12px; } +.worker-hall-leaderboard-tabs .ant-tabs-nav { + margin-bottom: 0; +} + .worker-hall-leaderboard-loading { display: flex; justify-content: center; diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index dc5ed343..8b02c802 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -473,6 +473,8 @@ export type WorkerLeaderboardEntry = { acceptedOrderCount: number } +export type WorkerLeaderboardPeriod = 'all' | 'daily' + export type WorkerLeaderboardResponse = { items: WorkerLeaderboardEntry[] current: {