diff --git a/src/main/ipc/auth-handler.ts b/src/main/ipc/auth-handler.ts index bc5fbe4..35a870f 100644 --- a/src/main/ipc/auth-handler.ts +++ b/src/main/ipc/auth-handler.ts @@ -11,9 +11,6 @@ */ import { ipcMain } from 'electron' -import { SessionManager } from '../services/user/session-manager' -import { createLogger } from '../services/logger' -import { logAudit } from '../services/logger/audit-logger' import type { UserInfo } from '../types/user.types' import type { CurrentUserResponse, @@ -23,212 +20,59 @@ import type { UserSelectionResponse } from '../types/auth-ipc.types' import { IPC_CHANNELS } from '../../shared/ipc-channels' -import { ValidationError } from '../types/errors' import { withErrorHandling, type IpcResult } from './index' -import { UpdateService } from '../services/update/update-service' - -const log = createLogger('AuthHandler') +import { AuthApplicationService } from '../services/auth/auth-application-service' /** * Register IPC handlers for user authentication */ export function registerAuthHandlers(): void { - const sessionManager = SessionManager.getInstance() - const updateService = UpdateService.getInstance() + const authService = new AuthApplicationService() - /** - * Get computer name - */ ipcMain.handle(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME, async (): Promise> => { - return withErrorHandling(async () => { - const os = await import('os') - return os.hostname() - }, 'auth:getComputerName') + return withErrorHandling(async () => authService.getComputerName(), 'auth:getComputerName') }) - /** - * Silent login by computer name - */ ipcMain.handle( IPC_CHANNELS.AUTH_SILENT_LOGIN, async (): Promise> => { - return withErrorHandling(async () => { - log.info('Attempting silent login') - const success = await sessionManager.loginByComputerName() - const userInfo = sessionManager.getUserInfo() - - if (success && userInfo) { - await updateService.setUserContext(userInfo.userType) - - // Check if admin needs user selection - const requiresUserSelection = userInfo.userType === 'Admin' - - log.info('Silent login successful', { - username: userInfo.username, - userType: userInfo.userType, - requiresUserSelection - }) - - // Audit log: LOGIN success (non-blocking) - const os = await import('os') - logAudit('LOGIN', String(userInfo.id), { - username: userInfo.username, - computerName: os.hostname(), - resource: 'ERP_SYSTEM', - status: 'success', - metadata: { loginType: 'silent', userType: userInfo.userType } - }).catch((err) => log.warn('Failed to write audit log', { err })) - - return { - success: true, - userInfo, - requiresUserSelection - } - } - - await updateService.setUserContext(null) - throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT') - }, 'auth:silentLogin') + return withErrorHandling(async () => authService.silentLogin(), 'auth:silentLogin') } ) - /** - * Login with username and password - */ ipcMain.handle( IPC_CHANNELS.AUTH_LOGIN, async (_event, request: LoginRequest): Promise> => { - return withErrorHandling(async () => { - const { username, password } = request - - if (!username || !password) { - log.warn('Login attempt with missing credentials') - throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED') - } - - log.info('Login attempt', { username }) - const success = await sessionManager.login(username, password) - const userInfo = sessionManager.getUserInfo() - - if (success && userInfo) { - log.info('Login successful', { username, userType: userInfo.userType }) - await updateService.setUserContext(userInfo.userType) - - // Audit log: LOGIN success (non-blocking) - const os = await import('os') - logAudit('LOGIN', String(userInfo.id), { - username: userInfo.username, - computerName: os.hostname(), - resource: 'ERP_SYSTEM', - status: 'success', - metadata: { loginType: 'credentials', userType: userInfo.userType } - }).catch((err) => log.warn('Failed to write audit log', { err })) - - return { - success: true, - userInfo - } - } - - // Audit log: LOGIN failure (non-blocking) - const os = await import('os') - logAudit('LOGIN', '0', { - username, - computerName: os.hostname(), - resource: 'ERP_SYSTEM', - status: 'failure', - metadata: { loginType: 'credentials', reason: 'invalid_credentials' } - }).catch((err) => log.warn('Failed to write audit log', { err })) - - log.warn('Login failed - invalid credentials', { username }) - await updateService.setUserContext(null) - throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT') - }, 'auth:login') + return withErrorHandling( + async () => authService.login(request.username, request.password), + 'auth:login' + ) } ) - /** - * Logout - */ ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async (): Promise> => { - return withErrorHandling(async () => { - const userInfo = sessionManager.getUserInfo() - log.info('User logout', { username: userInfo?.username }) - - // Audit log: LOGOUT (non-blocking) - if (userInfo) { - const os = await import('os') - logAudit('LOGOUT', String(userInfo.id), { - username: userInfo.username, - computerName: os.hostname(), - resource: 'ERP_SYSTEM', - status: 'success', - metadata: { userType: userInfo.userType } - }).catch((err) => log.warn('Failed to write audit log', { err })) - } - - sessionManager.logout() - await updateService.setUserContext(null) - }, 'auth:logout') + return withErrorHandling(async () => authService.logout(), 'auth:logout') }) - /** - * Get current user - */ ipcMain.handle( IPC_CHANNELS.AUTH_GET_CURRENT_USER, async (): Promise> => { - return withErrorHandling(async () => { - const isAuthenticated = sessionManager.isAuthenticated() - const userInfo = sessionManager.getUserInfo() - return { - isAuthenticated, - userInfo: userInfo ?? undefined - } - }, 'auth:getCurrentUser') + return withErrorHandling(async () => authService.getCurrentUser(), 'auth:getCurrentUser') } ) - /** - * Get all users (for admin user selection) - */ ipcMain.handle(IPC_CHANNELS.AUTH_GET_ALL_USERS, async (): Promise> => { - return withErrorHandling(async () => { - log.debug('Fetching all users for admin selection') - return await sessionManager.getAllUsers() - }, 'auth:getAllUsers') + return withErrorHandling(async () => authService.getAllUsers(), 'auth:getAllUsers') }) - /** - * Switch user (admin only) - */ ipcMain.handle( IPC_CHANNELS.AUTH_SWITCH_USER, async (_event, userInfo: UserInfo): Promise> => { - return withErrorHandling(async () => { - log.info('User switch attempt', { targetUser: userInfo.username }) - const success = sessionManager.switchUser(userInfo) - - if (success) { - const newUser = sessionManager.getUserInfo() - log.info('User switch successful', { newUsername: newUser?.username }) - await updateService.setUserContext(newUser?.userType ?? null) - return { - success: true, - userInfo: newUser ?? undefined - } - } - - log.warn('User switch failed') - throw new ValidationError('用户切换失败', 'VAL_INVALID_INPUT') - }, 'auth:switchUser') + return withErrorHandling(async () => authService.switchUser(userInfo), 'auth:switchUser') } ) - /** - * Check if current user is admin - */ ipcMain.handle(IPC_CHANNELS.AUTH_IS_ADMIN, async (): Promise> => { - return withErrorHandling(async () => sessionManager.isAdmin(), 'auth:isAdmin') + return withErrorHandling(async () => authService.isAdmin(), 'auth:isAdmin') }) } diff --git a/src/main/ipc/cleaner-handler.ts b/src/main/ipc/cleaner-handler.ts index 81778ed..7cf9ad2 100644 --- a/src/main/ipc/cleaner-handler.ts +++ b/src/main/ipc/cleaner-handler.ts @@ -1,368 +1,34 @@ -import { ipcMain, type WebContents } from 'electron' -import { ErpAuthService } from '../services/erp/erp-auth' -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 { RustfsService } from '../services/rustfs' -import { SessionManager } from '../services/user/session-manager' -import { createLogger } from '../services/logger' -import { logAudit } from '../services/logger/audit-logger' +import { ipcMain } from 'electron' import { withErrorHandling, type IpcResult } from './index' -import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' import type { CleanerInput, CleanerResult, - CleanerProgress, ExportResultItem, ExportResultResponse } from '../types/cleaner.types' -import { UserErpConfigService } from '../services/user/user-erp-config-service' import { IPC_CHANNELS } from '../../shared/ipc-channels' - -const log = createLogger('CleanerHandler') - -function sendProgress( - sender: WebContents, - message: string, - progress: number, - extra?: Partial -): void { - try { - const progressData: CleanerProgress = { - message, - progress, - currentOrderIndex: extra?.currentOrderIndex ?? 0, - totalOrders: extra?.totalOrders ?? 0, - currentMaterialIndex: extra?.currentMaterialIndex ?? 0, - totalMaterialsInOrder: extra?.totalMaterialsInOrder ?? 0, - currentOrderNumber: extra?.currentOrderNumber, - phase: extra?.phase ?? 'processing' - } - sender.send(IPC_CHANNELS.CLEANER_PROGRESS, progressData) - } catch (error) { - log.warn('Failed to send progress event', { error }) - } -} - -async function getDatabaseService(): Promise { - const configManager = ConfigManager.getInstance() - const config = configManager.getConfig() - const dbType = configManager.getDatabaseType() - - if (dbType === 'sqlserver') { - const dbConfig = config.database.sqlserver - const sqlServerService = new SqlServerService({ - server: dbConfig.server, - port: dbConfig.port, - user: dbConfig.username, - password: dbConfig.password, - database: dbConfig.database, - options: { - encrypt: false, - trustServerCertificate: dbConfig.trustServerCertificate - } - }) - await sqlServerService.connect() - return sqlServerService - } else { - const dbConfig = config.database.mysql - const mysqlService = new MySqlService({ - host: dbConfig.host, - port: dbConfig.port, - user: dbConfig.username, - password: dbConfig.password, - database: dbConfig.database - }) - await mysqlService.connect() - return mysqlService - } -} - -/** - * 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 -}> { - // Get ERP URL from config.yaml (fixed for all users) - const configManager = ConfigManager.getInstance() - const globalConfig = configManager.getConfig() - const erpUrl = globalConfig.erp.url - - // 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 用户名和密码', - 'VAL_MISSING_REQUIRED' - ) - } - - return { - url: erpUrl, - username: userConfig.username, - password: userConfig.password - } -} +import { CleanerApplicationService } from '../services/cleaner/cleaner-application-service' export function registerCleanerHandlers(): void { + const cleanerService = new CleanerApplicationService() + ipcMain.handle( IPC_CHANNELS.CLEANER_RUN, async (event, input: CleanerInput): Promise> => { - const sender = event.sender - const startTime = Date.now() - - return withErrorHandling(async () => { - let authService: ErpAuthService | null = null - let dbService: MySqlService | SqlServerService | null = null - - try { - // Get ERP configuration from database for current user - log.info('Fetching ERP configuration from database...') - const erpConfig = await getErpConfig() - - log.info('ERP config retrieved', { - url: erpConfig.url ? 'configured' : 'EMPTY', - username: erpConfig.username ? 'configured' : 'EMPTY' - }) - - const configManager = ConfigManager.getInstance() - const dbType = configManager.getDatabaseType() - log.info( - `Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : 'MySQL'} for order resolution...` - ) - - try { - dbService = await getDatabaseService() - } catch (error) { - throw new DatabaseQueryError( - '数据库连接失败', - 'DB_CONNECTION_FAILED', - error instanceof Error ? error : undefined - ) - } - - const resolver = new OrderNumberResolver(dbService) - const mappings = await resolver.resolve(input.orderNumbers) - - const validOrderNumbers = resolver.getValidOrderNumbers(mappings) - const warnings = resolver.getWarnings(mappings) - - if (warnings.length > 0) { - log.warn('Resolution warnings', { warnings }) - } - - if (validOrderNumbers.length === 0) { - throw new ValidationError( - '没有有效的生产订单号可处理。请检查输入的格式或数据库连接。', - 'VAL_INVALID_INPUT' - ) - } - - log.info('Resolved order numbers', { count: validOrderNumbers.length }) - - authService = new ErpAuthService({ - url: erpConfig.url, - username: erpConfig.username, - password: erpConfig.password, - headless: input.headless ?? true - }) - - log.info('Logging in to ERP...') - try { - await authService.login() - } catch (error) { - throw new ErpConnectionError( - 'ERP 登录失败', - 'ERP_LOGIN_FAILED', - error instanceof Error ? error : undefined - ) - } - log.info('Login successful') - - // Send login complete progress - const totalOrders = validOrderNumbers.length - const loginProgress = (1 / (1 + totalOrders)) * 100 - sendProgress(sender, 'ERP 登录成功', loginProgress, { - phase: 'login', - currentOrderIndex: 0, - totalOrders, - currentMaterialIndex: 0, - totalMaterialsInOrder: 0 - }) - - const cleaner = new CleanerService(authService) - - const modifiedInput: CleanerInput = { - ...input, - orderNumbers: validOrderNumbers, - onProgress: (message, progress, extra) => { - sendProgress(sender, message, progress ?? 0, extra) - } - } - - log.info('Starting cleaning', { - orderCount: validOrderNumbers.length, - queryBatchSize: input.queryBatchSize ?? 100, - processConcurrency: input.processConcurrency ?? 1 - }) - const result = await cleaner.clean(modifiedInput) - - if (warnings.length > 0) { - result.errors = [...warnings, ...result.errors] - } - - // Send completion progress - sendProgress(sender, '清理完成', 100, { - phase: 'complete', - currentOrderIndex: totalOrders, - totalOrders, - currentMaterialIndex: 0, - totalMaterialsInOrder: 0 - }) - - log.info('Cleaning completed', { - processedCount: result.ordersProcessed, - errorCount: result.errors.length - }) - - // Audit log: CLEAN (non-blocking) - const os = await import('os') - const currentUser = SessionManager.getInstance().getUserInfo() - if (currentUser) { - const status: 'success' | 'failure' | 'partial' = - result.errors.length > 0 && result.materialsDeleted > 0 - ? 'partial' - : result.errors.length > 0 - ? 'failure' - : 'success' - logAudit('CLEAN', String(currentUser.id), { - username: currentUser.username, - computerName: os.hostname(), - resource: 'MATERIAL_PLAN', - status, - metadata: { - orderCount: validOrderNumbers.length, - dryRun: input.dryRun ?? false, - queryBatchSize: input.queryBatchSize ?? 100, - processConcurrency: input.processConcurrency ?? 1, - materialsDeleted: result.materialsDeleted, - materialsSkipped: result.materialsSkipped, - errorCount: result.errors.length - } - }).catch((err) => log.warn('Failed to write audit log', { err })) - } - - // Generate report and upload to RustFS (silent, user unaware) - try { - const endTime = Date.now() - const currentUser = SessionManager.getInstance().getUserInfo() - const username = currentUser?.username ?? 'unknown' - - const reportGenerator = new CleanerReportGenerator() - const reportPath = await reportGenerator.generateReport(result, { - dryRun: input.dryRun ?? false, - username, - startTime, - endTime - }) - log.info('Report generated', { path: reportPath }) - - // Upload to RustFS if enabled - const configManager = ConfigManager.getInstance() - const config = configManager.getConfig() - - if (config.rustfs?.enabled && config.rustfs.endpoint) { - try { - const rustfs = new RustfsService({ config: config.rustfs }) - const reportFileName = reportPath.split(/[\\/]/).pop() || 'report.md' - const storageKey = rustfs.generateReportKey(reportFileName, username) - - log.info('Uploading report to RustFS', { - localPath: reportPath, - storageKey - }) - - const uploadResult = await rustfs.uploadFile( - reportPath, - storageKey, - 'text/markdown; charset=utf-8' - ) - - if (uploadResult.success) { - log.info('Report uploaded to RustFS successfully', { - key: storageKey, - etag: uploadResult.etag - }) - } else { - log.warn('Failed to upload report to RustFS', { - error: uploadResult.error, - key: storageKey - }) - } - } catch (rustfsError) { - log.error('RustFS upload failed', { - error: rustfsError instanceof Error ? rustfsError.message : String(rustfsError) - }) - } - } else { - log.debug('RustFS is not enabled, skipping upload') - } - } catch (reportError) { - log.warn('Failed to generate report', { - error: reportError instanceof Error ? reportError.message : String(reportError) - }) - } - - return result - } finally { - if (authService) { - try { - await authService.close() - log.debug('Browser closed') - } catch (closeError) { - log.warn('Error closing browser', { - error: closeError instanceof Error ? closeError.message : String(closeError) - }) - } - } - - if (dbService) { - try { - await dbService.disconnect() - log.debug('Database disconnected') - } catch (closeError) { - log.warn('Error disconnecting database', { - error: closeError instanceof Error ? closeError.message : String(closeError) - }) - } - } - } - }, 'cleaner:run') + return withErrorHandling( + async () => cleanerService.runCleaner(event.sender, input), + 'cleaner:run' + ) } ) ipcMain.handle( IPC_CHANNELS.CLEANER_EXPORT_RESULTS, async (_event, items: ExportResultItem[]): Promise> => { - return withErrorHandling(async () => { - log.info('Exporting validation results', { count: items.length }) - if (!items || items.length === 0) { - throw new ValidationError('没有数据可导出', 'VAL_INVALID_INPUT') - } - const exporter = new ResultExporter() - return await exporter.exportValidationResults(items) - }, 'cleaner:exportResults') + return withErrorHandling( + async () => cleanerService.exportResults(items), + 'cleaner:exportResults' + ) } ) } diff --git a/src/main/services/auth/auth-application-service.ts b/src/main/services/auth/auth-application-service.ts new file mode 100644 index 0000000..edfcc05 --- /dev/null +++ b/src/main/services/auth/auth-application-service.ts @@ -0,0 +1,167 @@ +import { hostname } from 'os' +import { SessionManager } from '../user/session-manager' +import { UpdateService } from '../update/update-service' +import { createLogger } from '../logger' +import { logAudit } from '../logger/audit-logger' +import { ValidationError } from '../../types/errors' +import type { UserInfo } from '../../types/user.types' +import type { + CurrentUserResponse, + LoginResponse, + SilentLoginResponse, + UserSelectionResponse +} from '../../types/auth-ipc.types' + +const log = createLogger('AuthApplicationService') + +export class AuthApplicationService { + constructor( + private readonly sessionManager: SessionManager = SessionManager.getInstance(), + private readonly updateService: UpdateService = UpdateService.getInstance() + ) {} + + async getComputerName(): Promise { + return hostname() + } + + async silentLogin(): Promise { + log.info('Attempting silent login') + const success = await this.sessionManager.loginByComputerName() + const userInfo = this.sessionManager.getUserInfo() + + if (!success || !userInfo) { + await this.updateService.setUserContext(null) + throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT') + } + + await this.updateService.setUserContext(userInfo.userType) + + const requiresUserSelection = userInfo.userType === 'Admin' + log.info('Silent login successful', { + username: userInfo.username, + userType: userInfo.userType, + requiresUserSelection + }) + + this.writeAuditLog('LOGIN', String(userInfo.id), { + username: userInfo.username, + computerName: hostname(), + resource: 'ERP_SYSTEM', + status: 'success', + metadata: { loginType: 'silent', userType: userInfo.userType } + }) + + return { + success: true, + userInfo, + requiresUserSelection + } + } + + async login(username: string, password: string): Promise { + if (!username || !password) { + log.warn('Login attempt with missing credentials') + throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED') + } + + log.info('Login attempt', { username }) + const success = await this.sessionManager.login(username, password) + const userInfo = this.sessionManager.getUserInfo() + + if (!success || !userInfo) { + this.writeAuditLog('LOGIN', '0', { + username, + computerName: hostname(), + resource: 'ERP_SYSTEM', + status: 'failure', + metadata: { loginType: 'credentials', reason: 'invalid_credentials' } + }) + + log.warn('Login failed - invalid credentials', { username }) + await this.updateService.setUserContext(null) + throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT') + } + + log.info('Login successful', { username, userType: userInfo.userType }) + await this.updateService.setUserContext(userInfo.userType) + + this.writeAuditLog('LOGIN', String(userInfo.id), { + username: userInfo.username, + computerName: hostname(), + resource: 'ERP_SYSTEM', + status: 'success', + metadata: { loginType: 'credentials', userType: userInfo.userType } + }) + + return { + success: true, + userInfo + } + } + + async logout(): Promise { + const userInfo = this.sessionManager.getUserInfo() + log.info('User logout', { username: userInfo?.username }) + + if (userInfo) { + this.writeAuditLog('LOGOUT', String(userInfo.id), { + username: userInfo.username, + computerName: hostname(), + resource: 'ERP_SYSTEM', + status: 'success', + metadata: { userType: userInfo.userType } + }) + } + + this.sessionManager.logout() + await this.updateService.setUserContext(null) + } + + getCurrentUser(): CurrentUserResponse { + const isAuthenticated = this.sessionManager.isAuthenticated() + const userInfo = this.sessionManager.getUserInfo() + + return { + isAuthenticated, + userInfo: userInfo ?? undefined + } + } + + async getAllUsers(): Promise { + log.debug('Fetching all users for admin selection') + return this.sessionManager.getAllUsers() + } + + async switchUser(userInfo: UserInfo): Promise { + log.info('User switch attempt', { targetUser: userInfo.username }) + const success = this.sessionManager.switchUser(userInfo) + + if (!success) { + log.warn('User switch failed') + throw new ValidationError('用户切换失败', 'VAL_INVALID_INPUT') + } + + const newUser = this.sessionManager.getUserInfo() + log.info('User switch successful', { newUsername: newUser?.username }) + await this.updateService.setUserContext(newUser?.userType ?? null) + + return { + success: true, + userInfo: newUser ?? undefined + } + } + + isAdmin(): boolean { + return this.sessionManager.isAdmin() + } + + private writeAuditLog( + action: 'LOGIN' | 'LOGOUT', + actorId: string, + payload: Parameters[2] + ): void { + logAudit(action, actorId, payload).catch((err) => + log.warn('Failed to write audit log', { err }) + ) + } +} diff --git a/src/main/services/cleaner/cleaner-application-service.ts b/src/main/services/cleaner/cleaner-application-service.ts new file mode 100644 index 0000000..0feeea0 --- /dev/null +++ b/src/main/services/cleaner/cleaner-application-service.ts @@ -0,0 +1,359 @@ +import type { WebContents } from 'electron' +import type { MySqlService } from '../database/mysql' +import type { SqlServerService } from '../database/sql-server' +import { ErpAuthService } from '../erp/erp-auth' +import { CleanerService } from '../erp/cleaner' +import { OrderNumberResolver } from '../erp/order-resolver' +import { MySqlService as MySqlServiceImpl } from '../database/mysql' +import { SqlServerService as SqlServerServiceImpl } from '../database/sql-server' +import { ConfigManager } from '../config/config-manager' +import { ResultExporter } from '../excel/result-exporter' +import { CleanerReportGenerator } from '../report/cleaner-report-generator' +import { RustfsService } from '../rustfs' +import { SessionManager } from '../user/session-manager' +import { UserErpConfigService } from '../user/user-erp-config-service' +import { createLogger } from '../logger' +import { logAudit } from '../logger/audit-logger' +import { IPC_CHANNELS } from '../../../shared/ipc-channels' +import { DatabaseQueryError, ErpConnectionError, ValidationError } from '../../types/errors' +import type { + CleanerInput, + CleanerProgress, + CleanerResult, + ExportResultItem, + ExportResultResponse +} from '../../types/cleaner.types' + +const log = createLogger('CleanerApplicationService') + +type DatabaseService = MySqlService | SqlServerService + +export class CleanerApplicationService { + async runCleaner(eventSender: WebContents, input: CleanerInput): Promise { + const startTime = Date.now() + let authService: ErpAuthService | null = null + let dbService: DatabaseService | null = null + + try { + log.info('Fetching ERP configuration from database...') + const erpConfig = await this.getErpConfig() + + log.info('ERP config retrieved', { + url: erpConfig.url ? 'configured' : 'EMPTY', + username: erpConfig.username ? 'configured' : 'EMPTY' + }) + + const configManager = ConfigManager.getInstance() + const dbType = configManager.getDatabaseType() + log.info( + `Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : 'MySQL'} for order resolution...` + ) + + try { + dbService = await this.getDatabaseService() + } catch (error) { + throw new DatabaseQueryError( + '数据库连接失败', + 'DB_CONNECTION_FAILED', + error instanceof Error ? error : undefined + ) + } + + const resolver = new OrderNumberResolver(dbService) + const mappings = await resolver.resolve(input.orderNumbers) + const validOrderNumbers = resolver.getValidOrderNumbers(mappings) + const warnings = resolver.getWarnings(mappings) + + if (warnings.length > 0) { + log.warn('Resolution warnings', { warnings }) + } + + if (validOrderNumbers.length === 0) { + throw new ValidationError( + '没有有效的生产订单号可处理。请检查输入的格式或数据库连接。', + 'VAL_INVALID_INPUT' + ) + } + + log.info('Resolved order numbers', { count: validOrderNumbers.length }) + + authService = new ErpAuthService({ + url: erpConfig.url, + username: erpConfig.username, + password: erpConfig.password, + headless: input.headless ?? true + }) + + log.info('Logging in to ERP...') + try { + await authService.login() + } catch (error) { + throw new ErpConnectionError( + 'ERP 登录失败', + 'ERP_LOGIN_FAILED', + error instanceof Error ? error : undefined + ) + } + log.info('Login successful') + + const totalOrders = validOrderNumbers.length + this.sendProgress(eventSender, 'ERP 登录成功', (1 / (1 + totalOrders)) * 100, { + phase: 'login', + currentOrderIndex: 0, + totalOrders, + currentMaterialIndex: 0, + totalMaterialsInOrder: 0 + }) + + const cleaner = new CleanerService(authService) + const modifiedInput: CleanerInput = { + ...input, + orderNumbers: validOrderNumbers, + onProgress: (message, progress, extra) => { + this.sendProgress(eventSender, message, progress ?? 0, extra) + } + } + + log.info('Starting cleaning', { + orderCount: validOrderNumbers.length, + queryBatchSize: input.queryBatchSize ?? 100, + processConcurrency: input.processConcurrency ?? 1 + }) + + const result = await cleaner.clean(modifiedInput) + + if (warnings.length > 0) { + result.errors = [...warnings, ...result.errors] + } + + this.sendProgress(eventSender, '清理完成', 100, { + phase: 'complete', + currentOrderIndex: totalOrders, + totalOrders, + currentMaterialIndex: 0, + totalMaterialsInOrder: 0 + }) + + log.info('Cleaning completed', { + processedCount: result.ordersProcessed, + errorCount: result.errors.length + }) + + await this.recordCleanupAudit(validOrderNumbers.length, input, result) + await this.generateAndUploadReport(input, result, startTime) + + return result + } finally { + if (authService) { + try { + await authService.close() + log.debug('Browser closed') + } catch (closeError) { + log.warn('Error closing browser', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) + } + } + + if (dbService) { + try { + await dbService.disconnect() + log.debug('Database disconnected') + } catch (closeError) { + log.warn('Error disconnecting database', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) + } + } + } + } + + async exportResults(items: ExportResultItem[]): Promise { + log.info('Exporting validation results', { count: items.length }) + if (!items || items.length === 0) { + throw new ValidationError('没有数据可导出', 'VAL_INVALID_INPUT') + } + + const exporter = new ResultExporter() + return exporter.exportValidationResults(items) + } + + private sendProgress( + sender: WebContents, + message: string, + progress: number, + extra?: Partial + ): void { + try { + const progressData: CleanerProgress = { + message, + progress, + currentOrderIndex: extra?.currentOrderIndex ?? 0, + totalOrders: extra?.totalOrders ?? 0, + currentMaterialIndex: extra?.currentMaterialIndex ?? 0, + totalMaterialsInOrder: extra?.totalMaterialsInOrder ?? 0, + currentOrderNumber: extra?.currentOrderNumber, + phase: extra?.phase ?? 'processing' + } + sender.send(IPC_CHANNELS.CLEANER_PROGRESS, progressData) + } catch (error) { + log.warn('Failed to send progress event', { error }) + } + } + + private async getDatabaseService(): Promise { + const configManager = ConfigManager.getInstance() + const config = configManager.getConfig() + const dbType = configManager.getDatabaseType() + + if (dbType === 'sqlserver') { + const dbConfig = config.database.sqlserver + const sqlServerService = new SqlServerServiceImpl({ + server: dbConfig.server, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, + options: { + encrypt: false, + trustServerCertificate: dbConfig.trustServerCertificate + } + }) + await sqlServerService.connect() + return sqlServerService + } + + const dbConfig = config.database.mysql + const mysqlService = new MySqlServiceImpl({ + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database + }) + await mysqlService.connect() + return mysqlService + } + + private async getErpConfig(): Promise<{ url: string; username: string; password: string }> { + const configManager = ConfigManager.getInstance() + const globalConfig = configManager.getConfig() + const erpUrl = globalConfig.erp.url + + const erpConfigService = UserErpConfigService.getInstance() + const userConfig = await erpConfigService.getCurrentUserErpConfig() + + if (!userConfig || !userConfig.username || !userConfig.password) { + throw new ValidationError( + 'ERP 配置不完整。请在设置中配置 ERP 用户名和密码', + 'VAL_MISSING_REQUIRED' + ) + } + + return { + url: erpUrl, + username: userConfig.username, + password: userConfig.password + } + } + + private async recordCleanupAudit( + orderCount: number, + input: CleanerInput, + result: CleanerResult + ): Promise { + const currentUser = SessionManager.getInstance().getUserInfo() + if (!currentUser) { + return + } + + const status: 'success' | 'failure' | 'partial' = + result.errors.length > 0 && result.materialsDeleted > 0 + ? 'partial' + : result.errors.length > 0 + ? 'failure' + : 'success' + + await logAudit('CLEAN', String(currentUser.id), { + username: currentUser.username, + computerName: (await import('os')).hostname(), + resource: 'MATERIAL_PLAN', + status, + metadata: { + orderCount, + dryRun: input.dryRun ?? false, + queryBatchSize: input.queryBatchSize ?? 100, + processConcurrency: input.processConcurrency ?? 1, + materialsDeleted: result.materialsDeleted, + materialsSkipped: result.materialsSkipped, + errorCount: result.errors.length + } + }).catch((err) => log.warn('Failed to write audit log', { err })) + } + + private async generateAndUploadReport( + input: CleanerInput, + result: CleanerResult, + startTime: number + ): Promise { + try { + const endTime = Date.now() + const currentUser = SessionManager.getInstance().getUserInfo() + const username = currentUser?.username ?? 'unknown' + + const reportGenerator = new CleanerReportGenerator() + const reportPath = await reportGenerator.generateReport(result, { + dryRun: input.dryRun ?? false, + username, + startTime, + endTime + }) + log.info('Report generated', { path: reportPath }) + + const configManager = ConfigManager.getInstance() + const config = configManager.getConfig() + + if (!config.rustfs?.enabled || !config.rustfs.endpoint) { + log.debug('RustFS is not enabled, skipping upload') + return + } + + try { + const rustfs = new RustfsService({ config: config.rustfs }) + const reportFileName = reportPath.split(/[\\/]/).pop() || 'report.md' + const storageKey = rustfs.generateReportKey(reportFileName, username) + + log.info('Uploading report to RustFS', { + localPath: reportPath, + storageKey + }) + + const uploadResult = await rustfs.uploadFile( + reportPath, + storageKey, + 'text/markdown; charset=utf-8' + ) + + if (uploadResult.success) { + log.info('Report uploaded to RustFS successfully', { + key: storageKey, + etag: uploadResult.etag + }) + } else { + log.warn('Failed to upload report to RustFS', { + error: uploadResult.error, + key: storageKey + }) + } + } catch (rustfsError) { + log.error('RustFS upload failed', { + error: rustfsError instanceof Error ? rustfsError.message : String(rustfsError) + }) + } + } catch (reportError) { + log.warn('Failed to generate report', { + error: reportError instanceof Error ? reportError.message : String(reportError) + }) + } + } +}