feat(supply-chain): 产品全量档案新增安装方式列(定厂优先+商品标签推导)
This commit is contained in:
parent
132331733e
commit
ee983673d5
@ -0,0 +1,2 @@
|
||||
-- 产品全量档案新增「安装方式」列:取值来源产品开发(产品定厂 SKU installMethod,整装/拆装)。
|
||||
ALTER TABLE "SupplyChainProductArchive" ADD COLUMN "installMethod" TEXT NOT NULL DEFAULT '';
|
||||
@ -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("")
|
||||
|
||||
@ -368,7 +368,7 @@ export function ProductArchiveView({
|
||||
style={{ background: GROUP_COLORS[g.group] || '#fff' }}
|
||||
>
|
||||
<div className="archive-th-field-content">
|
||||
<span className="archive-th-field-label" title={c.label}>{c.label}</span>
|
||||
<span className="archive-th-field-label" title={c.tip ?? c.label}>{c.label}</span>
|
||||
<ProductArchiveHeaderFilter
|
||||
prop={c.prop}
|
||||
label={c.label}
|
||||
|
||||
@ -6,6 +6,8 @@ export type ArchiveColumn = {
|
||||
prop: string
|
||||
label: string
|
||||
width: number
|
||||
/** 列头悬停提示;不填时悬停只显示列名。 */
|
||||
tip?: string
|
||||
}
|
||||
|
||||
export type ArchiveColumnGroup = {
|
||||
@ -46,6 +48,7 @@ export const ARCHIVE_COLUMNS: ArchiveColumnGroup[] = [
|
||||
{ prop: 'brand', label: '品牌', width: 100 },
|
||||
{ prop: 'orderType', label: '叫货类型', width: 100 },
|
||||
{ prop: 'storageType', label: '入库/代发', width: 100 },
|
||||
{ prop: 'installMethod', label: '安装方式', width: 100, tip: '定厂安装方式优先,为空时由商品标签(整装/组装)推导;空值需在聚水潭商品标签里补充' },
|
||||
{ prop: 'isSeasonal', label: '是否季节性产品', width: 130 },
|
||||
{ prop: 'replaceCount', label: '替换款数量', width: 100 },
|
||||
],
|
||||
|
||||
@ -45,6 +45,7 @@ export const ARCHIVE_PRECISE_FIELDS: ArchiveToolbarField[] = [
|
||||
field('supplierType', '供应商类型'),
|
||||
field('settleType', '结算方式'),
|
||||
field('storageType', '入库/代发'),
|
||||
field('installMethod', '安装方式'),
|
||||
field('boardProvider', '板材提供方'),
|
||||
field('installDocId', '安装图纸/视频ID'),
|
||||
]
|
||||
|
||||
@ -22,6 +22,7 @@ export const PRODUCT_ARCHIVE_CHANGE_FIELDS = [
|
||||
{ key: 'packType', label: '单双包' },
|
||||
{ key: 'productTag', label: '商品标签' },
|
||||
{ key: 'installDocId', label: '安装图纸/视频ID' },
|
||||
{ key: 'installMethod', label: '安装方式' },
|
||||
{ key: 'hasDrawing', label: '是否有图纸' },
|
||||
{ key: 'hasVideo', label: '是否有视频' },
|
||||
{ key: 'spec', label: '产品尺寸' },
|
||||
|
||||
@ -82,6 +82,7 @@ export const PRODUCT_DEVELOPMENT_ARCHIVE_FIELD_COLUMNS = {
|
||||
productAttribute: 'item_type',
|
||||
productTag: 'product_tag',
|
||||
installDocId: 'drawing_id',
|
||||
installMethod: 'install_method',
|
||||
spec: 'archive_spec',
|
||||
brand: 'brand_name',
|
||||
orderType: 'purchase_order_method',
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
installMethodLabel,
|
||||
materialLevelFromMaterial,
|
||||
storageTypeLabel,
|
||||
storageTypeFromShippingMethodLabel,
|
||||
@ -50,3 +51,24 @@ describe('产品全量档案 · 材质等级取值(从「材质」识别 E0/E1
|
||||
expect(materialLevelFromMaterial('E10级板')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('产品全量档案 · 安装方式取值(定厂优先,商品标签推导兜底)', () => {
|
||||
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('')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 ''
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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('组装')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 ?? [])
|
||||
|
||||
@ -38,6 +38,7 @@ export const productArchiveSelect = {
|
||||
packType: true,
|
||||
productTag: true,
|
||||
installDocId: true,
|
||||
installMethod: true,
|
||||
hasDrawing: true,
|
||||
hasVideo: true,
|
||||
spec: true,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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 选项)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user