增加每日接单排行榜
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -304,11 +304,14 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
'/hall/leaderboard',
|
'/hall/leaderboard',
|
||||||
requireActiveWorker,
|
requireActiveWorker,
|
||||||
createRouteHandler((req) => getWorkerHallLeaderboard(getRequiredWorkerSession(req)), {
|
createRouteHandler(
|
||||||
successMessage: 'ok',
|
(req) => getWorkerHallLeaderboard(getRequiredWorkerSession(req), req.query?.period),
|
||||||
errorMessage: '读取接单排行榜失败',
|
{
|
||||||
scope: '[worker/hall/leaderboard]',
|
successMessage: 'ok',
|
||||||
}),
|
errorMessage: '读取接单排行榜失败',
|
||||||
|
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">
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user