feat(cleaner): add multi-signal deletion verification with material-level retry
Replace fragile single-signal (row change only) deletion verification with a robust multi-signal approach using row change + material count + ERP message detection. Add material-level retry (up to 3 attempts) for transient failures, with detailed tracking of failed/uncertain deletions. - Add DeletionOutcome/DeletionErrorCategory enums and FailedMaterial type - Add deleteWithVerification() core method with retry logic - Add evaluateDeletionSignals() pure logic (unit tested, 9 cases) - Add helper methods: readMaterialCount, checkErpMessages, handleConfirmDialog - Extend CleanerResult/OrderCleanDetail with failed/uncertain tracking - Update report generator with failed materials detail section - Update ExecutionReportDialog to display failed/uncertain stats Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { CheckCircle, XCircle, SkipForward, Package, Loader2 } from 'lucide-react'
|
||||
import { CheckCircle, XCircle, SkipForward, Package, Loader2, AlertTriangle } from 'lucide-react'
|
||||
import { Modal } from './ui/Modal'
|
||||
import type { CleanerProgress } from '../hooks/cleaner/types'
|
||||
|
||||
@@ -27,6 +27,9 @@ interface ExecutionReportDialogProps {
|
||||
// Retry-related props
|
||||
retriedOrders?: number
|
||||
successfulRetries?: number
|
||||
// Deletion verification props
|
||||
materialsFailed?: number
|
||||
uncertainDeletions?: number
|
||||
}
|
||||
|
||||
export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
||||
@@ -42,12 +45,15 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
||||
startTime = null,
|
||||
triggerRef,
|
||||
retriedOrders = 0,
|
||||
successfulRetries = 0
|
||||
successfulRetries = 0,
|
||||
materialsFailed = 0,
|
||||
uncertainDeletions = 0
|
||||
}) => {
|
||||
const [now, setNow] = React.useState(() => Date.now())
|
||||
|
||||
const hasErrors = errors.length > 0
|
||||
const hasRetries = retriedOrders > 0
|
||||
const hasFailedMaterials = materialsFailed > 0 || uncertainDeletions > 0
|
||||
const showProgress = isExecuting && progress
|
||||
const isProgressing = !!showProgress
|
||||
|
||||
@@ -327,6 +333,36 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasFailedMaterials && (
|
||||
<>
|
||||
{materialsFailed > 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-3 flex items-center gap-3 border border-gray-200">
|
||||
<div className="w-9 h-9 rounded-lg bg-red-50 flex items-center justify-center flex-shrink-0">
|
||||
<XCircle size={20} className="text-red-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-gray-600">删除失败</div>
|
||||
<div className="text-xl font-semibold text-red-600">{materialsFailed}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uncertainDeletions > 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-3 flex items-center gap-3 border border-gray-200">
|
||||
<div className="w-9 h-9 rounded-lg bg-yellow-50 flex items-center justify-center flex-shrink-0">
|
||||
<AlertTriangle size={20} className="text-yellow-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-gray-600">不确定删除</div>
|
||||
<div className="text-xl font-semibold text-yellow-600">
|
||||
{uncertainDeletions}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{elapsedTime && (
|
||||
|
||||
@@ -20,6 +20,8 @@ interface CleanerRunPayload {
|
||||
errors: string[]
|
||||
retriedOrders: number
|
||||
successfulRetries: number
|
||||
materialsFailed?: number
|
||||
uncertainDeletions?: number
|
||||
}
|
||||
|
||||
export async function initializeCleanerPage(): Promise<CleanerInitializationResult> {
|
||||
@@ -161,7 +163,9 @@ export async function runCleanerExecution(params: {
|
||||
materialsSkipped: cleanerRunData.materialsSkipped,
|
||||
errors: cleanerRunData.errors,
|
||||
retriedOrders: cleanerRunData.retriedOrders,
|
||||
successfulRetries: cleanerRunData.successfulRetries
|
||||
successfulRetries: cleanerRunData.successfulRetries,
|
||||
materialsFailed: cleanerRunData.materialsFailed ?? 0,
|
||||
uncertainDeletions: cleanerRunData.uncertainDeletions ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ export interface CleanerReportData {
|
||||
errors: string[]
|
||||
retriedOrders?: number
|
||||
successfulRetries?: number
|
||||
materialsFailed?: number
|
||||
uncertainDeletions?: number
|
||||
}
|
||||
|
||||
export interface CleanerInitializationResult {
|
||||
|
||||
@@ -162,6 +162,8 @@ const CleanerPage: React.FC = () => {
|
||||
triggerRef={executeButtonRef}
|
||||
retriedOrders={reportData?.retriedOrders}
|
||||
successfulRetries={reportData?.successfulRetries}
|
||||
materialsFailed={reportData?.materialsFailed}
|
||||
uncertainDeletions={reportData?.uncertainDeletions}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user