fix(bom-costing): guard fact snapshot fingerprints
This commit is contained in:
parent
bdc8f393a8
commit
a51ec8a162
@ -6,6 +6,9 @@ jest.mock('@/lib/prisma', () => ({
|
||||
bomCostingCase: {
|
||||
updateMany: jest.fn(),
|
||||
},
|
||||
bomCostingSourceFactAnalysis: {
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@ -18,6 +21,8 @@ import { BomCostingRepository } from './bom-costing.repository'
|
||||
|
||||
const transaction = prisma.$transaction as jest.Mock
|
||||
const updateManyCase = prisma.bomCostingCase.updateMany as jest.Mock
|
||||
const findFirstSourceFactAnalysis =
|
||||
prisma.bomCostingSourceFactAnalysis.findFirst as jest.Mock
|
||||
|
||||
describe('BomCostingRepository dynamic-cost state transitions', () => {
|
||||
beforeEach(() => {
|
||||
@ -378,3 +383,70 @@ describe('BomCostingRepository dynamic-cost state transitions', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('BomCostingRepository reusable fact snapshot selection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('never searches a different asset fingerprint for reusable facts', async () => {
|
||||
findFirstSourceFactAnalysis.mockResolvedValue(null)
|
||||
|
||||
await new BomCostingRepository().findLatestReadySourceFactAnalysis({
|
||||
sourceSnapshotId: 'source-1',
|
||||
schemaVersion: 'competitor-facts/v1',
|
||||
assetFingerprint: 'current-assets',
|
||||
})
|
||||
|
||||
expect(findFirstSourceFactAnalysis).toHaveBeenCalledWith({
|
||||
where: {
|
||||
sourceSnapshotId: 'source-1',
|
||||
schemaVersion: 'competitor-facts/v1',
|
||||
assetFingerprint: 'current-assets',
|
||||
status: 'READY',
|
||||
},
|
||||
orderBy: [
|
||||
{ analyzedImageCount: 'desc' },
|
||||
{ revision: 'desc' },
|
||||
{ updatedAt: 'desc' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('treats analyzed images with empty facts as successful coverage', async () => {
|
||||
const readySnapshot = {
|
||||
id: 'facts-empty-but-successful',
|
||||
analyzedImageCount: 77,
|
||||
revision: 1,
|
||||
factSnapshot: {
|
||||
schemaVersion: 'competitor-facts/v1',
|
||||
imageFacts: [{
|
||||
imageId: 'IMG_0001',
|
||||
sourceImageUrl: 'https://example.com/image.jpg',
|
||||
kind: 'DETAIL',
|
||||
label: '详情图 1',
|
||||
externalSkuIds: [],
|
||||
observedTexts: [],
|
||||
facts: [],
|
||||
}],
|
||||
failedImages: [],
|
||||
extractedAt: '2026-08-12T00:00:00.000Z',
|
||||
},
|
||||
}
|
||||
findFirstSourceFactAnalysis.mockResolvedValue(readySnapshot)
|
||||
|
||||
const selected = await new BomCostingRepository()
|
||||
.findLatestReadySourceFactAnalysis({
|
||||
sourceSnapshotId: 'source-1',
|
||||
schemaVersion: 'competitor-facts/v1',
|
||||
assetFingerprint: 'current-assets',
|
||||
})
|
||||
|
||||
expect(selected).toBe(readySnapshot)
|
||||
expect(findFirstSourceFactAnalysis.mock.calls[0]?.[0]?.orderBy).toEqual([
|
||||
{ analyzedImageCount: 'desc' },
|
||||
{ revision: 'desc' },
|
||||
{ updatedAt: 'desc' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -624,7 +624,14 @@ export class BomCostingRepository {
|
||||
assetFingerprint: input.assetFingerprint,
|
||||
status: BOM_COSTING_FACT_ANALYSIS_STATUS.READY,
|
||||
},
|
||||
orderBy: { revision: 'desc' },
|
||||
orderBy: [
|
||||
// READY may still carry a retry ledger. Prefer the revision that
|
||||
// actually retained the most successful image records. An image with
|
||||
// no extracted facts is still a successful analysis result.
|
||||
{ analyzedImageCount: 'desc' },
|
||||
{ revision: 'desc' },
|
||||
{ updatedAt: 'desc' },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,8 @@ const mockBuildStoredSnapshot = jest.fn()
|
||||
const mockRefreshSourceMetadata = jest.fn()
|
||||
const mockArchive = jest.fn()
|
||||
const mockSourceImagesChangedError = class extends Error {}
|
||||
const SOURCE_ASSET_FINGERPRINT =
|
||||
'3f2d926e8473d40c4778839482560ac9869f0c6dcb4277b6844fcf1a941cb3f3'
|
||||
|
||||
jest.mock('@/lib/storage', () => ({
|
||||
readObjectStorageKeyFromUrl: (value: string) => {
|
||||
@ -307,7 +309,7 @@ function factAnalysisRecord(
|
||||
origin: 'INITIAL',
|
||||
baseAnalysisId: null,
|
||||
selectedAssetIds: [],
|
||||
assetFingerprint: 'asset-fingerprint',
|
||||
assetFingerprint: SOURCE_ASSET_FINGERPRINT,
|
||||
status,
|
||||
model: 'grok-4.5',
|
||||
factSnapshot:
|
||||
@ -483,6 +485,110 @@ describe('BomCostingProcessingService', () => {
|
||||
expect(bomCostingRepository.failCase).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('成本阶段恢复也不能绕过当前图片 fingerprint 门禁', async () => {
|
||||
jest.spyOn(bomCostingRepository, 'findProcessingAnalysisRun')
|
||||
.mockResolvedValue({
|
||||
id: 'analysis-run-stale-cost-recovery',
|
||||
caseId: 'case-1',
|
||||
revision: 2,
|
||||
mode: BomCostingAnalysisRunMode.REUSE_FACTS,
|
||||
status: BomCostingAnalysisRunStatus.PROCESSING,
|
||||
factAnalysisId: 'fact-analysis-old-images',
|
||||
selectedAssetIds: [],
|
||||
instruction: '',
|
||||
resultSnapshot: supportedV3AnalysisResult,
|
||||
} as never)
|
||||
jest.spyOn(bomCostingRepository, 'findCaseForProcessing')
|
||||
.mockResolvedValue({
|
||||
...caseRecord(),
|
||||
progress: 97,
|
||||
statusMessage: '已恢复后台任务,继续生成分层成本',
|
||||
skus: [{ id: 'sku-db-1', externalSkuId: 'sku-1' }],
|
||||
} as never)
|
||||
jest.spyOn(bomCostingRepository, 'findSourceFactAnalysis')
|
||||
.mockResolvedValue({
|
||||
...factAnalysisRecord(BOM_COSTING_FACT_ANALYSIS_STATUS.READY),
|
||||
id: 'fact-analysis-old-images',
|
||||
assetFingerprint: 'old-source-asset-fingerprint',
|
||||
} as never)
|
||||
jest.spyOn(bomCostingRepository, 'findLatestReadySourceFactAnalysis')
|
||||
.mockResolvedValue(null)
|
||||
const { BomCostingProcessingService } = await import(
|
||||
'./bom-costing-processing.service'
|
||||
)
|
||||
|
||||
await new BomCostingProcessingService().processCase('case-1')
|
||||
|
||||
expect(bomCostingScenarioService.calculateSameMaterialForCase)
|
||||
.not.toHaveBeenCalled()
|
||||
expect(bomCostingRepository.updateCaseStage).not.toHaveBeenCalledWith(
|
||||
'case-1',
|
||||
expect.objectContaining({ eventType: 'DYNAMIC_COST_RECOVERY_RESUMED' }),
|
||||
)
|
||||
expect(bomCostingRepository.failCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
analysisRunId: 'analysis-run-stale-cost-recovery',
|
||||
message: expect.stringContaining(
|
||||
'当前归档图片版本没有可复用的图片事实',
|
||||
),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('市场图片暂时归档为零张时也不能用旧 fingerprint 提前恢复成本', async () => {
|
||||
jest.spyOn(bomCostingRepository, 'findProcessingAnalysisRun')
|
||||
.mockResolvedValue({
|
||||
id: 'analysis-run-empty-rearchive',
|
||||
caseId: 'case-1',
|
||||
revision: 2,
|
||||
mode: BomCostingAnalysisRunMode.REUSE_FACTS,
|
||||
status: BomCostingAnalysisRunStatus.PROCESSING,
|
||||
factAnalysisId: 'fact-analysis-old-images',
|
||||
selectedAssetIds: [],
|
||||
instruction: '',
|
||||
resultSnapshot: supportedV3AnalysisResult,
|
||||
} as never)
|
||||
jest.spyOn(bomCostingRepository, 'findCaseForProcessing')
|
||||
.mockResolvedValue({
|
||||
...caseRecord(),
|
||||
progress: 97,
|
||||
statusMessage: '已恢复后台任务,继续生成分层成本',
|
||||
sourceSnapshot: {
|
||||
...caseRecord().sourceSnapshot,
|
||||
storedImageCount: 0,
|
||||
},
|
||||
skus: [{ id: 'sku-db-1', externalSkuId: 'sku-1' }],
|
||||
} as never)
|
||||
jest.spyOn(bomCostingRepository, 'findSourceFactAnalysis')
|
||||
.mockResolvedValue({
|
||||
...factAnalysisRecord(BOM_COSTING_FACT_ANALYSIS_STATUS.READY),
|
||||
id: 'fact-analysis-old-images',
|
||||
assetFingerprint: 'old-source-asset-fingerprint',
|
||||
} as never)
|
||||
jest.spyOn(bomCostingRepository, 'findLatestReadySourceFactAnalysis')
|
||||
.mockResolvedValue(null)
|
||||
const { BomCostingProcessingService } = await import(
|
||||
'./bom-costing-processing.service'
|
||||
)
|
||||
|
||||
await new BomCostingProcessingService().processCase('case-1')
|
||||
|
||||
expect(bomCostingScenarioService.calculateSameMaterialForCase)
|
||||
.not.toHaveBeenCalled()
|
||||
expect(bomCostingRepository.updateCaseStage).not.toHaveBeenCalledWith(
|
||||
'case-1',
|
||||
expect.objectContaining({ eventType: 'DYNAMIC_COST_RECOVERY_RESUMED' }),
|
||||
)
|
||||
expect(bomCostingRepository.failCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
analysisRunId: 'analysis-run-empty-rearchive',
|
||||
message: expect.stringContaining(
|
||||
'当前归档图片版本没有可复用的图片事实',
|
||||
),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('所有 SKU 动态成本均失败时不伪装已完成,保留可重新核算状态', async () => {
|
||||
jest.spyOn(bomCostingRepository, 'findProcessingAnalysisRun')
|
||||
.mockResolvedValue({
|
||||
@ -1705,6 +1811,192 @@ describe('BomCostingProcessingService', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: '复用事实且 schemaVersion 已变化',
|
||||
mode: BomCostingAnalysisRunMode.REUSE_FACTS,
|
||||
selectedAssetIds: [] as string[],
|
||||
linkedFactOverrides: {
|
||||
sourceSnapshotId: 'source-1',
|
||||
schemaVersion: 'competitor-analysis/facts-v2',
|
||||
assetFingerprint: SOURCE_ASSET_FINGERPRINT,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '补充图片分析且 sourceSnapshotId 已变化',
|
||||
mode: BomCostingAnalysisRunMode.REANALYZE_SELECTED,
|
||||
selectedAssetIds: ['asset-sku'],
|
||||
linkedFactOverrides: {
|
||||
sourceSnapshotId: 'source-old',
|
||||
schemaVersion: BOM_COSTING_FACT_SCHEMA_VERSION,
|
||||
assetFingerprint: SOURCE_ASSET_FINGERPRINT,
|
||||
},
|
||||
},
|
||||
])(
|
||||
'$label 恢复时拒绝三元组不一致的已链接事实,且只查询当前 fingerprint',
|
||||
async ({ mode, selectedAssetIds, linkedFactOverrides }) => {
|
||||
jest.spyOn(bomCostingRepository, 'findProcessingAnalysisRun')
|
||||
.mockResolvedValue({
|
||||
id: `analysis-run-stale-${mode}`,
|
||||
caseId: 'case-1',
|
||||
revision: 4,
|
||||
mode,
|
||||
status: BomCostingAnalysisRunStatus.PROCESSING,
|
||||
factAnalysisId: 'fact-analysis-old-images',
|
||||
selectedAssetIds,
|
||||
instruction: '',
|
||||
} as never)
|
||||
jest.spyOn(bomCostingRepository, 'findSourceFactAnalysis')
|
||||
.mockResolvedValue({
|
||||
...factAnalysisRecord(BOM_COSTING_FACT_ANALYSIS_STATUS.READY),
|
||||
id: 'fact-analysis-old-images',
|
||||
...linkedFactOverrides,
|
||||
} as never)
|
||||
const currentReadySpy = jest
|
||||
.spyOn(bomCostingRepository, 'findLatestReadySourceFactAnalysis')
|
||||
.mockResolvedValue(null)
|
||||
const { BomCostingProcessingService } = await import(
|
||||
'./bom-costing-processing.service'
|
||||
)
|
||||
|
||||
await new BomCostingProcessingService().processCase('case-1')
|
||||
|
||||
expect(currentReadySpy).toHaveBeenCalledWith({
|
||||
sourceSnapshotId: 'source-1',
|
||||
schemaVersion: BOM_COSTING_FACT_SCHEMA_VERSION,
|
||||
assetFingerprint: SOURCE_ASSET_FINGERPRINT,
|
||||
})
|
||||
expect(mockRunAnalysis).not.toHaveBeenCalled()
|
||||
expect(bomCostingRepository.linkAnalysisRunFact).not.toHaveBeenCalled()
|
||||
expect(bomCostingRepository.failCase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
analysisRunId: `analysis-run-stale-${mode}`,
|
||||
message: expect.stringContaining(
|
||||
'当前归档图片版本没有可复用的图片事实',
|
||||
),
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
it('全部重新分析恢复时图片变化会忽略旧链接并创建当前 fingerprint 的全量修订', async () => {
|
||||
jest.spyOn(bomCostingRepository, 'findProcessingAnalysisRun')
|
||||
.mockResolvedValue({
|
||||
id: 'analysis-run-all-changed-images',
|
||||
caseId: 'case-1',
|
||||
revision: 5,
|
||||
mode: BomCostingAnalysisRunMode.REANALYZE_ALL,
|
||||
status: BomCostingAnalysisRunStatus.PROCESSING,
|
||||
factAnalysisId: 'fact-analysis-old-images',
|
||||
selectedAssetIds: [],
|
||||
instruction: '重新核对全部图片',
|
||||
} as never)
|
||||
jest.spyOn(bomCostingRepository, 'findSourceFactAnalysis')
|
||||
.mockResolvedValue({
|
||||
...factAnalysisRecord(BOM_COSTING_FACT_ANALYSIS_STATUS.READY),
|
||||
id: 'fact-analysis-old-images',
|
||||
assetFingerprint: 'old-source-asset-fingerprint',
|
||||
} as never)
|
||||
jest
|
||||
.spyOn(bomCostingRepository, 'claimSourceSnapshotMetadataRefresh')
|
||||
.mockResolvedValue(true)
|
||||
jest.spyOn(bomCostingRepository, 'updateCaseTitle')
|
||||
.mockResolvedValue({} as never)
|
||||
jest.spyOn(marketDetailClient, 'readProduct')
|
||||
.mockResolvedValue(snapshot)
|
||||
mockRefreshSourceMetadata.mockResolvedValue(sourceSnapshot)
|
||||
const currentReadySpy = jest
|
||||
.spyOn(bomCostingRepository, 'findLatestReadySourceFactAnalysis')
|
||||
.mockResolvedValue(null)
|
||||
const createRevisionSpy = jest
|
||||
.spyOn(bomCostingRepository, 'createSourceFactAnalysisRevision')
|
||||
.mockImplementation(async (input) => ({
|
||||
...factAnalysisRecord(),
|
||||
id: 'fact-analysis-current-images',
|
||||
revision: 6,
|
||||
origin: 'FULL_REANALYSIS',
|
||||
baseAnalysisId: null,
|
||||
assetFingerprint: input.assetFingerprint,
|
||||
factSnapshot: input.factSnapshot,
|
||||
analyzedImageCount: 0,
|
||||
}) as never)
|
||||
mockRunAnalysis.mockImplementation(async (input) => {
|
||||
const analysisInput = input as {
|
||||
existingFactSnapshot?: BomCostingSourceFactSnapshot | null
|
||||
reuseExistingFactsOnly?: boolean
|
||||
imageIdentitiesToAnalyze?: string[]
|
||||
onFactExtractionCompleted?: (result: unknown) => Promise<void>
|
||||
}
|
||||
expect(analysisInput.existingFactSnapshot?.imageFacts).toEqual([])
|
||||
expect(analysisInput.reuseExistingFactsOnly).toBe(false)
|
||||
expect(new Set(analysisInput.imageIdentitiesToAnalyze)).toEqual(new Set([
|
||||
'https://storage.example.com/stored-main',
|
||||
'https://storage.example.com/stored-sku',
|
||||
]))
|
||||
await analysisInput.onFactExtractionCompleted?.({
|
||||
snapshot: factSnapshot,
|
||||
trace: {
|
||||
model: 'grok-4.5',
|
||||
callIds: ['fact-call-current-images'],
|
||||
batchCount: 1,
|
||||
reusedImageCount: 0,
|
||||
analyzedImageCount: 2,
|
||||
failedImageCount: 0,
|
||||
durationMs: 10_000,
|
||||
promptTokens: 40,
|
||||
completionTokens: 60,
|
||||
},
|
||||
})
|
||||
return {
|
||||
result: analysisResult,
|
||||
trace: {
|
||||
model: 'grok-4.5',
|
||||
callId: 'synthesis-call-current-images',
|
||||
imageCount: 2,
|
||||
inputSkuCount: 1,
|
||||
factBatchCount: 1,
|
||||
reusedImageCount: 0,
|
||||
analyzedImageCount: 2,
|
||||
durationMs: 20_000,
|
||||
promptTokens: 100,
|
||||
completionTokens: 150,
|
||||
},
|
||||
}
|
||||
})
|
||||
const { BomCostingProcessingService } = await import(
|
||||
'./bom-costing-processing.service'
|
||||
)
|
||||
|
||||
await new BomCostingProcessingService().processCase('case-1')
|
||||
|
||||
expect(currentReadySpy).toHaveBeenCalledWith({
|
||||
sourceSnapshotId: 'source-1',
|
||||
schemaVersion: BOM_COSTING_FACT_SCHEMA_VERSION,
|
||||
assetFingerprint: SOURCE_ASSET_FINGERPRINT,
|
||||
})
|
||||
expect(createRevisionSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceSnapshotId: 'source-1',
|
||||
assetFingerprint: SOURCE_ASSET_FINGERPRINT,
|
||||
origin: 'FULL_REANALYSIS',
|
||||
baseAnalysisId: null,
|
||||
analyzedImageCount: 0,
|
||||
factSnapshot: expect.objectContaining({
|
||||
imageFacts: [],
|
||||
failedImages: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: 'MAIN', reason: 'NOT_ANALYZED' }),
|
||||
expect.objectContaining({ kind: 'SKU', reason: 'NOT_ANALYZED' }),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(bomCostingRepository.linkAnalysisRunFact).toHaveBeenCalledWith(
|
||||
'analysis-run-all-changed-images',
|
||||
'fact-analysis-current-images',
|
||||
)
|
||||
expect(bomCostingRepository.failCase).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('明确复用事实时即使仍有失败图片也不创建新修订或触发图片重试', async () => {
|
||||
const readyWithFailure: BomCostingSourceFactSnapshot = {
|
||||
...factSnapshot,
|
||||
|
||||
@ -137,6 +137,26 @@ function buildSourceAssetFingerprint(
|
||||
return createHash('sha256').update(input).digest('hex')
|
||||
}
|
||||
|
||||
function isFactAnalysisForCurrentSource(
|
||||
factAnalysis: {
|
||||
sourceSnapshotId: string
|
||||
schemaVersion: string
|
||||
assetFingerprint: string
|
||||
} | null,
|
||||
input: {
|
||||
sourceSnapshotId: string
|
||||
schemaVersion: string
|
||||
assetFingerprint: string
|
||||
},
|
||||
) {
|
||||
return Boolean(
|
||||
factAnalysis &&
|
||||
factAnalysis.sourceSnapshotId === input.sourceSnapshotId &&
|
||||
factAnalysis.schemaVersion === input.schemaVersion &&
|
||||
factAnalysis.assetFingerprint === input.assetFingerprint,
|
||||
)
|
||||
}
|
||||
|
||||
function parseSourceFactSnapshot(value: Prisma.JsonValue | null) {
|
||||
if (value === null) return null
|
||||
return bomCostingSourceFactSnapshotSchema.parse(value)
|
||||
@ -579,6 +599,20 @@ export class BomCostingProcessingService {
|
||||
if (!record) throw new Error('BOM 核算案例不存在')
|
||||
sourceSnapshotId = record.sourceSnapshotId
|
||||
|
||||
const linkedFactAnalysis = analysisRun.factAnalysisId
|
||||
? await bomCostingRepository.findSourceFactAnalysis(
|
||||
analysisRun.factAnalysisId,
|
||||
)
|
||||
: null
|
||||
const linkedFactMatchesCurrentArchivedSource = Boolean(
|
||||
record.sourceSnapshot &&
|
||||
isFactAnalysisForCurrentSource(linkedFactAnalysis, {
|
||||
sourceSnapshotId: record.sourceSnapshot.id,
|
||||
schemaVersion: BOM_COSTING_FACT_SCHEMA_VERSION,
|
||||
assetFingerprint: buildSourceAssetFingerprint(record.sourceSnapshot),
|
||||
}),
|
||||
)
|
||||
|
||||
const persistedAnalysis = competitorAnalysisResultSchema.safeParse(
|
||||
analysisRun.resultSnapshot,
|
||||
)
|
||||
@ -593,7 +627,11 @@ export class BomCostingProcessingService {
|
||||
persistedAnalysis.data.structureBlueprint.supportStatus === 'SUPPORTED' &&
|
||||
savedSkuIds.length > 0 &&
|
||||
savedSkuIds.length === persistedSkuIds.length &&
|
||||
savedSkuIds.every((skuId, index) => skuId === persistedSkuIds[index])
|
||||
savedSkuIds.every((skuId, index) => skuId === persistedSkuIds[index]) &&
|
||||
(
|
||||
!record.sourceSnapshot ||
|
||||
linkedFactMatchesCurrentArchivedSource
|
||||
)
|
||||
if (canResumeDynamicCosting) {
|
||||
await bomCostingRepository.updateCaseStage(caseId, {
|
||||
eventType: 'DYNAMIC_COST_RECOVERY_RESUMED',
|
||||
@ -898,10 +936,15 @@ export class BomCostingProcessingService {
|
||||
analysisInputStats.imageCount > 0
|
||||
) {
|
||||
const assetFingerprint = buildSourceAssetFingerprint(archivedSource)
|
||||
let factAnalysis = analysisRun.factAnalysisId
|
||||
? await bomCostingRepository.findSourceFactAnalysis(
|
||||
analysisRun.factAnalysisId,
|
||||
)
|
||||
let factAnalysis = isFactAnalysisForCurrentSource(
|
||||
linkedFactAnalysis,
|
||||
{
|
||||
sourceSnapshotId: archivedSource.id,
|
||||
schemaVersion: BOM_COSTING_FACT_SCHEMA_VERSION,
|
||||
assetFingerprint,
|
||||
},
|
||||
)
|
||||
? linkedFactAnalysis
|
||||
: null
|
||||
if (!factAnalysis) {
|
||||
if (analysisRun.mode === BomCostingAnalysisRunMode.INITIAL) {
|
||||
@ -926,7 +969,7 @@ export class BomCostingProcessingService {
|
||||
!baseFactAnalysis
|
||||
) {
|
||||
throw new Error(
|
||||
'当前竞品没有可复用的完整图片事实,请选择全部重新分析',
|
||||
'当前归档图片版本没有可复用的图片事实;图片或归档版本可能已变化,请选择“全部重新分析”',
|
||||
)
|
||||
}
|
||||
if (analysisRun.mode === BomCostingAnalysisRunMode.REUSE_FACTS) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user