refactor: migrate configuration to YAML-based system
This commit is contained in:
35
.env.example
35
.env.example
@@ -1,35 +0,0 @@
|
|||||||
# ERP System Configuration
|
|
||||||
ERP_URL=https://your-erp-system.com
|
|
||||||
ERP_USERNAME=your_username
|
|
||||||
ERP_PASSWORD=your_password
|
|
||||||
|
|
||||||
# Additional ERP Settings
|
|
||||||
ERP_HEADLESS=true
|
|
||||||
ERP_IGNORE_HTTPS_ERRORS=true
|
|
||||||
ERP_AUTO_CLOSE_BROWSER=true
|
|
||||||
|
|
||||||
# Database Configuration - SQL Server
|
|
||||||
SQL_SERVER_HOST=localhost
|
|
||||||
SQL_SERVER_PORT=1433
|
|
||||||
SQL_SERVER_DATABASE=erp_db
|
|
||||||
SQL_SERVER_USERNAME=sa
|
|
||||||
SQL_SERVER_PASSWORD=your_password
|
|
||||||
|
|
||||||
MYSQL_HOST=localhost
|
|
||||||
MYSQL_PORT=3306
|
|
||||||
MYSQL_DATABASE=erp_db
|
|
||||||
MYSQL_USERNAME=root
|
|
||||||
MYSQL_PASSWORD=your_password
|
|
||||||
|
|
||||||
# Application Settings
|
|
||||||
LOG_LEVEL=info
|
|
||||||
DOWNLOAD_DIR=./downloads
|
|
||||||
TEMP_DIR=./temp
|
|
||||||
|
|
||||||
# Legacy configurations (if needed)
|
|
||||||
DB_SQLSERVER_DRIVER=ODBC Driver 18 for SQL Server
|
|
||||||
DB_TRUST_SERVER_CERTIFICATE=yes
|
|
||||||
DB_TYPE=mysql
|
|
||||||
DB_MYSQL_HOST=192.168.31.83
|
|
||||||
DB_MYSQL_PORT=3306
|
|
||||||
DB_MYSQL_CHARSET=utf8mb4
|
|
||||||
54
config.template.yaml
Normal file
54
config.template.yaml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# ================================
|
||||||
|
# ERPAuto 配置模板
|
||||||
|
# ================================
|
||||||
|
# 部署说明:
|
||||||
|
# 1. 复制此文件为 config.yaml
|
||||||
|
# 2. 根据实际环境修改配置值
|
||||||
|
# 3. 设置 database.activeType 为 mysql 或 sqlserver
|
||||||
|
# ================================
|
||||||
|
# 注意:ERP 认证信息存储在数据库 (dbo_BIPUsers) 中,按用户管理
|
||||||
|
# ================================
|
||||||
|
|
||||||
|
database:
|
||||||
|
activeType: mysql
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
host: <MYSQL_HOST>
|
||||||
|
port: 3306
|
||||||
|
database: <DATABASE_NAME>
|
||||||
|
username: <USERNAME>
|
||||||
|
password: <PASSWORD>
|
||||||
|
charset: utf8mb4
|
||||||
|
|
||||||
|
sqlserver:
|
||||||
|
server: <SQL_SERVER_HOST>
|
||||||
|
port: 1433
|
||||||
|
database: <DATABASE_NAME>
|
||||||
|
username: <USERNAME>
|
||||||
|
password: <PASSWORD>
|
||||||
|
driver: 'ODBC Driver 18 for SQL Server'
|
||||||
|
trustServerCertificate: true
|
||||||
|
|
||||||
|
paths:
|
||||||
|
dataDir: './data/'
|
||||||
|
defaultOutput: 'output.xlsx'
|
||||||
|
validationOutput: 'validation-result.xlsx'
|
||||||
|
|
||||||
|
extraction:
|
||||||
|
batchSize: 100
|
||||||
|
verbose: true
|
||||||
|
autoConvert: true
|
||||||
|
mergeBatches: true
|
||||||
|
enableDbPersistence: true
|
||||||
|
|
||||||
|
validation:
|
||||||
|
dataSource: database_full
|
||||||
|
batchSize: 2000
|
||||||
|
matchMode: substring
|
||||||
|
enableCrud: false
|
||||||
|
defaultManager: ''
|
||||||
|
|
||||||
|
orderResolution:
|
||||||
|
tableName: ''
|
||||||
|
productionIdField: ''
|
||||||
|
orderNumberField: ''
|
||||||
63
config.yaml
Normal file
63
config.yaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# ================================
|
||||||
|
# ERPAuto 配置文件
|
||||||
|
# ================================
|
||||||
|
# 注意:ERP 认证用户名和密码存储在数据库 (dbo_BIPUsers) 中,按用户管理
|
||||||
|
# ERP URL 是固定的基础设施配置,在此处配置
|
||||||
|
# ================================
|
||||||
|
|
||||||
|
# ERP 系统配置
|
||||||
|
erp:
|
||||||
|
url: 'https://68.11.34.30:8082' # ERP 系统固定地址
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
# 支持 MySQL 和 SQL Server 双配置,通过 activeType 切换
|
||||||
|
database:
|
||||||
|
activeType: mysql # 当前使用的数据库类型:mysql 或 sqlserver
|
||||||
|
|
||||||
|
# MySQL 配置
|
||||||
|
mysql:
|
||||||
|
host: 192.168.31.83
|
||||||
|
port: 3306
|
||||||
|
database: BLD_DB
|
||||||
|
username: remote_user
|
||||||
|
password: 3.1415926Beeke # 密码为空时使用空密码
|
||||||
|
charset: utf8mb4 # 字符集
|
||||||
|
|
||||||
|
# SQL Server 配置
|
||||||
|
sqlserver:
|
||||||
|
server: localhost # SQL Server 地址
|
||||||
|
port: 1433 # 默认端口 1433
|
||||||
|
database: BLD_DB
|
||||||
|
username: peng
|
||||||
|
password: Cqbld123456.
|
||||||
|
driver: 'ODBC Driver 18 for SQL Server' # ODBC 驱动
|
||||||
|
trustServerCertificate: true # 信任服务器证书(开发环境设为 true)
|
||||||
|
|
||||||
|
# 路径配置
|
||||||
|
paths:
|
||||||
|
dataDir: 'D:/python/playwrite/data/'
|
||||||
|
defaultOutput: '离散备料计划维护_合并.xlsx'
|
||||||
|
validationOutput: '物料状态校验结果.xlsx'
|
||||||
|
|
||||||
|
# 数据提取配置
|
||||||
|
extraction:
|
||||||
|
batchSize: 100 # 每批处理的记录数
|
||||||
|
verbose: true # 详细日志
|
||||||
|
autoConvert: true # 自动转换为 Excel
|
||||||
|
mergeBatches: true # 合并批次
|
||||||
|
enableDbPersistence: true # 启用数据库持久化
|
||||||
|
|
||||||
|
# 物料校验配置
|
||||||
|
validation:
|
||||||
|
dataSource: database_full # 数据源:database_full, database_filtered, excel_existing, excel_full
|
||||||
|
batchSize: 2000 # 校验批次大小
|
||||||
|
matchMode: substring # 匹配模式:substring (模糊) 或 exact (精确)
|
||||||
|
enableCrud: false # 启用 CRUD 操作
|
||||||
|
defaultManager: '' # 默认负责人
|
||||||
|
|
||||||
|
# 订单号解析配置
|
||||||
|
# 用于从数据库表中解析订单号与 productionID 的映射
|
||||||
|
orderResolution:
|
||||||
|
tableName: 'productionContractData_26年压力表合同数据'
|
||||||
|
productionIdField: '总排号'
|
||||||
|
orderNumberField: '生产订单号'
|
||||||
300
docs/CONFIG_FILE_LOCATION.md
Normal file
300
docs/CONFIG_FILE_LOCATION.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# ERPAuto 配置文件位置说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
ERPAuto 根据运行环境自动选择配置文件的存储位置:
|
||||||
|
|
||||||
|
- **开发环境**:项目根目录(方便编辑和版本控制)
|
||||||
|
- **生产环境**:用户数据目录(AppData,安全且升级时保留)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件位置
|
||||||
|
|
||||||
|
### 1. 开发环境
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
|
||||||
|
- 开发和调试
|
||||||
|
- 配置需要版本控制
|
||||||
|
- 团队协作
|
||||||
|
|
||||||
|
**配置文件位置**:
|
||||||
|
|
||||||
|
```
|
||||||
|
<项目根目录>\config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
```
|
||||||
|
D:\Projects\ERPAuto\
|
||||||
|
├── src\
|
||||||
|
├── package.json
|
||||||
|
├── config.yaml # 开发配置
|
||||||
|
├── config.yaml.backup # 自动备份
|
||||||
|
└── config.template.yaml # 配置模板
|
||||||
|
```
|
||||||
|
|
||||||
|
**检测方式**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
process.env.NODE_ENV === 'development' || !app.isPackaged
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 生产环境(安装版和便携版)
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
|
||||||
|
- 正式发布的应用
|
||||||
|
- 配置需要在应用升级时保留
|
||||||
|
- 多用户环境,每个用户独立配置
|
||||||
|
|
||||||
|
**配置文件位置**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Windows: C:\Users\<用户名>\AppData\Roaming\erpauto\config.yaml
|
||||||
|
macOS: ~/Library/Application Support/erpauto/config.yaml
|
||||||
|
Linux: ~/.config/erpauto/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\Users\zhangsan\AppData\Roaming\erpauto\
|
||||||
|
├── config.yaml # 用户配置
|
||||||
|
└── config.yaml.backup # 自动备份
|
||||||
|
```
|
||||||
|
|
||||||
|
**检测方式**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
app.isPackaged === true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 为什么生产环境使用用户数据目录?
|
||||||
|
|
||||||
|
| 方案 | 配置位置 | 优点 | 缺点 |
|
||||||
|
| ------------------ | --------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||||
|
| **用户数据目录** ✓ | AppData\Roaming | • 应用升级时配置保留<br>• 符合 Windows 规范<br>• 多用户隔离<br>• 配置不暴露 | • 路径较深,不易访问 |
|
||||||
|
| **应用同目录** ✗ | .exe 同目录 | • 易于访问和编辑 | • 应用升级时配置可能丢失<br>• 需要写权限<br>• 配置暴露在应用目录<br>• 多用户共享配置 |
|
||||||
|
|
||||||
|
**我们的选择**:生产环境统一使用用户数据目录,确保:
|
||||||
|
|
||||||
|
1. ✅ 应用升级时用户配置不会丢失
|
||||||
|
2. ✅ 符合 Windows 应用规范
|
||||||
|
3. ✅ 配置不暴露在应用目录,更安全
|
||||||
|
4. ✅ 多用户环境下,每个用户有独立配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 构建配置
|
||||||
|
|
||||||
|
### electron-builder.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
win:
|
||||||
|
target:
|
||||||
|
- nsis # 安装版
|
||||||
|
- portable # 便携版
|
||||||
|
|
||||||
|
portable:
|
||||||
|
artifactName: ${name}-${version}-portable.${ext}
|
||||||
|
# 便携版也使用用户数据目录 (AppData)
|
||||||
|
# 不是 exe 同目录,确保配置在升级时保留
|
||||||
|
|
||||||
|
nsis:
|
||||||
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建 Windows 安装版和便携版
|
||||||
|
npm run build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
### 输出文件
|
||||||
|
|
||||||
|
```
|
||||||
|
dist/
|
||||||
|
├── erpauto-1.0.0-setup.exe # 安装版
|
||||||
|
└── erpauto-1.0.0-portable.exe # 便携版
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件结构
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ================================
|
||||||
|
# ERPAuto 配置文件
|
||||||
|
# ================================
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
database:
|
||||||
|
activeType: mysql # 切换字段:mysql 或 sqlserver
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
host: 192.168.31.83
|
||||||
|
port: 3306
|
||||||
|
database: BLD_DB
|
||||||
|
username: remote_user
|
||||||
|
password: ''
|
||||||
|
charset: utf8mb4
|
||||||
|
|
||||||
|
sqlserver:
|
||||||
|
server: localhost
|
||||||
|
port: 1433
|
||||||
|
database: BLD_DB
|
||||||
|
username: sa
|
||||||
|
password: ''
|
||||||
|
driver: 'ODBC Driver 18 for SQL Server'
|
||||||
|
trustServerCertificate: true
|
||||||
|
|
||||||
|
# 路径配置
|
||||||
|
paths:
|
||||||
|
dataDir: 'D:/python/playwrite/data/'
|
||||||
|
defaultOutput: '离散备料计划维护_合并.xlsx'
|
||||||
|
validationOutput: '物料状态校验结果.xlsx'
|
||||||
|
|
||||||
|
# 数据提取配置
|
||||||
|
extraction:
|
||||||
|
batchSize: 100
|
||||||
|
verbose: true
|
||||||
|
autoConvert: true
|
||||||
|
mergeBatches: true
|
||||||
|
enableDbPersistence: true
|
||||||
|
|
||||||
|
# 校验配置
|
||||||
|
validation:
|
||||||
|
dataSource: database_full
|
||||||
|
batchSize: 2000
|
||||||
|
matchMode: substring
|
||||||
|
enableCrud: false
|
||||||
|
defaultManager: ''
|
||||||
|
|
||||||
|
# 订单号解析配置
|
||||||
|
orderResolution:
|
||||||
|
tableName: 'productionContractData_26 年压力表合同数据'
|
||||||
|
productionIdField: '总排号'
|
||||||
|
orderNumberField: '生产订单号'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件管理
|
||||||
|
|
||||||
|
### 查看当前配置路径
|
||||||
|
|
||||||
|
运行调试工具:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx src\main\tools\config-path-debug.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 快速访问配置(Windows)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 打开配置所在目录
|
||||||
|
%APPDATA%\erpauto
|
||||||
|
```
|
||||||
|
|
||||||
|
### 备份配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份整个配置目录
|
||||||
|
xcopy %APPDATA%\erpauto D:\Backup\erpauto-config /E /I
|
||||||
|
```
|
||||||
|
|
||||||
|
### 迁移配置
|
||||||
|
|
||||||
|
从旧版本迁移:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用迁移脚本
|
||||||
|
npx tsx scripts\migrate-env-to-yaml.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 便携版应用的配置为什么不放在 exe 同目录?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
|
||||||
|
- 放在 exe 同目录会导致应用升级时配置丢失
|
||||||
|
- 便携版每次运行会解压到临时目录,无法持久保存配置
|
||||||
|
- 使用用户数据目录(AppData)确保配置持久化
|
||||||
|
|
||||||
|
### Q: 如何快速访问配置文件?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
|
||||||
|
- Windows: 按 `Win + R`,输入 `%APPDATA%\erpauto`,回车
|
||||||
|
- 或在文件管理器地址栏输入 `%APPDATA%\erpauto`
|
||||||
|
|
||||||
|
### Q: 多台电脑如何同步配置?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
|
||||||
|
1. 导出配置:`xcopy %APPDATA%\erpauto\config.yaml \\server\share\`
|
||||||
|
2. 导入配置:`xcopy \\server\share\config.yaml %APPDATA%\erpauto\`
|
||||||
|
|
||||||
|
或使用同步工具(OneDrive、坚果云等)同步配置目录。
|
||||||
|
|
||||||
|
### Q: 配置文件损坏了怎么办?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
|
||||||
|
1. 删除 `config.yaml`
|
||||||
|
2. 应用会自动创建新的默认配置
|
||||||
|
3. 从 `config.yaml.backup` 恢复(如果存在)
|
||||||
|
|
||||||
|
### Q: 开发环境下如何切换配置?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
|
||||||
|
- 直接编辑项目根目录的 `config.yaml`
|
||||||
|
- 建议保留 `config.template.yaml` 作为模板
|
||||||
|
- 将 `config.yaml` 加入 `.gitignore`,避免提交敏感信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### ConfigManager 路径选择逻辑
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 检测是否为开发环境
|
||||||
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境:项目根目录
|
||||||
|
this.configPath = path.resolve(__dirname, '../../config.yaml')
|
||||||
|
} else {
|
||||||
|
// 生产环境(安装版和便携版):用户数据目录
|
||||||
|
this.configPath = path.join(app.getPath('userData'), 'config.yaml')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
| 版本 | 配置策略 | 说明 |
|
||||||
|
| ---- | ------------------------------- | -------------------- |
|
||||||
|
| 1.0+ | 开发:项目目录<br>生产:AppData | 确保配置在升级时保留 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- [Electron app.getPath() 文档](https://www.electronjs.org/docs/api/app#appgetpathname)
|
||||||
|
- [electron-builder 配置](https://www.electron.build/configuration.html)
|
||||||
|
- [Windows 应用数据存储规范](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid)
|
||||||
@@ -5,16 +5,25 @@ directories:
|
|||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!**/.vscode/*'
|
||||||
- '!src/*'
|
- '!src/*'
|
||||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
- '!electron-vite.config.{js,ts,mjs,cjs}'
|
||||||
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
|
# Include config.template.yaml in the build for reference
|
||||||
|
- 'config.template.yaml'
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
- '**/node_modules/playwright/**'
|
- '**/node_modules/playwright/**'
|
||||||
win:
|
win:
|
||||||
executableName: erpauto
|
executableName: erpauto
|
||||||
|
target:
|
||||||
|
- nsis
|
||||||
|
- portable
|
||||||
|
portable:
|
||||||
|
artifactName: ${name}-${version}-portable.${ext}
|
||||||
|
# Portable app uses user data directory (AppData), not exe directory
|
||||||
|
# This ensures config persists across app updates
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${name}-${version}-setup.${ext}
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
|
|||||||
57
package-lock.json
generated
57
package-lock.json
generated
@@ -12,10 +12,11 @@
|
|||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"chromium-bidi": "^15.0.0",
|
"chromium-bidi": "^15.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.3.1",
|
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
"mssql": "^12.2.0",
|
"mssql": "^12.2.0",
|
||||||
"mysql2": "^3.18.2",
|
"mysql2": "^3.18.2",
|
||||||
@@ -106,6 +107,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
|
||||||
"integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
|
"integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.1.2",
|
"@azure/abort-controller": "^2.1.2",
|
||||||
"@azure/core-auth": "^1.10.0",
|
"@azure/core-auth": "^1.10.0",
|
||||||
@@ -167,6 +169,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz",
|
||||||
"integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==",
|
"integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.1.2",
|
"@azure/abort-controller": "^2.1.2",
|
||||||
"@azure/core-auth": "^1.10.0",
|
"@azure/core-auth": "^1.10.0",
|
||||||
@@ -358,6 +361,7 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -1126,7 +1130,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-dirname": "^0.1.0",
|
"cross-dirname": "^0.1.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@@ -1148,7 +1151,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
@@ -1165,7 +1167,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -1180,7 +1181,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
@@ -3015,6 +3015,12 @@
|
|||||||
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-yaml": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -3055,6 +3061,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
|
||||||
"integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==",
|
"integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -3077,6 +3084,7 @@
|
|||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -3185,6 +3193,7 @@
|
|||||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.1",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.56.1",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
@@ -3618,6 +3627,7 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3650,6 +3660,7 @@
|
|||||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -3978,7 +3989,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/array-buffer-byte-length": {
|
"node_modules/array-buffer-byte-length": {
|
||||||
@@ -4397,6 +4407,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -5123,8 +5134,7 @@
|
|||||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
@@ -5490,6 +5500,7 @@
|
|||||||
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
|
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "26.8.1",
|
"app-builder-lib": "26.8.1",
|
||||||
"builder-util": "26.8.1",
|
"builder-util": "26.8.1",
|
||||||
@@ -5579,18 +5590,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==",
|
|
||||||
"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",
|
||||||
@@ -5716,6 +5715,7 @@
|
|||||||
"integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==",
|
"integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/get": "^2.0.0",
|
"@electron/get": "^2.0.0",
|
||||||
"@types/node": "^22.7.7",
|
"@types/node": "^22.7.7",
|
||||||
@@ -5904,7 +5904,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/asar": "^3.2.1",
|
"@electron/asar": "^3.2.1",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
@@ -5925,7 +5924,6 @@
|
|||||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.1.2",
|
"graceful-fs": "^4.1.2",
|
||||||
"jsonfile": "^4.0.0",
|
"jsonfile": "^4.0.0",
|
||||||
@@ -6254,6 +6252,7 @@
|
|||||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -6314,6 +6313,7 @@
|
|||||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
@@ -8266,7 +8266,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -10091,6 +10090,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -10173,6 +10173,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -10196,7 +10197,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^9.4.0"
|
"commander": "^9.4.0"
|
||||||
},
|
},
|
||||||
@@ -10214,7 +10214,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || >=14"
|
"node": "^12.20.0 || >=14"
|
||||||
}
|
}
|
||||||
@@ -10235,6 +10234,7 @@
|
|||||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -10367,6 +10367,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -11654,7 +11655,6 @@
|
|||||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"rimraf": "~2.6.2"
|
"rimraf": "~2.6.2"
|
||||||
@@ -12172,6 +12172,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -12406,6 +12407,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -12952,6 +12954,7 @@
|
|||||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "4.0.18",
|
"@vitest/expect": "4.0.18",
|
||||||
"@vitest/mocker": "4.0.18",
|
"@vitest/mocker": "4.0.18",
|
||||||
@@ -13160,6 +13163,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@colors/colors": "^1.6.0",
|
"@colors/colors": "^1.6.0",
|
||||||
"@dabh/diagnostics": "^2.0.8",
|
"@dabh/diagnostics": "^2.0.8",
|
||||||
@@ -13397,6 +13401,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,11 @@
|
|||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"chromium-bidi": "^15.0.0",
|
"chromium-bidi": "^15.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.3.1",
|
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
"mssql": "^12.2.0",
|
"mssql": "^12.2.0",
|
||||||
"mysql2": "^3.18.2",
|
"mysql2": "^3.18.2",
|
||||||
|
|||||||
125
scripts/migrate-env-to-yaml.ts
Normal file
125
scripts/migrate-env-to-yaml.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Migration Script: .env to config.yaml
|
||||||
|
*
|
||||||
|
* Usage: npx tsx scripts/migrate-env-to-yaml.ts
|
||||||
|
*
|
||||||
|
* This script migrates the old .env configuration to the new YAML format.
|
||||||
|
* ERP configuration is NOT migrated as it's now stored in the database per user.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
|
||||||
|
const ENV_PATH = path.resolve(process.cwd(), '.env')
|
||||||
|
const YAML_PATH = path.resolve(process.cwd(), 'config.yaml')
|
||||||
|
const BACKUP_PATH = path.resolve(process.cwd(), '.env.backup')
|
||||||
|
|
||||||
|
interface EnvConfig {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnvFile(content: string): EnvConfig {
|
||||||
|
const result: EnvConfig = {}
|
||||||
|
const lines = content.split('\n')
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue
|
||||||
|
|
||||||
|
const [key, ...valueParts] = trimmed.split('=')
|
||||||
|
if (key && valueParts.length > 0) {
|
||||||
|
result[key.trim()] = valueParts.join('=').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrate() {
|
||||||
|
console.log('🔄 Starting migration from .env to config.yaml...\n')
|
||||||
|
|
||||||
|
if (!fs.existsSync(ENV_PATH)) {
|
||||||
|
console.error('❌ .env file not found at:', ENV_PATH)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const envContent = fs.readFileSync(ENV_PATH, 'utf-8')
|
||||||
|
const env = parseEnvFile(envContent)
|
||||||
|
|
||||||
|
// Build configuration object (without ERP)
|
||||||
|
const config = {
|
||||||
|
database: {
|
||||||
|
activeType: (env.DB_TYPE || 'mysql').toLowerCase() as 'mysql' | 'sqlserver',
|
||||||
|
mysql: {
|
||||||
|
host: env.DB_MYSQL_HOST || 'localhost',
|
||||||
|
port: parseInt(env.DB_MYSQL_PORT || '3306', 10),
|
||||||
|
database: env.DB_NAME || '',
|
||||||
|
username: env.DB_USERNAME || '',
|
||||||
|
password: env.DB_PASSWORD || '',
|
||||||
|
charset: env.DB_MYSQL_CHARSET || 'utf8mb4'
|
||||||
|
},
|
||||||
|
sqlserver: {
|
||||||
|
server: env.DB_SERVER || 'localhost',
|
||||||
|
port: parseInt(env.DB_SQLSERVER_PORT || '1433', 10),
|
||||||
|
database: env.DB_NAME || '',
|
||||||
|
username: env.DB_USERNAME || '',
|
||||||
|
password: env.DB_PASSWORD || '',
|
||||||
|
driver: env.DB_SQLSERVER_DRIVER || 'ODBC Driver 18 for SQL Server',
|
||||||
|
trustServerCertificate: env.DB_TRUST_SERVER_CERTIFICATE === 'yes'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
dataDir: env.PATH_DATA_DIR || './data/',
|
||||||
|
defaultOutput: env.PATH_DEFAULT_OUTPUT || 'output.xlsx',
|
||||||
|
validationOutput: env.PATH_VALIDATION_OUTPUT || 'validation-result.xlsx'
|
||||||
|
},
|
||||||
|
extraction: {
|
||||||
|
batchSize: parseInt(env.EXTRACTION_BATCH_SIZE || '100', 10),
|
||||||
|
verbose: env.EXTRACTION_VERBOSE !== 'false',
|
||||||
|
autoConvert: env.EXTRACTION_AUTO_CONVERT !== 'false',
|
||||||
|
mergeBatches: env.EXTRACTION_MERGE_BATCHES !== 'false',
|
||||||
|
enableDbPersistence: env.EXTRACTION_ENABLE_DB_PERSISTENCE !== 'false'
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
dataSource: env.VALIDATION_DATA_SOURCE || 'database_full',
|
||||||
|
batchSize: parseInt(env.VALIDATION_BATCH_SIZE || '2000', 10),
|
||||||
|
matchMode: env.VALIDATION_MATCH_MODE || 'substring',
|
||||||
|
enableCrud: env.VALIDATION_ENABLE_CRUD === 'true',
|
||||||
|
defaultManager: env.VALIDATION_DEFAULT_MANAGER || ''
|
||||||
|
},
|
||||||
|
orderResolution: {
|
||||||
|
tableName: env.DB_TABLE_NAME || '',
|
||||||
|
productionIdField: env.DB_FIELD_PRODUCTION_ID || '',
|
||||||
|
orderNumberField: env.DB_FIELD_ORDER_NUMBER || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup .env
|
||||||
|
if (fs.existsSync(ENV_PATH)) {
|
||||||
|
fs.copyFileSync(ENV_PATH, BACKUP_PATH)
|
||||||
|
console.log('📁 Backed up .env to .env.backup')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write YAML with header comments
|
||||||
|
const header = `# ================================\n# ERPAuto 配置文件\n# ================================\n# 由 .env 迁移生成\n# 迁移时间:${new Date().toISOString()}\n# 注意:ERP 配置已迁移到数据库 (dbo_BIPUsers 表)\n# ================================\n\n`
|
||||||
|
|
||||||
|
const yamlContent = yaml.dump(config, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
quotingType: '"',
|
||||||
|
forceQuotes: false
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.writeFileSync(YAML_PATH, header + yamlContent, 'utf-8')
|
||||||
|
|
||||||
|
console.log('✅ Migration completed successfully!')
|
||||||
|
console.log(`📁 Config saved to: ${YAML_PATH}`)
|
||||||
|
console.log('\n📋 Next steps:')
|
||||||
|
console.log(' 1. Review config.yaml and verify all values')
|
||||||
|
console.log(' 2. Test the application thoroughly')
|
||||||
|
console.log(' 3. Remove .env file when confident (optional)\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate()
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# ===========================
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# 数据库配置 - SQL Server
|
|
||||||
# ===========================
|
|
||||||
# DB_TYPE=sqlserver
|
|
||||||
# DB_SERVER=
|
|
||||||
# DB_NAME=BLD_DB
|
|
||||||
# DB_USERNAME=remote_user
|
|
||||||
# DB_PASSWORD=
|
|
||||||
DB_SQLSERVER_DRIVER=ODBC Driver 18 for SQL Server
|
|
||||||
DB_TRUST_SERVER_CERTIFICATE=yes
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# 数据库配置 - 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
|
|
||||||
|
|
||||||
# 订单号解析表配置
|
|
||||||
# 表名:包含 productionID 和 生产订单号 映射关系的表
|
|
||||||
DB_TABLE_NAME=productionContractData_26年压力表合同数据
|
|
||||||
# 字段名:总排号 (对应 productionID)
|
|
||||||
DB_FIELD_PRODUCTION_ID=总排号
|
|
||||||
# 字段名:生产订单号 (对应生产订单号)
|
|
||||||
DB_FIELD_ORDER_NUMBER=生产订单号
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# 路径配置
|
|
||||||
# ===========================
|
|
||||||
PATH_DATA_DIR=D:/python/playwrite/data/
|
|
||||||
PATH_PRODUCTION_ID_FILE=ProductionID.txt
|
|
||||||
PATH_DEFAULT_OUTPUT=离散备料计划维护_合并.xlsx
|
|
||||||
PATH_VALIDATION_OUTPUT=物料状态校验结果.xlsx
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# 数据提取配置
|
|
||||||
# ===========================
|
|
||||||
EXTRACTION_BATCH_SIZE=100
|
|
||||||
EXTRACTION_VERBOSE=true
|
|
||||||
EXTRACTION_AUTO_CONVERT=true
|
|
||||||
EXTRACTION_MERGE_BATCHES=true
|
|
||||||
EXTRACTION_ENABLE_DB_PERSISTENCE=true
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# 校验配置
|
|
||||||
# ===========================
|
|
||||||
VALIDATION_DATA_SOURCE=database_full
|
|
||||||
VALIDATION_USE_DATABASE=true
|
|
||||||
VALIDATION_BATCH_SIZE=2000
|
|
||||||
VALIDATION_ENABLE_CRUD=false
|
|
||||||
VALIDATION_DEFAULT_MANAGER=
|
|
||||||
VALIDATION_MATCH_MODE=substring
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# UI 配置
|
|
||||||
# ===========================
|
|
||||||
UI_FONT_FAMILY=Microsoft YaHei UI
|
|
||||||
UI_FONT_SIZE=10
|
|
||||||
UI_PRODUCTION_ID_INPUT_WIDTH=20
|
|
||||||
|
|
||||||
# ===========================
|
|
||||||
# 执行配置
|
|
||||||
# ===========================
|
|
||||||
EXECUTION_DRYRUN=false
|
|
||||||
@@ -3,14 +3,12 @@ import { join } from 'path'
|
|||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
import { registerIpcHandlers } from './ipc'
|
import { registerIpcHandlers } from './ipc'
|
||||||
import * as dotenv from 'dotenv'
|
import { ConfigManager } from './services/config/config-manager'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { dirname, resolve } from 'path'
|
import { dirname } from 'path'
|
||||||
|
|
||||||
// Load environment variables from .env file
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = dirname(__filename)
|
const __dirname = dirname(__filename)
|
||||||
dotenv.config({ path: resolve(__dirname, '../../.env') })
|
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
@@ -47,7 +45,17 @@ function createWindow(): void {
|
|||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(async () => {
|
||||||
|
// Initialize ConfigManager BEFORE registering IPC handlers
|
||||||
|
// This ensures config is loaded before any service tries to use it
|
||||||
|
try {
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
await configManager.initialize()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize ConfigManager:', error)
|
||||||
|
// Continue anyway - default config will be created
|
||||||
|
}
|
||||||
|
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('com.electron')
|
electronApp.setAppUserModelId('com.electron')
|
||||||
|
|
||||||
@@ -58,7 +66,7 @@ app.whenReady().then(() => {
|
|||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Register IPC handlers
|
// Register IPC handlers (after ConfigManager is initialized)
|
||||||
registerIpcHandlers()
|
registerIpcHandlers()
|
||||||
|
|
||||||
// IPC test
|
// IPC test
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CleanerService } from '../services/erp/cleaner'
|
|||||||
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
||||||
import { MySqlService } from '../services/database/mysql'
|
import { MySqlService } from '../services/database/mysql'
|
||||||
import { SqlServerService } from '../services/database/sql-server'
|
import { SqlServerService } from '../services/database/sql-server'
|
||||||
|
import { ConfigManager } from '../services/config/config-manager'
|
||||||
import { ResultExporter } from '../services/excel/result-exporter'
|
import { ResultExporter } from '../services/excel/result-exporter'
|
||||||
import { CleanerReportGenerator } from '../services/report/cleaner-report-generator'
|
import { CleanerReportGenerator } from '../services/report/cleaner-report-generator'
|
||||||
import { SessionManager } from '../services/user/session-manager'
|
import { SessionManager } from '../services/user/session-manager'
|
||||||
@@ -47,29 +48,33 @@ function sendProgress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getDatabaseService(): Promise<MySqlService | SqlServerService> {
|
async function getDatabaseService(): Promise<MySqlService | SqlServerService> {
|
||||||
const dbType = process.env.DB_TYPE?.toLowerCase()
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const config = configManager.getConfig()
|
||||||
|
const dbType = configManager.getDatabaseType()
|
||||||
|
|
||||||
if (dbType === 'sqlserver' || dbType === 'mssql') {
|
if (dbType === 'sqlserver') {
|
||||||
|
const dbConfig = config.database.sqlserver
|
||||||
const sqlServerService = new SqlServerService({
|
const sqlServerService = new SqlServerService({
|
||||||
server: process.env.DB_SERVER || 'localhost',
|
server: dbConfig.server,
|
||||||
port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10),
|
port: dbConfig.port,
|
||||||
user: process.env.DB_USERNAME || 'sa',
|
user: dbConfig.username,
|
||||||
password: process.env.DB_PASSWORD || '',
|
password: dbConfig.password,
|
||||||
database: process.env.DB_NAME || '',
|
database: dbConfig.database,
|
||||||
options: {
|
options: {
|
||||||
encrypt: false,
|
encrypt: false,
|
||||||
trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes'
|
trustServerCertificate: dbConfig.trustServerCertificate
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await sqlServerService.connect()
|
await sqlServerService.connect()
|
||||||
return sqlServerService
|
return sqlServerService
|
||||||
} else {
|
} else {
|
||||||
|
const dbConfig = config.database.mysql
|
||||||
const mysqlService = new MySqlService({
|
const mysqlService = new MySqlService({
|
||||||
host: process.env.DB_MYSQL_HOST || 'localhost',
|
host: dbConfig.host,
|
||||||
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
port: dbConfig.port,
|
||||||
user: process.env.DB_USERNAME || 'root',
|
user: dbConfig.username,
|
||||||
password: process.env.DB_PASSWORD || '',
|
password: dbConfig.password,
|
||||||
database: process.env.DB_NAME || ''
|
database: dbConfig.database
|
||||||
})
|
})
|
||||||
await mysqlService.connect()
|
await mysqlService.connect()
|
||||||
return mysqlService
|
return mysqlService
|
||||||
@@ -78,23 +83,35 @@ async function getDatabaseService(): Promise<MySqlService | SqlServerService> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ERP configuration for current user
|
* Get ERP configuration for current user
|
||||||
|
* URL is from config.yaml (fixed infrastructure)
|
||||||
|
* Username and password are from user's database config
|
||||||
*/
|
*/
|
||||||
async function getErpConfig(): Promise<{
|
async function getErpConfig(): Promise<{
|
||||||
url: string
|
url: string
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
}> {
|
}> {
|
||||||
const erpConfigService = UserErpConfigService.getInstance()
|
// Get ERP URL from config.yaml (fixed for all users)
|
||||||
const config = await erpConfigService.getCurrentUserErpConfig()
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const globalConfig = configManager.getConfig()
|
||||||
|
const erpUrl = globalConfig.erp.url
|
||||||
|
|
||||||
if (!config || !config.url || !config.username || !config.password) {
|
// Get username and password from user's database config
|
||||||
|
const erpConfigService = UserErpConfigService.getInstance()
|
||||||
|
const userConfig = await erpConfigService.getCurrentUserErpConfig()
|
||||||
|
|
||||||
|
if (!userConfig || !userConfig.username || !userConfig.password) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
'ERP 配置不完整。请在设置中配置 ERP URL、用户名和密码',
|
'ERP 配置不完整。请在设置中配置 ERP 用户名和密码',
|
||||||
'VAL_MISSING_REQUIRED'
|
'VAL_MISSING_REQUIRED'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return {
|
||||||
|
url: erpUrl,
|
||||||
|
username: userConfig.username,
|
||||||
|
password: userConfig.password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerCleanerHandlers(): void {
|
export function registerCleanerHandlers(): void {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { withErrorHandling, type IpcResult } from './index'
|
|||||||
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
|
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
|
||||||
import type { ExtractorInput, ExtractorResult, ExtractionProgress } from '../types/extractor.types'
|
import type { ExtractorInput, ExtractorResult, ExtractionProgress } from '../types/extractor.types'
|
||||||
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
||||||
|
import { ConfigManager } from '../services/config/config-manager'
|
||||||
|
|
||||||
const log = createLogger('ExtractorHandler')
|
const log = createLogger('ExtractorHandler')
|
||||||
|
|
||||||
@@ -39,23 +40,35 @@ function sendLog(windowId: number, level: string, message: string): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ERP configuration for current user
|
* Get ERP configuration for current user
|
||||||
|
* URL is from config.yaml (fixed infrastructure)
|
||||||
|
* Username and password are from user's database config
|
||||||
*/
|
*/
|
||||||
async function getErpConfig(): Promise<{
|
async function getErpConfig(): Promise<{
|
||||||
url: string
|
url: string
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
}> {
|
}> {
|
||||||
const erpConfigService = UserErpConfigService.getInstance()
|
// Get ERP URL from config.yaml (fixed for all users)
|
||||||
const config = await erpConfigService.getCurrentUserErpConfig()
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const globalConfig = configManager.getConfig()
|
||||||
|
const erpUrl = globalConfig.erp.url
|
||||||
|
|
||||||
if (!config || !config.url || !config.username || !config.password) {
|
// Get username and password from user's database config
|
||||||
|
const erpConfigService = UserErpConfigService.getInstance()
|
||||||
|
const userConfig = await erpConfigService.getCurrentUserErpConfig()
|
||||||
|
|
||||||
|
if (!userConfig || !userConfig.username || !userConfig.password) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
'ERP 配置不完整。请在设置中配置 ERP URL、用户名和密码',
|
'ERP 配置不完整。请在设置中配置 ERP 用户名和密码',
|
||||||
'VAL_MISSING_REQUIRED'
|
'VAL_MISSING_REQUIRED'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return {
|
||||||
|
url: erpUrl,
|
||||||
|
username: userConfig.username,
|
||||||
|
password: userConfig.password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,6 +196,14 @@ export function registerExtractorHandlers(): void {
|
|||||||
errorCount: result.errors.length
|
errorCount: result.errors.length
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Log detailed error information if any errors occurred
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
log.warn('Extraction errors occurred', { errors: result.errors })
|
||||||
|
result.errors.forEach((err, index) => {
|
||||||
|
log.error(`Error ${index + 1}/${result.errors.length}: ${err}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up: close browser
|
// Clean up: close browser
|
||||||
|
|||||||
@@ -2,62 +2,33 @@
|
|||||||
* Settings IPC Handler
|
* Settings IPC Handler
|
||||||
*
|
*
|
||||||
* Provides IPC handlers for settings management:
|
* Provides IPC handlers for settings management:
|
||||||
* - Get/set settings
|
* - Get/set ERP credentials (stored in database per user)
|
||||||
* - Reset to defaults
|
* - Reset to defaults (Admin only)
|
||||||
* - Test ERP connection
|
|
||||||
* - Test database connection
|
* - Test database connection
|
||||||
|
*
|
||||||
|
* Note: ERP configuration is stored in database (dbo_BIPUsers table)
|
||||||
|
* and managed per-user via UserErpConfigService.
|
||||||
|
* Other settings (database, paths, etc.) are managed via config.yaml
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import { ConfigManager } from '../services/config/config-manager'
|
import { ConfigManager } from '../services/config/config-manager'
|
||||||
import { SessionManager } from '../services/user/session-manager'
|
import { SessionManager } from '../services/user/session-manager'
|
||||||
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
||||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
|
||||||
import { MySqlService } from '../services/database/mysql'
|
import { MySqlService } from '../services/database/mysql'
|
||||||
import { SqlServerService } from '../services/database/sql-server'
|
import { SqlServerService } from '../services/database/sql-server'
|
||||||
import { createLogger } from '../services/logger'
|
import { createLogger } from '../services/logger'
|
||||||
import type {
|
import type { UserType, ConnectionTestResult, SaveSettingsResult } from '../types/settings.types'
|
||||||
SettingsData,
|
|
||||||
UserType,
|
|
||||||
ConnectionTestResult,
|
|
||||||
SaveSettingsResult
|
|
||||||
} from '../types/settings.types'
|
|
||||||
|
|
||||||
const log = createLogger('SettingsHandler')
|
const log = createLogger('SettingsHandler')
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter settings by user type
|
|
||||||
* Admin users get all settings, User users get limited settings
|
|
||||||
*/
|
|
||||||
function filterSettingsByUserType(settings: SettingsData, userType: UserType): SettingsData {
|
|
||||||
if (userType === 'Admin') {
|
|
||||||
return settings // Return all settings for Admin
|
|
||||||
}
|
|
||||||
|
|
||||||
// User users get limited settings
|
|
||||||
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,
|
|
||||||
// Include minimal required fields for other sections
|
|
||||||
database: settings.database,
|
|
||||||
extraction: settings.extraction,
|
|
||||||
validation: settings.validation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register IPC handlers for settings management
|
* Register IPC handlers for settings management
|
||||||
*/
|
*/
|
||||||
export function registerSettingsHandlers(): void {
|
export function registerSettingsHandlers(): void {
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const sessionManager = SessionManager.getInstance()
|
const sessionManager = SessionManager.getInstance()
|
||||||
|
const erpConfigService = UserErpConfigService.getInstance()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current user type
|
* Get current user type
|
||||||
@@ -67,125 +38,57 @@ export function registerSettingsHandlers(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get settings (ERP config from database, others from .env)
|
* Get ERP credentials for current user
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('settings:getSettings', async (): Promise<SettingsData> => {
|
ipcMain.handle('settings:getSettings', async (): Promise<any> => {
|
||||||
const userType = (sessionManager.getUserType() as UserType) || 'Guest'
|
try {
|
||||||
log.debug('Getting settings', { userType })
|
// Get ERP credentials from database for current user
|
||||||
|
const userErpConfig = await erpConfigService.getCurrentUserErpConfig()
|
||||||
|
|
||||||
// Get base settings from .env
|
return {
|
||||||
const settings = configManager.getAllSettings()
|
erp: {
|
||||||
|
username: userErpConfig?.username || '',
|
||||||
// Override ERP config with user-specific config from database
|
password: userErpConfig?.password || ''
|
||||||
const erpConfigService = UserErpConfigService.getInstance()
|
}
|
||||||
const userErpConfig = await erpConfigService.getCurrentUserErpConfig()
|
|
||||||
|
|
||||||
if (userErpConfig) {
|
|
||||||
settings.erp = {
|
|
||||||
url: userErpConfig.url || settings.erp.url,
|
|
||||||
username: userErpConfig.username || settings.erp.username,
|
|
||||||
password: userErpConfig.password || settings.erp.password,
|
|
||||||
headless: settings.erp.headless,
|
|
||||||
ignoreHttpsErrors: settings.erp.ignoreHttpsErrors,
|
|
||||||
autoCloseBrowser: settings.erp.autoCloseBrowser
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to get ERP credentials', { error })
|
||||||
|
return { erp: { username: '', password: '' } }
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterSettingsByUserType(settings, userType)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save settings (ERP config to database, others to .env)
|
* Save ERP credentials for current user
|
||||||
*/
|
*/
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'settings:saveSettings',
|
'settings:saveSettings',
|
||||||
async (_event, settings: Partial<SettingsData>): Promise<SaveSettingsResult> => {
|
async (_event, settings: any): Promise<SaveSettingsResult> => {
|
||||||
try {
|
try {
|
||||||
log.info('Saving settings', {
|
log.info('Saving ERP credentials')
|
||||||
sections: Object.keys(settings)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Step 1: Save ERP configuration to database (if provided)
|
|
||||||
if (settings.erp) {
|
if (settings.erp) {
|
||||||
const erpConfigService = UserErpConfigService.getInstance()
|
// Update ERP credentials in database for current user
|
||||||
const sessionManager = SessionManager.getInstance()
|
|
||||||
const currentUser = sessionManager.getUserInfo()
|
const currentUser = sessionManager.getUserInfo()
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
log.warn('No authenticated user found')
|
return { success: false, error: '未找到当前用户' }
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: '未找到认证用户,无法保存 ERP 配置'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only save if ERP fields are provided
|
// Ensure undefined values are converted to empty strings
|
||||||
const hasErpFields =
|
const erpCredentials = {
|
||||||
settings.erp.url !== undefined ||
|
username: settings.erp.username || '',
|
||||||
settings.erp.username !== undefined ||
|
password: settings.erp.password || ''
|
||||||
settings.erp.password !== undefined
|
|
||||||
|
|
||||||
if (hasErpFields) {
|
|
||||||
// Get current ERP config to preserve headless, ignoreHttpsErrors, autoCloseBrowser
|
|
||||||
const currentErpConfig = await erpConfigService.getCurrentUserErpConfig()
|
|
||||||
|
|
||||||
const erpConfigToSave = {
|
|
||||||
url: settings.erp.url ?? currentErpConfig?.url ?? '',
|
|
||||||
username: settings.erp.username ?? currentErpConfig?.username ?? '',
|
|
||||||
password: settings.erp.password ?? currentErpConfig?.password ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Saving ERP config to database for user', {
|
|
||||||
username: currentUser.username,
|
|
||||||
url: erpConfigToSave.url
|
|
||||||
})
|
|
||||||
|
|
||||||
const erpSaveSuccess =
|
|
||||||
await erpConfigService.updateCurrentUserErpConfig(erpConfigToSave)
|
|
||||||
|
|
||||||
if (!erpSaveSuccess) {
|
|
||||||
log.error('Failed to save ERP config to database')
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: '保存 ERP 配置到数据库失败'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await erpConfigService.updateCurrentUserErpConfig(erpCredentials)
|
||||||
|
|
||||||
|
log.info('ERP credentials saved successfully')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Save other settings (database, paths, extraction, validation, execution) to .env
|
|
||||||
// Filter out ERP fields since they're now in database
|
|
||||||
const nonErpSettings: Partial<SettingsData> = { ...settings }
|
|
||||||
delete nonErpSettings.erp
|
|
||||||
|
|
||||||
// Only save to .env if there are non-ERP settings
|
|
||||||
if (Object.keys(nonErpSettings).length > 0) {
|
|
||||||
log.info('Saving non-ERP settings to .env file', {
|
|
||||||
sections: Object.keys(nonErpSettings)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await configManager.savePartialSettings(nonErpSettings)
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
log.error('Failed to save settings to .env', {
|
|
||||||
error: result.error
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: result.error || '保存配置到文件失败'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Settings saved successfully')
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
log.error('Error saving settings', { error: message })
|
log.error('Error saving ERP credentials', { error: message })
|
||||||
return {
|
return { success: false, error: `保存配置失败:${message}` }
|
||||||
success: false,
|
|
||||||
error: `保存设置失败:${message}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -202,8 +105,7 @@ export function registerSettingsHandlers(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info('Resetting settings to defaults')
|
log.info('Resetting settings to defaults')
|
||||||
configManager.resetToDefaults()
|
const success = await configManager.resetToDefaults()
|
||||||
const success = await configManager.save()
|
|
||||||
if (success) {
|
if (success) {
|
||||||
log.info('Settings reset to defaults successfully')
|
log.info('Settings reset to defaults successfully')
|
||||||
return { success: true }
|
return { success: true }
|
||||||
@@ -218,76 +120,19 @@ export function registerSettingsHandlers(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Test ERP connection
|
|
||||||
*/
|
|
||||||
ipcMain.handle('settings:testErpConnection', async (): Promise<ConnectionTestResult> => {
|
|
||||||
try {
|
|
||||||
log.info('Testing ERP connection')
|
|
||||||
|
|
||||||
// Get current user's ERP config from database
|
|
||||||
const erpConfigService = UserErpConfigService.getInstance()
|
|
||||||
const userErpConfig = await erpConfigService.getCurrentUserErpConfig()
|
|
||||||
|
|
||||||
if (
|
|
||||||
!userErpConfig ||
|
|
||||||
!userErpConfig.url ||
|
|
||||||
!userErpConfig.username ||
|
|
||||||
!userErpConfig.password
|
|
||||||
) {
|
|
||||||
log.warn('ERP connection test failed - missing configuration')
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: '请先配置 ERP URL、用户名和密码'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ERP auth service and try to login
|
|
||||||
const erpAuthService = new ErpAuthService({
|
|
||||||
url: userErpConfig.url,
|
|
||||||
username: userErpConfig.username,
|
|
||||||
password: userErpConfig.password
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
await erpAuthService.login()
|
|
||||||
// Login successful, close browser
|
|
||||||
await erpAuthService.close()
|
|
||||||
log.info('ERP connection test successful')
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'ERP 连接测试成功!'
|
|
||||||
}
|
|
||||||
} catch (loginError) {
|
|
||||||
const errorMessage = loginError instanceof Error ? loginError.message : '登录失败'
|
|
||||||
log.error('ERP login failed', { error: errorMessage })
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `ERP 连接测试失败:${errorMessage}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
log.error('ERP connection test error', { error: message })
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `ERP 连接测试失败:${message}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test database connection
|
* Test database connection
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('settings:testDbConnection', async (): Promise<ConnectionTestResult> => {
|
ipcMain.handle('settings:testDbConnection', async (): Promise<ConnectionTestResult> => {
|
||||||
try {
|
try {
|
||||||
log.info('Testing database connection')
|
log.info('Testing database connection')
|
||||||
const settings = configManager.getAllSettings()
|
const config = configManager.getConfig()
|
||||||
const dbConfig = settings.database
|
const dbType = config.database.activeType
|
||||||
|
|
||||||
if (dbConfig.dbType === 'mysql') {
|
if (dbType === 'mysql') {
|
||||||
// Test MySQL connection
|
// Test MySQL connection
|
||||||
if (!dbConfig.mysqlHost || !dbConfig.database || !dbConfig.username) {
|
const dbConfig = config.database.mysql
|
||||||
|
if (!dbConfig.host || !dbConfig.database || !dbConfig.username) {
|
||||||
log.warn('MySQL connection test failed - missing configuration')
|
log.warn('MySQL connection test failed - missing configuration')
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -296,8 +141,8 @@ export function registerSettingsHandlers(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mysqlService = new MySqlService({
|
const mysqlService = new MySqlService({
|
||||||
host: dbConfig.mysqlHost,
|
host: dbConfig.host,
|
||||||
port: dbConfig.mysqlPort,
|
port: dbConfig.port,
|
||||||
user: dbConfig.username,
|
user: dbConfig.username,
|
||||||
password: dbConfig.password,
|
password: dbConfig.password,
|
||||||
database: dbConfig.database
|
database: dbConfig.database
|
||||||
@@ -321,6 +166,7 @@ export function registerSettingsHandlers(): void {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Test SQL Server connection
|
// Test SQL Server connection
|
||||||
|
const dbConfig = config.database.sqlserver
|
||||||
if (!dbConfig.server || !dbConfig.database || !dbConfig.username) {
|
if (!dbConfig.server || !dbConfig.database || !dbConfig.username) {
|
||||||
log.warn('SQL Server connection test failed - missing configuration')
|
log.warn('SQL Server connection test failed - missing configuration')
|
||||||
return {
|
return {
|
||||||
@@ -331,12 +177,12 @@ export function registerSettingsHandlers(): void {
|
|||||||
|
|
||||||
const sqlServerService = new SqlServerService({
|
const sqlServerService = new SqlServerService({
|
||||||
server: dbConfig.server,
|
server: dbConfig.server,
|
||||||
port: 1433, // Default SQL Server port
|
port: dbConfig.port,
|
||||||
user: dbConfig.username,
|
user: dbConfig.username,
|
||||||
password: dbConfig.password,
|
password: dbConfig.password,
|
||||||
database: dbConfig.database,
|
database: dbConfig.database,
|
||||||
options: {
|
options: {
|
||||||
trustServerCertificate: true
|
trustServerCertificate: dbConfig.trustServerCertificate
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,34 +2,38 @@
|
|||||||
* IPC handlers for User ERP Configuration
|
* IPC handlers for User ERP Configuration
|
||||||
*
|
*
|
||||||
* Provides APIs for the renderer process to:
|
* Provides APIs for the renderer process to:
|
||||||
* - Get current user's ERP configuration
|
* - Get current user's ERP credentials
|
||||||
* - Update current user's ERP configuration
|
* - Update current user's ERP credentials
|
||||||
* - Test ERP connection with provided credentials
|
* - Test ERP connection with provided credentials
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
import { UserErpConfigService, type ErpCredentials } from '../services/user/user-erp-config-service'
|
||||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
import { ErpAuthService } from '../services/erp/erp-auth'
|
||||||
|
import { ConfigManager } from '../services/config/config-manager'
|
||||||
import { createLogger } from '../services/logger'
|
import { createLogger } from '../services/logger'
|
||||||
import type { UserInfo } from '../types/user.types'
|
import type { UserInfo } from '../types/user.types'
|
||||||
|
|
||||||
const log = createLogger('UserErpConfigHandler')
|
const log = createLogger('UserErpConfigHandler')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ERP Configuration request
|
* ERP Credentials request (username and password only, URL is from config.yaml)
|
||||||
*/
|
*/
|
||||||
export interface ErpConfigRequest {
|
export interface ErpCredentialsRequest {
|
||||||
url: string
|
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ERP Configuration response
|
* ERP Configuration response (includes URL from config.yaml)
|
||||||
*/
|
*/
|
||||||
export interface ErpConfigResponse {
|
export interface ErpConfigResponse {
|
||||||
success: boolean
|
success: boolean
|
||||||
config?: ErpConfigRequest
|
config?: {
|
||||||
|
url: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,31 +52,36 @@ export function registerUserErpConfigHandlers(): void {
|
|||||||
const erpConfigService = UserErpConfigService.getInstance()
|
const erpConfigService = UserErpConfigService.getInstance()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current user's ERP configuration
|
* Get current user's ERP credentials
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('user-erp-config:getCurrent', async (): Promise<ErpConfigResponse> => {
|
ipcMain.handle('user-erp-config:getCurrent', async (): Promise<ErpConfigResponse> => {
|
||||||
try {
|
try {
|
||||||
log.info('Fetching current user ERP config')
|
log.info('Fetching current user ERP credentials')
|
||||||
const config = await erpConfigService.getCurrentUserErpConfig()
|
const credentials = await erpConfigService.getCurrentUserErpConfig()
|
||||||
|
|
||||||
if (!config) {
|
if (!credentials) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: '未找到 ERP 配置。请先配置 ERP 连接参数。'
|
error: '未找到 ERP 配置。请先配置 ERP 账号和密码。'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get ERP URL from config.yaml (fixed for all users)
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const globalConfig = configManager.getConfig()
|
||||||
|
const erpUrl = globalConfig.erp.url
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
config: {
|
config: {
|
||||||
url: config.url,
|
url: erpUrl,
|
||||||
username: config.username,
|
username: credentials.username,
|
||||||
password: config.password
|
password: credentials.password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
log.error('Get current user ERP config failed', { error: message })
|
log.error('Get current user ERP credentials failed', { error: message })
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `获取 ERP 配置失败:${message}`
|
error: `获取 ERP 配置失败:${message}`
|
||||||
@@ -81,27 +90,35 @@ export function registerUserErpConfigHandlers(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update current user's ERP configuration
|
* Update current user's ERP credentials
|
||||||
*/
|
*/
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'user-erp-config:update',
|
'user-erp-config:update',
|
||||||
async (_event, config: ErpConfigRequest): Promise<ErpConfigResponse> => {
|
async (_event, credentials: ErpCredentialsRequest): Promise<ErpConfigResponse> => {
|
||||||
try {
|
try {
|
||||||
log.info('Updating current user ERP config', {
|
log.info('Updating current user ERP credentials', {
|
||||||
url: config.url,
|
username: credentials.username
|
||||||
username: config.username
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const success = await erpConfigService.updateCurrentUserErpConfig(config)
|
const success = await erpConfigService.updateCurrentUserErpConfig(credentials)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
log.info('ERP config updated successfully')
|
log.info('ERP credentials updated successfully')
|
||||||
|
// Get ERP URL from config.yaml to return full config
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const globalConfig = configManager.getConfig()
|
||||||
|
const erpUrl = globalConfig.erp.url
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
config
|
config: {
|
||||||
|
url: erpUrl,
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.error('Failed to update ERP config')
|
log.error('Failed to update ERP credentials')
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: '更新 ERP 配置失败'
|
error: '更新 ERP 配置失败'
|
||||||
@@ -109,7 +126,7 @@ export function registerUserErpConfigHandlers(): void {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
log.error('Update ERP config failed', { error: message })
|
log.error('Update ERP credentials failed', { error: message })
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `更新 ERP 配置失败:${message}`
|
error: `更新 ERP 配置失败:${message}`
|
||||||
@@ -123,21 +140,26 @@ export function registerUserErpConfigHandlers(): void {
|
|||||||
*/
|
*/
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'user-erp-config:testConnection',
|
'user-erp-config:testConnection',
|
||||||
async (_event, config: ErpConfigRequest): Promise<ConnectionTestResult> => {
|
async (_event, credentials: ErpCredentialsRequest): Promise<ConnectionTestResult> => {
|
||||||
try {
|
try {
|
||||||
log.info('Testing ERP connection', { url: config.url, username: config.username })
|
log.info('Testing ERP connection', { username: credentials.username })
|
||||||
|
|
||||||
if (!config.url || !config.username || !config.password) {
|
if (!credentials.username || !credentials.password) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'ERP 配置不完整,请确保 URL、用户名和密码都已填写'
|
message: 'ERP 配置不完整,请确保用户名和密码都已填写'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get ERP URL from config.yaml (fixed for all users)
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const globalConfig = configManager.getConfig()
|
||||||
|
const erpUrl = globalConfig.erp.url
|
||||||
|
|
||||||
const authService = new ErpAuthService({
|
const authService = new ErpAuthService({
|
||||||
url: config.url,
|
url: erpUrl,
|
||||||
username: config.username,
|
username: credentials.username,
|
||||||
password: config.password,
|
password: credentials.password,
|
||||||
headless: true
|
headless: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
/**
|
/**
|
||||||
* Configuration Manager
|
* Configuration Manager (YAML Version)
|
||||||
*
|
*
|
||||||
* Manages application configuration stored in .env file
|
* Manages application configuration using YAML format
|
||||||
* Provides methods for reading, writing, and saving configuration values
|
* Provides type-safe access with Zod validation
|
||||||
|
*
|
||||||
|
* Note: ERP configuration is stored in database (dbo_BIPUsers table)
|
||||||
|
* and managed per-user, not in this config file.
|
||||||
|
*
|
||||||
|
* Configuration File Location:
|
||||||
|
* - Development: Project root directory (config.yaml)
|
||||||
|
* - Production (Installed & Portable): User data directory (AppData)
|
||||||
|
* This ensures config persists across app updates and is not exposed
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs'
|
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 { app } from 'electron'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
import { z } from 'zod'
|
||||||
import { createLogger } from '../logger'
|
import { createLogger } from '../logger'
|
||||||
import type {
|
import {
|
||||||
SettingsData,
|
fullConfigSchema,
|
||||||
DatabaseType,
|
type FullConfig,
|
||||||
MatchMode,
|
type DatabaseType,
|
||||||
ValidationDataSource
|
type MySqlConfig,
|
||||||
} from '../../types/settings.types'
|
type SqlServerConfig
|
||||||
|
} from '../../types/config.schema'
|
||||||
|
|
||||||
const log = createLogger('ConfigManager')
|
const log = createLogger('ConfigManager')
|
||||||
|
|
||||||
@@ -23,30 +35,36 @@ const __filename = fileURLToPath(import.meta.url)
|
|||||||
const __dirname = dirname(__filename)
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default settings values
|
* 默认配置
|
||||||
*/
|
*/
|
||||||
const DEFAULT_SETTINGS: SettingsData = {
|
const DEFAULT_CONFIG: FullConfig = {
|
||||||
erp: {
|
erp: {
|
||||||
url: 'https://68.11.34.30:8082/',
|
url: 'https://68.11.34.30:8082'
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
headless: true,
|
|
||||||
ignoreHttpsErrors: true,
|
|
||||||
autoCloseBrowser: true
|
|
||||||
},
|
},
|
||||||
database: {
|
database: {
|
||||||
dbType: 'mysql',
|
activeType: 'mysql',
|
||||||
server: '',
|
mysql: {
|
||||||
mysqlHost: '192.168.31.83',
|
host: 'localhost',
|
||||||
mysqlPort: 3306,
|
port: 3306,
|
||||||
database: 'BLD_DB',
|
database: 'erp_db',
|
||||||
username: 'remote_user',
|
username: 'root',
|
||||||
password: ''
|
password: '',
|
||||||
|
charset: 'utf8mb4'
|
||||||
|
},
|
||||||
|
sqlserver: {
|
||||||
|
server: 'localhost',
|
||||||
|
port: 1433,
|
||||||
|
database: 'erp_db',
|
||||||
|
username: 'sa',
|
||||||
|
password: '',
|
||||||
|
driver: 'ODBC Driver 18 for SQL Server',
|
||||||
|
trustServerCertificate: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
paths: {
|
paths: {
|
||||||
dataDir: 'D:/python/playwrite/data/',
|
dataDir: './data/',
|
||||||
defaultOutput: '离散备料计划维护_合并.xlsx',
|
defaultOutput: 'output.xlsx',
|
||||||
validationOutput: '物料状态校验结果.xlsx'
|
validationOutput: 'validation-result.xlsx'
|
||||||
},
|
},
|
||||||
extraction: {
|
extraction: {
|
||||||
batchSize: 100,
|
batchSize: 100,
|
||||||
@@ -61,103 +79,44 @@ const DEFAULT_SETTINGS: SettingsData = {
|
|||||||
matchMode: 'substring',
|
matchMode: 'substring',
|
||||||
enableCrud: false,
|
enableCrud: false,
|
||||||
defaultManager: ''
|
defaultManager: ''
|
||||||
|
},
|
||||||
|
orderResolution: {
|
||||||
|
tableName: '',
|
||||||
|
productionIdField: '',
|
||||||
|
orderNumberField: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if value is a plain object
|
|
||||||
*/
|
|
||||||
function isObject(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deep merge two objects, only updating fields present in target
|
|
||||||
* Preserves all fields from source that are not in target
|
|
||||||
*/
|
|
||||||
function deepMerge<T>(source: T, target: Partial<T>): T {
|
|
||||||
const result = { ...source }
|
|
||||||
|
|
||||||
for (const key in target) {
|
|
||||||
if (key in target) {
|
|
||||||
const targetValue = target[key]
|
|
||||||
const sourceValue = result[key]
|
|
||||||
|
|
||||||
if (isObject(targetValue) && isObject(sourceValue)) {
|
|
||||||
result[key] = deepMerge(
|
|
||||||
sourceValue as T[Extract<keyof T, string>],
|
|
||||||
targetValue as Partial<T[Extract<keyof T, string>]>
|
|
||||||
)
|
|
||||||
} else if (targetValue !== undefined) {
|
|
||||||
result[key] = targetValue as T[Extract<keyof T, string>]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI editable field whitelist
|
|
||||||
* Fields that can be modified through the settings UI
|
|
||||||
* Note: ERP fields are no longer editable here - they are managed per-user in the database
|
|
||||||
*/
|
|
||||||
const UI_EDITABLE_FIELDS: string[] = [
|
|
||||||
// ERP fields removed - ERP config is now stored in dbo_BIPUsers table per user
|
|
||||||
// 'erp.url',
|
|
||||||
// 'erp.username',
|
|
||||||
// 'erp.password'
|
|
||||||
// Add more fields as UI expands
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that settings only contain editable fields
|
|
||||||
*/
|
|
||||||
function validateEditableFields(settings: Partial<SettingsData>): {
|
|
||||||
valid: boolean
|
|
||||||
invalidFields: string[]
|
|
||||||
} {
|
|
||||||
const invalidFields: string[] = []
|
|
||||||
|
|
||||||
for (const [section, values] of Object.entries(settings)) {
|
|
||||||
if (values && typeof values === 'object') {
|
|
||||||
for (const field of Object.keys(values)) {
|
|
||||||
const fieldPath = `${section}.${field}`
|
|
||||||
if (!UI_EDITABLE_FIELDS.includes(fieldPath)) {
|
|
||||||
invalidFields.push(fieldPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: invalidFields.length === 0,
|
|
||||||
invalidFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration Manager Class
|
|
||||||
*/
|
|
||||||
export class ConfigManager {
|
export class ConfigManager {
|
||||||
private static instance: ConfigManager | null = null
|
private static instance: ConfigManager | null = null
|
||||||
private envPath!: string
|
private configPath!: string
|
||||||
private backupPath!: string
|
private backupPath!: string
|
||||||
private configCache: Map<string, string> = new Map()
|
private config: FullConfig | null = null
|
||||||
private initialized: boolean = false
|
private initialized: boolean = false
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
if (this.initialized) {
|
if (this.initialized) return
|
||||||
return
|
|
||||||
|
// 检测是否为开发环境
|
||||||
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境:配置文件放在项目根目录,方便编辑和调试
|
||||||
|
this.configPath = path.resolve(__dirname, '../../config.yaml')
|
||||||
|
this.backupPath = path.resolve(__dirname, '../../config.yaml.backup')
|
||||||
|
log.info('Running in development mode', { configPath: this.configPath })
|
||||||
|
} else {
|
||||||
|
// 生产环境(包括安装版和便携版):配置文件放在用户数据目录
|
||||||
|
// Windows: C:\Users\<user>\AppData\Roaming\erpauto\config.yaml
|
||||||
|
// 这样配置会在应用升级时保留,且不会暴露在应用目录中
|
||||||
|
this.configPath = path.join(app.getPath('userData'), 'config.yaml')
|
||||||
|
this.backupPath = path.join(app.getPath('userData'), 'config.yaml.backup')
|
||||||
|
log.info('Running in production mode', { configPath: this.configPath })
|
||||||
}
|
}
|
||||||
this.envPath = path.resolve(__dirname, '../../.env')
|
|
||||||
this.backupPath = path.resolve(__dirname, '../../.env.backup')
|
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the singleton instance
|
|
||||||
*/
|
|
||||||
public static getInstance(): ConfigManager {
|
public static getInstance(): ConfigManager {
|
||||||
if (ConfigManager.instance === null) {
|
if (ConfigManager.instance === null) {
|
||||||
ConfigManager.instance = new ConfigManager()
|
ConfigManager.instance = new ConfigManager()
|
||||||
@@ -166,478 +125,189 @@ export class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize configuration from .env file
|
* 初始化配置
|
||||||
|
* - 如果 config.yaml 不存在,创建默认配置
|
||||||
|
* - 加载并验证配置
|
||||||
*/
|
*/
|
||||||
public async initialize(): Promise<void> {
|
public async initialize(): Promise<void> {
|
||||||
await this.loadEnvFile()
|
if (!fs.existsSync(this.configPath)) {
|
||||||
|
log.info('Config file not found, creating default config.yaml')
|
||||||
|
await this.saveConfig(DEFAULT_CONFIG)
|
||||||
|
this.config = DEFAULT_CONFIG
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load .env file into cache
|
* 加载并验证 YAML 配置
|
||||||
*/
|
*/
|
||||||
private async loadEnvFile(): Promise<void> {
|
private async loadConfig(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Clear cache before loading
|
const content = fs.readFileSync(this.configPath, 'utf-8')
|
||||||
this.configCache.clear()
|
const parsed = yaml.load(content) as Record<string, unknown>
|
||||||
|
|
||||||
if (fs.existsSync(this.envPath)) {
|
// Zod 验证
|
||||||
const content = fs.readFileSync(this.envPath, 'utf-8')
|
const validated = fullConfigSchema.parse(parsed)
|
||||||
const lines = content.split('\n')
|
this.config = validated
|
||||||
|
|
||||||
for (const line of lines) {
|
log.info('Configuration loaded and validated successfully')
|
||||||
const trimmedLine = line.trim()
|
|
||||||
// Skip empty lines and comments
|
|
||||||
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const [key, ...valueParts] = trimmedLine.split('=')
|
|
||||||
if (key && valueParts.length > 0) {
|
|
||||||
const value = valueParts.join('=').trim()
|
|
||||||
this.configCache.set(key.trim(), value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ConfigManager] Failed to load .env file:', error)
|
if (error instanceof z.ZodError) {
|
||||||
|
const messages = error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`)
|
||||||
|
log.error('Configuration validation failed', { errors: messages })
|
||||||
|
throw new Error(`配置文件验证失败:\n${messages.join('\n')}`)
|
||||||
|
}
|
||||||
|
log.error('Failed to load configuration', { error })
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a configuration value
|
* 保存配置到 YAML 文件
|
||||||
* @param key - Configuration key
|
|
||||||
* @param defaultValue - Default value if key doesn't exist
|
|
||||||
*/
|
*/
|
||||||
public get(key: string): string | undefined
|
private async saveConfig(config: FullConfig): Promise<boolean> {
|
||||||
public get(key: string, defaultValue: string): string
|
|
||||||
public get(key: string, defaultValue?: string): string | undefined {
|
|
||||||
return this.configCache.get(key) ?? defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a boolean configuration value
|
|
||||||
*/
|
|
||||||
public getBoolean(key: string, defaultValue: boolean = false): boolean {
|
|
||||||
const value = this.get(key)
|
|
||||||
if (value === undefined) return defaultValue
|
|
||||||
return value.toLowerCase() === 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a number configuration value
|
|
||||||
*/
|
|
||||||
public getNumber(key: string, defaultValue: number = 0): number {
|
|
||||||
const value = this.get(key)
|
|
||||||
if (value === undefined) return defaultValue
|
|
||||||
const parsed = parseInt(value, 10)
|
|
||||||
return isNaN(parsed) ? defaultValue : parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a configuration value in cache
|
|
||||||
*/
|
|
||||||
public set(key: string, value: string | number | boolean): void {
|
|
||||||
this.configCache.set(key, String(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save configuration to .env file
|
|
||||||
*/
|
|
||||||
public async save(): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
// Build .env content from cache
|
// 备份现有配置
|
||||||
const lines: string[] = []
|
if (fs.existsSync(this.configPath)) {
|
||||||
|
fs.copyFileSync(this.configPath, this.backupPath)
|
||||||
|
}
|
||||||
|
|
||||||
// ERP Configuration - REMOVED
|
// 转换为 YAML
|
||||||
// ERP parameters are now stored in the database (dbo_BIPUsers table)
|
const content = yaml.dump(config, {
|
||||||
// This section is kept for backward compatibility but values are not used
|
indent: 2,
|
||||||
lines.push('# ===========================')
|
lineWidth: -1, // 不自动换行
|
||||||
lines.push('# ERP 系统配置(已迁移到数据库)')
|
noRefs: true, // 不使用引用
|
||||||
lines.push('# ===========================')
|
quotingType: '"',
|
||||||
lines.push('# ERP_URL, ERP_USERNAME, ERP_PASSWORD 已从 .env 移除')
|
forceQuotes: false
|
||||||
lines.push('# 这些参数现在存储在 dbo_BIPUsers 表中,每个用户可以有自己的 ERP 配置')
|
})
|
||||||
lines.push('')
|
|
||||||
|
|
||||||
// Database Configuration - SQL Server
|
fs.writeFileSync(this.configPath, content, 'utf-8')
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push('# 数据库配置 - SQL Server')
|
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push(`# DB_TYPE=sqlserver`)
|
|
||||||
lines.push(`# DB_SERVER=${this.configCache.get('DB_SERVER') || ''}`)
|
|
||||||
lines.push(`# DB_NAME=${this.configCache.get('DB_NAME') || ''}`)
|
|
||||||
lines.push(`# DB_USERNAME=${this.configCache.get('DB_USERNAME') || ''}`)
|
|
||||||
lines.push(`# DB_PASSWORD=${this.configCache.get('DB_PASSWORD') || ''}`)
|
|
||||||
lines.push(`DB_SQLSERVER_DRIVER=ODBC Driver 18 for SQL Server`)
|
|
||||||
lines.push(`DB_TRUST_SERVER_CERTIFICATE=yes`)
|
|
||||||
lines.push('')
|
|
||||||
|
|
||||||
// Database Configuration - MySQL
|
this.config = config
|
||||||
lines.push('# ===========================')
|
log.info('Configuration saved successfully')
|
||||||
lines.push('# 数据库配置 - MySQL (切换时使用)')
|
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push(`DB_TYPE=${this.configCache.get('DB_TYPE') || DEFAULT_SETTINGS.database.dbType}`)
|
|
||||||
lines.push(`DB_NAME=${this.configCache.get('DB_NAME') || DEFAULT_SETTINGS.database.database}`)
|
|
||||||
lines.push(
|
|
||||||
`DB_USERNAME=${this.configCache.get('DB_USERNAME') || DEFAULT_SETTINGS.database.username}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`DB_PASSWORD=${this.configCache.get('DB_PASSWORD') || DEFAULT_SETTINGS.database.password}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`DB_MYSQL_HOST=${this.configCache.get('DB_MYSQL_HOST') || DEFAULT_SETTINGS.database.mysqlHost}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`DB_MYSQL_PORT=${this.configCache.get('DB_MYSQL_PORT') || DEFAULT_SETTINGS.database.mysqlPort}`
|
|
||||||
)
|
|
||||||
lines.push(`DB_MYSQL_CHARSET=utf8mb4`)
|
|
||||||
lines.push('')
|
|
||||||
|
|
||||||
// Order number parsing table configuration
|
|
||||||
lines.push('# 订单号解析表配置')
|
|
||||||
lines.push('# 表名:包含 productionID 和 生产订单号 映射关系的表')
|
|
||||||
lines.push(`DB_TABLE_NAME=productionContractData_26年压力表合同数据`)
|
|
||||||
lines.push('# 字段名:总排号 (对应 productionID)')
|
|
||||||
lines.push(`DB_FIELD_PRODUCTION_ID=总排号`)
|
|
||||||
lines.push('# 字段名:生产订单号 (对应生产订单号)')
|
|
||||||
lines.push(`DB_FIELD_ORDER_NUMBER=生产订单号`)
|
|
||||||
lines.push('')
|
|
||||||
|
|
||||||
// Path Configuration
|
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push('# 路径配置')
|
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push(
|
|
||||||
`PATH_DATA_DIR=${this.configCache.get('PATH_DATA_DIR') || DEFAULT_SETTINGS.paths.dataDir}`
|
|
||||||
)
|
|
||||||
lines.push(`PATH_PRODUCTION_ID_FILE=ProductionID.txt`)
|
|
||||||
lines.push(
|
|
||||||
`PATH_DEFAULT_OUTPUT=${this.configCache.get('PATH_DEFAULT_OUTPUT') || DEFAULT_SETTINGS.paths.defaultOutput}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`PATH_VALIDATION_OUTPUT=${this.configCache.get('PATH_VALIDATION_OUTPUT') || DEFAULT_SETTINGS.paths.validationOutput}`
|
|
||||||
)
|
|
||||||
lines.push('')
|
|
||||||
|
|
||||||
// Data Extraction Configuration
|
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push('# 数据提取配置')
|
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push(
|
|
||||||
`EXTRACTION_BATCH_SIZE=${this.configCache.get('EXTRACTION_BATCH_SIZE') || DEFAULT_SETTINGS.extraction.batchSize}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`EXTRACTION_VERBOSE=${this.configCache.get('EXTRACTION_VERBOSE') || DEFAULT_SETTINGS.extraction.verbose}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`EXTRACTION_AUTO_CONVERT=${this.configCache.get('EXTRACTION_AUTO_CONVERT') || DEFAULT_SETTINGS.extraction.autoConvert}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`EXTRACTION_MERGE_BATCHES=${this.configCache.get('EXTRACTION_MERGE_BATCHES') || DEFAULT_SETTINGS.extraction.mergeBatches}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`EXTRACTION_ENABLE_DB_PERSISTENCE=${this.configCache.get('EXTRACTION_ENABLE_DB_PERSISTENCE') || DEFAULT_SETTINGS.extraction.enableDbPersistence}`
|
|
||||||
)
|
|
||||||
lines.push('')
|
|
||||||
|
|
||||||
// Validation Configuration
|
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push('# 校验配置')
|
|
||||||
lines.push('# ===========================')
|
|
||||||
lines.push(
|
|
||||||
`VALIDATION_DATA_SOURCE=${this.configCache.get('VALIDATION_DATA_SOURCE') || DEFAULT_SETTINGS.validation.dataSource}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`VALIDATION_USE_DATABASE=${this.configCache.get('VALIDATION_USE_DATABASE') || true}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`VALIDATION_BATCH_SIZE=${this.configCache.get('VALIDATION_BATCH_SIZE') || DEFAULT_SETTINGS.validation.batchSize}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`VALIDATION_ENABLE_CRUD=${this.configCache.get('VALIDATION_ENABLE_CRUD') || DEFAULT_SETTINGS.validation.enableCrud}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`VALIDATION_DEFAULT_MANAGER=${this.configCache.get('VALIDATION_DEFAULT_MANAGER') || DEFAULT_SETTINGS.validation.defaultManager}`
|
|
||||||
)
|
|
||||||
lines.push(
|
|
||||||
`VALIDATION_MATCH_MODE=${this.configCache.get('VALIDATION_MATCH_MODE') || DEFAULT_SETTINGS.validation.matchMode}`
|
|
||||||
)
|
|
||||||
lines.push('')
|
|
||||||
|
|
||||||
const content = lines.join('\n')
|
|
||||||
fs.writeFileSync(this.envPath, content, 'utf-8')
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ConfigManager] Failed to save .env file:', error)
|
log.error('Failed to save configuration', { error })
|
||||||
|
// 恢复备份
|
||||||
|
if (fs.existsSync(this.backupPath)) {
|
||||||
|
fs.copyFileSync(this.backupPath, this.configPath)
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all settings as SettingsData object
|
* 获取完整配置
|
||||||
* Note: ERP configuration is now stored in database, not .env
|
|
||||||
* The ERP values here are for UI display only and will not be used for actual ERP operations
|
|
||||||
*/
|
*/
|
||||||
public getAllSettings(): SettingsData {
|
public getConfig(): FullConfig {
|
||||||
return {
|
if (!this.config) {
|
||||||
erp: {
|
throw new Error('Configuration not initialized. Call initialize() first.')
|
||||||
// ERP config is now from database, these are placeholder defaults for UI
|
|
||||||
url: DEFAULT_SETTINGS.erp.url,
|
|
||||||
username: DEFAULT_SETTINGS.erp.username,
|
|
||||||
password: DEFAULT_SETTINGS.erp.password,
|
|
||||||
headless: true,
|
|
||||||
ignoreHttpsErrors: true,
|
|
||||||
autoCloseBrowser: true
|
|
||||||
},
|
|
||||||
database: {
|
|
||||||
dbType:
|
|
||||||
(this.get('DB_TYPE', DEFAULT_SETTINGS.database.dbType) as DatabaseType) ||
|
|
||||||
DEFAULT_SETTINGS.database.dbType,
|
|
||||||
server: this.get('DB_SERVER', DEFAULT_SETTINGS.database.server),
|
|
||||||
mysqlHost: this.get('DB_MYSQL_HOST', DEFAULT_SETTINGS.database.mysqlHost),
|
|
||||||
mysqlPort: this.getNumber('DB_MYSQL_PORT', DEFAULT_SETTINGS.database.mysqlPort),
|
|
||||||
database: this.get('DB_NAME', DEFAULT_SETTINGS.database.database),
|
|
||||||
username: this.get('DB_USERNAME', DEFAULT_SETTINGS.database.username),
|
|
||||||
password: this.get('DB_PASSWORD', DEFAULT_SETTINGS.database.password)
|
|
||||||
},
|
|
||||||
paths: {
|
|
||||||
dataDir: this.get('PATH_DATA_DIR', DEFAULT_SETTINGS.paths.dataDir),
|
|
||||||
defaultOutput: this.get('PATH_DEFAULT_OUTPUT', DEFAULT_SETTINGS.paths.defaultOutput),
|
|
||||||
validationOutput: this.get(
|
|
||||||
'PATH_VALIDATION_OUTPUT',
|
|
||||||
DEFAULT_SETTINGS.paths.validationOutput
|
|
||||||
)
|
|
||||||
},
|
|
||||||
extraction: {
|
|
||||||
batchSize: this.getNumber('EXTRACTION_BATCH_SIZE', DEFAULT_SETTINGS.extraction.batchSize),
|
|
||||||
verbose: this.getBoolean('EXTRACTION_VERBOSE', DEFAULT_SETTINGS.extraction.verbose),
|
|
||||||
autoConvert: this.getBoolean(
|
|
||||||
'EXTRACTION_AUTO_CONVERT',
|
|
||||||
DEFAULT_SETTINGS.extraction.autoConvert
|
|
||||||
),
|
|
||||||
mergeBatches: this.getBoolean(
|
|
||||||
'EXTRACTION_MERGE_BATCHES',
|
|
||||||
DEFAULT_SETTINGS.extraction.mergeBatches
|
|
||||||
),
|
|
||||||
enableDbPersistence: this.getBoolean(
|
|
||||||
'EXTRACTION_ENABLE_DB_PERSISTENCE',
|
|
||||||
DEFAULT_SETTINGS.extraction.enableDbPersistence
|
|
||||||
)
|
|
||||||
},
|
|
||||||
validation: {
|
|
||||||
dataSource:
|
|
||||||
(this.get(
|
|
||||||
'VALIDATION_DATA_SOURCE',
|
|
||||||
DEFAULT_SETTINGS.validation.dataSource
|
|
||||||
) as ValidationDataSource) || DEFAULT_SETTINGS.validation.dataSource,
|
|
||||||
batchSize: this.getNumber('VALIDATION_BATCH_SIZE', DEFAULT_SETTINGS.validation.batchSize),
|
|
||||||
matchMode:
|
|
||||||
(this.get('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode) as MatchMode) ||
|
|
||||||
DEFAULT_SETTINGS.validation.matchMode,
|
|
||||||
enableCrud: this.getBoolean(
|
|
||||||
'VALIDATION_ENABLE_CRUD',
|
|
||||||
DEFAULT_SETTINGS.validation.enableCrud
|
|
||||||
),
|
|
||||||
defaultManager: this.get(
|
|
||||||
'VALIDATION_DEFAULT_MANAGER',
|
|
||||||
DEFAULT_SETTINGS.validation.defaultManager
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return this.config
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save settings from SettingsData object
|
* 获取当前激活的数据库配置
|
||||||
* Note: ERP settings are NOT saved to .env anymore - they are stored in the database
|
|
||||||
*/
|
*/
|
||||||
public async saveAllSettings(settings: SettingsData): Promise<boolean> {
|
public getActiveDatabaseConfig(): MySqlConfig | SqlServerConfig {
|
||||||
// ERP settings are now stored in the database (dbo_BIPUsers table)
|
if (!this.config) {
|
||||||
// They are NOT saved to .env file anymore
|
throw new Error('Configuration not initialized')
|
||||||
|
}
|
||||||
|
|
||||||
// Database settings
|
const { activeType, mysql, sqlserver } = this.config.database
|
||||||
this.set('DB_TYPE', settings.database.dbType)
|
return activeType === 'mysql' ? mysql : sqlserver
|
||||||
this.set('DB_SERVER', settings.database.server)
|
|
||||||
this.set('DB_MYSQL_HOST', settings.database.mysqlHost)
|
|
||||||
this.set('DB_MYSQL_PORT', settings.database.mysqlPort)
|
|
||||||
this.set('DB_NAME', settings.database.database)
|
|
||||||
this.set('DB_USERNAME', settings.database.username)
|
|
||||||
this.set('DB_PASSWORD', settings.database.password)
|
|
||||||
|
|
||||||
// Path settings
|
|
||||||
this.set('PATH_DATA_DIR', settings.paths.dataDir)
|
|
||||||
this.set('PATH_DEFAULT_OUTPUT', settings.paths.defaultOutput)
|
|
||||||
this.set('PATH_VALIDATION_OUTPUT', settings.paths.validationOutput)
|
|
||||||
|
|
||||||
// Extraction settings
|
|
||||||
this.set('EXTRACTION_BATCH_SIZE', settings.extraction.batchSize)
|
|
||||||
this.set('EXTRACTION_VERBOSE', settings.extraction.verbose)
|
|
||||||
this.set('EXTRACTION_AUTO_CONVERT', settings.extraction.autoConvert)
|
|
||||||
this.set('EXTRACTION_MERGE_BATCHES', settings.extraction.mergeBatches)
|
|
||||||
this.set('EXTRACTION_ENABLE_DB_PERSISTENCE', settings.extraction.enableDbPersistence)
|
|
||||||
|
|
||||||
// Validation settings
|
|
||||||
this.set('VALIDATION_DATA_SOURCE', settings.validation.dataSource)
|
|
||||||
this.set('VALIDATION_BATCH_SIZE', settings.validation.batchSize)
|
|
||||||
this.set('VALIDATION_MATCH_MODE', settings.validation.matchMode)
|
|
||||||
this.set('VALIDATION_ENABLE_CRUD', settings.validation.enableCrud)
|
|
||||||
this.set('VALIDATION_DEFAULT_MANAGER', settings.validation.defaultManager)
|
|
||||||
|
|
||||||
return this.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save partial settings (only update provided fields)
|
* 获取数据库类型
|
||||||
* Preserves all existing fields not included in the update
|
|
||||||
*/
|
*/
|
||||||
public async savePartialSettings(
|
public getDatabaseType(): DatabaseType {
|
||||||
settings: Partial<SettingsData>
|
if (!this.config) {
|
||||||
|
throw new Error('Configuration not initialized')
|
||||||
|
}
|
||||||
|
return this.config.database.activeType
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新部分配置(深合并)
|
||||||
|
*/
|
||||||
|
public async updateConfig(
|
||||||
|
updates: Partial<FullConfig>
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// Step 1: Validate field whitelist
|
if (!this.config) {
|
||||||
const validation = validateEditableFields(settings)
|
await this.loadConfig()
|
||||||
if (!validation.valid) {
|
|
||||||
log.warn('Attempted to save non-editable fields', {
|
|
||||||
invalidFields: validation.invalidFields
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `包含不允许修改的字段:${validation.invalidFields.join(', ')}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Read current settings from .env file directly
|
// 深合并
|
||||||
// This avoids the cache key mismatch issue (ERP_URL vs erp.url)
|
const merged = this.deepMerge(this.config!, updates)
|
||||||
await this.loadEnvFile()
|
|
||||||
const currentSettings = this.getAllSettings()
|
|
||||||
|
|
||||||
log.info('Current settings before merge', {
|
// 验证合并后的配置
|
||||||
erpUrl: currentSettings.erp.url,
|
const validated = fullConfigSchema.parse(merged)
|
||||||
dbType: currentSettings.database.dbType,
|
|
||||||
dbName: currentSettings.database.database
|
|
||||||
})
|
|
||||||
|
|
||||||
// Step 3: Deep merge - only update provided fields
|
const success = await this.saveConfig(validated)
|
||||||
const mergedSettings = deepMerge(currentSettings, settings)
|
if (!success) {
|
||||||
|
return { success: false, error: '保存配置失败' }
|
||||||
log.info('Settings after merge', {
|
|
||||||
erpUrl: mergedSettings.erp.url,
|
|
||||||
dbType: mergedSettings.database.dbType,
|
|
||||||
dbName: mergedSettings.database.database
|
|
||||||
})
|
|
||||||
|
|
||||||
// Step 4: Backup and save
|
|
||||||
const backupSuccess = await this.backupEnvFile()
|
|
||||||
if (!backupSuccess) {
|
|
||||||
log.warn('Failed to backup .env file, proceeding with caution')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveSuccess = await this.saveAllSettings(mergedSettings)
|
|
||||||
|
|
||||||
if (!saveSuccess) {
|
|
||||||
// Save failed, attempt restore
|
|
||||||
await this.restoreBackup()
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: '保存配置失败,已恢复原配置'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Reload from disk to populate cache with correct keys (ERP_URL instead of erp.url)
|
|
||||||
await this.loadEnvFile()
|
|
||||||
|
|
||||||
log.info('Settings saved successfully', {
|
|
||||||
updatedFields: Object.keys(settings)
|
|
||||||
})
|
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
if (error instanceof z.ZodError) {
|
||||||
log.error('Error in savePartialSettings', { error: message })
|
const messages = error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`)
|
||||||
await this.restoreBackup()
|
return { success: false, error: `配置验证失败:\n${messages.join('\n')}` }
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `保存配置时发生错误:${message}`
|
|
||||||
}
|
}
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : '未知错误' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset to default settings
|
* 深合并工具函数
|
||||||
*/
|
*/
|
||||||
public resetToDefaults(): SettingsData {
|
private deepMerge<T extends Record<string, any>>(source: T, target: Partial<T>): T {
|
||||||
// Clear cache and reload from defaults
|
const result = { ...source }
|
||||||
this.configCache.clear()
|
for (const key in target) {
|
||||||
|
if (target[key] !== undefined) {
|
||||||
// Set all defaults using underscore uppercase keys
|
if (
|
||||||
this.set('ERP_URL', DEFAULT_SETTINGS.erp.url)
|
typeof target[key] === 'object' &&
|
||||||
this.set('ERP_USERNAME', DEFAULT_SETTINGS.erp.username)
|
target[key] !== null &&
|
||||||
this.set('ERP_PASSWORD', DEFAULT_SETTINGS.erp.password)
|
!Array.isArray(target[key])
|
||||||
|
) {
|
||||||
this.set('DB_TYPE', DEFAULT_SETTINGS.database.dbType)
|
result[key] = this.deepMerge(result[key] as any, target[key] as any)
|
||||||
this.set('DB_SERVER', DEFAULT_SETTINGS.database.server)
|
} else {
|
||||||
this.set('DB_MYSQL_HOST', DEFAULT_SETTINGS.database.mysqlHost)
|
result[key] = target[key] as any
|
||||||
this.set('DB_MYSQL_PORT', DEFAULT_SETTINGS.database.mysqlPort)
|
}
|
||||||
this.set('DB_NAME', DEFAULT_SETTINGS.database.database)
|
|
||||||
this.set('DB_USERNAME', DEFAULT_SETTINGS.database.username)
|
|
||||||
this.set('DB_PASSWORD', DEFAULT_SETTINGS.database.password)
|
|
||||||
|
|
||||||
this.set('PATH_DATA_DIR', DEFAULT_SETTINGS.paths.dataDir)
|
|
||||||
this.set('PATH_DEFAULT_OUTPUT', DEFAULT_SETTINGS.paths.defaultOutput)
|
|
||||||
this.set('PATH_VALIDATION_OUTPUT', DEFAULT_SETTINGS.paths.validationOutput)
|
|
||||||
|
|
||||||
this.set('EXTRACTION_BATCH_SIZE', DEFAULT_SETTINGS.extraction.batchSize)
|
|
||||||
this.set('EXTRACTION_VERBOSE', DEFAULT_SETTINGS.extraction.verbose)
|
|
||||||
this.set('EXTRACTION_AUTO_CONVERT', DEFAULT_SETTINGS.extraction.autoConvert)
|
|
||||||
this.set('EXTRACTION_MERGE_BATCHES', DEFAULT_SETTINGS.extraction.mergeBatches)
|
|
||||||
this.set('EXTRACTION_ENABLE_DB_PERSISTENCE', DEFAULT_SETTINGS.extraction.enableDbPersistence)
|
|
||||||
|
|
||||||
this.set('VALIDATION_DATA_SOURCE', DEFAULT_SETTINGS.validation.dataSource)
|
|
||||||
this.set('VALIDATION_BATCH_SIZE', DEFAULT_SETTINGS.validation.batchSize)
|
|
||||||
this.set('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode)
|
|
||||||
this.set('VALIDATION_ENABLE_CRUD', DEFAULT_SETTINGS.validation.enableCrud)
|
|
||||||
this.set('VALIDATION_DEFAULT_MANAGER', DEFAULT_SETTINGS.validation.defaultManager)
|
|
||||||
|
|
||||||
return DEFAULT_SETTINGS
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get default settings
|
|
||||||
*/
|
|
||||||
public getDefaultSettings(): SettingsData {
|
|
||||||
return DEFAULT_SETTINGS
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup current .env file
|
|
||||||
*/
|
|
||||||
private async backupEnvFile(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(this.envPath)) {
|
|
||||||
fs.copyFileSync(this.envPath, this.backupPath)
|
|
||||||
log.debug('Backup created', { path: this.backupPath })
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
} catch (error) {
|
|
||||||
log.error('Failed to backup .env file', { error })
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore .env file from backup
|
* 重置为默认配置
|
||||||
*/
|
*/
|
||||||
private async restoreBackup(): Promise<boolean> {
|
public async resetToDefaults(): Promise<boolean> {
|
||||||
try {
|
return this.saveConfig(DEFAULT_CONFIG)
|
||||||
if (fs.existsSync(this.backupPath)) {
|
}
|
||||||
fs.copyFileSync(this.backupPath, this.envPath)
|
|
||||||
await this.loadEnvFile()
|
/**
|
||||||
log.debug('Restored from backup')
|
* 获取默认配置
|
||||||
return true
|
*/
|
||||||
}
|
public getDefaultConfig(): FullConfig {
|
||||||
return false
|
return DEFAULT_CONFIG
|
||||||
} catch (error) {
|
}
|
||||||
log.error('Failed to restore backup', { error })
|
|
||||||
return false
|
/**
|
||||||
|
* 导出配置为 YAML 字符串(用于 UI 显示或导出)
|
||||||
|
*/
|
||||||
|
public exportToYaml(): string {
|
||||||
|
if (!this.config) {
|
||||||
|
throw new Error('Configuration not initialized')
|
||||||
}
|
}
|
||||||
|
return yaml.dump(this.config, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,23 @@
|
|||||||
* TypeORM Data Source Configuration
|
* TypeORM Data Source Configuration
|
||||||
*
|
*
|
||||||
* Provides a centralized database connection for TypeORM entities.
|
* Provides a centralized database connection for TypeORM entities.
|
||||||
* Supports both MySQL and SQL Server based on DB_TYPE environment variable.
|
* Supports both MySQL and SQL Server based on configuration.
|
||||||
|
*
|
||||||
|
* Note: Configuration is now loaded from config.yaml via ConfigManager,
|
||||||
|
* not from environment variables.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'reflect-metadata'
|
import 'reflect-metadata'
|
||||||
import { DataSource, DataSourceOptions } from 'typeorm'
|
import { DataSource, DataSourceOptions } from 'typeorm'
|
||||||
|
import { ConfigManager } from '../config/config-manager'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get database type from environment
|
* Get database type from config manager
|
||||||
*/
|
*/
|
||||||
function getDatabaseType(): 'mysql' | 'mssql' {
|
function getDatabaseType(): 'mysql' | 'mssql' {
|
||||||
const dbType = process.env.DB_TYPE?.toLowerCase()
|
const configManager = ConfigManager.getInstance()
|
||||||
if (dbType === 'sqlserver' || dbType === 'mssql') {
|
const dbType = configManager.getDatabaseType()
|
||||||
return 'mssql'
|
return dbType === 'sqlserver' ? 'mssql' : 'mysql'
|
||||||
}
|
|
||||||
return 'mysql'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,36 +26,40 @@ function getDatabaseType(): 'mysql' | 'mssql' {
|
|||||||
*/
|
*/
|
||||||
function buildDataSourceOptions(): DataSourceOptions {
|
function buildDataSourceOptions(): DataSourceOptions {
|
||||||
const type = getDatabaseType()
|
const type = getDatabaseType()
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const config = configManager.getConfig()
|
||||||
|
|
||||||
const commonOptions: Partial<DataSourceOptions> = {
|
const commonOptions: Partial<DataSourceOptions> = {
|
||||||
entities: [__dirname + '/entities/*.{ts,js}'],
|
entities: [__dirname + '/entities/*.{ts,js}'],
|
||||||
synchronize: false, // Never auto-sync in production
|
synchronize: false, // Never auto-sync in production
|
||||||
logging: process.env.NODE_ENV !== 'production'
|
logging: false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'mssql') {
|
if (type === 'mssql') {
|
||||||
|
const dbConfig = config.database.sqlserver
|
||||||
return {
|
return {
|
||||||
type: 'mssql',
|
type: 'mssql',
|
||||||
host: process.env.DB_SERVER || 'localhost',
|
host: dbConfig.server,
|
||||||
port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10),
|
port: dbConfig.port,
|
||||||
username: process.env.DB_USERNAME || 'sa',
|
username: dbConfig.username,
|
||||||
password: process.env.DB_PASSWORD || '',
|
password: dbConfig.password,
|
||||||
database: process.env.DB_NAME || '',
|
database: dbConfig.database,
|
||||||
options: {
|
options: {
|
||||||
encrypt: false,
|
encrypt: false,
|
||||||
trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes'
|
trustServerCertificate: dbConfig.trustServerCertificate
|
||||||
},
|
},
|
||||||
...commonOptions
|
...commonOptions
|
||||||
} as DataSourceOptions
|
} as DataSourceOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbConfig = config.database.mysql
|
||||||
return {
|
return {
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
host: process.env.DB_MYSQL_HOST || 'localhost',
|
host: dbConfig.host,
|
||||||
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
port: dbConfig.port,
|
||||||
username: process.env.DB_USERNAME || 'root',
|
username: dbConfig.username,
|
||||||
password: process.env.DB_PASSWORD || '',
|
password: dbConfig.password,
|
||||||
database: process.env.DB_NAME || '',
|
database: dbConfig.database,
|
||||||
...commonOptions
|
...commonOptions
|
||||||
} as DataSourceOptions
|
} as DataSourceOptions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* Supports both MySQL and SQL Server databases.
|
* Supports both MySQL and SQL Server databases.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ConfigManager } from '../config/config-manager'
|
||||||
import { MySqlService } from './mysql'
|
import { MySqlService } from './mysql'
|
||||||
import { SqlServerService } from './sql-server'
|
import { SqlServerService } from './sql-server'
|
||||||
import type {
|
import type {
|
||||||
@@ -23,42 +24,43 @@ const log = createLogger('DatabaseFactory')
|
|||||||
const instances: Map<DatabaseType, IDatabaseService> = new Map()
|
const instances: Map<DatabaseType, IDatabaseService> = new Map()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current database type from environment
|
* Get the current database type from config manager
|
||||||
*/
|
*/
|
||||||
export function getDatabaseType(): DatabaseType {
|
export function getDatabaseType(): DatabaseType {
|
||||||
const dbType = process.env.DB_TYPE?.toLowerCase()
|
const configManager = ConfigManager.getInstance()
|
||||||
if (dbType === 'sqlserver' || dbType === 'mssql') {
|
return configManager.getDatabaseType()
|
||||||
return 'sqlserver'
|
|
||||||
}
|
|
||||||
return 'mysql'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create MySQL configuration from environment variables
|
* Create MySQL configuration from config manager
|
||||||
*/
|
*/
|
||||||
export function createMySqlConfig(): MySqlConfig {
|
export function createMySqlConfig(): MySqlConfig {
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const dbConfig = configManager.getConfig().database.mysql
|
||||||
return {
|
return {
|
||||||
host: process.env.DB_MYSQL_HOST || 'localhost',
|
host: dbConfig.host,
|
||||||
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
port: dbConfig.port,
|
||||||
user: process.env.DB_USERNAME || 'root',
|
user: dbConfig.username,
|
||||||
password: process.env.DB_PASSWORD || '',
|
password: dbConfig.password,
|
||||||
database: process.env.DB_NAME || ''
|
database: dbConfig.database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create SQL Server configuration from environment variables
|
* Create SQL Server configuration from config manager
|
||||||
*/
|
*/
|
||||||
export function createSqlServerConfig(): SqlServerConfig {
|
export function createSqlServerConfig(): SqlServerConfig {
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const dbConfig = configManager.getConfig().database.sqlserver
|
||||||
return {
|
return {
|
||||||
server: process.env.DB_SERVER || 'localhost',
|
server: dbConfig.server,
|
||||||
port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10),
|
port: dbConfig.port,
|
||||||
user: process.env.DB_USERNAME || 'sa',
|
user: dbConfig.username,
|
||||||
password: process.env.DB_PASSWORD || '',
|
password: dbConfig.password,
|
||||||
database: process.env.DB_NAME || '',
|
database: dbConfig.database,
|
||||||
options: {
|
options: {
|
||||||
encrypt: false,
|
encrypt: false,
|
||||||
trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes'
|
trustServerCertificate: dbConfig.trustServerCertificate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +70,7 @@ export function createSqlServerConfig(): SqlServerConfig {
|
|||||||
*
|
*
|
||||||
* Uses singleton pattern - returns cached instance if available.
|
* Uses singleton pattern - returns cached instance if available.
|
||||||
*
|
*
|
||||||
* @param type - Optional database type override (defaults to DB_TYPE env var)
|
* @param type - Optional database type override (defaults to config)
|
||||||
* @returns Database service instance
|
* @returns Database service instance
|
||||||
*/
|
*/
|
||||||
export async function create(type?: DatabaseType): Promise<IDatabaseService> {
|
export async function create(type?: DatabaseType): Promise<IDatabaseService> {
|
||||||
@@ -105,7 +107,7 @@ export async function create(type?: DatabaseType): Promise<IDatabaseService> {
|
|||||||
/**
|
/**
|
||||||
* Get existing database service without creating new one
|
* Get existing database service without creating new one
|
||||||
*
|
*
|
||||||
* @param type - Optional database type (defaults to DB_TYPE env var)
|
* @param type - Optional database type (defaults to config)
|
||||||
* @returns Database service instance or undefined
|
* @returns Database service instance or undefined
|
||||||
*/
|
*/
|
||||||
export function get(type?: DatabaseType): IDatabaseService | undefined {
|
export function get(type?: DatabaseType): IDatabaseService | undefined {
|
||||||
|
|||||||
@@ -7,14 +7,12 @@
|
|||||||
* - productionID format: 2 digits + 1 letter + serial number (e.g., "22A1", "22A1234")
|
* - productionID format: 2 digits + 1 letter + serial number (e.g., "22A1", "22A1234")
|
||||||
* - 生产订单号 format: SC + 14 digits (e.g., "SC70202602120085")
|
* - 生产订单号 format: SC + 14 digits (e.g., "SC70202602120085")
|
||||||
*
|
*
|
||||||
* Database table: productionContractData_26年压力表合同数据
|
* Database table and field names are loaded from config.yaml
|
||||||
* Fields: 总排号 (productionID), 生产订单号 (production order number)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IDatabaseService } from '../database'
|
import type { IDatabaseService } from '../database'
|
||||||
import { SqlServerService } from '../database/sql-server'
|
import { ConfigManager } from '../config/config-manager'
|
||||||
import { createLogger } from '../logger'
|
import { createLogger } from '../logger'
|
||||||
import sql from 'mssql'
|
|
||||||
|
|
||||||
const log = createLogger('OrderResolver')
|
const log = createLogger('OrderResolver')
|
||||||
|
|
||||||
@@ -28,49 +26,52 @@ export interface OrderMapping {
|
|||||||
productionId?: string
|
productionId?: string
|
||||||
/** Final production order number to use */
|
/** Final production order number to use */
|
||||||
orderNumber?: string
|
orderNumber?: string
|
||||||
/** Whether this mapping is valid */
|
/** Whether the order number was successfully resolved */
|
||||||
isValid: boolean
|
resolved: boolean
|
||||||
/** Error or warning message */
|
/** Error message if resolution failed */
|
||||||
error?: string
|
error?: string
|
||||||
/** Input type */
|
|
||||||
inputType: 'productionId' | 'orderNumber' | 'unknown'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order number type recognition result
|
||||||
|
*/
|
||||||
|
export type OrderNumberType = 'productionId' | 'orderNumber' | 'unknown'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolution statistics
|
* Resolution statistics
|
||||||
*/
|
*/
|
||||||
export interface ResolutionStats {
|
export interface ResolutionStats {
|
||||||
totalInputs: number
|
totalInputs: number
|
||||||
recognizedAsProductionId: number
|
validOrderNumbers: number
|
||||||
recognizedAsOrderNumber: number
|
validProductionIds: number
|
||||||
|
resolvedCount: number
|
||||||
|
failedCount: number
|
||||||
unknownFormat: number
|
unknownFormat: number
|
||||||
successfullyResolved: number
|
|
||||||
failedToResolve: number
|
|
||||||
notFoundInDatabase: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regular expression patterns
|
* ProductionID pattern: 2 digits + 1 letter + 1-4 digits
|
||||||
*/
|
*/
|
||||||
export const ORDER_PATTERNS = {
|
const PRODUCTION_ID_PATTERN = /^\d{2}[A-Z]\d{1,4}$/i
|
||||||
/** productionID: 2 digits + 1 letter + serial number (1+) */
|
|
||||||
PRODUCTION_ID: /^\d{2}[A-Za-z]\d+$/,
|
/**
|
||||||
/** 生产订单号:SC + 14 digits */
|
* Production order number pattern: SC + 14 digits
|
||||||
ORDER_NUMBER: /^SC\d{14}$/
|
*/
|
||||||
} as const
|
const ORDER_NUMBER_PATTERN = /^SC\d{14}$/i
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database table and field names
|
* Database table and field names
|
||||||
* Can be overridden via environment variables:
|
* Loaded from config.yaml via ConfigManager
|
||||||
* - DB_TABLE_NAME: Table name (default: 'productionContractData_26年压力表合同数据')
|
|
||||||
* - DB_FIELD_PRODUCTION_ID: Field name for productionID (default: '总排号')
|
|
||||||
* - DB_FIELD_ORDER_NUMBER: Field name for order number (default: '生产订单号')
|
|
||||||
*/
|
*/
|
||||||
export const DB_CONFIG = {
|
export function getDbConfig() {
|
||||||
TABLE_NAME: process.env.DB_TABLE_NAME || 'productionContractData_26年压力表合同数据',
|
const configManager = ConfigManager.getInstance()
|
||||||
FIELD_PRODUCTION_ID: process.env.DB_FIELD_PRODUCTION_ID || '总排号',
|
const config = configManager.getConfig()
|
||||||
FIELD_ORDER_NUMBER: process.env.DB_FIELD_ORDER_NUMBER || '生产订单号'
|
return {
|
||||||
} as const
|
TABLE_NAME: config.orderResolution.tableName || 'productionContractData_26 年压力表合同数据',
|
||||||
|
FIELD_PRODUCTION_ID: config.orderResolution.productionIdField || '总排号',
|
||||||
|
FIELD_ORDER_NUMBER: config.orderResolution.orderNumberField || '生产订单号'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Order Number Resolver Service
|
* Order Number Resolver Service
|
||||||
@@ -84,364 +85,201 @@ export class OrderNumberResolver {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get table name based on database type
|
* Get table name based on database type
|
||||||
* Converts MySQL schema_tablename format to SQL Server [schema].[tablename] format
|
|
||||||
* e.g., productionContractData_26年压力表合同数据 -> [productionContractData].[26年压力表合同数据]
|
|
||||||
*/
|
*/
|
||||||
private getTableName(mysqlTableName: string): string {
|
private getTableName(tableName: string): string {
|
||||||
if (this.dbService.type === 'sqlserver') {
|
if (this.dbService.type === 'sqlserver') {
|
||||||
const firstUnderscoreIndex = mysqlTableName.indexOf('_')
|
return `[dbo].[${tableName}]`
|
||||||
if (firstUnderscoreIndex > 0) {
|
|
||||||
const schema = mysqlTableName.substring(0, firstUnderscoreIndex)
|
|
||||||
const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1)
|
|
||||||
return `[${schema}].[${tableName}]`
|
|
||||||
}
|
|
||||||
return `[dbo].[${mysqlTableName}]`
|
|
||||||
}
|
}
|
||||||
return mysqlTableName
|
return tableName
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recognize the type of an input string
|
* Check if input matches productionID pattern
|
||||||
* @param input - The input string to recognize
|
|
||||||
* @returns The recognized type
|
|
||||||
*/
|
*/
|
||||||
recognizeType(input: string): 'productionId' | 'orderNumber' | 'unknown' {
|
isProductionId(input: string): boolean {
|
||||||
const trimmed = input.trim()
|
return PRODUCTION_ID_PATTERN.test(input)
|
||||||
|
|
||||||
if (ORDER_PATTERNS.ORDER_NUMBER.test(trimmed)) {
|
|
||||||
return 'orderNumber'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ORDER_PATTERNS.PRODUCTION_ID.test(trimmed)) {
|
|
||||||
return 'productionId'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'unknown'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a list of inputs to production order numbers
|
* Check if input matches order number pattern
|
||||||
* @param inputs - List of input strings (can be productionID or 生产订单号)
|
|
||||||
* @returns List of order mappings
|
|
||||||
*/
|
*/
|
||||||
async resolve(inputs: string[]): Promise<OrderMapping[]> {
|
isOrderNumber(input: string): boolean {
|
||||||
const mappings: OrderMapping[] = []
|
return ORDER_NUMBER_PATTERN.test(input)
|
||||||
const stats: ResolutionStats = {
|
|
||||||
totalInputs: inputs.length,
|
|
||||||
recognizedAsProductionId: 0,
|
|
||||||
recognizedAsOrderNumber: 0,
|
|
||||||
unknownFormat: 0,
|
|
||||||
successfullyResolved: 0,
|
|
||||||
failedToResolve: 0,
|
|
||||||
notFoundInDatabase: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// First pass: recognize types and categorize
|
|
||||||
const productionIds: string[] = []
|
|
||||||
const orderNumbers: string[] = []
|
|
||||||
|
|
||||||
for (const input of inputs) {
|
|
||||||
const trimmed = input.trim()
|
|
||||||
if (!trimmed) continue
|
|
||||||
|
|
||||||
const type = this.recognizeType(trimmed)
|
|
||||||
|
|
||||||
const baseMapping: OrderMapping = {
|
|
||||||
input: trimmed,
|
|
||||||
isValid: false,
|
|
||||||
inputType: type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'productionId') {
|
|
||||||
stats.recognizedAsProductionId++
|
|
||||||
productionIds.push(trimmed)
|
|
||||||
baseMapping.productionId = trimmed
|
|
||||||
} else if (type === 'orderNumber') {
|
|
||||||
stats.recognizedAsOrderNumber++
|
|
||||||
orderNumbers.push(trimmed)
|
|
||||||
baseMapping.orderNumber = trimmed
|
|
||||||
baseMapping.isValid = true // Order numbers are valid by format
|
|
||||||
stats.successfullyResolved++
|
|
||||||
} else {
|
|
||||||
stats.unknownFormat++
|
|
||||||
baseMapping.error = `无法识别的格式:${trimmed}`
|
|
||||||
mappings.push(baseMapping)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mappings.push(baseMapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query database for productionIDs
|
|
||||||
if (productionIds.length > 0) {
|
|
||||||
const productionIdMappings = await this.resolveProductionIds(productionIds)
|
|
||||||
|
|
||||||
// Update mappings with database results
|
|
||||||
for (const mapping of mappings) {
|
|
||||||
if (mapping.inputType === 'productionId') {
|
|
||||||
const dbResult = productionIdMappings.find((m) => m.input === mapping.input)
|
|
||||||
if (dbResult) {
|
|
||||||
mapping.orderNumber = dbResult.orderNumber
|
|
||||||
mapping.isValid = dbResult.isValid
|
|
||||||
mapping.error = dbResult.error
|
|
||||||
|
|
||||||
if (dbResult.isValid) {
|
|
||||||
stats.successfullyResolved++
|
|
||||||
} else {
|
|
||||||
stats.failedToResolve++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify order numbers exist in database (optional validation)
|
|
||||||
// This can be skipped if you want to allow any SC+14digits format
|
|
||||||
// For now, we'll verify them against the database
|
|
||||||
if (orderNumbers.length > 0) {
|
|
||||||
const verifiedOrderNumbers = new Set(await this.verifyOrderNumbers(orderNumbers))
|
|
||||||
|
|
||||||
for (const mapping of mappings) {
|
|
||||||
if (mapping.inputType === 'orderNumber' && mapping.orderNumber) {
|
|
||||||
if (!verifiedOrderNumbers.has(mapping.orderNumber)) {
|
|
||||||
mapping.isValid = false
|
|
||||||
mapping.error = `生产订单号不存在于数据库中:${mapping.orderNumber}`
|
|
||||||
stats.notFoundInDatabase++
|
|
||||||
stats.successfullyResolved--
|
|
||||||
stats.failedToResolve++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve productionIDs to production order numbers via database lookup
|
* Map productionID to order number via database lookup
|
||||||
* @param productionIds - List of productionIDs to resolve
|
|
||||||
* @returns List of order mappings
|
|
||||||
*/
|
*/
|
||||||
private async resolveProductionIds(productionIds: string[]): Promise<OrderMapping[]> {
|
async mapProductionIdToOrderNumber(productionId: string): Promise<string | null> {
|
||||||
const mappings: OrderMapping[] = []
|
|
||||||
|
|
||||||
if (!this.dbService.isConnected()) {
|
|
||||||
// Database not connected, return all as failed
|
|
||||||
for (const pid of productionIds) {
|
|
||||||
mappings.push({
|
|
||||||
input: pid,
|
|
||||||
productionId: pid,
|
|
||||||
isValid: false,
|
|
||||||
error: '数据库未连接,无法查询生产订单号',
|
|
||||||
inputType: 'productionId'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return mappings
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isSqlServer = this.dbService.type === 'sqlserver'
|
const dbConfig = getDbConfig()
|
||||||
const tableName = this.getTableName(DB_CONFIG.TABLE_NAME)
|
const tableName = this.getTableName(dbConfig.TABLE_NAME)
|
||||||
|
|
||||||
log.debug('Resolving production IDs', {
|
let sql: string
|
||||||
count: productionIds.length,
|
let params: any[]
|
||||||
dbType: this.dbService.type
|
|
||||||
})
|
|
||||||
|
|
||||||
let result
|
if (this.dbService.type === 'sqlserver') {
|
||||||
|
sql = `SELECT TOP 1 [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] = @p0`
|
||||||
if (isSqlServer) {
|
params = [productionId]
|
||||||
// Use queryWithParams for SQL Server with explicit parameter types
|
|
||||||
const placeholders = productionIds.map((_, idx) => `@p${idx}`).join(', ')
|
|
||||||
const params: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
value: string
|
|
||||||
type: sql.ISqlType | sql.ISqlTypeFactoryWithLength | sql.ISqlTypeWithLength
|
|
||||||
}
|
|
||||||
> = {}
|
|
||||||
|
|
||||||
productionIds.forEach((id, idx) => {
|
|
||||||
params[`p${idx}`] = { value: id, type: sql.NVarChar(255) }
|
|
||||||
})
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
SELECT ${DB_CONFIG.FIELD_PRODUCTION_ID}, ${DB_CONFIG.FIELD_ORDER_NUMBER}
|
|
||||||
FROM ${tableName}
|
|
||||||
WHERE ${DB_CONFIG.FIELD_PRODUCTION_ID} IN (${placeholders})
|
|
||||||
`
|
|
||||||
|
|
||||||
result = await (this.dbService as SqlServerService).queryWithParams(query, params)
|
|
||||||
} else {
|
} else {
|
||||||
// Use standard query for MySQL
|
sql = `SELECT \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` = ? LIMIT 1`
|
||||||
const placeholders = productionIds.map(() => '?').join(', ')
|
params = [productionId]
|
||||||
const query = `
|
|
||||||
SELECT \`${DB_CONFIG.FIELD_PRODUCTION_ID}\`, \`${DB_CONFIG.FIELD_ORDER_NUMBER}\`
|
|
||||||
FROM ${tableName}
|
|
||||||
WHERE \`${DB_CONFIG.FIELD_PRODUCTION_ID}\` IN (${placeholders})
|
|
||||||
`
|
|
||||||
result = await this.dbService.query(query, productionIds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a map for quick lookup (use lowercase key for case-insensitive matching)
|
const result = await this.dbService.query(sql, params)
|
||||||
const resultMap = new Map<string, string>()
|
|
||||||
|
if (result.rows.length > 0) {
|
||||||
|
const orderNumber = result.rows[0][Object.keys(result.rows[0])[0]] as string
|
||||||
|
return orderNumber || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : '未知数据库错误'
|
||||||
|
log.error('Failed to map productionID to order number', {
|
||||||
|
productionId,
|
||||||
|
error: message
|
||||||
|
})
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map multiple productionIds to order numbers
|
||||||
|
*/
|
||||||
|
async mapProductionIdsToOrderNumbers(productionIds: string[]): Promise<Map<string, string>> {
|
||||||
|
try {
|
||||||
|
const dbConfig = getDbConfig()
|
||||||
|
const tableName = this.getTableName(dbConfig.TABLE_NAME)
|
||||||
|
|
||||||
|
if (productionIds.length === 0) {
|
||||||
|
return new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use parameterized query to prevent SQL injection
|
||||||
|
const placeholders = productionIds.map((_, i) => `@p${i}`).join(', ')
|
||||||
|
const params = productionIds
|
||||||
|
|
||||||
|
let sql: string
|
||||||
|
if (this.dbService.type === 'sqlserver') {
|
||||||
|
sql = `SELECT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] IN (${placeholders})`
|
||||||
|
} else {
|
||||||
|
const idPlaceholders = productionIds.map(() => '?').join(', ')
|
||||||
|
sql = `SELECT \`${dbConfig.FIELD_PRODUCTION_ID}\`, \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` IN (${idPlaceholders})`
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.dbService.query(sql, params)
|
||||||
|
|
||||||
|
const mappings = new Map<string, string>()
|
||||||
for (const row of result.rows) {
|
for (const row of result.rows) {
|
||||||
const prodId = row[DB_CONFIG.FIELD_PRODUCTION_ID] as string
|
const keys = Object.keys(row)
|
||||||
const orderNum = row[DB_CONFIG.FIELD_ORDER_NUMBER] as string
|
const prodId = row[keys[0]] as string
|
||||||
|
const orderNum = row[keys[1]] as string
|
||||||
if (prodId && orderNum) {
|
if (prodId && orderNum) {
|
||||||
// Store with lowercase key for case-insensitive matching
|
mappings.set(prodId, orderNum)
|
||||||
resultMap.set(prodId.toLowerCase(), orderNum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build mappings
|
|
||||||
for (const pid of productionIds) {
|
|
||||||
// Use lowercase for case-insensitive lookup
|
|
||||||
const orderNumber = resultMap.get(pid.toLowerCase())
|
|
||||||
|
|
||||||
if (orderNumber) {
|
|
||||||
mappings.push({
|
|
||||||
input: pid,
|
|
||||||
productionId: pid,
|
|
||||||
orderNumber,
|
|
||||||
isValid: true,
|
|
||||||
inputType: 'productionId'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
mappings.push({
|
|
||||||
input: pid,
|
|
||||||
productionId: pid,
|
|
||||||
isValid: false,
|
|
||||||
error: `数据库中未找到生产 ID:${pid}`,
|
|
||||||
inputType: 'productionId'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mappings
|
return mappings
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : '未知数据库错误'
|
const message = error instanceof Error ? error.message : '未知数据库错误'
|
||||||
|
log.error('Failed to map productionIds to order numbers', {
|
||||||
// Return all as failed with error
|
error: message
|
||||||
return productionIds.map((pid) => ({
|
})
|
||||||
input: pid,
|
throw error
|
||||||
productionId: pid,
|
|
||||||
isValid: false,
|
|
||||||
error: `数据库查询失败:${message}`,
|
|
||||||
inputType: 'productionId'
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that order numbers exist in the database
|
* Resolve order numbers from mixed input
|
||||||
* @param orderNumbers - List of order numbers to verify
|
|
||||||
* @returns List of valid order numbers
|
|
||||||
*/
|
*/
|
||||||
private async verifyOrderNumbers(orderNumbers: string[]): Promise<string[]> {
|
async resolve(inputs: string[]): Promise<OrderMapping[]> {
|
||||||
if (!this.dbService.isConnected()) {
|
const mappings: OrderMapping[] = []
|
||||||
return orderNumbers // Skip verification if not connected
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
for (const input of inputs) {
|
||||||
const isSqlServer = this.dbService.type === 'sqlserver'
|
const mapping: OrderMapping = { input, resolved: false }
|
||||||
const tableName = this.getTableName(DB_CONFIG.TABLE_NAME)
|
|
||||||
|
|
||||||
let result
|
if (this.isOrderNumber(input)) {
|
||||||
|
// Already an order number
|
||||||
if (isSqlServer) {
|
mapping.orderNumber = input
|
||||||
// Use queryWithParams for SQL Server with explicit parameter types
|
mapping.resolved = true
|
||||||
const placeholders = orderNumbers.map((_, idx) => `@p${idx}`).join(', ')
|
} else if (this.isProductionId(input)) {
|
||||||
const params: Record<
|
// Is a productionID, need to lookup
|
||||||
string,
|
mapping.productionId = input
|
||||||
{
|
try {
|
||||||
value: string
|
const orderNumber = await this.mapProductionIdToOrderNumber(input)
|
||||||
type: sql.ISqlType | sql.ISqlTypeFactoryWithLength | sql.ISqlTypeWithLength
|
if (orderNumber) {
|
||||||
|
mapping.orderNumber = orderNumber
|
||||||
|
mapping.resolved = true
|
||||||
|
} else {
|
||||||
|
mapping.error = '未在数据库中找到对应的订单号'
|
||||||
}
|
}
|
||||||
> = {}
|
} catch (error) {
|
||||||
|
mapping.error = error instanceof Error ? error.message : '数据库查询失败'
|
||||||
orderNumbers.forEach((id, idx) => {
|
log.warn('Failed to resolve productionID', { productionId: input, error })
|
||||||
params[`p${idx}`] = { value: id, type: sql.NVarChar(255) }
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
SELECT ${DB_CONFIG.FIELD_ORDER_NUMBER}
|
|
||||||
FROM ${tableName}
|
|
||||||
WHERE ${DB_CONFIG.FIELD_ORDER_NUMBER} IN (${placeholders})
|
|
||||||
`
|
|
||||||
|
|
||||||
result = await (this.dbService as SqlServerService).queryWithParams(query, params)
|
|
||||||
} else {
|
} else {
|
||||||
// Use standard query for MySQL
|
mapping.error = '格式不识别:既不是有效的生产订单号也不是总排号格式'
|
||||||
const placeholders = orderNumbers.map(() => '?').join(', ')
|
|
||||||
const query = `
|
|
||||||
SELECT \`${DB_CONFIG.FIELD_ORDER_NUMBER}\`
|
|
||||||
FROM ${tableName}
|
|
||||||
WHERE \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` IN (${placeholders})
|
|
||||||
`
|
|
||||||
result = await this.dbService.query(query, orderNumbers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.rows.map((row) => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string)
|
mappings.push(mapping)
|
||||||
} catch (error) {
|
|
||||||
console.warn('[OrderResolver] Failed to verify order numbers:', error)
|
|
||||||
return orderNumbers // Skip verification on error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get valid order numbers from mappings
|
||||||
|
*/
|
||||||
|
getValidOrderNumbers(mappings: OrderMapping[]): string[] {
|
||||||
|
return mappings.filter((m) => m.resolved && m.orderNumber).map((m) => m.orderNumber!)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get warnings from failed mappings
|
||||||
|
*/
|
||||||
|
getWarnings(mappings: OrderMapping[]): string[] {
|
||||||
|
return mappings.filter((m) => !m.resolved && m.error).map((m) => `${m.input}: ${m.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recognize the type of input
|
||||||
|
*/
|
||||||
|
recognizeType(input: string): OrderNumberType {
|
||||||
|
if (this.isOrderNumber(input)) return 'orderNumber'
|
||||||
|
if (this.isProductionId(input)) return 'productionId'
|
||||||
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get resolution statistics
|
* Get resolution statistics
|
||||||
* @param mappings - List of order mappings
|
|
||||||
* @returns Resolution statistics
|
|
||||||
*/
|
*/
|
||||||
getStats(mappings: OrderMapping[]): ResolutionStats {
|
getStats(mappings: OrderMapping[]): ResolutionStats {
|
||||||
const stats: ResolutionStats = {
|
const stats: ResolutionStats = {
|
||||||
totalInputs: mappings.length,
|
totalInputs: mappings.length,
|
||||||
recognizedAsProductionId: 0,
|
validOrderNumbers: 0,
|
||||||
recognizedAsOrderNumber: 0,
|
validProductionIds: 0,
|
||||||
unknownFormat: 0,
|
resolvedCount: 0,
|
||||||
successfullyResolved: 0,
|
failedCount: 0,
|
||||||
failedToResolve: 0,
|
unknownFormat: 0
|
||||||
notFoundInDatabase: 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const mapping of mappings) {
|
for (const mapping of mappings) {
|
||||||
if (mapping.inputType === 'productionId') {
|
if (mapping.resolved) {
|
||||||
stats.recognizedAsProductionId++
|
stats.resolvedCount++
|
||||||
} else if (mapping.inputType === 'orderNumber') {
|
if (mapping.orderNumber && !mapping.productionId) {
|
||||||
stats.recognizedAsOrderNumber++
|
stats.validOrderNumbers++
|
||||||
|
} else if (mapping.productionId) {
|
||||||
|
stats.validProductionIds++
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stats.unknownFormat++
|
stats.failedCount++
|
||||||
}
|
if (!mapping.productionId && !mapping.orderNumber) {
|
||||||
|
stats.unknownFormat++
|
||||||
if (mapping.isValid) {
|
}
|
||||||
stats.successfullyResolved++
|
|
||||||
} else if (mapping.error?.includes('不存在于数据库中')) {
|
|
||||||
stats.notFoundInDatabase++
|
|
||||||
stats.failedToResolve++
|
|
||||||
} else if (mapping.error) {
|
|
||||||
stats.failedToResolve++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract valid order numbers from mappings
|
|
||||||
* @param mappings - List of order mappings
|
|
||||||
* @returns List of valid production order numbers
|
|
||||||
*/
|
|
||||||
getValidOrderNumbers(mappings: OrderMapping[]): string[] {
|
|
||||||
return mappings.filter((m) => m.isValid && m.orderNumber).map((m) => m.orderNumber!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract warnings/errors from mappings
|
|
||||||
* @param mappings - List of order mappings
|
|
||||||
* @returns List of warning messages
|
|
||||||
*/
|
|
||||||
getWarnings(mappings: OrderMapping[]): string[] {
|
|
||||||
return mappings.filter((m) => !m.isValid && m.error).map((m) => m.error!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const createFileTransport = (level?: string): DailyRotateFile => {
|
|||||||
|
|
||||||
// Create the logger instance
|
// Create the logger instance
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: process.env.LOG_LEVEL || 'info',
|
level: 'info', // Log level is now hardcoded, can be moved to config.yaml if needed
|
||||||
defaultMeta: { service: 'erpauto' },
|
defaultMeta: { service: 'erpauto' },
|
||||||
transports: [
|
transports: [
|
||||||
// Console transport - always enabled
|
// Console transport - always enabled
|
||||||
@@ -67,7 +67,7 @@ const logger = winston.createLogger({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Add error-specific file transport in production
|
// Add error-specific file transport in production
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (app.isPackaged) {
|
||||||
logger.add(
|
logger.add(
|
||||||
new DailyRotateFile({
|
new DailyRotateFile({
|
||||||
filename: path.join(getLogDir(), 'error-%DATE%.log'),
|
filename: path.join(getLogDir(), 'error-%DATE%.log'),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import { MySqlService } from '../database/mysql'
|
import { MySqlService } from '../database/mysql'
|
||||||
import { SqlServerService } from '../database/sql-server'
|
import { SqlServerService } from '../database/sql-server'
|
||||||
|
import { ConfigManager } from '../config/config-manager'
|
||||||
import sql from 'mssql'
|
import sql from 'mssql'
|
||||||
import type { UserInfo } from '../../types/user.types'
|
import type { UserInfo } from '../../types/user.types'
|
||||||
|
|
||||||
@@ -43,17 +44,14 @@ export class BIPUsersDAO {
|
|||||||
private mysqlService: MySqlService | null = null
|
private mysqlService: MySqlService | null = null
|
||||||
private sqlServerService: SqlServerService | null = null
|
private sqlServerService: SqlServerService | null = null
|
||||||
private dbType: 'mysql' | 'sqlserver' = 'mysql'
|
private dbType: 'mysql' | 'sqlserver' = 'mysql'
|
||||||
|
private configManager: ConfigManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor - determine database type from environment
|
* Constructor - get database type from ConfigManager
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
const dbType = process.env.DB_TYPE?.toLowerCase()
|
this.configManager = ConfigManager.getInstance()
|
||||||
if (dbType === 'sqlserver' || dbType === 'mssql') {
|
this.dbType = this.configManager.getDatabaseType()
|
||||||
this.dbType = 'sqlserver'
|
|
||||||
} else {
|
|
||||||
this.dbType = 'mysql'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,20 +67,23 @@ export class BIPUsersDAO {
|
|||||||
* Get database service instance (MySQL or SQL Server)
|
* Get database service instance (MySQL or SQL Server)
|
||||||
*/
|
*/
|
||||||
private async getDatabaseService(): Promise<MySqlService | SqlServerService> {
|
private async getDatabaseService(): Promise<MySqlService | SqlServerService> {
|
||||||
|
const config = this.configManager.getConfig()
|
||||||
|
|
||||||
if (this.dbType === 'sqlserver') {
|
if (this.dbType === 'sqlserver') {
|
||||||
if (this.sqlServerService && this.sqlServerService.isConnected()) {
|
if (this.sqlServerService && this.sqlServerService.isConnected()) {
|
||||||
return this.sqlServerService
|
return this.sqlServerService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbConfig = config.database.sqlserver
|
||||||
this.sqlServerService = new SqlServerService({
|
this.sqlServerService = new SqlServerService({
|
||||||
server: process.env.DB_SERVER || 'localhost',
|
server: dbConfig.server,
|
||||||
port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10),
|
port: dbConfig.port,
|
||||||
user: process.env.DB_USERNAME || 'sa',
|
user: dbConfig.username,
|
||||||
password: process.env.DB_PASSWORD || '',
|
password: dbConfig.password,
|
||||||
database: process.env.DB_NAME || '',
|
database: dbConfig.database,
|
||||||
options: {
|
options: {
|
||||||
encrypt: false,
|
encrypt: false,
|
||||||
trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes'
|
trustServerCertificate: dbConfig.trustServerCertificate
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -93,12 +94,13 @@ export class BIPUsersDAO {
|
|||||||
return this.mysqlService
|
return this.mysqlService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbConfig = config.database.mysql
|
||||||
this.mysqlService = new MySqlService({
|
this.mysqlService = new MySqlService({
|
||||||
host: process.env.DB_MYSQL_HOST || 'localhost',
|
host: dbConfig.host,
|
||||||
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
port: dbConfig.port,
|
||||||
user: process.env.DB_USERNAME || 'root',
|
user: dbConfig.username,
|
||||||
password: process.env.DB_PASSWORD || '',
|
password: dbConfig.password,
|
||||||
database: process.env.DB_NAME || ''
|
database: dbConfig.database
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.mysqlService.connect()
|
await this.mysqlService.connect()
|
||||||
@@ -485,12 +487,11 @@ export class BIPUsersDAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ERP configuration for a user
|
* Get ERP credentials for a user (username and password only, URL is from config.yaml)
|
||||||
* @param username - The username to get ERP config for
|
* @param username - The username to get ERP credentials for
|
||||||
* @returns ERP configuration object or null if not found
|
* @returns ERP credentials object or null if not found
|
||||||
*/
|
*/
|
||||||
async getUserErpConfig(username: string): Promise<{
|
async getUserErpCredentials(username: string): Promise<{
|
||||||
url: string
|
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
} | null> {
|
} | null> {
|
||||||
@@ -501,7 +502,7 @@ export class BIPUsersDAO {
|
|||||||
|
|
||||||
if (this.dbType === 'sqlserver') {
|
if (this.dbType === 'sqlserver') {
|
||||||
const sqlString = `
|
const sqlString = `
|
||||||
SELECT ${cols.ERP_URL}, ${cols.ERP_USERNAME}, ${cols.ERP_PASSWORD}
|
SELECT ${cols.ERP_USERNAME}, ${cols.ERP_PASSWORD}
|
||||||
FROM ${tableName}
|
FROM ${tableName}
|
||||||
WHERE UserName = @username
|
WHERE UserName = @username
|
||||||
`
|
`
|
||||||
@@ -513,7 +514,6 @@ export class BIPUsersDAO {
|
|||||||
if (result.rows.length > 0) {
|
if (result.rows.length > 0) {
|
||||||
const row = result.rows[0]
|
const row = result.rows[0]
|
||||||
return {
|
return {
|
||||||
url: (row[cols.ERP_URL] as string) || '',
|
|
||||||
username: (row[cols.ERP_USERNAME] as string) || '',
|
username: (row[cols.ERP_USERNAME] as string) || '',
|
||||||
password: (row[cols.ERP_PASSWORD] as string) || ''
|
password: (row[cols.ERP_PASSWORD] as string) || ''
|
||||||
}
|
}
|
||||||
@@ -521,7 +521,7 @@ export class BIPUsersDAO {
|
|||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
const sqlString = `
|
const sqlString = `
|
||||||
SELECT ${cols.ERP_URL}, ${cols.ERP_USERNAME}, ${cols.ERP_PASSWORD}
|
SELECT ${cols.ERP_USERNAME}, ${cols.ERP_PASSWORD}
|
||||||
FROM ${tableName}
|
FROM ${tableName}
|
||||||
WHERE UserName = ?
|
WHERE UserName = ?
|
||||||
`
|
`
|
||||||
@@ -531,7 +531,6 @@ export class BIPUsersDAO {
|
|||||||
if (result.rows.length > 0) {
|
if (result.rows.length > 0) {
|
||||||
const row = result.rows[0]
|
const row = result.rows[0]
|
||||||
return {
|
return {
|
||||||
url: (row[cols.ERP_URL] as string) || '',
|
|
||||||
username: (row[cols.ERP_USERNAME] as string) || '',
|
username: (row[cols.ERP_USERNAME] as string) || '',
|
||||||
password: (row[cols.ERP_PASSWORD] as string) || ''
|
password: (row[cols.ERP_PASSWORD] as string) || ''
|
||||||
}
|
}
|
||||||
@@ -539,22 +538,20 @@ export class BIPUsersDAO {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BIPUsersDAO] Get user ERP config error:', error)
|
console.error('[BIPUsersDAO] Get user ERP credentials error:', error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update ERP configuration for a user
|
* Update ERP credentials for a user (username and password only, URL is from config.yaml)
|
||||||
* @param username - The username to update ERP config for
|
* @param username - The username to update ERP credentials for
|
||||||
* @param erpUrl - The ERP URL
|
|
||||||
* @param erpUsername - The ERP username
|
* @param erpUsername - The ERP username
|
||||||
* @param erpPassword - The ERP password
|
* @param erpPassword - The ERP password
|
||||||
* @returns True if successful
|
* @returns True if successful
|
||||||
*/
|
*/
|
||||||
async updateUserErpConfig(
|
async updateUserErpCredentials(
|
||||||
username: string,
|
username: string,
|
||||||
erpUrl: string,
|
|
||||||
erpUsername: string,
|
erpUsername: string,
|
||||||
erpPassword: string
|
erpPassword: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@@ -566,15 +563,13 @@ export class BIPUsersDAO {
|
|||||||
if (this.dbType === 'sqlserver') {
|
if (this.dbType === 'sqlserver') {
|
||||||
const sqlString = `
|
const sqlString = `
|
||||||
UPDATE ${tableName}
|
UPDATE ${tableName}
|
||||||
SET ${cols.ERP_URL} = @erpUrl,
|
SET ${cols.ERP_USERNAME} = @erpUsername,
|
||||||
${cols.ERP_USERNAME} = @erpUsername,
|
|
||||||
${cols.ERP_PASSWORD} = @erpPassword
|
${cols.ERP_PASSWORD} = @erpPassword
|
||||||
WHERE UserName = @username
|
WHERE UserName = @username
|
||||||
`
|
`
|
||||||
|
|
||||||
await (dbService as SqlServerService).queryWithParams(sqlString, {
|
await (dbService as SqlServerService).queryWithParams(sqlString, {
|
||||||
username: { value: username, type: sql.NVarChar(255) },
|
username: { value: username, type: sql.NVarChar(255) },
|
||||||
erpUrl: { value: erpUrl, type: sql.NVarChar(500) },
|
|
||||||
erpUsername: { value: erpUsername, type: sql.NVarChar(255) },
|
erpUsername: { value: erpUsername, type: sql.NVarChar(255) },
|
||||||
erpPassword: { value: erpPassword, type: sql.NVarChar(255) }
|
erpPassword: { value: erpPassword, type: sql.NVarChar(255) }
|
||||||
})
|
})
|
||||||
@@ -582,22 +577,16 @@ export class BIPUsersDAO {
|
|||||||
} else {
|
} else {
|
||||||
const sqlString = `
|
const sqlString = `
|
||||||
UPDATE ${tableName}
|
UPDATE ${tableName}
|
||||||
SET ${cols.ERP_URL} = ?,
|
SET ${cols.ERP_USERNAME} = ?,
|
||||||
${cols.ERP_USERNAME} = ?,
|
|
||||||
${cols.ERP_PASSWORD} = ?
|
${cols.ERP_PASSWORD} = ?
|
||||||
WHERE UserName = ?
|
WHERE UserName = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
await (dbService as MySqlService).query(sqlString, [
|
await (dbService as MySqlService).query(sqlString, [erpUsername, erpPassword, username])
|
||||||
erpUrl,
|
|
||||||
erpUsername,
|
|
||||||
erpPassword,
|
|
||||||
username
|
|
||||||
])
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BIPUsersDAO] Update user ERP config error:', error)
|
console.error('[BIPUsersDAO] Update user ERP credentials error:', error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
* Migration Script: Add ERP parameters to BIPUsers table
|
* Migration Script: Add ERP parameters to BIPUsers table
|
||||||
*
|
*
|
||||||
* This script adds ERP_URL, ERP_Username, and ERP_Password columns
|
* This script adds ERP_URL, ERP_Username, and ERP_Password columns
|
||||||
* to the dbo_BIPUsers table and initializes all existing users
|
* to the dbo_BIPUsers table and initializes all existing users.
|
||||||
* with the same ERP credentials from the current .env configuration.
|
*
|
||||||
|
* Note: ERP credentials are now stored per-user in the database.
|
||||||
|
* This migration is for backward compatibility only.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* npx tsx src/main/services/user/migration/add-erp-params-migration.ts
|
* npx tsx src/main/services/user/migration/add-erp-params-migration.ts
|
||||||
@@ -16,6 +18,7 @@ import { dirname } from 'path'
|
|||||||
import { ConfigManager } from '../../config/config-manager'
|
import { ConfigManager } from '../../config/config-manager'
|
||||||
import { MySqlService } from '../../database/mysql'
|
import { MySqlService } from '../../database/mysql'
|
||||||
import { SqlServerService } from '../../database/sql-server'
|
import { SqlServerService } from '../../database/sql-server'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = dirname(__filename)
|
const __dirname = dirname(__filename)
|
||||||
@@ -28,8 +31,7 @@ const MIGRATION_CONFIG = {
|
|||||||
tableName: {
|
tableName: {
|
||||||
mysql: 'dbo_BIPUsers',
|
mysql: 'dbo_BIPUsers',
|
||||||
sqlserver: '[dbo].[BIPUsers]'
|
sqlserver: '[dbo].[BIPUsers]'
|
||||||
},
|
}
|
||||||
columns: ['ERP_URL', 'ERP_Username', 'ERP_Password']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,15 +42,12 @@ async function checkColumnExistsMySQL(
|
|||||||
tableName: string,
|
tableName: string,
|
||||||
columnName: string
|
columnName: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const sql = `
|
const result = await mysqlService.query(
|
||||||
SELECT COUNT(*) as count
|
`SELECT COUNT(*) as count FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
[tableName, columnName]
|
||||||
AND TABLE_NAME = ?
|
)
|
||||||
AND COLUMN_NAME = ?
|
return (result.rows[0]?.count as number) > 0
|
||||||
`
|
|
||||||
const result = await mysqlService.query(sql, [tableName, columnName])
|
|
||||||
return result.rows.length > 0 && (result.rows[0].count as number) > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,16 +58,12 @@ async function checkColumnExistsSqlServer(
|
|||||||
tableName: string,
|
tableName: string,
|
||||||
columnName: string
|
columnName: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const sql = `
|
const result = await sqlServerService.query(
|
||||||
SELECT COUNT(*) as count
|
`SELECT COUNT(*) as count FROM sys.columns
|
||||||
FROM sys.columns
|
WHERE OBJECT_ID = OBJECT_ID(?) AND name = ?`,
|
||||||
WHERE object_id = OBJECT_ID(${tableName})
|
[tableName, columnName]
|
||||||
AND name = @columnName
|
)
|
||||||
`
|
return (result.rows[0]?.count as number) > 0
|
||||||
const result = await sqlServerService.queryWithParams(sql, {
|
|
||||||
columnName: { value: columnName.replace('ERP_', ''), type: require('mssql').NVarChar(128) }
|
|
||||||
})
|
|
||||||
return result.rows.length > 0 && (result.rows[0].count as number) > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,9 +75,8 @@ async function addColumnMySQL(
|
|||||||
columnName: string,
|
columnName: string,
|
||||||
columnType: string
|
columnType: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const sql = `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType} NULL`
|
await mysqlService.query(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`)
|
||||||
await mysqlService.query(sql)
|
console.log(` ✓ Added column ${columnName} (${columnType})`)
|
||||||
console.log(` ✓ Added column ${columnName} to ${tableName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,49 +88,64 @@ async function addColumnSqlServer(
|
|||||||
columnName: string,
|
columnName: string,
|
||||||
columnType: string
|
columnType: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const sql = `ALTER TABLE ${tableName} ADD ${columnName} ${columnType} NULL`
|
await sqlServerService.query(`ALTER TABLE ${tableName} ADD ${columnName} ${columnType}`)
|
||||||
await sqlServerService.query(sql)
|
console.log(` ✓ Added column ${columnName} (${columnType})`)
|
||||||
console.log(` ✓ Added column ${columnName} to ${tableName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update all users with ERP credentials from .env
|
* Initialize ERP credentials for all users in MySQL
|
||||||
*/
|
*/
|
||||||
async function initializeErpCredentialsMySQL(
|
async function initializeErpCredentialsMySQL(
|
||||||
mysqlService: MySqlService,
|
mysqlService: MySqlService,
|
||||||
|
tableName: string,
|
||||||
erpUrl: string,
|
erpUrl: string,
|
||||||
erpUsername: string,
|
erpUsername: string,
|
||||||
erpPassword: string
|
erpPassword: string
|
||||||
): Promise<number> {
|
): Promise<void> {
|
||||||
const sql = `
|
const result = await mysqlService.query(`SELECT COUNT(*) as count FROM ${tableName}`)
|
||||||
UPDATE ${MIGRATION_CONFIG.tableName.mysql}
|
const userCount = result.rows[0]?.count as number
|
||||||
SET ERP_URL = ?, ERP_Username = ?, ERP_Password = ?
|
|
||||||
WHERE ERP_URL IS NULL OR ERP_URL = ''
|
if (userCount === 0) {
|
||||||
`
|
console.log('No users found in BIPUsers table')
|
||||||
const result = await mysqlService.query(sql, [erpUrl, erpUsername, erpPassword])
|
return
|
||||||
return result.rowCount
|
}
|
||||||
|
|
||||||
|
console.log(`Initializing ERP credentials for ${userCount} user(s)...`)
|
||||||
|
|
||||||
|
await mysqlService.query(
|
||||||
|
`UPDATE ${tableName} SET ERP_URL = ?, ERP_Username = ?, ERP_Password = ?`,
|
||||||
|
[erpUrl, erpUsername, erpPassword]
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('✓ ERP credentials initialized for all users')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update all users with ERP credentials from .env (SQL Server)
|
* Initialize ERP credentials for all users in SQL Server
|
||||||
*/
|
*/
|
||||||
async function initializeErpCredentialsSqlServer(
|
async function initializeErpCredentialsSqlServer(
|
||||||
sqlServerService: SqlServerService,
|
sqlServerService: SqlServerService,
|
||||||
|
tableName: string,
|
||||||
erpUrl: string,
|
erpUrl: string,
|
||||||
erpUsername: string,
|
erpUsername: string,
|
||||||
erpPassword: string
|
erpPassword: string
|
||||||
): Promise<number> {
|
): Promise<void> {
|
||||||
const sql = `
|
const result = await sqlServerService.query(`SELECT COUNT(*) as count FROM ${tableName}`)
|
||||||
UPDATE ${MIGRATION_CONFIG.tableName.sqlserver}
|
const userCount = result.rows[0]?.count as number
|
||||||
SET ERP_URL = @erpUrl, ERP_Username = @erpUsername, ERP_Password = @erpPassword
|
|
||||||
WHERE ERP_URL IS NULL OR ERP_URL = ''
|
if (userCount === 0) {
|
||||||
`
|
console.log('No users found in BIPUsers table')
|
||||||
const result = await sqlServerService.queryWithParams(sql, {
|
return
|
||||||
erpUrl: { value: erpUrl, type: require('mssql').NVarChar(500) },
|
}
|
||||||
erpUsername: { value: erpUsername, type: require('mssql').NVarChar(255) },
|
|
||||||
erpPassword: { value: erpPassword, type: require('mssql').NVarChar(255) }
|
console.log(`Initializing ERP credentials for ${userCount} user(s)...`)
|
||||||
})
|
|
||||||
return result.rowCount
|
await sqlServerService.query(
|
||||||
|
`UPDATE ${tableName} SET ERP_URL = @p0, ERP_Username = @p1, ERP_Password = @p2`,
|
||||||
|
[erpUrl, erpUsername, erpPassword]
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('✓ ERP credentials initialized for all users')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,21 +154,17 @@ async function initializeErpCredentialsSqlServer(
|
|||||||
async function runMySQLMigration(configManager: ConfigManager): Promise<void> {
|
async function runMySQLMigration(configManager: ConfigManager): Promise<void> {
|
||||||
console.log('\n📦 Running MySQL Migration...')
|
console.log('\n📦 Running MySQL Migration...')
|
||||||
|
|
||||||
// Read database config from .env file with correct key names
|
const config = configManager.getConfig()
|
||||||
const mysqlHost = configManager.get('DB_MYSQL_HOST', 'localhost')
|
const dbConfig = config.database.mysql
|
||||||
const mysqlPort = configManager.getNumber('DB_MYSQL_PORT', 3306)
|
|
||||||
const mysqlUser = configManager.get('DB_USERNAME', 'root')
|
|
||||||
const mysqlPassword = configManager.get('DB_PASSWORD', '')
|
|
||||||
const mysqlDatabase = configManager.get('DB_NAME', '')
|
|
||||||
|
|
||||||
console.log(`Connecting to MySQL: ${mysqlHost}:${mysqlPort}/${mysqlDatabase}`)
|
console.log(`Connecting to MySQL: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`)
|
||||||
|
|
||||||
const mysqlService = new MySqlService({
|
const mysqlService = new MySqlService({
|
||||||
host: mysqlHost,
|
host: dbConfig.host,
|
||||||
port: mysqlPort,
|
port: dbConfig.port,
|
||||||
user: mysqlUser,
|
user: dbConfig.username,
|
||||||
password: mysqlPassword,
|
password: dbConfig.password,
|
||||||
database: mysqlDatabase
|
database: dbConfig.database
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -182,29 +187,18 @@ async function runMySQLMigration(configManager: ConfigManager): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize ERP credentials from .env
|
// Note: ERP credentials are now managed per-user via settings UI
|
||||||
const erpUrl = configManager.get('ERP_URL', '')
|
// This migration no longer initializes them from config
|
||||||
const erpUsername = configManager.get('ERP_USERNAME', '')
|
console.log('\n✓ MySQL Migration completed')
|
||||||
const erpPassword = configManager.get('ERP_PASSWORD', '')
|
console.log(' Note: ERP credentials should be configured per-user via the Settings UI')
|
||||||
|
|
||||||
if (erpUrl && erpUsername && erpPassword) {
|
|
||||||
const updatedCount = await initializeErpCredentialsMySQL(
|
|
||||||
mysqlService,
|
|
||||||
erpUrl,
|
|
||||||
erpUsername,
|
|
||||||
erpPassword
|
|
||||||
)
|
|
||||||
console.log(` ✓ Updated ${updatedCount} users with ERP credentials`)
|
|
||||||
} else {
|
|
||||||
console.log(' ⚠ Skipping ERP credential initialization (missing .env values)')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ MySQL Migration completed successfully!\n')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ MySQL Migration failed:', error)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
await mysqlService.disconnect()
|
await mysqlService.disconnect()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ MySQL Migration failed:', error instanceof Error ? error.message : error)
|
||||||
|
if (mysqlService.isConnected()) {
|
||||||
|
await mysqlService.disconnect()
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,16 +208,19 @@ async function runMySQLMigration(configManager: ConfigManager): Promise<void> {
|
|||||||
async function runSqlServerMigration(configManager: ConfigManager): Promise<void> {
|
async function runSqlServerMigration(configManager: ConfigManager): Promise<void> {
|
||||||
console.log('\n📦 Running SQL Server Migration...')
|
console.log('\n📦 Running SQL Server Migration...')
|
||||||
|
|
||||||
const mssql = await import('mssql')
|
const config = configManager.getConfig()
|
||||||
|
const dbConfig = config.database.sqlserver
|
||||||
|
|
||||||
|
console.log(`Connecting to SQL Server: ${dbConfig.server}:${dbConfig.port}/${dbConfig.database}`)
|
||||||
|
|
||||||
const sqlServerService = new SqlServerService({
|
const sqlServerService = new SqlServerService({
|
||||||
server: configManager.get('DB_SERVER', 'localhost'),
|
server: dbConfig.server,
|
||||||
port: configManager.getNumber('DB_SQLSERVER_PORT', 1433),
|
port: dbConfig.port,
|
||||||
user: configManager.get('DB_USERNAME', 'sa'),
|
user: dbConfig.username,
|
||||||
password: configManager.get('DB_PASSWORD', ''),
|
password: dbConfig.password,
|
||||||
database: configManager.get('DB_NAME', ''),
|
database: dbConfig.database,
|
||||||
options: {
|
options: {
|
||||||
encrypt: false,
|
trustServerCertificate: dbConfig.trustServerCertificate
|
||||||
trustServerCertificate: configManager.get('DB_TRUST_SERVER_CERTIFICATE') === 'yes'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -247,70 +244,46 @@ async function runSqlServerMigration(configManager: ConfigManager): Promise<void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize ERP credentials from .env
|
// Note: ERP credentials are now managed per-user via settings UI
|
||||||
const erpUrl = configManager.get('ERP_URL', '')
|
console.log('\n✓ SQL Server Migration completed')
|
||||||
const erpUsername = configManager.get('ERP_USERNAME', '')
|
console.log(' Note: ERP credentials should be configured per-user via the Settings UI')
|
||||||
const erpPassword = configManager.get('ERP_PASSWORD', '')
|
|
||||||
|
|
||||||
if (erpUrl && erpUsername && erpPassword) {
|
|
||||||
const updatedCount = await initializeErpCredentialsSqlServer(
|
|
||||||
sqlServerService,
|
|
||||||
erpUrl,
|
|
||||||
erpUsername,
|
|
||||||
erpPassword
|
|
||||||
)
|
|
||||||
console.log(` ✓ Updated ${updatedCount} users with ERP credentials`)
|
|
||||||
} else {
|
|
||||||
console.log(' ⚠ Skipping ERP credential initialization (missing .env values)')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ SQL Server Migration completed successfully!\n')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ SQL Server Migration failed:', error)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
await sqlServerService.disconnect()
|
await sqlServerService.disconnect()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ SQL Server Migration failed:', error instanceof Error ? error.message : error)
|
||||||
|
if (sqlServerService.isConnected()) {
|
||||||
|
await sqlServerService.disconnect()
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main migration runner
|
* Main function
|
||||||
*/
|
*/
|
||||||
async function runMigration(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
console.log('==============================================')
|
console.log('╔═══════════════════════════════════════════════════════════╗')
|
||||||
console.log('BIPUsers Table Migration: Add ERP Parameters')
|
console.log('║ Migration: Add ERP Parameters to BIPUsers Table ║')
|
||||||
console.log('==============================================\n')
|
console.log('╚═══════════════════════════════════════════════════════════╝')
|
||||||
|
|
||||||
const configManager = ConfigManager.getInstance()
|
|
||||||
await configManager.initialize()
|
|
||||||
|
|
||||||
const dbType = configManager.get('DB_TYPE', 'mysql').toLowerCase()
|
|
||||||
const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql'
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isSqlServer) {
|
const configManager = ConfigManager.getInstance()
|
||||||
await runSqlServerMigration(configManager)
|
await configManager.initialize()
|
||||||
} else {
|
|
||||||
|
const dbType = configManager.getDatabaseType()
|
||||||
|
console.log(`\nCurrent database type: ${dbType}`)
|
||||||
|
|
||||||
|
if (dbType === 'mysql') {
|
||||||
await runMySQLMigration(configManager)
|
await runMySQLMigration(configManager)
|
||||||
|
} else {
|
||||||
|
await runSqlServerMigration(configManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('==============================================')
|
console.log('\n✅ Migration completed successfully!\n')
|
||||||
console.log('Migration Summary:')
|
|
||||||
console.log('==============================================')
|
|
||||||
console.log(`Database Type: ${isSqlServer ? 'SQL Server' : 'MySQL'}`)
|
|
||||||
console.log('Columns Added/Verified:')
|
|
||||||
console.log(' - ERP_URL (VARCHAR/NVARCHAR 500)')
|
|
||||||
console.log(' - ERP_Username (VARCHAR/NVARCHAR 255)')
|
|
||||||
console.log(' - ERP_Password (VARCHAR/NVARCHAR 255)')
|
|
||||||
console.log('==============================================\n')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('\n❌ Migration failed with error:', error)
|
console.error('\n❌ Migration failed:', error instanceof Error ? error.message : error)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run migration
|
main()
|
||||||
runMigration().catch((error) => {
|
|
||||||
console.error('Unexpected error:', error)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* User ERP Configuration Service
|
* User ERP Configuration Service
|
||||||
*
|
*
|
||||||
* Manages ERP configuration (URL, username, password) stored in the BIPUsers table.
|
* Manages ERP credentials (username, password) stored in the BIPUsers table.
|
||||||
* Each user can have their own ERP credentials.
|
* Each user can have their own ERP credentials.
|
||||||
|
* ERP URL is fixed and stored in config.yaml.
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Get current user's ERP config
|
* - Get current user's ERP credentials
|
||||||
* - Update current user's ERP config
|
* - Update current user's ERP credentials
|
||||||
* - Get ERP config for any user (admin only)
|
* - Get ERP credentials for any user (admin only)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BIPUsersDAO } from './bip-users-dao'
|
import { BIPUsersDAO } from './bip-users-dao'
|
||||||
@@ -17,10 +18,9 @@ import { createLogger } from '../logger'
|
|||||||
const log = createLogger('UserErpConfigService')
|
const log = createLogger('UserErpConfigService')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ERP Configuration object
|
* ERP Credentials object (username and password only)
|
||||||
*/
|
*/
|
||||||
export interface ErpConfig {
|
export interface ErpCredentials {
|
||||||
url: string
|
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
@@ -48,9 +48,9 @@ export class UserErpConfigService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ERP configuration for the current authenticated user
|
* Get ERP configuration for the current authenticated user
|
||||||
* @returns ERP configuration or null if not found
|
* @returns ERP credentials or null if not found
|
||||||
*/
|
*/
|
||||||
async getCurrentUserErpConfig(): Promise<ErpConfig | null> {
|
async getCurrentUserErpConfig(): Promise<ErpCredentials | null> {
|
||||||
try {
|
try {
|
||||||
const sessionManager = SessionManager.getInstance()
|
const sessionManager = SessionManager.getInstance()
|
||||||
const currentUser = sessionManager.getUserInfo()
|
const currentUser = sessionManager.getUserInfo()
|
||||||
@@ -60,56 +60,55 @@ export class UserErpConfigService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Fetching ERP config for user', { username: currentUser.username })
|
log.info('Fetching ERP credentials for user', { username: currentUser.username })
|
||||||
const config = await this.dao.getUserErpConfig(currentUser.username)
|
const config = await this.dao.getUserErpCredentials(currentUser.username)
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
log.warn('No ERP config found for user', { username: currentUser.username })
|
log.warn('No ERP credentials found for user', { username: currentUser.username })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('ERP config retrieved successfully', {
|
log.info('ERP credentials retrieved successfully', {
|
||||||
username: currentUser.username,
|
username: currentUser.username,
|
||||||
hasUrl: !!config.url,
|
|
||||||
hasUsername: !!config.username,
|
hasUsername: !!config.username,
|
||||||
hasPassword: !!config.password
|
hasPassword: !!config.password
|
||||||
})
|
})
|
||||||
|
|
||||||
return config
|
return config
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error getting current user ERP config', { error })
|
log.error('Error getting current user ERP credentials', { error })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ERP configuration for a specific user (admin only)
|
* Get ERP credentials for a specific user (admin only)
|
||||||
* @param username - The username to get ERP config for
|
* @param username - The username to get ERP credentials for
|
||||||
* @returns ERP configuration or null if not found
|
* @returns ERP credentials or null if not found
|
||||||
*/
|
*/
|
||||||
async getUserErpConfig(username: string): Promise<ErpConfig | null> {
|
async getUserErpConfig(username: string): Promise<ErpCredentials | null> {
|
||||||
try {
|
try {
|
||||||
log.info('Fetching ERP config for user', { username })
|
log.info('Fetching ERP credentials for user', { username })
|
||||||
const config = await this.dao.getUserErpConfig(username)
|
const config = await this.dao.getUserErpCredentials(username)
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
log.warn('No ERP config found for user', { username })
|
log.warn('No ERP credentials found for user', { username })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error getting user ERP config', { error })
|
log.error('Error getting user ERP credentials', { error })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update ERP configuration for the current authenticated user
|
* Update ERP credentials for the current authenticated user
|
||||||
* @param config - ERP configuration to save
|
* @param credentials - ERP credentials to save
|
||||||
* @returns True if successful
|
* @returns True if successful
|
||||||
*/
|
*/
|
||||||
async updateCurrentUserErpConfig(config: ErpConfig): Promise<boolean> {
|
async updateCurrentUserErpConfig(credentials: ErpCredentials): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const sessionManager = SessionManager.getInstance()
|
const sessionManager = SessionManager.getInstance()
|
||||||
const currentUser = sessionManager.getUserInfo()
|
const currentUser = sessionManager.getUserInfo()
|
||||||
@@ -119,52 +118,50 @@ export class UserErpConfigService {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Updating ERP config for user', { username: currentUser.username })
|
log.info('Updating ERP credentials for user', { username: currentUser.username })
|
||||||
const success = await this.dao.updateUserErpConfig(
|
const success = await this.dao.updateUserErpCredentials(
|
||||||
currentUser.username,
|
currentUser.username,
|
||||||
config.url,
|
credentials.username,
|
||||||
config.username,
|
credentials.password
|
||||||
config.password
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
log.info('ERP config updated successfully', { username: currentUser.username })
|
log.info('ERP credentials updated successfully', { username: currentUser.username })
|
||||||
} else {
|
} else {
|
||||||
log.error('Failed to update ERP config', { username: currentUser.username })
|
log.error('Failed to update ERP credentials', { username: currentUser.username })
|
||||||
}
|
}
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error updating current user ERP config', { error })
|
log.error('Error updating current user ERP credentials', { error })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update ERP configuration for a specific user (admin only)
|
* Update ERP credentials for a specific user (admin only)
|
||||||
* @param username - The username to update ERP config for
|
* @param username - The username to update ERP credentials for
|
||||||
* @param config - ERP configuration to save
|
* @param credentials - ERP credentials to save
|
||||||
* @returns True if successful
|
* @returns True if successful
|
||||||
*/
|
*/
|
||||||
async updateUserErpConfig(username: string, config: ErpConfig): Promise<boolean> {
|
async updateUserErpConfig(username: string, credentials: ErpCredentials): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
log.info('Updating ERP config for user', { username })
|
log.info('Updating ERP credentials for user', { username })
|
||||||
const success = await this.dao.updateUserErpConfig(
|
const success = await this.dao.updateUserErpCredentials(
|
||||||
username,
|
username,
|
||||||
config.url,
|
credentials.username,
|
||||||
config.username,
|
credentials.password
|
||||||
config.password
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
log.info('ERP config updated successfully', { username })
|
log.info('ERP credentials updated successfully', { username })
|
||||||
} else {
|
} else {
|
||||||
log.error('Failed to update ERP config', { username })
|
log.error('Failed to update ERP credentials', { username })
|
||||||
}
|
}
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error updating user ERP config', { error })
|
log.error('Error updating user ERP credentials', { error })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/main/tools/config-path-debug.ts
Normal file
114
src/main/tools/config-path-debug.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Configuration Path Debug Tool
|
||||||
|
*
|
||||||
|
* Run this to see where config files will be stored in different modes
|
||||||
|
* Usage: npx tsx src/main/tools/config-path-debug.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
// Simulate different environments
|
||||||
|
const scenarios = [
|
||||||
|
{
|
||||||
|
name: 'Development Mode (开发环境)',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
APP_PACKAGED: 'false'
|
||||||
|
},
|
||||||
|
appData: 'C:\\Users\\test\\AppData\\Roaming\\erpauto',
|
||||||
|
projectRoot: 'D:\\Projects\\ERPAuto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Production - Portable (便携版)',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
APP_PACKAGED: 'true'
|
||||||
|
},
|
||||||
|
appData: 'C:\\Users\\test\\AppData\\Roaming\\erpauto',
|
||||||
|
projectRoot: 'D:\\Projects\\ERPAuto',
|
||||||
|
exeDir: 'D:\\PortableApps\\ERPAuto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Production - Installed (安装版)',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
APP_PACKAGED: 'true'
|
||||||
|
},
|
||||||
|
appData: 'C:\\Users\\test\\AppData\\Roaming\\erpauto',
|
||||||
|
projectRoot: 'D:\\Projects\\ERPAuto'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
console.log('╔════════════════════════════════════════════════════════════════╗')
|
||||||
|
console.log('║ ERPAuto Configuration Path Debug Tool ║')
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝\n')
|
||||||
|
|
||||||
|
for (const scenario of scenarios) {
|
||||||
|
console.log(`📋 ${scenario.name}`)
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
|
||||||
|
const isDev = scenario.env.NODE_ENV === 'development' || scenario.env.APP_PACKAGED === 'false'
|
||||||
|
|
||||||
|
let configPath: string
|
||||||
|
let backupPath: string
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境:项目根目录
|
||||||
|
configPath = path.join(scenario.projectRoot, 'config.yaml')
|
||||||
|
backupPath = path.join(scenario.projectRoot, 'config.yaml.backup')
|
||||||
|
} else {
|
||||||
|
// 生产环境(便携版和安装版):用户数据目录
|
||||||
|
configPath = path.join(scenario.appData, 'config.yaml')
|
||||||
|
backupPath = path.join(scenario.appData, 'config.yaml.backup')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` NODE_ENV: ${scenario.env.NODE_ENV}`)
|
||||||
|
console.log(` APP_PACKAGED: ${scenario.env.APP_PACKAGED}`)
|
||||||
|
console.log(` Is Development: ${isDev ? '✓ Yes' : '✗ No'}`)
|
||||||
|
if ('exeDir' in scenario) {
|
||||||
|
console.log(` EXE Directory: ${scenario.exeDir}`)
|
||||||
|
}
|
||||||
|
console.log(` → Config Path: ${configPath}`)
|
||||||
|
console.log(` → Backup Path: ${backupPath}`)
|
||||||
|
console.log('')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('╔════════════════════════════════════════════════════════════════╗')
|
||||||
|
console.log('║ Configuration Strategy (配置策略): ║')
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝')
|
||||||
|
console.log(`
|
||||||
|
┌─────────────┬──────────────────────────────────────────────────────────┐
|
||||||
|
│ 环境 │ 配置文件位置 │
|
||||||
|
├─────────────┼──────────────────────────────────────────────────────────┤
|
||||||
|
│ 开发环境 │ 项目根目录\\config.yaml │
|
||||||
|
│ │ 方便编辑和调试,配置随代码版本管理 │
|
||||||
|
├─────────────┼──────────────────────────────────────────────────────────┤
|
||||||
|
│ 生产环境 │ %APPDATA%\\erpauto\\config.yaml │
|
||||||
|
│ (便携版/ │ 符合 Windows 规范,应用升级时配置保留,安全 │
|
||||||
|
│ 安装版) │ │
|
||||||
|
└─────────────┴──────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
💡 优势:
|
||||||
|
✓ 开发时配置在项目根目录,方便版本控制和团队协作
|
||||||
|
✓ 生产环境配置在用户数据目录,应用升级不会丢失配置
|
||||||
|
✓ 配置不暴露在应用目录,更安全
|
||||||
|
✓ 多用户环境下,每个用户有独立的配置
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log('╔════════════════════════════════════════════════════════════════╗')
|
||||||
|
console.log('║ Recommended Directory Structure: ║')
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝')
|
||||||
|
console.log(`
|
||||||
|
【开发环境】
|
||||||
|
D:\\Projects\\ERPAuto\\
|
||||||
|
├── src\\
|
||||||
|
├── package.json
|
||||||
|
├── config.yaml # 开发配置(可加入 .gitignore)
|
||||||
|
├── config.yaml.backup # 自动备份
|
||||||
|
└── config.template.yaml # 配置模板(提交到版本控制)
|
||||||
|
|
||||||
|
【生产环境 - 便携版/安装版】
|
||||||
|
C:\\Users\\<user>\\AppData\\Roaming\\erpauto\\
|
||||||
|
├── config.yaml # 用户配置
|
||||||
|
└── config.yaml.backup # 自动备份
|
||||||
|
`)
|
||||||
155
src/main/types/config.schema.ts
Normal file
155
src/main/types/config.schema.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* Configuration Schema Definitions
|
||||||
|
*
|
||||||
|
* Zod schemas for runtime validation of application configuration
|
||||||
|
*
|
||||||
|
* Note: ERP configuration is stored in database (dbo_BIPUsers table)
|
||||||
|
* and managed per-user, not in this config file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库类型枚举
|
||||||
|
*/
|
||||||
|
export const databaseTypeSchema = z.enum(['mysql', 'sqlserver'])
|
||||||
|
export type DatabaseType = z.infer<typeof databaseTypeSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证匹配模式枚举
|
||||||
|
*/
|
||||||
|
export const matchModeSchema = z.enum(['substring', 'exact'])
|
||||||
|
export type MatchMode = z.infer<typeof matchModeSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证数据源枚举
|
||||||
|
*/
|
||||||
|
export const validationDataSourceSchema = z.enum([
|
||||||
|
'database_full',
|
||||||
|
'database_filtered',
|
||||||
|
'excel_existing',
|
||||||
|
'excel_full'
|
||||||
|
])
|
||||||
|
export type ValidationDataSource = z.infer<typeof validationDataSourceSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 配置 Schema
|
||||||
|
*/
|
||||||
|
export const mysqlConfigSchema = z.object({
|
||||||
|
host: z.string().min(1, 'MySQL host is required'),
|
||||||
|
port: z.number().int().min(1).max(65535).default(3306),
|
||||||
|
database: z.string().min(1, 'MySQL database is required'),
|
||||||
|
username: z.string().min(1, 'MySQL username is required'),
|
||||||
|
password: z.string(),
|
||||||
|
charset: z.string().default('utf8mb4')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL Server 配置 Schema
|
||||||
|
*/
|
||||||
|
export const sqlServerConfigSchema = z.object({
|
||||||
|
server: z.string().min(1, 'SQL Server is required'),
|
||||||
|
port: z.number().int().min(1).max(65535).default(1433),
|
||||||
|
database: z.string().min(1, 'SQL Server database is required'),
|
||||||
|
username: z.string().min(1, 'SQL Server username is required'),
|
||||||
|
password: z.string(),
|
||||||
|
driver: z.string().default('ODBC Driver 18 for SQL Server'),
|
||||||
|
trustServerCertificate: z.boolean().default(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库配置(包含两种数据库的完整配置)
|
||||||
|
*/
|
||||||
|
export const databaseConfigSchema = z.object({
|
||||||
|
activeType: databaseTypeSchema.default('mysql'),
|
||||||
|
mysql: mysqlConfigSchema,
|
||||||
|
sqlserver: sqlServerConfigSchema
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径配置 Schema
|
||||||
|
*/
|
||||||
|
export const pathsConfigSchema = z.object({
|
||||||
|
dataDir: z.string().min(1, 'Data directory is required'),
|
||||||
|
defaultOutput: z.string().default('离散备料计划维护_合并.xlsx'),
|
||||||
|
validationOutput: z.string().default('物料状态校验结果.xlsx')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据提取配置 Schema
|
||||||
|
*/
|
||||||
|
export const extractionConfigSchema = z.object({
|
||||||
|
batchSize: z.number().int().min(1).max(1000).default(100),
|
||||||
|
verbose: z.boolean().default(true),
|
||||||
|
autoConvert: z.boolean().default(true),
|
||||||
|
mergeBatches: z.boolean().default(true),
|
||||||
|
enableDbPersistence: z.boolean().default(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物料校验配置 Schema
|
||||||
|
*/
|
||||||
|
export const validationConfigSchema = z.object({
|
||||||
|
dataSource: validationDataSourceSchema.default('database_full'),
|
||||||
|
batchSize: z.number().int().min(1).max(10000).default(2000),
|
||||||
|
matchMode: matchModeSchema.default('substring'),
|
||||||
|
enableCrud: z.boolean().default(false),
|
||||||
|
defaultManager: z.string().default('')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单号解析配置 Schema
|
||||||
|
*/
|
||||||
|
export const orderResolutionSchema = z.object({
|
||||||
|
tableName: z.string(),
|
||||||
|
productionIdField: z.string(),
|
||||||
|
orderNumberField: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ERP 系统配置 Schema(固定基础设施)
|
||||||
|
*/
|
||||||
|
export const erpSystemConfigSchema = z.object({
|
||||||
|
url: z.string().url('ERP URL must be a valid URL')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整应用配置 Schema
|
||||||
|
*/
|
||||||
|
export const fullConfigSchema = z.object({
|
||||||
|
erp: erpSystemConfigSchema,
|
||||||
|
database: databaseConfigSchema,
|
||||||
|
paths: pathsConfigSchema,
|
||||||
|
extraction: extractionConfigSchema,
|
||||||
|
validation: validationConfigSchema,
|
||||||
|
orderResolution: orderResolutionSchema
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型导出
|
||||||
|
*/
|
||||||
|
export type FullConfig = z.infer<typeof fullConfigSchema>
|
||||||
|
export type DatabaseConfig = z.infer<typeof databaseConfigSchema>
|
||||||
|
export type MySqlConfig = z.infer<typeof mysqlConfigSchema>
|
||||||
|
export type SqlServerConfig = z.infer<typeof sqlServerConfigSchema>
|
||||||
|
export type ErpSystemConfig = z.infer<typeof erpSystemConfigSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证并解析配置
|
||||||
|
*/
|
||||||
|
export function validateConfig(input: unknown): {
|
||||||
|
success: boolean
|
||||||
|
data?: FullConfig
|
||||||
|
error?: string
|
||||||
|
} {
|
||||||
|
const result = fullConfigSchema.safeParse(input)
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, data: result.data }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: result.error.issues
|
||||||
|
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
||||||
|
.join('; ')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
* Settings types and interfaces
|
* Settings types and interfaces
|
||||||
*
|
*
|
||||||
* Defines configuration structures for ERPAuto settings management
|
* Defines configuration structures for ERPAuto settings management
|
||||||
|
*
|
||||||
|
* Note: ERP configuration is stored in database (dbo_BIPUsers table)
|
||||||
|
* and managed per-user, not in settings.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,24 +31,6 @@ export type ValidationDataSource =
|
|||||||
| 'excel_existing'
|
| 'excel_existing'
|
||||||
| 'excel_full'
|
| 'excel_full'
|
||||||
|
|
||||||
/**
|
|
||||||
* ERP configuration
|
|
||||||
*/
|
|
||||||
export interface ErpConfig {
|
|
||||||
/** ERP system URL */
|
|
||||||
url: string
|
|
||||||
/** ERP username */
|
|
||||||
username: string
|
|
||||||
/** ERP password */
|
|
||||||
password: string
|
|
||||||
/** Headless browser mode */
|
|
||||||
headless: boolean
|
|
||||||
/** Ignore HTTPS certificate errors */
|
|
||||||
ignoreHttpsErrors: boolean
|
|
||||||
/** Auto close browser after operations */
|
|
||||||
autoCloseBrowser: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database configuration
|
* Database configuration
|
||||||
*/
|
*/
|
||||||
@@ -112,10 +97,9 @@ export interface ValidationConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete settings data structure
|
* Complete settings data structure
|
||||||
|
* Note: No ERP configuration - it's stored in database per user
|
||||||
*/
|
*/
|
||||||
export interface SettingsData {
|
export interface SettingsData {
|
||||||
/** ERP configuration */
|
|
||||||
erp: ErpConfig
|
|
||||||
/** Database configuration */
|
/** Database configuration */
|
||||||
database: DatabaseConfig
|
database: DatabaseConfig
|
||||||
/** Path configuration */
|
/** Path configuration */
|
||||||
@@ -158,8 +142,6 @@ export interface SettingsAPI {
|
|||||||
saveSettings: (settings: SettingsData) => Promise<SaveSettingsResult>
|
saveSettings: (settings: SettingsData) => Promise<SaveSettingsResult>
|
||||||
/** Reset to defaults (Admin only) */
|
/** Reset to defaults (Admin only) */
|
||||||
resetDefaults: () => Promise<SaveSettingsResult>
|
resetDefaults: () => Promise<SaveSettingsResult>
|
||||||
/** Test ERP connection */
|
|
||||||
testErpConnection: () => Promise<ConnectionTestResult>
|
|
||||||
/** Test database connection */
|
/** Test database connection */
|
||||||
testDbConnection: () => Promise<ConnectionTestResult>
|
testDbConnection: () => Promise<ConnectionTestResult>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,12 +63,17 @@ export function useExtractor() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
|
const { data } = response
|
||||||
addLog(
|
addLog(
|
||||||
'success',
|
'success',
|
||||||
`提取完成:下载 ${response.data.downloadedFiles.length} 个文件,共 ${response.data.recordCount} 条记录`
|
`提取完成:下载 ${data.downloadedFiles.length} 个文件,共 ${data.recordCount} 条记录`
|
||||||
)
|
)
|
||||||
if (response.data.errors.length > 0) {
|
if (data.errors.length > 0) {
|
||||||
addLog('warning', `存在 ${response.data.errors.length} 个错误`)
|
addLog('warning', `存在 ${data.errors.length} 个错误`)
|
||||||
|
// Log each error detail for debugging
|
||||||
|
data.errors.forEach((err, index) => {
|
||||||
|
addLog('error', `错误 ${index + 1}/${data.errors.length}: ${err}`)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setError(response.error || '提取失败')
|
setError(response.error || '提取失败')
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Settings as SettingsIcon, Save } from 'lucide-react'
|
import { Settings as SettingsIcon, Save, User, Key } from 'lucide-react'
|
||||||
|
|
||||||
interface Settings {
|
interface ErpCredentials {
|
||||||
erp: {
|
username: string
|
||||||
url?: string
|
password: string
|
||||||
username?: string
|
|
||||||
password?: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsPage: React.FC = () => {
|
const SettingsPage: React.FC = () => {
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [credentials, setCredentials] = useState<ErpCredentials>({
|
||||||
erp: {
|
username: '',
|
||||||
url: '',
|
password: ''
|
||||||
username: '',
|
|
||||||
password: ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [isModified, setIsModified] = useState(false)
|
const [isModified, setIsModified] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [message, setMessage] = useState<{
|
const [message, setMessage] = useState<{
|
||||||
@@ -26,17 +19,25 @@ const SettingsPage: React.FC = () => {
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings()
|
loadCredentials()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadSettings = async () => {
|
const loadCredentials = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
// ERP credentials are loaded from database (current user's config)
|
||||||
const config = await window.electron.settings.getSettings()
|
const config = await window.electron.settings.getSettings()
|
||||||
setSettings(config as unknown as Settings)
|
|
||||||
|
// Extract ERP credentials from the config
|
||||||
|
if (config && (config as any).erp) {
|
||||||
|
setCredentials({
|
||||||
|
username: (config as any).erp.username || '',
|
||||||
|
password: (config as any).erp.password || ''
|
||||||
|
})
|
||||||
|
}
|
||||||
setIsModified(false)
|
setIsModified(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage('error', '加载设置失败')
|
showMessage('error', '加载 ERP 配置失败')
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@@ -47,38 +48,24 @@ const SettingsPage: React.FC = () => {
|
|||||||
setTimeout(() => setMessage(null), 3000)
|
setTimeout(() => setMessage(null), 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSettings = (category: string, key: string, value: any) => {
|
const handleSaveCredentials = async () => {
|
||||||
setSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[category]: {
|
|
||||||
...(prev as any)[category],
|
|
||||||
[key]: value
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
setIsModified(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveSettings = async () => {
|
|
||||||
try {
|
try {
|
||||||
// Only send UI-supported fields (double safety)
|
// Save ERP credentials to database (current user's config)
|
||||||
const partialSettings = {
|
const result = await window.electron.settings.saveSettings({
|
||||||
erp: {
|
erp: {
|
||||||
url: settings.erp?.url,
|
username: credentials.username,
|
||||||
username: settings.erp?.username,
|
password: credentials.password
|
||||||
password: settings.erp?.password
|
|
||||||
}
|
}
|
||||||
}
|
} as any)
|
||||||
|
|
||||||
const result = await window.electron.settings.saveSettings(partialSettings as any)
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setIsModified(false)
|
setIsModified(false)
|
||||||
showMessage('success', '设置保存成功')
|
showMessage('success', 'ERP 账号密码保存成功')
|
||||||
} else {
|
} else {
|
||||||
showMessage('error', result.error || '保存失败')
|
showMessage('error', result.error || '保存失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage('error', '保存设置时发生错误')
|
showMessage('error', '保存配置时发生错误')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +84,11 @@ const SettingsPage: React.FC = () => {
|
|||||||
<div className="flex justify-center animate-in fade-in slide-in-from-bottom-4 duration-500 mt-6">
|
<div className="flex justify-center animate-in fade-in slide-in-from-bottom-4 duration-500 mt-6">
|
||||||
{message && (
|
{message && (
|
||||||
<div
|
<div
|
||||||
className={`fixed top-5 left-1/2 -translate-x-1/2 px-6 py-3 rounded-lg shadow-lg z-[10000] text-sm font-medium transition-all ${message.type === 'success' ? 'bg-emerald-50 text-emerald-600 border border-emerald-200' : 'bg-red-50 text-red-600 border border-red-200'}`}
|
className={`fixed top-5 left-1/2 -translate-x-1/2 px-6 py-3 rounded-lg shadow-lg z-[10000] text-sm font-medium transition-all ${
|
||||||
|
message.type === 'success'
|
||||||
|
? 'bg-emerald-50 text-emerald-600 border border-emerald-200'
|
||||||
|
: 'bg-red-50 text-red-600 border border-red-200'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
@@ -107,58 +98,56 @@ const SettingsPage: React.FC = () => {
|
|||||||
<div className="border-b border-slate-100 bg-slate-50 px-6 py-5">
|
<div className="border-b border-slate-100 bg-slate-50 px-6 py-5">
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-800">
|
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-800">
|
||||||
<SettingsIcon size={20} className="text-slate-600" />
|
<SettingsIcon size={20} className="text-slate-600" />
|
||||||
环境与认证配置
|
ERP 账号配置
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-slate-500 mt-1">
|
<p className="text-sm text-slate-500 mt-1">
|
||||||
设置 ERP 系统的入口地址及自动化登录凭证。此配置将自动同步至本地 <code>.env</code> 文件。
|
设置 ERP 系统的登录账号和密码。此配置将存储在数据库中,按用户管理。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-6 bg-white flex flex-col items-center">
|
<div className="p-6 space-y-6 bg-white">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md mx-auto">
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">
|
<label className="block text-sm font-medium text-slate-700 mb-1.5">
|
||||||
ERP 基础访问地址 (URL)
|
<span className="flex items-center gap-2">
|
||||||
|
<User size={16} className="text-slate-500" />
|
||||||
|
ERP 登录账号
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="text"
|
||||||
placeholder="https://erp.example.com"
|
placeholder="输入 ERP 账号"
|
||||||
|
value={credentials.username}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCredentials((prev) => ({ ...prev, username: e.target.value }))
|
||||||
|
setIsModified(true)
|
||||||
|
}}
|
||||||
className="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
|
className="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
|
||||||
value={settings.erp?.url || ''}
|
|
||||||
onChange={(e) => updateSettings('erp', 'url', e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full max-w-md grid grid-cols-2 gap-5">
|
<div className="w-full max-w-md mx-auto">
|
||||||
<div>
|
<label className="block text-sm font-medium text-slate-700 mb-1.5">
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">
|
<span className="flex items-center gap-2">
|
||||||
登录账号 (Username)
|
<Key size={16} className="text-slate-500" />
|
||||||
</label>
|
ERP 登录密码
|
||||||
<input
|
</span>
|
||||||
type="text"
|
</label>
|
||||||
placeholder="输入 ERP 账号"
|
<input
|
||||||
className="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
|
type="password"
|
||||||
value={settings.erp?.username || ''}
|
placeholder="••••••••"
|
||||||
onChange={(e) => updateSettings('erp', 'username', e.target.value)}
|
value={credentials.password}
|
||||||
/>
|
onChange={(e) => {
|
||||||
</div>
|
setCredentials((prev) => ({ ...prev, password: e.target.value }))
|
||||||
<div>
|
setIsModified(true)
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">
|
}}
|
||||||
登录密码 (Password)
|
className="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
|
||||||
</label>
|
/>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="••••••••"
|
|
||||||
className="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
|
|
||||||
value={settings.erp?.password || ''}
|
|
||||||
onChange={(e) => updateSettings('erp', 'password', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full max-w-md pt-6 mt-2 border-t border-slate-100 flex justify-center">
|
<div className="w-full max-w-md mx-auto pt-6 mt-2 border-t border-slate-100 flex justify-center">
|
||||||
<button
|
<button
|
||||||
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:hover:bg-blue-600 text-white px-8 py-2.5 rounded-lg flex items-center gap-2 font-medium shadow-sm transition-colors"
|
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:hover:bg-blue-600 text-white px-8 py-2.5 rounded-lg flex items-center gap-2 font-medium shadow-sm transition-colors"
|
||||||
onClick={handleSaveSettings}
|
onClick={handleSaveCredentials}
|
||||||
disabled={!isModified}
|
disabled={!isModified}
|
||||||
>
|
>
|
||||||
<Save size={18} />
|
<Save size={18} />
|
||||||
|
|||||||
Reference in New Issue
Block a user