From fe2a28454d8f63c37c2a88ccb90aa430b1fda2e2 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 6 Sep 2026 12:55:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=89=93=E6=89=8B=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=AD=9B=E9=80=89=E6=B1=87=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/repositories/worker-platform/types.ts | 12 + .../worker-platform/worker-repo.ts | 71 ++++- .../admin-worker-user-service.ts | 17 +- .../src/pages/admin/panels/WorkersPanel.tsx | 247 +++++++++++------- .../src/services/admin/worker-platform.ts | 3 +- apps/frontend/src/types/worker-platform.ts | 16 ++ 6 files changed, 268 insertions(+), 98 deletions(-) diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index bee23f83..ad82a106 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -44,6 +44,18 @@ export type WorkerUserRow = { last_renamed_at?: string | null } +export type WorkerUserStatistics = { + totalCount: number + statusCounts: Record + workerTypeCounts: Record + availableAmount: number + frozenDepositAmount: number + pendingUnfreezeAmount: number + totalCreditedAmount: number + totalSettledAmount: number + timeoutOrderCount: number +} + export type WorkerSessionRow = { id: number session_id: string diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index 35b7b7f7..bf61f4e4 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -1,6 +1,6 @@ import { query, withTransaction } from '../../db/client.js' import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js' -import type { CreateWorkerInput, ListInput, WorkerUserRow } from './types.js' +import type { CreateWorkerInput, ListInput, WorkerUserRow, WorkerUserStatistics } from './types.js' import type { PoolClient } from 'pg' const WORKER_USER_SELECT = ` @@ -265,6 +265,75 @@ export async function listWorkerUsers({ return { items: itemsResult.rows, total: Number(totalResult.rows[0]?.total || 0) } } +/** 按筛选条件汇总打手,统计范围不受当前分页影响。 */ +export async function getWorkerUserStatistics( + input: Pick = {}, +): Promise { + const { whereClause, params } = buildWorkerUserWhere(input) + const statisticsResult = await query<{ + status: string + worker_type: string + total_count: number + available_amount: number + frozen_deposit_amount: number + pending_unfreeze_amount: number + total_credited_amount: number + total_settled_amount: number + }>( + ` + SELECT + wu.status, + wu.worker_type, + COUNT(*)::int AS total_count, + COALESCE(SUM(COALESCE(ww.available_amount, 0)), 0)::bigint AS available_amount, + COALESCE(SUM(COALESCE(ww.frozen_deposit_amount, 0)), 0)::bigint AS frozen_deposit_amount, + COALESCE(SUM(COALESCE(ww.pending_unfreeze_amount, 0)), 0)::bigint AS pending_unfreeze_amount, + COALESCE(SUM(COALESCE(ww.total_credited_amount, 0)), 0)::bigint AS total_credited_amount, + COALESCE(SUM(COALESCE(ww.total_settled_amount, 0)), 0)::bigint AS total_settled_amount + FROM worker_users wu + LEFT JOIN worker_wallets ww ON ww.worker_id = wu.id + ${whereClause} + GROUP BY wu.status, wu.worker_type + `, + params, + ) + + const timeoutResult = await query<{ total: number }>( + ` + SELECT COUNT(*)::int AS total + FROM work_order_events e + INNER JOIN worker_users wu ON e.payload_json->>'workerId' = wu.id::text + ${whereClause ? `${whereClause} AND` : 'WHERE'} e.event_type LIKE 'timeout_%' + `, + params, + ) + + const statistics: WorkerUserStatistics = { + totalCount: 0, + statusCounts: {}, + workerTypeCounts: {}, + availableAmount: 0, + frozenDepositAmount: 0, + pendingUnfreezeAmount: 0, + totalCreditedAmount: 0, + totalSettledAmount: 0, + timeoutOrderCount: Number(timeoutResult.rows[0]?.total || 0), + } + for (const row of statisticsResult.rows) { + const count = Number(row.total_count || 0) + statistics.totalCount += count + statistics.statusCounts[row.status] = (statistics.statusCounts[row.status] || 0) + count + statistics.workerTypeCounts[row.worker_type] = + (statistics.workerTypeCounts[row.worker_type] || 0) + count + statistics.availableAmount += Number(row.available_amount || 0) + statistics.frozenDepositAmount += Number(row.frozen_deposit_amount || 0) + statistics.pendingUnfreezeAmount += Number(row.pending_unfreeze_amount || 0) + statistics.totalCreditedAmount += Number(row.total_credited_amount || 0) + statistics.totalSettledAmount += Number(row.total_settled_amount || 0) + } + return statistics +} + export { addWorkerWalletCredit, ensureWorkerWallet, diff --git a/apps/backend/src/services/worker-platform/admin-worker-user-service.ts b/apps/backend/src/services/worker-platform/admin-worker-user-service.ts index 6fcd742b..beb2be26 100644 --- a/apps/backend/src/services/worker-platform/admin-worker-user-service.ts +++ b/apps/backend/src/services/worker-platform/admin-worker-user-service.ts @@ -1,5 +1,6 @@ import { countTimeoutEventsByWorkerIds, + getWorkerUserStatistics, getWorkerLevelById, listWorkerUsers, revokeAllWorkerSessions, @@ -14,6 +15,7 @@ import { hashWorkerPassword, mapWorkerUser, normalizeAmountFen, + normalizeBoolean, normalizeOptionalId, normalizeReviewStatus, normalizeSessionVersion, @@ -29,15 +31,21 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) { const pageSize = normalizePageSize(query.pageSize) const workerId = normalizeOptionalId(query.workerId ?? query.worker_id) || 0 const levelId = normalizeOptionalId(query.levelId ?? query.level_id) || 0 - const { items, total } = await listWorkerUsers({ - page, - pageSize, + const filters = { workerId, levelId, status: String(query.status || '').trim(), keyword: String(query.keyword || '').trim(), workerType: String(query.workerType || query.worker_type || '').trim(), - }) + } + const includeStatistics = normalizeBoolean( + query.includeStatistics ?? query.include_statistics, + false, + ) + const [{ items, total }, statistics] = await Promise.all([ + listWorkerUsers({ page, pageSize, ...filters }), + includeStatistics ? getWorkerUserStatistics(filters) : Promise.resolve(undefined), + ]) const timeoutCounts = await countTimeoutEventsByWorkerIds(items.map((item) => Number(item.id))) return { items: items.map((item) => ({ @@ -45,6 +53,7 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) { timeoutOrderCount: timeoutCounts.get(Number(item.id)) || 0, })), pagination: { page, pageSize, total }, + ...(statistics ? { statistics } : {}), } } diff --git a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx index 24e34ccf..b6b5b992 100644 --- a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx @@ -33,6 +33,7 @@ import type { AdminWorkerWithdrawalAccount, UploadedFile, WorkerUser, + WorkerUserStatistics, } from '@/types/worker-platform' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import WorkerDetailDrawer from './WorkerDetailDrawer' @@ -54,6 +55,62 @@ type PersonalDepositFreeAmountFormValues = { depositFreeAmount?: number } +const WORKER_STATISTICS_STATUS_ORDER = ['pending_review', 'active', 'rejected', 'disabled'] +const WORKER_STATISTICS_TYPE_ORDER = ['internal', 'external'] + +function WorkerStatisticsBar({ statistics }: { statistics?: WorkerUserStatistics }) { + if (!statistics) return null + const statusItems = WORKER_STATISTICS_STATUS_ORDER.map((status) => ({ + status, + count: Number(statistics.statusCounts?.[status] || 0), + })) + const otherStatusItems = Object.entries(statistics.statusCounts || {}) + .filter(([status]) => !WORKER_STATISTICS_STATUS_ORDER.includes(status)) + .map(([status, count]) => ({ status, count: Number(count || 0) })) + const typeItems = WORKER_STATISTICS_TYPE_ORDER.map((workerType) => ({ + workerType, + count: Number(statistics.workerTypeCounts?.[workerType] || 0), + })) + const otherTypeItems = Object.entries(statistics.workerTypeCounts || {}) + .filter(([workerType]) => !WORKER_STATISTICS_TYPE_ORDER.includes(workerType)) + .map(([workerType, count]) => ({ workerType, count: Number(count || 0) })) + const workerTypeLabels: Record = { + internal: '内部打手', + external: '外部打手', + } + + return ( + +
+ 本次筛选统计共 {statistics.totalCount} 名,余额合计{' '} + {formatMoney(statistics.availableAmount)},冻结押金合计{' '} + {formatMoney(statistics.frozenDepositAmount)},待解冻合计{' '} + {formatMoney(statistics.pendingUnfreezeAmount)},累计结算{' '} + {formatMoney(statistics.totalSettledAmount)},累计充值{' '} + {formatMoney(statistics.totalCreditedAmount)}。 +
+
+ 其中: + {[...statusItems, ...otherStatusItems].map(({ status, count }, index) => ( + + {index > 0 ? ',' : ''} + {formatWorkerStatus(status)} + {count}名 + + ))} + {[...typeItems, ...otherTypeItems].map(({ workerType, count }, index) => ( + + {index === 0 ? ',' : ''} + {workerTypeLabels[workerType] || workerType} + {count}名 + + ))} + ,超时订单{statistics.timeoutOrderCount}单 +
+
+ ) +} + export default function WorkersPanel() { const { message, modal } = App.useApp() const queryClient = useQueryClient() @@ -123,6 +180,7 @@ export default function WorkersPanel() { workerType, levelId: levelId || undefined, workerId: targetWorkerId || undefined, + includeStatistics: true, page, pageSize, }), @@ -491,97 +549,102 @@ export default function WorkersPanel() { ] return ( - - setKeywordInput(event.target.value)} - onPressEnter={searchWorkers} - /> - { - setWorkerType(nextType || '') - setPage(1) - }} - /> - setKeywordInput(event.target.value)} + onPressEnter={searchWorkers} + /> + { + setWorkerType(nextType || '') + setPage(1) + }} + /> +