diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 81af4c5c..83cf1219 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -487,7 +487,7 @@ export type FinanceRequestListInput = { status?: string requestType?: string keyword?: string - /** 申请时间区间(YYYY-MM-DD,上海时区,含起止日);后台列表筛选使用。 */ + /** 申请时间区间(YYYY-MM-DD 含全天,或 YYYY-MM-DD HH:mm:ss 精确到秒;上海时区);后台列表筛选使用。 */ createdFrom?: string createdTo?: string /** 收款渠道筛选:alipay / wechat */ 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 0e192eb1..eb3eddcd 100644 --- a/apps/backend/src/repositories/worker-platform/worker-finance-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-finance-repo.ts @@ -482,14 +482,18 @@ function buildWorkerFinanceRequestWhere({ } if (createdFrom) { params.push(createdFrom) + // 纯日期字符串 cast 后即当天 00:00:00;带时间则精确到秒 filters.push( - `wfr.created_at >= ($${params.length}::date)::timestamp AT TIME ZONE 'Asia/Shanghai'`, + `wfr.created_at >= ($${params.length}::timestamp) AT TIME ZONE 'Asia/Shanghai'`, ) } if (createdTo) { params.push(createdTo) + // 带时间精确到秒(含截止时刻);纯日期保持含截止日全天 filters.push( - `wfr.created_at < (($${params.length}::date + 1)::timestamp AT TIME ZONE 'Asia/Shanghai')`, + createdTo.length > 10 + ? `wfr.created_at <= ($${params.length}::timestamp) AT TIME ZONE 'Asia/Shanghai'` + : `wfr.created_at < (($${params.length}::date + 1)::timestamp) AT TIME ZONE 'Asia/Shanghai'`, ) } if (requestId) { diff --git a/apps/backend/src/services/admin/admin-query-utils.test.ts b/apps/backend/src/services/admin/admin-query-utils.test.ts new file mode 100644 index 00000000..54d36074 --- /dev/null +++ b/apps/backend/src/services/admin/admin-query-utils.test.ts @@ -0,0 +1,27 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { normalizeDateTimeString } from './admin-query-utils.js' + +test('normalizeDateTimeString 保留纯日期并按天过滤', () => { + assert.equal(normalizeDateTimeString('2026-08-22'), '2026-08-22') +}) + +test('normalizeDateTimeString 接受到分钟并补零秒', () => { + assert.equal(normalizeDateTimeString('2026-08-22 10:30'), '2026-08-22 10:30:00') +}) + +test('normalizeDateTimeString 接受到秒', () => { + assert.equal(normalizeDateTimeString('2026-08-22 10:30:59'), '2026-08-22 10:30:59') +}) + +test('normalizeDateTimeString 拒绝非法输入', () => { + assert.equal(normalizeDateTimeString(''), '') + assert.equal(normalizeDateTimeString(undefined), '') + assert.equal(normalizeDateTimeString('2026-8-22'), '') + assert.equal(normalizeDateTimeString('2026-08-22T10:30:00'), '') + assert.equal(normalizeDateTimeString('2026-08-22 24:00'), '') + assert.equal(normalizeDateTimeString('2026-08-22 10:60'), '') + assert.equal(normalizeDateTimeString('2026-08-22 10:30:60'), '') + assert.equal(normalizeDateTimeString('2026-02-31 10:30'), '') +}) diff --git a/apps/backend/src/services/admin/admin-query-utils.ts b/apps/backend/src/services/admin/admin-query-utils.ts index b7f21955..97f60da1 100644 --- a/apps/backend/src/services/admin/admin-query-utils.ts +++ b/apps/backend/src/services/admin/admin-query-utils.ts @@ -32,6 +32,34 @@ export function normalizeDateString(rawValue: unknown) { return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : '' } +/** + * 校验并归一化日期时间参数:YYYY-MM-DD 或 YYYY-MM-DD HH:mm[:ss]。 + * 纯日期原样返回(SQL 侧按当天零点处理),缺秒补 :00;非法或空值返回空字符串。 + */ +export function normalizeDateTimeString(rawValue: unknown) { + const normalized = String(rawValue || '').trim() + if (!normalized) { + return '' + } + if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) { + return normalized + } + const match = normalized.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}):(\d{2})(?::(\d{2}))?$/) + if (!match) { + return '' + } + const [, datePart, hour, minute, second] = match + // roundtrip 校验拦截 2026-02-31 这类会被 Date 溢出成其他日期的非法值 + const parsedDate = new Date(`${datePart}T00:00:00Z`) + if (Number.isNaN(parsedDate.getTime()) || parsedDate.toISOString().slice(0, 10) !== datePart) { + return '' + } + if (Number(hour) > 23 || Number(minute) > 59 || Number(second ?? '0') > 59) { + return '' + } + return `${datePart} ${hour}:${minute}:${second ?? '00'}` +} + export function safeParseJson(rawText: unknown): JsonObject { if (rawText && typeof rawText === 'object') { return rawText 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 0d38c3b6..19acff5b 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 @@ -12,7 +12,7 @@ import type { JsonObject } from '../../types/json.js' import { createHttpError } from '../../utils/http.js' import { nowIso } from '../../utils/time.js' import { - normalizeDateString, + normalizeDateTimeString, normalizePage, normalizePageSize, safeParseJson, @@ -168,8 +168,8 @@ export async function listAdminWorkerFinanceRequests(query: JsonObject = {}) { const status = normalizeFinanceRequestStatus(query.status) const requestType = normalizeFinanceRequestType(query.requestType) const keyword = String(query.keyword || '').trim() - const createdFrom = normalizeDateString(query.createdFrom) - const createdTo = normalizeDateString(query.createdTo) + const createdFrom = normalizeDateTimeString(query.createdFrom) + const createdTo = normalizeDateTimeString(query.createdTo) const accountChannel = ['alipay', 'wechat'].includes(String(query.accountChannel || '')) ? String(query.accountChannel) : '' diff --git a/apps/frontend/src/pages/admin/panels/FinancePanel.tsx b/apps/frontend/src/pages/admin/panels/FinancePanel.tsx index 626e334a..efdf5c1b 100644 --- a/apps/frontend/src/pages/admin/panels/FinancePanel.tsx +++ b/apps/frontend/src/pages/admin/panels/FinancePanel.tsx @@ -35,7 +35,7 @@ import type { WorkerFinanceRequest, } from '@/types/worker-platform' import { AdminRangePicker } from '@/components/admin/AdminDatePicker' -import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs' +import dayjs, { ADMIN_DATE_TIME_FORMAT } from '@/lib/dayjs' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' import { asRecord, formatMoney } from './shared' @@ -491,12 +491,21 @@ export default function FinancePanel() { }} /> { const [start, end] = dates || [] setCreatedRange( start && end - ? [start.format(ADMIN_DATE_FORMAT), end.format(ADMIN_DATE_FORMAT)] + ? [ + // 起止各占满整分钟:开始 xx:00、结束 xx:59 + start.second(0).format(ADMIN_DATE_TIME_FORMAT), + end.second(59).format(ADMIN_DATE_TIME_FORMAT), + ] : null, ) setPage(1)