fix(supply-chain): 运营负责人改为一次性定标冻结(摘除跟随最新销售的每日翻转)
This commit is contained in:
parent
139aec0e70
commit
c3b20aace6
@ -62,8 +62,6 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [],
|
||||
dwsOpsOwners: [],
|
||||
spuOpsOwners: [],
|
||||
intakeSpuOwners: new Map(),
|
||||
ownerHierarchyByPerson: new Map(),
|
||||
catalogCosts: [],
|
||||
@ -88,8 +86,6 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
dwsGoodsCosts: [{ skuoid: 'DF-A3BJ006601', goodsCost: '70.18' }],
|
||||
skumapPics: [{ skuId: 'DF-A3BJ006601', pic: 'https://img.pddpic.com/mms-material-img/df.jpg' }],
|
||||
skumapFillPics: [],
|
||||
dwsOpsOwners: [],
|
||||
spuOpsOwners: [],
|
||||
intakeSpuOwners: new Map(),
|
||||
ownerHierarchyByPerson: new Map(),
|
||||
catalogCosts: [],
|
||||
@ -163,8 +159,6 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [{ skuId: 'BAR-001', pic: 'https://img.example.com/bar-001.jpg' }],
|
||||
dwsOpsOwners: [],
|
||||
spuOpsOwners: [],
|
||||
intakeSpuOwners: new Map(),
|
||||
ownerHierarchyByPerson: new Map(),
|
||||
catalogCosts: [],
|
||||
@ -210,7 +204,7 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
expect(filledUpdates.some((update) => 'image' in update.patch)).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to spu-level ops owners when the barcode has no direct dws row', () => {
|
||||
it('binds the intake spuOwner only when the archive has no stored opsHead', () => {
|
||||
const lookups = {
|
||||
clearanceStocks: [],
|
||||
publicAvailableStocks: [],
|
||||
@ -221,70 +215,6 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [],
|
||||
dwsOpsOwners: [],
|
||||
spuOpsOwners: [{ spu: 'DF-A3CJ0024', cater: '虞姬', ider: '李逍遥' }],
|
||||
intakeSpuOwners: new Map(),
|
||||
ownerHierarchyByPerson: new Map(),
|
||||
catalogCosts: [],
|
||||
warehouseConfigs: [],
|
||||
}
|
||||
|
||||
// Same-spu barcode without its own dws row inherits the spu owners
|
||||
const fallbackUpdates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-df-spu',
|
||||
barcode: 'DF-A3CJ002401',
|
||||
spu: 'DF-A3CJ0024',
|
||||
sku: 'DF-A3CJ0024',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
image: '',
|
||||
},
|
||||
lookups,
|
||||
)
|
||||
const opsUpdate = fallbackUpdates.find((update) => 'opsHead' in update.patch)
|
||||
expect(opsUpdate?.patch).toEqual({ opsHead: '虞姬' })
|
||||
expect(opsUpdate?.source).toEqual(
|
||||
expect.objectContaining({ sourceRecordId: 'spu:DF-A3CJ0024' }),
|
||||
)
|
||||
|
||||
// Direct dws match still wins over the spu fallback
|
||||
const directLookups = {
|
||||
...lookups,
|
||||
dwsOpsOwners: [{ skuoid: 'DF-A3CJ002401', cater: '比干', ider: '', lastSaleDate: '2026-09-01' }],
|
||||
}
|
||||
const directUpdates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-df-spu',
|
||||
barcode: 'DF-A3CJ002401',
|
||||
spu: 'DF-A3CJ0024',
|
||||
sku: 'DF-A3CJ0024',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
image: '',
|
||||
},
|
||||
directLookups,
|
||||
)
|
||||
const directUpdate = directUpdates.find((update) => 'opsHead' in update.patch)
|
||||
expect(directUpdate?.patch).toEqual({ opsHead: '比干' })
|
||||
expect(directUpdate?.source).toEqual(
|
||||
expect.objectContaining({ sourceRecordId: 'DF-A3CJ002401' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to the intake process spuOwner when there is no sales history', () => {
|
||||
const lookups = {
|
||||
clearanceStocks: [],
|
||||
publicAvailableStocks: [],
|
||||
suppliers: [],
|
||||
stocks: [],
|
||||
warehouses: [],
|
||||
prices: [],
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [],
|
||||
dwsOpsOwners: [],
|
||||
spuOpsOwners: [],
|
||||
intakeSpuOwners: new Map([['BJ0220', '萧何']]),
|
||||
ownerHierarchyByPerson: new Map([
|
||||
['萧何', { departmentOwner: '东邪', generalOwner: '無尘' }],
|
||||
@ -317,6 +247,39 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
expect(hierarchyUpdate?.patch).toEqual({ opsDeptHead: '东邪', opsDirector: '無尘' })
|
||||
})
|
||||
|
||||
it('never overwrites a stored opsHead with the intake binding', () => {
|
||||
const lookups = {
|
||||
clearanceStocks: [],
|
||||
publicAvailableStocks: [],
|
||||
suppliers: [],
|
||||
stocks: [],
|
||||
warehouses: [],
|
||||
prices: [],
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [],
|
||||
intakeSpuOwners: new Map([['BJ0220', '萧何']]),
|
||||
ownerHierarchyByPerson: new Map(),
|
||||
catalogCosts: [],
|
||||
warehouseConfigs: [],
|
||||
}
|
||||
|
||||
const updates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-bound',
|
||||
barcode: 'BJ022001N100V01',
|
||||
spu: 'BJ0220',
|
||||
sku: 'BJ022001',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
image: '',
|
||||
opsHead: '灵犀',
|
||||
},
|
||||
lookups,
|
||||
)
|
||||
expect(updates.some((update) => 'opsHead' in update.patch)).toBe(false)
|
||||
})
|
||||
|
||||
it('normalizes invisible characters so identical names always match', () => {
|
||||
expect(normalizeIdentity('琼\u200B琚')).toBe(normalizeIdentity('琼琚'))
|
||||
expect(normalizeIdentity(' 子\uFEFF芩 ')).toBe('子芩')
|
||||
@ -334,8 +297,6 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [],
|
||||
dwsOpsOwners: [],
|
||||
spuOpsOwners: [],
|
||||
intakeSpuOwners: new Map(),
|
||||
ownerHierarchyByPerson: new Map([
|
||||
['子芩', { departmentOwner: '子芩', generalOwner: '江陵' }],
|
||||
@ -365,7 +326,7 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
expect(hierarchyUpdate?.source).toEqual(
|
||||
expect.objectContaining({ sourceRecordId: '子芩' }),
|
||||
)
|
||||
// 推不出运营负责人时只补层级,不动 opsHead 本身
|
||||
// 层级只按档案已存的运营负责人匹配,不再反推 opsHead 本身
|
||||
expect(updates.some((update) => 'opsHead' in update.patch)).toBe(false)
|
||||
})
|
||||
|
||||
@ -380,8 +341,6 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [],
|
||||
dwsOpsOwners: [],
|
||||
spuOpsOwners: [],
|
||||
intakeSpuOwners: new Map(),
|
||||
ownerHierarchyByPerson: new Map([
|
||||
['琼琚', { departmentOwner: '半夏', generalOwner: '江陵' }],
|
||||
@ -410,83 +369,6 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
expect(hierarchyUpdate?.patch).toEqual({ opsDeptHead: '半夏', opsDirector: '江陵' })
|
||||
})
|
||||
|
||||
it('takes ops owners from the dws sales table only when dws has a value', () => {
|
||||
const lookups = {
|
||||
clearanceStocks: [],
|
||||
publicAvailableStocks: [],
|
||||
suppliers: [],
|
||||
stocks: [],
|
||||
warehouses: [],
|
||||
prices: [],
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [],
|
||||
dwsOpsOwners: [
|
||||
{ skuoid: 'BAR-001', cater: '比干', ider: '精卫' },
|
||||
{ skuoid: 'BAR-002', cater: '香茗', ider: '' },
|
||||
],
|
||||
spuOpsOwners: [],
|
||||
intakeSpuOwners: new Map(),
|
||||
ownerHierarchyByPerson: new Map(),
|
||||
catalogCosts: [],
|
||||
warehouseConfigs: [],
|
||||
}
|
||||
|
||||
const bothUpdates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-1',
|
||||
barcode: 'BAR-001',
|
||||
spu: 'SPU-001',
|
||||
sku: 'SKU-001',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
image: '',
|
||||
},
|
||||
lookups,
|
||||
)
|
||||
const bothPatch = bothUpdates.find((update) => 'opsHead' in update.patch)
|
||||
expect(bothPatch?.patch).toEqual({ opsHead: '比干' })
|
||||
expect(bothPatch?.source).toEqual(
|
||||
expect.objectContaining({
|
||||
sourceCode: 'DWS_SALES_OPS',
|
||||
sourceObject: 'dws_sn_sales_full_info_di',
|
||||
sourceColumns: { opsHead: 'cater' },
|
||||
}),
|
||||
)
|
||||
|
||||
// Only cater present: opsHead still filled from cater (ider is unused now)
|
||||
const partialUpdates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-2',
|
||||
barcode: 'BAR-002',
|
||||
spu: 'SPU-002',
|
||||
sku: 'SKU-002',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
image: '',
|
||||
},
|
||||
lookups,
|
||||
)
|
||||
const partialPatch = partialUpdates.find((update) => 'opsHead' in update.patch)
|
||||
expect(partialPatch?.patch).toEqual({ opsHead: '香茗' })
|
||||
|
||||
// Barcode without dws ops data: no ops patch at all (keeps archive value)
|
||||
const untouchedUpdates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-3',
|
||||
barcode: 'BAR-009',
|
||||
spu: 'SPU-009',
|
||||
sku: 'SKU-009',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
image: '',
|
||||
},
|
||||
lookups,
|
||||
)
|
||||
expect(untouchedUpdates.some((update) => 'opsDeptHead' in update.patch)).toBe(false)
|
||||
expect(untouchedUpdates.some((update) => 'opsHead' in update.patch)).toBe(false)
|
||||
})
|
||||
|
||||
it('takes dropship (DF) cost from the sales goods_cost source and keeps jst_sku cost for others', () => {
|
||||
const lookups = {
|
||||
clearanceStocks: [],
|
||||
@ -501,8 +383,6 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
dwsGoodsCosts: [{ skuoid: 'DF-A3BJ006601', goodsCost: '70.18' }],
|
||||
skumapPics: [],
|
||||
skumapFillPics: [],
|
||||
dwsOpsOwners: [],
|
||||
spuOpsOwners: [],
|
||||
intakeSpuOwners: new Map(),
|
||||
ownerHierarchyByPerson: new Map(),
|
||||
catalogCosts: [],
|
||||
@ -718,25 +598,9 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
expect.objectContaining({ scanned: 1, changed: 1 }),
|
||||
)
|
||||
|
||||
// 仓库配置拆成两条:负责人照常更新;入库/代发只填空值(产品定厂带回的值不被覆盖)
|
||||
expect(repository.applyPatch).toHaveBeenCalledTimes(12)
|
||||
// dws 运营负责人:cater→opsHead(ider 不再使用)
|
||||
expect(repository.applyPatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
patch: expect.objectContaining({ opsHead: '李逍遥' }),
|
||||
source: expect.objectContaining({ sourceObject: 'dws_sn_sales_full_info_di' }),
|
||||
}),
|
||||
)
|
||||
// 人员信息表层级覆盖 dws 的部门负责人猜测值,并补齐运营总负责人
|
||||
expect(repository.applyPatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
patch: expect.objectContaining({ opsDeptHead: '东邪', opsDirector: '無尘' }),
|
||||
source: expect.objectContaining({
|
||||
sourceObject: 'SupplyChainManagementOwnerConfig',
|
||||
sourceColumns: { opsDeptHead: 'departmentOwner', opsDirector: 'generalOwner' },
|
||||
}),
|
||||
}),
|
||||
)
|
||||
// 仓库配置拆成两条:负责人照常更新;入库/代发只填空值(产品定厂带回的值不被覆盖)。
|
||||
// 运营负责人(opsHead)2026-10 起为一次性定标冻结口径,enrichment 不再产生这两条。
|
||||
expect(repository.applyPatch).toHaveBeenCalledTimes(10)
|
||||
expect(repository.applyPatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
patch: expect.objectContaining({ warehouseHead: '负责人' }),
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import {
|
||||
DWS_SALES_GOODS_COST_ARCHIVE_SOURCE_CODE,
|
||||
DWS_SALES_OPS_ARCHIVE_SOURCE_CODE,
|
||||
JST_CLEARANCE_STOCK_ARCHIVE_SOURCE_CODE,
|
||||
JST_FACTORY_STOCK_ARCHIVE_SOURCE_CODE,
|
||||
JST_PUBLIC_AVAILABLE_STOCK_ARCHIVE_SOURCE_CODE,
|
||||
@ -15,10 +14,7 @@ import {
|
||||
WAREHOUSE_CONFIG_ARCHIVE_SOURCE_CODE,
|
||||
} from '@/lib/supply-chain-product-archive-field-source'
|
||||
import type { ProductArchiveClearanceStockProfile } from '@/server/repositories/supply-chain-product-archive-clearance-stock.repository'
|
||||
import type {
|
||||
DwsSalesGoodsCostProfile,
|
||||
DwsSalesOpsOwnerProfile,
|
||||
} from '@/server/repositories/dws-sales-goods-cost.repository'
|
||||
import type { DwsSalesGoodsCostProfile } from '@/server/repositories/dws-sales-goods-cost.repository'
|
||||
import type { JstSkumapPicProfile } from '@/server/repositories/jst-skumap-pic.repository'
|
||||
import type { JstInventoryPublicAvailableProfile } from '@/server/repositories/jst-inventory-public-available.repository'
|
||||
import type { JstSkuPriceProfile } from '@/server/repositories/jst-sku-price.repository'
|
||||
@ -56,8 +52,6 @@ export type ProductArchiveEnrichmentLookups = {
|
||||
dwsGoodsCosts: readonly DwsSalesGoodsCostProfile[]
|
||||
skumapPics: readonly JstSkumapPicProfile[]
|
||||
skumapFillPics: readonly JstSkumapPicProfile[]
|
||||
dwsOpsOwners: readonly DwsSalesOpsOwnerProfile[]
|
||||
spuOpsOwners: readonly { spu: string; cater: string; ider: string }[]
|
||||
intakeSpuOwners: ReadonlyMap<string, string>
|
||||
ownerHierarchyByPerson: ReadonlyMap<
|
||||
string,
|
||||
@ -185,55 +179,37 @@ export function buildProductArchiveEnrichmentUpdates(
|
||||
},
|
||||
]
|
||||
: []
|
||||
// 运营负责人(opsHead)取数口径(2026-09-22 用户确认):
|
||||
// ① dws_sn_sales_full_info_di.cater——有销售历史的条码取最新销售行的 cater;
|
||||
// 条码无直连行时回退同 SPU 最新销售条码的 cater(SPU 级兜底);
|
||||
// ② 无任何销售数据时回退新品流程(IntakeNonIntegratedProductProfile.spuOwner,SPU 级)。
|
||||
// dws 的 ider 列不再使用;运营部门负责人/总负责人不来自 dws(见下方人员信息表层)。
|
||||
const dwsOpsOwner = lookups.dwsOpsOwners.find(
|
||||
(profile) => normalizeIdentity(profile.skuoid) === normalizeIdentity(target.barcode),
|
||||
)
|
||||
const spuOpsOwner = !dwsOpsOwner?.cater
|
||||
? lookups.spuOpsOwners.find(
|
||||
(profile) => normalizeIdentity(profile.spu) === normalizeIdentity(target.spu),
|
||||
)
|
||||
: undefined
|
||||
const intakeSpuOwner = !dwsOpsOwner?.cater && !spuOpsOwner?.cater
|
||||
? lookups.intakeSpuOwners.get(normalizeIdentity(target.spu))
|
||||
: undefined
|
||||
const effectiveOpsHead = dwsOpsOwner?.cater || spuOpsOwner?.cater || intakeSpuOwner || ''
|
||||
const opsOwnerUpdates: ProductArchiveEnrichmentUpdate[] =
|
||||
effectiveOpsHead
|
||||
? [
|
||||
{
|
||||
patch: { opsHead: effectiveOpsHead },
|
||||
source: intakeSpuOwner
|
||||
? source(
|
||||
INTAKE_PROFILE_ARCHIVE_SOURCE_CODE,
|
||||
PRODUCT_DEVELOPMENT_SOURCE_SYSTEM,
|
||||
PUBLIC_SOURCE_SCHEMA,
|
||||
'IntakeNonIntegratedProductProfile',
|
||||
normalizeIdentity(target.spu),
|
||||
{ opsHead: 'spuOwner' },
|
||||
)
|
||||
: source(
|
||||
DWS_SALES_OPS_ARCHIVE_SOURCE_CODE,
|
||||
JST_SOURCE_SYSTEM,
|
||||
JST_SOURCE_SCHEMA,
|
||||
'dws_sn_sales_full_info_di',
|
||||
dwsOpsOwner?.skuoid || `spu:${normalizeIdentity(target.spu)}`,
|
||||
{ opsHead: 'cater' },
|
||||
),
|
||||
},
|
||||
]
|
||||
: []
|
||||
// 运营负责人(opsHead)2026-10 起改为一次性定标冻结:定标只允许发生在
|
||||
// ① 新品绑定(无销售档案按新品流程 spuOwner 打底,见下方兜底);
|
||||
// ② 一次性定标脚本(按 SPU 累计业绩最多的运营,见 scripts/fix-archive-ops-head-once.ts);
|
||||
// ③ 人工在页面修改。
|
||||
// enrichment 同步不再跟随 dws 最新销售翻单;部门负责人/总负责人仍按人员信息
|
||||
// 匹配档案当前运营负责人,保持层级一致。
|
||||
const storedOpsHead = (target.opsHead ?? '').trim()
|
||||
const intakeSpuOwner = storedOpsHead
|
||||
? undefined
|
||||
: lookups.intakeSpuOwners.get(normalizeIdentity(target.spu))
|
||||
const intakeOpsHeadUpdates: ProductArchiveEnrichmentUpdate[] = intakeSpuOwner
|
||||
? [
|
||||
{
|
||||
patch: { opsHead: intakeSpuOwner },
|
||||
source: source(
|
||||
INTAKE_PROFILE_ARCHIVE_SOURCE_CODE,
|
||||
PRODUCT_DEVELOPMENT_SOURCE_SYSTEM,
|
||||
PUBLIC_SOURCE_SCHEMA,
|
||||
'IntakeNonIntegratedProductProfile',
|
||||
normalizeIdentity(target.spu),
|
||||
{ opsHead: 'spuOwner' },
|
||||
),
|
||||
},
|
||||
]
|
||||
: []
|
||||
// 部门负责人/总负责人层级只来自人员信息表(SupplyChainManagementOwnerConfig):
|
||||
// 按运营负责人花名匹配人员,取其 departmentOwner/generalOwner;未匹配则不动这两个字段。
|
||||
// 匹配基准优先本轮推出的运营负责人(dws 销售/新品流程);推不出时回退档案已存的
|
||||
// 运营负责人——只要人员信息里能匹配到人,层级字段就不允许留空(不动运营负责人本身)。
|
||||
const ownerConfigBasis = effectiveOpsHead || (target.opsHead ?? '').trim()
|
||||
const ownerConfig = ownerConfigBasis
|
||||
? lookups.ownerHierarchyByPerson.get(normalizeIdentity(ownerConfigBasis))
|
||||
// 按档案当前运营负责人花名匹配人员(含本轮新品绑定的新负责人),取其
|
||||
// departmentOwner/generalOwner;未匹配则不动这两个字段。
|
||||
const hierarchyBasis = storedOpsHead || intakeSpuOwner || ''
|
||||
const ownerConfig = hierarchyBasis
|
||||
? lookups.ownerHierarchyByPerson.get(normalizeIdentity(hierarchyBasis))
|
||||
: undefined
|
||||
const ownerConfigUpdates: ProductArchiveEnrichmentUpdate[] =
|
||||
ownerConfig && (ownerConfig.departmentOwner || ownerConfig.generalOwner)
|
||||
@ -248,7 +224,7 @@ export function buildProductArchiveEnrichmentUpdates(
|
||||
CONFIG_SOURCE_SYSTEM,
|
||||
PUBLIC_SOURCE_SCHEMA,
|
||||
'SupplyChainManagementOwnerConfig',
|
||||
ownerConfigBasis,
|
||||
hierarchyBasis,
|
||||
{ opsDeptHead: 'departmentOwner', opsDirector: 'generalOwner' },
|
||||
),
|
||||
},
|
||||
@ -273,7 +249,7 @@ export function buildProductArchiveEnrichmentUpdates(
|
||||
]
|
||||
: []),
|
||||
...skumapFillUpdates,
|
||||
...opsOwnerUpdates,
|
||||
...intakeOpsHeadUpdates,
|
||||
...ownerConfigUpdates,
|
||||
{
|
||||
patch: {
|
||||
|
||||
@ -65,7 +65,7 @@ type EnrichmentDependencies = {
|
||||
>
|
||||
dwsGoodsCostRepository: Pick<
|
||||
DwsSalesGoodsCostRepository,
|
||||
'listDfLatestGoodsCost' | 'listLatestOpsOwners' | 'close'
|
||||
'listDfLatestGoodsCost' | 'close'
|
||||
>
|
||||
skumapPicRepository: Pick<
|
||||
JstSkumapPicRepository,
|
||||
@ -95,45 +95,6 @@ function unique<T>(values: readonly T[]) {
|
||||
return Array.from(new Set(values))
|
||||
}
|
||||
|
||||
export type SpuOpsOwnerProfile = {
|
||||
spu: string
|
||||
cater: string
|
||||
ider: string
|
||||
}
|
||||
|
||||
// Collapse per-barcode dws ops owners into per-spu owners: for each spu keep
|
||||
// the latest-selling non-empty cater and ider (fields may come from different
|
||||
// barcodes). Barcodes without a direct dws row inherit these in the mapper.
|
||||
export function buildSpuOpsOwners(
|
||||
dwsOpsOwners: ReadonlyArray<{ skuoid: string; cater: string; ider: string; lastSaleDate: string }>,
|
||||
spuByBarcode: ReadonlyMap<string, string>,
|
||||
): SpuOpsOwnerProfile[] {
|
||||
const bySpu = new Map<string, { cater: string; caterDate: string; ider: string; iderDate: string }>()
|
||||
for (const owner of dwsOpsOwners) {
|
||||
const skuoid = owner.skuoid?.trim()?.toUpperCase()
|
||||
if (!skuoid) continue
|
||||
const spu = spuByBarcode.get(skuoid)
|
||||
if (!spu) continue
|
||||
const bucket = bySpu.get(spu) || { cater: '', caterDate: '', ider: '', iderDate: '' }
|
||||
const cater = owner.cater?.trim() ?? ''
|
||||
const ider = owner.ider?.trim() ?? ''
|
||||
if (cater && (!bucket.caterDate || owner.lastSaleDate >= bucket.caterDate)) {
|
||||
bucket.cater = cater
|
||||
bucket.caterDate = owner.lastSaleDate
|
||||
}
|
||||
if (ider && (!bucket.iderDate || owner.lastSaleDate >= bucket.iderDate)) {
|
||||
bucket.ider = ider
|
||||
bucket.iderDate = owner.lastSaleDate
|
||||
}
|
||||
bySpu.set(spu, bucket)
|
||||
}
|
||||
return [...bySpu.entries()].map(([spu, bucket]) => ({
|
||||
spu,
|
||||
cater: bucket.cater,
|
||||
ider: bucket.ider,
|
||||
}))
|
||||
}
|
||||
|
||||
export class SupplyChainProductArchiveEnrichmentSyncService {
|
||||
constructor(
|
||||
private readonly dependencies: EnrichmentDependencies = {
|
||||
@ -167,16 +128,9 @@ export class SupplyChainProductArchiveEnrichmentSyncService {
|
||||
const dwsGoodsCosts = await this.dependencies.dwsGoodsCostRepository.listDfLatestGoodsCost()
|
||||
// One shot: latest platform picture for every DF skuoid.
|
||||
const skumapPics = await this.dependencies.skumapPicRepository.listDfLatestSkuPics()
|
||||
// One shot: latest ops owners (cater/ider) for every barcode with sales.
|
||||
const dwsOpsOwners = await this.dependencies.dwsGoodsCostRepository.listLatestOpsOwners()
|
||||
// One shot: spu-level ops owners — barcodes without a direct dws match
|
||||
// inherit the owners of the latest-selling barcode under the same spu
|
||||
// ("只要能匹配到的都放上去" — no empty ops fields when the spu sold).
|
||||
const barcodeSpuPairs = await this.dependencies.repository.listBarcodeSpuPairs()
|
||||
const spuByBarcode = new Map(
|
||||
barcodeSpuPairs.map((pair) => [pair.barcode.trim().toUpperCase(), pair.spu.trim().toUpperCase()]),
|
||||
)
|
||||
const spuOpsOwners = buildSpuOpsOwners(dwsOpsOwners, spuByBarcode)
|
||||
// 运营负责人(opsHead)自 2026-10 起为一次性定标冻结口径,enrichment 不再
|
||||
// 从 dws 销售表取 cater 翻单;这里只加载新品流程负责人(给无销售的档案
|
||||
// 打底绑定)与人员信息层级(按档案当前运营负责人匹配部门/总负责人)。
|
||||
// One shot: 新品流程(非集成产品)每个 SPU 的负责人(spuOwner),给无销售
|
||||
// 数据的档案行兜底运营负责人。
|
||||
const intakeSpuOwners = new Map(
|
||||
@ -186,7 +140,7 @@ export class SupplyChainProductArchiveEnrichmentSyncService {
|
||||
]),
|
||||
)
|
||||
// One shot: 人员信息表的「人员 → 部门负责人/总负责人」层级。运营负责人花名
|
||||
// 匹配到人员后,部门负责人/总负责人以这张表为准(覆盖 dws 的同名猜测值)。
|
||||
// 匹配到人员后,部门负责人/总负责人以这张表为准。
|
||||
// 键与档案侧统一走 normalizeIdentity(含不可见字符清洗),避免同一个人因
|
||||
// 源数据混入零宽字符而永远匹配不上。
|
||||
const ownerHierarchyByPerson = new Map(
|
||||
@ -250,8 +204,6 @@ export class SupplyChainProductArchiveEnrichmentSyncService {
|
||||
dwsGoodsCosts,
|
||||
skumapPics,
|
||||
skumapFillPics,
|
||||
dwsOpsOwners,
|
||||
spuOpsOwners,
|
||||
intakeSpuOwners,
|
||||
ownerHierarchyByPerson,
|
||||
catalogCosts,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user