diff --git a/furniture-3d-system/prisma/migrations/20261019120000_add_product_archive_install_method/migration.sql b/furniture-3d-system/prisma/migrations/20261019120000_add_product_archive_install_method/migration.sql new file mode 100644 index 00000000..b823950d --- /dev/null +++ b/furniture-3d-system/prisma/migrations/20261019120000_add_product_archive_install_method/migration.sql @@ -0,0 +1,2 @@ +-- 产品全量档案新增「安装方式」列:取值来源产品开发(产品定厂 SKU installMethod,整装/拆装)。 +ALTER TABLE "SupplyChainProductArchive" ADD COLUMN "installMethod" TEXT NOT NULL DEFAULT ''; diff --git a/furniture-3d-system/prisma/schema.prisma b/furniture-3d-system/prisma/schema.prisma index 5a76570c..b3fc51cd 100644 --- a/furniture-3d-system/prisma/schema.prisma +++ b/furniture-3d-system/prisma/schema.prisma @@ -711,6 +711,7 @@ model SupplyChainProductArchive { productAttribute String @default("") productTag String @default("") installDocId String @default("") + installMethod String @default("") hasDrawing String @default("") hasVideo String @default("") spec String @default("") diff --git a/furniture-3d-system/src/app/supply-chain/database/product-archive/ProductArchiveView.tsx b/furniture-3d-system/src/app/supply-chain/database/product-archive/ProductArchiveView.tsx index 8b3db2d4..9576ac71 100644 --- a/furniture-3d-system/src/app/supply-chain/database/product-archive/ProductArchiveView.tsx +++ b/furniture-3d-system/src/app/supply-chain/database/product-archive/ProductArchiveView.tsx @@ -368,7 +368,7 @@ export function ProductArchiveView({ style={{ background: GROUP_COLORS[g.group] || '#fff' }} >
- {c.label} + {c.label} { + it('定厂值非空时直接使用定厂值', () => { + expect(installMethodLabel('整装', '组装')).toBe('整装') + expect(installMethodLabel('拆装', '1688一件代发,整装')).toBe('拆装') + expect(installMethodLabel('组装', '')).toBe('组装') + }) + + it('定厂为空时按商品标签推导:整装优先于组装', () => { + expect(installMethodLabel('', '1688一件代发,不包安装,含税,已匹配,普票,组装')).toBe('组装') + expect(installMethodLabel('', '1688一件代发,不包安装,含税,已匹配,整装,普票')).toBe('整装') + expect(installMethodLabel('', '含税,普票,整装,组装')).toBe('整装') + }) + + it('两边都推不出为空', () => { + expect(installMethodLabel('', '')).toBe('') + expect(installMethodLabel('', null)).toBe('') + expect(installMethodLabel(null, '1688一件代发,含税,普票')).toBe('') + expect(installMethodLabel(undefined, undefined)).toBe('') + }) +}) diff --git a/furniture-3d-system/src/lib/supply-chain-product-archive-values.ts b/furniture-3d-system/src/lib/supply-chain-product-archive-values.ts index fd8552d0..dfaa7d1e 100644 --- a/furniture-3d-system/src/lib/supply-chain-product-archive-values.ts +++ b/furniture-3d-system/src/lib/supply-chain-product-archive-values.ts @@ -46,3 +46,21 @@ export function materialLevelFromMaterial(material: string | null | undefined): if (/E1(?![0-9])/.test(text)) return 'E1' return '' } + +/** + * 「安装方式」展示口径:产品定厂 installMethod 优先(整装/拆装/组装); + * 为空时由商品标签(productTag,源头聚水潭 jst_sku.labels,产品开发 product_tag 补充) + * 推导:含「整装」→整装、含「组装」→组装,都不含为空。 + * 为空值要去聚水潭的商品标签里补,不在本系统维护。查询时推导,不落库。 + */ +export function installMethodLabel( + installMethod: string | null | undefined, + productTag: string | null | undefined, +): string { + const factory = String(installMethod ?? '').trim() + if (factory) return factory + const tag = String(productTag ?? '') + if (tag.includes('整装')) return '整装' + if (tag.includes('组装')) return '组装' + return '' +} diff --git a/furniture-3d-system/src/lib/supply-chain-product-archive.ts b/furniture-3d-system/src/lib/supply-chain-product-archive.ts index 71d31a4c..c672f78d 100644 --- a/furniture-3d-system/src/lib/supply-chain-product-archive.ts +++ b/furniture-3d-system/src/lib/supply-chain-product-archive.ts @@ -36,6 +36,7 @@ export type SupplyChainProductArchiveRow = { packType: string productTag: string installDocId: string + installMethod: string hasDrawing: string hasVideo: string /** 是否有检测报告(对外接口按 SPU 匹配,'有' / '无' / '' 表示读取不到) */ @@ -230,6 +231,7 @@ export type ProductArchiveFilters = { replaceCount?: string[] orderType?: string[] storageType?: string[] + installMethod?: string[] isSeasonal?: string[] supplierType?: string[] supplierStatus?: string[] @@ -304,7 +306,7 @@ export const PRODUCT_ARCHIVE_FILTER_OPTION_KEYS = [ 'image', 'categoryL1', 'categoryL2', 'spu', 'sku', 'barcode', 'productName', 'designer', 'productStatus', 'productionType', 'material', 'materialLevel', 'inspectionReport', 'color', 'spec', 'weight', 'length', 'width', 'height', 'volume', - 'packType', 'productTag', 'brand', 'orderType', 'storageType', 'isSeasonal', 'replaceCount', + 'packType', 'productTag', 'brand', 'orderType', 'storageType', 'installMethod', 'isSeasonal', 'replaceCount', 'region', 'supplier', 'supplierType', 'supplierStatus', 'purchaser', 'settleType', 'boardProvider', 'penaltyStatus', 'moq', 'firstLeadTime', 'stableLeadTime', 'palletSpec', 'backupSupplier', 'opsDirector', 'opsDeptHead', 'opsHead', 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 5ab8b524..2e8ec02a 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 @@ -201,4 +201,44 @@ describe('buildProductArchiveWhereWithTextRules normal code', () => { expect(serialized).toContain('ENF') expect(serialized).toContain('E1') }) + + // 「安装方式」展示口径 = 定厂值优先、商品标签(整装/组装)推导兜底,筛选按同一口径拼条件。 + it('filters 整装 by factory value or product tag derivation', async () => { + const where = await buildProductArchiveWhereWithTextRules({ installMethod: ['整装'] }) as { AND?: unknown[] } + const serialized = JSON.stringify(where.AND) + + expect(serialized).toContain('installMethod') + expect(serialized).toContain('整装') + expect(serialized).toContain('productTag') + expect(serialized).not.toContain('组装') + }) + + it('filters 组装 by factory value or product tag derivation', async () => { + const where = await buildProductArchiveWhereWithTextRules({ installMethod: ['组装'] }) as { AND?: unknown[] } + const serialized = JSON.stringify(where.AND) + + expect(serialized).toContain('组装') + expect(serialized).toContain('productTag') + expect(serialized).not.toContain('整装') + }) + + it('filters 拆装 by factory column only(标签里没有拆装)', async () => { + const where = await buildProductArchiveWhereWithTextRules({ installMethod: ['拆装'] }) as { AND?: unknown[] } + const serialized = JSON.stringify(where.AND) + + expect(serialized).toContain('拆装') + expect(serialized).not.toContain('productTag') + }) + + it('keeps the empty option for rows neither factory nor tag derives', async () => { + const where = await buildProductArchiveWhereWithTextRules({ + installMethod: [PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE], + }) as { AND?: unknown[] } + const serialized = JSON.stringify(where.AND) + + expect(serialized).toContain('"installMethod":""') + expect(serialized).toContain('"NOT"') + expect(serialized).toContain('整装') + expect(serialized).toContain('组装') + }) }) 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 b8ab00e1..dd5fbff8 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 @@ -138,6 +138,40 @@ function buildEmptyMaterialLevelClause(): Prisma.SupplyChainProductArchiveWhereI } } +/** + * 「安装方式」筛选:展示口径 = 定厂值优先、商品标签(整装/组装)推导兜底, + * 筛选按同一口径拼条件(整装/组装可由标签推导;拆装等只存在于定厂原始列)。 + */ +function buildInstallMethodClauses(values: string[]): Prisma.SupplyChainProductArchiveWhereInput[] { + const clauses: Prisma.SupplyChainProductArchiveWhereInput[] = [] + for (const raw of values) { + const value = raw.trim() + if (!value) continue + if (value === '整装' || value === '组装') { + clauses.push({ + OR: [ + { installMethod: value }, + { AND: [{ installMethod: '' }, { productTag: { contains: value } }] }, + ], + }) + } else { + clauses.push({ installMethod: value }) + } + } + return clauses +} + +/** 「安装方式」= 空值:定厂列为空,且商品标签里既没有「整装」也没有「组装」。 */ +function buildEmptyInstallMethodClause(): Prisma.SupplyChainProductArchiveWhereInput { + return { + AND: [ + { installMethod: '' }, + { NOT: { productTag: { contains: '整装' } } }, + { NOT: { productTag: { contains: '组装' } } }, + ], + } +} + /** * 「是否有检测报告」筛选:数据来自外部检测报告接口(按 SPU 匹配,10 分钟缓存)。 * 有 → SPU 在报告清单里;无 → SPU 不在(含空 SPU)。 @@ -549,6 +583,19 @@ export async function buildProductArchiveWhereWithTextRules( if (clauses.length > 0) extra.push(clauses.length === 1 ? clauses[0] : { OR: clauses }) } } + // 「安装方式」:按展示口径(定厂值优先 > 商品标签整装/组装推导)筛选。 + if (omitFilter !== 'installMethod') { + const installValues = filters.installMethod ?? [] + if (installValues.length > 0) { + const clauses = buildInstallMethodClauses( + installValues.filter((value) => value !== PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE), + ) + if (installValues.includes(PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE)) { + clauses.push(buildEmptyInstallMethodClause()) + } + if (clauses.length > 0) extra.push(clauses.length === 1 ? clauses[0] : { OR: clauses }) + } + } // 「是否有检测报告」:按外部检测报告接口的 SPU 清单筛选。 if (omitFilter !== 'inspectionReport') { const reportValues = (filters.inspectionReport ?? []) diff --git a/furniture-3d-system/src/server/repositories/supply-chain-product-archive-select.ts b/furniture-3d-system/src/server/repositories/supply-chain-product-archive-select.ts index 1159f05a..ef91f090 100644 --- a/furniture-3d-system/src/server/repositories/supply-chain-product-archive-select.ts +++ b/furniture-3d-system/src/server/repositories/supply-chain-product-archive-select.ts @@ -38,6 +38,7 @@ export const productArchiveSelect = { packType: true, productTag: true, installDocId: true, + installMethod: true, hasDrawing: true, hasVideo: true, spec: true, diff --git a/furniture-3d-system/src/server/repositories/supply-chain-product-archive-sync.repository.ts b/furniture-3d-system/src/server/repositories/supply-chain-product-archive-sync.repository.ts index 06b9282b..0f0c2e5a 100644 --- a/furniture-3d-system/src/server/repositories/supply-chain-product-archive-sync.repository.ts +++ b/furniture-3d-system/src/server/repositories/supply-chain-product-archive-sync.repository.ts @@ -52,6 +52,7 @@ export type ProductCatalogWideArchiveRecord = { purchase_order_method: string | null factory_warehouse: string | null shipping_method: string | null + install_method: string | null sku_image_url: string | null updated_at: Date | null } @@ -110,6 +111,7 @@ const archiveComparableSelect = { productAttribute: true, productTag: true, installDocId: true, + installMethod: true, hasDrawing: true, hasVideo: true, spec: true, @@ -185,6 +187,7 @@ function buildArchiveUpdate( productAttribute: archive.productAttribute, productTag: archive.productTag, installDocId: archive.installDocId, + installMethod: archive.installMethod, spec: archive.spec, brand: archive.brand, orderType: archive.orderType, @@ -276,6 +279,7 @@ class SupplyChainProductArchiveSyncRepositoryClass purchase_order_method, factory_warehouse, shipping_method, + install_method, CASE WHEN NULLIF(btrim(COALESCE(archive_image, '')), '') IS NOT NULL THEN image_url ELSE NULL 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 3889f2d3..75c475b4 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 @@ -1,6 +1,6 @@ import { Prisma } from '@prisma/client' import { prisma } from '@/lib/prisma' -import { materialLevelFromMaterial, storageTypeLabel } from '@/lib/supply-chain-product-archive-values' +import { installMethodLabel, materialLevelFromMaterial, storageTypeLabel } from '@/lib/supply-chain-product-archive-values' import { inferStorageTypeLabelFromWarehouseName } from '@/lib/supply-chain-product-archive-warehouse-config' import { supplyChainProductArchiveWarehouseConfigRepository } from './supply-chain-product-archive-warehouse-config.repository' import { productArchiveCertificateReportService } from '@/server/services/product-archive-certificate-report.service' @@ -124,6 +124,14 @@ class SupplyChainProductArchiveRepositoryClass implements SupplyChainProductArch const values = records.map((record) => materialLevelFromMaterial(record.material)) return pageOptions(values, params) } + // 「安装方式」:展示值 = 定厂列优先、商品标签(整装/组装)推导兜底, + // 候选值按同一口径算,否则下拉里会缺推导值。 + if (params.field === 'installMethod') { + const query = await preparePeriodArchiveQuery(params.filters, params.field) + const records = await readSelectedArchives(query, { installMethod: true, productTag: true }) + const values = records.map((record) => installMethodLabel(record.installMethod, record.productTag)) + return pageOptions(values, params) + } // 「是否有检测报告」:外部接口按 SPU 匹配,候选项固定「有 / 无」 // (接口未配置/读取失败时返回空列表,界面显示为空)。 if (params.field === 'inspectionReport') { diff --git a/furniture-3d-system/src/server/services/__tests__/supply-chain-product-archive-sync.service.test.ts b/furniture-3d-system/src/server/services/__tests__/supply-chain-product-archive-sync.service.test.ts index c05625a0..4b2b3f40 100644 --- a/furniture-3d-system/src/server/services/__tests__/supply-chain-product-archive-sync.service.test.ts +++ b/furniture-3d-system/src/server/services/__tests__/supply-chain-product-archive-sync.service.test.ts @@ -50,6 +50,7 @@ function createSourceRecord() { purchase_order_method: '非订单式', factory_warehouse: '成品仓', shipping_method: '', + install_method: '整装', sku_image_url: 'https://cp.jiansun.vip/api/storage/object?key=sku-1.jpeg', updated_at: new Date('2026-07-17T08:00:00.000Z'), } @@ -82,6 +83,7 @@ describe('supply-chain product archive wide-catalog sync', () => { supplierName: '源沁家具', purchaseOwner: '宁珂', minimumOrderQuantity: '1', + installMethod: '整装', firstSourceCode: PRODUCT_DEVELOPMENT_ARCHIVE_SOURCE_CODE, }), source: { @@ -175,6 +177,38 @@ describe('supply-chain product archive wide-catalog sync', () => { expect(mapped?.archive).not.toHaveProperty('storageType') }) + it('maps the factory install method into the archive install method with lineage', () => { + const mapped = mapProductCatalogWideArchiveSource({ + ...createSourceRecord(), + install_method: '拆装', + }) + + expect(mapped?.archive).toEqual( + expect.objectContaining({ installMethod: '拆装' }), + ) + const fieldSourceMap = mapped?.archive.fieldSourceMap as Record< + string, + { sourceCode: string; sourceColumn: string } + > + expect(fieldSourceMap.installMethod).toEqual( + expect.objectContaining({ + sourceCode: PRODUCT_DEVELOPMENT_ARCHIVE_SOURCE_CODE, + sourceColumn: 'install_method', + }), + ) + }) + + it('keeps the archive install method empty when the factory row has none', () => { + const mapped = mapProductCatalogWideArchiveSource({ + ...createSourceRecord(), + install_method: null, + }) + + expect(mapped?.archive).toEqual( + expect.objectContaining({ installMethod: '' }), + ) + }) + it('upserts eligible rows and removes stale product sources', async () => { const valid = createSourceRecord() const missingBarcode = { diff --git a/furniture-3d-system/src/server/services/supply-chain-product-archive-query.ts b/furniture-3d-system/src/server/services/supply-chain-product-archive-query.ts index b6d29593..05df2635 100644 --- a/furniture-3d-system/src/server/services/supply-chain-product-archive-query.ts +++ b/furniture-3d-system/src/server/services/supply-chain-product-archive-query.ts @@ -57,6 +57,7 @@ const productArchiveFiltersSchema = z.object({ productTag: archiveValueArraySchema.optional(), orderType: archiveValueArraySchema.optional(), storageType: archiveValueArraySchema.optional(), + installMethod: archiveValueArraySchema.optional(), isSeasonal: archiveValueArraySchema.optional(), replaceCount: archiveValueArraySchema.optional(), region: archiveValueArraySchema.optional(), diff --git a/furniture-3d-system/src/server/services/supply-chain-product-archive-sync.service.ts b/furniture-3d-system/src/server/services/supply-chain-product-archive-sync.service.ts index 67beab87..ee67b8a5 100644 --- a/furniture-3d-system/src/server/services/supply-chain-product-archive-sync.service.ts +++ b/furniture-3d-system/src/server/services/supply-chain-product-archive-sync.service.ts @@ -158,6 +158,7 @@ export function mapProductCatalogWideArchiveSource( packType: resolveProductArchivePackageType(text(record.package_set_count)), productTag: text(record.product_tag), installDocId: text(record.drawing_id), + installMethod: text(record.install_method), spec: text(record.archive_spec), brand: text(record.brand_name), orderType: normalizeProductArchiveOrderType(record.purchase_order_method), diff --git a/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts b/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts index bfc36af6..15e49371 100644 --- a/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts +++ b/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts @@ -1,7 +1,7 @@ import type { Prisma } from '@prisma/client' import type { SupplyChainProductArchiveRow } from '@/lib/supply-chain-product-archive' import { productArchiveDataTagText } from '@/lib/supply-chain-product-archive' -import { storageTypeLabel } from '@/lib/supply-chain-product-archive-values' +import { installMethodLabel, storageTypeLabel } from '@/lib/supply-chain-product-archive-values' import { inferStorageTypeLabelFromWarehouseName } from '@/lib/supply-chain-product-archive-warehouse-config' import type { ProductArchiveRecord } from '@/server/repositories/supply-chain-product-archive.repository' @@ -107,6 +107,8 @@ export function mapProductArchiveRow({ packType: record.packType, productTag: record.productTag, installDocId: record.installDocId, + // 展示口径:定厂值优先,为空时由商品标签(整装/组装)推导,与筛选口径一致。 + installMethod: installMethodLabel(record.installMethod, record.productTag), hasDrawing: record.hasDrawing, hasVideo: record.hasVideo, // 是否有检测报告:由外部「检测报告」接口按 SPU 注入(见 listPage 的 withInspectionReport 选项)