diff --git a/.env.example b/.env.example deleted file mode 100644 index 6f8f8d3..0000000 --- a/.env.example +++ /dev/null @@ -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 \ No newline at end of file diff --git a/config.template.yaml b/config.template.yaml new file mode 100644 index 0000000..2030604 --- /dev/null +++ b/config.template.yaml @@ -0,0 +1,54 @@ +# ================================ +# ERPAuto 配置模板 +# ================================ +# 部署说明: +# 1. 复制此文件为 config.yaml +# 2. 根据实际环境修改配置值 +# 3. 设置 database.activeType 为 mysql 或 sqlserver +# ================================ +# 注意:ERP 认证信息存储在数据库 (dbo_BIPUsers) 中,按用户管理 +# ================================ + +database: + activeType: mysql + + mysql: + host: + port: 3306 + database: + username: + password: + charset: utf8mb4 + + sqlserver: + server: + port: 1433 + database: + username: + 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: '' diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..e9b21e9 --- /dev/null +++ b/config.yaml @@ -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: '生产订单号' diff --git a/docs/CONFIG_FILE_LOCATION.md b/docs/CONFIG_FILE_LOCATION.md new file mode 100644 index 0000000..363b451 --- /dev/null +++ b/docs/CONFIG_FILE_LOCATION.md @@ -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 | • 应用升级时配置保留
• 符合 Windows 规范
• 多用户隔离
• 配置不暴露 | • 路径较深,不易访问 | +| **应用同目录** ✗ | .exe 同目录 | • 易于访问和编辑 | • 应用升级时配置可能丢失
• 需要写权限
• 配置暴露在应用目录
• 多用户共享配置 | + +**我们的选择**:生产环境统一使用用户数据目录,确保: + +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+ | 开发:项目目录
生产: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) diff --git a/electron-builder.yml b/electron-builder.yml index 4bbe49d..441eff3 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -5,16 +5,25 @@ directories: files: - '!**/.vscode/*' - '!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}' - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' - 'package.json' + # Include config.template.yaml in the build for reference + - 'config.template.yaml' asarUnpack: - resources/** - '**/node_modules/playwright/**' win: 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: artifactName: ${name}-${version}-setup.${ext} shortcutName: ${productName} diff --git a/package-lock.json b/package-lock.json index b10a7de..456a348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,11 @@ "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@tailwindcss/vite": "^4.2.1", + "@types/js-yaml": "^4.0.9", "chromium-bidi": "^15.0.0", "date-fns": "^4.1.0", - "dotenv": "^17.3.1", "exceljs": "^4.4.0", + "js-yaml": "^4.1.1", "lucide-react": "^0.575.0", "mssql": "^12.2.0", "mysql2": "^3.18.2", @@ -106,6 +107,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@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", "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -358,6 +361,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1126,7 +1130,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1148,7 +1151,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1165,7 +1167,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1180,7 +1181,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -3015,6 +3015,12 @@ "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "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": { "version": "7.0.15", "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", "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3077,6 +3084,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3185,6 +3193,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -3618,6 +3627,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3650,6 +3660,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3978,7 +3989,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -4397,6 +4407,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5123,8 +5134,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -5490,6 +5500,7 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -5579,18 +5590,6 @@ "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": { "version": "11.0.7", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", @@ -5716,6 +5715,7 @@ "integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", @@ -5904,7 +5904,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -5925,7 +5924,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -6254,6 +6252,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6314,6 +6313,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8266,7 +8266,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10091,6 +10090,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10173,6 +10173,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10196,7 +10197,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -10214,7 +10214,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -10235,6 +10234,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10367,6 +10367,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11654,7 +11655,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -12172,6 +12172,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12406,6 +12407,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12952,6 +12954,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -13160,6 +13163,7 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -13397,6 +13401,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index b2389a7..6ade0d7 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,11 @@ "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@tailwindcss/vite": "^4.2.1", + "@types/js-yaml": "^4.0.9", "chromium-bidi": "^15.0.0", "date-fns": "^4.1.0", - "dotenv": "^17.3.1", "exceljs": "^4.4.0", + "js-yaml": "^4.1.1", "lucide-react": "^0.575.0", "mssql": "^12.2.0", "mysql2": "^3.18.2", diff --git a/scripts/migrate-env-to-yaml.ts b/scripts/migrate-env-to-yaml.ts new file mode 100644 index 0000000..6f5a09c --- /dev/null +++ b/scripts/migrate-env-to-yaml.ts @@ -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() diff --git a/src/main/.env.backup b/src/main/.env.backup deleted file mode 100644 index 052a4e9..0000000 --- a/src/main/.env.backup +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index 653dfd0..ceddea8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,14 +3,12 @@ import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' import { registerIpcHandlers } from './ipc' -import * as dotenv from 'dotenv' +import { ConfigManager } from './services/config/config-manager' 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 __dirname = dirname(__filename) -dotenv.config({ path: resolve(__dirname, '../../.env') }) function createWindow(): void { // Create the browser window. @@ -47,7 +45,17 @@ function createWindow(): void { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // 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 electronApp.setAppUserModelId('com.electron') @@ -58,7 +66,7 @@ app.whenReady().then(() => { optimizer.watchWindowShortcuts(window) }) - // Register IPC handlers + // Register IPC handlers (after ConfigManager is initialized) registerIpcHandlers() // IPC test diff --git a/src/main/ipc/cleaner-handler.ts b/src/main/ipc/cleaner-handler.ts index acad705..b0bb241 100644 --- a/src/main/ipc/cleaner-handler.ts +++ b/src/main/ipc/cleaner-handler.ts @@ -4,6 +4,7 @@ import { CleanerService } from '../services/erp/cleaner' import { OrderNumberResolver } from '../services/erp/order-resolver' import { MySqlService } from '../services/database/mysql' import { SqlServerService } from '../services/database/sql-server' +import { ConfigManager } from '../services/config/config-manager' import { ResultExporter } from '../services/excel/result-exporter' import { CleanerReportGenerator } from '../services/report/cleaner-report-generator' import { SessionManager } from '../services/user/session-manager' @@ -47,29 +48,33 @@ function sendProgress( } async function getDatabaseService(): Promise { - 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({ - server: process.env.DB_SERVER || 'localhost', - port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), - user: process.env.DB_USERNAME || 'sa', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '', + server: dbConfig.server, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, options: { encrypt: false, - trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + trustServerCertificate: dbConfig.trustServerCertificate } }) await sqlServerService.connect() return sqlServerService } else { + const dbConfig = config.database.mysql const mysqlService = new MySqlService({ - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database }) await mysqlService.connect() return mysqlService @@ -78,23 +83,35 @@ async function getDatabaseService(): Promise { /** * 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<{ url: string username: string password: string }> { - const erpConfigService = UserErpConfigService.getInstance() - const config = await erpConfigService.getCurrentUserErpConfig() + // Get ERP URL from config.yaml (fixed for all users) + 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( - 'ERP 配置不完整。请在设置中配置 ERP URL、用户名和密码', + 'ERP 配置不完整。请在设置中配置 ERP 用户名和密码', 'VAL_MISSING_REQUIRED' ) } - return config + return { + url: erpUrl, + username: userConfig.username, + password: userConfig.password + } } export function registerCleanerHandlers(): void { diff --git a/src/main/ipc/extractor-handler.ts b/src/main/ipc/extractor-handler.ts index fedc322..faf281f 100644 --- a/src/main/ipc/extractor-handler.ts +++ b/src/main/ipc/extractor-handler.ts @@ -8,6 +8,7 @@ import { withErrorHandling, type IpcResult } from './index' import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' import type { ExtractorInput, ExtractorResult, ExtractionProgress } from '../types/extractor.types' import { UserErpConfigService } from '../services/user/user-erp-config-service' +import { ConfigManager } from '../services/config/config-manager' const log = createLogger('ExtractorHandler') @@ -39,23 +40,35 @@ function sendLog(windowId: number, level: string, message: string): void { /** * 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<{ url: string username: string password: string }> { - const erpConfigService = UserErpConfigService.getInstance() - const config = await erpConfigService.getCurrentUserErpConfig() + // Get ERP URL from config.yaml (fixed for all users) + 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( - 'ERP 配置不完整。请在设置中配置 ERP URL、用户名和密码', + 'ERP 配置不完整。请在设置中配置 ERP 用户名和密码', '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 }) + // 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 } finally { // Clean up: close browser diff --git a/src/main/ipc/settings-handler.ts b/src/main/ipc/settings-handler.ts index 32482fc..abd7402 100644 --- a/src/main/ipc/settings-handler.ts +++ b/src/main/ipc/settings-handler.ts @@ -2,62 +2,33 @@ * Settings IPC Handler * * Provides IPC handlers for settings management: - * - Get/set settings - * - Reset to defaults - * - Test ERP connection + * - Get/set ERP credentials (stored in database per user) + * - Reset to defaults (Admin only) * - 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 { ConfigManager } from '../services/config/config-manager' import { SessionManager } from '../services/user/session-manager' import { UserErpConfigService } from '../services/user/user-erp-config-service' -import { ErpAuthService } from '../services/erp/erp-auth' import { MySqlService } from '../services/database/mysql' import { SqlServerService } from '../services/database/sql-server' import { createLogger } from '../services/logger' -import type { - SettingsData, - UserType, - ConnectionTestResult, - SaveSettingsResult -} from '../types/settings.types' +import type { UserType, ConnectionTestResult, SaveSettingsResult } from '../types/settings.types' 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 */ export function registerSettingsHandlers(): void { const configManager = ConfigManager.getInstance() const sessionManager = SessionManager.getInstance() + const erpConfigService = UserErpConfigService.getInstance() /** * 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 => { - const userType = (sessionManager.getUserType() as UserType) || 'Guest' - log.debug('Getting settings', { userType }) + ipcMain.handle('settings:getSettings', async (): Promise => { + try { + // Get ERP credentials from database for current user + const userErpConfig = await erpConfigService.getCurrentUserErpConfig() - // Get base settings from .env - const settings = configManager.getAllSettings() - - // Override ERP config with user-specific config from database - 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 + return { + erp: { + username: userErpConfig?.username || '', + password: userErpConfig?.password || '' + } } + } 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( 'settings:saveSettings', - async (_event, settings: Partial): Promise => { + async (_event, settings: any): Promise => { try { - log.info('Saving settings', { - sections: Object.keys(settings) - }) + log.info('Saving ERP credentials') - // Step 1: Save ERP configuration to database (if provided) if (settings.erp) { - const erpConfigService = UserErpConfigService.getInstance() - const sessionManager = SessionManager.getInstance() + // Update ERP credentials in database for current user const currentUser = sessionManager.getUserInfo() - if (!currentUser) { - log.warn('No authenticated user found') - return { - success: false, - error: '未找到认证用户,无法保存 ERP 配置' - } + return { success: false, error: '未找到当前用户' } } - // Only save if ERP fields are provided - const hasErpFields = - settings.erp.url !== undefined || - settings.erp.username !== undefined || - 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 配置到数据库失败' - } - } + // Ensure undefined values are converted to empty strings + const erpCredentials = { + username: settings.erp.username || '', + password: settings.erp.password || '' } + + 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 = { ...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 } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Error saving settings', { error: message }) - return { - success: false, - error: `保存设置失败:${message}` - } + log.error('Error saving ERP credentials', { error: message }) + return { success: false, error: `保存配置失败:${message}` } } } ) @@ -202,8 +105,7 @@ export function registerSettingsHandlers(): void { } log.info('Resetting settings to defaults') - configManager.resetToDefaults() - const success = await configManager.save() + const success = await configManager.resetToDefaults() if (success) { log.info('Settings reset to defaults successfully') return { success: true } @@ -218,76 +120,19 @@ export function registerSettingsHandlers(): void { } }) - /** - * Test ERP connection - */ - ipcMain.handle('settings:testErpConnection', async (): Promise => { - 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 */ ipcMain.handle('settings:testDbConnection', async (): Promise => { try { log.info('Testing database connection') - const settings = configManager.getAllSettings() - const dbConfig = settings.database + const config = configManager.getConfig() + const dbType = config.database.activeType - if (dbConfig.dbType === 'mysql') { + if (dbType === 'mysql') { // 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') return { success: false, @@ -296,8 +141,8 @@ export function registerSettingsHandlers(): void { } const mysqlService = new MySqlService({ - host: dbConfig.mysqlHost, - port: dbConfig.mysqlPort, + host: dbConfig.host, + port: dbConfig.port, user: dbConfig.username, password: dbConfig.password, database: dbConfig.database @@ -321,6 +166,7 @@ export function registerSettingsHandlers(): void { } } else { // Test SQL Server connection + const dbConfig = config.database.sqlserver if (!dbConfig.server || !dbConfig.database || !dbConfig.username) { log.warn('SQL Server connection test failed - missing configuration') return { @@ -331,12 +177,12 @@ export function registerSettingsHandlers(): void { const sqlServerService = new SqlServerService({ server: dbConfig.server, - port: 1433, // Default SQL Server port + port: dbConfig.port, user: dbConfig.username, password: dbConfig.password, database: dbConfig.database, options: { - trustServerCertificate: true + trustServerCertificate: dbConfig.trustServerCertificate } }) diff --git a/src/main/ipc/user-erp-config-handler.ts b/src/main/ipc/user-erp-config-handler.ts index e5df880..b647e32 100644 --- a/src/main/ipc/user-erp-config-handler.ts +++ b/src/main/ipc/user-erp-config-handler.ts @@ -2,34 +2,38 @@ * IPC handlers for User ERP Configuration * * Provides APIs for the renderer process to: - * - Get current user's ERP configuration - * - Update current user's ERP configuration + * - Get current user's ERP credentials + * - Update current user's ERP credentials * - Test ERP connection with provided credentials */ 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 { ConfigManager } from '../services/config/config-manager' import { createLogger } from '../services/logger' import type { UserInfo } from '../types/user.types' const log = createLogger('UserErpConfigHandler') /** - * ERP Configuration request + * ERP Credentials request (username and password only, URL is from config.yaml) */ -export interface ErpConfigRequest { - url: string +export interface ErpCredentialsRequest { username: string password: string } /** - * ERP Configuration response + * ERP Configuration response (includes URL from config.yaml) */ export interface ErpConfigResponse { success: boolean - config?: ErpConfigRequest + config?: { + url: string + username: string + password: string + } error?: string } @@ -48,31 +52,36 @@ export function registerUserErpConfigHandlers(): void { const erpConfigService = UserErpConfigService.getInstance() /** - * Get current user's ERP configuration + * Get current user's ERP credentials */ ipcMain.handle('user-erp-config:getCurrent', async (): Promise => { try { - log.info('Fetching current user ERP config') - const config = await erpConfigService.getCurrentUserErpConfig() + log.info('Fetching current user ERP credentials') + const credentials = await erpConfigService.getCurrentUserErpConfig() - if (!config) { + if (!credentials) { return { 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 { success: true, config: { - url: config.url, - username: config.username, - password: config.password + url: erpUrl, + username: credentials.username, + password: credentials.password } } } catch (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 { success: false, 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( 'user-erp-config:update', - async (_event, config: ErpConfigRequest): Promise => { + async (_event, credentials: ErpCredentialsRequest): Promise => { try { - log.info('Updating current user ERP config', { - url: config.url, - username: config.username + log.info('Updating current user ERP credentials', { + username: credentials.username }) - const success = await erpConfigService.updateCurrentUserErpConfig(config) + const success = await erpConfigService.updateCurrentUserErpConfig(credentials) 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 { success: true, - config + config: { + url: erpUrl, + username: credentials.username, + password: credentials.password + } } } else { - log.error('Failed to update ERP config') + log.error('Failed to update ERP credentials') return { success: false, error: '更新 ERP 配置失败' @@ -109,7 +126,7 @@ export function registerUserErpConfigHandlers(): void { } } catch (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 { success: false, error: `更新 ERP 配置失败:${message}` @@ -123,21 +140,26 @@ export function registerUserErpConfigHandlers(): void { */ ipcMain.handle( 'user-erp-config:testConnection', - async (_event, config: ErpConfigRequest): Promise => { + async (_event, credentials: ErpCredentialsRequest): Promise => { 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 { 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({ - url: config.url, - username: config.username, - password: config.password, + url: erpUrl, + username: credentials.username, + password: credentials.password, headless: true }) diff --git a/src/main/services/config/config-manager.ts b/src/main/services/config/config-manager.ts index 41023d0..25f6585 100644 --- a/src/main/services/config/config-manager.ts +++ b/src/main/services/config/config-manager.ts @@ -1,21 +1,33 @@ /** - * Configuration Manager + * Configuration Manager (YAML Version) * - * Manages application configuration stored in .env file - * Provides methods for reading, writing, and saving configuration values + * Manages application configuration using YAML format + * 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 path from 'path' import { fileURLToPath } from 'url' import { dirname } from 'path' +import { app } from 'electron' +import yaml from 'js-yaml' +import { z } from 'zod' import { createLogger } from '../logger' -import type { - SettingsData, - DatabaseType, - MatchMode, - ValidationDataSource -} from '../../types/settings.types' +import { + fullConfigSchema, + type FullConfig, + type DatabaseType, + type MySqlConfig, + type SqlServerConfig +} from '../../types/config.schema' const log = createLogger('ConfigManager') @@ -23,30 +35,36 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) /** - * Default settings values + * 默认配置 */ -const DEFAULT_SETTINGS: SettingsData = { +const DEFAULT_CONFIG: FullConfig = { erp: { - url: 'https://68.11.34.30:8082/', - username: '', - password: '', - headless: true, - ignoreHttpsErrors: true, - autoCloseBrowser: true + url: 'https://68.11.34.30:8082' }, database: { - dbType: 'mysql', - server: '', - mysqlHost: '192.168.31.83', - mysqlPort: 3306, - database: 'BLD_DB', - username: 'remote_user', - password: '' + activeType: 'mysql', + mysql: { + host: 'localhost', + port: 3306, + database: 'erp_db', + username: 'root', + password: '', + charset: 'utf8mb4' + }, + sqlserver: { + server: 'localhost', + port: 1433, + database: 'erp_db', + username: 'sa', + password: '', + driver: 'ODBC Driver 18 for SQL Server', + trustServerCertificate: true + } }, paths: { - dataDir: 'D:/python/playwrite/data/', - defaultOutput: '离散备料计划维护_合并.xlsx', - validationOutput: '物料状态校验结果.xlsx' + dataDir: './data/', + defaultOutput: 'output.xlsx', + validationOutput: 'validation-result.xlsx' }, extraction: { batchSize: 100, @@ -61,103 +79,44 @@ const DEFAULT_SETTINGS: SettingsData = { matchMode: 'substring', enableCrud: false, defaultManager: '' + }, + orderResolution: { + tableName: '', + productionIdField: '', + orderNumberField: '' } } -/** - * Check if value is a plain object - */ -function isObject(value: unknown): value is Record { - 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(source: T, target: Partial): 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], - targetValue as Partial]> - ) - } else if (targetValue !== undefined) { - result[key] = targetValue as T[Extract] - } - } - } - - 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): { - 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 { private static instance: ConfigManager | null = null - private envPath!: string + private configPath!: string private backupPath!: string - private configCache: Map = new Map() + private config: FullConfig | null = null private initialized: boolean = false private constructor() { - if (this.initialized) { - return + if (this.initialized) 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\\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 } - /** - * Get the singleton instance - */ public static getInstance(): ConfigManager { if (ConfigManager.instance === null) { ConfigManager.instance = new ConfigManager() @@ -166,478 +125,189 @@ export class ConfigManager { } /** - * Initialize configuration from .env file + * 初始化配置 + * - 如果 config.yaml 不存在,创建默认配置 + * - 加载并验证配置 */ public async initialize(): Promise { - 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 { + private async loadConfig(): Promise { try { - // Clear cache before loading - this.configCache.clear() + const content = fs.readFileSync(this.configPath, 'utf-8') + const parsed = yaml.load(content) as Record - if (fs.existsSync(this.envPath)) { - const content = fs.readFileSync(this.envPath, 'utf-8') - const lines = content.split('\n') + // Zod 验证 + const validated = fullConfigSchema.parse(parsed) + this.config = validated - for (const line of lines) { - 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) - } - } - } + log.info('Configuration loaded and validated successfully') } 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 - * @param key - Configuration key - * @param defaultValue - Default value if key doesn't exist + * 保存配置到 YAML 文件 */ - public get(key: string): string | undefined - 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 { + private async saveConfig(config: FullConfig): Promise { try { - // Build .env content from cache - const lines: string[] = [] + // 备份现有配置 + if (fs.existsSync(this.configPath)) { + fs.copyFileSync(this.configPath, this.backupPath) + } - // ERP Configuration - REMOVED - // ERP parameters are now stored in the database (dbo_BIPUsers table) - // This section is kept for backward compatibility but values are not used - lines.push('# ===========================') - lines.push('# ERP 系统配置(已迁移到数据库)') - lines.push('# ===========================') - lines.push('# ERP_URL, ERP_USERNAME, ERP_PASSWORD 已从 .env 移除') - lines.push('# 这些参数现在存储在 dbo_BIPUsers 表中,每个用户可以有自己的 ERP 配置') - lines.push('') + // 转换为 YAML + const content = yaml.dump(config, { + indent: 2, + lineWidth: -1, // 不自动换行 + noRefs: true, // 不使用引用 + quotingType: '"', + forceQuotes: false + }) - // Database Configuration - SQL Server - 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('') + fs.writeFileSync(this.configPath, content, 'utf-8') - // Database Configuration - MySQL - lines.push('# ===========================') - 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') + this.config = config + log.info('Configuration saved successfully') return true } 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 } } /** - * 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 { - return { - erp: { - // 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 - ) - } + public getConfig(): FullConfig { + if (!this.config) { + throw new Error('Configuration not initialized. Call initialize() first.') } + 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 { - // ERP settings are now stored in the database (dbo_BIPUsers table) - // They are NOT saved to .env file anymore + public getActiveDatabaseConfig(): MySqlConfig | SqlServerConfig { + if (!this.config) { + throw new Error('Configuration not initialized') + } - // Database settings - this.set('DB_TYPE', settings.database.dbType) - 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() + const { activeType, mysql, sqlserver } = this.config.database + return activeType === 'mysql' ? mysql : sqlserver } /** - * Save partial settings (only update provided fields) - * Preserves all existing fields not included in the update + * 获取数据库类型 */ - public async savePartialSettings( - settings: Partial + public getDatabaseType(): DatabaseType { + if (!this.config) { + throw new Error('Configuration not initialized') + } + return this.config.database.activeType + } + + /** + * 更新部分配置(深合并) + */ + public async updateConfig( + updates: Partial ): Promise<{ success: boolean; error?: string }> { try { - // Step 1: Validate field whitelist - const validation = validateEditableFields(settings) - if (!validation.valid) { - log.warn('Attempted to save non-editable fields', { - invalidFields: validation.invalidFields - }) - return { - success: false, - error: `包含不允许修改的字段:${validation.invalidFields.join(', ')}` - } + if (!this.config) { + await this.loadConfig() } - // Step 2: Read current settings from .env file directly - // This avoids the cache key mismatch issue (ERP_URL vs erp.url) - await this.loadEnvFile() - const currentSettings = this.getAllSettings() + // 深合并 + const merged = this.deepMerge(this.config!, updates) - log.info('Current settings before merge', { - erpUrl: currentSettings.erp.url, - dbType: currentSettings.database.dbType, - dbName: currentSettings.database.database - }) + // 验证合并后的配置 + const validated = fullConfigSchema.parse(merged) - // Step 3: Deep merge - only update provided fields - const mergedSettings = deepMerge(currentSettings, settings) - - 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 success = await this.saveConfig(validated) + if (!success) { + return { success: false, error: '保存配置失败' } } - 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 } } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Error in savePartialSettings', { error: message }) - await this.restoreBackup() - return { - success: false, - error: `保存配置时发生错误:${message}` + if (error instanceof z.ZodError) { + const messages = error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`) + return { success: false, error: `配置验证失败:\n${messages.join('\n')}` } } + return { success: false, error: error instanceof Error ? error.message : '未知错误' } } } /** - * Reset to default settings + * 深合并工具函数 */ - public resetToDefaults(): SettingsData { - // Clear cache and reload from defaults - this.configCache.clear() - - // Set all defaults using underscore uppercase keys - this.set('ERP_URL', DEFAULT_SETTINGS.erp.url) - this.set('ERP_USERNAME', DEFAULT_SETTINGS.erp.username) - this.set('ERP_PASSWORD', DEFAULT_SETTINGS.erp.password) - - this.set('DB_TYPE', DEFAULT_SETTINGS.database.dbType) - this.set('DB_SERVER', DEFAULT_SETTINGS.database.server) - this.set('DB_MYSQL_HOST', DEFAULT_SETTINGS.database.mysqlHost) - 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 { - try { - if (fs.existsSync(this.envPath)) { - fs.copyFileSync(this.envPath, this.backupPath) - log.debug('Backup created', { path: this.backupPath }) - return true + private deepMerge>(source: T, target: Partial): T { + const result = { ...source } + for (const key in target) { + if (target[key] !== undefined) { + if ( + typeof target[key] === 'object' && + target[key] !== null && + !Array.isArray(target[key]) + ) { + result[key] = this.deepMerge(result[key] as any, target[key] as any) + } else { + result[key] = target[key] as any + } } - 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 { - try { - if (fs.existsSync(this.backupPath)) { - fs.copyFileSync(this.backupPath, this.envPath) - await this.loadEnvFile() - log.debug('Restored from backup') - return true - } - return false - } catch (error) { - log.error('Failed to restore backup', { error }) - return false + public async resetToDefaults(): Promise { + return this.saveConfig(DEFAULT_CONFIG) + } + + /** + * 获取默认配置 + */ + public getDefaultConfig(): FullConfig { + return DEFAULT_CONFIG + } + + /** + * 导出配置为 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 + }) } } diff --git a/src/main/services/database/data-source.ts b/src/main/services/database/data-source.ts index cca48cb..7ab0865 100644 --- a/src/main/services/database/data-source.ts +++ b/src/main/services/database/data-source.ts @@ -2,21 +2,23 @@ * TypeORM Data Source Configuration * * 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 { 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' { - const dbType = process.env.DB_TYPE?.toLowerCase() - if (dbType === 'sqlserver' || dbType === 'mssql') { - return 'mssql' - } - return 'mysql' + const configManager = ConfigManager.getInstance() + const dbType = configManager.getDatabaseType() + return dbType === 'sqlserver' ? 'mssql' : 'mysql' } /** @@ -24,36 +26,40 @@ function getDatabaseType(): 'mysql' | 'mssql' { */ function buildDataSourceOptions(): DataSourceOptions { const type = getDatabaseType() + const configManager = ConfigManager.getInstance() + const config = configManager.getConfig() const commonOptions: Partial = { entities: [__dirname + '/entities/*.{ts,js}'], synchronize: false, // Never auto-sync in production - logging: process.env.NODE_ENV !== 'production' + logging: false } if (type === 'mssql') { + const dbConfig = config.database.sqlserver return { type: 'mssql', - host: process.env.DB_SERVER || 'localhost', - port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), - username: process.env.DB_USERNAME || 'sa', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '', + host: dbConfig.server, + port: dbConfig.port, + username: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, options: { encrypt: false, - trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + trustServerCertificate: dbConfig.trustServerCertificate }, ...commonOptions } as DataSourceOptions } + const dbConfig = config.database.mysql return { type: 'mysql', - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - username: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '', + host: dbConfig.host, + port: dbConfig.port, + username: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, ...commonOptions } as DataSourceOptions } diff --git a/src/main/services/database/index.ts b/src/main/services/database/index.ts index a365352..2872ce3 100644 --- a/src/main/services/database/index.ts +++ b/src/main/services/database/index.ts @@ -5,6 +5,7 @@ * Supports both MySQL and SQL Server databases. */ +import { ConfigManager } from '../config/config-manager' import { MySqlService } from './mysql' import { SqlServerService } from './sql-server' import type { @@ -23,42 +24,43 @@ const log = createLogger('DatabaseFactory') const instances: Map = new Map() /** - * Get the current database type from environment + * Get the current database type from config manager */ export function getDatabaseType(): DatabaseType { - const dbType = process.env.DB_TYPE?.toLowerCase() - if (dbType === 'sqlserver' || dbType === 'mssql') { - return 'sqlserver' - } - return 'mysql' + const configManager = ConfigManager.getInstance() + return configManager.getDatabaseType() } /** - * Create MySQL configuration from environment variables + * Create MySQL configuration from config manager */ export function createMySqlConfig(): MySqlConfig { + const configManager = ConfigManager.getInstance() + const dbConfig = configManager.getConfig().database.mysql return { - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database } } /** - * Create SQL Server configuration from environment variables + * Create SQL Server configuration from config manager */ export function createSqlServerConfig(): SqlServerConfig { + const configManager = ConfigManager.getInstance() + const dbConfig = configManager.getConfig().database.sqlserver return { - server: process.env.DB_SERVER || 'localhost', - port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), - user: process.env.DB_USERNAME || 'sa', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '', + server: dbConfig.server, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, options: { 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. * - * @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 */ export async function create(type?: DatabaseType): Promise { @@ -105,7 +107,7 @@ export async function create(type?: DatabaseType): Promise { /** * 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 */ export function get(type?: DatabaseType): IDatabaseService | undefined { diff --git a/src/main/services/erp/order-resolver.ts b/src/main/services/erp/order-resolver.ts index 120dcba..0ebae1d 100644 --- a/src/main/services/erp/order-resolver.ts +++ b/src/main/services/erp/order-resolver.ts @@ -7,14 +7,12 @@ * - productionID format: 2 digits + 1 letter + serial number (e.g., "22A1", "22A1234") * - 生产订单号 format: SC + 14 digits (e.g., "SC70202602120085") * - * Database table: productionContractData_26年压力表合同数据 - * Fields: 总排号 (productionID), 生产订单号 (production order number) + * Database table and field names are loaded from config.yaml */ import type { IDatabaseService } from '../database' -import { SqlServerService } from '../database/sql-server' +import { ConfigManager } from '../config/config-manager' import { createLogger } from '../logger' -import sql from 'mssql' const log = createLogger('OrderResolver') @@ -28,49 +26,52 @@ export interface OrderMapping { productionId?: string /** Final production order number to use */ orderNumber?: string - /** Whether this mapping is valid */ - isValid: boolean - /** Error or warning message */ + /** Whether the order number was successfully resolved */ + resolved: boolean + /** Error message if resolution failed */ error?: string - /** Input type */ - inputType: 'productionId' | 'orderNumber' | 'unknown' } +/** + * Order number type recognition result + */ +export type OrderNumberType = 'productionId' | 'orderNumber' | 'unknown' + /** * Resolution statistics */ export interface ResolutionStats { totalInputs: number - recognizedAsProductionId: number - recognizedAsOrderNumber: number + validOrderNumbers: number + validProductionIds: number + resolvedCount: number + failedCount: 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 = { - /** productionID: 2 digits + 1 letter + serial number (1+) */ - PRODUCTION_ID: /^\d{2}[A-Za-z]\d+$/, - /** 生产订单号:SC + 14 digits */ - ORDER_NUMBER: /^SC\d{14}$/ -} as const +const PRODUCTION_ID_PATTERN = /^\d{2}[A-Z]\d{1,4}$/i + +/** + * Production order number pattern: SC + 14 digits + */ +const ORDER_NUMBER_PATTERN = /^SC\d{14}$/i /** * Database table and field names - * Can be overridden via environment variables: - * - 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: '生产订单号') + * Loaded from config.yaml via ConfigManager */ -export const DB_CONFIG = { - TABLE_NAME: process.env.DB_TABLE_NAME || 'productionContractData_26年压力表合同数据', - FIELD_PRODUCTION_ID: process.env.DB_FIELD_PRODUCTION_ID || '总排号', - FIELD_ORDER_NUMBER: process.env.DB_FIELD_ORDER_NUMBER || '生产订单号' -} as const +export function getDbConfig() { + const configManager = ConfigManager.getInstance() + const config = configManager.getConfig() + return { + TABLE_NAME: config.orderResolution.tableName || 'productionContractData_26 年压力表合同数据', + FIELD_PRODUCTION_ID: config.orderResolution.productionIdField || '总排号', + FIELD_ORDER_NUMBER: config.orderResolution.orderNumberField || '生产订单号' + } +} /** * Order Number Resolver Service @@ -84,364 +85,201 @@ export class OrderNumberResolver { /** * 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') { - const firstUnderscoreIndex = mysqlTableName.indexOf('_') - if (firstUnderscoreIndex > 0) { - const schema = mysqlTableName.substring(0, firstUnderscoreIndex) - const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1) - return `[${schema}].[${tableName}]` - } - return `[dbo].[${mysqlTableName}]` + return `[dbo].[${tableName}]` } - return mysqlTableName + return tableName } /** - * Recognize the type of an input string - * @param input - The input string to recognize - * @returns The recognized type + * Check if input matches productionID pattern */ - recognizeType(input: string): 'productionId' | 'orderNumber' | 'unknown' { - const trimmed = input.trim() - - if (ORDER_PATTERNS.ORDER_NUMBER.test(trimmed)) { - return 'orderNumber' - } - - if (ORDER_PATTERNS.PRODUCTION_ID.test(trimmed)) { - return 'productionId' - } - - return 'unknown' + isProductionId(input: string): boolean { + return PRODUCTION_ID_PATTERN.test(input) } /** - * Resolve a list of inputs to production order numbers - * @param inputs - List of input strings (can be productionID or 生产订单号) - * @returns List of order mappings + * Check if input matches order number pattern */ - async resolve(inputs: string[]): Promise { - const mappings: OrderMapping[] = [] - 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 + isOrderNumber(input: string): boolean { + return ORDER_NUMBER_PATTERN.test(input) } /** - * Resolve productionIDs to production order numbers via database lookup - * @param productionIds - List of productionIDs to resolve - * @returns List of order mappings + * Map productionID to order number via database lookup */ - private async resolveProductionIds(productionIds: string[]): Promise { - 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 - } - + async mapProductionIdToOrderNumber(productionId: string): Promise { try { - const isSqlServer = this.dbService.type === 'sqlserver' - const tableName = this.getTableName(DB_CONFIG.TABLE_NAME) + const dbConfig = getDbConfig() + const tableName = this.getTableName(dbConfig.TABLE_NAME) - log.debug('Resolving production IDs', { - count: productionIds.length, - dbType: this.dbService.type - }) + let sql: string + let params: any[] - let result - - if (isSqlServer) { - // 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) + if (this.dbService.type === 'sqlserver') { + sql = `SELECT TOP 1 [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] = @p0` + params = [productionId] } else { - // Use standard query for MySQL - const placeholders = productionIds.map(() => '?').join(', ') - 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) + sql = `SELECT \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` = ? LIMIT 1` + params = [productionId] } - // Create a map for quick lookup (use lowercase key for case-insensitive matching) - const resultMap = new Map() + const result = await this.dbService.query(sql, params) + + 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> { + 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() for (const row of result.rows) { - const prodId = row[DB_CONFIG.FIELD_PRODUCTION_ID] as string - const orderNum = row[DB_CONFIG.FIELD_ORDER_NUMBER] as string + const keys = Object.keys(row) + const prodId = row[keys[0]] as string + const orderNum = row[keys[1]] as string if (prodId && orderNum) { - // Store with lowercase key for case-insensitive matching - 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' - }) + mappings.set(prodId, orderNum) } } return mappings } catch (error) { const message = error instanceof Error ? error.message : '未知数据库错误' - - // Return all as failed with error - return productionIds.map((pid) => ({ - input: pid, - productionId: pid, - isValid: false, - error: `数据库查询失败:${message}`, - inputType: 'productionId' - })) + log.error('Failed to map productionIds to order numbers', { + error: message + }) + throw error } } /** - * Verify that order numbers exist in the database - * @param orderNumbers - List of order numbers to verify - * @returns List of valid order numbers + * Resolve order numbers from mixed input */ - private async verifyOrderNumbers(orderNumbers: string[]): Promise { - if (!this.dbService.isConnected()) { - return orderNumbers // Skip verification if not connected - } + async resolve(inputs: string[]): Promise { + const mappings: OrderMapping[] = [] - try { - const isSqlServer = this.dbService.type === 'sqlserver' - const tableName = this.getTableName(DB_CONFIG.TABLE_NAME) + for (const input of inputs) { + const mapping: OrderMapping = { input, resolved: false } - let result - - if (isSqlServer) { - // Use queryWithParams for SQL Server with explicit parameter types - const placeholders = orderNumbers.map((_, idx) => `@p${idx}`).join(', ') - const params: Record< - string, - { - value: string - type: sql.ISqlType | sql.ISqlTypeFactoryWithLength | sql.ISqlTypeWithLength + if (this.isOrderNumber(input)) { + // Already an order number + mapping.orderNumber = input + mapping.resolved = true + } else if (this.isProductionId(input)) { + // Is a productionID, need to lookup + mapping.productionId = input + try { + const orderNumber = await this.mapProductionIdToOrderNumber(input) + if (orderNumber) { + mapping.orderNumber = orderNumber + mapping.resolved = true + } else { + mapping.error = '未在数据库中找到对应的订单号' } - > = {} - - orderNumbers.forEach((id, idx) => { - 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) + } catch (error) { + mapping.error = error instanceof Error ? error.message : '数据库查询失败' + log.warn('Failed to resolve productionID', { productionId: input, error }) + } } else { - // Use standard query for MySQL - 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) + mapping.error = '格式不识别:既不是有效的生产订单号也不是总排号格式' } - return result.rows.map((row) => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string) - } catch (error) { - console.warn('[OrderResolver] Failed to verify order numbers:', error) - return orderNumbers // Skip verification on error + mappings.push(mapping) } + + 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 - * @param mappings - List of order mappings - * @returns Resolution statistics */ getStats(mappings: OrderMapping[]): ResolutionStats { const stats: ResolutionStats = { totalInputs: mappings.length, - recognizedAsProductionId: 0, - recognizedAsOrderNumber: 0, - unknownFormat: 0, - successfullyResolved: 0, - failedToResolve: 0, - notFoundInDatabase: 0 + validOrderNumbers: 0, + validProductionIds: 0, + resolvedCount: 0, + failedCount: 0, + unknownFormat: 0 } for (const mapping of mappings) { - if (mapping.inputType === 'productionId') { - stats.recognizedAsProductionId++ - } else if (mapping.inputType === 'orderNumber') { - stats.recognizedAsOrderNumber++ + if (mapping.resolved) { + stats.resolvedCount++ + if (mapping.orderNumber && !mapping.productionId) { + stats.validOrderNumbers++ + } else if (mapping.productionId) { + stats.validProductionIds++ + } } else { - stats.unknownFormat++ - } - - if (mapping.isValid) { - stats.successfullyResolved++ - } else if (mapping.error?.includes('不存在于数据库中')) { - stats.notFoundInDatabase++ - stats.failedToResolve++ - } else if (mapping.error) { - stats.failedToResolve++ + stats.failedCount++ + if (!mapping.productionId && !mapping.orderNumber) { + stats.unknownFormat++ + } } } 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!) - } } diff --git a/src/main/services/logger/index.ts b/src/main/services/logger/index.ts index 4e7d1a3..8c8663d 100644 --- a/src/main/services/logger/index.ts +++ b/src/main/services/logger/index.ts @@ -54,7 +54,7 @@ const createFileTransport = (level?: string): DailyRotateFile => { // Create the logger instance 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' }, transports: [ // Console transport - always enabled @@ -67,7 +67,7 @@ const logger = winston.createLogger({ }) // Add error-specific file transport in production -if (process.env.NODE_ENV === 'production') { +if (app.isPackaged) { logger.add( new DailyRotateFile({ filename: path.join(getLogDir(), 'error-%DATE%.log'), diff --git a/src/main/services/user/bip-users-dao.ts b/src/main/services/user/bip-users-dao.ts index 6dd0f3e..f28f6d4 100644 --- a/src/main/services/user/bip-users-dao.ts +++ b/src/main/services/user/bip-users-dao.ts @@ -10,6 +10,7 @@ import { MySqlService } from '../database/mysql' import { SqlServerService } from '../database/sql-server' +import { ConfigManager } from '../config/config-manager' import sql from 'mssql' import type { UserInfo } from '../../types/user.types' @@ -43,17 +44,14 @@ export class BIPUsersDAO { private mysqlService: MySqlService | null = null private sqlServerService: SqlServerService | null = null private dbType: 'mysql' | 'sqlserver' = 'mysql' + private configManager: ConfigManager /** - * Constructor - determine database type from environment + * Constructor - get database type from ConfigManager */ constructor() { - const dbType = process.env.DB_TYPE?.toLowerCase() - if (dbType === 'sqlserver' || dbType === 'mssql') { - this.dbType = 'sqlserver' - } else { - this.dbType = 'mysql' - } + this.configManager = ConfigManager.getInstance() + this.dbType = this.configManager.getDatabaseType() } /** @@ -69,20 +67,23 @@ export class BIPUsersDAO { * Get database service instance (MySQL or SQL Server) */ private async getDatabaseService(): Promise { + const config = this.configManager.getConfig() + if (this.dbType === 'sqlserver') { if (this.sqlServerService && this.sqlServerService.isConnected()) { return this.sqlServerService } + const dbConfig = config.database.sqlserver this.sqlServerService = new SqlServerService({ - server: process.env.DB_SERVER || 'localhost', - port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), - user: process.env.DB_USERNAME || 'sa', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '', + server: dbConfig.server, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, options: { encrypt: false, - trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + trustServerCertificate: dbConfig.trustServerCertificate } }) @@ -93,12 +94,13 @@ export class BIPUsersDAO { return this.mysqlService } + const dbConfig = config.database.mysql this.mysqlService = new MySqlService({ - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database }) await this.mysqlService.connect() @@ -485,12 +487,11 @@ export class BIPUsersDAO { } /** - * Get ERP configuration for a user - * @param username - The username to get ERP config for - * @returns ERP configuration object or null if not found + * Get ERP credentials for a user (username and password only, URL is from config.yaml) + * @param username - The username to get ERP credentials for + * @returns ERP credentials object or null if not found */ - async getUserErpConfig(username: string): Promise<{ - url: string + async getUserErpCredentials(username: string): Promise<{ username: string password: string } | null> { @@ -501,7 +502,7 @@ export class BIPUsersDAO { if (this.dbType === 'sqlserver') { const sqlString = ` - SELECT ${cols.ERP_URL}, ${cols.ERP_USERNAME}, ${cols.ERP_PASSWORD} + SELECT ${cols.ERP_USERNAME}, ${cols.ERP_PASSWORD} FROM ${tableName} WHERE UserName = @username ` @@ -513,7 +514,6 @@ export class BIPUsersDAO { if (result.rows.length > 0) { const row = result.rows[0] return { - url: (row[cols.ERP_URL] as string) || '', username: (row[cols.ERP_USERNAME] as string) || '', password: (row[cols.ERP_PASSWORD] as string) || '' } @@ -521,7 +521,7 @@ export class BIPUsersDAO { return null } else { const sqlString = ` - SELECT ${cols.ERP_URL}, ${cols.ERP_USERNAME}, ${cols.ERP_PASSWORD} + SELECT ${cols.ERP_USERNAME}, ${cols.ERP_PASSWORD} FROM ${tableName} WHERE UserName = ? ` @@ -531,7 +531,6 @@ export class BIPUsersDAO { if (result.rows.length > 0) { const row = result.rows[0] return { - url: (row[cols.ERP_URL] as string) || '', username: (row[cols.ERP_USERNAME] as string) || '', password: (row[cols.ERP_PASSWORD] as string) || '' } @@ -539,22 +538,20 @@ export class BIPUsersDAO { return null } } catch (error) { - console.error('[BIPUsersDAO] Get user ERP config error:', error) + console.error('[BIPUsersDAO] Get user ERP credentials error:', error) return null } } /** - * Update ERP configuration for a user - * @param username - The username to update ERP config for - * @param erpUrl - The ERP URL + * Update ERP credentials for a user (username and password only, URL is from config.yaml) + * @param username - The username to update ERP credentials for * @param erpUsername - The ERP username * @param erpPassword - The ERP password * @returns True if successful */ - async updateUserErpConfig( + async updateUserErpCredentials( username: string, - erpUrl: string, erpUsername: string, erpPassword: string ): Promise { @@ -566,15 +563,13 @@ export class BIPUsersDAO { if (this.dbType === 'sqlserver') { const sqlString = ` UPDATE ${tableName} - SET ${cols.ERP_URL} = @erpUrl, - ${cols.ERP_USERNAME} = @erpUsername, + SET ${cols.ERP_USERNAME} = @erpUsername, ${cols.ERP_PASSWORD} = @erpPassword WHERE UserName = @username ` await (dbService as SqlServerService).queryWithParams(sqlString, { username: { value: username, type: sql.NVarChar(255) }, - erpUrl: { value: erpUrl, type: sql.NVarChar(500) }, erpUsername: { value: erpUsername, type: sql.NVarChar(255) }, erpPassword: { value: erpPassword, type: sql.NVarChar(255) } }) @@ -582,22 +577,16 @@ export class BIPUsersDAO { } else { const sqlString = ` UPDATE ${tableName} - SET ${cols.ERP_URL} = ?, - ${cols.ERP_USERNAME} = ?, + SET ${cols.ERP_USERNAME} = ?, ${cols.ERP_PASSWORD} = ? WHERE UserName = ? ` - await (dbService as MySqlService).query(sqlString, [ - erpUrl, - erpUsername, - erpPassword, - username - ]) + await (dbService as MySqlService).query(sqlString, [erpUsername, erpPassword, username]) return true } } catch (error) { - console.error('[BIPUsersDAO] Update user ERP config error:', error) + console.error('[BIPUsersDAO] Update user ERP credentials error:', error) return false } } diff --git a/src/main/services/user/migration/add-erp-params-migration.ts b/src/main/services/user/migration/add-erp-params-migration.ts index 6b4e8b2..879a516 100644 --- a/src/main/services/user/migration/add-erp-params-migration.ts +++ b/src/main/services/user/migration/add-erp-params-migration.ts @@ -2,8 +2,10 @@ * Migration Script: Add ERP parameters to BIPUsers table * * This script adds ERP_URL, ERP_Username, and ERP_Password columns - * to the dbo_BIPUsers table and initializes all existing users - * with the same ERP credentials from the current .env configuration. + * to the dbo_BIPUsers table and initializes all existing users. + * + * Note: ERP credentials are now stored per-user in the database. + * This migration is for backward compatibility only. * * Usage: * 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 { MySqlService } from '../../database/mysql' import { SqlServerService } from '../../database/sql-server' +import yaml from 'js-yaml' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -28,8 +31,7 @@ const MIGRATION_CONFIG = { tableName: { mysql: 'dbo_BIPUsers', sqlserver: '[dbo].[BIPUsers]' - }, - columns: ['ERP_URL', 'ERP_Username', 'ERP_Password'] + } } /** @@ -40,15 +42,12 @@ async function checkColumnExistsMySQL( tableName: string, columnName: string ): Promise { - const sql = ` - SELECT COUNT(*) as count - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = ? - AND COLUMN_NAME = ? - ` - const result = await mysqlService.query(sql, [tableName, columnName]) - return result.rows.length > 0 && (result.rows[0].count as number) > 0 + const result = await mysqlService.query( + `SELECT COUNT(*) as count FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`, + [tableName, columnName] + ) + return (result.rows[0]?.count as number) > 0 } /** @@ -59,16 +58,12 @@ async function checkColumnExistsSqlServer( tableName: string, columnName: string ): Promise { - const sql = ` - SELECT COUNT(*) as count - FROM sys.columns - WHERE object_id = OBJECT_ID(${tableName}) - AND name = @columnName - ` - 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 + const result = await sqlServerService.query( + `SELECT COUNT(*) as count FROM sys.columns + WHERE OBJECT_ID = OBJECT_ID(?) AND name = ?`, + [tableName, columnName] + ) + return (result.rows[0]?.count as number) > 0 } /** @@ -80,9 +75,8 @@ async function addColumnMySQL( columnName: string, columnType: string ): Promise { - const sql = `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType} NULL` - await mysqlService.query(sql) - console.log(` ✓ Added column ${columnName} to ${tableName}`) + await mysqlService.query(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`) + console.log(` ✓ Added column ${columnName} (${columnType})`) } /** @@ -94,49 +88,64 @@ async function addColumnSqlServer( columnName: string, columnType: string ): Promise { - const sql = `ALTER TABLE ${tableName} ADD ${columnName} ${columnType} NULL` - await sqlServerService.query(sql) - console.log(` ✓ Added column ${columnName} to ${tableName}`) + await sqlServerService.query(`ALTER TABLE ${tableName} ADD ${columnName} ${columnType}`) + console.log(` ✓ Added column ${columnName} (${columnType})`) } /** - * Update all users with ERP credentials from .env + * Initialize ERP credentials for all users in MySQL */ async function initializeErpCredentialsMySQL( mysqlService: MySqlService, + tableName: string, erpUrl: string, erpUsername: string, erpPassword: string -): Promise { - const sql = ` - UPDATE ${MIGRATION_CONFIG.tableName.mysql} - SET ERP_URL = ?, ERP_Username = ?, ERP_Password = ? - WHERE ERP_URL IS NULL OR ERP_URL = '' - ` - const result = await mysqlService.query(sql, [erpUrl, erpUsername, erpPassword]) - return result.rowCount +): Promise { + const result = await mysqlService.query(`SELECT COUNT(*) as count FROM ${tableName}`) + const userCount = result.rows[0]?.count as number + + if (userCount === 0) { + console.log('No users found in BIPUsers table') + return + } + + 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( sqlServerService: SqlServerService, + tableName: string, erpUrl: string, erpUsername: string, erpPassword: string -): Promise { - const sql = ` - UPDATE ${MIGRATION_CONFIG.tableName.sqlserver} - SET ERP_URL = @erpUrl, ERP_Username = @erpUsername, ERP_Password = @erpPassword - WHERE ERP_URL IS NULL OR ERP_URL = '' - ` - const result = await sqlServerService.queryWithParams(sql, { - erpUrl: { value: erpUrl, type: require('mssql').NVarChar(500) }, - erpUsername: { value: erpUsername, type: require('mssql').NVarChar(255) }, - erpPassword: { value: erpPassword, type: require('mssql').NVarChar(255) } - }) - return result.rowCount +): Promise { + const result = await sqlServerService.query(`SELECT COUNT(*) as count FROM ${tableName}`) + const userCount = result.rows[0]?.count as number + + if (userCount === 0) { + console.log('No users found in BIPUsers table') + return + } + + console.log(`Initializing ERP credentials for ${userCount} user(s)...`) + + 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 { console.log('\n📦 Running MySQL Migration...') - // Read database config from .env file with correct key names - const mysqlHost = configManager.get('DB_MYSQL_HOST', 'localhost') - 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', '') + const config = configManager.getConfig() + const dbConfig = config.database.mysql - console.log(`Connecting to MySQL: ${mysqlHost}:${mysqlPort}/${mysqlDatabase}`) + console.log(`Connecting to MySQL: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`) const mysqlService = new MySqlService({ - host: mysqlHost, - port: mysqlPort, - user: mysqlUser, - password: mysqlPassword, - database: mysqlDatabase + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database }) try { @@ -182,29 +187,18 @@ async function runMySQLMigration(configManager: ConfigManager): Promise { } } - // Initialize ERP credentials from .env - const erpUrl = configManager.get('ERP_URL', '') - const erpUsername = configManager.get('ERP_USERNAME', '') - const erpPassword = configManager.get('ERP_PASSWORD', '') + // Note: ERP credentials are now managed per-user via settings UI + // This migration no longer initializes them from config + console.log('\n✓ MySQL Migration completed') + 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() + } 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 { async function runSqlServerMigration(configManager: ConfigManager): Promise { 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({ - server: configManager.get('DB_SERVER', 'localhost'), - port: configManager.getNumber('DB_SQLSERVER_PORT', 1433), - user: configManager.get('DB_USERNAME', 'sa'), - password: configManager.get('DB_PASSWORD', ''), - database: configManager.get('DB_NAME', ''), + server: dbConfig.server, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, options: { - encrypt: false, - trustServerCertificate: configManager.get('DB_TRUST_SERVER_CERTIFICATE') === 'yes' + trustServerCertificate: dbConfig.trustServerCertificate } }) @@ -247,70 +244,46 @@ async function runSqlServerMigration(configManager: ConfigManager): Promise { - console.log('==============================================') - console.log('BIPUsers Table Migration: Add ERP Parameters') - console.log('==============================================\n') - - const configManager = ConfigManager.getInstance() - await configManager.initialize() - - const dbType = configManager.get('DB_TYPE', 'mysql').toLowerCase() - const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' +async function main(): Promise { + console.log('╔═══════════════════════════════════════════════════════════╗') + console.log('║ Migration: Add ERP Parameters to BIPUsers Table ║') + console.log('╚═══════════════════════════════════════════════════════════╝') try { - if (isSqlServer) { - await runSqlServerMigration(configManager) - } else { + const configManager = ConfigManager.getInstance() + await configManager.initialize() + + const dbType = configManager.getDatabaseType() + console.log(`\nCurrent database type: ${dbType}`) + + if (dbType === 'mysql') { await runMySQLMigration(configManager) + } else { + await runSqlServerMigration(configManager) } - console.log('==============================================') - 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') + console.log('\n✅ Migration completed successfully!\n') } catch (error) { - console.error('\n❌ Migration failed with error:', error) + console.error('\n❌ Migration failed:', error instanceof Error ? error.message : error) process.exit(1) } } -// Run migration -runMigration().catch((error) => { - console.error('Unexpected error:', error) - process.exit(1) -}) +main() diff --git a/src/main/services/user/user-erp-config-service.ts b/src/main/services/user/user-erp-config-service.ts index f2c7d47..52e20f9 100644 --- a/src/main/services/user/user-erp-config-service.ts +++ b/src/main/services/user/user-erp-config-service.ts @@ -1,13 +1,14 @@ /** * 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. + * ERP URL is fixed and stored in config.yaml. * * Features: - * - Get current user's ERP config - * - Update current user's ERP config - * - Get ERP config for any user (admin only) + * - Get current user's ERP credentials + * - Update current user's ERP credentials + * - Get ERP credentials for any user (admin only) */ import { BIPUsersDAO } from './bip-users-dao' @@ -17,10 +18,9 @@ import { createLogger } from '../logger' const log = createLogger('UserErpConfigService') /** - * ERP Configuration object + * ERP Credentials object (username and password only) */ -export interface ErpConfig { - url: string +export interface ErpCredentials { username: string password: string } @@ -48,9 +48,9 @@ export class UserErpConfigService { /** * 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 { + async getCurrentUserErpConfig(): Promise { try { const sessionManager = SessionManager.getInstance() const currentUser = sessionManager.getUserInfo() @@ -60,56 +60,55 @@ export class UserErpConfigService { return null } - log.info('Fetching ERP config for user', { username: currentUser.username }) - const config = await this.dao.getUserErpConfig(currentUser.username) + log.info('Fetching ERP credentials for user', { username: currentUser.username }) + const config = await this.dao.getUserErpCredentials(currentUser.username) 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 } - log.info('ERP config retrieved successfully', { + log.info('ERP credentials retrieved successfully', { username: currentUser.username, - hasUrl: !!config.url, hasUsername: !!config.username, hasPassword: !!config.password }) return config } catch (error) { - log.error('Error getting current user ERP config', { error }) + log.error('Error getting current user ERP credentials', { error }) return null } } /** - * Get ERP configuration for a specific user (admin only) - * @param username - The username to get ERP config for - * @returns ERP configuration or null if not found + * Get ERP credentials for a specific user (admin only) + * @param username - The username to get ERP credentials for + * @returns ERP credentials or null if not found */ - async getUserErpConfig(username: string): Promise { + async getUserErpConfig(username: string): Promise { try { - log.info('Fetching ERP config for user', { username }) - const config = await this.dao.getUserErpConfig(username) + log.info('Fetching ERP credentials for user', { username }) + const config = await this.dao.getUserErpCredentials(username) if (!config) { - log.warn('No ERP config found for user', { username }) + log.warn('No ERP credentials found for user', { username }) return null } return config } catch (error) { - log.error('Error getting user ERP config', { error }) + log.error('Error getting user ERP credentials', { error }) return null } } /** - * Update ERP configuration for the current authenticated user - * @param config - ERP configuration to save + * Update ERP credentials for the current authenticated user + * @param credentials - ERP credentials to save * @returns True if successful */ - async updateCurrentUserErpConfig(config: ErpConfig): Promise { + async updateCurrentUserErpConfig(credentials: ErpCredentials): Promise { try { const sessionManager = SessionManager.getInstance() const currentUser = sessionManager.getUserInfo() @@ -119,52 +118,50 @@ export class UserErpConfigService { return false } - log.info('Updating ERP config for user', { username: currentUser.username }) - const success = await this.dao.updateUserErpConfig( + log.info('Updating ERP credentials for user', { username: currentUser.username }) + const success = await this.dao.updateUserErpCredentials( currentUser.username, - config.url, - config.username, - config.password + credentials.username, + credentials.password ) if (success) { - log.info('ERP config updated successfully', { username: currentUser.username }) + log.info('ERP credentials updated successfully', { username: currentUser.username }) } else { - log.error('Failed to update ERP config', { username: currentUser.username }) + log.error('Failed to update ERP credentials', { username: currentUser.username }) } return success } catch (error) { - log.error('Error updating current user ERP config', { error }) + log.error('Error updating current user ERP credentials', { error }) return false } } /** - * Update ERP configuration for a specific user (admin only) - * @param username - The username to update ERP config for - * @param config - ERP configuration to save + * Update ERP credentials for a specific user (admin only) + * @param username - The username to update ERP credentials for + * @param credentials - ERP credentials to save * @returns True if successful */ - async updateUserErpConfig(username: string, config: ErpConfig): Promise { + async updateUserErpConfig(username: string, credentials: ErpCredentials): Promise { try { - log.info('Updating ERP config for user', { username }) - const success = await this.dao.updateUserErpConfig( + log.info('Updating ERP credentials for user', { username }) + const success = await this.dao.updateUserErpCredentials( username, - config.url, - config.username, - config.password + credentials.username, + credentials.password ) if (success) { - log.info('ERP config updated successfully', { username }) + log.info('ERP credentials updated successfully', { username }) } else { - log.error('Failed to update ERP config', { username }) + log.error('Failed to update ERP credentials', { username }) } return success } catch (error) { - log.error('Error updating user ERP config', { error }) + log.error('Error updating user ERP credentials', { error }) return false } } diff --git a/src/main/tools/config-path-debug.ts b/src/main/tools/config-path-debug.ts new file mode 100644 index 0000000..05996fe --- /dev/null +++ b/src/main/tools/config-path-debug.ts @@ -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\\\\AppData\\Roaming\\erpauto\\ +├── config.yaml # 用户配置 +└── config.yaml.backup # 自动备份 +`) diff --git a/src/main/types/config.schema.ts b/src/main/types/config.schema.ts new file mode 100644 index 0000000..7c1b6c2 --- /dev/null +++ b/src/main/types/config.schema.ts @@ -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 + +/** + * 验证匹配模式枚举 + */ +export const matchModeSchema = z.enum(['substring', 'exact']) +export type MatchMode = z.infer + +/** + * 验证数据源枚举 + */ +export const validationDataSourceSchema = z.enum([ + 'database_full', + 'database_filtered', + 'excel_existing', + 'excel_full' +]) +export type ValidationDataSource = z.infer + +/** + * 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 +export type DatabaseConfig = z.infer +export type MySqlConfig = z.infer +export type SqlServerConfig = z.infer +export type ErpSystemConfig = z.infer + +/** + * 验证并解析配置 + */ +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('; ') + } +} diff --git a/src/main/types/settings.types.ts b/src/main/types/settings.types.ts index d08b1f7..f27642e 100644 --- a/src/main/types/settings.types.ts +++ b/src/main/types/settings.types.ts @@ -2,6 +2,9 @@ * Settings types and interfaces * * 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_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 */ @@ -112,10 +97,9 @@ export interface ValidationConfig { /** * Complete settings data structure + * Note: No ERP configuration - it's stored in database per user */ export interface SettingsData { - /** ERP configuration */ - erp: ErpConfig /** Database configuration */ database: DatabaseConfig /** Path configuration */ @@ -158,8 +142,6 @@ export interface SettingsAPI { saveSettings: (settings: SettingsData) => Promise /** Reset to defaults (Admin only) */ resetDefaults: () => Promise - /** Test ERP connection */ - testErpConnection: () => Promise /** Test database connection */ testDbConnection: () => Promise } diff --git a/src/renderer/src/hooks/useExtractor.ts b/src/renderer/src/hooks/useExtractor.ts index 5be807f..f672830 100644 --- a/src/renderer/src/hooks/useExtractor.ts +++ b/src/renderer/src/hooks/useExtractor.ts @@ -63,12 +63,17 @@ export function useExtractor() { }) if (response.success && response.data) { + const { data } = response addLog( 'success', - `提取完成:下载 ${response.data.downloadedFiles.length} 个文件,共 ${response.data.recordCount} 条记录` + `提取完成:下载 ${data.downloadedFiles.length} 个文件,共 ${data.recordCount} 条记录` ) - if (response.data.errors.length > 0) { - addLog('warning', `存在 ${response.data.errors.length} 个错误`) + if (data.errors.length > 0) { + 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 { setError(response.error || '提取失败') diff --git a/src/renderer/src/pages/SettingsPage.tsx b/src/renderer/src/pages/SettingsPage.tsx index 77eb600..716e0cc 100644 --- a/src/renderer/src/pages/SettingsPage.tsx +++ b/src/renderer/src/pages/SettingsPage.tsx @@ -1,23 +1,16 @@ 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 { - erp: { - url?: string - username?: string - password?: string - } +interface ErpCredentials { + username: string + password: string } const SettingsPage: React.FC = () => { - const [settings, setSettings] = useState({ - erp: { - url: '', - username: '', - password: '' - } + const [credentials, setCredentials] = useState({ + username: '', + password: '' }) - const [isModified, setIsModified] = useState(false) const [isLoading, setIsLoading] = useState(true) const [message, setMessage] = useState<{ @@ -26,17 +19,25 @@ const SettingsPage: React.FC = () => { } | null>(null) useEffect(() => { - loadSettings() + loadCredentials() }, []) - const loadSettings = async () => { + const loadCredentials = async () => { try { setIsLoading(true) + // ERP credentials are loaded from database (current user's config) 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) } catch (error) { - showMessage('error', '加载设置失败') + showMessage('error', '加载 ERP 配置失败') } finally { setIsLoading(false) } @@ -47,38 +48,24 @@ const SettingsPage: React.FC = () => { setTimeout(() => setMessage(null), 3000) } - const updateSettings = (category: string, key: string, value: any) => { - setSettings((prev) => ({ - ...prev, - [category]: { - ...(prev as any)[category], - [key]: value - } - })) - setIsModified(true) - } - - const handleSaveSettings = async () => { + const handleSaveCredentials = async () => { try { - // Only send UI-supported fields (double safety) - const partialSettings = { + // Save ERP credentials to database (current user's config) + const result = await window.electron.settings.saveSettings({ erp: { - url: settings.erp?.url, - username: settings.erp?.username, - password: settings.erp?.password + username: credentials.username, + password: credentials.password } - } - - const result = await window.electron.settings.saveSettings(partialSettings as any) + } as any) if (result.success) { setIsModified(false) - showMessage('success', '设置保存成功') + showMessage('success', 'ERP 账号密码保存成功') } else { showMessage('error', result.error || '保存失败') } } catch (error) { - showMessage('error', '保存设置时发生错误') + showMessage('error', '保存配置时发生错误') } } @@ -97,7 +84,11 @@ const SettingsPage: React.FC = () => {
{message && (
{message.text}
@@ -107,58 +98,56 @@ const SettingsPage: React.FC = () => {

- 环境与认证配置 + ERP 账号配置

- 设置 ERP 系统的入口地址及自动化登录凭证。此配置将自动同步至本地 .env 文件。 + 设置 ERP 系统的登录账号和密码。此配置将存储在数据库中,按用户管理。

-
-
+
+
{ + 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" - value={settings.erp?.url || ''} - onChange={(e) => updateSettings('erp', 'url', e.target.value)} />
-
-
- - updateSettings('erp', 'username', e.target.value)} - /> -
-
- - updateSettings('erp', 'password', e.target.value)} - /> -
+
+ + { + setCredentials((prev) => ({ ...prev, password: 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" + />
-
+