From 625df529925e20d005f88acd1b000240faaf6295 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 2 Sep 2026 14:32:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=B4=A2=E5=8A=A1=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E6=97=B6=E9=97=B4=E7=B1=BB=E5=9E=8B=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/repositories/worker-platform/types.ts | 3 + .../worker-platform/worker-finance-repo.ts | 26 +++++++- .../worker-finance-withdraw-review.test.ts | 27 +++++++- .../services/admin/admin-finance-service.ts | 64 +++++++++++++------ .../admin-worker-finance-service.ts | 4 ++ .../src/pages/admin/AdminFinancePage.tsx | 30 +++++++-- .../src/pages/admin/panels/FinancePanel.tsx | 34 ++++++++-- apps/frontend/src/services/admin/finance.ts | 7 +- 8 files changed, 162 insertions(+), 33 deletions(-) diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index c15a8ea6..848086d6 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -513,6 +513,9 @@ export type FinanceRequestListInput = { /** 申请时间区间(YYYY-MM-DD 含全天,或 YYYY-MM-DD HH:mm:ss 精确到秒;上海时区);后台列表筛选使用。 */ createdFrom?: string createdTo?: string + /** 后台审核通过时间区间(上海时区);仅匹配已审核通过的申请。 */ + reviewedFrom?: string + reviewedTo?: string /** 收款渠道筛选:alipay / wechat */ accountChannel?: string } diff --git a/apps/backend/src/repositories/worker-platform/worker-finance-repo.ts b/apps/backend/src/repositories/worker-platform/worker-finance-repo.ts index d75a2a73..3d129bb4 100644 --- a/apps/backend/src/repositories/worker-platform/worker-finance-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-finance-repo.ts @@ -290,6 +290,8 @@ export async function listWorkerFinanceRequests({ keyword = '', createdFrom = '', createdTo = '', + reviewedFrom = '', + reviewedTo = '', accountChannel = '', }: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> { const { whereClause, params } = buildWorkerFinanceRequestWhere({ @@ -300,6 +302,8 @@ export async function listWorkerFinanceRequests({ keyword, createdFrom, createdTo, + reviewedFrom, + reviewedTo, accountChannel, }) const totalResult = await query<{ total: number }>( @@ -335,6 +339,8 @@ export async function summarizeWorkerFinanceRequests( keyword: input.keyword || '', createdFrom: input.createdFrom || '', createdTo: input.createdTo || '', + reviewedFrom: input.reviewedFrom || '', + reviewedTo: input.reviewedTo || '', accountChannel: input.accountChannel || '', }) const result = await query<{ channel: string; total: number; amount: string | number }>( @@ -612,7 +618,7 @@ async function getWorkerFinanceRequestByIdWithClient( return result.rows[0] || null } -function buildWorkerFinanceRequestWhere({ +export function buildWorkerFinanceRequestWhere({ requestId = 0, workerId = 0, status = '', @@ -620,6 +626,8 @@ function buildWorkerFinanceRequestWhere({ keyword = '', createdFrom = '', createdTo = '', + reviewedFrom = '', + reviewedTo = '', accountChannel = '', }: FinanceRequestListInput) { const filters: string[] = [] @@ -642,6 +650,22 @@ function buildWorkerFinanceRequestWhere({ : `wfr.created_at < (($${params.length}::date + 1)::timestamp) AT TIME ZONE 'Asia/Shanghai'`, ) } + if (reviewedFrom || reviewedTo) { + // “审核通过时间”只适用于已通过的申请;待处理、拒绝和取消记录不参与该时间筛选。 + filters.push(`wfr.status = 'approved'`) + } + if (reviewedFrom) { + params.push(reviewedFrom) + filters.push(`wfr.reviewed_at >= ($${params.length}::timestamp) AT TIME ZONE 'Asia/Shanghai'`) + } + if (reviewedTo) { + params.push(reviewedTo) + filters.push( + reviewedTo.length > 10 + ? `wfr.reviewed_at <= ($${params.length}::timestamp) AT TIME ZONE 'Asia/Shanghai'` + : `wfr.reviewed_at < (($${params.length}::date + 1)::timestamp) AT TIME ZONE 'Asia/Shanghai'`, + ) + } if (requestId) { params.push(requestId) filters.push(`wfr.id = $${params.length}`) diff --git a/apps/backend/src/repositories/worker-platform/worker-finance-withdraw-review.test.ts b/apps/backend/src/repositories/worker-platform/worker-finance-withdraw-review.test.ts index 29c5ec0d..908054fc 100644 --- a/apps/backend/src/repositories/worker-platform/worker-finance-withdraw-review.test.ts +++ b/apps/backend/src/repositories/worker-platform/worker-finance-withdraw-review.test.ts @@ -1,7 +1,32 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { resolveWithdrawApprovalPlan, resolveWithdrawRejectionPlan } from './worker-finance-repo.js' +import { + buildWorkerFinanceRequestWhere, + resolveWithdrawApprovalPlan, + resolveWithdrawRejectionPlan, +} from './worker-finance-repo.js' + +test('资金申请列表支持按打手申请时间和后台审核通过时间筛选', () => { + const result = buildWorkerFinanceRequestWhere({ + createdFrom: '2026-09-01 10:00:00', + createdTo: '2026-09-01 18:30:59', + reviewedFrom: '2026-09-02', + reviewedTo: '2026-09-03', + }) + + assert.deepEqual(result.params, [ + '2026-09-01 10:00:00', + '2026-09-01 18:30:59', + '2026-09-02', + '2026-09-03', + ]) + assert.match(result.whereClause, /wfr\.created_at >= \(\$1::timestamp\)/) + assert.match(result.whereClause, /wfr\.created_at <= \(\$2::timestamp\)/) + assert.match(result.whereClause, /wfr\.status = 'approved'/) + assert.match(result.whereClause, /wfr\.reviewed_at >= \(\$3::timestamp\)/) + assert.match(result.whereClause, /wfr\.reviewed_at < \(\(\$4::date \+ 1\)::timestamp\)/) +}) test('提现通过(新口径):申请时已冻结,确认打款只解冻不动可用余额', () => { const plan = resolveWithdrawApprovalPlan({ diff --git a/apps/backend/src/services/admin/admin-finance-service.ts b/apps/backend/src/services/admin/admin-finance-service.ts index 905410c2..9e48fd86 100644 --- a/apps/backend/src/services/admin/admin-finance-service.ts +++ b/apps/backend/src/services/admin/admin-finance-service.ts @@ -23,7 +23,9 @@ const FINANCE_EVENTS_CTE = ` o.total_amount::bigint AS amount, '订单收款'::text AS label, o.platform_order_id::text AS reference_no, - COALESCE(NULLIF(CONCAT_WS('/', NULLIF(o.provider, ''), NULLIF(o.platform, '')), ''), '其他')::text AS channel + COALESCE(NULLIF(CONCAT_WS('/', NULLIF(o.provider, ''), NULLIF(o.platform, '')), ''), '其他')::text AS channel, + o.created_at AS request_created_at, + NULL::timestamptz AS request_reviewed_at FROM orders o WHERE o.pay_status IN ('paid', 'refunded') AND o.total_amount > 0 @@ -37,7 +39,9 @@ const FINANCE_EVENTS_CTE = ` o.total_amount::bigint, '订单退款'::text, o.platform_order_id::text, - COALESCE(NULLIF(CONCAT_WS('/', NULLIF(o.provider, ''), NULLIF(o.platform, '')), ''), '其他')::text + COALESCE(NULLIF(CONCAT_WS('/', NULLIF(o.provider, ''), NULLIF(o.platform, '')), ''), '其他')::text, + o.created_at, + NULL::timestamptz FROM orders o WHERE o.pay_status = 'refunded' AND o.total_amount > 0 @@ -53,7 +57,9 @@ const FINANCE_EVENTS_CTE = ` CASE WHEN wl.ledger_type IN ('reward_settlement', 'sharing_reward') THEN '打手报酬' ELSE '打手资金回收' END::text, COALESCE(wl.related_work_order_id::text, ''), - 'worker-platform'::text + 'worker-platform'::text, + wl.created_at, + NULL::timestamptz FROM worker_wallet_ledgers wl WHERE wl.ledger_type IN ( 'reward_settlement', 'sharing_reward', 'after_sales_deposit_deduction', @@ -70,7 +76,9 @@ const FINANCE_EVENTS_CTE = ` wfr.amount::bigint, '打手提现'::text, wfr.account_channel::text, - COALESCE(NULLIF(wfr.account_channel, ''), '其他')::text + COALESCE(NULLIF(wfr.account_channel, ''), '其他')::text, + wfr.created_at, + wfr.reviewed_at FROM worker_finance_requests wfr WHERE wfr.request_type = 'withdraw' AND wfr.status = 'approved' @@ -86,7 +94,9 @@ const FINANCE_EVENTS_CTE = ` wfr.amount::bigint, '打手充值'::text, wfr.account_channel::text, - COALESCE(NULLIF(wfr.account_channel, ''), '其他')::text + COALESCE(NULLIF(wfr.account_channel, ''), '其他')::text, + wfr.created_at, + wfr.reviewed_at FROM worker_finance_requests wfr WHERE wfr.request_type = 'recharge' AND wfr.status = 'approved' @@ -102,7 +112,9 @@ const FINANCE_EVENTS_CTE = ` wasc.refunded_amount::bigint, '售后追缴退还'::text, wasc.case_no::text, - 'worker-platform'::text + 'worker-platform'::text, + wasc.created_at, + NULL::timestamptz FROM worker_after_sales_cases wasc WHERE wasc.refunded_at IS NOT NULL AND wasc.refunded_amount > 0 @@ -111,6 +123,8 @@ const FINANCE_EVENTS_CTE = ` export async function getAdminFinanceSummary(payload: JsonObject = {}) { const range = resolveFinanceRange(payload, nowIso()) + const dateType = normalizeFinanceDateType(payload.dateType) + const eventDateExpression = resolveFinanceEventDateExpression(dateType) const [dailyResult, snapshotResult] = await Promise.all([ query( `${FINANCE_EVENTS_CTE} @@ -126,10 +140,10 @@ export async function getAdminFinanceSummary(payload: JsonObject = {}) { COALESCE(SUM(amount) FILTER (WHERE category = 'worker_recovery'), 0)::bigint AS worker_recovery_amount FROM generate_series($3::timestamptz, $4::timestamptz - interval '1 day', interval '1 day') day LEFT JOIN finance_events event - ON event.occurred_at >= $1::timestamptz - AND event.occurred_at < $2::timestamptz - AND event.occurred_at >= day - AND event.occurred_at < day + interval '1 day' + ON ${eventDateExpression} >= $1::timestamptz + AND ${eventDateExpression} < $2::timestamptz + AND ${eventDateExpression} >= day + AND ${eventDateExpression} < day + interval '1 day' GROUP BY day ORDER BY day DESC`, [range.from, range.to, range.bucketFrom, range.bucketTo], @@ -151,14 +165,14 @@ export async function getAdminFinanceSummary(payload: JsonObject = {}) { const channelDailyResult = await query( `${FINANCE_EVENTS_CTE} SELECT - to_char(event.occurred_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS day, + to_char(${eventDateExpression} AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS day, event.channel, event.category, COUNT(*)::int AS total, COALESCE(SUM(event.amount), 0)::bigint AS amount FROM finance_events event - WHERE event.occurred_at >= $1::timestamptz - AND event.occurred_at < $2::timestamptz + WHERE ${eventDateExpression} >= $1::timestamptz + AND ${eventDateExpression} < $2::timestamptz GROUP BY 1, 2, 3 ORDER BY day DESC, event.channel ASC, event.category ASC`, [range.from, range.to], @@ -203,6 +217,8 @@ export async function getAdminFinanceSummary(payload: JsonObject = {}) { export async function listAdminFinanceTransactions(payload: JsonObject = {}) { const range = resolveFinanceRange(payload, nowIso()) + const dateType = normalizeFinanceDateType(payload.dateType) + const eventDateExpression = resolveFinanceEventDateExpression(dateType) const page = normalizePositiveInt(payload.page, 1) const pageSize = Math.min(normalizePositiveInt(payload.pageSize, 20), 100) const category = String(payload.category || '').trim() @@ -226,8 +242,8 @@ export async function listAdminFinanceTransactions(payload: JsonObject = {}) { `${FINANCE_EVENTS_CTE} SELECT COUNT(*)::int AS total FROM finance_events event - WHERE event.occurred_at >= $1 - AND event.occurred_at < $2${categoryClause}${channelClause}`, + WHERE ${eventDateExpression} >= $1 + AND ${eventDateExpression} < $2${categoryClause}${channelClause}`, params, ) const itemsParams = [...params, pageSize, offset] @@ -235,9 +251,9 @@ export async function listAdminFinanceTransactions(payload: JsonObject = {}) { `${FINANCE_EVENTS_CTE} SELECT category, source_id, occurred_at, amount, label, reference_no, channel FROM finance_events event - WHERE event.occurred_at >= $1 - AND event.occurred_at < $2${categoryClause}${channelClause} - ORDER BY event.occurred_at DESC, event.source_id DESC + WHERE ${eventDateExpression} >= $1 + AND ${eventDateExpression} < $2${categoryClause}${channelClause} + ORDER BY ${eventDateExpression} DESC, event.source_id DESC LIMIT $${itemsParams.length - 1} OFFSET $${itemsParams.length}`, itemsParams, ) @@ -385,6 +401,18 @@ function normalizePositiveInt(value: unknown, fallback: number) { return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback } +function normalizeFinanceDateType(value: unknown): 'occurred' | 'created' | 'reviewed' { + const normalized = String(value || '').trim() + if (normalized === 'created' || normalized === 'reviewed') return normalized + return 'occurred' +} + +function resolveFinanceEventDateExpression(dateType: 'occurred' | 'created' | 'reviewed') { + if (dateType === 'created') return 'COALESCE(event.request_created_at, event.occurred_at)' + if (dateType === 'reviewed') return 'event.request_reviewed_at' + return 'event.occurred_at' +} + function normalizeDateTimeDate(value: string) { const match = value.trim().match(/^(\d{4}-\d{2}-\d{2})/) if (!match || Number.isNaN(Date.parse(`${match[1]}T00:00:00Z`))) return '' diff --git a/apps/backend/src/services/worker-platform/admin-worker-finance-service.ts b/apps/backend/src/services/worker-platform/admin-worker-finance-service.ts index 3efb29c1..b4ac445a 100644 --- a/apps/backend/src/services/worker-platform/admin-worker-finance-service.ts +++ b/apps/backend/src/services/worker-platform/admin-worker-finance-service.ts @@ -170,6 +170,8 @@ export async function listAdminWorkerFinanceRequests(query: JsonObject = {}) { const keyword = String(query.keyword || '').trim() const createdFrom = normalizeDateTimeString(query.createdFrom) const createdTo = normalizeDateTimeString(query.createdTo) + const reviewedFrom = normalizeDateTimeString(query.reviewedFrom) + const reviewedTo = normalizeDateTimeString(query.reviewedTo) const accountChannel = ['alipay', 'wechat'].includes(String(query.accountChannel || '')) ? String(query.accountChannel) : '' @@ -180,6 +182,8 @@ export async function listAdminWorkerFinanceRequests(query: JsonObject = {}) { keyword, ...(createdFrom ? { createdFrom } : {}), ...(createdTo ? { createdTo } : {}), + ...(reviewedFrom ? { reviewedFrom } : {}), + ...(reviewedTo ? { reviewedTo } : {}), ...(accountChannel ? { accountChannel } : {}), } const [listResult, channels] = await Promise.all([ diff --git a/apps/frontend/src/pages/admin/AdminFinancePage.tsx b/apps/frontend/src/pages/admin/AdminFinancePage.tsx index 0e57b76b..1861f924 100644 --- a/apps/frontend/src/pages/admin/AdminFinancePage.tsx +++ b/apps/frontend/src/pages/admin/AdminFinancePage.tsx @@ -20,7 +20,11 @@ import { useState } from 'react' import { AdminRangePicker } from '@/components/admin/AdminDatePicker' import PageHeader from '@/components/admin/PageHeader' import dayjs from '@/lib/dayjs' -import { fetchAdminFinanceSummary, fetchAdminFinanceTransactions } from '@/services/admin' +import { + fetchAdminFinanceSummary, + fetchAdminFinanceTransactions, + type AdminFinanceDateType, +} from '@/services/admin' import type { AdminFinanceChannelDailyItem, AdminFinanceDailyItem, @@ -30,7 +34,7 @@ import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admi import { formatAdminDateTime } from '@/utils/admin-time' import { formatMoney } from './panels/shared' -type FinanceFilterForm = { dateRange?: unknown } +type FinanceFilterForm = { dateRange?: unknown; dateType?: AdminFinanceDateType } const CATEGORY_OPTIONS = [ { value: '', label: '全部账目' }, @@ -45,7 +49,11 @@ const CATEGORY_OPTIONS = [ export default function AdminFinancePage() { const [form] = Form.useForm() - const [filters, setFilters] = useState({ dateFrom: '', dateTo: '' }) + const [filters, setFilters] = useState<{ + dateFrom: string + dateTo: string + dateType: AdminFinanceDateType + }>({ dateFrom: '', dateTo: '', dateType: 'occurred' }) const [category, setCategory] = useState('') const [channel, setChannel] = useState('') const [channelSummaryFilter, setChannelSummaryFilter] = useState('') @@ -83,7 +91,7 @@ export default function AdminFinancePage() { ? dayjs(range[1]).second(59).format('YYYY-MM-DD HH:mm:ss') : '' setPage(1) - setFilters({ dateFrom, dateTo }) + setFilters({ dateFrom, dateTo, dateType: values.dateType || 'occurred' }) } function resetFilters() { @@ -92,7 +100,7 @@ export default function AdminFinancePage() { setChannel('') setChannelSummaryFilter('') setPage(1) - setFilters({ dateFrom: '', dateTo: '' }) + setFilters({ dateFrom: '', dateTo: '', dateType: 'occurred' }) } const dailyColumns: TableColumnsType = [ @@ -161,7 +169,17 @@ export default function AdminFinancePage() { className="filter-form" onFinish={applyFilters} > - + + { + setRequestDateType(nextType) + setPage(1) + }} + /> { const [start, end] = dates || [] - setCreatedRange( + setRequestDateRange( start && end ? [ // 起止各占满整分钟:开始 xx:00、结束 xx:59 diff --git a/apps/frontend/src/services/admin/finance.ts b/apps/frontend/src/services/admin/finance.ts index 050483cb..afadcb32 100644 --- a/apps/frontend/src/services/admin/finance.ts +++ b/apps/frontend/src/services/admin/finance.ts @@ -1,7 +1,11 @@ import { apiGet } from '@/lib/http' import type { AdminFinanceSummary, AdminFinanceTransactionResult } from '@/types/admin/finance' -export function fetchAdminFinanceSummary(params: { dateFrom?: string; dateTo?: string } = {}) { +export type AdminFinanceDateType = 'occurred' | 'created' | 'reviewed' + +export function fetchAdminFinanceSummary( + params: { dateFrom?: string; dateTo?: string; dateType?: AdminFinanceDateType } = {}, +) { return apiGet('/api/v1/admin/finance/summary', params) } @@ -9,6 +13,7 @@ export function fetchAdminFinanceTransactions( params: { dateFrom?: string dateTo?: string + dateType?: AdminFinanceDateType category?: string channel?: string page?: number