diff --git a/apps/backend/src/repositories/worker-platform/work-order-deposit-query-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-deposit-query-repo.ts index e467f496..ffa65da4 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-deposit-query-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-deposit-query-repo.ts @@ -46,3 +46,39 @@ export async function listDueDepositUnfreezes({ ) return result.rows } + +/** 管理端按打手列出全部解冻记录(含未到期与已释放),观察待解冻到账进度。 */ +export async function listWorkerDepositUnfreezes({ + workerId, + status = '', + page = 1, + pageSize = 20, +}: { + workerId: number + status?: string + page?: number + pageSize?: number +}): Promise<{ items: WorkerDepositUnfreezeRow[]; total: number }> { + const params: unknown[] = [Number(workerId)] + const normalizedStatus = String(status || '').trim() + let statusClause = '' + if (normalizedStatus) { + params.push(normalizedStatus) + statusClause = ` AND status = $${params.length}` + } + const totalResult = await query<{ total: number }>( + `SELECT COUNT(*)::int AS total FROM worker_deposit_unfreezes WHERE worker_id = $1${statusClause}`, + params, + ) + params.push(pageSize, (page - 1) * pageSize) + const result = await query( + ` + SELECT * FROM worker_deposit_unfreezes + WHERE worker_id = $1${statusClause} + ORDER BY unfreeze_at DESC, id DESC + LIMIT $${params.length - 1} OFFSET $${params.length} + `, + params, + ) + return { items: result.rows, total: Number(totalResult.rows[0]?.total || 0) } +} diff --git a/apps/backend/src/routes/admin/worker-platform/workers.ts b/apps/backend/src/routes/admin/worker-platform/workers.ts index 45be1037..3820330b 100644 --- a/apps/backend/src/routes/admin/worker-platform/workers.ts +++ b/apps/backend/src/routes/admin/worker-platform/workers.ts @@ -2,7 +2,10 @@ import { Router } from 'express' import { creditAdminWorkerWallet, + getAdminWorkerDetail, + listAdminWorkerDepositUnfreezes, listAdminWorkerUsers, + listAdminWorkerWalletLedgers, listAdminWorkerWithdrawalAccounts, resetAdminWorkerPassword, reviewAdminWorkerUser, @@ -22,6 +25,42 @@ router.get( }), ) +router.get( + '/worker-platform/workers/:workerId/detail', + requireAdminRoles(['admin', 'operator', 'support']), + createJsonHandler((req) => getAdminWorkerDetail(String(req.params.workerId || '')), { + successMessage: 'ok', + errorMessage: '读取打手详情失败', + scope: '[admin/worker-platform/workers/:workerId/detail]', + }), +) + +router.get( + '/worker-platform/workers/:workerId/wallet-ledgers', + requireAdminRoles(['admin', 'operator', 'support']), + createJsonHandler( + (req) => listAdminWorkerWalletLedgers(String(req.params.workerId || ''), req.query), + { + successMessage: 'ok', + errorMessage: '读取打手资金流水失败', + scope: '[admin/worker-platform/workers/:workerId/wallet-ledgers]', + }, + ), +) + +router.get( + '/worker-platform/workers/:workerId/deposit-unfreezes', + requireAdminRoles(['admin', 'operator', 'support']), + createJsonHandler( + (req) => listAdminWorkerDepositUnfreezes(String(req.params.workerId || ''), req.query), + { + successMessage: 'ok', + errorMessage: '读取打手待解冻记录失败', + scope: '[admin/worker-platform/workers/:workerId/deposit-unfreezes]', + }, + ), +) + router.post( '/worker-platform/workers/:workerId/review', requireAdminRoles(['admin', 'operator']), diff --git a/apps/backend/src/services/worker-platform/admin-worker-detail-service.ts b/apps/backend/src/services/worker-platform/admin-worker-detail-service.ts new file mode 100644 index 00000000..46fb1353 --- /dev/null +++ b/apps/backend/src/services/worker-platform/admin-worker-detail-service.ts @@ -0,0 +1,80 @@ +import type { WorkerDepositUnfreezeRow } from '../../repositories/worker-platform/index.js' +import { + countWorkerAcceptedOrders, + countWorkerActiveOrders, + countWorkerTimeoutEvents, + getWorkerOrderOverview, + listWorkerDepositUnfreezes, + listWorkerWalletLedgers, +} from '../../repositories/worker-platform/index.js' +import type { JsonObject } from '../../types/json.js' +import { normalizePage, normalizePageSize } from '../admin/admin-query-utils.js' +import { mapWalletLedger, mapWorkerUser } from './mappers.js' +import { getRequiredWorker } from './worker-session-context-service.js' + +/** 管理端打手详情:聚合账号钱包、接单统计与订单概览,全部只读。 */ +export async function getAdminWorkerDetail(workerId: number | string) { + const worker = await getRequiredWorker(workerId) + const [activeOrderCount, acceptedOrderCount, timeoutOrderCount, orderOverview] = + await Promise.all([ + countWorkerActiveOrders(worker.id), + countWorkerAcceptedOrders(worker.id), + countWorkerTimeoutEvents(worker.id), + getWorkerOrderOverview(worker.id), + ]) + return { + worker: mapWorkerUser(worker), + summary: { activeOrderCount, acceptedOrderCount, timeoutOrderCount }, + orderOverview, + } +} + +export async function listAdminWorkerWalletLedgers( + workerId: number | string, + payload: JsonObject = {}, +) { + const worker = await getRequiredWorker(workerId) + const page = normalizePage(payload.page) + const pageSize = normalizePageSize(payload.pageSize) + const ledgerType = String(payload.ledgerType || payload.ledger_type || '').trim() + const { items, total } = await listWorkerWalletLedgers({ + page, + pageSize, + workerId: Number(worker.id), + ledgerType, + }) + return { items: items.map(mapWalletLedger), pagination: { page, pageSize, total } } +} + +export async function listAdminWorkerDepositUnfreezes( + workerId: number | string, + payload: JsonObject = {}, +) { + const worker = await getRequiredWorker(workerId) + const page = normalizePage(payload.page) + const pageSize = normalizePageSize(payload.pageSize) + const status = String(payload.status || '').trim() + const { items, total } = await listWorkerDepositUnfreezes({ + workerId: Number(worker.id), + status, + page, + pageSize, + }) + return { + items: items.map(mapDepositUnfreeze), + pagination: { page, pageSize, total }, + } +} + +function mapDepositUnfreeze(row: WorkerDepositUnfreezeRow) { + return { + id: Number(row.id), + workerId: Number(row.worker_id), + workOrderId: Number(row.work_order_id), + amount: Number(row.amount || 0), + status: row.status, + unfreezeAt: row.unfreeze_at, + releasedAt: row.released_at || null, + createdAt: row.created_at, + } +} diff --git a/apps/backend/src/services/worker-platform/index.ts b/apps/backend/src/services/worker-platform/index.ts index 5ab9e6d2..d549f8bc 100644 --- a/apps/backend/src/services/worker-platform/index.ts +++ b/apps/backend/src/services/worker-platform/index.ts @@ -28,6 +28,7 @@ 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' +export * from './admin-worker-detail-service.js' export * from './admin-work-order-query-service.js' export * from './admin-worker-feedback-service.js' export * from './admin-worker-cancel-service.js' diff --git a/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx b/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx new file mode 100644 index 00000000..5924484b --- /dev/null +++ b/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx @@ -0,0 +1,398 @@ +import { useState } from 'react' +import { Card, Descriptions, Drawer, Select, Space, Table, Tabs, Tag, Typography } from 'antd' +import type { TableColumnsType } from 'antd' +import { useQuery } from '@tanstack/react-query' + +import { + fetchAdminWorkOrders, + fetchAdminWorkerDetail, + fetchAdminWorkerDepositUnfreezes, + fetchAdminWorkerWalletLedgers, +} from '@/services/admin' +import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' +import { formatAdminDateTime } from '@/utils/admin-time' +import type { + WorkOrder, + WorkerDepositUnfreeze, + WorkerUser, + WorkerWalletLedger, +} from '@/types/worker-platform' +import { formatMoney, formatStatus, formatWorkerStatus, resolveStatusColor } from './shared' + +/** 与打手端 worker-profile-view-utils 的流水文案保持一致;未知类型原样展示。 */ +const LEDGER_TYPE_LABELS: Record = { + manual_credit: '人工充值', + withdraw_paid: '提现打款', + deposit_freeze: '冻结押金', + deposit_release: '释放押金', + deposit_pending_unfreeze: '待解冻金额', + deposit_unfreeze: '押金已解冻', + deposit_deduction: '扣除押金', + reward_settlement: '结算报酬', + sharing_reward: '拼单报酬', + after_sales_deposit_deduction: '售后扣减押金', + after_sales_balance_deduction: '售后扣减余额', + after_sales_debt_offset: '抵扣售后欠款', + after_sales_refund: '售后追缴退还', +} + +function formatLedgerType(ledgerType: string) { + return LEDGER_TYPE_LABELS[ledgerType] || ledgerType || '-' +} + +function resolveLedgerTagColor(ledgerType: string) { + if ( + ['deposit_freeze', 'deposit_pending_unfreeze', 'after_sales_debt_offset'].includes(ledgerType) + ) + return 'gold' + if (['deposit_release', 'deposit_unfreeze', 'after_sales_refund'].includes(ledgerType)) + return 'green' + if ( + [ + 'deposit_deduction', + 'after_sales_deposit_deduction', + 'after_sales_balance_deduction', + ].includes(ledgerType) + ) + return 'red' + if (['reward_settlement', 'sharing_reward'].includes(ledgerType)) return 'cyan' + if (ledgerType === 'manual_credit') return 'blue' + if (ledgerType === 'withdraw_paid') return 'purple' + return 'default' +} + +function formatUnfreezeStatus(status: string) { + if (status === 'pending') return { label: '待解冻', color: 'orange' } + if (status === 'released') return { label: '已到账', color: 'green' } + if (status === 'deducted') return { label: '已扣减', color: 'red' } + return { label: status || '-', color: 'default' } +} + +export default function WorkerDetailDrawer({ + worker, + onClose, +}: { + worker: WorkerUser | null + onClose: () => void +}) { + const workerId = worker?.workerId || 0 + + return ( + + {worker.displayName || worker.username} + + {worker.workerType === 'internal' ? '内部' : '外部'} + + + {formatWorkerStatus(worker.status)} + + + ) : null + } + > + }, + { + key: 'unfreezes', + label: '待解冻', + children: , + }, + { + key: 'ledgers', + label: '资金流水', + children: , + }, + { key: 'orders', label: '他的订单', children: }, + ]} + /> + + ) +} + +function OverviewSection({ workerId }: { workerId: number }) { + const detailQuery = useQuery({ + queryKey: ['admin-worker-detail', workerId], + queryFn: () => fetchAdminWorkerDetail(workerId), + enabled: workerId > 0, + }) + const detail = detailQuery.data?.data + if (!workerId) return null + if (!detail) { + return 加载中… + } + const { worker, summary, orderOverview } = detail + return ( +
+ + + {worker.displayName || '-'} + {worker.username} + {worker.phone || '-'} + {worker.level?.name || '-'} + + {worker.inviter ? `${worker.inviter.displayName || worker.inviter.username}` : '-'} + + + {formatAdminDateTime(worker.createdAt)} + + + + + + + {formatMoney(worker.wallet.availableAmount)} + + + {formatMoney(worker.wallet.frozenDepositAmount)} + + + {formatMoney(worker.wallet.pendingUnfreezeAmount)} + + + {formatMoney(worker.wallet.totalCreditedAmount)} + + + {formatMoney(worker.wallet.totalSettledAmount)} + + + + + + {summary.activeOrderCount} + {summary.acceptedOrderCount} + + {summary.timeoutOrderCount > 0 ? {summary.timeoutOrderCount} : 0} + + + + + {orderOverview.length === 0 ? ( + 暂无订单 + ) : ( + + {orderOverview.map((item) => ( + + {formatStatus(item.status)} {item.orderCount} 单 /{' '} + {formatMoney(item.settlementAmount)} + + ))} + + )} + +
+ ) +} + +function UnfreezesSection({ workerId }: { workerId: number }) { + const [page, setPage] = useState(1) + const unfreezesQuery = useQuery({ + queryKey: ['admin-worker-detail-unfreezes', workerId, page], + queryFn: () => + fetchAdminWorkerDepositUnfreezes(workerId, { page, pageSize: ADMIN_DEFAULT_PAGE_SIZE }), + enabled: workerId > 0, + }) + const data = unfreezesQuery.data?.data + const columns: TableColumnsType = [ + { + title: '金额', + width: 110, + render: (_, row) => formatMoney(row.amount), + }, + { + title: '状态', + width: 100, + render: (_, row) => { + const status = formatUnfreezeStatus(row.status) + return {status.label} + }, + }, + { + title: '到期时间', + width: 170, + render: (_, row) => formatAdminDateTime(row.unfreezeAt), + }, + { + title: '到账时间', + width: 170, + render: (_, row) => (row.releasedAt ? formatAdminDateTime(row.releasedAt) : '-'), + }, + { title: '关联工单', dataIndex: 'workOrderId', width: 100 }, + { + title: '创建时间', + width: 170, + render: (_, row) => formatAdminDateTime(row.createdAt), + }, + ] + return ( + + rowKey="id" + size="small" + columns={columns} + dataSource={data?.items || []} + loading={unfreezesQuery.isFetching} + pagination={buildAdminTablePagination({ + page, + pageSize: ADMIN_DEFAULT_PAGE_SIZE, + total: data?.pagination.total || 0, + onChange: setPage, + })} + /> + ) +} + +function LedgersSection({ workerId }: { workerId: number }) { + const [page, setPage] = useState(1) + const [ledgerType, setLedgerType] = useState('') + const ledgersQuery = useQuery({ + queryKey: ['admin-worker-detail-ledgers', workerId, page, ledgerType], + queryFn: () => + fetchAdminWorkerWalletLedgers(workerId, { + page, + pageSize: ADMIN_DEFAULT_PAGE_SIZE, + ledgerType: ledgerType || undefined, + }), + enabled: workerId > 0, + }) + const data = ledgersQuery.data?.data + const columns: TableColumnsType = [ + { + title: '类型', + width: 130, + render: (_, row) => ( + {formatLedgerType(row.ledgerType)} + ), + }, + { + title: '金额', + width: 110, + render: (_, row) => ( + = 0 ? 'success' : 'danger'}> + {row.amount >= 0 ? '+' : '-'} + {formatMoney(Math.abs(row.amount))} + + ), + }, + { + title: '变动后余额', + width: 110, + render: (_, row) => formatMoney(row.balanceAfter), + }, + { + title: '冻结押金', + width: 110, + render: (_, row) => formatMoney(row.frozenAfter), + }, + { + title: '关联订单', + width: 150, + render: (_, row) => row.relatedPlatformOrderId || row.relatedWorkOrderId || '-', + }, + { + title: '说明', + ellipsis: true, + render: (_, row) => row.note || '-', + }, + { + title: '时间', + width: 170, + render: (_, row) => formatAdminDateTime(row.createdAt), + }, + ] + return ( +
+