From 6698d82d6b4e7d70e62c9740319e44b14371ffab Mon Sep 17 00:00:00 2001 From: Misaka Date: Tue, 3 Mar 2026 19:01:48 +0800 Subject: [PATCH] docs: update extractor-start-button-flow.md to match current implementation - Update version to 1.1 with new related files list - Fix sequence diagram step numbers and add IPC serialization note - Add note about progress state not receiving backend updates - Add code references for real-time sync useEffect and preload API - Update summary with custom error types and sessionStorage details - Add new section for known limitations and pending features - Document progress update and file merge as unimplemented Co-Authored-By: Claude (glm-5) --- docs/extractor-start-button-flow.md | 365 ++++++++++++++++++++-------- 1 file changed, 267 insertions(+), 98 deletions(-) diff --git a/docs/extractor-start-button-flow.md b/docs/extractor-start-button-flow.md index e889969..5544c87 100644 --- a/docs/extractor-start-button-flow.md +++ b/docs/extractor-start-button-flow.md @@ -1,14 +1,16 @@ # 数据提取界面 - 开始按钮工作流程详解 -> **文档版本**: 1.0 -> **创建日期**: 2026-03-03 +> **文档版本**: 1.1 +> **更新日期**: 2026-03-03 > **适用范围**: ERPAuto v1.0+ > **相关文件**: > - `src/renderer/src/pages/ExtractorPage.tsx` (UI层) +> - `src/preload/index.ts` (IPC API 暴露) > - `src/main/ipc/extractor-handler.ts` (IPC处理层) > - `src/main/services/erp/extractor.ts` (业务逻辑层) > - `src/main/services/erp/order-resolver.ts` (订单号解析服务) > - `src/main/services/erp/erp-auth.ts` (ERP认证服务) +> - `src/main/types/extractor.types.ts` (类型定义) ## 目录 @@ -18,6 +20,7 @@ 4. [错误处理机制](#错误处理机制) 5. [数据流转过程](#数据流转过程) 6. [关键代码引用](#关键代码引用) +7. [已知限制与待实现功能](#已知限制与待实现功能) --- @@ -160,40 +163,39 @@ sequenceDiagram loop 批处理循环 (每批最多100个订单) Extractor->>Extractor: 31. 创建批次
(按batchSize分组) - Extractor->>UI: 32. 发送进度更新
onProgress(message, progress%) - UI->>UI: 33. 更新进度条和日志 + Note over Extractor: onProgress回调存在但
无法通过IPC传递(函数不可序列化) - Extractor->>ERP: 34. 填充订单号到搜索框 - Extractor->>ERP: 35. 点击搜索按钮 - Extractor->>ERP: 36. 等待加载完成 - Extractor->>ERP: 37. 点击第一行复选框 - Extractor->>ERP: 38. 悬停并点击"更多" - Extractor->>ERP: 39. 点击"输出" - Extractor->>ERP: 40. 设置行数阈值为300000 - Extractor->>ERP: 41. 点击"确定(Y)" + Extractor->>ERP: 32. 填充订单号到搜索框 + Extractor->>ERP: 33. 点击搜索按钮 + Extractor->>ERP: 34. 等待加载完成 + Extractor->>ERP: 35. 点击第一行复选框 + Extractor->>ERP: 36. 悬停并点击"更多" + Extractor->>ERP: 37. 点击"输出" + Extractor->>ERP: 38. 设置行数阈值为300000 + Extractor->>ERP: 39. 点击"确定(Y)" - Browser->>Browser: 42. 监听下载事件 - ERP->>Browser: 43. 触发文件下载 - Browser->>Browser: 44. 保存文件到downloads目录 - Browser-->>Extractor: 45. 返回文件路径 - Extractor->>Extractor: 46. 记录下载文件路径 + Browser->>Browser: 40. 监听下载事件 + ERP->>Browser: 41. 触发文件下载 + Browser->>Browser: 42. 保存文件到downloads目录 + Browser-->>Extractor: 43. 返回文件路径 + Extractor->>Extractor: 44. 记录下载文件路径 end - Extractor->>Extractor: 47. 汇总结果
(文件列表, 记录数, 错误) - Extractor-->>Handler: 48. 返回ExtractorResult - Handler->>Handler: 49. 添加解析警告到错误列表 + Extractor->>Extractor: 45. 汇总结果
(文件列表, 记录数, 错误) + Extractor-->>Handler: 46. 返回ExtractorResult + Handler->>Handler: 47. 添加解析警告到错误列表 Note over Handler,IPC: 清理阶段 - Handler->>Browser: 50. 关闭浏览器 - Handler->>MySQL: 51. 断开数据库连接 + Handler->>Browser: 48. 关闭浏览器 + Handler->>MySQL: 49. 断开数据库连接 Note over Handler,UI: 响应阶段 - Handler-->>IPC: 52. 返回IPC响应
(success: true, data: result) - IPC-->>UI: 53. 返回response - UI->>UI: 54. 设置result状态 - UI->>UI: 55. 设置isRunning=false - UI->>UI: 56. 清空进度状态 - UI->>User: 57. 显示提取结果
(文件数, 记录数, 错误数) + Handler-->>IPC: 50. 返回IPC响应
(success: true, data: result) + IPC-->>UI: 51. 返回response + UI->>UI: 52. 设置result状态 + UI->>UI: 53. 设置isRunning=false + UI->>UI: 54. 清空进度状态 + UI->>User: 55. 显示提取结果
(文件数, 记录数, 错误数) alt 发生任何错误 Handler-->>UI: 返回error响应 @@ -272,11 +274,13 @@ stateDiagram-v2 | `orderNumbers` | string | 用户输入的订单号列表 | ✅ sessionStorage | | `batchSize` | number | 每批处理的订单数量 (默认100) | ✅ sessionStorage | | `isRunning` | boolean | 是否正在执行提取 | ❌ 内存状态 | -| `progress` | ExtractorProgress \| null | 当前进度信息 | ❌ 内存状态 | +| `progress` | ExtractorProgress \| null | 当前进度信息 (当前实现中未从后端接收) | ❌ 内存状态 | | `result` | ExtractorResult \| null | 提取结果 | ❌ 内存状态 | | `error` | string \| null | 错误信息 | ❌ 内存状态 | | `logs` | string[] | 执行日志列表 | ❌ 内存状态 | +> **注意**: `progress` 状态目前未从后端接收实时更新。虽然 `ExtractorService` 内部调用 `onProgress` 回调,但函数无法通过 IPC 序列化传递。后续可通过 IPC 事件通道实现实时进度更新。 + --- ## 错误处理机制 @@ -480,7 +484,7 @@ const handleExtract = async () => { .map((line) => line.trim()) .filter((line) => line.length > 0) - // 4. 存储到共享状态 + // 4. 存储到共享状态 (与Cleaner模块共享) await window.electron.validation.setSharedProductionIds(orderNumberList) // 5. 调用后端API @@ -505,114 +509,213 @@ const handleExtract = async () => { } ``` +### 1.1 订单号实时同步到共享状态 (ExtractorPage.tsx:34-45) + +```typescript +// 当用户输入订单号时,实时同步到共享状态 +useEffect(() => { + sessionStorage.setItem('extractor_orderNumbers', orderNumbers) + // 实时更新共享的 Production IDs + if (orderNumbers.trim()) { + const orderNumberList = orderNumbers + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0) + window.electron.validation.setSharedProductionIds(orderNumberList) + } +}, [orderNumbers]) +``` + +> **设计说明**: 订单号通过两种方式存储到共享状态: +> 1. `useEffect` 在用户输入时实时更新 +> 2. `handleExtract` 在提取开始前再次确认存储 +> +> 这确保了即使用户在Cleaner页面刷新,数据也已同步。 + ### 2. IPC处理器核心逻辑 (extractor-handler.ts:17-154) ```typescript -ipcMain.handle('extractor:run', async (_event, input: ExtractorInput) => { - return withErrorHandling(async () => { - // 1. 环境配置检查 - const erpUrl = process.env.ERP_URL || '' - const erpUsername = process.env.ERP_USERNAME || '' - const erpPassword = process.env.ERP_PASSWORD || '' +ipcMain.handle( + 'extractor:run', + async (_event, input: ExtractorInput): Promise> => { + return withErrorHandling(async () => { + let authService: ErpAuthService | null = null + let mysqlService: MySqlService | null = null - if (!erpUrl || !erpUsername || !erpPassword) { - throw new ValidationError('ERP 配置不完整') - } + try { + // 1. 环境配置检查 + const erpUrl = process.env.ERP_URL || '' + const erpUsername = process.env.ERP_USERNAME || '' + const erpPassword = process.env.ERP_PASSWORD || '' - // 2. 订单号解析 - const mysqlService = new MySqlService(mysqlConfig) - await mysqlService.connect() + if (!erpUrl || !erpUsername || !erpPassword) { + throw new ValidationError('ERP 配置不完整') + } - const resolver = new OrderNumberResolver(mysqlService) - const mappings = await resolver.resolve(input.orderNumbers) - const validOrderNumbers = resolver.getValidOrderNumbers(mappings) + // 2. 连接MySQL并解析订单号 + const mysqlConfig = { /* ... */ } + mysqlService = new MySqlService(mysqlConfig) + await mysqlService.connect() - if (validOrderNumbers.length === 0) { - throw new ValidationError('没有有效的生产订单号可处理') - } + const resolver = new OrderNumberResolver(mysqlService) + const mappings = await resolver.resolve(input.orderNumbers) + const validOrderNumbers = resolver.getValidOrderNumbers(mappings) + const warnings = resolver.getWarnings(mappings) - // 3. ERP登录 - const authService = new ErpAuthService({...}) - await authService.login() + if (validOrderNumbers.length === 0) { + throw new ValidationError('没有有效的生产订单号可处理') + } - // 4. 执行提取 - const extractor = new ExtractorService(authService) - const result = await extractor.extract({ - ...input, - orderNumbers: validOrderNumbers - }) + // 3. ERP登录 + authService = new ErpAuthService({ url, username, password, headless: true }) + await authService.login() - // 5. 资源清理 - await authService.close() - await mysqlService.disconnect() + // 4. 执行提取 + const extractor = new ExtractorService(authService) + const result = await extractor.extract({ + ...input, + orderNumbers: validOrderNumbers + }) - return result - }, 'extractor:run') -}) + // 5. 添加警告到结果 + if (warnings.length > 0) { + result.errors = [...warnings, ...result.errors] + } + + return result + } finally { + // 6. 资源清理 + if (authService) await authService.close() + if (mysqlService) await mysqlService.disconnect() + } + }, 'extractor:run') + } +) ``` -### 3. 提取服务批处理逻辑 (extractor.ts:43-67) +### 3. 提取服务批处理逻辑 (extractor.ts:29-77) ```typescript -// 批处理循环 -const batches = this.createBatches(input.orderNumbers, batchSize) - -for (let i = 0; i < batches.length; i++) { - const batch = batches[i] - const progress = ((i + 1) / batches.length) * 100 - - // 发送进度更新 - input.onProgress?.(`Processing batch ${i + 1}/${batches.length}`, progress) +async extract(input: ExtractorInput): Promise { + const result: ExtractorResult = { + downloadedFiles: [], + mergedFile: null, + recordCount: 0, + errors: [] + } try { - const filePath = await this.downloadBatch( - session, popupPage, workFrame, batch, i, batches.length - ) - result.downloadedFiles.push(filePath) + const session = this.authService.getSession() + + // 导航到提取页面并获取工作框架 + const { popupPage, workFrame } = await this.navigateToExtractorPage(session) + + // 批处理设置 + const batchSize = input.batchSize || 100 + const batches = this.createBatches(input.orderNumbers, batchSize) + + for (let i = 0; i < batches.length; i++) { + const batch = batches[i] + const progress = ((i + 1) / batches.length) * 100 + + // 注意: onProgress 回调存在但无法通过 IPC 传递 + // 后续可通过 IPC 事件通道实现实时进度 + input.onProgress?.(`Processing batch ${i + 1}/${batches.length}`, progress) + + try { + const filePath = await this.downloadBatch( + session, popupPage, workFrame, batch, i, batches.length + ) + result.downloadedFiles.push(filePath) + } catch (error) { + // 单批次失败不影响其他批次 + result.errors.push(`Batch ${i + 1}: ${error.message}`) + } + } + + // TODO: 合并文件功能待实现 } catch (error) { - // 记录错误但继续处理 - result.errors.push(`Batch ${i + 1}: ${error.message}`) + result.errors.push(`Extraction failed: ${error.message}`) } + + return result } ``` ### 4. 浏览器自动化单批次处理 (extractor.ts:151-194) ```typescript -private async downloadBatch(...): Promise { - // 1. 填充订单号 +private async downloadBatch( + session: ErpSession, + popupPage: any, + workFrame: any, + orderNumbers: string[], + batchIndex: number, + totalBatches: number +): Promise { + // 1. 清空并填充订单号 const textbox = workFrame.getByRole('textbox', { name: '来源生产订单号' }) + await textbox.fill('') await textbox.fill(orderNumbers.join(',')) - // 2. 点击搜索 + // 2. 点击搜索按钮 await workFrame.locator('.search-component-searchBtn').click() - // 3. 等待加载 + // 3. 等待加载完成 await this.waitForLoading(workFrame) - // 4. 选择第一行 + // 4. 选择第一行(全选) await workFrame.getByRole('row', { name: '序号' }).getByLabel('').click() - // 5. 点击更多 -> 输出 + // 5. 悬停"更多"按钮并点击"输出" await workFrame.getByRole('button', { name: '更多' }).hover() await workFrame.getByText('输出', { exact: true }).click() - // 6. 设置阈值 + // 6. 设置行数阈值 + const thresholdBox = workFrame + .locator('div') + .filter({ hasText: /^行数阈值$/ }) + .locator('input[type="text"]') await thresholdBox.fill('300000') - // 7. 等待下载 + // 7. 等待下载并保存 + const downloadPath = path.join(this.downloadDir, `temp_batch_${batchIndex + 1}.xlsx`) const downloadPromise = popupPage.waitForEvent('download') await workFrame.getByRole('button', { name: '确定(Y)' }).click() - const download = await downloadPromise - // 8. 保存文件 - const downloadPath = path.join(this.downloadDir, `temp_batch_${batchIndex + 1}.xlsx`) + const download = await downloadPromise await download.saveAs(downloadPath) return downloadPath } ``` +### 5. Preload API 暴露 (preload/index.ts:24-26) + +```typescript +// Extractor service +extractor: { + runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input) +} +``` + +### 6. 类型定义 (types/extractor.types.ts) + +```typescript +export interface ExtractorInput { + orderNumbers: string[] + batchSize?: number + onProgress?: (message: string, progress: number) => void // 注意: 函数无法通过IPC传递 +} + +export interface ExtractorResult { + downloadedFiles: string[] + mergedFile: string | null + recordCount: number + errors: string[] +} +``` + --- ## 总结 @@ -622,35 +725,101 @@ private async downloadBatch(...): Promise { 1. **三层验证机制**: - 前端验证: 非空检查 - 配置验证: 环境变量完整性 - - 数据验证: 订单号有效性 + - 数据验证: 订单号有效性(通过MySQL查询) 2. **资源管理策略**: - - 使用try-finally确保资源清理 + - 使用 try-finally 确保资源清理 - 浏览器在使用后立即关闭 - 数据库连接在使用后断开 + - 清理操作在 finally 块中独立 try-catch,避免清理失败影响结果返回 3. **错误容错设计**: - 单个批次失败不影响其他批次 - 警告信息独立收集,不影响主流程 - 详细错误信息返回给前端展示 + - 使用自定义错误类型 (`ValidationError`, `DatabaseQueryError`, `ErpConnectionError`) 4. **用户体验优化**: - - sessionStorage持久化用户输入 - - 实时进度反馈 - - 共享状态支持跨页面数据传递 + - sessionStorage 持久化用户输入(`orderNumbers`, `batchSize`) + - 订单号实时同步到共享状态(供 Cleaner 模块使用) - 详细的日志记录 + - 结果面板显示文件数、记录数、错误数 + +### 已知限制 + +1. **进度更新未实现**: + - `ExtractorInput.onProgress` 回调存在但无法通过 IPC 传递 + - 前端 `progress` 状态当前未从后端接收实时更新 + - 后续可通过 IPC 事件通道(`ipcRenderer.on` / `webContents.send`)实现 + +2. **文件合并未实现**: + - `ExtractorResult.mergedFile` 当前始终为 `null` + - 各批次文件独立保存在 `downloads` 目录 ### 性能考虑 - **批处理**: 默认每批100个订单,平衡性能与稳定性 -- **异步并发**: 使用async/await处理异步操作 -- **进度反馈**: 避免长时间无响应的用户体验 +- **异步并发**: 使用 async/await 处理异步操作 +- **下载监听**: 使用 Playwright 事件监听处理文件下载 ### 扩展性 -- **配置化**: batchSize可配置 +- **配置化**: batchSize 可配置 - **模块化**: 服务独立,易于测试和维护 - **错误类型化**: 使用自定义错误类型便于精确处理 +- **共享状态**: 通过 `validation.setSharedProductionIds` 实现跨页面数据共享 + +--- + +## 已知限制与待实现功能 + +### 进度更新机制 + +**当前状态**: 未实现 + +**原因**: IPC 通信无法序列化函数,`onProgress` 回调无法传递到主进程。 + +**当前实现**: +```typescript +// extractor.ts 中调用但无效 +input.onProgress?.(`Processing batch ${i + 1}/${batches.length}`, progress) +``` + +**建议实现方案**: +```typescript +// 方案: 使用 IPC 事件通道 + +// 1. 主进程发送进度 +event.sender.send('extractor:progress', { message, progress }) + +// 2. Preload 暴露事件监听 +extractor: { + onProgress: (callback) => { + ipcRenderer.on('extractor:progress', (_event, data) => callback(data)) + } +} + +// 3. 渲染进程监听 +useEffect(() => { + window.electron.extractor.onProgress((data) => { + setProgress(data) + setLogs(prev => [...prev, `[${new Date().toLocaleTimeString()}] ${data.message}`]) + }) +}, []) +``` + +### 文件合并功能 + +**当前状态**: 未实现 + +**待实现**: 将多个批次下载的文件合并为单一 Excel 文件。 + +**相关代码位置**: `extractor.ts:69-70` + +```typescript +// TODO: Merge files (implement in separate task) +// result.mergedFile = await this.mergeFiles(result.downloadedFiles); +``` ---