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 可配置
- **模块化**: 服务独立,易于测试和维护
- **错误类型化**: 使用自定义错误类型便于精确处理