Merge branch 'cleanup/remove-env-variables' into dev
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -33,3 +33,5 @@ logs
|
|||||||
# Runtime config files
|
# Runtime config files
|
||||||
*.yaml
|
*.yaml
|
||||||
*.yaml.backup
|
*.yaml.backup
|
||||||
|
# But keep config.template.yaml
|
||||||
|
!config.template.yaml
|
||||||
|
|||||||
20
CLAUDE.md
20
CLAUDE.md
@@ -44,7 +44,7 @@ ERPAuto is an **Electron desktop application** for automating ERP system data pr
|
|||||||
- Node.js environment managing application lifecycle
|
- Node.js environment managing application lifecycle
|
||||||
- Entry point: `src/main/index.ts`
|
- Entry point: `src/main/index.ts`
|
||||||
- Registers all IPC handlers via `registerIpcHandlers()`
|
- Registers all IPC handlers via `registerIpcHandlers()`
|
||||||
- Loads environment variables from `.env` at startup
|
- Loads configuration from `config.yaml` via ConfigManager at startup
|
||||||
|
|
||||||
2. **Preload Script** (`src/preload/`)
|
2. **Preload Script** (`src/preload/`)
|
||||||
- Security bridge between main and renderer processes
|
- Security bridge between main and renderer processes
|
||||||
@@ -113,13 +113,23 @@ Admin users see logout buttons and can access user switching. Non-admin users ha
|
|||||||
- `@services` → `src/main/services` (main process, tests only)
|
- `@services` → `src/main/services` (main process, tests only)
|
||||||
- `@types` → `src/main/types` (main process, tests only)
|
- `@types` → `src/main/types` (main process, tests only)
|
||||||
|
|
||||||
## Environment Configuration
|
## Configuration Management
|
||||||
|
|
||||||
The application requires a `.env` file in the project root. Reference `.env.example` for the full structure. Key configurations:
|
The application uses a YAML-based configuration system (`config.yaml`) managed by `ConfigManager`:
|
||||||
|
|
||||||
- **ERP Settings**: URL, credentials, headless mode, HTTPS error handling
|
- **Development**: `config.yaml` in project root (easy to edit and version control)
|
||||||
|
- **Production**: `config.yaml` in user data directory (AppData on Windows)
|
||||||
|
|
||||||
|
Key configurations in `config.yaml`:
|
||||||
|
|
||||||
|
- **ERP Settings**: URL (fixed infrastructure)
|
||||||
- **Database**: MySQL and SQL Server connection configs (dual support)
|
- **Database**: MySQL and SQL Server connection configs (dual support)
|
||||||
- **App Settings**: Log level, download/temp directories
|
- **Paths**: Data directory and output file settings
|
||||||
|
- **Extraction**: Batch size, verbosity, persistence options
|
||||||
|
- **Validation**: Data source, batch size, match mode
|
||||||
|
- **Order Resolution**: Database table and field names for order number lookup
|
||||||
|
|
||||||
|
Note: ERP credentials (username/password) are stored in the database (`dbo_BIPUsers` table) per user, managed via the Settings UI.
|
||||||
|
|
||||||
## Key Technologies
|
## Key Technologies
|
||||||
|
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -30,29 +30,43 @@ npm install
|
|||||||
|
|
||||||
### 配置
|
### 配置
|
||||||
|
|
||||||
在项目根目录创建 `.env` 文件:
|
在项目根目录创建 `config.yaml` 文件(可参考 `config.template.yaml`):
|
||||||
|
|
||||||
```bash
|
```yaml
|
||||||
# ERP 配置
|
# ERP 配置(固定基础设施)
|
||||||
ERP_URL=https://your-erp-server.com
|
erp:
|
||||||
ERP_USERNAME=your_username
|
url: https://your-erp-server.com
|
||||||
ERP_PASSWORD=your_password
|
|
||||||
|
|
||||||
# MySQL 配置(可选)
|
# 数据库配置
|
||||||
MYSQL_HOST=localhost
|
database:
|
||||||
MYSQL_PORT=3306
|
activeType: mysql # 或 sqlserver
|
||||||
MYSQL_USER=root
|
|
||||||
MYSQL_PASSWORD=password
|
|
||||||
MYSQL_DATABASE=erpauto
|
|
||||||
|
|
||||||
# SQL Server 配置(可选)
|
mysql:
|
||||||
SQL_SERVER_HOST=localhost
|
host: localhost
|
||||||
SQL_SERVER_PORT=1433
|
port: 3306
|
||||||
SQL_SERVER_USER=sa
|
database: erpauto
|
||||||
SQL_SERVER_PASSWORD=password
|
username: root
|
||||||
SQL_SERVER_DATABASE=erpauto
|
password: your_password
|
||||||
|
charset: utf8mb4
|
||||||
|
|
||||||
|
sqlserver:
|
||||||
|
server: localhost
|
||||||
|
port: 1433
|
||||||
|
database: erpauto
|
||||||
|
username: sa
|
||||||
|
password: your_password
|
||||||
|
driver: 'ODBC Driver 18 for SQL Server'
|
||||||
|
trustServerCertificate: true
|
||||||
|
|
||||||
|
# 路径配置
|
||||||
|
paths:
|
||||||
|
dataDir: './data/'
|
||||||
|
defaultOutput: 'output.xlsx'
|
||||||
|
validationOutput: 'validation-result.xlsx'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**注意**:ERP 用户名和密码在应用的设置界面中配置,存储在数据库中(按用户管理)。
|
||||||
|
|
||||||
### 运行开发环境
|
### 运行开发环境
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -136,9 +150,10 @@ ERPAuto/
|
|||||||
|
|
||||||
### 无法连接 ERP 系统
|
### 无法连接 ERP 系统
|
||||||
|
|
||||||
1. 检查 `.env` 文件中的 ERP_URL 是否正确
|
1. 检查 `config.yaml` 中的 ERP URL 是否正确
|
||||||
2. 确认网络连接正常
|
2. 确认网络连接正常
|
||||||
3. 检查 ERP 系统是否可访问
|
3. 检查 ERP 系统是否可访问
|
||||||
|
4. 在设置界面中确认 ERP 用户名和密码已配置
|
||||||
|
|
||||||
### 提取失败
|
### 提取失败
|
||||||
|
|
||||||
@@ -149,7 +164,7 @@ ERPAuto/
|
|||||||
### 数据库连接失败
|
### 数据库连接失败
|
||||||
|
|
||||||
1. 确认数据库服务已启动
|
1. 确认数据库服务已启动
|
||||||
2. 检查 `.env` 中的数据库配置
|
2. 检查 `config.yaml` 中的数据库配置
|
||||||
3. 确认防火墙允许数据库端口访问
|
3. 确认防火墙允许数据库端口访问
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|||||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -41,7 +41,6 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"autoprefixer": "^10.4.27",
|
"autoprefixer": "^10.4.27",
|
||||||
"dotenv": "^17.3.1",
|
|
||||||
"electron": "^39.2.6",
|
"electron": "^39.2.6",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
@@ -5592,19 +5591,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
|
||||||
"version": "17.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
|
||||||
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://dotenvx.com"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dotenv-expand": {
|
"node_modules/dotenv-expand": {
|
||||||
"version": "11.0.7",
|
"version": "11.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
|
||||||
@@ -11905,7 +11891,6 @@
|
|||||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.27.0",
|
"esbuild": "~0.27.0",
|
||||||
"get-tsconfig": "^4.7.5"
|
"get-tsconfig": "^4.7.5"
|
||||||
@@ -11927,7 +11912,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -11944,7 +11928,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -11961,7 +11944,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -11978,7 +11960,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -11995,7 +11976,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12012,7 +11992,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12029,7 +12008,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12046,7 +12024,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12063,7 +12040,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12080,7 +12056,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12097,7 +12072,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12114,7 +12088,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12131,7 +12104,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12148,7 +12120,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12165,7 +12136,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12182,7 +12152,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12199,7 +12168,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12216,7 +12184,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12233,7 +12200,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12250,7 +12216,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12267,7 +12232,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12284,7 +12248,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12301,7 +12264,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12318,7 +12280,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12335,7 +12296,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12352,7 +12312,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12408,7 +12367,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|||||||
@@ -61,7 +61,6 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"autoprefixer": "^10.4.27",
|
"autoprefixer": "^10.4.27",
|
||||||
"dotenv": "^17.3.1",
|
|
||||||
"electron": "^39.2.6",
|
"electron": "^39.2.6",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ export function registerCleanerHandlers(): void {
|
|||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => {
|
||||||
let authService: ErpAuthService | null = null
|
let authService: ErpAuthService | null = null
|
||||||
let dbService: MySqlService | SqlServerService | null = null
|
let dbService: MySqlService | SqlServerService | null = null
|
||||||
let erpConfigService: UserErpConfigService | null = null
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get ERP configuration from database for current user
|
// Get ERP configuration from database for current user
|
||||||
@@ -136,9 +135,10 @@ export function registerCleanerHandlers(): void {
|
|||||||
username: erpConfig.username ? 'configured' : 'EMPTY'
|
username: erpConfig.username ? 'configured' : 'EMPTY'
|
||||||
})
|
})
|
||||||
|
|
||||||
const dbType = process.env.DB_TYPE?.toLowerCase()
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const dbType = configManager.getDatabaseType()
|
||||||
log.info(
|
log.info(
|
||||||
`Connecting to ${dbType === 'sqlserver' || dbType === 'mssql' ? 'SQL Server' : 'MySQL'} for order resolution...`
|
`Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : 'MySQL'} for order resolution...`
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,38 +13,45 @@ import * as fs from 'fs'
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { dirname } from 'path'
|
import { dirname } from 'path'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = dirname(__filename)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load .env file manually
|
* MySQL configuration schema
|
||||||
*/
|
*/
|
||||||
function loadEnv(filePath: string): Map<string, string> {
|
const mysqlConfigSchema = z.object({
|
||||||
const envMap = new Map<string, string>()
|
host: z.string(),
|
||||||
|
port: z.number(),
|
||||||
|
database: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load config.yaml file
|
||||||
|
*/
|
||||||
|
function loadConfig(filePath: string): {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
database: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
} {
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
console.warn(`.env file not found: ${filePath}`)
|
throw new Error(`Config file not found: ${filePath}`)
|
||||||
return envMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(filePath, 'utf-8')
|
const content = fs.readFileSync(filePath, 'utf-8')
|
||||||
const lines = content.split('\n')
|
const parsed = yaml.load(content) as Record<string, unknown>
|
||||||
|
|
||||||
for (const line of lines) {
|
// Safely extract database.mysql config
|
||||||
const trimmedLine = line.trim()
|
const database = parsed?.database as Record<string, unknown> | undefined
|
||||||
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
const mysql = database?.mysql as Record<string, unknown> | undefined
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const [key, ...valueParts] = trimmedLine.split('=')
|
const result = mysqlConfigSchema.parse(mysql)
|
||||||
if (key && valueParts.length > 0) {
|
return result
|
||||||
const value = valueParts.join('=').trim()
|
|
||||||
envMap.set(key.trim(), value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return envMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,17 +96,29 @@ async function runMigration(): Promise<void> {
|
|||||||
console.log('BIPUsers Table Migration: Add ERP Parameters')
|
console.log('BIPUsers Table Migration: Add ERP Parameters')
|
||||||
console.log('==============================================\n')
|
console.log('==============================================\n')
|
||||||
|
|
||||||
// Load .env file from project root
|
// Load config.yaml from project root or user data directory
|
||||||
const envPath = path.resolve(process.cwd(), '.env')
|
const isDev = !process.execPath.includes('Resources\\app')
|
||||||
console.log(`Loading .env from: ${envPath}`)
|
const configPath = isDev
|
||||||
const env = loadEnv(envPath)
|
? path.resolve(process.cwd(), 'config.yaml')
|
||||||
|
: path.join(process.env.APPDATA || '', 'erpauto', 'config.yaml')
|
||||||
|
|
||||||
// Get database configuration
|
console.log(`Loading config from: ${configPath}`)
|
||||||
const dbHost = env.get('DB_MYSQL_HOST') || 'localhost'
|
|
||||||
const dbPort = parseInt(env.get('DB_MYSQL_PORT') || '3306', 10)
|
let dbConfig: { host: string; port: number; database: string; username: string; password: string }
|
||||||
const dbUser = env.get('DB_USERNAME') || 'root'
|
|
||||||
const dbPassword = env.get('DB_PASSWORD') || ''
|
try {
|
||||||
const dbName = env.get('DB_NAME') || ''
|
dbConfig = loadConfig(configPath)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load config.yaml:', error instanceof Error ? error.message : error)
|
||||||
|
console.error('Please ensure config.yaml exists and contains valid MySQL configuration.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbHost = dbConfig.host || 'localhost'
|
||||||
|
const dbPort = dbConfig.port || 3306
|
||||||
|
const dbUser = dbConfig.username || 'root'
|
||||||
|
const dbPassword = dbConfig.password || ''
|
||||||
|
const dbName = dbConfig.database || ''
|
||||||
|
|
||||||
console.log(`Database: ${dbHost}:${dbPort}/${dbName}`)
|
console.log(`Database: ${dbHost}:${dbPort}/${dbName}`)
|
||||||
console.log(`Username: ${dbUser}`)
|
console.log(`Username: ${dbUser}`)
|
||||||
@@ -160,7 +179,7 @@ async function runMigration(): Promise<void> {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
console.error('\nTroubleshooting:')
|
console.error('\nTroubleshooting:')
|
||||||
console.error('1. Check if MySQL server is running')
|
console.error('1. Check if MySQL server is running')
|
||||||
console.error('2. Verify database credentials in .env file')
|
console.error('2. Verify database credentials in config.yaml file')
|
||||||
console.error('3. Ensure database "' + dbName + '" exists')
|
console.error('3. Ensure database "' + dbName + '" exists')
|
||||||
console.error('4. Check network connectivity to ' + dbHost + ':' + dbPort)
|
console.error('4. Check network connectivity to ' + dbHost + ':' + dbPort)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
@@ -170,7 +189,7 @@ async function runMigration(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await connection.end()
|
await connection.end()
|
||||||
console.log('Disconnected from MySQL')
|
console.log('Disconnected from MySQL')
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignore disconnect errors
|
// Ignore disconnect errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ test.describe('Authentication Flow', () => {
|
|||||||
electronApp = await electron.launch({
|
electronApp = await electron.launch({
|
||||||
args: [path.join(__dirname, '../../out/main/index.js')],
|
args: [path.join(__dirname, '../../out/main/index.js')],
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
|
||||||
NODE_ENV: 'test'
|
NODE_ENV: 'test'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
describe('Cleaner Service (Integration)', () => {
|
describe('Cleaner Service (Integration)', () => {
|
||||||
|
// For integration tests, use fixed test credentials or configure via config.yaml
|
||||||
const config: ErpConfig = {
|
const config: ErpConfig = {
|
||||||
url: process.env.ERP_URL || '',
|
url: '',
|
||||||
username: process.env.ERP_USERNAME || '',
|
username: '',
|
||||||
password: process.env.ERP_PASSWORD || ''
|
password: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test data paths
|
// Test data paths
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import type { ErpConfig } from '../../src/main/types/erp.types'
|
|||||||
|
|
||||||
describe('ERP Authentication Service (Integration)', () => {
|
describe('ERP Authentication Service (Integration)', () => {
|
||||||
let authService: ErpAuthService
|
let authService: ErpAuthService
|
||||||
|
// For integration tests, use fixed test credentials or configure via config.yaml
|
||||||
const config: ErpConfig = {
|
const config: ErpConfig = {
|
||||||
url: process.env.ERP_URL || '',
|
url: '',
|
||||||
username: process.env.ERP_USERNAME || '',
|
username: '',
|
||||||
password: process.env.ERP_PASSWORD || ''
|
password: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have ERP credentials
|
// Check if we have ERP credentials
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
describe('Extractor Service (Integration)', () => {
|
describe('Extractor Service (Integration)', () => {
|
||||||
|
// For integration tests, use fixed test credentials or configure via config.yaml
|
||||||
const config: ErpConfig = {
|
const config: ErpConfig = {
|
||||||
url: process.env.ERP_URL || '',
|
url: '',
|
||||||
username: process.env.ERP_USERNAME || '',
|
username: '',
|
||||||
password: process.env.ERP_PASSWORD || ''
|
password: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const testOrderNumber = 'SC70202602120085' // From references/demo/productionID.txt
|
const testOrderNumber = 'SC70202602120085' // From references/demo/productionID.txt
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|||||||
import { MySqlService, MySqlConfig } from '@services/database/mysql'
|
import { MySqlService, MySqlConfig } from '@services/database/mysql'
|
||||||
|
|
||||||
// MySQL test configuration
|
// MySQL test configuration
|
||||||
// In production, these should come from environment variables
|
// For integration tests, use fixed test credentials or configure via config.yaml
|
||||||
const testConfig: MySqlConfig = {
|
const testConfig: MySqlConfig = {
|
||||||
host: process.env.MYSQL_HOST || 'localhost',
|
host: 'localhost',
|
||||||
port: parseInt(process.env.MYSQL_PORT || '3306'),
|
port: 3306,
|
||||||
user: process.env.MYSQL_USER || 'root',
|
user: 'root',
|
||||||
password: process.env.MYSQL_PASSWORD || 'password',
|
password: 'password',
|
||||||
database: process.env.MYSQL_DATABASE || 'test_db'
|
database: 'test_db'
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('MySqlService Integration Tests', () => {
|
describe('MySqlService Integration Tests', () => {
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import { SqlServerService, SqlServerConfig } from '@main/services/database/sql-s
|
|||||||
import * as sql from 'mssql'
|
import * as sql from 'mssql'
|
||||||
|
|
||||||
// SQL Server test configuration
|
// SQL Server test configuration
|
||||||
|
// For integration tests, use fixed test credentials or configure via config.yaml
|
||||||
const testConfig: SqlServerConfig = {
|
const testConfig: SqlServerConfig = {
|
||||||
server: process.env.SQL_SERVER_HOST || 'localhost',
|
server: 'localhost',
|
||||||
port: parseInt(process.env.SQL_SERVER_PORT || '1433'),
|
port: 1433,
|
||||||
user: process.env.SQL_SERVER_USER || 'sa',
|
user: 'sa',
|
||||||
password: process.env.SQL_SERVER_PASSWORD || 'password',
|
password: 'password',
|
||||||
database: process.env.SQL_SERVER_DATABASE || 'testdb',
|
database: 'testdb',
|
||||||
options: {
|
options: {
|
||||||
encrypt: false, // Set to true for Azure SQL
|
encrypt: false, // Set to true for Azure SQL
|
||||||
trustServerCertificate: true // Set to false in production with valid cert
|
trustServerCertificate: true // Set to false in production with valid cert
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest'
|
|
||||||
import { ConfigManager } from '@services/config/config-manager'
|
|
||||||
import type { SettingsData } from '@types/settings.types'
|
|
||||||
|
|
||||||
describe('ConfigManager - deep merge utilities', () => {
|
|
||||||
it('should deep merge objects, updating only specified fields', async () => {
|
|
||||||
const manager = ConfigManager.getInstance()
|
|
||||||
await manager.initialize()
|
|
||||||
// Reload to ensure clean state from previous tests
|
|
||||||
await manager['loadEnvFile']()
|
|
||||||
|
|
||||||
// Setup initial state
|
|
||||||
const initial: SettingsData = {
|
|
||||||
erp: {
|
|
||||||
url: 'http://old.com',
|
|
||||||
username: 'user1',
|
|
||||||
password: 'pass1',
|
|
||||||
headless: true,
|
|
||||||
ignoreHttpsErrors: true,
|
|
||||||
autoCloseBrowser: true
|
|
||||||
},
|
|
||||||
database: {
|
|
||||||
dbType: 'mysql',
|
|
||||||
server: '',
|
|
||||||
mysqlHost: 'localhost',
|
|
||||||
mysqlPort: 3306,
|
|
||||||
database: 'db',
|
|
||||||
username: 'user',
|
|
||||||
password: ''
|
|
||||||
},
|
|
||||||
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'validation.xlsx' },
|
|
||||||
extraction: {
|
|
||||||
batchSize: 100,
|
|
||||||
verbose: true,
|
|
||||||
autoConvert: true,
|
|
||||||
mergeBatches: true,
|
|
||||||
enableDbPersistence: true
|
|
||||||
},
|
|
||||||
validation: {
|
|
||||||
dataSource: 'database_full',
|
|
||||||
batchSize: 2000,
|
|
||||||
matchMode: 'substring',
|
|
||||||
enableCrud: false,
|
|
||||||
defaultManager: ''
|
|
||||||
},
|
|
||||||
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
|
|
||||||
execution: { dryRun: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load initial settings
|
|
||||||
await manager.saveAllSettings(initial)
|
|
||||||
// Reload from disk to populate cache
|
|
||||||
await manager['loadEnvFile']()
|
|
||||||
|
|
||||||
// Partial update
|
|
||||||
const partial = {
|
|
||||||
erp: { url: 'http://new.com' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await manager.savePartialSettings(partial)
|
|
||||||
|
|
||||||
expect(result.success).toBe(true)
|
|
||||||
|
|
||||||
const current = manager.getAllSettings()
|
|
||||||
|
|
||||||
// Updated field
|
|
||||||
expect(current.erp.url).toBe('http://new.com')
|
|
||||||
|
|
||||||
// Preserved fields
|
|
||||||
expect(current.erp.username).toBe('user1')
|
|
||||||
expect(current.database.dbType).toBe('mysql')
|
|
||||||
expect(current.paths.dataDir).toBe('/data')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ConfigManager - backup and restore', () => {
|
|
||||||
it('should create backup before saving', async () => {
|
|
||||||
const manager = ConfigManager.getInstance()
|
|
||||||
await manager.initialize()
|
|
||||||
|
|
||||||
const backupSuccess = await manager['backupEnvFile']()
|
|
||||||
|
|
||||||
expect(backupSuccess).toBe(true)
|
|
||||||
|
|
||||||
// Check backup file exists (in same location as .env file, which is src/main/)
|
|
||||||
const fs = await import('fs')
|
|
||||||
const path = await import('path')
|
|
||||||
const backupPath = path.resolve(process.cwd(), 'src/main/.env.backup')
|
|
||||||
|
|
||||||
expect(fs.existsSync(backupPath)).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Note: Skipping fs.writeFileSync mock test due to ESM limitations in Vitest
|
|
||||||
// The restoreBackup functionality is tested indirectly through the savePartialSettings rollback test
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ConfigManager.savePartialSettings', () => {
|
|
||||||
it('should save only specified fields and preserve others', async () => {
|
|
||||||
const manager = ConfigManager.getInstance()
|
|
||||||
await manager.initialize()
|
|
||||||
|
|
||||||
// Setup initial state with multiple categories
|
|
||||||
await manager.saveAllSettings({
|
|
||||||
erp: {
|
|
||||||
url: 'http://old.com',
|
|
||||||
username: 'user1',
|
|
||||||
password: 'pass1',
|
|
||||||
headless: true,
|
|
||||||
ignoreHttpsErrors: true,
|
|
||||||
autoCloseBrowser: true
|
|
||||||
},
|
|
||||||
database: {
|
|
||||||
dbType: 'mysql',
|
|
||||||
server: '',
|
|
||||||
mysqlHost: '192.168.1.1',
|
|
||||||
mysqlPort: 3306,
|
|
||||||
database: 'testdb',
|
|
||||||
username: 'dbuser',
|
|
||||||
password: ''
|
|
||||||
},
|
|
||||||
paths: { dataDir: '/old/path', defaultOutput: 'out.xlsx', validationOutput: 'val.xlsx' },
|
|
||||||
extraction: {
|
|
||||||
batchSize: 50,
|
|
||||||
verbose: true,
|
|
||||||
autoConvert: true,
|
|
||||||
mergeBatches: true,
|
|
||||||
enableDbPersistence: true
|
|
||||||
},
|
|
||||||
validation: {
|
|
||||||
dataSource: 'database_full',
|
|
||||||
batchSize: 1000,
|
|
||||||
matchMode: 'exact',
|
|
||||||
enableCrud: false,
|
|
||||||
defaultManager: ''
|
|
||||||
},
|
|
||||||
ui: { fontFamily: 'Tahoma', fontSize: 14, productionIdInputWidth: 25 },
|
|
||||||
execution: { dryRun: true }
|
|
||||||
})
|
|
||||||
// Reload from disk to populate cache
|
|
||||||
await manager['loadEnvFile']()
|
|
||||||
|
|
||||||
// Update only ERP URL
|
|
||||||
const result = await manager.savePartialSettings({
|
|
||||||
erp: { url: 'http://new.com' }
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.success).toBe(true)
|
|
||||||
|
|
||||||
const current = manager.getAllSettings()
|
|
||||||
|
|
||||||
// Verify updated field
|
|
||||||
expect(current.erp.url).toBe('http://new.com')
|
|
||||||
|
|
||||||
// Verify preserved ERP fields
|
|
||||||
expect(current.erp.username).toBe('user1')
|
|
||||||
expect(current.erp.password).toBe('pass1')
|
|
||||||
|
|
||||||
// Verify preserved other categories
|
|
||||||
expect(current.database.dbType).toBe('mysql')
|
|
||||||
expect(current.database.mysqlHost).toBe('192.168.1.1')
|
|
||||||
expect(current.paths.dataDir).toBe('/old/path')
|
|
||||||
expect(current.extraction.batchSize).toBe(50)
|
|
||||||
expect(current.ui.fontFamily).toBe('Tahoma')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should reject updates to non-whitelisted fields', async () => {
|
|
||||||
const manager = ConfigManager.getInstance()
|
|
||||||
await manager.initialize()
|
|
||||||
// Reset to ensure clean state
|
|
||||||
manager.resetToDefaults()
|
|
||||||
await manager.save()
|
|
||||||
|
|
||||||
const result = await manager.savePartialSettings({
|
|
||||||
database: { dbType: 'postgres' }
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.success).toBe(false)
|
|
||||||
expect(result.error).toContain('不允许修改')
|
|
||||||
expect(result.error).toContain('database.dbType')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle nested object updates correctly', async () => {
|
|
||||||
const manager = ConfigManager.getInstance()
|
|
||||||
await manager.initialize()
|
|
||||||
// Reset to ensure clean state
|
|
||||||
manager.resetToDefaults()
|
|
||||||
await manager.save()
|
|
||||||
|
|
||||||
await manager.saveAllSettings({
|
|
||||||
erp: {
|
|
||||||
url: 'http://test.com',
|
|
||||||
username: 'u',
|
|
||||||
password: 'p',
|
|
||||||
headless: false,
|
|
||||||
ignoreHttpsErrors: false,
|
|
||||||
autoCloseBrowser: false
|
|
||||||
},
|
|
||||||
database: {
|
|
||||||
dbType: 'mysql',
|
|
||||||
server: '',
|
|
||||||
mysqlHost: 'localhost',
|
|
||||||
mysqlPort: 3306,
|
|
||||||
database: 'db',
|
|
||||||
username: 'user',
|
|
||||||
password: ''
|
|
||||||
},
|
|
||||||
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'val.xlsx' },
|
|
||||||
extraction: {
|
|
||||||
batchSize: 100,
|
|
||||||
verbose: true,
|
|
||||||
autoConvert: true,
|
|
||||||
mergeBatches: true,
|
|
||||||
enableDbPersistence: true
|
|
||||||
},
|
|
||||||
validation: {
|
|
||||||
dataSource: 'database_full',
|
|
||||||
batchSize: 2000,
|
|
||||||
matchMode: 'substring',
|
|
||||||
enableCrud: false,
|
|
||||||
defaultManager: ''
|
|
||||||
},
|
|
||||||
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
|
|
||||||
execution: { dryRun: false }
|
|
||||||
})
|
|
||||||
// Reload from disk to populate cache
|
|
||||||
await manager['loadEnvFile']()
|
|
||||||
|
|
||||||
// Update multiple ERP fields at once
|
|
||||||
const result = await manager.savePartialSettings({
|
|
||||||
erp: {
|
|
||||||
url: 'http://updated.com',
|
|
||||||
username: 'newuser',
|
|
||||||
password: 'newpass'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.success).toBe(true)
|
|
||||||
|
|
||||||
const current = manager.getAllSettings()
|
|
||||||
|
|
||||||
expect(current.erp.url).toBe('http://updated.com')
|
|
||||||
expect(current.erp.username).toBe('newuser')
|
|
||||||
expect(current.erp.password).toBe('newpass')
|
|
||||||
expect(current.erp.headless).toBe(false) // preserved
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should restore backup on save failure', async () => {
|
|
||||||
const manager = ConfigManager.getInstance()
|
|
||||||
await manager.initialize()
|
|
||||||
// Reset to ensure clean state
|
|
||||||
manager.resetToDefaults()
|
|
||||||
await manager.save()
|
|
||||||
|
|
||||||
const originalUrl = manager.getAllSettings().erp.url
|
|
||||||
|
|
||||||
// Mock save to fail
|
|
||||||
vi.spyOn(manager, 'save').mockResolvedValueOnce(false)
|
|
||||||
|
|
||||||
const result = await manager.savePartialSettings({
|
|
||||||
erp: { url: 'http://should-not-apply.com' }
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.success).toBe(false)
|
|
||||||
expect(result.error).toContain('保存配置失败')
|
|
||||||
|
|
||||||
// Verify rollback
|
|
||||||
expect(manager.getAllSettings().erp.url).toBe(originalUrl)
|
|
||||||
|
|
||||||
manager.save.mockRestore()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { beforeAll, afterAll, vi } from 'vitest'
|
import { beforeAll, afterAll, vi } from 'vitest'
|
||||||
import dotenv from 'dotenv'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
// Mock electron app module for unit tests
|
// Mock electron app module for unit tests
|
||||||
@@ -12,9 +11,6 @@ vi.mock('electron', () => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Load environment variables from project root
|
|
||||||
dotenv.config({ path: path.resolve(process.cwd(), '.env') })
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Global test setup
|
// Global test setup
|
||||||
console.log('Test suite starting...')
|
console.log('Test suite starting...')
|
||||||
|
|||||||
Reference in New Issue
Block a user