From c104a029c9aa36348e968bb379ab5ad4d8f4e1a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=85=E5=B1=85?= Date: Mon, 19 Oct 2026 13:15:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(supply-chain):=20=E9=99=90=E4=BB=B7?= =?UTF-8?q?=E6=98=8E=E7=BB=86=E3=80=8C=E8=B4=9F=E8=B4=A3=E4=BA=BA=E9=99=90?= =?UTF-8?q?=E4=BB=B7=E3=80=8D=E7=AD=9B=E9=80=89=E8=A1=A5=E4=B8=8A=E5=80=99?= =?UTF-8?q?=E9=80=89=E5=80=BC=E4=B8=8E=E7=AD=9B=E9=80=89=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 负责人限价是 SupplyChainStyleLimitPrice.ownerLimitPrice 的派生列(按货号),此前 option 接口没有对应分支, 下拉框显示「没有可选值」、选了也不生效 - 候选值:无其他筛选时取限价表的全部去重价格(两位小数,与界面一致);有筛选时只取当前结果集里货号的限价; 无流程限价的商品提供「空值」选项 - 筛选:选中的价格先经 ROUND(price,2) 换回货号再按 sku 过滤;选中的价格无对应货号返回空结果; 「空值」= 货号不在限价表里 - 补 3 条单测(价格→货号、无匹配、空值) --- ...y-chain-product-archive-text-rules.test.ts | 35 ++++++++++++++++++- .../supply-chain-product-archive-filters.ts | 32 +++++++++++++++++ ...supply-chain-product-archive.repository.ts | 28 +++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/furniture-3d-system/src/server/repositories/__tests__/supply-chain-product-archive-text-rules.test.ts b/furniture-3d-system/src/server/repositories/__tests__/supply-chain-product-archive-text-rules.test.ts index 2e8ec02a..a328cb8f 100644 --- a/furniture-3d-system/src/server/repositories/__tests__/supply-chain-product-archive-text-rules.test.ts +++ b/furniture-3d-system/src/server/repositories/__tests__/supply-chain-product-archive-text-rules.test.ts @@ -11,17 +11,24 @@ import { } from '../supply-chain-product-archive-filters' jest.mock('@/lib/prisma', () => ({ - prisma: { $queryRaw: jest.fn() }, + prisma: { + $queryRaw: jest.fn(), + supplyChainStyleLimitPrice: { findMany: jest.fn() }, + }, })) const mockedQueryRaw = (jest.requireMock('@/lib/prisma') as { prisma: { $queryRaw: jest.Mock } }).prisma.$queryRaw +const mockedStyleLimitFindMany = (jest.requireMock('@/lib/prisma') as { + prisma: { supplyChainStyleLimitPrice: { findMany: jest.Mock } } +}).prisma.supplyChainStyleLimitPrice.findMany beforeEach(() => { clearNormalCodeValueCache() mockedQueryRaw.mockReset() mockedQueryRaw.mockResolvedValue([{ value: 'DNZ0411' }, { value: 'XG0022' }]) + mockedStyleLimitFindMany.mockReset() }) describe('normal archive code pattern', () => { @@ -241,4 +248,30 @@ describe('buildProductArchiveWhereWithTextRules normal code', () => { expect(serialized).toContain('整装') expect(serialized).toContain('组装') }) + + // 「负责人限价」是 SupplyChainStyleLimitPrice 的派生列:按价格换回货号再过滤。 + it('filters 负责人限价 by mapping the selected price back to 货号', async () => { + mockedQueryRaw.mockResolvedValueOnce([{ styleCode: 'SJ004833' }, { styleCode: 'SJ004834' }]) + const where = await buildProductArchiveWhereWithTextRules({ opsPriceLimit: ['213.80'] }) as { AND?: unknown[] } + const serialized = JSON.stringify(where.AND) + + expect(serialized).toContain('{"sku":{"in":["SJ004833","SJ004834"]}}') + }) + + it('returns no rows when the selected 负责人限价 has no matching 货号', async () => { + mockedQueryRaw.mockResolvedValueOnce([]) + const where = await buildProductArchiveWhereWithTextRules({ opsPriceLimit: ['123.45'] }) as { AND?: unknown[] } + + expect(JSON.stringify(where.AND)).toContain('__NO_MATCH__') + }) + + it('treats 负责人限价空值 as 货号不在限价表里', async () => { + mockedStyleLimitFindMany.mockResolvedValueOnce([{ styleCode: 'SJ004833' }, { styleCode: 'SJ004834' }]) + const where = await buildProductArchiveWhereWithTextRules({ + opsPriceLimit: [PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE], + }) as { AND?: unknown[] } + const serialized = JSON.stringify(where.AND) + + expect(serialized).toContain('"NOT":{"sku":{"in":["SJ004833","SJ004834"]}}') + }) }) diff --git a/furniture-3d-system/src/server/repositories/supply-chain-product-archive-filters.ts b/furniture-3d-system/src/server/repositories/supply-chain-product-archive-filters.ts index dd5fbff8..cbee24b8 100644 --- a/furniture-3d-system/src/server/repositories/supply-chain-product-archive-filters.ts +++ b/furniture-3d-system/src/server/repositories/supply-chain-product-archive-filters.ts @@ -605,6 +605,38 @@ export async function buildProductArchiveWhereWithTextRules( if (clause) extra.push(clause) } } + // 「负责人限价」:派生自 SupplyChainStyleLimitPrice.ownerLimitPrice(按货号), + // 先把选中的价格换回货号,再按 sku 过滤;「空值」= 不在限价表里的货号。 + if (omitFilter !== 'opsPriceLimit') { + const rawValues = (filters.opsPriceLimit ?? []).filter(Boolean) + if (rawValues.length > 0) { + const includesEmpty = rawValues.includes(PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE) + const prices = rawValues + .filter((value) => value !== PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE) + .map((value) => Number(String(value).replace(/[^\d.-]/g, ''))) + .filter((value) => Number.isFinite(value)) + const priceClauses: Prisma.SupplyChainProductArchiveWhereInput[] = [] + if (prices.length > 0) { + const rows = await prisma.$queryRaw>(Prisma.sql` + SELECT DISTINCT "styleCode" + FROM "SupplyChainStyleLimitPrice" + WHERE "ownerLimitPrice" IS NOT NULL + AND ROUND("ownerLimitPrice"::numeric, 2) IN (${Prisma.join(prices)}) + `) + const styleCodes = rows.map((row) => row.styleCode).filter(Boolean) + // 选中的价格在限价表里没有对应货号时返回空结果(而不是忽略条件) + priceClauses.push({ sku: { in: styleCodes.length > 0 ? styleCodes : ['__NO_MATCH__'] } }) + } + if (includesEmpty) { + const limitedStyles = await prisma.supplyChainStyleLimitPrice.findMany({ select: { styleCode: true } }) + priceClauses.push({ + NOT: { sku: { in: limitedStyles.map((row) => row.styleCode).filter(Boolean) } }, + }) + } + if (priceClauses.length === 1) extra.push(priceClauses[0]) + else if (priceClauses.length > 1) extra.push({ OR: priceClauses }) + } + } const asyncClauses = [...extra, ...ownerClauses] return asyncClauses.length > 0 ? { AND: [base, ...asyncClauses] } : base } diff --git a/furniture-3d-system/src/server/repositories/supply-chain-product-archive.repository.ts b/furniture-3d-system/src/server/repositories/supply-chain-product-archive.repository.ts index 75c475b4..2e80d2b9 100644 --- a/furniture-3d-system/src/server/repositories/supply-chain-product-archive.repository.ts +++ b/furniture-3d-system/src/server/repositories/supply-chain-product-archive.repository.ts @@ -140,6 +140,34 @@ class SupplyChainProductArchiveRepositoryClass implements SupplyChainProductArch const values = index.spuSet.size > 0 ? ['有', '无'] : ['无'] return pageOptions(values, params) } + // 「负责人限价」是派生列(SupplyChainStyleLimitPrice.ownerLimitPrice,按货号), + // 候选值 = 当前筛选范围内实际出现的限价(两位小数,与界面展示一致)。 + if (params.field === 'opsPriceLimit') { + const query = await preparePeriodArchiveQuery(params.filters, params.field) + const unfiltered = query.ids === null && Object.keys(query.where).length === 0 + if (unfiltered) { + const rows = await prisma.$queryRaw>(Prisma.sql` + SELECT DISTINCT ROUND("ownerLimitPrice"::numeric, 2)::text AS price + FROM "SupplyChainStyleLimitPrice" + WHERE "ownerLimitPrice" IS NOT NULL + ORDER BY price + `) + // 没有流程限价的商品占多数,「空值」始终是一个有效候选 + return pageOptions([...rows.map((row) => row.price), PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE], params) + } + const records = await readSelectedArchives(query, { sku: true }) + const styleCodes = [...new Set(records.map((record) => String(record.sku ?? '').trim()).filter(Boolean))] + const limitRows = styleCodes.length + ? await prisma.supplyChainStyleLimitPrice.findMany({ + where: { styleCode: { in: styleCodes }, ownerLimitPrice: { not: null } }, + select: { styleCode: true, ownerLimitPrice: true }, + }) + : [] + const values = [...new Set(limitRows.map((row) => Number(row.ownerLimitPrice).toFixed(2)))] + const coveredStyles = new Set(limitRows.map((row) => row.styleCode)) + const hasMissingLimit = styleCodes.some((styleCode) => !coveredStyles.has(styleCode)) + return pageOptions(hasMissingLimit ? [...values, PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE] : values, params) + } const field = FILTER_OPTION_DATABASE_FIELDS[params.field as keyof typeof FILTER_OPTION_DATABASE_FIELDS] if (!field) return { values: [], hasMore: false, total: 0 } const query = await preparePeriodArchiveQuery(params.filters, params.field)