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 选项)