From 1bf12808774d6860634f1efa88ccbc02225f2c2a Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 5 Nov 2026 21:19:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(consignment):=20=E5=AF=84=E5=94=AE?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=A2=9E=E5=8A=A0=E5=85=A5=E5=BA=93=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker-platform/consignment-repo.test.ts | 22 +++++++++++++++ .../worker-platform/consignment-repo.ts | 14 ++++++++++ .../consignment-admin-service.test.ts | 12 ++++++++ .../consignment-admin-service.ts | 18 ++++++++++++ .../panels/ConsignmentInventoryPanel.tsx | 23 +++++++++++++++ .../consignment-inventory-date-filter.test.ts | 28 +++++++++++++++++++ 6 files changed, 117 insertions(+) create mode 100644 apps/backend/src/services/worker-platform/consignment-admin-service.test.ts create mode 100644 apps/frontend/tests/consignment-inventory-date-filter.test.ts diff --git a/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts b/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts index c3a4ac3a..51bb80f5 100644 --- a/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts +++ b/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts @@ -54,6 +54,28 @@ test('寄卖仓库搜索必须覆盖供货打手姓名、打手电话与页面 ) }) +test('寄卖仓库按通过审核入库时间筛选,结束日期必须包含当天全天', async () => { + const source = await readFile(new URL('./consignment-repo.ts', import.meta.url), 'utf8') + const inventoryListStart = source.indexOf('export async function listConsignmentInventories') + const inventoryListBody = source.slice( + inventoryListStart, + source.indexOf('\n}\n', inventoryListStart), + ) + + assert.ok( + inventoryListBody.includes( + "ci.inbound_at >= ($${values.length}::date::timestamp AT TIME ZONE 'Asia/Shanghai')", + ), + '开始日期必须按上海时区当天零点筛选 inbound_at', + ) + assert.ok( + inventoryListBody.includes( + "ci.inbound_at < (($${values.length}::date + INTERVAL '1 day')::timestamp AT TIME ZONE 'Asia/Shanghai')", + ), + '结束日期必须筛选到次日零点之前,不能漏掉结束日当天', + ) +}) + test('匹配候选要带出寄卖单号,并且 join 之后所有列名都必须加 ci. 前缀', async () => { const source = await readFile(new URL('./consignment-repo.ts', import.meta.url), 'utf8') const start = source.indexOf('export async function listActiveConsignmentInventoriesForMatching') diff --git a/apps/backend/src/repositories/worker-platform/consignment-repo.ts b/apps/backend/src/repositories/worker-platform/consignment-repo.ts index bf3c41a8..87ff4d82 100644 --- a/apps/backend/src/repositories/worker-platform/consignment-repo.ts +++ b/apps/backend/src/repositories/worker-platform/consignment-repo.ts @@ -128,6 +128,8 @@ export async function listConsignmentInventories(filters: { allowMatching?: boolean | undefined workerId?: number | undefined keyword?: string | undefined + dateFrom?: string | undefined + dateTo?: string | undefined page?: number | undefined pageSize?: number | undefined }): Promise<{ items: ConsignmentInventoryRow[]; total: number }> { @@ -201,6 +203,18 @@ export async function listConsignmentInventories(filters: { OR ccs.name ILIKE $${values.length})`, ) } + if (filters.dateFrom) { + values.push(filters.dateFrom) + conditions.push( + `ci.inbound_at >= ($${values.length}::date::timestamp AT TIME ZONE 'Asia/Shanghai')`, + ) + } + if (filters.dateTo) { + values.push(filters.dateTo) + conditions.push( + `ci.inbound_at < (($${values.length}::date + INTERVAL '1 day')::timestamp AT TIME ZONE 'Asia/Shanghai')`, + ) + } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '' // 计数与列表必须使用同一组 join,否则新增的提报单 / 供货打手关键字会直接报错。 diff --git a/apps/backend/src/services/worker-platform/consignment-admin-service.test.ts b/apps/backend/src/services/worker-platform/consignment-admin-service.test.ts new file mode 100644 index 00000000..4673380f --- /dev/null +++ b/apps/backend/src/services/worker-platform/consignment-admin-service.test.ts @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { normalizeInventoryDate } from './consignment-admin-service.js' + +test('寄售仓库入库日期筛选只接受合法的 YYYY-MM-DD', () => { + assert.equal(normalizeInventoryDate('2026-11-05'), '2026-11-05') + assert.equal(normalizeInventoryDate(' 2026-11-05 '), '2026-11-05') + assert.equal(normalizeInventoryDate('2026-02-31'), undefined) + assert.equal(normalizeInventoryDate('2026/11/05'), undefined) + assert.equal(normalizeInventoryDate(''), undefined) +}) diff --git a/apps/backend/src/services/worker-platform/consignment-admin-service.ts b/apps/backend/src/services/worker-platform/consignment-admin-service.ts index 5db66551..4a1b2dd2 100644 --- a/apps/backend/src/services/worker-platform/consignment-admin-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-admin-service.ts @@ -294,6 +294,8 @@ export async function fetchAdminConsignmentInventories(query: JsonObject = {}) { const allowMatching = query.allowMatching !== undefined ? String(query.allowMatching) === 'true' : undefined const keyword = query.keyword ? String(query.keyword).trim() : undefined + const dateFrom = normalizeInventoryDate(query.dateFrom) + const dateTo = normalizeInventoryDate(query.dateTo) const page = query.page ? Number(query.page) : 1 const pageSize = query.pageSize ? Number(query.pageSize) : 20 @@ -309,6 +311,8 @@ export async function fetchAdminConsignmentInventories(query: JsonObject = {}) { diamondLevel, allowMatching, keyword, + dateFrom, + dateTo, page, pageSize, }) @@ -319,6 +323,20 @@ export async function fetchAdminConsignmentInventories(query: JsonObject = {}) { } } +/** + * 寄售仓库的“通过审核入库时间”按自然日筛选。 + * 只接受 YYYY-MM-DD,并通过 UTC roundtrip 拦截 2026-02-31 这类会被 Date 溢出的日期。 + */ +export function normalizeInventoryDate(rawValue: unknown) { + const normalized = String(rawValue || '').trim() + if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) return undefined + + const parsed = new Date(`${normalized}T00:00:00.000Z`) + return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== normalized + ? undefined + : normalized +} + export async function fetchAdminConsignmentStats() { return getConsignmentInventoryStats() } diff --git a/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx b/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx index 25630c9d..eda166af 100644 --- a/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx @@ -6,6 +6,7 @@ import { ThunderboltOutlined, } from '@ant-design/icons' import { useQuery } from '@tanstack/react-query' +import dayjs from 'dayjs' import { Alert, App, @@ -28,7 +29,9 @@ import { Tooltip, Typography, } from 'antd' +import { AdminRangePicker } from '@/components/admin/AdminDatePicker' import PercentTable from '@/components/common/PercentTable' +import { ADMIN_DATE_FORMAT } from '@/lib/dayjs' import { buildAdminTablePagination } from '@/utils/admin-pagination' import type { TableColumnsType } from 'antd' import { useState } from 'react' @@ -148,6 +151,7 @@ export default function ConsignmentInventoryPanel() { const [series, setSeries] = useState('') const [diamondLevel, setDiamondLevel] = useState(0) const [allowMatching, setAllowMatching] = useState('') + const [inboundRange, setInboundRange] = useState<[string, string] | null>(null) const [keyword, setKeyword] = useState('') const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) @@ -205,6 +209,8 @@ export default function ConsignmentInventoryPanel() { series, diamondLevel, allowMatching, + inboundRange?.[0], + inboundRange?.[1], keyword, page, pageSize, @@ -217,6 +223,8 @@ export default function ConsignmentInventoryPanel() { series: series || undefined, diamondLevel: diamondLevel > 0 ? diamondLevel : undefined, allowMatching: allowMatching === 'yes' ? true : allowMatching === 'no' ? false : undefined, + dateFrom: inboundRange?.[0], + dateTo: inboundRange?.[1], keyword: keyword || undefined, page, pageSize, @@ -835,6 +843,21 @@ export default function ConsignmentInventoryPanel() { ]} /> + { + setInboundRange( + dates?.[0] && dates[1] + ? [dates[0].format(ADMIN_DATE_FORMAT), dates[1].format(ADMIN_DATE_FORMAT)] + : null, + ) + setPage(1) + }} + /> + { + const source = readFileSync( + new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname, + 'utf8', + ) + + assert.ok( + source.includes("from '@/components/admin/AdminDatePicker'"), + '必须复用后台日期范围控件', + ) + assert.ok(source.includes("placeholder={['入库开始日期', '入库结束日期']}"), '必须明确筛选口径') + assert.ok(source.includes('dateFrom: inboundRange?.[0]'), '开始日期必须透传后端') + assert.ok(source.includes('dateTo: inboundRange?.[1]'), '结束日期必须透传后端') + assert.ok( + source.includes('inboundRange?.[0],\n inboundRange?.[1],'), + '日期变化必须进入 React Query key 并触发重新查询', + ) + assert.ok( + source + .slice(source.indexOf('