资金申请时间筛选支持精确到时分并修复截止日SQL括号缺失
This commit is contained in:
@@ -487,7 +487,7 @@ export type FinanceRequestListInput = {
|
|||||||
status?: string
|
status?: string
|
||||||
requestType?: string
|
requestType?: string
|
||||||
keyword?: string
|
keyword?: string
|
||||||
/** 申请时间区间(YYYY-MM-DD,上海时区,含起止日);后台列表筛选使用。 */
|
/** 申请时间区间(YYYY-MM-DD 含全天,或 YYYY-MM-DD HH:mm:ss 精确到秒;上海时区);后台列表筛选使用。 */
|
||||||
createdFrom?: string
|
createdFrom?: string
|
||||||
createdTo?: string
|
createdTo?: string
|
||||||
/** 收款渠道筛选:alipay / wechat */
|
/** 收款渠道筛选:alipay / wechat */
|
||||||
|
|||||||
@@ -482,14 +482,18 @@ function buildWorkerFinanceRequestWhere({
|
|||||||
}
|
}
|
||||||
if (createdFrom) {
|
if (createdFrom) {
|
||||||
params.push(createdFrom)
|
params.push(createdFrom)
|
||||||
|
// 纯日期字符串 cast 后即当天 00:00:00;带时间则精确到秒
|
||||||
filters.push(
|
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) {
|
if (createdTo) {
|
||||||
params.push(createdTo)
|
params.push(createdTo)
|
||||||
|
// 带时间精确到秒(含截止时刻);纯日期保持含截止日全天
|
||||||
filters.push(
|
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) {
|
if (requestId) {
|
||||||
|
|||||||
@@ -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'), '')
|
||||||
|
})
|
||||||
@@ -32,6 +32,34 @@ export function normalizeDateString(rawValue: unknown) {
|
|||||||
return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : ''
|
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 {
|
export function safeParseJson(rawText: unknown): JsonObject {
|
||||||
if (rawText && typeof rawText === 'object') {
|
if (rawText && typeof rawText === 'object') {
|
||||||
return rawText
|
return rawText
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { JsonObject } from '../../types/json.js'
|
|||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import {
|
import {
|
||||||
normalizeDateString,
|
normalizeDateTimeString,
|
||||||
normalizePage,
|
normalizePage,
|
||||||
normalizePageSize,
|
normalizePageSize,
|
||||||
safeParseJson,
|
safeParseJson,
|
||||||
@@ -168,8 +168,8 @@ export async function listAdminWorkerFinanceRequests(query: JsonObject = {}) {
|
|||||||
const status = normalizeFinanceRequestStatus(query.status)
|
const status = normalizeFinanceRequestStatus(query.status)
|
||||||
const requestType = normalizeFinanceRequestType(query.requestType)
|
const requestType = normalizeFinanceRequestType(query.requestType)
|
||||||
const keyword = String(query.keyword || '').trim()
|
const keyword = String(query.keyword || '').trim()
|
||||||
const createdFrom = normalizeDateString(query.createdFrom)
|
const createdFrom = normalizeDateTimeString(query.createdFrom)
|
||||||
const createdTo = normalizeDateString(query.createdTo)
|
const createdTo = normalizeDateTimeString(query.createdTo)
|
||||||
const accountChannel = ['alipay', 'wechat'].includes(String(query.accountChannel || ''))
|
const accountChannel = ['alipay', 'wechat'].includes(String(query.accountChannel || ''))
|
||||||
? String(query.accountChannel)
|
? String(query.accountChannel)
|
||||||
: ''
|
: ''
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import type {
|
|||||||
WorkerFinanceRequest,
|
WorkerFinanceRequest,
|
||||||
} from '@/types/worker-platform'
|
} from '@/types/worker-platform'
|
||||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
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 { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import { asRecord, formatMoney } from './shared'
|
import { asRecord, formatMoney } from './shared'
|
||||||
@@ -491,12 +491,21 @@ export default function FinancePanel() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AdminRangePicker
|
<AdminRangePicker
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
showTime={{
|
||||||
|
format: 'HH:mm',
|
||||||
|
defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')],
|
||||||
|
}}
|
||||||
value={createdRange ? [dayjs(createdRange[0]), dayjs(createdRange[1])] : null}
|
value={createdRange ? [dayjs(createdRange[0]), dayjs(createdRange[1])] : null}
|
||||||
onChange={(dates) => {
|
onChange={(dates) => {
|
||||||
const [start, end] = dates || []
|
const [start, end] = dates || []
|
||||||
setCreatedRange(
|
setCreatedRange(
|
||||||
start && end
|
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,
|
: null,
|
||||||
)
|
)
|
||||||
setPage(1)
|
setPage(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user