From 9fbf75a8a0971f853ea68eeef51050e89b48e912 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Mon, 2 Mar 2026 12:49:31 +0800 Subject: [PATCH] docs: add confirm deletion button flow analysis to cleaner validation docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive documentation for the "确认删除 (同步数据库)" button flow, including: - Core flow overview with Mermaid diagram - Frontend interaction layer analysis (handleConfirmDeletion) - IPC handler layer details (upsertBatch, delete handlers) - Database DAO layer implementation with MySQL vs SQL Server differences - Data flow diagram showing all layers - Key data structures and error handling - Comparison table with validation status flow The document now covers both core business flows in the cleaner page: 1. Get and validate material status (read operation) 2. Confirm deletion/sync to database (write operation) Co-Authored-By: Claude Sonnet 4.5 --- docs/cleaner-validation-flow.md | 753 +++++++++++++++++++++++++++++++- 1 file changed, 747 insertions(+), 6 deletions(-) diff --git a/docs/cleaner-validation-flow.md b/docs/cleaner-validation-flow.md index 0182b30..3def0e2 100644 --- a/docs/cleaner-validation-flow.md +++ b/docs/cleaner-validation-flow.md @@ -1,12 +1,18 @@ -# 清理界面 - 获取校验状态流程分析文档 +# 清理界面 - 业务流程分析文档 -**文档版本**: 1.0 +**文档版本**: 2.0 **创建日期**: 2026-03-02 +**最后更新**: 2026-03-02 **面向对象**: 开发人员 ## 概述 -本文档详细分析了在清理界面(CleanerPage)中,用户点击"获取并校验物料状态"按钮后,程序的完整运行逻辑,包括前端交互、IPC通信、后端处理和数据库交互。 +本文档详细分析了清理界面(CleanerPage)中两个核心业务流程的完整运行逻辑: + +1. **获取并校验物料状态**按钮 - 从数据库读取物料数据并校验 +2. **确认删除 (同步数据库)**按钮 - 将用户勾选状态同步到数据库 + +每个流程都包含前端交互、IPC通信、后端处理和数据库交互的完整分析。 --- @@ -483,15 +489,750 @@ flowchart TB --- +## "确认删除 (同步数据库)" 按钮流程 + +### 1. 核心流程概览 + +```mermaid +flowchart TB + Start([用户点击
确认删除同步数据库]) --> ValidateData{validationResults
有数据?} + ValidateData -->|否| Alert1[alert
没有可处理的数据] + ValidateData -->|是| Classify[数据处理和分类
遍历validationResults] + + Classify --> CheckSelected{物料是否勾选?} + CheckSelected -->|已勾选| CheckManager{有负责人?} + CheckSelected -->|未勾选| AddToDelete[添加到
materialsToDelete] + + CheckManager -->|否| AddMissing[添加到
missingManager] + CheckManager -->|是| AddToUpsert[添加到
materialsToUpsert] + + AddMissing --> ValidateManager{missingManager
有数据?} + AddToUpsert --> ValidateManager + AddToDelete --> ValidateManager + + ValidateManager -->|是| Alert2[alert警告
缺少负责人的物料] + ValidateManager -->|否| CheckEmpty{两个数组
都为空?} + + CheckEmpty -->|是| Alert3[alert
没有需要处理的记录] + CheckEmpty -->|否| ShowConfirm[显示确认对话框
写入/更新 N 条
删除 N 条] + + ShowConfirm --> UserConfirm{用户确认?} + UserConfirm -->|否| End([流程结束]) + UserConfirm -->|是| SetRunning[设置isRunning=true] + + SetRunning --> UpsertCheck{materialsToUpsert
有数据?} + UpsertCheck -->|是| CallUpsert[IPC调用
materials:upsertBatch] + UpsertCheck -->|否| DeleteCheck{materialsToDelete
有数据?} + + CallUpsert --> UpsertSuccess{success?} + UpsertSuccess -->|否| ThrowError[抛出错误] + UpsertSuccess -->|是| AddMsg1[添加消息:
写入/更新成功 N 条] + AddMsg1 --> DeleteCheck + + DeleteCheck -->|是| CallDelete[IPC调用
materials:delete] + DeleteCheck -->|否| ShowResult[显示操作结果
alert成功消息] + + CallDelete --> DeleteSuccess{success?} + DeleteSuccess -->|否| ThrowError + DeleteSuccess -->|是| AddMsg2[添加消息:
删除成功 N 条] + AddMsg2 --> ShowResult + + ShowResult --> AdminCheck{isAdmin?} + AdminCheck -->|是| ReloadManagers[重新加载负责人列表
materials:getManagers] + AdminCheck -->|否| SetRunningFalse[设置isRunning=false] + ReloadManagers --> SetRunningFalse + SetRunningFalse --> End + + ThrowError --> CatchError[catch块
alert错误消息] + CatchError --> SetRunningFalse + Alert1 --> End + Alert2 --> End + Alert3 --> End +``` + +--- + +### 2. 前端交互层 (CleanerPage.tsx) + +**触发位置**: `src/renderer/src/pages/CleanerPage.tsx:166-226` + +```mermaid +sequenceDiagram + participant User as 用户 + participant UI as CleanerPage + participant API as window.electron.materials + + User->>UI: 点击"确认删除(同步数据库)"按钮 + UI->>UI: 检查 validationResults 是否为空 + + alt 没有数据 + UI->>User: alert("没有可处理的数据") + else 有数据 + UI->>UI: 数据分类(遍历validationResults) + Note over UI: 分类逻辑:
- 已勾选 + 有负责人 → materialsToUpsert
- 已勾选 + 无负责人 → missingManager
- 未勾选 → materialsToDelete + + alt 有缺少负责人的物料 + UI->>User: alert("以下已勾选的记录缺少负责人信息") + else 没有缺少负责人 + UI->>User: confirm("确认以下操作吗?") + alt 用户取消 + Note over UI: 流程结束 + else 用户确认 + UI->>UI: setIsRunning(true) + UI->>API: upsertBatch(materialsToUpsert) + API-->>UI: MaterialOperationResponse + alt upsert失败 + UI->>User: throw Error + else upsert成功 + Note over UI: 记录成功数量 + UI->>API: delete(materialsToDelete) + API-->>UI: MaterialOperationResponse + alt delete失败 + UI->>User: throw Error + else delete成功 + Note over UI: 记录删除数量 + UI->>User: alert("操作完成!") + alt isAdmin + UI->>API: getManagers() + API-->>UI: { managers } + UI->>UI: setManagers(resp.managers) + end + end + end + UI->>UI: setIsRunning(false) + end + end + end +``` + +**关键代码逻辑**: + +```typescript +const handleConfirmDeletion = async () => { + // 1. 数据验证 + if (validationResults.length === 0) return alert('没有可处理的数据') + + // 2. 数据分类 + const materialsToUpsert: { materialCode: string; managerName: string }[] = [] + const materialsToDelete: string[] = [] + const missingManager: string[] = [] + + for (const result of validationResults) { + if (!result.materialCode?.trim()) continue + + const code = result.materialCode.trim() + if (selectedItems.has(code)) { + // 已勾选: 检查是否有负责人 + if (!result.managerName?.trim()) missingManager.push(code) + else materialsToUpsert.push({ materialCode: code, managerName: result.managerName.trim() }) + } else { + // 未勾选: 标记为删除 + materialsToDelete.push(code) + } + } + + // 3. 负责人验证 + if (missingManager.length > 0) { + alert(`以下已勾选的记录缺少负责人信息,无法保存:\n\n${missingManager.slice(0, 10).join('\n')}`) + return + } + + // 4. 空数据处理 + if (materialsToUpsert.length === 0 && materialsToDelete.length === 0) { + return alert('没有需要处理的记录') + } + + // 5. 用户确认 + const confirmParts: string[] = [] + if (materialsToUpsert.length > 0) confirmParts.push(`写入/更新 ${materialsToUpsert.length} 条记录`) + if (materialsToDelete.length > 0) confirmParts.push(`删除 ${materialsToDelete.length} 条记录`) + + if (!window.confirm(`确认以下操作吗?\n\n${confirmParts.join('\n')}`)) return + + // 6. 执行数据库操作 + try { + setIsRunning(true) + let msgParts: string[] = [] + + // 6.1 批量写入/更新 + if (materialsToUpsert.length > 0) { + const res = await window.electron.materials.upsertBatch(materialsToUpsert) + if (!res.success) throw new Error(res.error || '写入物料失败') + msgParts.push(`写入/更新成功:${res.stats?.success || 0} 条`) + } + + // 6.2 批量删除 + if (materialsToDelete.length > 0) { + const res = await window.electron.materials.delete(materialsToDelete) + if (!res.success) throw new Error(res.error || '删除物料失败') + msgParts.push(`删除成功:${res.count || 0} 条`) + } + + // 7. 显示结果 + alert(`操作完成!\n\n${msgParts.join('\n')}`) + + // 8. 重新加载负责人列表(管理员) + if (isAdmin) { + const resp = await window.electron.materials.getManagers() + setManagers(resp.managers) + } + } catch (err) { + alert(err instanceof Error ? err.message : '操作失败') + } finally { + setIsRunning(false) + } +} +``` + +**数据分类逻辑**: + +| 物料状态 | 勾选状态 | 负责人信息 | 处理方式 | +|---------|---------|-----------|---------| +| 已标记删除 | ✅ 勾选 | ✅ 有 | 保存/更新到数据库 | +| 已标记删除 | ✅ 勾选 | ❌ 无 | 拒绝操作,弹出警告 | +| 已标记删除 | ❌ 未勾选 | - | 从数据库删除 | +| 未标记删除 | ✅ 勾选 | ✅ 有 | 保存/更新到数据库 | +| 未标记删除 | ✅ 勾选 | ❌ 无 | 拒绝操作,弹出警告 | +| 未标记删除 | ❌ 未勾选 | - | 从数据库删除 | + +--- + +### 3. IPC Handler层 (validation-handler.ts) + +**处理位置**: `src/main/ipc/validation-handler.ts:399-449` + +#### 3.1 materials:upsertBatch 处理器 + +```mermaid +flowchart TB + Start([接收IPC调用
materials:upsertBatch]) --> CreateDAO[创建
MaterialsToBeDeletedDAO] + CreateDAO --> CallUpsert[调用
dao.upsertBatch] + CallUpsert --> ExecuteDAO[DAO层执行
逐条upsert操作] + ExecuteDAO --> Success{操作成功?} + + Success -->|是| BuildSuccess[构建成功响应
success=true
包含stats] + Success -->|否| BuildError[构建错误响应
success=false
error=错误消息] + + BuildSuccess --> Return([返回响应]) + BuildError --> LogError[console.error
Upsert batch error] + LogError --> Return +``` + +**关键代码**: + +```typescript +ipcMain.handle( + 'materials:upsertBatch', + async (_event, request: MaterialUpsertBatchRequest): Promise => { + try { + const dao = new MaterialsToBeDeletedDAO() + const stats = await dao.upsertBatch(request.materials) + + return { + success: true, + stats + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + console.error('[Materials] Upsert batch error:', error) + return { + success: false, + error: `Upsert failed: ${message}` + } + } + } +) +``` + +#### 3.2 materials:delete 处理器 + +```mermaid +flowchart TB + Start([接收IPC调用
materials:delete]) --> CreateDAO[创建
MaterialsToBeDeletedDAO] + CreateDAO --> CallDelete[调用
dao.deleteByMaterialCodes] + CallDelete --> ExecuteDAO[DAO层执行
批量删除操作] + ExecuteDAO --> Success{操作成功?} + + Success -->|是| BuildSuccess[构建成功响应
success=true
包含count] + Success -->|否| BuildError[构建错误响应
success=false
error=错误消息] + + BuildSuccess --> Return([返回响应]) + BuildError --> LogError[console.error
Delete error] + LogError --> Return +``` + +**关键代码**: + +```typescript +ipcMain.handle( + 'materials:delete', + async (_event, request: MaterialDeleteRequest): Promise => { + try { + const dao = new MaterialsToBeDeletedDAO() + const count = await dao.deleteByMaterialCodes(request.materialCodes) + + return { + success: true, + count + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + console.error('[Materials] Delete error:', error) + return { + success: false, + error: `Delete failed: ${message}` + } + } + } +) +``` + +--- + +### 4. 数据库DAO层 (materials-to-be-deleted-dao.ts) + +#### 4.1 upsertBatch 方法 + +**位置**: `src/main/services/database/materials-to-be-deleted-dao.ts:180-240` + +```mermaid +flowchart TB + Start([upsertBatch调用]) --> CheckEmpty{materials
数组为空?} + CheckEmpty -->|是| ReturnEmpty[返回空stats
total=0] + CheckEmpty -->|否| InitStats[初始化stats
total=materials.length] + + InitStats --> GetDB[获取数据库服务
getDatabaseService] + GetDB --> GetTable[获取表名
getTableName] + + GetTable --> Loop[遍历materials数组] + Loop --> TrimData[trim清理数据
materialCode
managerName] + + TrimData --> ValidateCode{materialCode
为空?} + ValidateCode -->|是| IncrementFailed[stats.failed++
continue] + ValidateCode -->|否| CheckDB{检查dbType} + + CheckDB -->|sqlserver| MergeSQL[执行MERGE语句
INSERT ON DUPLICATE UPDATE] + CheckDB -->|其他| InsertSQL[执行INSERT...
ON DUPLICATE KEY UPDATE] + + MergeSQL --> DBSuccess{执行成功?} + InsertSQL --> DBSuccess + + DBSuccess -->|是| IncrementSuccess[stats.success++] + DBSuccess -->|否| LogError[console.error
stats.failed++] + + IncrementSuccess --> NextItem{还有物料?} + IncrementFailed --> NextItem + LogError --> NextItem + + NextItem -->|是| Loop + NextItem -->|否| ReturnStats[返回stats] + + ReturnEmpty --> End([结束]) + ReturnStats --> End +``` + +**SQL差异对比**: + +**MySQL (使用 INSERT ... ON DUPLICATE KEY UPDATE)**: + +```sql +INSERT INTO dbo_MaterialsToBeDeleted (MaterialCode, ManagerName) +VALUES (?, ?) +ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) +``` + +**SQL Server (使用 MERGE)**: + +```sql +MERGE [dbo].[MaterialsToBeDeleted] AS target +USING (VALUES (@materialCode, @managerName)) AS source (MaterialCode, ManagerName) +ON target.MaterialCode = source.MaterialCode +WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName +WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); +``` + +**核心代码**: + +```typescript +async upsertBatch(materials: { materialCode: string; managerName: string }[]): Promise { + if (!materials || materials.length === 0) { + return { total: 0, success: 0, failed: 0 } + } + + const stats: UpsertStats = { + total: materials.length, + success: 0, + failed: 0 + } + + try { + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + + for (const material of materials) { + const materialCode = material.materialCode?.trim() + const managerName = material.managerName?.trim() || '' + + if (!materialCode) { + stats.failed++ + continue + } + + try { + if (this.dbType === 'sqlserver') { + // SQL Server: MERGE 语句 + const sqlString = ` + MERGE ${tableName} AS target + USING (VALUES (@materialCode, @managerName)) AS source (MaterialCode, ManagerName) + ON target.MaterialCode = source.MaterialCode + WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName + WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); + ` + + await (dbService as SqlServerService).queryWithParams(sqlString, { + materialCode: { value: materialCode, type: sql.NVarChar }, + managerName: { value: managerName || null, type: sql.NVarChar } + }) + } else { + // MySQL: INSERT ... ON DUPLICATE KEY UPDATE + const sqlString = ` + INSERT INTO ${tableName} (MaterialCode, ManagerName) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) + ` + + await (dbService as MySqlService).query(sqlString, [materialCode, managerName || null]) + } + + stats.success++ + } catch (error) { + console.error('[MaterialsToBeDeletedDAO] Error upserting material:', materialCode, error) + stats.failed++ + } + } + } catch (error) { + console.error('[MaterialsToBeDeletedDAO] Batch upsert error:', error) + stats.failed = stats.total - stats.success + } + + return stats +} +``` + +**处理特点**: + +- **逐条处理**: 不使用批量事务,每条记录单独执行 upsert +- **错误隔离**: 单条记录失败不影响其他记录继续处理 +- **数据清理**: 自动 trim materialCode 和 managerName +- **空值处理**: managerName 为空时使用 NULL +- **统计准确**: 返回成功/失败/总计的详细统计 + +#### 4.2 deleteByMaterialCodes 方法 + +**位置**: `src/main/services/database/materials-to-be-deleted-dao.ts:539-586` + +```mermaid +flowchart TB + Start([deleteByMaterialCodes调用]) --> CheckEmpty{materialCodes
数组为空?} + CheckEmpty -->|是| ReturnZero[返回 0] + CheckEmpty -->|否| InitTotal[初始化
totalDeleted=0
batchSize=1000] + + InitTotal --> GetDB[获取数据库服务
getDatabaseService] + GetDB --> GetTable[获取表名
getTableName] + + GetTable --> BatchLoop[分批处理循环
每批1000条] + BatchLoop --> SliceBatch[切片
materialCodes.slice
i, i+batchSize] + + SliceBatch --> CheckDB{检查dbType} + CheckDB -->|sqlserver| BuildSQLServer[构建参数化SQL
@p0, @p1, ...] + CheckDB -->|其他| BuildMySQL[构建占位符SQL
?, ?, ...] + + BuildSQLServer --> ExecSQLServer[执行DELETE...IN] + BuildMySQL --> ExecMySQL[执行DELETE...IN] + + ExecSQLServer --> AddTotal[totalDeleted +=
result.rowCount] + ExecMySQL --> AddTotal + + AddTotal --> NextBatch{还有批次?} + NextBatch -->|是| BatchLoop + NextBatch -->|否| ReturnTotal[返回totalDeleted] + + ReturnZero --> End([结束]) + ReturnTotal --> End +``` + +**批处理机制**: + +```typescript +// 每批处理1000条记录 +const batchSize = 1000 + +for (let i = 0; i < materialCodes.length; i += batchSize) { + const batch = materialCodes.slice(i, i + batchSize) + // 执行 DELETE ... WHERE MaterialCode IN (...) +} +``` + +**SQL示例**: + +**MySQL**: +```sql +DELETE FROM dbo_MaterialsToDeleted +WHERE MaterialCode IN (?, ?, ?, ..., ?) -- 最多1000个占位符 +``` + +**SQL Server**: +```sql +DELETE FROM [dbo].[MaterialsToBeDeleted] +WHERE MaterialCode IN (@p0, @p1, @p2, ..., @p999) -- 最多1000个参数 +``` + +**核心代码**: + +```typescript +async deleteByMaterialCodes(materialCodes: string[]): Promise { + if (!materialCodes || materialCodes.length === 0) { + return 0 + } + + let totalDeleted = 0 + const batchSize = 1000 + + try { + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + + for (let i = 0; i < materialCodes.length; i += batchSize) { + const batch = materialCodes.slice(i, i + batchSize) + + if (this.dbType === 'sqlserver') { + const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} + + batch.forEach((code, idx) => { + params[`p${idx}`] = { value: code.trim(), type: sql.NVarChar } + }) + + const sqlString = ` + DELETE FROM ${tableName} + WHERE MaterialCode IN (${placeholders}) + ` + + const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) + totalDeleted += result.rowCount + } else { + const placeholders = batch.map(() => '?').join(',') + + const sqlString = ` + DELETE FROM ${tableName} + WHERE MaterialCode IN (${placeholders}) + ` + + const result = await (dbService as MySqlService).query(sqlString, batch) + totalDeleted += result.rowCount + } + } + } catch (error) { + console.error('[MaterialsToBeDeletedDAO] Delete by material codes error:', error) + } + + return totalDeleted +} +``` + +**处理特点**: + +- **批量删除**: 使用 IN 子句一次删除最多1000条记录 +- **自动分批**: 超过1000条自动分批处理 +- **数据清理**: 自动 trim 每个物料代码 +- **累加统计**: 累加每批删除的记录数 +- **错误容错**: 发生错误返回已删除的数量,不抛出异常 + +--- + +### 5. 数据流向图 + +```mermaid +graph TB + subgraph 前端["前端 (Renderer Process)"] + Button[确认删除按钮] --> Handler[handleConfirmDeletion函数] + Handler --> Classify[数据分类逻辑] + Classify --> UserConfirm[用户确认对话框] + end + + subgraph Preload["Preload Script"] + UpsertAPI[window.electron.materials.upsertBatch] + DeleteAPI[window.electron.materials.delete] + ManagersAPI[window.electron.materials.getManagers] + end + + subgraph Main["主进程 (Main Process)"] + UpsertIPC[materials:upsertBatch Handler] + DeleteIPC[materials:delete Handler] + ManagersIPC[materials:getManagers Handler] + DAO[MaterialsToBeDeletedDAO] + DB[(Database
MySQL/SQL Server)] + end + + UserConfirm -->|用户确认| UpsertAPI + UserConfirm -->|用户确认| DeleteAPI + UpsertAPI --> UpsertIPC + DeleteAPI --> DeleteIPC + + UpsertIPC --> DAO + DeleteIPC --> DAO + + DAO -->|MERGE/INSERT ON DUPLICATE| DB + DAO -->|DELETE WHERE IN| DB + + DB --> DAO + DAO --> UpsertIPC + DAO --> DeleteIPC + + UpsertIPC --> UpsertAPI + DeleteIPC --> DeleteAPI + + UpsertAPI --> Handler + DeleteAPI --> Handler + + Handler -->|刷新负责人列表| ManagersAPI + ManagersAPI --> ManagersIPC + ManagersIPC --> DAO + DAO --> DB + DB --> DAO + DAO --> ManagersIPC + ManagersIPC --> ManagersAPI + ManagersAPI --> Handler + + Handler -->|更新UI| Alert[显示操作结果] +``` + +--- + +### 6. 关键数据结构 + +#### 6.1 MaterialUpsertBatchRequest (IPC输入) + +```typescript +interface MaterialUpsertBatchRequest { + materials: { + materialCode: string // 物料代码 + managerName: string // 负责人名称 + }[] +} +``` + +#### 6.2 MaterialDeleteRequest (IPC输入) + +```typescript +interface MaterialDeleteRequest { + materialCodes: string[] // 要删除的物料代码数组 +} +``` + +#### 6.3 MaterialOperationResponse (IPC输出) + +```typescript +interface MaterialOperationResponse { + success: boolean + stats?: UpsertStats // upsert操作返回 + count?: number // delete操作返回 + error?: string +} +``` + +#### 6.4 UpsertStats (操作统计) + +```typescript +interface UpsertStats { + total: number // 总处理数量 + success: number // 成功数量 + failed: number // 失败数量 +} +``` + +--- + +### 7. 错误处理流程 + +```mermaid +flowchart TB + Try([try-catch块开始]) --> Exec[执行数据库操作] + + Exec --> CatchUpsert{upsert操作
发生异常?} + CatchUpsert -->|是| LogUpsert[console.error
stats.failed++] + LogUpsert --> ContinueUpsert[继续处理下一条] + CatchUpsert -->|否| CheckDelete{delete操作
发生异常?} + + ContinueUpsert --> CheckDelete + + CheckDelete -->|是| LogDelete[console.error
返回已删除数量] + CheckDelete -->|否| CheckIPC{IPC层
发生异常?} + + LogDelete --> CheckIPC + + CheckIPC -->|是| BuildIPCError[构建错误响应
success=false
error=错误消息] + CheckIPC -->|否| CheckFront{前端try-catch
发生异常?} + + BuildIPCError --> ReturnIPC[返回响应给前端] + + CheckFront -->|是| ShowFrontError[alert错误消息] + CheckFront -->|否| Finally[finally块] + + ShowFrontError --> Finally + ReturnIPC --> ShowFrontError + Finally --> SetRunningFalse[setIsRunning=false
结束] +``` + +**常见错误场景**: + +1. **缺少负责人信息**: + - 场景: 已勾选的物料没有 managerName + - 错误信息: "以下已勾选的记录缺少负责人信息,无法保存:" + - 处理: 阻止操作,弹出警告,列出前10条缺失的物料代码 + +2. **Upsert操作失败**: + - 场景: 数据库连接断开、约束冲突、权限不足 + - 错误信息: "Upsert failed: ${message}" + - 处理: 记录到 stats.failed,继续处理下一条 + +3. **Delete操作失败**: + - 场景: 数据库连接断开、表锁定、权限不足 + - 错误信息: "Delete failed: ${message}" + - 处理: 返回已删除的数量,不抛出异常 + +4. **网络/IPC通信失败**: + - 场景: 主进程未响应、IPC通道关闭 + - 错误信息: 具体的错误消息 + - 处理: catch块捕获,alert显示错误信息 + +--- + +### 8. 与"获取校验状态"流程的对比 + +| 对比维度 | 获取校验状态 | 确认删除(同步数据库) | +|---------|------------|-------------------| +| **操作方向** | 数据库 → 前端 (读取) | 前端 → 数据库 (写入) | +| **主要操作** | SELECT 查询 | MERGE/INSERT + DELETE | +| **数据量** | 可能很大(全表查询) | 取决于用户勾选数量 | +| **事务性** | 只读,无需事务 | 写操作,逐条处理 | +| **用户交互** | 单次点击 | 点击 → 确认对话框 → 执行 | +| **错误处理** | 整体失败或成功 | 部分失败继续处理 | +| **结果反馈** | 校验结果列表 | 操作统计(成功/删除条数) | +| **副作用** | 无 | 修改数据库内容 | +| **权限要求** | 读取权限 | 写入+删除权限 | + +--- + ## 文件索引 | 文件路径 | 说明 | 关键行号 | |---------|------|---------| -| `src/renderer/src/pages/CleanerPage.tsx` | 前端清理页面 | 117-155 (handleValidation) | -| `src/main/ipc/validation-handler.ts` | IPC处理器 | 209-392 (validation:validate) | +| `src/renderer/src/pages/CleanerPage.tsx` | 前端清理页面 | 117-155 (handleValidation)
166-226 (handleConfirmDeletion) | +| `src/main/ipc/validation-handler.ts` | IPC处理器 | 209-392 (validation:validate)
399-422 (materials:upsertBatch)
427-449 (materials:delete) | | `src/main/services/database/discrete-material-plan-dao.ts` | 物料计划DAO | 191-227 (queryAllDistinctByMaterialCode) | | `src/main/services/database/discrete-material-plan-dao.ts` | 物料计划DAO | 294-377 (queryBySourceNumbersDistinct) | -| `src/main/services/database/materials-to-be-deleted-dao.ts` | 待删除物料DAO | 248-268 (getAllMaterialCodes) | +| `src/main/services/database/materials-to-be-deleted-dao.ts` | 待删除物料DAO | 180-240 (upsertBatch)
248-268 (getAllMaterialCodes)
539-586 (deleteByMaterialCodes) | ---