chore(master-data): 增加聚水潭残留软归档脚本

This commit is contained in:
WuYongJie 2026-06-05 11:38:41 +08:00
parent 4974869f6a
commit 3da51e926b
4 changed files with 1526 additions and 0 deletions

View File

@ -26,6 +26,7 @@
"db:export-product-category-review": "node scripts/export-non-integrated-product-category-review.js",
"db:build-product-category-cleanup-plan": "node scripts/build-non-integrated-product-category-cleanup-plan.js",
"db:match-jst-product-category-cleanup-plan": "node scripts/match-jst-product-category-cleanup-plan.js",
"db:archive-jst-unresolved-products": "node scripts/archive-jst-unresolved-non-integrated-products.js",
"prisma:seed": "tsx prisma/seed.ts",
"open-platform:seed": "tsx scripts/seed-open-platform-demo.ts",
"type-check": "node scripts/run-type-check.js",

File diff suppressed because it is too large Load Diff

View File

@ -831,6 +831,10 @@ const approvedScriptMasterDataWritePaths = new Map([
'scripts/apply-pre-sku-formalization-repair.js',
'准 SKU 生命周期专项修复入口;只允许处理 currentEvidence/evidenceFingerprint 匹配的历史错位,不作为正式入库替代入口。',
],
[
'scripts/archive-jst-unresolved-non-integrated-products.js',
'聚水潭历史残留软归档入口;只允许处理来源匹配报告证明无法唯一补分类且无下游引用的非集成产品,默认 dry-run,--apply 必须带确认、库身份和备份 SHA。',
],
[
'scripts/migrate-historical-assets-to-qiniu.ts',
'七牛历史素材一次性迁移脚本;默认 dry-run,--apply 前必须确认迁移计划、备份和素材替换范围,只允许更新旧素材 URL 引用。',
@ -1103,6 +1107,10 @@ const approvedRawSqlMasterDataWritePaths = new Map([
'scripts/apply-pre-sku-formalization-repair.js',
'准 SKU 生命周期专项修复计划使用原生 SQL 更新 PreSku 状态、ProductSKU.sourcePreSkuId 和 DraftGroup 状态;默认 dry-run,--apply 必须带业务确认、当前证据、库身份和备份 SHA。',
],
[
'scripts/archive-jst-unresolved-non-integrated-products.js',
'聚水潭历史残留软归档计划使用原生 SQL 软删除非集成产品、清理其结构化 SKU/核价行并关闭开放质量决策;默认 dry-run,--apply 必须带业务确认、库身份和备份 SHA。',
],
[
'src/server/services/spu-number-reservation.service.ts',
'SPU 号段服务使用原生 SQL 在同一事务内初始化和推进 SpuNumberCounter,并依赖行锁保证并发安全。',
@ -1141,6 +1149,15 @@ const approvedRawSqlMasterDataWriteTableScopes = new Map(
'ProductSKU',
],
],
[
'scripts/archive-jst-unresolved-non-integrated-products.js',
[
'IntakeNonIntegratedProduct',
'IntakeNonIntegratedPricingRow',
'IntakeNonIntegratedSkuItem',
'MasterDataQualityDecision',
],
],
['src/server/services/spu-number-reservation.service.ts', ['SpuNumberCounter']],
].map(([file, tableNames]) => [file, new Set(tableNames)]),
)
@ -1162,6 +1179,10 @@ const approvedRawSqlMasterDataReadPaths = new Map([
'scripts/match-jst-product-category-cleanup-plan.js',
'聚水潭来源分类匹配脚本只允许只读读取非集成产品/profile/SKU 和正式分类字典,结合 JST 来源库生成可审计 cleanup plan;不得直接写库或关键词猜分类。',
],
[
'scripts/archive-jst-unresolved-non-integrated-products.js',
'聚水潭历史残留软归档脚本允许读取非集成产品/profile/SKU、审批流、质检、草稿组和准 SKU 引用,用于归档前证据校验和 dry-run 对账。',
],
[
'scripts/apply-pre-sku-formalization-repair.js',
'准 SKU 生命周期专项修复入口允许读取 PreSku、ProductSKU、DraftGroup、包装和库位关系,用于 currentEvidence 校验、dry-run 和 apply 前事务内复核。',
@ -1233,6 +1254,20 @@ const approvedRawSqlMasterDataReadTableScopes = new Map([
'SpuNumberCounter',
]),
],
[
'scripts/archive-jst-unresolved-non-integrated-products.js',
new Set([
'ApprovalWorkflow',
'DraftGroup',
'IntakeNonIntegratedPricingRow',
'IntakeNonIntegratedProduct',
'IntakeNonIntegratedProductProfile',
'IntakeNonIntegratedSkuItem',
'MasterDataQualityDecision',
'PreSku',
'ProductSkuQualityInspection',
]),
],
[
'scripts/apply-pre-sku-formalization-repair.js',
new Set([

View File

@ -0,0 +1,281 @@
const {
APPLY_CONFIRMATION,
ARCHIVE_PLAN_MODE,
addReference,
assertArchiveApplyReady,
buildArchivePlanFromRows,
buildExactReferenceTokens,
buildFuzzyReferenceTokens,
classifyMatch,
emptyReferenceSummary,
isJstArchiveIdentity,
isJstSourceValue,
normalizeProductRow,
parseArgs,
validateArchivePlanShape,
} = require('../../scripts/archive-jst-unresolved-non-integrated-products')
function product(overrides = {}) {
return normalizeProductRow({
productId: 'jst-stock-product-1',
spu: 'SPU001',
sourceSpu: '1001',
productName: '聚水潭存量桌',
productCategory: '',
productDevelopmentKind: 'product',
sourceCategory: '',
sourceItemType: '成品',
profileSource: 'PAYLOAD_SYNC',
skuItemCount: 1,
pricingRowCount: 1,
skuRows: [
{
id: 'sku-row-1',
skuItemId: 'sku-item-1',
sourceSkuId: 'BAR001',
skuCode: 'SKU001',
barcode: 'BAR001',
source: 'jst_sku',
sourceTable: 'jst_sku',
},
],
...overrides,
})
}
function classification({
unique = [],
conflicting = [],
unresolved = [],
} = {}) {
return {
uniqueResolvedProductIds: new Set(unique),
conflictingProductIds: new Set(conflicting),
unresolvedProductIds: new Set(unresolved),
missingFromReportProductIds: new Set(),
}
}
describe('archive-jst-unresolved-non-integrated-products', () => {
it('parses plan generation, dry-run, and guarded apply arguments', () => {
expect(parseArgs([
'--match-report',
'report.json',
'--category-cleanup-plan=cleanup-plan.json',
'--output',
'archive-plan.json',
'--confirmed-by',
'吴勇杰',
'--limit',
'20',
'--summary-stdout',
])).toEqual({
apply: false,
planPath: '',
outputPath: 'archive-plan.json',
matchReportPath: 'report.json',
categoryCleanupPlanPath: 'cleanup-plan.json',
confirmedBy: '吴勇杰',
confirm: '',
expectedDatabaseIdentity: '',
expectedBackupSha256: '',
limit: 20,
summaryStdout: true,
})
expect(parseArgs([
'--apply',
'--plan=archive-plan.json',
'--confirm',
APPLY_CONFIRMATION,
'--expected-database-identity',
'postgresql|furniture|localhost|5432|furniture3d|',
'--expected-backup-sha256',
'A'.repeat(64),
])).toEqual(expect.objectContaining({
apply: true,
planPath: 'archive-plan.json',
confirm: APPLY_CONFIRMATION,
expectedBackupSha256: 'a'.repeat(64),
}))
expect(() => parseArgs([])).toThrow('--match-report')
expect(() => parseArgs(['--confirm', APPLY_CONFIRMATION, '--match-report', 'report.json']))
.toThrow('--apply')
expect(() => parseArgs(['--limit=0', '--match-report=report.json'])).toThrow('正整数')
})
it('recognizes JST source identity from stable id, profile source, and structured SKU rows', () => {
expect(isJstSourceValue('jst-sku')).toBe(true)
expect(isJstSourceValue('JST_SKU')).toBe(true)
expect(isJstArchiveIdentity(product({ productId: 'jst-stock-abc', sourceSpu: '' }))).toBe(true)
expect(isJstArchiveIdentity(product({
productId: 'product-1',
sourceSpu: '',
profileSource: 'jst_sku',
skuRows: [],
}))).toBe(true)
expect(isJstArchiveIdentity(product({
productId: 'product-1',
sourceSpu: '',
profileSource: '',
skuRows: [{ source: '', sourceTable: 'jst_sku' }],
}))).toBe(true)
expect(isJstArchiveIdentity(product({
productId: 'product-1',
sourceSpu: '',
profileSource: '',
skuRows: [],
}))).toBe(false)
})
it('archives unresolved JST residuals but excludes uniquely resolved category matches', () => {
const unresolved = product({ productId: 'jst-stock-unresolved', spu: 'SPU001' })
const unique = product({ productId: 'jst-stock-unique', spu: 'SPU002' })
const matches = classification({
unresolved: ['jst-stock-unresolved'],
unique: ['jst-stock-unique'],
})
const plan = buildArchivePlanFromRows({
products: [unresolved, unique],
matchClassification: matches,
databaseIdentity: 'postgresql|furniture|localhost|5432|furniture3d|',
confirmedBy: '吴勇杰',
generatedAt: '2026-06-05T00:00:00.000Z',
})
expect(plan).toEqual(expect.objectContaining({
mode: ARCHIVE_PLAN_MODE,
databaseIdentity: 'postgresql|furniture|localhost|5432|furniture3d|',
confirmation: APPLY_CONFIRMATION,
confirmedBy: '吴勇杰',
summary: expect.objectContaining({
scannedProducts: 2,
archiveCandidateCount: 1,
excludedResolvedProductCount: 1,
}),
}))
expect(plan.archiveCandidates[0]).toEqual(expect.objectContaining({
productId: 'jst-stock-unresolved',
matchClassification: 'unresolved-source-category',
current: expect.objectContaining({
productCategory: '',
missingFields: ['productCategory'],
}),
referenceSummary: expect.objectContaining({ total: 0 }),
}))
expect(plan.excludedResolvedProducts[0]).toEqual(expect.objectContaining({
productId: 'jst-stock-unique',
}))
expect(validateArchivePlanShape(plan)).toEqual({
candidateCount: 1,
blockedProductCount: 0,
excludedResolvedProductCount: 1,
})
})
it('keeps conflicting products archiveable only when no downstream references exist', () => {
const conflicted = product({ productId: 'jst-stock-conflict' })
const referenceSummary = emptyReferenceSummary()
addReference(new Map([['jst-stock-conflict', referenceSummary]]), 'jst-stock-conflict', {
type: 'quality-inspection',
count: 1,
samples: ['QI-001'],
})
const referenceSummaries = new Map([['jst-stock-conflict', referenceSummary]])
const matches = classification({ conflicting: ['jst-stock-conflict'] })
const plan = buildArchivePlanFromRows({
products: [conflicted],
matchClassification: matches,
referenceSummaries,
confirmedBy: '吴勇杰',
})
expect(plan.summary).toEqual(expect.objectContaining({
archiveCandidateCount: 0,
blockedProductCount: 1,
}))
expect(plan.blockedProducts[0]).toEqual(expect.objectContaining({
productId: 'jst-stock-conflict',
blockReason: '仍被后续业务链路引用,不能自动归档。',
matchClassification: 'conflicting-source-category',
referenceSummary: expect.objectContaining({
total: 1,
qualityInspectionCount: 1,
}),
}))
})
it('blocks products missing from the JST match report instead of guessing', () => {
const matches = classification({ unresolved: [] })
const plan = buildArchivePlanFromRows({
products: [product({ productId: 'jst-stock-missing-report' })],
matchClassification: matches,
confirmedBy: '吴勇杰',
})
expect(classifyMatch('jst-stock-missing-report', matches)).toBe('missing-from-match-report')
expect(plan.summary).toEqual(expect.objectContaining({
archiveCandidateCount: 0,
blockedProductCount: 1,
missingFromReportCount: 1,
}))
expect(plan.blockedProducts[0].blockReason).toContain('匹配报告')
})
it('rejects apply mode without explicit confirmation and audit actor', () => {
const plan = buildArchivePlanFromRows({
products: [product({ productId: 'jst-stock-unresolved' })],
matchClassification: classification({ unresolved: ['jst-stock-unresolved'] }),
confirmedBy: '吴勇杰',
})
expect(() => assertArchiveApplyReady({ apply: true, confirm: '' }, plan)).toThrow('--confirm')
expect(() => assertArchiveApplyReady(
{ apply: true, confirm: APPLY_CONFIRMATION },
{ ...plan, confirmation: '' },
)).toThrow('confirmation')
expect(() => assertArchiveApplyReady(
{ apply: true, confirm: APPLY_CONFIRMATION },
{ ...plan, confirmedBy: 'UNKNOWN' },
)).toThrow('占位值或默认值')
expect(() => assertArchiveApplyReady(
{ apply: true, confirm: APPLY_CONFIRMATION },
plan,
)).not.toThrow()
})
it('builds exact and fuzzy reference tokens from product and SKU identity', () => {
const row = product({
productId: 'jst-stock-product-abcdef123456',
spu: 'SPU001',
sourceSpu: '1001',
skuRows: [
{
id: 'sku-row-abcdef123456',
skuItemId: 'sku-item-1',
sourceSkuId: 'BAR001',
skuCode: 'SKU001',
barcode: 'BAR001',
source: 'jst_sku',
sourceTable: 'jst_sku',
},
],
})
expect(buildExactReferenceTokens(row)).toEqual(expect.arrayContaining([
'jst-stock-product-abcdef123456',
'SPU001',
'1001',
'sku-row-abcdef123456',
'sku-item-1',
'BAR001',
'SKU001',
]))
expect(buildFuzzyReferenceTokens(row)).toEqual(expect.arrayContaining([
'jst-stock-product-abcdef123456',
'sku-row-abcdef123456',
]))
expect(buildFuzzyReferenceTokens(row)).not.toContain('SPU001')
})
})