From 715dfb4d7142f460482fda465aaa506430cf3ef1 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Fri, 13 Mar 2026 15:59:10 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(cleaner):=20add=20automatic=20?= =?UTF-8?q?retry=20mechanism=20for=20failed=20orders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add retry logic with max 2 attempts per failed order - Track retry statistics (retriedOrders, successfulRetries) - Generate detailed retry report section in execution reports - Display retry metrics in ExecutionReportDialog UI Co-Authored-By: Claude Opus 4.6 --- src/main/ipc/validation-handler.ts | 6 +- src/main/services/erp/cleaner.ts | 169 +++++++++++++++++- .../report/cleaner-report-generator.ts | 79 +++++++- src/main/types/cleaner.types.ts | 14 ++ .../src/components/ExecutionReportDialog.tsx | 48 ++++- .../src/components/ui/ConfirmDialog.tsx | 18 +- src/renderer/src/hooks/useCleaner.ts | 6 +- src/renderer/src/hooks/useDialogFocus.ts | 4 +- src/renderer/src/pages/CleanerPage.tsx | 2 + 9 files changed, 325 insertions(+), 21 deletions(-) diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts index f26ee2b..774d78e 100644 --- a/src/main/ipc/validation-handler.ts +++ b/src/main/ipc/validation-handler.ts @@ -296,7 +296,8 @@ export function registerValidationHandlers(): void { if (sourceNumbers.length === 0) { return { success: false, - error: '共享的 Production ID 没有找到对应的订单数据。请确保在数据提取页面输入了有效的 Production ID 并成功获取了订单数据。', + error: + '共享的 Production ID 没有找到对应的订单数据。请确保在数据提取页面输入了有效的 Production ID 并成功获取了订单数据。', stats: { totalRecords: 0, matchedCount: 0, @@ -315,7 +316,8 @@ export function registerValidationHandlers(): void { if (sourceNumbers.length === 0) { return { success: false, - error: '文件中的 Production ID 没有找到对应的订单数据。请检查 Production ID 是否正确,或确保数据库中有对应的订单数据。', + error: + '文件中的 Production ID 没有找到对应的订单数据。请检查 Production ID 是否正确,或确保数据库中有对应的订单数据。', stats: { totalRecords: 0, matchedCount: 0, diff --git a/src/main/services/erp/cleaner.ts b/src/main/services/erp/cleaner.ts index afde676..7bbb04a 100644 --- a/src/main/services/erp/cleaner.ts +++ b/src/main/services/erp/cleaner.ts @@ -7,6 +7,12 @@ import { createLogger } from '../logger' const log = createLogger('CleanerService') +interface RetryResult { + retriedOrders: number + successfulRetries: number + updatedDetails: OrderCleanDetail[] +} + /** * Cleaner Service Options */ @@ -102,7 +108,9 @@ export class CleanerService { materialsDeleted: 0, materialsSkipped: 0, errors: [], - details: [] + details: [], + retriedOrders: 0, + successfulRetries: 0 } const totalOrders = input.orderNumbers.length @@ -158,7 +166,11 @@ export class CleanerService { materialsDeleted: 0, materialsSkipped: 0, errors: [message], - skippedMaterials: [] + skippedMaterials: [], + retryCount: 0, + retryAttempts: [], + retriedAt: undefined, + retrySuccess: false }) } } @@ -171,6 +183,36 @@ export class CleanerService { materialsSkipped: result.materialsSkipped, errorCount: result.errors.length }) + + // Retry failed orders + const retryResult = await this.retryFailedOrders({ + workFrame, + popupPage, + failedDetails: result.details.filter((d) => d.errors.length > 0), + deleteSet, + dryRun, + onProgress: input.onProgress + }) + + // Merge retry results + result.retriedOrders = retryResult.retriedOrders + result.successfulRetries = retryResult.successfulRetries + + // Update details with retry information + retryResult.updatedDetails.forEach((updatedDetail) => { + const index = result.details.findIndex((d) => d.orderNumber === updatedDetail.orderNumber) + if (index !== -1) { + result.details[index] = updatedDetail + } + }) + + // Clear errors for successfully retried orders + const successfulRetryOrders = new Set( + retryResult.updatedDetails.filter((d) => d.retrySuccess).map((d) => d.orderNumber) + ) + result.errors = result.errors.filter( + (err) => !successfulRetryOrders.has(err.split(':')[0].replace('Order ', '')) + ) } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' log.error('Cleaner failed', { error: message }) @@ -265,7 +307,11 @@ export class CleanerService { materialsDeleted: 0, materialsSkipped: 0, errors: [], - skippedMaterials: [] + skippedMaterials: [], + retryCount: 0, + retryAttempts: [], + retriedAt: undefined, + retrySuccess: false } // Query the order (Python lines 187-189) @@ -531,4 +577,121 @@ export class CleanerService { private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } + + /** + * Retry failed orders from the initial execution + * Maximum 2 retry attempts per failed order + */ + private async retryFailedOrders(params: { + workFrame: FrameLocator + popupPage: Page + failedDetails: OrderCleanDetail[] + deleteSet: Set + dryRun: boolean + onProgress?: ( + message: string, + progress?: number, + extra?: Partial + ) => void + }): Promise { + const { workFrame, popupPage, failedDetails, deleteSet, dryRun, onProgress } = params + + const result: RetryResult = { + retriedOrders: 0, + successfulRetries: 0, + updatedDetails: [] + } + + if (failedDetails.length === 0) { + return result + } + + log.info('Starting retry for failed orders', { count: failedDetails.length }) + + const MAX_RETRIES = 2 + + for (const failedDetail of failedDetails) { + const orderNumber = failedDetail.orderNumber + const retryAttempts: import('../../types/cleaner.types').RetryAttempt[] = [] + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + log.info(`Retrying order ${orderNumber} (attempt ${attempt}/${MAX_RETRIES})`) + + // Create a new detail for retry + const retryDetail: OrderCleanDetail = { + orderNumber, + materialsDeleted: 0, + materialsSkipped: 0, + errors: [], + skippedMaterials: [], + retryCount: attempt, + retryAttempts: [], + retriedAt: undefined, + retrySuccess: false + } + + // Re-run the order processing + await this.processOrder({ + workFrame, + popupPage, + orderNumber, + orderIndex: 0, // Not used for retry + totalOrders: failedDetails.length, + deleteSet, + dryRun, + onProgress: (message, progress, extra) => { + onProgress?.( + `[重试 ${attempt}/${MAX_RETRIES}] ${message}`, + progress, + extra ? { ...extra, phase: 'processing' as const } : undefined + ) + } + }) + + // If we reach here, retry succeeded + result.successfulRetries++ + log.info(`Retry succeeded for order ${orderNumber}`) + + // Merge the successful retry detail + result.updatedDetails.push({ + ...retryDetail, + retriedAt: Date.now(), + retrySuccess: true + }) + result.retriedOrders++ + break // Exit retry loop for this order + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + log.warn(`Retry attempt ${attempt} failed for order ${orderNumber}: ${message}`) + + retryAttempts.push({ + attempt, + error: message, + timestamp: Date.now() + }) + + if (attempt === MAX_RETRIES) { + // All retries exhausted + log.error(`All retries failed for order ${orderNumber}`) + result.updatedDetails.push({ + ...failedDetail, + retryCount: MAX_RETRIES, + retryAttempts, + retriedAt: Date.now(), + retrySuccess: false + }) + result.retriedOrders++ + } + } + } + } + + log.info('Retry process completed', { + retriedOrders: result.retriedOrders, + successfulRetries: result.successfulRetries + }) + + return result + } } diff --git a/src/main/services/report/cleaner-report-generator.ts b/src/main/services/report/cleaner-report-generator.ts index 6f7ce10..4c3e4d1 100644 --- a/src/main/services/report/cleaner-report-generator.ts +++ b/src/main/services/report/cleaner-report-generator.ts @@ -89,6 +89,10 @@ export class CleanerReportGenerator { lines.push(`| **删除物料数** | \`${result.materialsDeleted}\``) lines.push(`| **跳过物料数** | \`${result.materialsSkipped}\``) lines.push(`| **错误数量** | \`${result.errors.length}\``) + if (result.retriedOrders > 0) { + lines.push(`| **重试订单数** | \`${result.retriedOrders}\``) + lines.push(`| **成功重试数** | \`${result.successfulRetries}\``) + } lines.push(`| **执行耗时** | \`${this.formatDuration(options.startTime, options.endTime)}\``) lines.push('') lines.push('---') @@ -100,6 +104,12 @@ export class CleanerReportGenerator { lines.push('| ----------- | ---- | ------ |') lines.push(`| ✅ 成功订单 | ${stats.successCount} | ${stats.successRate.toFixed(1)}% |`) lines.push(`| ❌ 失败订单 | ${stats.failureCount} | ${(100 - stats.successRate).toFixed(1)}% |`) + if (result.retriedOrders > 0) { + const retrySuccessRate = + result.retriedOrders > 0 ? (result.successfulRetries / result.retriedOrders) * 100 : 0 + lines.push(`| 🔄 重试订单 | ${result.retriedOrders} | 100% |`) + lines.push(`| ✅ 成功重试 | ${result.successfulRetries} | ${retrySuccessRate.toFixed(1)}% |`) + } lines.push('') lines.push('---') lines.push('') @@ -111,10 +121,19 @@ export class CleanerReportGenerator { result.details.forEach((detail, index) => { const orderNum = index + 1 - const status = detail.errors.length > 0 ? '❌ 失败' : '✅ 成功' + let status = detail.errors.length > 0 ? '❌ 失败' : '✅ 成功' + + // Override status if retry was successful + if (detail.retrySuccess) { + status = '✅ 重试成功' + } else if (detail.retryCount > 0 && !detail.retrySuccess) { + status = '❌ 重试失败' + } + const errorMsg = detail.errors.length > 0 ? detail.errors[0] : '-' + const retryInfo = detail.retryCount > 0 ? ` [重试${detail.retryCount}次]` : '' lines.push( - `| ${orderNum} | \`${detail.orderNumber}\` | ${detail.materialsDeleted} | ${detail.materialsSkipped} | ${status} | \`${errorMsg}\` |` + `| ${orderNum} | \`${detail.orderNumber}\` | ${detail.materialsDeleted} | ${detail.materialsSkipped} | ${status}${retryInfo} | \`${errorMsg}\` |` ) }) @@ -175,6 +194,62 @@ export class CleanerReportGenerator { lines.push('') } + // Add retry details section + if (result.retriedOrders > 0) { + lines.push('## 重试执行详情') + lines.push('') + lines.push( + `**重试订单总数**: \`${result.retriedOrders}\` | **成功**: \`${result.successfulRetries}\` | **失败**: \`${result.retriedOrders - result.successfulRetries}\`` + ) + lines.push('') + + const retriedDetails = result.details.filter((d) => d.retryCount > 0) + + if (retriedDetails.length > 0) { + lines.push('### 重试订单列表') + lines.push('') + lines.push('| 订单号 | 重试次数 | 重试结果 | 重试时间 |') + lines.push('| -------- | -------- | -------- | ------------ |') + + retriedDetails.forEach((detail) => { + const retryStatus = detail.retrySuccess ? '✅ 成功' : '❌ 失败' + const retryTime = detail.retriedAt ? this.formatDateTime(detail.retriedAt) : '-' + lines.push( + `| \`${detail.orderNumber}\` | ${detail.retryCount} | ${retryStatus} | ${retryTime} |` + ) + }) + + lines.push('') + lines.push('### 重试尝试详细记录') + lines.push('') + + retriedDetails.forEach((detail) => { + lines.push(`#### \`${detail.orderNumber}\``) + lines.push('') + lines.push(`- **重试次数**: ${detail.retryCount}`) + lines.push(`- **最终结果**: ${detail.retrySuccess ? '✅ 成功' : '❌ 失败'}`) + + if (detail.retryAttempts && detail.retryAttempts.length > 0) { + lines.push('') + lines.push('**重试尝试记录**:') + lines.push('') + detail.retryAttempts.forEach((attempt, idx) => { + lines.push( + `${idx + 1}. **第${attempt.attempt}次尝试** - ${this.formatDateTime(attempt.timestamp)}` + ) + lines.push(` - 错误:${attempt.error}`) + }) + lines.push('') + } + + lines.push('---') + lines.push('') + }) + } + + lines.push('') + } + lines.push(`**报告生成时间**: \`${this.formatDateTime(options.endTime)}\``) lines.push('**报表版本**: `v1.0`') diff --git a/src/main/types/cleaner.types.ts b/src/main/types/cleaner.types.ts index bcc255c..68c99f6 100644 --- a/src/main/types/cleaner.types.ts +++ b/src/main/types/cleaner.types.ts @@ -25,6 +25,9 @@ export interface CleanerResult { materialsSkipped: number errors: string[] details: OrderCleanDetail[] + // Retry statistics + retriedOrders: number + successfulRetries: number } export interface SkippedMaterial { @@ -34,12 +37,23 @@ export interface SkippedMaterial { reason: string } +export interface RetryAttempt { + attempt: number + error: string + timestamp: number +} + export interface OrderCleanDetail { orderNumber: string materialsDeleted: number materialsSkipped: number errors: string[] skippedMaterials: SkippedMaterial[] + // Retry-related fields + retryCount: number + retryAttempts?: RetryAttempt[] + retriedAt?: number + retrySuccess?: boolean } /** diff --git a/src/renderer/src/components/ExecutionReportDialog.tsx b/src/renderer/src/components/ExecutionReportDialog.tsx index eeb465b..4494460 100644 --- a/src/renderer/src/components/ExecutionReportDialog.tsx +++ b/src/renderer/src/components/ExecutionReportDialog.tsx @@ -34,6 +34,9 @@ interface ExecutionReportDialogProps { progress?: CleanerProgress | null startTime?: number | null triggerRef?: React.RefObject + // Retry-related props + retriedOrders?: number + successfulRetries?: number } export const ExecutionReportDialog: React.FC = ({ @@ -47,11 +50,14 @@ export const ExecutionReportDialog: React.FC = ({ isExecuting = false, progress = null, startTime = null, - triggerRef + triggerRef, + retriedOrders = 0, + successfulRetries = 0 }) => { const [now, setNow] = React.useState(() => Date.now()) const hasErrors = errors.length > 0 + const hasRetries = retriedOrders > 0 const showProgress = isExecuting && progress const isProgressing = !!showProgress @@ -259,6 +265,46 @@ export const ExecutionReportDialog: React.FC = ({ )} + + {hasRetries && ( + <> +
+
+ + + + + + +
+
+
重试订单
+
{retriedOrders}
+
+
+ +
+
+ +
+
+
成功重试
+
{successfulRetries}
+
+
+ + )} {hasErrors && ( diff --git a/src/renderer/src/components/ui/ConfirmDialog.tsx b/src/renderer/src/components/ui/ConfirmDialog.tsx index dbd449a..3b04e66 100644 --- a/src/renderer/src/components/ui/ConfirmDialog.tsx +++ b/src/renderer/src/components/ui/ConfirmDialog.tsx @@ -107,11 +107,7 @@ export function ConfirmDialog({ - @@ -125,14 +121,14 @@ export function ConfirmDialog({ */ export function useConfirmDialog() { const [config, setConfig] = useState< - (Omit & { - resolve: (value: boolean) => void - }) | null>(null) + | (Omit & { + resolve: (value: boolean) => void + }) + | null + >(null) const confirm = useCallback( - ( - options: Omit - ): Promise => { + (options: Omit): Promise => { return new Promise((resolve) => { setConfig({ ...options, diff --git a/src/renderer/src/hooks/useCleaner.ts b/src/renderer/src/hooks/useCleaner.ts index 72e8bcc..eb554c8 100644 --- a/src/renderer/src/hooks/useCleaner.ts +++ b/src/renderer/src/hooks/useCleaner.ts @@ -65,6 +65,8 @@ export function useCleaner() { materialsDeleted: number materialsSkipped: number errors: string[] + retriedOrders?: number + successfulRetries?: number } | null>(null) // Progress state @@ -426,7 +428,9 @@ export function useCleaner() { ordersProcessed: cleanerRunData.ordersProcessed, materialsDeleted: cleanerRunData.materialsDeleted, materialsSkipped: cleanerRunData.materialsSkipped, - errors: cleanerRunData.errors + errors: cleanerRunData.errors, + retriedOrders: cleanerRunData.retriedOrders, + successfulRetries: cleanerRunData.successfulRetries }) } else { throw new Error(response.error || '清理失败') diff --git a/src/renderer/src/hooks/useDialogFocus.ts b/src/renderer/src/hooks/useDialogFocus.ts index baade84..03dfd8b 100644 --- a/src/renderer/src/hooks/useDialogFocus.ts +++ b/src/renderer/src/hooks/useDialogFocus.ts @@ -239,7 +239,9 @@ export function useDialogFocus(options: UseDialogFocusOptions): UseDialogFocusRe if (style.visibility === 'hidden') { if (import.meta.env.DEV) { - console.warn('[useDialogFocus] Trigger element is visibility: hidden, cannot restore focus') + console.warn( + '[useDialogFocus] Trigger element is visibility: hidden, cannot restore focus' + ) } return } diff --git a/src/renderer/src/pages/CleanerPage.tsx b/src/renderer/src/pages/CleanerPage.tsx index a044ae9..c783f47 100644 --- a/src/renderer/src/pages/CleanerPage.tsx +++ b/src/renderer/src/pages/CleanerPage.tsx @@ -496,6 +496,8 @@ const CleanerPage: React.FC = () => { progress={progress} startTime={startTime} triggerRef={executeButtonRef} + retriedOrders={reportData?.retriedOrders} + successfulRetries={reportData?.successfulRetries} /> {/* Confirmation Dialog */}