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:
Misaka
2026-04-07 21:55:47 +08:00
parent b065e23306
commit 8b173890fa
9 changed files with 647 additions and 48 deletions

View File

@@ -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 && (

View File

@@ -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
}
}

View File

@@ -45,6 +45,8 @@ export interface CleanerReportData {
errors: string[]
retriedOrders?: number
successfulRetries?: number
materialsFailed?: number
uncertainDeletions?: number
}
export interface CleanerInitializationResult {

View File

@@ -162,6 +162,8 @@ const CleanerPage: React.FC = () => {
triggerRef={executeButtonRef}
retriedOrders={reportData?.retriedOrders}
successfulRetries={reportData?.successfulRetries}
materialsFailed={reportData?.materialsFailed}
uncertainDeletions={reportData?.uncertainDeletions}
/>
</Suspense>