refactor(docs): reorganize documentation directory structure
- Create user/ - User guides and configuration documentation - Create features/ - Feature specifications and business flows - Create debugging/ - Debug guides and quick references - Create testing/ - Test infrastructure, reports, and plans - Create internal/ - Internal plans, analyses, and templates - Move cleaner/*.md to cleaner/ directory - Move LOGGING_*.md to developer/guides/ Add docs/README.md as documentation index with category navigation and quick lookup guide. The reorganized structure makes it easier for users and developers to quickly locate relevant documentation.
This commit is contained in:
1268
docs/features/extractor-start-button-flow.md
Normal file
1268
docs/features/extractor-start-button-flow.md
Normal file
File diff suppressed because it is too large
Load Diff
62
docs/features/settings-partial-save.md
Normal file
62
docs/features/settings-partial-save.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Settings Partial Save Feature
|
||||
|
||||
## Overview
|
||||
|
||||
The settings system now implements partial save functionality to prevent unintended overwrites of configuration values.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Field Whitelist**: Only fields exposed in the UI can be modified
|
||||
2. **Deep Merge**: Updates are merged with existing config, preserving unmodified fields
|
||||
3. **Backup & Rollback**: Config is backed up before save; failures trigger automatic rollback
|
||||
|
||||
## Editable Fields
|
||||
|
||||
Currently editable via UI:
|
||||
|
||||
- `erp.url` - ERP system URL
|
||||
- `erp.username` - ERP login username
|
||||
- `erp.password` - ERP login password
|
||||
|
||||
## Adding New Editable Fields
|
||||
|
||||
To add a new field to the UI:
|
||||
|
||||
1. Add field to whitelist in `src/main/services/config/config-manager.ts`:
|
||||
|
||||
```typescript
|
||||
const UI_EDITABLE_FIELDS: string[] = [
|
||||
'erp.url',
|
||||
'erp.username',
|
||||
'erp.password',
|
||||
'database.dbType' // Add new field here
|
||||
]
|
||||
```
|
||||
|
||||
2. Add UI input in `src/renderer/src/pages/SettingsPage.tsx`
|
||||
3. Update `handleSaveSettings` to include the new field
|
||||
|
||||
## API
|
||||
|
||||
### savePartialSettings(settings: Partial<SettingsData>)
|
||||
|
||||
Saves only the provided fields, preserving all existing configuration.
|
||||
|
||||
**Returns:** `{ success: boolean, error?: string }`
|
||||
|
||||
**Validation:**
|
||||
|
||||
- Checks whitelist before applying changes
|
||||
- Returns error for unauthorized fields
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Unauthorized field**: Returns error message listing invalid fields
|
||||
- **Save failure**: Automatically restores from backup
|
||||
- **Backup failure**: Logs warning, continues with save
|
||||
|
||||
## Backup File
|
||||
|
||||
Location: `.env.backup` (in project root)
|
||||
|
||||
Created before every save operation. Used for rollback on failure.
|
||||
927
docs/features/settings-save-button-flow.md
Normal file
927
docs/features/settings-save-button-flow.md
Normal file
@@ -0,0 +1,927 @@
|
||||
# 系统设置保存按钮工作流程分析
|
||||
|
||||
# System Settings Save Button Workflow Analysis
|
||||
|
||||
## 文档概述 / Document Overview
|
||||
|
||||
本文档详细分析了 ERPAuto 系统设置界面中保存按钮的完整工作流程,包括架构设计、数据流转、技术实现细节以及错误处理机制。
|
||||
|
||||
This document provides a comprehensive analysis of the save button workflow in the ERPAuto system settings interface, including architecture design, data flow, technical implementation details, and error handling mechanisms.
|
||||
|
||||
---
|
||||
|
||||
## 目录 / Table of Contents
|
||||
|
||||
1. [架构概览](#架构概览)
|
||||
2. [数据流程图](#数据流程图)
|
||||
3. [组件详解](#组件详解)
|
||||
4. [数据结构](#数据结构)
|
||||
5. [错误处理机制](#错误处理机制)
|
||||
6. [安全考虑](#安全考虑)
|
||||
7. [技术实现细节](#技术实现细节)
|
||||
|
||||
---
|
||||
|
||||
## 架构概览 / Architecture Overview
|
||||
|
||||
### 系统架构 / System Architecture
|
||||
|
||||
系统设置保存功能采用典型的 Electron 三层架构模式:
|
||||
|
||||
The system settings save functionality follows the classic Electron three-tier architecture pattern:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Renderer Process 渲染进程"
|
||||
UI[SettingsPage.tsx<br/>UI Component]
|
||||
end
|
||||
|
||||
subgraph "Preload Script 预加载脚本"
|
||||
BRIDGE[contextBridge API<br/>Security Boundary]
|
||||
end
|
||||
|
||||
subgraph "Main Process 主进程"
|
||||
IPC[settings-handler.ts<br/>IPC Handler]
|
||||
SERVICE[ConfigManager.ts<br/>Configuration Service]
|
||||
FILE[.env File<br/>Persistent Storage]
|
||||
end
|
||||
|
||||
UI -->|IPC Invoke| BRIDGE
|
||||
BRIDGE -->|Secure Channel| IPC
|
||||
IPC -->|Business Logic| SERVICE
|
||||
SERVICE -->|Write| FILE
|
||||
FILE -->|Confirm| SERVICE
|
||||
SERVICE -->|Result| IPC
|
||||
IPC -->|Response| BRIDGE
|
||||
BRIDGE -->|Promise Resolve| UI
|
||||
|
||||
style UI fill:#e1f5ff
|
||||
style BRIDGE fill:#fff4e1
|
||||
style IPC fill:#ffe1f5
|
||||
style SERVICE fill:#e1ffe1
|
||||
style FILE fill:#f5f5f5
|
||||
```
|
||||
|
||||
### 核心设计模式 / Core Design Patterns
|
||||
|
||||
1. **单向数据流**:数据从 UI → Main Process → File,响应沿相反路径返回
|
||||
2. **安全隔离**:Preload 脚本作为安全桥梁,通过 `contextBridge` 暴露受限 API
|
||||
3. **单例模式**:ConfigManager 使用单例确保配置一致性
|
||||
4. **缓存优先**:配置读取优先从内存缓存获取,写入时同步到磁盘
|
||||
|
||||
---
|
||||
|
||||
## 数据流程图 / Data Flow Diagrams
|
||||
|
||||
### 完整保存流程 / Complete Save Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User as 用户 User
|
||||
participant UI as SettingsPage.tsx
|
||||
participant Preload as preload/index.ts
|
||||
participant IPC as settings-handler.ts
|
||||
participant Config as ConfigManager.ts
|
||||
participant File as .env File
|
||||
|
||||
User->>UI: 点击保存按钮<br/>Click Save Button
|
||||
activate UI
|
||||
|
||||
UI->>UI: handleSaveSettings()
|
||||
Note over UI: 检查是否修改<br/>Check isModified
|
||||
|
||||
UI->>Preload: window.electron.settings<br/>.saveSettings(settings)
|
||||
activate Preload
|
||||
|
||||
Preload->>IPC: ipcRenderer.invoke<br/>('settings:saveSettings', settings)
|
||||
activate IPC
|
||||
|
||||
IPC->>IPC: 验证用户类型<br/>Validate User Type
|
||||
IPC->>Config: configManager<br/>.saveAllSettings(settings)
|
||||
activate Config
|
||||
|
||||
Config->>Config: 更新内存缓存<br/>Update Cache
|
||||
Note over Config: set('erp.url', value)<br/>set('erp.username', value)<br/>... (40+ fields)
|
||||
|
||||
Config->>File: fs.writeFileSync<br/>(.env, content)
|
||||
activate File
|
||||
File-->>Config: true/false
|
||||
deactivate File
|
||||
|
||||
Config-->>IPC: Promise<boolean>
|
||||
deactivate Config
|
||||
|
||||
IPC-->>Preload: {success, error?}
|
||||
deactivate IPC
|
||||
|
||||
Preload-->>UI: Promise resolve
|
||||
deactivate Preload
|
||||
|
||||
alt 保存成功 / Save Success
|
||||
UI->>UI: setIsModified(false)
|
||||
UI->>User: 显示成功消息<br/>Show Success Message
|
||||
else 保存失败 / Save Failed
|
||||
UI->>User: 显示错误消息<br/>Show Error Message
|
||||
end
|
||||
|
||||
deactivate UI
|
||||
```
|
||||
|
||||
### 数据转换流程 / Data Transformation Flow
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "UI State"
|
||||
STATE[Settings Interface<br/>settings.erp.url = 'https://...']
|
||||
end
|
||||
|
||||
subgraph "Type Conversion"
|
||||
T1[SettingsData Object<br/>TypeScript Interface]
|
||||
end
|
||||
|
||||
subgraph "IPC Transport"
|
||||
JSON[JSON Serialization<br/>String Transfer]
|
||||
end
|
||||
|
||||
subgraph "Service Layer"
|
||||
CACHE[Config Cache<br/>Map<string, string>]
|
||||
end
|
||||
|
||||
subgraph "File System"
|
||||
ENV[.env File Format<br/>KEY=VALUE]
|
||||
end
|
||||
|
||||
STATE -->|Object| T1
|
||||
T1 -->|JSON.stringify| JSON
|
||||
JSON -->|Deserialize| T1
|
||||
T1 -->|set key-value| CACHE
|
||||
CACHE -->|Format| ENV
|
||||
|
||||
style STATE fill:#e1f5ff
|
||||
style JSON fill:#fff4e1
|
||||
style CACHE fill:#e1ffe1
|
||||
style ENV fill:#f5f5f5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 组件详解 / Component Details
|
||||
|
||||
### 1. 渲染进程 / Renderer Process
|
||||
|
||||
#### SettingsPage.tsx (`src/renderer/src/pages/SettingsPage.tsx`)
|
||||
|
||||
**主要职责 / Main Responsibilities:**
|
||||
|
||||
- 用户界面渲染和交互
|
||||
- 本地状态管理(settings, isModified, message)
|
||||
- 调用 IPC 通信
|
||||
|
||||
**关键函数 / Key Functions:**
|
||||
|
||||
```typescript
|
||||
// 第 61-73 行 / Lines 61-73
|
||||
const handleSaveSettings = async () => {
|
||||
try {
|
||||
const result = await window.electron.settings.saveSettings(settings as any)
|
||||
if (result.success) {
|
||||
setIsModified(false) // 清除修改标记
|
||||
showMessage('success', '设置保存成功')
|
||||
} else {
|
||||
showMessage('error', result.error || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('error', '保存设置时发生错误')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**状态管理 / State Management:**
|
||||
|
||||
| 状态变量 | 类型 | 用途 |
|
||||
| ------------ | ---------------- | ----------------------------------------------------------- |
|
||||
| `settings` | `Settings` | 当前配置数据,结构为 `{ erp: { url, username, password } }` |
|
||||
| `isModified` | `boolean` | 标记配置是否已修改,控制保存按钮启用状态 |
|
||||
| `isLoading` | `boolean` | 加载状态,显示加载动画 |
|
||||
| `message` | `object \| null` | 临时消息,3秒后自动消失 |
|
||||
|
||||
**UI 交互逻辑 / UI Interaction Logic:**
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Loading: 组件挂载
|
||||
Loading --> Ready: loadSettings()
|
||||
Ready --> Modified: updateSettings()
|
||||
Modified --> Modified: 继续修改
|
||||
Modified --> Ready: 保存成功
|
||||
Modified --> Error: 保存失败
|
||||
Error --> Modified: 用户继续操作
|
||||
Ready --> [*]: 组件卸载
|
||||
|
||||
note right of Modified
|
||||
保存按钮启用
|
||||
Save Button Enabled
|
||||
end note
|
||||
|
||||
note right of Ready
|
||||
保存按钮禁用
|
||||
Save Button Disabled
|
||||
end note
|
||||
```
|
||||
|
||||
### 2. 预加载脚本 / Preload Script
|
||||
|
||||
#### preload/index.ts (`src/preload/index.ts`)
|
||||
|
||||
**主要职责 / Main Responsibilities:**
|
||||
|
||||
- 安全桥梁,暴露受限 API 到渲染进程
|
||||
- 类型安全的 IPC 通道定义
|
||||
|
||||
**关键代码 / Key Code:**
|
||||
|
||||
```typescript
|
||||
// 第 89-97 行 / Lines 89-97
|
||||
settings: {
|
||||
getUserType: () => ipcRenderer.invoke('settings:getUserType'),
|
||||
getSettings: () => ipcRenderer.invoke('settings:getSettings'),
|
||||
saveSettings: (settings: SettingsData) =>
|
||||
ipcRenderer.invoke('settings:saveSettings', settings),
|
||||
resetDefaults: () => ipcRenderer.invoke('settings:resetDefaults'),
|
||||
testErpConnection: () => ipcRenderer.invoke('settings:testErpConnection'),
|
||||
testDbConnection: () => ipcRenderer.invoke('settings:testDbConnection')
|
||||
}
|
||||
```
|
||||
|
||||
**安全隔离机制 / Security Isolation:**
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Renderer[Renderer Process<br/>Untrusted Context]
|
||||
Preload[Preload Script<br/>Trusted Context]
|
||||
Main[Main Process<br/>Trusted Context]
|
||||
|
||||
Renderer -->|window.electron| Preload
|
||||
Preload -->|ipcRenderer.invoke| Main
|
||||
Main -->|Validation| Preload
|
||||
Preload -->|Return Promise| Renderer
|
||||
|
||||
style Renderer fill:#ffe1e1
|
||||
style Preload fill:#e1ffe1
|
||||
style Main fill:#e1e1ff
|
||||
```
|
||||
|
||||
### 3. 主进程 / Main Process
|
||||
|
||||
#### settings-handler.ts (`src/main/ipc/settings-handler.ts`)
|
||||
|
||||
**主要职责 / Main Responsibilities:**
|
||||
|
||||
- IPC 通道注册和处理
|
||||
- 权限验证(基于用户类型)
|
||||
- 业务逻辑协调
|
||||
|
||||
**保存设置处理函数 / Save Settings Handler:**
|
||||
|
||||
```typescript
|
||||
// 第 83-102 行 / Lines 83-102
|
||||
ipcMain.handle(
|
||||
'settings:saveSettings',
|
||||
async (_event, settings: SettingsData): Promise<SaveSettingsResult> => {
|
||||
try {
|
||||
log.info('Saving settings')
|
||||
const success = await configManager.saveAllSettings(settings)
|
||||
if (success) {
|
||||
log.info('Settings saved successfully')
|
||||
return { success: true }
|
||||
} else {
|
||||
log.warn('Failed to save settings')
|
||||
return { success: false, error: '保存设置失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Error saving settings', { error: message })
|
||||
return { success: false, error: `保存设置失败:${message}` }
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**用户类型过滤 / User Type Filtering:**
|
||||
|
||||
```typescript
|
||||
// 第 31-54 行 / Lines 31-54
|
||||
function filterSettingsByUserType(settings: SettingsData, userType: UserType): SettingsData {
|
||||
if (userType === 'Admin') {
|
||||
return settings // Admin 获取完整配置
|
||||
}
|
||||
|
||||
// User 用户获取受限配置
|
||||
return {
|
||||
erp: {
|
||||
username: settings.erp.username,
|
||||
password: settings.erp.password,
|
||||
headless: settings.erp.headless,
|
||||
url: settings.erp.url,
|
||||
ignoreHttpsErrors: settings.erp.ignoreHttpsErrors,
|
||||
autoCloseBrowser: settings.erp.autoCloseBrowser
|
||||
},
|
||||
paths: settings.paths,
|
||||
execution: settings.execution,
|
||||
database: settings.database,
|
||||
extraction: settings.extraction,
|
||||
validation: settings.validation,
|
||||
ui: settings.ui
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**权限控制矩阵 / Permission Control Matrix:**
|
||||
|
||||
| 功能 / Feature | Admin | User | Guest |
|
||||
| -------------- | ----- | ------- | ----- |
|
||||
| 查看所有设置 | ✅ | ⚠️ 部分 | ❌ |
|
||||
| 保存设置 | ✅ | ✅ | ❌ |
|
||||
| 恢复默认值 | ✅ | ❌ | ❌ |
|
||||
| 测试 ERP 连接 | ✅ | ✅ | ❌ |
|
||||
| 测试数据库连接 | ✅ | ✅ | ❌ |
|
||||
|
||||
### 4. 配置管理服务 / Configuration Manager Service
|
||||
|
||||
#### config-manager.ts (`src/main/services/config/config-manager.ts`)
|
||||
|
||||
**主要职责 / Main Responsibilities:**
|
||||
|
||||
- .env 文件读写
|
||||
- 配置缓存管理
|
||||
- 默认值管理
|
||||
- 类型转换和验证
|
||||
|
||||
**类结构 / Class Structure:**
|
||||
|
||||
```typescript
|
||||
export class ConfigManager {
|
||||
private static instance: ConfigManager | null = null // 单例模式
|
||||
private envPath: string // .env 文件路径
|
||||
private configCache: Map<string, string> // 内存缓存
|
||||
private initialized: boolean = false // 初始化标记
|
||||
|
||||
// 单例获取方法
|
||||
public static getInstance(): ConfigManager
|
||||
|
||||
// 配置读取
|
||||
public get(key: string, defaultValue?: string): string | undefined
|
||||
public getBoolean(key: string, defaultValue?: boolean): boolean
|
||||
public getNumber(key: string, defaultValue?: number): number
|
||||
|
||||
// 配置写入
|
||||
public set(key: string, value: string | number | boolean): void
|
||||
|
||||
// 持久化
|
||||
public async save(): Promise<boolean>
|
||||
|
||||
// 高级操作
|
||||
public getAllSettings(): SettingsData
|
||||
public async saveAllSettings(settings: SettingsData): Promise<boolean>
|
||||
public resetToDefaults(): SettingsData
|
||||
}
|
||||
```
|
||||
|
||||
**保存详细流程 / Save Detailed Flow:**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
START[saveAllSettings] --> STEP1[更新 ERP 配置 6 字段]
|
||||
STEP1 --> STEP2[更新数据库配置 7 字段]
|
||||
STEP2 --> STEP3[更新路径配置 3 字段]
|
||||
STEP3 --> STEP4[更新提取配置 5 字段]
|
||||
STEP4 --> STEP5[更新校验配置 5 字段]
|
||||
STEP5 --> STEP6[更新 UI 配置 3 字段]
|
||||
STEP6 --> STEP7[更新执行配置 1 字段]
|
||||
STEP7 --> SAVE[调用 save 方法]
|
||||
SAVE --> BUILD[构建 .env 内容]
|
||||
BUILD --> WRITE[写入文件系统]
|
||||
WRITE --> CHECK{检查结果}
|
||||
CHECK -->|成功| SUCCESS[返回 true]
|
||||
CHECK -->|失败| FAILURE[返回 false]
|
||||
```
|
||||
|
||||
**.env 文件格式 / .env File Format:**
|
||||
|
||||
```bash
|
||||
# ===========================
|
||||
# ERP 系统配置
|
||||
# ===========================
|
||||
ERP_URL=https://68.11.34.30:8082/
|
||||
ERP_USERNAME=
|
||||
ERP_PASSWORD=
|
||||
ERP_HEADLESS=true
|
||||
ERP_IGNORE_HTTPS_ERRORS=true
|
||||
ERP_AUTO_CLOSE_BROWSER=true
|
||||
|
||||
# ===========================
|
||||
# 数据库配置 - MySQL
|
||||
# ===========================
|
||||
DB_TYPE=mysql
|
||||
DB_NAME=BLD_DB
|
||||
DB_USERNAME=remote_user
|
||||
DB_PASSWORD=
|
||||
DB_MYSQL_HOST=192.168.31.83
|
||||
DB_MYSQL_PORT=3306
|
||||
DB_MYSQL_CHARSET=utf8mb4
|
||||
|
||||
# ===========================
|
||||
# 路径配置
|
||||
# ===========================
|
||||
PATH_DATA_DIR=D:/python/playwrite/data/
|
||||
PATH_DEFAULT_OUTPUT=离散备料计划维护_合并.xlsx
|
||||
PATH_VALIDATION_OUTPUT=物料状态校验结果.xlsx
|
||||
|
||||
# ... 更多配置节
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据结构 / Data Structures
|
||||
|
||||
### SettingsData 接口 / Interface Definition
|
||||
|
||||
**类型定义位置 / Type Definition Location:**
|
||||
`src/main/types/settings.types.ts` (第 136-151 行)
|
||||
|
||||
```typescript
|
||||
export interface SettingsData {
|
||||
erp: ErpConfig
|
||||
database: DatabaseConfig
|
||||
paths: PathsConfig
|
||||
extraction: ExtractionConfig
|
||||
validation: ValidationConfig
|
||||
ui: UiConfig
|
||||
execution: ExecutionConfig
|
||||
}
|
||||
```
|
||||
|
||||
### 完整数据结构树 / Complete Data Structure Tree
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Settings[SettingsData]
|
||||
|
||||
Settings --> Erp[ErpConfig]
|
||||
Erp --> Erp1[url: string]
|
||||
Erp --> Erp2[username: string]
|
||||
Erp --> Erp3[password: string]
|
||||
Erp --> Erp4[headless: boolean]
|
||||
Erp --> Erp5[ignoreHttpsErrors: boolean]
|
||||
Erp --> Erp6[autoCloseBrowser: boolean]
|
||||
|
||||
Settings --> DB[DatabaseConfig]
|
||||
DB --> DB1[dbType: mysql or sqlserver]
|
||||
DB --> DB2[server: string]
|
||||
DB --> DB3[mysqlHost: string]
|
||||
DB --> DB4[mysqlPort: number]
|
||||
DB --> DB5[database: string]
|
||||
DB --> DB6[username: string]
|
||||
DB --> DB7[password: string]
|
||||
|
||||
Settings --> Paths[PathsConfig]
|
||||
Paths --> Paths1[dataDir: string]
|
||||
Paths --> Paths2[defaultOutput: string]
|
||||
Paths --> Paths3[validationOutput: string]
|
||||
|
||||
Settings --> Extract[ExtractionConfig]
|
||||
Extract --> Extract1[batchSize: number]
|
||||
Extract --> Extract2[verbose: boolean]
|
||||
Extract --> Extract3[autoConvert: boolean]
|
||||
Extract --> Extract4[mergeBatches: boolean]
|
||||
Extract --> Extract5[enableDbPersistence: boolean]
|
||||
|
||||
Settings --> Valid[ValidationConfig]
|
||||
Valid --> Valid1[dataSource: ValidationDataSource]
|
||||
Valid --> Valid2[batchSize: number]
|
||||
Valid --> Valid3[matchMode: MatchMode]
|
||||
Valid --> Valid4[enableCrud: boolean]
|
||||
Valid --> Valid5[defaultManager: string]
|
||||
|
||||
Settings --> UI[UiConfig]
|
||||
UI --> UI1[fontFamily: string]
|
||||
UI --> UI2[fontSize: number]
|
||||
UI --> UI3[productionIdInputWidth: number]
|
||||
|
||||
Settings --> Exec[ExecutionConfig]
|
||||
Exec --> Exec1[dryRun: boolean]
|
||||
|
||||
style Settings fill:#e1f5ff
|
||||
style Erp fill:#ffe1f5
|
||||
style DB fill:#e1ffe1
|
||||
style Paths fill:#fff4e1
|
||||
style Extract fill:#f5e1ff
|
||||
style Valid fill:#ffe1e1
|
||||
style UI fill:#e1f5ff
|
||||
style Exec fill:#f5f5f5
|
||||
```
|
||||
|
||||
### IPC 通信数据格式 / IPC Communication Data Format
|
||||
|
||||
**请求格式 / Request Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"erp": {
|
||||
"url": "https://68.11.34.30:8082/",
|
||||
"username": "admin",
|
||||
"password": "password123",
|
||||
"headless": true,
|
||||
"ignoreHttpsErrors": true,
|
||||
"autoCloseBrowser": true
|
||||
},
|
||||
"database": { ... },
|
||||
"paths": { ... },
|
||||
"extraction": { ... },
|
||||
"validation": { ... },
|
||||
"ui": { ... },
|
||||
"execution": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**响应格式 / Response Format:**
|
||||
|
||||
```json
|
||||
// 成功 / Success
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
|
||||
// 失败 / Failure
|
||||
{
|
||||
"success": false,
|
||||
"error": "保存设置失败:Access denied"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理机制 / Error Handling Mechanism
|
||||
|
||||
### 错误处理层次 / Error Handling Layers
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "UI Layer"
|
||||
UI_TRY[try-catch in handleSaveSettings]
|
||||
UI_MSG[showMessage display]
|
||||
end
|
||||
|
||||
subgraph "IPC Layer"
|
||||
IPC_TRY[try-catch in handler]
|
||||
IPC_LOG[Structured logging]
|
||||
IPC_RETURN[Return error object]
|
||||
end
|
||||
|
||||
subgraph "Service Layer"
|
||||
SVC_TRY[try-catch in save]
|
||||
SVC_LOG[Console error log]
|
||||
SVC_RETURN[Return false]
|
||||
end
|
||||
|
||||
subgraph "File System"
|
||||
FS_CHECK[File exists check]
|
||||
FS_WRITE[Write with error handling]
|
||||
end
|
||||
|
||||
UI_TRY -->|Catch| UI_MSG
|
||||
IPC_TRY -->|Catch| IPC_LOG --> IPC_RETURN
|
||||
SVC_TRY -->|Catch| SVC_LOG --> SVC_RETURN
|
||||
FS_WRITE -->|Error| SVC_TRY
|
||||
|
||||
style UI_TRY fill:#ffe1e1
|
||||
style IPC_TRY fill:#ffe1e1
|
||||
style SVC_TRY fill:#ffe1e1
|
||||
```
|
||||
|
||||
### 错误场景分析 / Error Scenario Analysis
|
||||
|
||||
| 错误场景 / Error Scenario | 触发位置 / Location | 处理方式 / Handling | 用户反馈 / User Feedback |
|
||||
| ------------------------- | ------------------- | --------------------- | ------------------------ |
|
||||
| IPC 通信失败 | Renderer | try-catch | 显示"保存设置时发生错误" |
|
||||
| 权限不足 | Main Process | 检查 UserType | 返回权限错误信息 |
|
||||
| 文件写入失败 | ConfigManager | fs.writeFileSync 捕获 | 返回"保存设置失败" |
|
||||
| 无效数据类型 | IPC Handler | TypeScript 类型检查 | 返回验证错误 |
|
||||
| 磁盘空间不足 | File System | OS 异常捕获 | 返回系统错误信息 |
|
||||
|
||||
### 日志记录策略 / Logging Strategy
|
||||
|
||||
```typescript
|
||||
// Main Process 结构化日志 / Structured Logging
|
||||
log.info('Saving settings')
|
||||
log.info('Settings saved successfully')
|
||||
log.warn('Failed to save settings')
|
||||
log.error('Error saving settings', { error: message })
|
||||
```
|
||||
|
||||
**日志级别使用 / Log Level Usage:**
|
||||
|
||||
- `info`: 正常操作流程
|
||||
- `warn`: 潜在问题(如保存失败但未崩溃)
|
||||
- `error`: 严重错误(如异常抛出)
|
||||
|
||||
---
|
||||
|
||||
## 安全考虑 / Security Considerations
|
||||
|
||||
### 安全机制层级 / Security Layers
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
L1[Layer 1: Context Isolation<br/>渲染进程隔离]
|
||||
L2[Layer 2: contextBridge<br/>受限 API 暴露]
|
||||
L3[Layer 3: User Type Filtering<br/>基于角色的访问控制]
|
||||
L4[Layer 4: File System Permissions<br/>.env 文件保护]
|
||||
|
||||
L1 --> L2 --> L3 --> L4
|
||||
|
||||
style L1 fill:#e1f5ff
|
||||
style L2 fill:#fff4e1
|
||||
style L3 fill:#e1ffe1
|
||||
style L4 fill:#ffe1f5
|
||||
```
|
||||
|
||||
### 关键安全措施 / Key Security Measures
|
||||
|
||||
1. **密码明文存储风险 / Password Storage Risk**
|
||||
- ⚠️ 当前:密码以明文形式存储在 .env 文件中
|
||||
- 🔒 建议:实现加密存储机制
|
||||
|
||||
2. **用户权限隔离 / User Permission Isolation**
|
||||
- ✅ 实现:基于用户类型过滤可见配置
|
||||
- ✅ 实现:Guest 用户无法访问设置页面
|
||||
|
||||
3. **IPC 通信安全 / IPC Communication Security**
|
||||
- ✅ 实现:使用 `contextBridge` 而非直接暴露
|
||||
- ✅ 实现:类型安全的 TypeScript 接口
|
||||
|
||||
4. **文件系统访问 / File System Access**
|
||||
- ✅ 实现:.env 文件仅主进程可访问
|
||||
- ⚠️ 风险:文件权限取决于操作系统
|
||||
|
||||
### 敏感数据流向 / Sensitive Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 用户输入
|
||||
participant UI as UI State (内存)
|
||||
participant IPC as IPC Channel
|
||||
participant Cache as Config Cache
|
||||
participant File as .env File
|
||||
|
||||
User->>UI: password = "secret123"
|
||||
UI->>IPC: JSON 传输 (未加密)
|
||||
IPC->>Cache: Map.set('erp.password', 'secret123')
|
||||
Cache->>File: 写入明文到磁盘
|
||||
|
||||
Note over File: ⚠️ 安全风险:<br/>密码以明文形式持久化
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术实现细节 / Technical Implementation Details
|
||||
|
||||
### 文件位置索引 / File Location Index
|
||||
|
||||
| 组件 / Component | 文件路径 / File Path | 关键行数 / Key Lines |
|
||||
| ---------------- | -------------------------------------------- | --------------------- |
|
||||
| UI 组件 | `src/renderer/src/pages/SettingsPage.tsx` | 61-73 (保存处理) |
|
||||
| 预加载脚本 | `src/preload/index.ts` | 89-97 (API 定义) |
|
||||
| IPC 处理器 | `src/main/ipc/settings-handler.ts` | 83-102 (保存处理) |
|
||||
| 配置管理器 | `src/main/services/config/config-manager.ts` | 437-483 (保存方法) |
|
||||
| 类型定义 | `src/main/types/settings.types.ts` | 136-171 (接口定义) |
|
||||
| IPC 注册 | `src/main/ipc/index.ts` | 导入 settings-handler |
|
||||
|
||||
### 性能特性 / Performance Characteristics
|
||||
|
||||
1. **异步操作 / Async Operations**
|
||||
- 所有 IPC 调用使用 `async/await` 模式
|
||||
- 避免阻塞主进程事件循环
|
||||
|
||||
2. **内存优化 / Memory Optimization**
|
||||
- 使用 Map 缓存配置,减少文件读取
|
||||
- 按需加载配置项
|
||||
|
||||
3. **写入策略 / Write Strategy**
|
||||
- 每次保存完整重写 .env 文件
|
||||
- 原子写入(writeFileSync)
|
||||
|
||||
### 依赖关系图 / Dependency Graph
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[SettingsPage.tsx] -->|imports| B[lucide-react]
|
||||
A -->|uses| C[window.electron.settings]
|
||||
|
||||
C -->|exposed by| D[preload/index.ts]
|
||||
D -->|imports| E[electron API]
|
||||
D -->|imports| F[SettingsData Type]
|
||||
|
||||
G[settings-handler.ts] -->|imports| H[ipcMain]
|
||||
G -->|imports| I[ConfigManager]
|
||||
G -->|imports| J[SessionManager]
|
||||
G -->|imports| K[Logger]
|
||||
|
||||
I -->|imports| L[fs/path]
|
||||
I -->|imports| M[SettingsData Type]
|
||||
I -->|imports| N[DEFAULT_SETTINGS]
|
||||
|
||||
style A fill:#e1f5ff
|
||||
style D fill:#fff4e1
|
||||
style G fill:#ffe1f5
|
||||
style I fill:#e1ffe1
|
||||
```
|
||||
|
||||
### 关键代码片段分析 / Key Code Snippet Analysis
|
||||
|
||||
**1. 状态更新逻辑 / State Update Logic**
|
||||
|
||||
```typescript
|
||||
// SettingsPage.tsx 第 50-59 行
|
||||
const updateSettings = (category: string, key: string, value: any) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
[category]: {
|
||||
...(prev as any)[category],
|
||||
[key]: value
|
||||
}
|
||||
}))
|
||||
setIsModified(true) // 标记为已修改
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点 / Design Points:**
|
||||
|
||||
- 不可变更新模式(Immutable Update Pattern)
|
||||
- 使用展开运算符保持对象引用
|
||||
- 自动启用保存按钮
|
||||
|
||||
**2. 配置保存逻辑 / Configuration Save Logic**
|
||||
|
||||
```typescript
|
||||
// config-manager.ts 第 437-483 行
|
||||
public async saveAllSettings(settings: SettingsData): Promise<boolean> {
|
||||
// 批量更新缓存 (40+ 字段)
|
||||
this.set('erp.url', settings.erp.url)
|
||||
this.set('erp.username', settings.erp.username)
|
||||
// ... 更多字段
|
||||
|
||||
// 同步写入文件
|
||||
return this.save()
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点 / Design Points:**
|
||||
|
||||
- 先更新内存,后写入磁盘
|
||||
- 失败时缓存保持不变
|
||||
- 返回布尔值表示成功/失败
|
||||
|
||||
**3. .env 文件生成逻辑 / .env File Generation**
|
||||
|
||||
```typescript
|
||||
// config-manager.ts 第 179-345 行
|
||||
public async save(): Promise<boolean> {
|
||||
const lines: string[] = []
|
||||
|
||||
// 构建格式化的 .env 内容
|
||||
lines.push('# ===========================')
|
||||
lines.push('# ERP 系统配置')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`ERP_URL=${this.configCache.get('erp.url') || DEFAULT_SETTINGS.erp.url}`)
|
||||
|
||||
const content = lines.join('\n')
|
||||
fs.writeFileSync(this.envPath, content, 'utf-8')
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点 / Design Points:**
|
||||
|
||||
- 添加注释分隔符提高可读性
|
||||
- 使用默认值作为后备
|
||||
- 同步写入确保一致性
|
||||
|
||||
---
|
||||
|
||||
## 扩展与改进建议 / Extension and Improvement Suggestions
|
||||
|
||||
### 短期改进 / Short-term Improvements
|
||||
|
||||
1. **输入验证 / Input Validation**
|
||||
- 添加 URL 格式验证
|
||||
- 密码强度检查
|
||||
- 端口号范围验证
|
||||
|
||||
2. **用户体验 / User Experience**
|
||||
- 添加保存进度指示器
|
||||
- 实现自动保存功能
|
||||
- 添加配置导入/导出
|
||||
|
||||
3. **错误处理 / Error Handling**
|
||||
- 更详细的错误消息
|
||||
- 错误恢复建议
|
||||
- 错误日志导出
|
||||
|
||||
### 长期改进 / Long-term Improvements
|
||||
|
||||
1. **安全性增强 / Security Enhancement**
|
||||
|
||||
```typescript
|
||||
// 建议实现密码加密
|
||||
interface SecureSettingsData extends SettingsData {
|
||||
erp: {
|
||||
...ErpConfig
|
||||
encryptedPassword: string // 替代明文密码
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **配置版本控制 / Configuration Versioning**
|
||||
- 实现配置历史记录
|
||||
- 支持回滚到之前版本
|
||||
- 配置变更审计日志
|
||||
|
||||
3. **实时配置重载 / Live Config Reload**
|
||||
- 监听 .env 文件变化
|
||||
- 自动重载配置
|
||||
- 通知相关服务更新
|
||||
|
||||
---
|
||||
|
||||
## 测试建议 / Testing Recommendations
|
||||
|
||||
### 单元测试 / Unit Tests
|
||||
|
||||
```typescript
|
||||
// 测试用例示例
|
||||
describe('ConfigManager', () => {
|
||||
it('should save settings successfully', async () => {
|
||||
const manager = ConfigManager.getInstance()
|
||||
const settings: SettingsData = {
|
||||
/* mock data */
|
||||
}
|
||||
const result = await manager.saveAllSettings(settings)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle file write errors', async () => {
|
||||
// Mock fs.writeFileSync to throw error
|
||||
const result = await manager.saveAllSettings(settings)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 集成测试 / Integration Tests
|
||||
|
||||
```typescript
|
||||
describe('Settings Save Flow', () => {
|
||||
it('should complete full save cycle', async () => {
|
||||
// 1. User modifies settings
|
||||
// 2. Clicks save button
|
||||
// 3. Verifies .env file updated
|
||||
// 4. Confirms UI feedback
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录 / Appendix
|
||||
|
||||
### 完整配置字段列表 / Complete Configuration Field List
|
||||
|
||||
| 类别 / Category | 字段数 / Field Count | 字段列表 / Field List |
|
||||
| ---------------- | -------------------- | ---------------------------------------------------------------------- |
|
||||
| ERP | 6 | url, username, password, headless, ignoreHttpsErrors, autoCloseBrowser |
|
||||
| Database | 7 | dbType, server, mysqlHost, mysqlPort, database, username, password |
|
||||
| Paths | 3 | dataDir, defaultOutput, validationOutput |
|
||||
| Extraction | 5 | batchSize, verbose, autoConvert, mergeBatches, enableDbPersistence |
|
||||
| Validation | 5 | dataSource, batchSize, matchMode, enableCrud, defaultManager |
|
||||
| UI | 3 | fontFamily, fontSize, productionIdInputWidth |
|
||||
| Execution | 1 | dryRun |
|
||||
| **总计 / Total** | **30** | |
|
||||
|
||||
### 相关文档 / Related Documentation
|
||||
|
||||
- [Electron Security Guidelines](https://www.electronjs.org/docs/latest/tutorial/security)
|
||||
- [IPC 通信最佳实践](https://www.electronjs.org/docs/latest/tutorial/ipc)
|
||||
- [环境变量管理规范](.env.example)
|
||||
|
||||
### 版本历史 / Version History
|
||||
|
||||
| 版本 / Version | 日期 / Date | 变更 / Changes |
|
||||
| -------------- | ----------- | -------------------------- |
|
||||
| 1.0 | 2025-03-03 | 初始版本 / Initial version |
|
||||
|
||||
---
|
||||
|
||||
**文档生成时间 / Document Generated:** 2025-03-03
|
||||
**最后更新 / Last Updated:** 2025-03-03
|
||||
**维护者 / Maintainer:** ERPAuto Development Team
|
||||
246
docs/features/use-cleaner-refactor-overview.md
Normal file
246
docs/features/use-cleaner-refactor-overview.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# useCleaner 重构说明
|
||||
|
||||
本文档记录 `src/renderer/src/hooks/useCleaner.ts` 的第一阶段重构工作。目标不是一次性把整个 Cleaner 页面完全组件化,而是优先拆出共享类型、纯函数和 IPC 编排逻辑,让 `useCleaner` 从“大而全逻辑容器”逐步收敛为“组合层”。
|
||||
|
||||
## 1. 重构背景
|
||||
|
||||
重构前,`useCleaner.ts` 同时负责:
|
||||
|
||||
- 页面初始化
|
||||
- 权限判断
|
||||
- sessionStorage 持久化
|
||||
- 校验请求
|
||||
- 结果筛选
|
||||
- 勾选状态处理
|
||||
- 删除计划构建
|
||||
- 保存物料变更
|
||||
- Cleaner 执行编排
|
||||
- 导出编排
|
||||
- 弹窗确认
|
||||
- 报告状态维护
|
||||
|
||||
这导致它虽然名义上是一个 hook,但实际上已经接近一个“前端页面服务总线”。
|
||||
|
||||
## 2. 重构目标
|
||||
|
||||
本次重构目标是:
|
||||
|
||||
- 提取共享类型,消除重复定义
|
||||
- 提取纯函数,隔离无副作用逻辑
|
||||
- 提取 IPC / 异步编排,隔离对 `window.electron` 的直接调用
|
||||
- 保持 `useCleaner()` 返回值和 `CleanerPage.tsx` 使用方式不变
|
||||
|
||||
## 3. 重构后结构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Page[CleanerPage.tsx]
|
||||
Hook[useCleaner.ts]
|
||||
|
||||
subgraph CleanerHookModules[Cleaner Hook Modules]
|
||||
Types[hooks/cleaner/types.ts]
|
||||
Helpers[hooks/cleaner/helpers.ts]
|
||||
Api[hooks/cleaner/api.ts]
|
||||
end
|
||||
|
||||
subgraph ExternalDeps[External Dependencies]
|
||||
Electron[window.electron]
|
||||
Store[useAppStore / Toast]
|
||||
Dialog[ConfirmDialog]
|
||||
end
|
||||
|
||||
Page --> Hook
|
||||
Hook --> Types
|
||||
Hook --> Helpers
|
||||
Hook --> Api
|
||||
Hook --> Store
|
||||
Hook --> Dialog
|
||||
Api --> Electron
|
||||
```
|
||||
|
||||
## 4. 本次拆分内容
|
||||
|
||||
### 4.1 共享类型
|
||||
|
||||
新增:
|
||||
|
||||
- `src/renderer/src/hooks/cleaner/types.ts`
|
||||
|
||||
统一收敛了以下类型:
|
||||
|
||||
- `ValidationRequest`
|
||||
- `ValidationResult`
|
||||
- `ValidationStats`
|
||||
- `ValidationResponsePayload`
|
||||
- `CleanerProgress`
|
||||
- `CleanerReportData`
|
||||
- `CleanerInitializationResult`
|
||||
- `CleanerConfigResult`
|
||||
|
||||
这一步解决了原来多个文件重复定义同类类型的问题,比如:
|
||||
|
||||
- `useCleaner.ts`
|
||||
- `useValidation.ts`
|
||||
- `ExecutionReportDialog.tsx`
|
||||
|
||||
### 4.2 纯函数与数据构造
|
||||
|
||||
新增:
|
||||
|
||||
- `src/renderer/src/hooks/cleaner/helpers.ts`
|
||||
|
||||
提取出的纯函数包括:
|
||||
|
||||
- `getStoredBoolean()`
|
||||
- `getStoredValidationMode()`
|
||||
- `filterValidationResults()`
|
||||
- `buildDeletionPlan()`
|
||||
- `buildExportItems()`
|
||||
|
||||
这些逻辑之前都散落在 `useCleaner.ts` 的 `useMemo` 或事件处理函数里,现在可以单独测试。
|
||||
|
||||
### 4.3 IPC 与异步编排
|
||||
|
||||
新增:
|
||||
|
||||
- `src/renderer/src/hooks/cleaner/api.ts`
|
||||
|
||||
提取出的异步编排包括:
|
||||
|
||||
- `initializeCleanerPage()`
|
||||
- `loadCleanerConfig()`
|
||||
- `runValidationRequest()`
|
||||
- `saveDeletionPlan()`
|
||||
- `reloadManagers()`
|
||||
- `runCleanerExecution()`
|
||||
- `exportCleanerResults()`
|
||||
|
||||
这样做之后,`useCleaner.ts` 不再需要在每个 handler 里直接拼接 `window.electron.xxx` 调用细节。
|
||||
|
||||
## 5. useCleaner 的角色变化
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Before[重构前]
|
||||
A[useCleaner.ts]
|
||||
A --> A1[本地状态]
|
||||
A --> A2[筛选逻辑]
|
||||
A --> A3[删除计划构建]
|
||||
A --> A4[执行清理请求]
|
||||
A --> A5[导出请求]
|
||||
A --> A6[初始化请求]
|
||||
A --> A7[共享类型定义]
|
||||
end
|
||||
|
||||
subgraph After[重构后]
|
||||
B[useCleaner.ts]
|
||||
B --> B1[组合状态]
|
||||
B --> B2[调用 helpers]
|
||||
B --> B3[调用 api]
|
||||
|
||||
C[helpers.ts]
|
||||
D[api.ts]
|
||||
E[types.ts]
|
||||
|
||||
B --> C
|
||||
B --> D
|
||||
B --> E
|
||||
end
|
||||
```
|
||||
|
||||
重构后,`useCleaner.ts` 更接近“组合层”:
|
||||
|
||||
- 管理 React state
|
||||
- 串联用户交互流程
|
||||
- 调用 helpers 和 api
|
||||
- 将最终能力暴露给页面
|
||||
|
||||
## 6. 受影响的文件
|
||||
|
||||
### 6.1 主体修改
|
||||
|
||||
- `src/renderer/src/hooks/useCleaner.ts`
|
||||
- `src/renderer/src/hooks/useValidation.ts`
|
||||
- `src/renderer/src/components/ExecutionReportDialog.tsx`
|
||||
|
||||
### 6.2 新增模块
|
||||
|
||||
- `src/renderer/src/hooks/cleaner/types.ts`
|
||||
- `src/renderer/src/hooks/cleaner/helpers.ts`
|
||||
- `src/renderer/src/hooks/cleaner/api.ts`
|
||||
|
||||
### 6.3 新增测试
|
||||
|
||||
- `tests/unit/cleaner-helpers.test.ts`
|
||||
|
||||
## 7. 具体收益
|
||||
|
||||
### 7.1 类型一致性提升
|
||||
|
||||
之前 `ValidationResult`、`CleanerProgress` 在多个文件重复定义,修改字段时容易遗漏。
|
||||
现在统一从 `hooks/cleaner/types.ts` 引用,降低了类型漂移风险。
|
||||
|
||||
### 7.2 可测试性提升
|
||||
|
||||
原先删除计划构建、筛选和导出映射逻辑只能通过 hook 间接覆盖。
|
||||
现在这些逻辑已经被抽成纯函数,可以直接做单测。
|
||||
|
||||
### 7.3 Hook 复杂度下降
|
||||
|
||||
虽然 `useCleaner.ts` 还没有变成一个很小的文件,但其中的“细节密度”已经明显下降:
|
||||
|
||||
- 数据变换逻辑外提
|
||||
- API 编排逻辑外提
|
||||
- 重复类型移除
|
||||
|
||||
### 7.4 为下一步组件拆分做准备
|
||||
|
||||
后续如果要拆 `CleanerPage.tsx`:
|
||||
|
||||
- 左侧筛选区
|
||||
- 表格工具栏
|
||||
- 底部执行区
|
||||
|
||||
这些组件就可以直接消费已经整理好的 hook 能力,而不是继续把逻辑往页面里塞。
|
||||
|
||||
## 8. 验证方式
|
||||
|
||||
本次重构后执行了以下验证:
|
||||
|
||||
- `npm run typecheck:node`
|
||||
- `tests/unit/cleaner-helpers.test.ts`
|
||||
- `tests/unit/cleaner.test.ts`
|
||||
|
||||
## 9. 新增测试覆盖点
|
||||
|
||||
`tests/unit/cleaner-helpers.test.ts` 覆盖了:
|
||||
|
||||
- 非管理员筛选逻辑
|
||||
- 删除计划构建逻辑
|
||||
- 导出数据构建逻辑
|
||||
|
||||
## 10. 仍然保留在 useCleaner 中的内容
|
||||
|
||||
为了控制改动风险,这次没有继续下沉以下能力:
|
||||
|
||||
- `ConfirmDialog` 的 Promise 封装
|
||||
- 编辑状态 `editingCell / editValue`
|
||||
- `isRunning / isExecuting / isReportDialogOpen` 等 UI 状态
|
||||
- 页面层直接依赖的完整返回对象
|
||||
|
||||
这些能力仍然保留在 `useCleaner.ts`,因为它们和当前页面交互绑定较深。
|
||||
|
||||
## 11. 下一步建议
|
||||
|
||||
基于目前的结构,建议下一阶段继续做:
|
||||
|
||||
1. 拆 `CleanerPage.tsx` 为“左侧筛选区”和“右侧结果与执行区”两个子组件。
|
||||
2. 将 `showConfirmDialog()` 封装为独立 hook,例如 `useConfirmDialogController()`。
|
||||
3. 将 inline edit 相关逻辑提取到更专门的 manager-assignment controller。
|
||||
4. 视情况把 Cleaner 相关状态进一步收敛到专门 store 或 domain hook 中。
|
||||
|
||||
## 12. 总结
|
||||
|
||||
这次 `useCleaner` 重构的核心价值,不是“让文件立刻变得很小”,而是先把最容易复用、最适合测试、最不应继续堆在 hook 里的部分拆出来。
|
||||
|
||||
它为接下来的页面组件拆分提供了一个更稳的基础,也让 Cleaner 模块开始从“页面驱动逻辑”向“模块化前端能力”转变。
|
||||
287
docs/features/user-override-match-feature.md
Normal file
287
docs/features/user-override-match-feature.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# 物料匹配算法增强 - 用户覆盖匹配功能
|
||||
|
||||
**实施日期**: 2026-03-03
|
||||
**功能版本**: 1.0
|
||||
**修改文件**: `src/main/ipc/validation-handler.ts`
|
||||
|
||||
---
|
||||
|
||||
## 功能概述
|
||||
|
||||
为 **User 用户类型** 在物料清理界面增加了 **优先级3:用户覆盖匹配** 功能,确保 User 用户能够优先看到并管理与自己关键词匹配的物料。
|
||||
|
||||
---
|
||||
|
||||
## 实现的更改
|
||||
|
||||
### 1. 获取当前用户信息
|
||||
|
||||
**位置**: `validation-handler.ts:218-239`
|
||||
|
||||
```typescript
|
||||
// Get current user info
|
||||
const sessionManager = (
|
||||
await import('../services/user/session-manager')
|
||||
).SessionManager.getInstance()
|
||||
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
if (!userInfo) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户未登录',
|
||||
stats: {
|
||||
totalRecords: 0,
|
||||
matchedCount: 0,
|
||||
markedCount: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isAdmin = userInfo.userType === 'Admin'
|
||||
const username = userInfo.username
|
||||
|
||||
log.info('Starting validation', { mode: request.mode, user: username, isAdmin })
|
||||
```
|
||||
|
||||
**说明**:
|
||||
|
||||
- 在 `validation:validate` handler 开始时获取当前登录用户信息
|
||||
- 提取 `isAdmin` 和 `username` 用于后续匹配逻辑
|
||||
- 如果用户未登录,返回错误响应
|
||||
|
||||
### 2. 新增优先级3:用户覆盖匹配
|
||||
|
||||
**位置**: `validation-handler.ts:359-370`
|
||||
|
||||
```typescript
|
||||
// Priority 3: User Override Match (only for non-admin users)
|
||||
// Override with current user's typeKeyword if available
|
||||
if (!isAdmin && username) {
|
||||
const userKeywords = typeKeywords.filter((tk) => tk.managerName === username)
|
||||
for (const userKeyword of userKeywords) {
|
||||
if (userKeyword.materialName && materialName.includes(userKeyword.materialName)) {
|
||||
matchedTypeKeyword = userKeyword.materialName
|
||||
managerName = userKeyword.managerName
|
||||
break // Force override with first match
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**匹配逻辑**:
|
||||
|
||||
1. **适用范围**: 仅对 `isAdmin === false` 的 User 用户生效
|
||||
2. **筛选关键词**: 从 `typeKeywords` 中筛选 `managerName === username` 的记录
|
||||
3. **匹配规则**: 使用 `materialName.includes(userKeyword.materialName)` 包含关系匹配
|
||||
4. **强制覆盖**: 只要匹配成功,立即覆盖原有的 `managerName` 和 `matchedTypeKeyword`
|
||||
5. **无匹配时**: 保持优先级2的匹配结果不变
|
||||
|
||||
---
|
||||
|
||||
## 匹配优先级(更新后)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Start([物料数据]) --> P1{优先级1<br/>MaterialsToBeDeleted<br/>精确匹配?}
|
||||
P1 -->|MaterialCode匹配| M1[✅ 已标记删除<br/>isMarkedForDeletion=true]
|
||||
P1 -->|未匹配| P2{优先级2<br/>MaterialsTypeToBeDeleted<br/>包含匹配?}
|
||||
|
||||
P2 -->|匹配到| M2[⚠️ 类型匹配<br/>managerName=其他用户]
|
||||
P2 -->|未匹配| M3[❌ 未匹配<br/>managerName='']
|
||||
|
||||
M1 --> Check{用户类型?}
|
||||
M2 --> Check
|
||||
M3 --> Check
|
||||
|
||||
Check -->|Admin| Skip[跳过覆盖]
|
||||
Check -->|User| P3{优先级3<br/>用户覆盖匹配?}
|
||||
|
||||
P3 -->|匹配成功| Override[✅ 覆为当前用户<br/>managerName=当前用户]
|
||||
P3 -->|未匹配| Keep[保持原结果]
|
||||
|
||||
Skip --> End([返回结果])
|
||||
Override --> End
|
||||
Keep --> End
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试场景
|
||||
|
||||
### 场景1: User 用户匹配到自己的 typeKeyword
|
||||
|
||||
**输入**:
|
||||
|
||||
- 当前用户: `user1`
|
||||
- 物料名称: `螺丝 M6`
|
||||
- MaterialsTypeToBeDeleted: `{ materialName: "螺丝", managerName: "user1" }`
|
||||
|
||||
**预期输出**:
|
||||
|
||||
```json
|
||||
{
|
||||
"materialName": "螺丝 M6",
|
||||
"managerName": "user1",
|
||||
"matchedTypeKeyword": "螺丝",
|
||||
"isMarkedForDeletion": false
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2: User 用户覆盖其他用户的匹配
|
||||
|
||||
**输入**:
|
||||
|
||||
- 当前用户: `user1`
|
||||
- 物料名称: `螺丝 M6`
|
||||
- MaterialsTypeToBeDeleted:
|
||||
- `{ materialName: "螺丝", managerName: "user2" }`
|
||||
- `{ materialName: "螺丝", managerName: "user1" }`
|
||||
|
||||
**优先级2结果**: `managerName = "user2"`
|
||||
**优先级3结果**: `managerName = "user1"` ✅ 强制覆盖
|
||||
|
||||
### 场景3: User 用户无匹配关键词
|
||||
|
||||
**输入**:
|
||||
|
||||
- 当前用户: `user1`
|
||||
- 物料名称: `螺丝 M6`
|
||||
- MaterialsTypeToBeDeleted:
|
||||
- `{ materialName: "螺丝", managerName: "user2" }`
|
||||
|
||||
**预期输出**:
|
||||
|
||||
```json
|
||||
{
|
||||
"materialName": "螺丝 M6",
|
||||
"managerName": "user2",
|
||||
"matchedTypeKeyword": "螺丝",
|
||||
"isMarkedForDeletion": false
|
||||
}
|
||||
```
|
||||
|
||||
**说明**: 保持优先级2的匹配结果
|
||||
|
||||
### 场景4: Admin 用户不执行覆盖
|
||||
|
||||
**输入**:
|
||||
|
||||
- 当前用户: `admin` (isAdmin=true)
|
||||
- 物料名称: `螺丝 M6`
|
||||
- MaterialsTypeToBeDeleted:
|
||||
- `{ materialName: "螺丝", managerName: "user1" }`
|
||||
- `{ materialName: "螺丝", managerName: "admin" }`
|
||||
|
||||
**预期输出**:
|
||||
|
||||
```json
|
||||
{
|
||||
"materialName": "螺丝 M6",
|
||||
"managerName": "user1",
|
||||
"matchedTypeKeyword": "螺丝",
|
||||
"isMarkedForDeletion": false
|
||||
}
|
||||
```
|
||||
|
||||
**说明**: Admin 不执行优先级3,保持原有匹配行为
|
||||
|
||||
### 场景5: 优先级1匹配不受影响
|
||||
|
||||
**输入**:
|
||||
|
||||
- 当前用户: `user1`
|
||||
- 物料代码: `MAT001`
|
||||
- MaterialsToBeDeleted: `{ materialCode: "MAT001", managerName: "user2" }`
|
||||
|
||||
**预期输出**:
|
||||
|
||||
```json
|
||||
{
|
||||
"materialCode": "MAT001",
|
||||
"managerName": "user2",
|
||||
"isMarkedForDeletion": true,
|
||||
"matchedTypeKeyword": undefined
|
||||
}
|
||||
```
|
||||
|
||||
**说明**: 优先级1的精确匹配不受覆盖影响
|
||||
|
||||
---
|
||||
|
||||
## 数据库配置示例
|
||||
|
||||
### MaterialsTypeToBeDeleted 表数据
|
||||
|
||||
| MaterialName | ManagerName | 说明 |
|
||||
| ------------ | ----------- | ------------------------------ |
|
||||
| 螺丝 | user1 | user1 负责所有包含"螺丝"的物料 |
|
||||
| 螺母 | user2 | user2 负责所有包含"螺母"的物料 |
|
||||
| 垫圈 | user1 | user1 也负责"垫圈"类物料 |
|
||||
| 电缆 | admin | admin 负责电缆类物料 |
|
||||
|
||||
### 匹配结果示例
|
||||
|
||||
| 物料名称 | 当前用户 | 原匹配 (优先级2) | 覆盖后 (优先级3) |
|
||||
| -------- | -------- | ---------------- | ----------------- |
|
||||
| 螺丝 M6 | user1 | user2 | **user1** ✅ |
|
||||
| 螺母 M8 | user1 | user2 | user2 (无匹配) |
|
||||
| 垫圈 φ10 | user1 | user2 | **user1** ✅ |
|
||||
| 电缆 5m | user1 | admin | user1 (无匹配) |
|
||||
| 螺丝 M6 | admin | user2 | user2 (Admin跳过) |
|
||||
|
||||
---
|
||||
|
||||
## 与前端协同
|
||||
|
||||
前端过滤器逻辑 (`CleanerPage.tsx`) 保持不变:
|
||||
|
||||
```typescript
|
||||
const filteredResults = React.useMemo(() => {
|
||||
let results = validationResults
|
||||
if (!isAdmin && currentUsername) {
|
||||
// User 只看到自己的物料 + 未分配的物料
|
||||
results = results.filter((r) => r.managerName === currentUsername || !r.managerName)
|
||||
}
|
||||
return results
|
||||
}, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems])
|
||||
```
|
||||
|
||||
**协同效果**:
|
||||
|
||||
1. 后端匹配算法确保 User 用户的物料优先分配给自己
|
||||
2. 前端过滤器只显示属于当前用户或未分配的物料
|
||||
3. Admin 用户可以看到所有物料并切换查看不同负责人
|
||||
|
||||
---
|
||||
|
||||
## 代码审查检查点
|
||||
|
||||
- ✅ User 信息获取正确使用 `SessionManager`
|
||||
- ✅ 只对 `!isAdmin` 的用户执行覆盖逻辑
|
||||
- ✅ 使用相同的包含匹配规则 `materialName.includes(typeKeyword.materialName)`
|
||||
- ✅ 优先级1(精确匹配)不受覆盖影响
|
||||
- ✅ 无匹配时保持原有结果
|
||||
- ✅ 日志记录包含用户信息 `{ user: username, isAdmin }`
|
||||
- ✅ 未登录时返回明确的错误信息
|
||||
|
||||
---
|
||||
|
||||
## 潜在改进方向
|
||||
|
||||
1. **性能优化**: 如果 `typeKeywords` 数量很大,可以预先构建 `Map<username, typeKeyword[]>` 索引
|
||||
2. **日志增强**: 添加覆盖匹配的统计信息(覆盖了多少条记录)
|
||||
3. **配置开关**: 允许 Admin 用户通过配置启用/禁用覆盖功能
|
||||
4. **UI 反馈**: 在前端显示哪些物料是通过覆盖匹配分配的
|
||||
|
||||
---
|
||||
|
||||
## 相关文件
|
||||
|
||||
- **实现文件**: `src/main/ipc/validation-handler.ts` (Lines 218-239, 359-370)
|
||||
- **前端页面**: `src/renderer/src/pages/CleanerPage.tsx`
|
||||
- **会话管理**: `src/main/services/user/session-manager.ts`
|
||||
- **类型定义**: `src/main/types/validation.types.ts`
|
||||
|
||||
---
|
||||
|
||||
**文档结束**
|
||||
217
docs/features/validation-handler-refactor-overview.md
Normal file
217
docs/features/validation-handler-refactor-overview.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# validation-handler 重构说明
|
||||
|
||||
本文档记录 `src/main/ipc/validation-handler.ts` 的第一阶段重构工作,目标是把“超大 IPC Handler”拆回到更清晰的职责边界中,同时保持对外 IPC 协议和业务行为不变。
|
||||
|
||||
## 1. 重构背景
|
||||
|
||||
重构前,`validation-handler.ts` 同时承担了以下职责:
|
||||
|
||||
- IPC 通道注册
|
||||
- 跨页面共享 `Production ID` 状态
|
||||
- 数据库连接创建与释放
|
||||
- MySQL / SQL Server 方言分支
|
||||
- 输入识别与订单号解析
|
||||
- 物料校验结果组装
|
||||
- Cleaner 执行前数据准备
|
||||
- 物料查询与富化
|
||||
|
||||
这种结构的主要问题是:
|
||||
|
||||
- 文件过大,理解成本高
|
||||
- 数据库和业务规则直接堆叠在 IPC 层
|
||||
- 复用困难,后续其他模块无法直接复用这些逻辑
|
||||
- 单元测试难以细粒度编写
|
||||
|
||||
## 2. 重构目标
|
||||
|
||||
本次重构聚焦在“职责下沉、行为不变”:
|
||||
|
||||
- 保留原有 IPC channel 和返回结构
|
||||
- 将共享状态、数据库工厂、输入解析、验证业务流程拆出
|
||||
- 让 `validation-handler.ts` 回归为薄 IPC 壳层
|
||||
- 为后续继续拆 `cleaner-handler`、前端校验流程提供复用基础
|
||||
|
||||
## 3. 重构后结构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Renderer[Renderer / Preload]
|
||||
Handler[validation-handler.ts]
|
||||
|
||||
subgraph ValidationServices[Validation Services]
|
||||
Store[shared-production-ids-store.ts]
|
||||
DbFactory[validation-database.ts]
|
||||
InputSvc[production-input-service.ts]
|
||||
AppSvc[validation-application-service.ts]
|
||||
end
|
||||
|
||||
subgraph ExistingServices[Existing Services]
|
||||
MaterialsDAO[MaterialsToBeDeletedDAO]
|
||||
PlanDAO[DiscreteMaterialPlanDAO]
|
||||
Session[SessionManager]
|
||||
end
|
||||
|
||||
Renderer --> Handler
|
||||
Handler --> Session
|
||||
Handler --> Store
|
||||
Handler --> AppSvc
|
||||
Handler --> MaterialsDAO
|
||||
|
||||
AppSvc --> Store
|
||||
AppSvc --> DbFactory
|
||||
AppSvc --> InputSvc
|
||||
AppSvc --> MaterialsDAO
|
||||
AppSvc --> PlanDAO
|
||||
```
|
||||
|
||||
## 4. 新增与调整的文件
|
||||
|
||||
### 4.1 IPC 薄壳
|
||||
|
||||
- `src/main/ipc/validation-handler.ts`
|
||||
|
||||
职责收敛为:
|
||||
|
||||
- 注册 IPC handler
|
||||
- 从 `SessionManager` 读取当前用户
|
||||
- 调用应用服务
|
||||
- 对简单 DAO 操作做最轻量转发
|
||||
|
||||
### 4.2 共享状态模块
|
||||
|
||||
- `src/main/services/validation/shared-production-ids-store.ts`
|
||||
|
||||
职责:
|
||||
|
||||
- 管理按 `senderId` 隔离的共享 `Production IDs`
|
||||
- 提供 `set/get/clear`
|
||||
|
||||
价值:
|
||||
|
||||
- 将原本散落在 handler 文件顶部的状态提升为独立服务
|
||||
- 后续如果要迁移到更持久的 session store,只需替换这一层
|
||||
|
||||
### 4.3 数据库创建与表名适配
|
||||
|
||||
- `src/main/services/validation/validation-database.ts`
|
||||
|
||||
职责:
|
||||
|
||||
- 创建用于 validation 相关流程的数据库服务
|
||||
- 提供 `getValidationTableName()` 做表名方言转换
|
||||
|
||||
价值:
|
||||
|
||||
- 收敛 MySQL / SQL Server 的连接逻辑
|
||||
- 避免 IPC 文件里反复出现数据库构造代码
|
||||
|
||||
### 4.4 输入解析服务
|
||||
|
||||
- `src/main/services/validation/production-input-service.ts`
|
||||
|
||||
职责:
|
||||
|
||||
- 读取 Production ID 文件
|
||||
- 识别输入是 `production_id`、`order_number` 还是 `unknown`
|
||||
- 从输入解析出订单号列表
|
||||
|
||||
价值:
|
||||
|
||||
- 把“输入解析规则”变成可复用、可测试的纯业务模块
|
||||
|
||||
### 4.5 应用服务
|
||||
|
||||
- `src/main/services/validation/validation-application-service.ts`
|
||||
|
||||
职责:
|
||||
|
||||
- 校验流程编排
|
||||
- Cleaner 数据准备
|
||||
- 物料按负责人查询 / 全量查询的富化逻辑
|
||||
- 统一管理数据库生命周期
|
||||
|
||||
价值:
|
||||
|
||||
- 形成明确的 application service 层
|
||||
- 让后续业务扩展不再从 IPC 文件开刀
|
||||
|
||||
## 5. 重构前后职责对比
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Before[重构前]
|
||||
A1[validation-handler.ts]
|
||||
A1 --> A2[IPC 注册]
|
||||
A1 --> A3[共享状态]
|
||||
A1 --> A4[数据库连接]
|
||||
A1 --> A5[输入解析]
|
||||
A1 --> A6[校验编排]
|
||||
A1 --> A7[物料富化]
|
||||
A1 --> A8[Cleaner 数据准备]
|
||||
end
|
||||
|
||||
subgraph After[重构后]
|
||||
B1[validation-handler.ts]
|
||||
B2[shared-production-ids-store.ts]
|
||||
B3[validation-database.ts]
|
||||
B4[production-input-service.ts]
|
||||
B5[validation-application-service.ts]
|
||||
|
||||
B1 --> B5
|
||||
B1 --> B2
|
||||
B5 --> B3
|
||||
B5 --> B4
|
||||
end
|
||||
```
|
||||
|
||||
## 6. 本次保留不变的部分
|
||||
|
||||
为了控制风险,这次没有修改以下内容:
|
||||
|
||||
- IPC channel 名称
|
||||
- Preload / Renderer 调用方式
|
||||
- 物料匹配规则
|
||||
- Cleaner 数据准备规则
|
||||
- DAO 层的既有 SQL 结构
|
||||
|
||||
也就是说,这次更像是一次“结构性搬迁”,不是业务规则改造。
|
||||
|
||||
## 7. 验证方式
|
||||
|
||||
本次重构完成后,做了以下验证:
|
||||
|
||||
- `npm run typecheck:node`
|
||||
- `tests/unit/shared-production-ids-store.test.ts`
|
||||
- `tests/unit/production-input-service.test.ts`
|
||||
- 既有 `tests/unit/ipc-index.test.ts`
|
||||
|
||||
## 8. 新增测试
|
||||
|
||||
新增测试文件:
|
||||
|
||||
- `tests/unit/shared-production-ids-store.test.ts`
|
||||
- `tests/unit/production-input-service.test.ts`
|
||||
|
||||
覆盖内容:
|
||||
|
||||
- sender 隔离存储
|
||||
- 去重行为
|
||||
- 清空逻辑
|
||||
- 输入类型识别
|
||||
|
||||
## 9. 收益总结
|
||||
|
||||
这次重构带来的直接收益:
|
||||
|
||||
- `validation-handler.ts` 不再承担过多业务职责
|
||||
- validation 相关逻辑形成了可复用服务层
|
||||
- 输入解析与共享状态有了独立测试入口
|
||||
- 后续继续拆 `cleaner-handler` 时,可以直接复用订单号解析和 cleaner 数据准备逻辑
|
||||
|
||||
## 10. 后续建议
|
||||
|
||||
建议在这个基础上继续推进:
|
||||
|
||||
1. 将 `validation-application-service.ts` 中的 SQL Server / MySQL 分支继续下沉到 repository 或 dialect adapter。
|
||||
2. 逐步给 `getCleanerData()`、`getMaterialsByManager()` 这类编排逻辑补更多单测。
|
||||
3. 把和 validation 强耦合的 renderer 逻辑改成显式依赖 application contract,而不是隐式依赖 payload shape。
|
||||
Reference in New Issue
Block a user