增加每日接单排行榜

This commit is contained in:
yml2213
2026-08-19 18:55:04 +08:00
parent ca34187b0b
commit 7b92d6b219
8 changed files with 65 additions and 15 deletions
@@ -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';
@@ -960,6 +960,7 @@ export async function countWorkerAcceptedOrders(workerId: number | string): Prom
export async function listWorkerCompletionLeaderboard(input: { export async function listWorkerCompletionLeaderboard(input: {
limit: number limit: number
currentWorkerId: number currentWorkerId: number
period?: 'all' | 'daily'
}): Promise< }): Promise<
Array<{ Array<{
rank: number 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 limit = Math.min(50, Math.max(1, Math.floor(Number(input.limit || 10))))
const dailyOnly = input.period === 'daily'
const result = await query<{ const result = await query<{
rank: number rank: number
worker_id: number worker_id: number
@@ -978,7 +980,12 @@ export async function listWorkerCompletionLeaderboard(input: {
accepted_order_count: number 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 SELECT
worker_id, worker_id,
SUM(accepted_order_count)::int AS accepted_order_count, SUM(accepted_order_count)::int AS accepted_order_count,
@@ -989,7 +996,13 @@ export async function listWorkerCompletionLeaderboard(input: {
COUNT(*)::int AS accepted_order_count, COUNT(*)::int AS accepted_order_count,
MAX(accepted_at) AS last_accepted_at MAX(accepted_at) AS last_accepted_at
FROM work_orders 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 GROUP BY assigned_worker_id
UNION ALL UNION ALL
@@ -999,7 +1012,12 @@ export async function listWorkerCompletionLeaderboard(input: {
COALESCE(SUM(quantity), 0)::int AS accepted_order_count, COALESCE(SUM(quantity), 0)::int AS accepted_order_count,
MAX(accepted_at) AS last_accepted_at MAX(accepted_at) AS last_accepted_at
FROM work_order_shares FROM work_order_shares
CROSS JOIN leaderboard_day
WHERE status = 'accepted' 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 GROUP BY worker_id
) AS accepted_records ) AS accepted_records
GROUP BY worker_id GROUP BY worker_id
@@ -1023,7 +1041,7 @@ export async function listWorkerCompletionLeaderboard(input: {
WHERE rank <= $1 OR worker_id = $2 WHERE rank <= $1 OR worker_id = $2
ORDER BY rank ASC ORDER BY rank ASC
`, `,
[limit, Number(input.currentWorkerId)], [limit, Number(input.currentWorkerId), dailyOnly],
) )
return result.rows.map((row) => ({ return result.rows.map((row) => ({
rank: Number(row.rank || 0), rank: Number(row.rank || 0),
+5 -2
View File
@@ -304,11 +304,14 @@ router.get(
router.get( router.get(
'/hall/leaderboard', '/hall/leaderboard',
requireActiveWorker, requireActiveWorker,
createRouteHandler((req) => getWorkerHallLeaderboard(getRequiredWorkerSession(req)), { createRouteHandler(
(req) => getWorkerHallLeaderboard(getRequiredWorkerSession(req), req.query?.period),
{
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取接单排行榜失败', errorMessage: '读取接单排行榜失败',
scope: '[worker/hall/leaderboard]', scope: '[worker/hall/leaderboard]',
}), },
),
) )
router.post( router.post(
@@ -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) requireActiveWorkerSession(session)
const leaderboardPeriod = period === 'daily' ? 'daily' : 'all'
const entries = await listWorkerCompletionLeaderboard({ const entries = await listWorkerCompletionLeaderboard({
limit: 10, limit: 10,
currentWorkerId: session.workerId, currentWorkerId: session.workerId,
period: leaderboardPeriod,
}) })
const current = entries.find((entry) => entry.workerId === session.workerId) const current = entries.find((entry) => entry.workerId === session.workerId)
return { return {
@@ -34,7 +34,7 @@ import {
grabWorkerOrder, grabWorkerOrder,
joinWorkerSharingOrder, joinWorkerSharingOrder,
} from '@/services/worker' } 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 { useIsMobile } from '@/utils/use-is-mobile'
import { import {
resolveHallDepositAmount, resolveHallDepositAmount,
@@ -60,6 +60,7 @@ export default function WorkerHallPage() {
const [grabbingId, setGrabbingId] = useState(0) const [grabbingId, setGrabbingId] = useState(0)
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null) const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [leaderboardOpen, setLeaderboardOpen] = useState(false) const [leaderboardOpen, setLeaderboardOpen] = useState(false)
const [leaderboardPeriod, setLeaderboardPeriod] = useState<WorkerLeaderboardPeriod>('all')
const [sharingQuantity, setSharingQuantity] = useState(1) const [sharingQuantity, setSharingQuantity] = useState(1)
const [submittingSharing, setSubmittingSharing] = useState(false) const [submittingSharing, setSubmittingSharing] = useState(false)
const loadMoreRef = useRef<HTMLDivElement | null>(null) const loadMoreRef = useRef<HTMLDivElement | null>(null)
@@ -117,8 +118,8 @@ export default function WorkerHallPage() {
}) })
const leaderboardQuery = useQuery({ const leaderboardQuery = useQuery({
queryKey: ['worker-hall-leaderboard'], queryKey: ['worker-hall-leaderboard', leaderboardPeriod],
queryFn: () => fetchWorkerHallLeaderboard(), queryFn: () => fetchWorkerHallLeaderboard(leaderboardPeriod),
enabled: leaderboardOpen, enabled: leaderboardOpen,
retry: false, retry: false,
}) })
@@ -778,8 +779,19 @@ export default function WorkerHallPage() {
onClose={() => setLeaderboardOpen(false)} onClose={() => setLeaderboardOpen(false)}
> >
<div className="worker-hall-leaderboard"> <div className="worker-hall-leaderboard">
<Tabs
activeKey={leaderboardPeriod}
className="worker-hall-leaderboard-tabs"
items={[
{ key: 'all', label: '总榜' },
{ key: 'daily', label: '每日榜' },
]}
onChange={(key) => setLeaderboardPeriod(key === 'daily' ? 'daily' : 'all')}
/>
<Typography.Text type="secondary" className="worker-hall-leaderboard-caption"> <Typography.Text type="secondary" className="worker-hall-leaderboard-caption">
{leaderboardPeriod === 'daily'
? '按今日验收完成数量排行(北京时间)'
: '按累计验收完成数量排行'}
</Typography.Text> </Typography.Text>
{leaderboardQuery.isLoading ? ( {leaderboardQuery.isLoading ? (
<div className="worker-hall-leaderboard-loading"> <div className="worker-hall-leaderboard-loading">
+3 -2
View File
@@ -8,6 +8,7 @@ import type {
WorkerFinanceRequest, WorkerFinanceRequest,
WorkerHallOrdersResponse, WorkerHallOrdersResponse,
WorkerLeaderboardResponse, WorkerLeaderboardResponse,
WorkerLeaderboardPeriod,
WorkerListResponse, WorkerListResponse,
WorkerLoginResponse, WorkerLoginResponse,
WorkerOrderOverviewResponse, WorkerOrderOverviewResponse,
@@ -145,8 +146,8 @@ export function fetchWorkerHallOrders(params?: Record<string, unknown>) {
return apiGet<WorkerHallOrdersResponse>('/api/v1/worker/hall/orders', params) return apiGet<WorkerHallOrdersResponse>('/api/v1/worker/hall/orders', params)
} }
export function fetchWorkerHallLeaderboard() { export function fetchWorkerHallLeaderboard(period: WorkerLeaderboardPeriod = 'all') {
return apiGet<WorkerLeaderboardResponse>('/api/v1/worker/hall/leaderboard') return apiGet<WorkerLeaderboardResponse>('/api/v1/worker/hall/leaderboard', { period })
} }
export function grabWorkerOrder(workOrderId: number) { export function grabWorkerOrder(workOrderId: number) {
+4
View File
@@ -1915,6 +1915,10 @@ body {
font-size: 12px; font-size: 12px;
} }
.worker-hall-leaderboard-tabs .ant-tabs-nav {
margin-bottom: 0;
}
.worker-hall-leaderboard-loading { .worker-hall-leaderboard-loading {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -473,6 +473,8 @@ export type WorkerLeaderboardEntry = {
acceptedOrderCount: number acceptedOrderCount: number
} }
export type WorkerLeaderboardPeriod = 'all' | 'daily'
export type WorkerLeaderboardResponse = { export type WorkerLeaderboardResponse = {
items: WorkerLeaderboardEntry[] items: WorkerLeaderboardEntry[]
current: { current: {