新增后台接单排行榜支持周期与自定义区间筛选
This commit is contained in:
@@ -277,3 +277,145 @@ function buildWorkerAcceptedOrdersCountSql() {
|
||||
) AS completed_orders
|
||||
`
|
||||
}
|
||||
|
||||
export type AdminLeaderboardPeriod = 'all' | 'daily' | 'weekly' | 'monthly'
|
||||
|
||||
/** 后台排行榜:预置周期按上海时区自然日/周/月计算;from/to 为自定义区间(YYYY-MM-DD,含起止日)。 */
|
||||
export async function listAdminWorkerLeaderboard(input: {
|
||||
period: AdminLeaderboardPeriod
|
||||
limit: number
|
||||
fromDate?: string | null
|
||||
toDate?: string | null
|
||||
}): Promise<
|
||||
Array<{
|
||||
rank: number
|
||||
workerId: number
|
||||
username: string
|
||||
displayName: string
|
||||
levelName: string
|
||||
workerStatus: string
|
||||
acceptedOrderCount: number
|
||||
rewardAmount: number
|
||||
lastAcceptedAt: string | null
|
||||
}>
|
||||
> {
|
||||
const limit = Math.min(100, Math.max(1, Math.floor(Number(input.limit || 50))))
|
||||
const periodUnit =
|
||||
input.period === 'daily'
|
||||
? 'day'
|
||||
: input.period === 'weekly'
|
||||
? 'week'
|
||||
: input.period === 'monthly'
|
||||
? 'month'
|
||||
: ''
|
||||
const result = await query<{
|
||||
rank: number
|
||||
worker_id: number
|
||||
username: string
|
||||
display_name: string
|
||||
level_name: string | null
|
||||
worker_status: string
|
||||
accepted_order_count: number
|
||||
reward_amount: string | number
|
||||
last_accepted_at: Date | string | null
|
||||
}>(
|
||||
`
|
||||
WITH leaderboard_window AS (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN $1::text <> '' THEN date_trunc($1::text, CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')
|
||||
AT TIME ZONE 'Asia/Shanghai'
|
||||
WHEN $2::date IS NOT NULL THEN $2::date::timestamp AT TIME ZONE 'Asia/Shanghai'
|
||||
ELSE NULL
|
||||
END AS starts_at,
|
||||
CASE
|
||||
WHEN $1::text = 'day' THEN (date_trunc('day', CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')
|
||||
AT TIME ZONE 'Asia/Shanghai') + INTERVAL '1 day'
|
||||
WHEN $1::text = 'week' THEN (date_trunc('week', CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')
|
||||
AT TIME ZONE 'Asia/Shanghai') + INTERVAL '7 days'
|
||||
WHEN $1::text = 'month' THEN (date_trunc('month', CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')
|
||||
AT TIME ZONE 'Asia/Shanghai') + INTERVAL '1 month'
|
||||
WHEN $3::date IS NOT NULL THEN ($3::date + 1)::timestamp AT TIME ZONE 'Asia/Shanghai'
|
||||
ELSE NULL
|
||||
END AS ends_at
|
||||
),
|
||||
accepted_totals AS (
|
||||
SELECT
|
||||
worker_id,
|
||||
COUNT(DISTINCT order_key)::int AS accepted_order_count,
|
||||
SUM(reward_amount)::bigint AS reward_amount,
|
||||
MAX(last_accepted_at) AS last_accepted_at
|
||||
FROM (
|
||||
SELECT
|
||||
assigned_worker_id AS worker_id,
|
||||
COALESCE(
|
||||
'order:' || NULLIF(order_id::text, ''),
|
||||
'platform_order:' || NULLIF(BTRIM(platform_order_id), ''),
|
||||
'work_order:' || id::text
|
||||
) AS order_key,
|
||||
reward_amount,
|
||||
MAX(accepted_at) AS last_accepted_at
|
||||
FROM work_orders
|
||||
CROSS JOIN leaderboard_window
|
||||
WHERE assigned_worker_id IS NOT NULL
|
||||
AND status = 'accepted'
|
||||
AND (starts_at IS NULL OR (accepted_at >= starts_at AND accepted_at < ends_at))
|
||||
GROUP BY assigned_worker_id, order_key, reward_amount
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
wos.worker_id,
|
||||
COALESCE(
|
||||
'order:' || NULLIF(wo.order_id::text, ''),
|
||||
'platform_order:' || NULLIF(BTRIM(wo.platform_order_id), ''),
|
||||
'work_order:' || wo.id::text
|
||||
) AS order_key,
|
||||
wos.share_reward AS reward_amount,
|
||||
MAX(wos.accepted_at) AS last_accepted_at
|
||||
FROM work_order_shares wos
|
||||
INNER JOIN work_orders wo ON wo.id = wos.work_order_id
|
||||
CROSS JOIN leaderboard_window
|
||||
WHERE wos.status = 'accepted'
|
||||
AND (starts_at IS NULL OR (wos.accepted_at >= starts_at AND wos.accepted_at < ends_at))
|
||||
GROUP BY wos.worker_id, order_key, wos.share_reward
|
||||
) 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,
|
||||
wu.username,
|
||||
wu.display_name,
|
||||
wu.status AS worker_status,
|
||||
wl.name AS level_name,
|
||||
totals.accepted_order_count,
|
||||
totals.reward_amount,
|
||||
totals.last_accepted_at
|
||||
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
|
||||
)
|
||||
SELECT rank, worker_id, username, display_name, level_name, worker_status,
|
||||
accepted_order_count, reward_amount, last_accepted_at
|
||||
FROM ranked_workers
|
||||
ORDER BY rank ASC
|
||||
LIMIT $4
|
||||
`,
|
||||
[periodUnit, input.fromDate || null, input.toDate || null, limit],
|
||||
)
|
||||
return result.rows.map((row) => ({
|
||||
rank: Number(row.rank || 0),
|
||||
workerId: Number(row.worker_id || 0),
|
||||
username: String(row.username || '').trim(),
|
||||
displayName: String(row.display_name || '').trim(),
|
||||
levelName: String(row.level_name || '').trim(),
|
||||
workerStatus: String(row.worker_status || ''),
|
||||
acceptedOrderCount: Number(row.accepted_order_count || 0),
|
||||
rewardAmount: Number(row.reward_amount || 0),
|
||||
lastAcceptedAt: row.last_accepted_at ? new Date(row.last_accepted_at).toISOString() : null,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import adminWorkerPlatformOrdersRoutes from './worker-platform/orders.js'
|
||||
import adminWorkerPlatformReviewRoutes from './worker-platform/review.js'
|
||||
import adminWorkerPlatformAcceptanceRoutes from './worker-platform/acceptance.js'
|
||||
import adminWorkerPlatformAfterSalesRoutes from './worker-platform/after-sales.js'
|
||||
import adminWorkerPlatformLeaderboardRoutes from './worker-platform/leaderboard.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -17,5 +18,6 @@ router.use(adminWorkerPlatformOrdersRoutes)
|
||||
router.use(adminWorkerPlatformReviewRoutes)
|
||||
router.use(adminWorkerPlatformAfterSalesRoutes)
|
||||
router.use(adminWorkerPlatformAcceptanceRoutes)
|
||||
router.use(adminWorkerPlatformLeaderboardRoutes)
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { getAdminWorkerLeaderboard } from '../../../services/worker-platform/index.js'
|
||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get(
|
||||
'/worker-platform/leaderboard',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler((req) => getAdminWorkerLeaderboard(req.query), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取接单排行榜失败',
|
||||
scope: '[admin/worker-platform/leaderboard]',
|
||||
}),
|
||||
)
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
listAdminWorkerLeaderboard,
|
||||
type AdminLeaderboardPeriod,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
const PERIODS: AdminLeaderboardPeriod[] = ['all', 'daily', 'weekly', 'monthly']
|
||||
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
||||
const MAX_CUSTOM_RANGE_DAYS = 366
|
||||
|
||||
function normalizeDate(value: unknown): string | null {
|
||||
const text = String(value || '').trim()
|
||||
return DATE_PATTERN.test(text) ? text : null
|
||||
}
|
||||
|
||||
/** 后台接单排行榜读取:预置周期或自定义日期区间(含起止日)+ 完成单数与奖励汇总。 */
|
||||
export async function getAdminWorkerLeaderboard(query: JsonObject = {}) {
|
||||
const requested = String(query.period || 'all').trim() as AdminLeaderboardPeriod
|
||||
const period = PERIODS.includes(requested) ? requested : 'all'
|
||||
const limit = Number(query.limit || 50)
|
||||
const fromDate = normalizeDate(query.from)
|
||||
const toDate = normalizeDate(query.to)
|
||||
if ((fromDate || toDate) && !(fromDate && toDate)) {
|
||||
throw new Error('自定义区间需要同时提供开始与结束日期')
|
||||
}
|
||||
if (fromDate && toDate) {
|
||||
const fromTime = new Date(`${fromDate}T00:00:00Z`).getTime()
|
||||
const toTime = new Date(`${toDate}T00:00:00Z`).getTime()
|
||||
if (fromTime > toTime) {
|
||||
throw new Error('自定义区间的开始日期不能晚于结束日期')
|
||||
}
|
||||
if ((toTime - fromTime) / 86400000 > MAX_CUSTOM_RANGE_DAYS) {
|
||||
throw new Error('自定义区间最长不能超过一年')
|
||||
}
|
||||
}
|
||||
const items = await listAdminWorkerLeaderboard({ period, limit, fromDate, toDate })
|
||||
return { period: fromDate && toDate ? 'custom' : period, from: fromDate, to: toDate, items }
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export * from './after-sales-service.js'
|
||||
export * from './admin-service.js'
|
||||
export * from './admin-worker-finance-service.js'
|
||||
export * from './admin-worker-catalog-service.js'
|
||||
export * from './admin-worker-leaderboard-service.js'
|
||||
export * from './admin-kuaishou-match-service.js'
|
||||
export * from './admin-worker-config-service.js'
|
||||
export * from './admin-worker-user-service.js'
|
||||
|
||||
Reference in New Issue
Block a user