diff --git a/docs/extractor-start-button-flow.md b/docs/extractor-start-button-flow.md index 5544c87..202ed10 100644 --- a/docs/extractor-start-button-flow.md +++ b/docs/extractor-start-button-flow.md @@ -1,6 +1,6 @@ # 数据提取界面 - 开始按钮工作流程详解 -> **文档版本**: 1.1 +> **文档版本**: 1.2 > **更新日期**: 2026-03-03 > **适用范围**: ERPAuto v1.0+ > **相关文件**: @@ -10,6 +10,8 @@ > - `src/main/services/erp/extractor.ts` (业务逻辑层) > - `src/main/services/erp/order-resolver.ts` (订单号解析服务) > - `src/main/services/erp/erp-auth.ts` (ERP认证服务) +> - `src/main/services/database/index.ts` (数据库工厂) +> - `src/main/types/database.types.ts` (数据库类型定义) > - `src/main/types/extractor.types.ts` (类型定义) ## 目录 @@ -47,8 +49,16 @@ graph TB J[ExtractorService] end + subgraph "Database Layer (数据库层)" + N["DatabaseFactory + create()"] + O["IDatabaseService + 统一接口"] + P1[(MySQL)] + P2[(SQL Server)] + end + subgraph "External Services (外部服务)" - K[(MySQL Database)] L[ERP Web System] M[Playwright Browser] end @@ -58,7 +68,10 @@ graph TB E -->|IPC通信| F F -->|接收请求| G G -->|解析订单号| H - H -->|查询数据| K + H -->|创建服务| N + N -->|返回实例| O + O -->|查询数据| P1 + O -->|查询数据| P2 G -->|登录认证| I I -->|自动化操作| M M -->|访问页面| L @@ -71,7 +84,8 @@ graph TB style A fill:#e1f5ff style G fill:#fff4e1 - style K fill:#e8f5e9 + style N fill:#e8f5e9 + style O fill:#e8f5e9 style L fill:#f3e5f5 ``` @@ -80,7 +94,11 @@ graph TB - **Renderer Process**: 负责UI展示和用户交互,使用React管理状态 - **IPC Bridge**: 安全的进程间通信桥梁,通过preload脚本暴露 - **Main Process**: 处理业务逻辑、数据库操作、浏览器自动化 -- **External Services**: MySQL数据库和ERP Web系统 +- **Database Layer**: 数据库抽象层,通过工厂模式创建服务实例 + - 支持 MySQL 和 SQL Server 双数据库 + - 通过 `IDatabaseService` 统一接口操作 + - 由 `DB_TYPE` 环境变量决定使用哪种数据库 +- **External Services**: ERP Web 系统 --- @@ -94,7 +112,8 @@ sequenceDiagram participant IPC as electron API participant Handler as extractor-handler.ts participant Resolver as OrderNumberResolver - participant MySQL as MySQL Database + participant DBFactory as DatabaseFactory + participant Database as IDatabaseService participant Auth as ErpAuthService participant Extractor as ExtractorService participant Browser as Playwright Browser @@ -122,17 +141,27 @@ sequenceDiagram UI->>UI: 设置isRunning=false end - Note over Handler,MySQL: 订单号解析阶段 - Handler->>MySQL: 11. 连接MySQL数据库 - alt MySQL连接失败 + Note over Handler,Database: 数据库连接阶段 + Handler->>DBFactory: 11. 调用 create() + DBFactory->>DBFactory: 12. 读取 DB_TYPE 环境变量 + alt DB_TYPE = mysql + DBFactory->>Database: 创建 MySqlService 实例 + else DB_TYPE = sqlserver + DBFactory->>Database: 创建 SqlServerService 实例 + end + Database->>Database: 13. connect() + alt 数据库连接失败 + Database-->>Handler: 抛出异常 Handler-->>UI: 返回DatabaseQueryError end + DBFactory-->>Handler: 14. 返回 IDatabaseService 实例 - Handler->>Resolver: 12. 创建OrderNumberResolver - Handler->>Resolver: 13. 调用resolve(orderNumbers) - Resolver->>MySQL: 14. 查询生产订单号映射 - MySQL-->>Resolver: 15. 返回映射结果 - Resolver-->>Handler: 16. 返回映射结果
(包含有效订单号和警告) + Note over Handler,Database: 订单号解析阶段 + Handler->>Resolver: 15. 创建OrderNumberResolver(dbService) + Handler->>Resolver: 16. 调用resolve(orderNumbers) + Resolver->>Database: 17. 查询生产订单号映射 + Database-->>Resolver: 18. 返回映射结果 + Resolver-->>Handler: 19. 返回映射结果
(包含有效订单号和警告) alt 没有有效订单号 Handler-->>UI: 返回ValidationError @@ -140,62 +169,62 @@ sequenceDiagram end Note over Handler,Browser: ERP认证阶段 - Handler->>Auth: 17. 创建ErpAuthService - Handler->>Auth: 18. 调用login() - Auth->>Browser: 19. 启动Playwright浏览器 - Browser->>ERP: 20. 访问ERP登录页面 - Browser->>ERP: 21. 填写用户名密码 - Browser->>ERP: 22. 点击登录按钮 - ERP-->>Browser: 23. 登录成功 - Browser-->>Auth: 24. 返回session对象 - Auth-->>Handler: 25. 登录成功 + Handler->>Auth: 20. 创建ErpAuthService + Handler->>Auth: 21. 调用login() + Auth->>Browser: 22. 启动Playwright浏览器 + Browser->>ERP: 23. 访问ERP登录页面 + Browser->>ERP: 24. 填写用户名密码 + Browser->>ERP: 25. 点击登录按钮 + ERP-->>Browser: 26. 登录成功 + Browser-->>Auth: 27. 返回session对象 + Auth-->>Handler: 28. 登录成功 alt 登录失败 Auth-->>Handler: 抛出异常 Handler-->>UI: 返回ErpConnectionError end Note over Extractor,ERP: 数据提取阶段 - Handler->>Extractor: 26. 创建ExtractorService - Handler->>Extractor: 27. 调用extract()
传入有效订单号 - Extractor->>Browser: 28. 使用已有session - Extractor->>ERP: 29. 导航到离散备料计划维护页面 - Extractor->>ERP: 30. 设置查询界面
(订单号查询, 全部标签, 限制5000) + Handler->>Extractor: 29. 创建ExtractorService + Handler->>Extractor: 30. 调用extract()
传入有效订单号 + Extractor->>Browser: 31. 使用已有session + Extractor->>ERP: 32. 导航到离散备料计划维护页面 + Extractor->>ERP: 33. 设置查询界面
(订单号查询, 全部标签, 限制5000) loop 批处理循环 (每批最多100个订单) - Extractor->>Extractor: 31. 创建批次
(按batchSize分组) + Extractor->>Extractor: 34. 创建批次
(按batchSize分组) Note over Extractor: onProgress回调存在但
无法通过IPC传递(函数不可序列化) - 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)" + Extractor->>ERP: 35. 填充订单号到搜索框 + Extractor->>ERP: 36. 点击搜索按钮 + Extractor->>ERP: 37. 等待加载完成 + Extractor->>ERP: 38. 点击第一行复选框 + Extractor->>ERP: 39. 悬停并点击"更多" + Extractor->>ERP: 40. 点击"输出" + Extractor->>ERP: 41. 设置行数阈值为300000 + Extractor->>ERP: 42. 点击"确定(Y)" - Browser->>Browser: 40. 监听下载事件 - ERP->>Browser: 41. 触发文件下载 - Browser->>Browser: 42. 保存文件到downloads目录 - Browser-->>Extractor: 43. 返回文件路径 - Extractor->>Extractor: 44. 记录下载文件路径 + Browser->>Browser: 43. 监听下载事件 + ERP->>Browser: 44. 触发文件下载 + Browser->>Browser: 45. 保存文件到downloads目录 + Browser-->>Extractor: 46. 返回文件路径 + Extractor->>Extractor: 47. 记录下载文件路径 end - Extractor->>Extractor: 45. 汇总结果
(文件列表, 记录数, 错误) - Extractor-->>Handler: 46. 返回ExtractorResult - Handler->>Handler: 47. 添加解析警告到错误列表 + Extractor->>Extractor: 48. 汇总结果
(文件列表, 记录数, 错误) + Extractor-->>Handler: 49. 返回ExtractorResult + Handler->>Handler: 50. 添加解析警告到错误列表 Note over Handler,IPC: 清理阶段 - Handler->>Browser: 48. 关闭浏览器 - Handler->>MySQL: 49. 断开数据库连接 + Handler->>Browser: 51. 关闭浏览器 + Handler->>Database: 52. 断开数据库连接
(dbService.disconnect()) Note over Handler,UI: 响应阶段 - 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. 显示提取结果
(文件数, 记录数, 错误数) + Handler-->>IPC: 53. 返回IPC响应
(success: true, data: result) + IPC-->>UI: 54. 返回response + UI->>UI: 55. 设置result状态 + UI->>UI: 56. 设置isRunning=false + UI->>UI: 57. 清空进度状态 + UI->>User: 58. 显示提取结果
(文件数, 记录数, 错误数) alt 发生任何错误 Handler-->>UI: 返回error响应 @@ -288,23 +317,29 @@ stateDiagram-v2 ```mermaid flowchart TD Start([用户点击开始]) --> Validate{前端验证} - Validate -->|订单号为空| ShowEmptyError[显示错误:
请输入至少一个订单号] + Validate -->|订单号为空| ShowEmptyError["显示错误: + 请输入至少一个订单号"] Validate -->|验证通过| CallIPC[调用IPC API] CallIPC --> ConfigCheck{环境配置检查} - ConfigCheck -->|配置不完整| ConfigError[返回ValidationError:
ERP配置不完整] - ConfigCheck -->|配置完整| ConnectMySQL[连接MySQL] + ConfigCheck -->|配置不完整| ConfigError["返回ValidationError: + ERP配置不完整"] + ConfigCheck -->|配置完整| ConnectDB["连接数据库 + (MySQL或SQL Server)"] - ConnectMySQL --> MySQLCheck{连接成功?} - MySQLCheck -->|失败| MySQLError[返回DatabaseQueryError:
MySQL连接失败] - MySQLCheck -->|成功| ResolveOrders[解析订单号] + ConnectDB --> DBCheck{连接成功?} + DBCheck -->|失败| DBError["返回DatabaseQueryError: + 数据库连接失败"] + DBCheck -->|成功| ResolveOrders[解析订单号] ResolveOrders --> ValidOrders{有有效订单号?} - ValidOrders -->|无| NoOrdersError[返回ValidationError:
没有有效的生产订单号] + ValidOrders -->|无| NoOrdersError["返回ValidationError: + 没有有效的生产订单号"] ValidOrders -->|有| LoginERP[ERP登录] LoginERP --> LoginCheck{登录成功?} - LoginCheck -->|失败| LoginError[返回ErpConnectionError:
ERP登录失败] + LoginCheck -->|失败| LoginError["返回ErpConnectionError: + ERP登录失败"] LoginCheck -->|成功| ExtractData[执行数据提取] ExtractData --> BatchLoop[批处理循环] @@ -324,7 +359,7 @@ flowchart TD ShowEmptyError --> ResetState1[设置isRunning=false] ConfigError --> ResetState2[设置isRunning=false] - MySQLError --> ResetState3[设置isRunning=false] + DBError --> ResetState3[设置isRunning=false] NoOrdersError --> ResetState4[设置isRunning=false] LoginError --> ResetState5[设置isRunning=false] @@ -337,7 +372,7 @@ flowchart TD style ShowEmptyError fill:#ffcccc style ConfigError fill:#ffcccc - style MySQLError fill:#ffcccc + style DBError fill:#ffcccc style NoOrdersError fill:#ffcccc style LoginError fill:#ffcccc style RecordError fill:#fff4cc @@ -349,7 +384,7 @@ flowchart TD | 错误类型 | 触发条件 | 用户反馈 | 恢复策略 | |---------|---------|---------|---------| | `ValidationError` | 订单号为空、配置不完整、无有效订单号 | 显示红色错误消息 | 修正输入后重试 | -| `DatabaseQueryError` | MySQL连接失败 | 显示数据库连接错误 | 检查数据库配置 | +| `DatabaseQueryError` | 数据库连接失败 (MySQL/SQL Server) | 显示数据库连接错误 | 检查数据库配置 | | `ErpConnectionError` | ERP登录失败 | 显示ERP登录错误 | 检查ERP凭据 | | `BatchError` | 单个批次处理失败 | 记录到错误列表,继续处理 | 查看错误详情 | | `SystemError` | 未知系统错误 | 显示通用错误消息 | 查看日志 | @@ -361,42 +396,61 @@ flowchart TD ```mermaid flowchart LR subgraph "Input (用户输入)" - A1[原始输入
订单号列表] - A2[批次大小
batchSize=100] + A1["原始输入 + 订单号列表"] + A2["批次大小 + batchSize=100"] end subgraph "Transformation (数据转换)" - B1[行解析
按换行符分割] - B2[去空白
trim每行] - B3[过滤空行
移除空字符串] - B4[存储共享状态
Production IDs] + B1["行解析 + 按换行符分割"] + B2["去空白 + trim每行"] + B3["过滤空行 + 移除空字符串"] + B4["存储共享状态 + Production IDs"] end subgraph "Resolution (订单号解析)" - C1[查询MySQL
查找映射关系] - C2[提取生产订单号
获取有效值] - C3[收集警告
记录未映射项] + C0["DatabaseFactory + create()"] + C1["查询数据库 + 查找映射关系"] + C2["提取生产订单号 + 获取有效值"] + C3["收集警告 + 记录未映射项"] end subgraph "Processing (批量处理)" - D1[批次分组
按batchSize切分] - D2[批次迭代
逐批处理] - D3[订单拼接
逗号连接] + D1["批次分组 + 按batchSize切分"] + D2["批次迭代 + 逐批处理"] + D3["订单拼接 + 逗号连接"] end subgraph "Output (结果输出)" - E1[下载文件列表
downloadedFiles数组] - E2[合并文件
mergedFile TODO] - E3[记录总数
recordCount] - E4[错误列表
errors数组] + E1["下载文件列表 + downloadedFiles数组"] + E2["合并文件 + mergedFile TODO"] + E3["记录总数 + recordCount"] + E4["错误列表 + errors数组"] end A1 --> B1 B1 --> B2 B2 --> B3 B3 --> B4 - B4 --> C1 + B4 --> C0 A2 --> D1 + C0 --> C1 C1 --> C2 C2 --> D1 C3 --> E4 @@ -407,6 +461,7 @@ flowchart LR style A1 fill:#e3f2fd style A2 fill:#e3f2fd + style C0 fill:#e8f5e9 style E1 fill:#e8f5e9 style E2 fill:#e8f5e9 style E3 fill:#e8f5e9 @@ -532,7 +587,7 @@ useEffect(() => { > > 这确保了即使用户在Cleaner页面刷新,数据也已同步。 -### 2. IPC处理器核心逻辑 (extractor-handler.ts:17-154) +### 2. IPC处理器核心逻辑 (extractor-handler.ts:17-145) ```typescript ipcMain.handle( @@ -540,7 +595,7 @@ ipcMain.handle( async (_event, input: ExtractorInput): Promise> => { return withErrorHandling(async () => { let authService: ErpAuthService | null = null - let mysqlService: MySqlService | null = null + let dbService: IDatabaseService | null = null try { // 1. 环境配置检查 @@ -552,12 +607,15 @@ ipcMain.handle( throw new ValidationError('ERP 配置不完整') } - // 2. 连接MySQL并解析订单号 - const mysqlConfig = { /* ... */ } - mysqlService = new MySqlService(mysqlConfig) - await mysqlService.connect() + // 2. 使用数据库工厂创建服务实例 (支持 MySQL 和 SQL Server) + try { + dbService = await create() // 工厂方法,根据 DB_TYPE 自动选择数据库 + } catch (error) { + throw new DatabaseQueryError('数据库连接失败', 'DB_CONNECTION_FAILED', error) + } - const resolver = new OrderNumberResolver(mysqlService) + // 3. 解析订单号 + const resolver = new OrderNumberResolver(dbService) const mappings = await resolver.resolve(input.orderNumbers) const validOrderNumbers = resolver.getValidOrderNumbers(mappings) const warnings = resolver.getWarnings(mappings) @@ -566,33 +624,105 @@ ipcMain.handle( throw new ValidationError('没有有效的生产订单号可处理') } - // 3. ERP登录 + // 4. ERP登录 authService = new ErpAuthService({ url, username, password, headless: true }) await authService.login() - // 4. 执行提取 + // 5. 执行提取 const extractor = new ExtractorService(authService) const result = await extractor.extract({ ...input, orderNumbers: validOrderNumbers }) - // 5. 添加警告到结果 + // 6. 添加警告到结果 if (warnings.length > 0) { result.errors = [...warnings, ...result.errors] } return result } finally { - // 6. 资源清理 + // 7. 资源清理 if (authService) await authService.close() - if (mysqlService) await mysqlService.disconnect() + if (dbService) await dbService.disconnect() } }, 'extractor:run') } ) ``` +### 2.1 数据库工厂模式 (database/index.ts) + +```typescript +/** + * 数据库工厂 - 创建数据库服务实例 + * 支持 MySQL 和 SQL Server 双数据库 + */ +export async function create(type?: DatabaseType): Promise { + const dbType = type || getDatabaseType() // 从 DB_TYPE 环境变量读取 + + // 返回缓存的实例(单例模式) + const cached = instances.get(dbType) + if (cached && cached.isConnected()) { + return cached + } + + // 创建新实例 + let service: IDatabaseService + + if (dbType === 'sqlserver') { + service = new SqlServerService(createSqlServerConfig()) + } else { + service = new MySqlService(createMySqlConfig()) + } + + await service.connect() + instances.set(dbType, service) // 缓存实例 + + return service +} + +/** + * 数据库类型判断 + */ +export function getDatabaseType(): DatabaseType { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + return 'sqlserver' + } + return 'mysql' +} +``` + +### 2.2 数据库服务接口 (types/database.types.ts) + +```typescript +/** + * 数据库服务统一接口 + */ +export interface IDatabaseService { + /** 数据库类型标识 */ + readonly type: DatabaseType + + /** 连接数据库 */ + connect(): Promise + + /** 断开连接 */ + disconnect(): Promise + + /** 检查连接状态 */ + isConnected(): boolean + + /** 执行查询 */ + query(sql: string, params?: any[]): Promise + + /** 事务执行 */ + transaction(queries: { sql: string; params?: any[] }[]): Promise +} + +export type DatabaseType = 'mysql' | 'sqlserver' +``` + ### 3. 提取服务批处理逻辑 (extractor.ts:29-77) ```typescript @@ -725,21 +855,27 @@ export interface ExtractorResult { 1. **三层验证机制**: - 前端验证: 非空检查 - 配置验证: 环境变量完整性 - - 数据验证: 订单号有效性(通过MySQL查询) + - 数据验证: 订单号有效性(通过数据库查询) -2. **资源管理策略**: +2. **数据库架构 (v1.2 更新)**: + - 使用工厂模式 (`create()`) 创建数据库服务实例 + - 支持 MySQL 和 SQL Server 双数据库,通过 `DB_TYPE` 环境变量切换 + - 通过 `IDatabaseService` 统一接口实现数据库无关操作 + - 单例缓存机制,避免重复创建连接 + +3. **资源管理策略**: - 使用 try-finally 确保资源清理 - 浏览器在使用后立即关闭 - 数据库连接在使用后断开 - 清理操作在 finally 块中独立 try-catch,避免清理失败影响结果返回 -3. **错误容错设计**: +4. **错误容错设计**: - 单个批次失败不影响其他批次 - 警告信息独立收集,不影响主流程 - 详细错误信息返回给前端展示 - 使用自定义错误类型 (`ValidationError`, `DatabaseQueryError`, `ErpConnectionError`) -4. **用户体验优化**: +5. **用户体验优化**: - sessionStorage 持久化用户输入(`orderNumbers`, `batchSize`) - 订单号实时同步到共享状态(供 Cleaner 模块使用) - 详细的日志记录 @@ -761,9 +897,11 @@ export interface ExtractorResult { - **批处理**: 默认每批100个订单,平衡性能与稳定性 - **异步并发**: 使用 async/await 处理异步操作 - **下载监听**: 使用 Playwright 事件监听处理文件下载 +- **数据库连接池**: 工厂模式缓存实例,复用连接 ### 扩展性 +- **数据库可切换**: 通过 `DB_TYPE` 环境变量切换 MySQL/SQL Server - **配置化**: batchSize 可配置 - **模块化**: 服务独立,易于测试和维护 - **错误类型化**: 使用自定义错误类型便于精确处理