diff --git a/docs/cleaner-validation-flow.md b/docs/cleaner-validation-flow.md new file mode 100644 index 0000000..0182b30 --- /dev/null +++ b/docs/cleaner-validation-flow.md @@ -0,0 +1,531 @@ +# 清理界面 - 获取校验状态流程分析文档 + +**文档版本**: 1.0 +**创建日期**: 2026-03-02 +**面向对象**: 开发人员 + +## 概述 + +本文档详细分析了在清理界面(CleanerPage)中,用户点击"获取并校验物料状态"按钮后,程序的完整运行逻辑,包括前端交互、IPC通信、后端处理和数据库交互。 + +--- + +## 核心流程概览 + +```mermaid +flowchart TB + Start([用户点击
获取并校验物料状态]) --> UI1[设置状态
isValidationRunning=true] + UI1 --> UI2["清空现有数据
validationResults=empty
selectedItems=empty"] + UI2 --> IPC[调用IPC
window.electron.validation.validate] + + IPC --> Handler{validation-handler.ts
validation:validate} + Handler --> DB[连接数据库
MySQL/SQL Server] + DB --> ModeCheck{校验模式?} + + ModeCheck -->|database_full| FullQuery[查询DiscreteMaterialPlanData
全表去重查询
queryAllDistinctByMaterialCode] + ModeCheck -->|database_filtered| FilterCheck{useSharedProductionIds?} + + FilterCheck -->|true| SharedIds[获取共享Production IDs
getSharedProductionIds] + FilterCheck -->|false| FileCheck[productionIdFile存在?] + SharedIds --> SourceQuery[查询生产订单号
getSourceNumbersFromInputs] + FileCheck -->|true| SourceQuery + FileCheck -->|false| Error1[返回错误
没有可用Production ID] + + FullQuery --> TypeQuery[查询类型关键词
MaterialsTypeToBeDeleted] + SourceQuery --> TypeQuery + + TypeQuery --> MarkedQuery[查询已标记物料
MaterialsToBeDeleted] + MarkedQuery --> Match[物料匹配算法
优先级匹配] + Match --> Return[返回ValidationResponse
success/results/stats] + + Error1 --> Return + Return --> UI3[前端接收响应] + UI3 --> SuccessCheck{success?} + + SuccessCheck -->|true| UI4["更新validationResults
自动勾选已标记物料"] + UI4 --> UI5[管理员更新负责人列表] + UI5 --> UI6[设置isValidationRunning=false] + UI6 --> End([流程结束]) + + SuccessCheck -->|false| Error2[显示错误弹窗
alert error] + Error2 --> UI6 +``` + +--- + +## 详细流程分解 + +### 1. 前端交互层 (CleanerPage.tsx) + +**触发位置**: `src/renderer/src/pages/CleanerPage.tsx:117-155` + +```mermaid +sequenceDiagram + participant User as 用户 + participant UI as CleanerPage + participant IPC as window.electron.validation + + User->>UI: 点击"获取并校验物料状态"按钮 + UI->>UI: setIsValidationRunning(true) + UI->>UI: 清空 validationResults, selectedItems, hiddenItems + UI->>IPC: validate({ mode, useSharedProductionIds }) + + Note over UI,IPC: 请求参数:
- mode: 'database_full' | 'database_filtered'
- useSharedProductionIds: boolean + + IPC-->>UI: ValidationResponse + alt success=true + UI->>UI: setValidationResults(results) + UI->>UI: 自动勾选已标记物料 + UI->>UI: 管理员更新负责人列表 + else success=false + UI->>User: alert(error) + end + UI->>UI: setIsValidationRunning(false) +``` + +**关键代码逻辑**: + +```typescript +const handleValidation = async () => { + setIsValidationRunning(true) + setValidationResults([]) + setSelectedItems(new Set()) + setHiddenItems(new Set()) + + try { + const response = await window.electron.validation.validate({ + mode: valMode === 'full' ? 'database_full' : 'database_filtered', + useSharedProductionIds: valMode === 'filtered' + }) + + if (response.success && response.results) { + setValidationResults(response.results) + // 自动勾选已标记物料 + const markedCodes = new Set( + response.results + .filter(r => r.isMarkedForDeletion) + .map(r => r.materialCode) + ) + setSelectedItems(markedCodes) + + // 管理员更新负责人列表 + if (isAdmin) { + const uniqueManagers = new Set( + response.results + .map(r => r.managerName) + .filter(Boolean) + ) + setManagers([...uniqueManagers]) + setSelectedManagers(uniqueManagers) + } + } + } finally { + setIsValidationRunning(false) + } +} +``` + +--- + +### 2. IPC Handler层 (validation-handler.ts) + +**处理位置**: `src/main/ipc/validation-handler.ts:209-392` + +```mermaid +flowchart TB + subgraph IPC_Handler["IPC Handler: validation:validate"] + Start([接收请求]) --> Log[console.log开始校验] + Log --> Connect[getValidationDatabaseService
连接MySQL/SQLServer] + Connect --> CheckMode{检查mode参数} + + CheckMode -->|database_full| FullMode[全表校验分支] + CheckMode -->|database_filtered| FilterMode[过滤校验分支] + + FullMode --> QueryMaterial[DiscreteMaterialPlanDAO
queryAllDistinctByMaterialCode] + + FilterMode --> CheckShared{useSharedProductionIds?} + CheckShared -->|true| GetShared[getSharedProductionIds
获取共享ID] + CheckShared -->|false| CheckFile{productionIdFile?} + CheckFile -->|true| ReadFile[readProductionIds
读取文件] + CheckFile -->|false| ReturnError1[返回错误] + + GetShared --> GetSource[getSourceNumbersFromInputs
查询生产订单号] + ReadFile --> GetSource + + QueryMaterial --> QueryType[查询MaterialsTypeToBeDeleted
获取类型关键词] + GetSource --> QueryType + + QueryType --> QueryMarked[查询MaterialsToBeDeleted
获取已标记物料] + QueryMarked --> Match[物料匹配算法] + Match --> BuildResponse[构建ValidationResponse] + BuildResponse --> Disconnect[断开数据库连接] + Disconnect --> ReturnResponse[返回响应] + ReturnError1 --> ReturnResponse + end +``` + +--- + +### 3. 数据库交互层 + +#### 3.1 数据库连接与类型选择 + +**位置**: `validation-handler.ts:55-83` + +```mermaid +flowchart LR + Start([getValidationDatabaseService]) --> CheckEnv{检查DB_TYPE环境变量} + CheckEnv -->|sqlserver/mssql| SQLServer[创建SqlServerService
使用mssql驱动] + CheckEnv -->|其他| MySQL[创建MySqlService
使用mysql2驱动] + + SQLServer --> ConnectSQL[connect
连接SQL Server] + MySQL --> ConnectMySQL[connect
连接MySQL] + + ConnectSQL --> ReturnSQL[返回SqlServerService实例] + ConnectMySQL --> ReturnMySQL[返回MySqlService实例] +``` + +**表名转换逻辑**: + +```typescript +// MySQL: dbo_MaterialsToBeDeleted +// SQL Server: [dbo].[MaterialsToBeDeleted] +function getTableName(mysqlTableName: string): string { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + // 找到第一个下划线分割schema和表名 + const firstUnderscoreIndex = mysqlTableName.indexOf('_') + if (firstUnderscoreIndex > 0) { + const schema = mysqlTableName.substring(0, firstUnderscoreIndex) + const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1) + return `[${schema}].[${tableName}]` + } + return `[dbo].[${mysqlTableName}]` + } + return mysqlTableName +} +``` + +#### 3.2 核心查询流程 + +```mermaid +sequenceDiagram + participant Handler as IPC Handler + participant DAO as DiscreteMaterialPlanDAO + participant DB as Database(MySQL/SQLServer) + participant TypeDAO as MaterialsTypeToBeDeleted + participant MarkedDAO as MaterialsToBeDeleted + + Handler->>DAO: 查询物料计划数据 + Note over DAO,DB: 根据mode决定查询方式 + + alt 全表模式 (database_full) + DAO->>DB: queryAllDistinctByMaterialCode() + DB-->>DAO: 全表去重数据(按MaterialCode) + else 过滤模式 (database_filtered) + DAO->>DB: queryBySourceNumbersDistinct(sourceNumbers) + DB-->>DAO: 按订单号过滤去重数据 + end + + Handler->>TypeDAO: 查询类型关键词 + TypeDAO->>DB: SELECT MaterialName, ManagerName
FROM MaterialsTypeToBeDeleted + DB-->>TypeDAO: 类型关键词列表 + TypeDAO-->>Handler: typeKeywords[] + + Handler->>MarkedDAO: 查询已标记物料 + MarkedDAO->>DB: SELECT MaterialCode, ManagerName
FROM MaterialsToBeDeleted + DB-->>MarkedDAO: 已标记物料字典 + MarkedDAO-->>Handler: markedCodesDict(Map) +``` + +#### 3.3 查询SQL详解 + +**全表去重查询** (MySQL/SQL Server通用): + +```sql +WITH RankedRecords AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM dbo_DiscreteMaterialPlanData + WHERE MaterialCode IS NOT NULL +) +SELECT + Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, + ProductCode, ProductName, ProductUnit, ProductPlanQuantity, + UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, + SequenceNumber, MaterialCode, MaterialName, Specification, Model, + DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, + Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion +FROM RankedRecords +WHERE rn = 1 +``` + +**按订单号过滤查询** (批量处理,每批2000条): + +```sql +WITH RankedRecords AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM dbo_DiscreteMaterialPlanData + WHERE SourceNumber IN (?, ?, ...) -- 批量占位符 + AND MaterialCode IS NOT NULL +) +SELECT [字段列表] +FROM RankedRecords +WHERE rn = 1 +``` + +--- + +### 4. 物料匹配算法 + +**位置**: `validation-handler.ts:325-361` + +```mermaid +flowchart TB + Start([开始物料匹配]) --> Loop[遍历materialRecords] + Loop --> Extract[提取物料信息
materialName, materialCode
specification, model] + + Extract --> Priority1{优先级1:
MaterialsToBeDeleted
精确匹配?} + + Priority1 -->|materialCode
在markedCodesDict中| SetMarked[设置managerName
isMarkedForDeletion=true] + Priority1 -->|未匹配| Priority2{优先级2:
MaterialsTypeToBeDeleted
名称包含匹配?} + + SetMarked --> PushResult[添加到results] + Priority2 -->|遍历typeKeywords| CheckContains{typeKeyword.materialName
包含 materialName?} + + CheckContains -->|是| SetMatched[设置managerName
matchedTypeKeyword
isMarkedForDeletion=false] + CheckContains -->|否| SetNull[managerName=null
isMarkedForDeletion=false] + + SetMatched --> PushResult + SetNull --> PushResult + + PushResult --> Next{还有物料?} + Next -->|是| Loop + Next -->|否| Stats[计算统计数据
totalRecords, matchedCount, markedCount] + Stats --> Return([返回ValidationResponse]) +``` + +**匹配优先级**: + +1. **优先级1 (最高)**: `MaterialsToBeDeleted` 表精确匹配 + - 匹配条件: `MaterialCode` 完全相等 + - 结果: `isMarkedForDeletion = true`, `managerName` 从表中获取 + +2. **优先级2 (次高)**: `MaterialsTypeToBeDeleted` 表包含匹配 + - 匹配条件: `MaterialName` 包含关系 (`typeKeyword.materialName.includes(materialName)`) + - 结果: `isMarkedForDeletion = false`, `managerName` 从表中获取, `matchedTypeKeyword` 记录匹配项 + +3. **未匹配**: 无任何匹配 + - 结果: `isMarkedForDeletion = false`, `managerName = ''`, `matchedTypeKeyword = undefined` + +**核心代码**: + +```typescript +for (const record of materialRecords) { + const materialName = (record.MaterialName as string) || '' + const materialCode = (record.MaterialCode as string) || '' + const specification = (record.Specification as string) || '' + const model = (record.Model as string) || '' + + // 优先级1: 检查 MaterialsToBeDeleted (MaterialCode 精确匹配) + let managerName = markedCodesDict.get(materialCode) || null + const isMarkedForDeletion = managerName !== null + let matchedTypeKeyword: string | undefined = undefined + + // 优先级2: 匹配 MaterialsTypeToBeDeleted (MaterialName 包含匹配) + if (!managerName) { + for (const typeKeyword of typeKeywords) { + if ( + typeKeyword.materialName && + typeKeyword.materialName.includes(materialName) + ) { + matchedTypeKeyword = typeKeyword.materialName + managerName = typeKeyword.managerName + break + } + } + } + + results.push({ + materialName, + materialCode, + specification, + model, + managerName: managerName || '', + isMarkedForDeletion, + matchedTypeKeyword + }) +} +``` + +--- + +### 5. 数据流向图 + +```mermaid +graph TB + subgraph 前端["前端 (Renderer Process)"] + Button[获取并校验按钮] --> Handler[handleValidation函数] + end + + subgraph Preload["Preload Script"] + API[window.electron.validation.validate] + end + + subgraph Main["主进程 (Main Process)"] + IPC[validation:validate Handler] + DAO1[DiscreteMaterialPlanDAO] + DAO2[MaterialsToBeDeletedDAO] + DB[(Database
MySQL/SQL Server)] + end + + Handler -->|IPC调用| API + API --> IPC + IPC -->|查询物料数据| DAO1 + IPC -->|查询已标记| DAO2 + DAO1 --> DB + DAO2 --> DB + DB --> DAO1 + DB --> DAO2 + DAO1 --> IPC + DAO2 --> IPC + IPC --> API + API --> Handler + Handler -->|更新UI| State[validationResults State] +``` + +--- + +### 6. 关键数据结构 + +#### 6.1 ValidationRequest (IPC输入) + +```typescript +interface ValidationRequest { + mode: 'database_full' | 'database_filtered' + useSharedProductionIds?: boolean + productionIdFile?: string // 可选,文件路径 +} +``` + +#### 6.2 ValidationResponse (IPC输出) + +```typescript +interface ValidationResponse { + success: boolean + results?: ValidationResult[] + stats?: { + totalRecords: number + matchedCount: number // 有负责人(包括类型匹配) + markedCount: number // 已标记删除 + } + error?: string +} +``` + +#### 6.3 ValidationResult (单个物料结果) + +```typescript +interface ValidationResult { + materialName: string + materialCode: string + specification: string + model: string + managerName: string // 负责人名称 + isMarkedForDeletion: boolean // 是否精确匹配MaterialsToBeDeleted + matchedTypeKeyword?: string // 如果匹配了类型关键词,记录匹配项 +} +``` + +--- + +### 7. 错误处理流程 + +```mermaid +flowchart TB + Try([try-catch块开始]) --> Exec[执行数据库操作] + Exec --> Catch{发生异常?} + + Catch -->|是| LogError[console.error错误信息] + LogError --> BuildError[构建错误响应
success=false
error=错误消息] + BuildError --> Finally[finally块] + + Catch -->|否| Finally + + Finally --> CheckDB{数据库连接存在?} + CheckDB -->|是| Disconnect[断开连接
dbService.disconnect] + CheckDB -->|否| ReturnResponse[返回响应] + Disconnect --> ReturnResponse +``` + +**常见错误场景**: + +1. **没有共享Production ID**: + - 场景: `mode='database_filtered'` 且 `useSharedProductionIds=true`,但共享ID为空 + - 错误信息: "没有可用的共享 Production ID。请在数据提取页面输入 Production ID。" + +2. **没有找到物料记录**: + - 场景: 数据库查询返回空结果 + - 错误信息: "No material records found" + +3. **数据库连接失败**: + - 场景: 数据库服务未启动、配置错误 + - 错误信息: 具体的数据库错误消息 + +--- + +## 文件索引 + +| 文件路径 | 说明 | 关键行号 | +|---------|------|---------| +| `src/renderer/src/pages/CleanerPage.tsx` | 前端清理页面 | 117-155 (handleValidation) | +| `src/main/ipc/validation-handler.ts` | IPC处理器 | 209-392 (validation:validate) | +| `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) | + +--- + +## 附录: 共享Production IDs机制 + +**用途**: 在数据提取页面和清理页面之间共享订单号列表 + +**存储位置**: `validation-handler.ts:28-43` (内存Set) + +**相关IPC接口**: + +- `validation:setSharedProductionIds`: 设置共享ID (提取页面调用) +- `validation:getSharedProductionIds`: 获取共享ID (清理页面调用) + +**流程**: + +```mermaid +sequenceDiagram + participant Extractor as 数据提取页面 + participant IPC as validation-handler + participant Cleaner as 清理页面 + participant Memory as sharedProductionIds
内存Set + + Extractor->>IPC: setSharedProductionIds([...]) + IPC->>Memory: 存储到内存Set + + Note over Cleaner: 页面加载时 + Cleaner->>IPC: getSharedProductionIds() + IPC->>Memory: 读取内存Set + Memory-->>IPC: productionIds[] + IPC-->>Cleaner: { productionIds } + Cleaner->>Cleaner: 显示Production IDs数量 +``` + +--- + +**文档结束**