refactor: migrate configuration to YAML-based system

This commit is contained in:
test
2026-03-07 11:30:09 +08:00
parent 48f1f51d76
commit c13be9e19a
27 changed files with 1716 additions and 1638 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<MySqlService | SqlServerService> {
const dbType = process.env.DB_TYPE?.toLowerCase()
const configManager = ConfigManager.getInstance()
const config = configManager.getConfig()
const dbType = configManager.getDatabaseType()
if (dbType === 'sqlserver' || dbType === 'mssql') {
if (dbType === 'sqlserver') {
const dbConfig = config.database.sqlserver
const sqlServerService = new SqlServerService({
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<MySqlService | SqlServerService> {
/**
* 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 {

View File

@@ -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

View File

@@ -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<SettingsData> => {
const userType = (sessionManager.getUserType() as UserType) || 'Guest'
log.debug('Getting settings', { userType })
ipcMain.handle('settings:getSettings', async (): Promise<any> => {
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<SettingsData>): Promise<SaveSettingsResult> => {
async (_event, settings: any): Promise<SaveSettingsResult> => {
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<SettingsData> = { ...settings }
delete nonErpSettings.erp
// Only save to .env if there are non-ERP settings
if (Object.keys(nonErpSettings).length > 0) {
log.info('Saving non-ERP settings to .env file', {
sections: Object.keys(nonErpSettings)
})
const result = await configManager.savePartialSettings(nonErpSettings)
if (!result.success) {
log.error('Failed to save settings to .env', {
error: result.error
})
return {
success: false,
error: result.error || '保存配置到文件失败'
}
}
}
log.info('Settings saved successfully')
return { success: true }
} 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<ConnectionTestResult> => {
try {
log.info('Testing ERP connection')
// Get current user's ERP config from database
const erpConfigService = UserErpConfigService.getInstance()
const userErpConfig = await erpConfigService.getCurrentUserErpConfig()
if (
!userErpConfig ||
!userErpConfig.url ||
!userErpConfig.username ||
!userErpConfig.password
) {
log.warn('ERP connection test failed - missing configuration')
return {
success: false,
message: '请先配置 ERP URL、用户名和密码'
}
}
// Create ERP auth service and try to login
const erpAuthService = new ErpAuthService({
url: userErpConfig.url,
username: userErpConfig.username,
password: userErpConfig.password
})
try {
await erpAuthService.login()
// Login successful, close browser
await erpAuthService.close()
log.info('ERP connection test successful')
return {
success: true,
message: 'ERP 连接测试成功!'
}
} catch (loginError) {
const errorMessage = loginError instanceof Error ? loginError.message : '登录失败'
log.error('ERP login failed', { error: errorMessage })
return {
success: false,
message: `ERP 连接测试失败:${errorMessage}`
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('ERP connection test error', { error: message })
return {
success: false,
message: `ERP 连接测试失败:${message}`
}
}
})
/**
* Test database connection
*/
ipcMain.handle('settings:testDbConnection', async (): Promise<ConnectionTestResult> => {
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
}
})

View File

@@ -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<ErpConfigResponse> => {
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<ErpConfigResponse> => {
async (_event, credentials: ErpCredentialsRequest): Promise<ErpConfigResponse> => {
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<ConnectionTestResult> => {
async (_event, credentials: ErpCredentialsRequest): Promise<ConnectionTestResult> => {
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
})

View File

@@ -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<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/**
* Deep merge two objects, only updating fields present in target
* Preserves all fields from source that are not in target
*/
function deepMerge<T>(source: T, target: Partial<T>): T {
const result = { ...source }
for (const key in target) {
if (key in target) {
const targetValue = target[key]
const sourceValue = result[key]
if (isObject(targetValue) && isObject(sourceValue)) {
result[key] = deepMerge(
sourceValue as T[Extract<keyof T, string>],
targetValue as Partial<T[Extract<keyof T, string>]>
)
} else if (targetValue !== undefined) {
result[key] = targetValue as T[Extract<keyof T, string>]
}
}
}
return result
}
/**
* UI editable field whitelist
* Fields that can be modified through the settings UI
* Note: ERP fields are no longer editable here - they are managed per-user in the database
*/
const UI_EDITABLE_FIELDS: string[] = [
// ERP fields removed - ERP config is now stored in dbo_BIPUsers table per user
// 'erp.url',
// 'erp.username',
// 'erp.password'
// Add more fields as UI expands
]
/**
* Validate that settings only contain editable fields
*/
function validateEditableFields(settings: Partial<SettingsData>): {
valid: boolean
invalidFields: string[]
} {
const invalidFields: string[] = []
for (const [section, values] of Object.entries(settings)) {
if (values && typeof values === 'object') {
for (const field of Object.keys(values)) {
const fieldPath = `${section}.${field}`
if (!UI_EDITABLE_FIELDS.includes(fieldPath)) {
invalidFields.push(fieldPath)
}
}
}
}
return {
valid: invalidFields.length === 0,
invalidFields
}
}
/**
* Configuration Manager Class
*/
export class ConfigManager {
private static instance: ConfigManager | null = null
private envPath!: string
private configPath!: string
private backupPath!: string
private configCache: Map<string, string> = 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\<user>\AppData\Roaming\erpauto\config.yaml
// 这样配置会在应用升级时保留,且不会暴露在应用目录中
this.configPath = path.join(app.getPath('userData'), 'config.yaml')
this.backupPath = path.join(app.getPath('userData'), 'config.yaml.backup')
log.info('Running in production mode', { configPath: this.configPath })
}
this.envPath = path.resolve(__dirname, '../../.env')
this.backupPath = path.resolve(__dirname, '../../.env.backup')
this.initialized = true
}
/**
* 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<void> {
await this.loadEnvFile()
if (!fs.existsSync(this.configPath)) {
log.info('Config file not found, creating default config.yaml')
await this.saveConfig(DEFAULT_CONFIG)
this.config = DEFAULT_CONFIG
return
}
await this.loadConfig()
}
/**
* Load .env file into cache
* 加载并验证 YAML 配置
*/
private async loadEnvFile(): Promise<void> {
private async loadConfig(): Promise<void> {
try {
// Clear cache before loading
this.configCache.clear()
const content = fs.readFileSync(this.configPath, 'utf-8')
const parsed = yaml.load(content) as Record<string, unknown>
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<boolean> {
private async saveConfig(config: FullConfig): Promise<boolean> {
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<boolean> {
// 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<SettingsData>
public getDatabaseType(): DatabaseType {
if (!this.config) {
throw new Error('Configuration not initialized')
}
return this.config.database.activeType
}
/**
* 更新部分配置(深合并)
*/
public async updateConfig(
updates: Partial<FullConfig>
): Promise<{ success: boolean; error?: string }> {
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<boolean> {
try {
if (fs.existsSync(this.envPath)) {
fs.copyFileSync(this.envPath, this.backupPath)
log.debug('Backup created', { path: this.backupPath })
return true
private deepMerge<T extends Record<string, any>>(source: T, target: Partial<T>): 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<boolean> {
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<boolean> {
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
})
}
}

View File

@@ -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<DataSourceOptions> = {
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
}

View File

@@ -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<DatabaseType, IDatabaseService> = 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<IDatabaseService> {
@@ -105,7 +107,7 @@ export async function create(type?: DatabaseType): Promise<IDatabaseService> {
/**
* 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 {

View File

@@ -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<OrderMapping[]> {
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<OrderMapping[]> {
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<string | null> {
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<string, string>()
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<Map<string, string>> {
try {
const dbConfig = getDbConfig()
const tableName = this.getTableName(dbConfig.TABLE_NAME)
if (productionIds.length === 0) {
return new Map()
}
// Use parameterized query to prevent SQL injection
const placeholders = productionIds.map((_, i) => `@p${i}`).join(', ')
const params = productionIds
let sql: string
if (this.dbService.type === 'sqlserver') {
sql = `SELECT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] IN (${placeholders})`
} else {
const idPlaceholders = productionIds.map(() => '?').join(', ')
sql = `SELECT \`${dbConfig.FIELD_PRODUCTION_ID}\`, \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` IN (${idPlaceholders})`
}
const result = await this.dbService.query(sql, params)
const mappings = new Map<string, string>()
for (const row of result.rows) {
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<string[]> {
if (!this.dbService.isConnected()) {
return orderNumbers // Skip verification if not connected
}
async resolve(inputs: string[]): Promise<OrderMapping[]> {
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!)
}
}

View File

@@ -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'),

View File

@@ -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<MySqlService | SqlServerService> {
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<boolean> {
@@ -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
}
}

View File

@@ -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<boolean> {
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<boolean> {
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<void> {
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<void> {
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<number> {
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<void> {
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<number> {
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<void> {
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<void> {
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<void> {
}
}
// 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<void> {
async function runSqlServerMigration(configManager: ConfigManager): Promise<void> {
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<void
}
}
// 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
console.log('\n✓ SQL Server Migration completed')
console.log(' Note: ERP credentials should be configured per-user via the Settings UI')
if (erpUrl && erpUsername && erpPassword) {
const updatedCount = await initializeErpCredentialsSqlServer(
sqlServerService,
erpUrl,
erpUsername,
erpPassword
)
console.log(` ✓ Updated ${updatedCount} users with ERP credentials`)
} else {
console.log(' ⚠ Skipping ERP credential initialization (missing .env values)')
}
console.log('✅ SQL Server Migration completed successfully!\n')
} catch (error) {
console.error('❌ SQL Server Migration failed:', error)
throw error
} finally {
await sqlServerService.disconnect()
} catch (error) {
console.error('✗ SQL Server Migration failed:', error instanceof Error ? error.message : error)
if (sqlServerService.isConnected()) {
await sqlServerService.disconnect()
}
throw error
}
}
/**
* Main migration runner
* Main function
*/
async function runMigration(): Promise<void> {
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<void> {
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()

View File

@@ -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<ErpConfig | null> {
async getCurrentUserErpConfig(): Promise<ErpCredentials | null> {
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<ErpConfig | null> {
async getUserErpConfig(username: string): Promise<ErpCredentials | null> {
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<boolean> {
async updateCurrentUserErpConfig(credentials: ErpCredentials): Promise<boolean> {
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<boolean> {
async updateUserErpConfig(username: string, credentials: ErpCredentials): Promise<boolean> {
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
}
}

View File

@@ -0,0 +1,114 @@
/**
* Configuration Path Debug Tool
*
* Run this to see where config files will be stored in different modes
* Usage: npx tsx src/main/tools/config-path-debug.ts
*/
import * as path from 'path'
// Simulate different environments
const scenarios = [
{
name: 'Development Mode (开发环境)',
env: {
NODE_ENV: 'development',
APP_PACKAGED: 'false'
},
appData: 'C:\\Users\\test\\AppData\\Roaming\\erpauto',
projectRoot: 'D:\\Projects\\ERPAuto'
},
{
name: 'Production - Portable (便携版)',
env: {
NODE_ENV: 'production',
APP_PACKAGED: 'true'
},
appData: 'C:\\Users\\test\\AppData\\Roaming\\erpauto',
projectRoot: 'D:\\Projects\\ERPAuto',
exeDir: 'D:\\PortableApps\\ERPAuto'
},
{
name: 'Production - Installed (安装版)',
env: {
NODE_ENV: 'production',
APP_PACKAGED: 'true'
},
appData: 'C:\\Users\\test\\AppData\\Roaming\\erpauto',
projectRoot: 'D:\\Projects\\ERPAuto'
}
]
console.log('╔════════════════════════════════════════════════════════════════╗')
console.log('║ ERPAuto Configuration Path Debug Tool ║')
console.log('╚════════════════════════════════════════════════════════════════╝\n')
for (const scenario of scenarios) {
console.log(`📋 ${scenario.name}`)
console.log('─'.repeat(60))
const isDev = scenario.env.NODE_ENV === 'development' || scenario.env.APP_PACKAGED === 'false'
let configPath: string
let backupPath: string
if (isDev) {
// 开发环境:项目根目录
configPath = path.join(scenario.projectRoot, 'config.yaml')
backupPath = path.join(scenario.projectRoot, 'config.yaml.backup')
} else {
// 生产环境(便携版和安装版):用户数据目录
configPath = path.join(scenario.appData, 'config.yaml')
backupPath = path.join(scenario.appData, 'config.yaml.backup')
}
console.log(` NODE_ENV: ${scenario.env.NODE_ENV}`)
console.log(` APP_PACKAGED: ${scenario.env.APP_PACKAGED}`)
console.log(` Is Development: ${isDev ? '✓ Yes' : '✗ No'}`)
if ('exeDir' in scenario) {
console.log(` EXE Directory: ${scenario.exeDir}`)
}
console.log(` → Config Path: ${configPath}`)
console.log(` → Backup Path: ${backupPath}`)
console.log('')
}
console.log('╔════════════════════════════════════════════════════════════════╗')
console.log('║ Configuration Strategy (配置策略): ║')
console.log('╚════════════════════════════════════════════════════════════════╝')
console.log(`
┌─────────────┬──────────────────────────────────────────────────────────┐
│ 环境 │ 配置文件位置 │
├─────────────┼──────────────────────────────────────────────────────────┤
│ 开发环境 │ 项目根目录\\config.yaml │
│ │ 方便编辑和调试,配置随代码版本管理 │
├─────────────┼──────────────────────────────────────────────────────────┤
│ 生产环境 │ %APPDATA%\\erpauto\\config.yaml │
│ (便携版/ │ 符合 Windows 规范,应用升级时配置保留,安全 │
│ 安装版) │ │
└─────────────┴──────────────────────────────────────────────────────────┘
💡 优势:
✓ 开发时配置在项目根目录,方便版本控制和团队协作
✓ 生产环境配置在用户数据目录,应用升级不会丢失配置
✓ 配置不暴露在应用目录,更安全
✓ 多用户环境下,每个用户有独立的配置
`)
console.log('╔════════════════════════════════════════════════════════════════╗')
console.log('║ Recommended Directory Structure: ║')
console.log('╚════════════════════════════════════════════════════════════════╝')
console.log(`
【开发环境】
D:\\Projects\\ERPAuto\\
├── src\\
├── package.json
├── config.yaml # 开发配置(可加入 .gitignore
├── config.yaml.backup # 自动备份
└── config.template.yaml # 配置模板(提交到版本控制)
【生产环境 - 便携版/安装版】
C:\\Users\\<user>\\AppData\\Roaming\\erpauto\\
├── config.yaml # 用户配置
└── config.yaml.backup # 自动备份
`)

View File

@@ -0,0 +1,155 @@
/**
* Configuration Schema Definitions
*
* Zod schemas for runtime validation of application configuration
*
* Note: ERP configuration is stored in database (dbo_BIPUsers table)
* and managed per-user, not in this config file.
*/
import { z } from 'zod'
/**
* 数据库类型枚举
*/
export const databaseTypeSchema = z.enum(['mysql', 'sqlserver'])
export type DatabaseType = z.infer<typeof databaseTypeSchema>
/**
* 验证匹配模式枚举
*/
export const matchModeSchema = z.enum(['substring', 'exact'])
export type MatchMode = z.infer<typeof matchModeSchema>
/**
* 验证数据源枚举
*/
export const validationDataSourceSchema = z.enum([
'database_full',
'database_filtered',
'excel_existing',
'excel_full'
])
export type ValidationDataSource = z.infer<typeof validationDataSourceSchema>
/**
* MySQL 配置 Schema
*/
export const mysqlConfigSchema = z.object({
host: z.string().min(1, 'MySQL host is required'),
port: z.number().int().min(1).max(65535).default(3306),
database: z.string().min(1, 'MySQL database is required'),
username: z.string().min(1, 'MySQL username is required'),
password: z.string(),
charset: z.string().default('utf8mb4')
})
/**
* SQL Server 配置 Schema
*/
export const sqlServerConfigSchema = z.object({
server: z.string().min(1, 'SQL Server is required'),
port: z.number().int().min(1).max(65535).default(1433),
database: z.string().min(1, 'SQL Server database is required'),
username: z.string().min(1, 'SQL Server username is required'),
password: z.string(),
driver: z.string().default('ODBC Driver 18 for SQL Server'),
trustServerCertificate: z.boolean().default(true)
})
/**
* 数据库配置(包含两种数据库的完整配置)
*/
export const databaseConfigSchema = z.object({
activeType: databaseTypeSchema.default('mysql'),
mysql: mysqlConfigSchema,
sqlserver: sqlServerConfigSchema
})
/**
* 路径配置 Schema
*/
export const pathsConfigSchema = z.object({
dataDir: z.string().min(1, 'Data directory is required'),
defaultOutput: z.string().default('离散备料计划维护_合并.xlsx'),
validationOutput: z.string().default('物料状态校验结果.xlsx')
})
/**
* 数据提取配置 Schema
*/
export const extractionConfigSchema = z.object({
batchSize: z.number().int().min(1).max(1000).default(100),
verbose: z.boolean().default(true),
autoConvert: z.boolean().default(true),
mergeBatches: z.boolean().default(true),
enableDbPersistence: z.boolean().default(true)
})
/**
* 物料校验配置 Schema
*/
export const validationConfigSchema = z.object({
dataSource: validationDataSourceSchema.default('database_full'),
batchSize: z.number().int().min(1).max(10000).default(2000),
matchMode: matchModeSchema.default('substring'),
enableCrud: z.boolean().default(false),
defaultManager: z.string().default('')
})
/**
* 订单号解析配置 Schema
*/
export const orderResolutionSchema = z.object({
tableName: z.string(),
productionIdField: z.string(),
orderNumberField: z.string()
})
/**
* ERP 系统配置 Schema固定基础设施
*/
export const erpSystemConfigSchema = z.object({
url: z.string().url('ERP URL must be a valid URL')
})
/**
* 完整应用配置 Schema
*/
export const fullConfigSchema = z.object({
erp: erpSystemConfigSchema,
database: databaseConfigSchema,
paths: pathsConfigSchema,
extraction: extractionConfigSchema,
validation: validationConfigSchema,
orderResolution: orderResolutionSchema
})
/**
* 类型导出
*/
export type FullConfig = z.infer<typeof fullConfigSchema>
export type DatabaseConfig = z.infer<typeof databaseConfigSchema>
export type MySqlConfig = z.infer<typeof mysqlConfigSchema>
export type SqlServerConfig = z.infer<typeof sqlServerConfigSchema>
export type ErpSystemConfig = z.infer<typeof erpSystemConfigSchema>
/**
* 验证并解析配置
*/
export function validateConfig(input: unknown): {
success: boolean
data?: FullConfig
error?: string
} {
const result = fullConfigSchema.safeParse(input)
if (result.success) {
return { success: true, data: result.data }
}
return {
success: false,
error: result.error.issues
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
.join('; ')
}
}

View File

@@ -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<SaveSettingsResult>
/** Reset to defaults (Admin only) */
resetDefaults: () => Promise<SaveSettingsResult>
/** Test ERP connection */
testErpConnection: () => Promise<ConnectionTestResult>
/** Test database connection */
testDbConnection: () => Promise<ConnectionTestResult>
}