From 3d18a89e5531258f5df14413398fd0775c6a0e42 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Wed, 5 Aug 2026 11:42:28 +0800 Subject: [PATCH] fix(bom): guard historical and invalid costing results --- .../bom-costing/BomCostingAssetWorkspace.tsx | 8 +- .../BomCostingWorkbenchClient.test.tsx | 4 + .../bom-costing/BomCostingWorkbenchClient.tsx | 127 +++++++-- .../src/lib/bom-costing/client-types.ts | 7 + .../src/lib/bom-costing/contracts.ts | 5 + .../repositories/bom-costing.repository.ts | 14 + .../bom-costing-processing.service.ts | 46 +--- .../bom-costing-result-quality.test.ts | 159 +++++++++++ .../bom-costing/bom-costing-result-quality.ts | 255 ++++++++++++++++++ .../bom-costing/bom-costing.service.test.ts | 9 +- .../bom-costing/bom-costing.service.ts | 133 ++++++--- 11 files changed, 673 insertions(+), 94 deletions(-) create mode 100644 furniture-3d-system/src/server/services/bom-costing/bom-costing-result-quality.test.ts create mode 100644 furniture-3d-system/src/server/services/bom-costing/bom-costing-result-quality.ts diff --git a/furniture-3d-system/src/app/bom-costing/BomCostingAssetWorkspace.tsx b/furniture-3d-system/src/app/bom-costing/BomCostingAssetWorkspace.tsx index 677da514..74486283 100644 --- a/furniture-3d-system/src/app/bom-costing/BomCostingAssetWorkspace.tsx +++ b/furniture-3d-system/src/app/bom-costing/BomCostingAssetWorkspace.tsx @@ -662,6 +662,7 @@ export function BomCostingAnalysisProgressPanel({ ) ) const stageLabel = PROCESS_PHASE_LABELS[activity.phase] + const processingStopped = detail.status === 'FAILED' const displayMessage = detail.status === 'FAILED' ? detail.statusMessage : activity.latestMessage const activeBatchLabel = activity.activeBatchIndexes.length > 0 @@ -731,7 +732,7 @@ export function BomCostingAnalysisProgressPanel({ ) : null} - {detail.progress}% + {processingStopped ? '已停止' : `${detail.progress}%`} @@ -746,6 +747,11 @@ export function BomCostingAnalysisProgressPanel({ aria-valuemin={0} aria-valuemax={100} aria-valuenow={detail.progress} + aria-valuetext={ + processingStopped + ? `任务已停止,最后记录进度 ${detail.progress}%` + : undefined + } > { progress: 52, statusMessage: '多模态分析服务暂时不可用', sourceCacheHit: false, + sourceSkuCount: 7, storedImageCount: 33, failedImageCount: 0, createdById: 'user-1', @@ -743,6 +744,9 @@ describe('BomCostingWorkbenchClient', () => { ).toBeInTheDocument() expect(screen.queryByText('当前案例')).not.toBeInTheDocument() expect(screen.getByLabelText('失败')).toBeInTheDocument() + expect(screen.getByText('已停止 · 已读取 7 SKU · 33 图')).toBeInTheDocument() + expect(screen.queryByText('0 SKU · 33 图')).not.toBeInTheDocument() + expect(screen.getAllByText('已停止').length).toBeGreaterThan(0) expect(screen.queryByText('本次核算已停止')).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: '继续处理' })) diff --git a/furniture-3d-system/src/app/bom-costing/BomCostingWorkbenchClient.tsx b/furniture-3d-system/src/app/bom-costing/BomCostingWorkbenchClient.tsx index 3d333ccf..49470f86 100644 --- a/furniture-3d-system/src/app/bom-costing/BomCostingWorkbenchClient.tsx +++ b/furniture-3d-system/src/app/bom-costing/BomCostingWorkbenchClient.tsx @@ -142,6 +142,55 @@ const STATUS_DOT_CLASSES: Record = { FAILED: 'bg-[#b6534e]', } +function getHistoryStatusLabel(item: BomCostingCaseSummary) { + if (item.status !== 'READY') return STATUS_LABELS[item.status] + if (item.requiresRecalculation) return '旧规则结果' + if (item.hasFailedRecalculation) return '重算失败' + if (item.needsPriceReview) return '待复核' + return STATUS_LABELS[item.status] +} + +function getHistoryStatusDotClass(item: BomCostingCaseSummary) { + if ( + item.status === 'READY' && + (item.requiresRecalculation || item.hasFailedRecalculation || item.needsPriceReview) + ) { + return 'bg-[#c68b37]' + } + return STATUS_DOT_CLASSES[item.status] +} + +function getKnownSourceSkuCount(item: BomCostingCaseSummary) { + return item.sourceSkuCount ?? item.skuCount +} + +function getHistoryMetrics(item: BomCostingCaseSummary) { + const sourceSkuCount = getKnownSourceSkuCount(item) + const sourceSkuText = sourceSkuCount > 0 + ? `已读取 ${sourceSkuCount} SKU` + : '' + if (['QUEUED', 'ANALYZING'].includes(item.status)) { + return [ + `${item.progress}%`, + sourceSkuText, + `${item.storedImageCount} 图`, + ].filter(Boolean).join(' · ') + } + if (item.status === 'FAILED') { + return [ + '已停止', + sourceSkuText, + `${item.storedImageCount} 图`, + ].filter(Boolean).join(' · ') + } + const resultSkuText = item.skuCount > 0 ? `${item.skuCount} SKU` : '结果待核' + return [ + item.requiresRecalculation ? '历史结果' : '', + resultSkuText, + `${item.storedImageCount} 图`, + ].filter(Boolean).join(' · ') +} + const HISTORY_STATUS_OPTIONS: Array<{ value: HistoryStatusFilter label: string @@ -348,6 +397,7 @@ async function readApiData(response: Response) { } function toSummary(detail: BomCostingCaseDetail): BomCostingCaseSummary { + const requiresRecalculation = Boolean(detail.requiresRecalculation) const costs = detail.skus .map((sku) => sku.estimatedCost) .filter((value): value is number => value !== null) @@ -359,13 +409,17 @@ function toSummary(detail: BomCostingCaseDetail): BomCostingCaseSummary { status: detail.status, progress: detail.progress, statusMessage: detail.statusMessage, + sourceSkuCount: detail.sourceSnapshot?.skuCount ?? detail.skus.length, + requiresRecalculation, + hasFailedRecalculation: Boolean(detail.hasFailedRecalculation), + needsPriceReview: Boolean(detail.needsPriceReview), sourceCacheHit: detail.sourceCacheHit, storedImageCount: detail.sourceSnapshot?.storedImageCount ?? 0, failedImageCount: detail.sourceSnapshot?.failedImageCount ?? 0, createdById: detail.createdById, createdAt: detail.createdAt, skuCount: detail.skus.length, - estimatedCostRange: costs.length + estimatedCostRange: !requiresRecalculation && costs.length ? { min: Math.min(...costs), max: Math.max(...costs) } : null, } @@ -546,12 +600,17 @@ function ProcessInspector({ (focusAsset ? `${focusAsset.kind === 'MAIN' ? '主图' : focusAsset.kind === 'SKU' ? 'SKU 图' : '详情图'} ${focusAsset.sortOrder + 1}` : detail.statusMessage) + const processingStopped = detail.status === 'FAILED' return (