diff --git a/src/main/index.ts b/src/main/index.ts index 24374aa..726a714 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -24,7 +24,7 @@ function createWindow(): void { ...(process.platform === 'linux' ? { icon } : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), - sandbox: false + sandbox: true } }) diff --git a/src/main/ipc/auth-handler.ts b/src/main/ipc/auth-handler.ts index f9d5945..bee15aa 100644 --- a/src/main/ipc/auth-handler.ts +++ b/src/main/ipc/auth-handler.ts @@ -14,6 +14,9 @@ import { ipcMain } from 'electron' import { SessionManager } from '../services/user/session-manager' import { createLogger } from '../services/logger' import type { UserInfo } from '../types/user.types' +import { IPC_CHANNELS } from '../../shared/ipc-channels' +import { ValidationError } from '../types/errors' +import { withErrorHandling, type IpcResult } from './index' const log = createLogger('AuthHandler') @@ -70,16 +73,18 @@ export function registerAuthHandlers(): void { /** * Get computer name */ - ipcMain.handle('auth:getComputerName', async (): Promise => { - const os = await import('os') - return os.hostname() + ipcMain.handle(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME, async (): Promise> => { + return withErrorHandling(async () => { + const os = await import('os') + return os.hostname() + }, 'auth:getComputerName') }) /** * Silent login by computer name */ - ipcMain.handle('auth:silentLogin', async (): Promise => { - try { + 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() @@ -101,34 +106,22 @@ export function registerAuthHandlers(): void { } } - log.warn('Silent login failed - no matching user') - return { - success: false, - requiresUserSelection: false - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Silent login error', { error: message }) - return { - success: false, - error: `无感登录失败:${message}` - } - } + throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT') + }, 'auth:silentLogin') }) /** * Login with username and password */ - ipcMain.handle('auth:login', async (_event, request: LoginRequest): Promise => { - try { + 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') - return { - success: false, - error: '请输入用户名和密码' - } + throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED') } log.info('Login attempt', { username }) @@ -144,57 +137,53 @@ export function registerAuthHandlers(): void { } log.warn('Login failed - invalid credentials', { username }) - return { - success: false, - error: '用户名或密码错误' - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Login error', { error: message }) - return { - success: false, - error: `登录失败:${message}` - } + throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT') + }, 'auth:login') } - }) + ) /** * Logout */ - ipcMain.handle('auth:logout', async (): Promise => { - const userInfo = sessionManager.getUserInfo() - log.info('User logout', { username: userInfo?.username }) - sessionManager.logout() + ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async (): Promise> => { + return withErrorHandling(async () => { + const userInfo = sessionManager.getUserInfo() + log.info('User logout', { username: userInfo?.username }) + sessionManager.logout() + }, 'auth:logout') }) /** * Get current user */ - ipcMain.handle('auth:getCurrentUser', async (): Promise => { - const isAuthenticated = sessionManager.isAuthenticated() - const userInfo = sessionManager.getUserInfo() - - return { - isAuthenticated, - userInfo: userInfo ?? undefined - } + 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') }) /** * Get all users (for admin user selection) */ - ipcMain.handle('auth:getAllUsers', async (): Promise => { - log.debug('Fetching all users for admin selection') - return await sessionManager.getAllUsers() + 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') }) /** * Switch user (admin only) */ ipcMain.handle( - 'auth:switchUser', - async (_event, userInfo: UserInfo): Promise => { - try { + 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) @@ -208,25 +197,16 @@ export function registerAuthHandlers(): void { } log.warn('User switch failed') - return { - success: false, - error: '用户切换失败' - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('User switch error', { error: message }) - return { - success: false, - error: `用户切换失败:${message}` - } - } + throw new ValidationError('用户切换失败', 'VAL_INVALID_INPUT') + }, 'auth:switchUser') } ) /** * Check if current user is admin */ - ipcMain.handle('auth:isAdmin', async (): Promise => { - return sessionManager.isAdmin() + ipcMain.handle(IPC_CHANNELS.AUTH_IS_ADMIN, async (): Promise> => { + return withErrorHandling(async () => sessionManager.isAdmin(), 'auth:isAdmin') }) } + diff --git a/src/main/ipc/cleaner-handler.ts b/src/main/ipc/cleaner-handler.ts index df9f19b..a04db2f 100644 --- a/src/main/ipc/cleaner-handler.ts +++ b/src/main/ipc/cleaner-handler.ts @@ -1,4 +1,4 @@ -import { ipcMain, webContents } from 'electron' +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' @@ -19,11 +19,12 @@ import type { 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( - windowId: number, + sender: WebContents, message: string, progress: number, extra?: Partial @@ -39,9 +40,7 @@ function sendProgress( currentOrderNumber: extra?.currentOrderNumber, phase: extra?.phase ?? 'processing' } - webContents.getAllWebContents().forEach((wc) => { - wc.send('cleaner:progress', progressData) - }) + sender.send(IPC_CHANNELS.CLEANER_PROGRESS, progressData) } catch (error) { log.warn('Failed to send progress event', { error }) } @@ -116,9 +115,9 @@ async function getErpConfig(): Promise<{ export function registerCleanerHandlers(): void { ipcMain.handle( - 'cleaner:run', + IPC_CHANNELS.CLEANER_RUN, async (event, input: CleanerInput): Promise> => { - const windowId = event.sender.id + const sender = event.sender const startTime = Date.now() return withErrorHandling(async () => { @@ -192,7 +191,7 @@ export function registerCleanerHandlers(): void { // Send login complete progress const totalOrders = validOrderNumbers.length const loginProgress = (1 / (1 + totalOrders)) * 100 - sendProgress(windowId, 'ERP 登录成功', loginProgress, { + sendProgress(sender, 'ERP 登录成功', loginProgress, { phase: 'login', currentOrderIndex: 0, totalOrders, @@ -206,7 +205,7 @@ export function registerCleanerHandlers(): void { ...input, orderNumbers: validOrderNumbers, onProgress: (message, progress, extra) => { - sendProgress(windowId, message, progress ?? 0, extra) + sendProgress(sender, message, progress ?? 0, extra) } } @@ -218,7 +217,7 @@ export function registerCleanerHandlers(): void { } // Send completion progress - sendProgress(windowId, '清理完成', 100, { + sendProgress(sender, '清理完成', 100, { phase: 'complete', currentOrderIndex: totalOrders, totalOrders, @@ -280,30 +279,16 @@ export function registerCleanerHandlers(): void { ) ipcMain.handle( - 'cleaner:exportResults', - async (_event, items: ExportResultItem[]): Promise => { - try { + 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) { - return { - success: false, - error: '没有数据可导出' - } + throw new ValidationError('没有数据可导出', 'VAL_INVALID_INPUT') } - const exporter = new ResultExporter() - const result = await exporter.exportValidationResults(items) - - return result - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - log.error('Export handler failed', { error: errorMessage }) - return { - success: false, - error: errorMessage - } - } + return await exporter.exportValidationResults(items) + }, 'cleaner:exportResults') } ) } diff --git a/src/main/ipc/database-handler.ts b/src/main/ipc/database-handler.ts index a4276f4..041612b 100644 --- a/src/main/ipc/database-handler.ts +++ b/src/main/ipc/database-handler.ts @@ -2,13 +2,15 @@ import { ipcMain } from 'electron' import { MySqlService } from '../services/database/mysql' import { SqlServerService } from '../services/database/sql-server' import { createLogger } from '../services/logger' -import { DatabaseQueryError, ValidationError } from '../types/errors' +import { ValidationError } from '../types/errors' import type { MySqlConfig, MySqlQueryResult, SqlServerConfig, SqlServerQueryResult } from '../types/ipc-api.types' +import { IPC_CHANNELS } from '../../shared/ipc-channels' +import { withErrorHandling, type IpcResult } from './index' const log = createLogger('DatabaseHandler') @@ -17,6 +19,34 @@ const mysqlServices = new Map() // Store SQL Server service instances per window/connection const sqlServerServices = new Map() +const cleanupBoundWindows = new Set() + +function bindWindowCleanup(windowId: string, sender: { once: (event: string, listener: () => void) => void }): void { + if (cleanupBoundWindows.has(windowId)) { + return + } + + sender.once('destroyed', () => { + const mysql = getMySqlService(windowId) + const sqlServer = getSqlServerService(windowId) + + if (mysql) { + mysql.disconnect().catch((error) => log.warn('MySQL disconnect on window destroy failed', { error })) + deleteMySqlService(windowId) + } + + if (sqlServer) { + sqlServer + .disconnect() + .catch((error) => log.warn('SQL Server disconnect on window destroy failed', { error })) + deleteSqlServerService(windowId) + } + + cleanupBoundWindows.delete(windowId) + }) + + cleanupBoundWindows.add(windowId) +} /** * Get or create MySQL service for a connection ID @@ -65,29 +95,25 @@ function deleteSqlServerService(connectionId: string): void { */ export function registerDatabaseHandlers(): void { // Connect to MySQL - ipcMain.handle('database:mysql:connect', async (event, config: MySqlConfig): Promise => { - try { + ipcMain.handle( + IPC_CHANNELS.DATABASE_MYSQL_CONNECT, + async (event, config: MySqlConfig): Promise> => { + return withErrorHandling(async () => { // Use window ID as connection identifier const windowId = (event.sender as { id: number }).id.toString() + bindWindowCleanup(windowId, event.sender as { once: (event: string, listener: () => void) => void }) log.info('Connecting to MySQL', { windowId }) const service = new MySqlService(config) await service.connect() setMySqlService(windowId, service) log.info('MySQL connected', { windowId }) - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to connect to MySQL' - log.error('MySQL connection failed', { error: message }) - throw new DatabaseQueryError( - message, - 'DB_CONNECTION_FAILED', - error instanceof Error ? error : undefined - ) + }, 'database:mysql:connect') } - }) + ) // Disconnect from MySQL - ipcMain.handle('database:mysql:disconnect', async (event): Promise => { - try { + ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT, async (event): Promise> => { + return withErrorHandling(async () => { const windowId = (event.sender as { id: number }).id.toString() const service = getMySqlService(windowId) if (service) { @@ -95,29 +121,23 @@ export function registerDatabaseHandlers(): void { deleteMySqlService(windowId) log.info('MySQL disconnected', { windowId }) } - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to disconnect from MySQL' - log.error('MySQL disconnect failed', { error: message }) - throw new DatabaseQueryError( - message, - 'DB_CONNECTION_FAILED', - error instanceof Error ? error : undefined - ) - } + }, 'database:mysql:disconnect') }) // Check if MySQL is connected - ipcMain.handle('database:mysql:isConnected', async (event): Promise => { - const windowId = (event.sender as { id: number }).id.toString() - const service = getMySqlService(windowId) - return service ? service.isConnected() : false + ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_IS_CONNECTED, async (event): Promise> => { + return withErrorHandling(async () => { + const windowId = (event.sender as { id: number }).id.toString() + const service = getMySqlService(windowId) + return service ? service.isConnected() : false + }, 'database:mysql:isConnected') }) // Execute MySQL query ipcMain.handle( - 'database:mysql:query', - async (event, sql: string, params?: unknown[]): Promise => { - try { + IPC_CHANNELS.DATABASE_MYSQL_QUERY, + async (event, sql: string, params?: unknown[]): Promise> => { + return withErrorHandling(async () => { const windowId = (event.sender as { id: number }).id.toString() const service = getMySqlService(windowId) @@ -130,44 +150,29 @@ export function registerDatabaseHandlers(): void { log.debug('Executing MySQL query', { windowId, sql: sql.substring(0, 100) }) return await service.query(sql, params) - } catch (error) { - const message = error instanceof Error ? error.message : 'MySQL query failed' - log.error('MySQL query failed', { error: message }) - throw new DatabaseQueryError( - message, - 'DB_QUERY_FAILED', - error instanceof Error ? error : undefined - ) - } + }, 'database:mysql:query') } ) // Connect to SQL Server ipcMain.handle( - 'database:sqlserver:connect', - async (event, config: SqlServerConfig): Promise => { - try { + IPC_CHANNELS.DATABASE_SQLSERVER_CONNECT, + async (event, config: SqlServerConfig): Promise> => { + return withErrorHandling(async () => { const windowId = (event.sender as { id: number }).id.toString() + bindWindowCleanup(windowId, event.sender as { once: (event: string, listener: () => void) => void }) log.info('Connecting to SQL Server', { windowId }) const service = new SqlServerService(config) await service.connect() setSqlServerService(windowId, service) log.info('SQL Server connected', { windowId }) - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to connect to SQL Server' - log.error('SQL Server connection failed', { error: message }) - throw new DatabaseQueryError( - message, - 'DB_CONNECTION_FAILED', - error instanceof Error ? error : undefined - ) - } + }, 'database:sqlserver:connect') } ) // Disconnect from SQL Server - ipcMain.handle('database:sqlserver:disconnect', async (event): Promise => { - try { + ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT, async (event): Promise> => { + return withErrorHandling(async () => { const windowId = (event.sender as { id: number }).id.toString() const service = getSqlServerService(windowId) if (service) { @@ -175,34 +180,27 @@ export function registerDatabaseHandlers(): void { deleteSqlServerService(windowId) log.info('SQL Server disconnected', { windowId }) } - } catch (error) { - const message = - error instanceof Error ? error.message : 'Failed to disconnect from SQL Server' - log.error('SQL Server disconnect failed', { error: message }) - throw new DatabaseQueryError( - message, - 'DB_CONNECTION_FAILED', - error instanceof Error ? error : undefined - ) - } + }, 'database:sqlserver:disconnect') }) // Check if SQL Server is connected - ipcMain.handle('database:sqlserver:isConnected', async (event): Promise => { - const windowId = (event.sender as { id: number }).id.toString() - const service = getSqlServerService(windowId) - return service ? service.isConnected() : false + ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_IS_CONNECTED, async (event): Promise> => { + return withErrorHandling(async () => { + const windowId = (event.sender as { id: number }).id.toString() + const service = getSqlServerService(windowId) + return service ? service.isConnected() : false + }, 'database:sqlserver:isConnected') }) // Execute SQL Server query ipcMain.handle( - 'database:sqlserver:query', + IPC_CHANNELS.DATABASE_SQLSERVER_QUERY, async ( event, sqlString: string, params?: Record - ): Promise => { - try { + ): Promise> => { + return withErrorHandling(async () => { const windowId = (event.sender as { id: number }).id.toString() const service = getSqlServerService(windowId) @@ -226,15 +224,7 @@ export function registerDatabaseHandlers(): void { } else { return await service.query(sqlString) } - } catch (error) { - const message = error instanceof Error ? error.message : 'SQL Server query failed' - log.error('SQL Server query failed', { error: message }) - throw new DatabaseQueryError( - message, - 'DB_QUERY_FAILED', - error instanceof Error ? error : undefined - ) - } + }, 'database:sqlserver:query') } ) } diff --git a/src/main/ipc/extractor-handler.ts b/src/main/ipc/extractor-handler.ts index 050946f..ee9c8f6 100644 --- a/src/main/ipc/extractor-handler.ts +++ b/src/main/ipc/extractor-handler.ts @@ -1,4 +1,4 @@ -import { ipcMain, webContents } from 'electron' +import { ipcMain, type WebContents } from 'electron' import { ErpAuthService } from '../services/erp/erp-auth' import { ExtractorService } from '../services/erp/extractor' import { OrderNumberResolver } from '../services/erp/order-resolver' @@ -9,30 +9,27 @@ import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../type 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' +import { IPC_CHANNELS } from '../../shared/ipc-channels' const log = createLogger('ExtractorHandler') function sendProgress( - windowId: number, + sender: WebContents, message: string, progress: number, extra?: Partial ): void { try { const progressData = { message, progress, ...extra } - webContents.getAllWebContents().forEach((wc) => { - wc.send('extractor:progress', progressData) - }) + sender.send(IPC_CHANNELS.EXTRACTOR_PROGRESS, progressData) } catch (error) { log.warn('Failed to send progress event', { error }) } } -function sendLog(windowId: number, level: string, message: string): void { +function sendLog(sender: WebContents, level: string, message: string): void { try { - webContents.getAllWebContents().forEach((wc) => { - wc.send('extractor:log', { level, message }) - }) + sender.send(IPC_CHANNELS.EXTRACTOR_LOG, { level, message }) } catch (error) { log.warn('Failed to send log event', { error }) } @@ -76,14 +73,13 @@ async function getErpConfig(): Promise<{ */ export function registerExtractorHandlers(): void { ipcMain.handle( - 'extractor:run', + IPC_CHANNELS.EXTRACTOR_RUN, async (event, input: ExtractorInput): Promise> => { - const windowId = event.sender.id + const sender = event.sender return withErrorHandling(async () => { let authService: ErpAuthService | null = null let dbService: IDatabaseService | null = null - let erpConfigService: UserErpConfigService | null = null try { // Get ERP configuration from database for current user @@ -97,11 +93,11 @@ export function registerExtractorHandlers(): void { // Create database service using factory log.info('Connecting to database for order resolution...') - sendProgress(windowId, '连接数据库...', 3.33, { + sendProgress(sender, '连接数据库...', 3.33, { phase: 'login', subProgress: { step: '连接数据库', current: 1, total: 3 } }) - sendLog(windowId, 'system', '正在连接数据库...') + sendLog(sender, 'system', '正在连接数据库...') try { dbService = await create() @@ -114,11 +110,11 @@ export function registerExtractorHandlers(): void { } // Resolve order numbers (convert productionIDs to 生产订单号) - sendProgress(windowId, '解析订单号...', 6.67, { + sendProgress(sender, '解析订单号...', 6.67, { phase: 'login', subProgress: { step: '解析订单号', current: 2, total: 3 } }) - sendLog(windowId, 'info', '正在解析订单号...') + sendLog(sender, 'info', '正在解析订单号...') const resolver = new OrderNumberResolver(dbService) const mappings = await resolver.resolve(input.orderNumbers) @@ -139,7 +135,7 @@ export function registerExtractorHandlers(): void { } log.info('Resolved order numbers', { count: validOrderNumbers.length }) - sendLog(windowId, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`) + sendLog(sender, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`) // Create auth service and login authService = new ErpAuthService({ @@ -149,18 +145,18 @@ export function registerExtractorHandlers(): void { headless: true }) - sendProgress(windowId, '登录 ERP 系统...', 9.99, { + sendProgress(sender, '登录 ERP 系统...', 9.99, { phase: 'login', subProgress: { step: '登录 ERP 系统', current: 3, total: 3 } }) - sendLog(windowId, 'system', '正在登录 ERP 系统...') + sendLog(sender, 'system', '正在登录 ERP 系统...') log.info('Logging in to ERP...') try { await authService.login() } catch (error) { const errorMsg = error instanceof Error ? error.message : '未知错误' - sendLog(windowId, 'error', `ERP 登录失败:${errorMsg}`) + sendLog(sender, 'error', `ERP 登录失败:${errorMsg}`) throw new ErpConnectionError( 'ERP 登录失败', 'ERP_LOGIN_FAILED', @@ -168,7 +164,7 @@ export function registerExtractorHandlers(): void { ) } log.info('Login successful') - sendLog(windowId, 'success', 'ERP 登录成功') + sendLog(sender, 'success', 'ERP 登录成功') // Create extractor service and run extraction with resolved order numbers const extractor = new ExtractorService(authService) @@ -178,11 +174,11 @@ export function registerExtractorHandlers(): void { ...input, orderNumbers: validOrderNumbers, onProgress: (message, progress, extra) => { - sendProgress(windowId, message, progress, extra) - sendLog(windowId, 'info', message) + sendProgress(sender, message, progress, extra) + sendLog(sender, 'info', message) }, onLog: (level, message) => { - sendLog(windowId, level, message) + sendLog(sender, level, message) } } diff --git a/src/main/ipc/file-handler.ts b/src/main/ipc/file-handler.ts index 4afcdff..09bf646 100644 --- a/src/main/ipc/file-handler.ts +++ b/src/main/ipc/file-handler.ts @@ -1,67 +1,87 @@ -import { ipcMain, shell } from 'electron' +import { app, ipcMain, shell } from 'electron' import * as fs from 'fs/promises' import * as path from 'path' import { createLogger } from '../services/logger' +import { IPC_CHANNELS } from '../../shared/ipc-channels' +import { ValidationError } from '../types/errors' +import { withErrorHandling, type IpcResult } from './index' const log = createLogger('FileHandler') +function getAllowedRoots(): string[] { + return [path.resolve(app.getAppPath()), path.resolve(app.getPath('userData'))] +} + +export function isPathWithinAllowedRoots(inputPath: string, roots: string[]): boolean { + const normalized = path.resolve(inputPath) + return roots.some((root) => { + const rel = path.relative(root, normalized) + return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel)) + }) +} + +function normalizeAndValidatePath(inputPath: string): string { + const normalized = path.resolve(inputPath) + const isAllowed = isPathWithinAllowedRoots(normalized, getAllowedRoots()) + if (!isAllowed) { + throw new ValidationError('Path is outside allowed roots', 'VAL_INVALID_INPUT') + } + + return normalized +} + export function registerFileHandlers(): void { - ipcMain.handle('file:read', async (_event, filePath: string): Promise => { - try { - log.debug('Reading file', { filePath }) - return await fs.readFile(filePath, 'utf-8') - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to read file' - log.error('Failed to read file', { filePath, error: message }) - throw new Error(message) - } + ipcMain.handle(IPC_CHANNELS.FILE_READ, async (_event, filePath: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(filePath) + log.debug('Reading file', { filePath: safePath }) + return await fs.readFile(safePath, 'utf-8') + }, 'file:read') }) - ipcMain.handle('file:write', async (_event, filePath: string, content: string): Promise => { - try { - log.debug('Writing file', { filePath }) - const dir = path.dirname(filePath) - await fs.mkdir(dir, { recursive: true }) - await fs.writeFile(filePath, content, 'utf-8') - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to write file' - log.error('Failed to write file', { filePath, error: message }) - throw new Error(message) + ipcMain.handle( + IPC_CHANNELS.FILE_WRITE, + async (_event, filePath: string, content: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(filePath) + log.debug('Writing file', { filePath: safePath }) + const dir = path.dirname(safePath) + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(safePath, content, 'utf-8') + }, 'file:write') } + ) + + ipcMain.handle(IPC_CHANNELS.FILE_EXISTS, async (_event, filePath: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(filePath) + try { + await fs.access(safePath) + return true + } catch { + return false + } + }, 'file:exists') }) - ipcMain.handle('file:exists', async (_event, filePath: string): Promise => { - try { - await fs.access(filePath) - return true - } catch { - return false - } - }) - - ipcMain.handle('file:list', async (_event, dirPath: string): Promise => { - try { - log.debug('Listing directory', { dirPath }) - const entries = await fs.readdir(dirPath, { withFileTypes: true }) + ipcMain.handle(IPC_CHANNELS.FILE_LIST, async (_event, dirPath: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(dirPath) + log.debug('Listing directory', { dirPath: safePath }) + const entries = await fs.readdir(safePath, { withFileTypes: true }) return entries .filter((entry) => entry.isFile()) .map((entry) => entry.name) .sort() - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to list directory' - log.error('Failed to list directory', { dirPath, error: message }) - throw new Error(message) - } + }, 'file:list') }) - ipcMain.handle('file:openPath', async (_event, filePath: string): Promise => { - try { - log.debug('Opening path in explorer', { filePath }) - await shell.openPath(filePath) - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to open path' - log.error('Failed to open path', { filePath, error: message }) - throw new Error(message) - } + ipcMain.handle(IPC_CHANNELS.FILE_OPEN_PATH, async (_event, filePath: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(filePath) + log.debug('Opening path in explorer', { filePath: safePath }) + await fs.access(safePath) + await shell.openPath(safePath) + }, 'file:openPath') }) } diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 0155055..a70cabe 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -28,6 +28,14 @@ export interface IpcResult { code?: string } +export function ok(data: T): IpcResult { + return { success: true, data } +} + +export function fail(error: string, code?: string): IpcResult { + return { success: false, error, code } +} + /** * Higher-order function to wrap IPC handlers with consistent error handling * @param handler - The async handler function to wrap @@ -39,9 +47,9 @@ export function withErrorHandling( context: string ): Promise> { return handler() - .then((data) => { + .then((data): IpcResult => { log.debug(`[${context}] Handler completed successfully`) - return { success: true, data } + return ok(data) }) .catch((error: unknown) => { const message = getErrorMessage(error) @@ -58,7 +66,7 @@ export function withErrorHandling( log.debug(`[${context}] Stack trace:`, { stack: error.stack }) } - return { success: false, error: message, code } + return fail(message, code) }) } diff --git a/src/main/ipc/material-type-handler.ts b/src/main/ipc/material-type-handler.ts index 933e78c..44a9d59 100644 --- a/src/main/ipc/material-type-handler.ts +++ b/src/main/ipc/material-type-handler.ts @@ -17,6 +17,9 @@ import { type MaterialTypeBatchRequest } from '../services/database/materials-type-to-be-deleted-dao' import { createLogger } from '../services/logger' +import { IPC_CHANNELS } from '../../shared/ipc-channels' +import { ValidationError } from '../types/errors' +import { withErrorHandling, type IpcResult } from './index' const log = createLogger('MaterialTypeHandler') @@ -30,16 +33,12 @@ export function registerMaterialTypeHandlers(): void { * Get all material type records */ ipcMain.handle( - 'materialType:getAll', - async (): Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> => { - try { + IPC_CHANNELS.MATERIAL_TYPE_GET_ALL, + async (): Promise> => { + return withErrorHandling(async () => { const records = await dao.getAllMaterials() - return { success: true, data: records } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Get all material types error', { error: message }) - return { success: false, error: message } - } + return records + }, 'materialType:getAll') } ) @@ -47,19 +46,15 @@ export function registerMaterialTypeHandlers(): void { * Get material types by manager */ ipcMain.handle( - 'materialType:getByManager', + IPC_CHANNELS.MATERIAL_TYPE_GET_BY_MANAGER, async ( _event, managerName: string - ): Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> => { - try { + ): Promise> => { + return withErrorHandling(async () => { const records = await dao.getMaterialsByManager(managerName) - return { success: true, data: records } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Get material types by manager error', { error: message }) - return { success: false, error: message } - } + return records + }, 'materialType:getByManager') } ) @@ -67,16 +62,12 @@ export function registerMaterialTypeHandlers(): void { * Get list of managers */ ipcMain.handle( - 'materialType:getManagers', - async (): Promise<{ success: boolean; data?: string[]; error?: string }> => { - try { + IPC_CHANNELS.MATERIAL_TYPE_GET_MANAGERS, + async (): Promise> => { + return withErrorHandling(async () => { const managers = await dao.getManagers() - return { success: true, data: managers } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Get managers error', { error: message }) - return { success: false, error: message } - } + return managers + }, 'materialType:getManagers') } ) @@ -84,22 +75,18 @@ export function registerMaterialTypeHandlers(): void { * Upsert (insert or update) a material type record */ ipcMain.handle( - 'materialType:upsert', + IPC_CHANNELS.MATERIAL_TYPE_UPSERT, async ( _event, { materialName, managerName }: { materialName: string; managerName: string } - ): Promise<{ success: boolean; error?: string }> => { - try { + ): Promise> => { + return withErrorHandling(async () => { const result = await dao.upsertMaterial(materialName, managerName) if (!result) { - return { success: false, error: 'Failed to upsert material type' } + throw new ValidationError('Failed to upsert material type', 'VAL_INVALID_INPUT') } - return { success: true } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Upsert material type error', { error: message }) - return { success: false, error: message } - } + return { updated: true } + }, 'materialType:upsert') } ) @@ -107,22 +94,18 @@ export function registerMaterialTypeHandlers(): void { * Delete a material type record */ ipcMain.handle( - 'materialType:delete', + IPC_CHANNELS.MATERIAL_TYPE_DELETE, async ( _event, { materialName, managerName }: { materialName: string; managerName: string } - ): Promise<{ success: boolean; error?: string }> => { - try { + ): Promise> => { + return withErrorHandling(async () => { const result = await dao.deleteMaterial(materialName, managerName) if (!result) { - return { success: false, error: 'Failed to delete material type' } + throw new ValidationError('Failed to delete material type', 'VAL_INVALID_INPUT') } - return { success: true } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Delete material type error', { error: message }) - return { success: false, error: message } - } + return { deleted: true } + }, 'materialType:delete') } ) @@ -130,25 +113,18 @@ export function registerMaterialTypeHandlers(): void { * Batch operation for material types (insert, update, delete) */ ipcMain.handle( - 'materialType:upsertBatch', + IPC_CHANNELS.MATERIAL_TYPE_UPSERT_BATCH, async ( _event, request: MaterialTypeBatchRequest - ): Promise<{ - success: boolean - stats?: { total: number; success: number; failed: number } - error?: string - }> => { - try { + ): Promise> => { + return withErrorHandling(async () => { const stats = await dao.upsertBatch(request) - return { success: true, stats } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Batch upsert material types error', { error: message }) - return { success: false, error: message } - } + return { stats } + }, 'materialType:upsertBatch') } ) log.info('Material type handlers registered') } + diff --git a/src/main/ipc/resolver-handler.ts b/src/main/ipc/resolver-handler.ts index 6255d99..367b4b9 100644 --- a/src/main/ipc/resolver-handler.ts +++ b/src/main/ipc/resolver-handler.ts @@ -11,6 +11,8 @@ import { create, type IDatabaseService } from '../services/database' import { OrderNumberResolver } from '../services/erp/order-resolver' import { createLogger } from '../services/logger' import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver' +import { IPC_CHANNELS } from '../../shared/ipc-channels' +import { withErrorHandling, type IpcResult } from './index' const log = createLogger('ResolverHandler') @@ -49,11 +51,11 @@ export function registerResolverHandlers(): void { * Converts productionIDs and 生产订单号 to production order numbers */ ipcMain.handle( - 'resolver:resolve', - async (_event, input: ResolverInput): Promise => { + IPC_CHANNELS.RESOLVER_RESOLVE, + async (_event, input: ResolverInput): Promise> => { let dbService: IDatabaseService | null = null - try { + return withErrorHandling(async () => { // Create database service using factory log.info('Connecting to database for resolution', { inputCount: input.inputs.length }) dbService = await create() @@ -80,14 +82,7 @@ export function registerResolverHandlers(): void { warnings, stats } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Resolution failed', { error: message }) - return { - success: false, - error: `解析失败:${message}` - } - } finally { + }, 'resolver:resolve').finally(async () => { // Clean up database connection if (dbService) { try { @@ -99,7 +94,7 @@ export function registerResolverHandlers(): void { }) } } - } + }) } ) @@ -107,16 +102,12 @@ export function registerResolverHandlers(): void { * Validate input format only (without database lookup) */ ipcMain.handle( - 'resolver:validateFormat', + IPC_CHANNELS.RESOLVER_VALIDATE_FORMAT, async ( _event, inputs: string[] - ): Promise<{ - success: boolean - results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }> - error?: string - }> => { - try { + ): Promise>> => { + return withErrorHandling(async () => { // Create a mock resolver without database connection const resolver = new OrderNumberResolver({ isConnected: () => false, @@ -130,15 +121,8 @@ export function registerResolverHandlers(): void { log.debug('Format validation completed', { inputCount: inputs.length }) - return { success: true, results } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Format validation failed', { error: message }) - return { - success: false, - error: `验证失败:${message}` - } - } + return results + }, 'resolver:validateFormat') } ) } diff --git a/src/main/ipc/settings-handler.ts b/src/main/ipc/settings-handler.ts index abd7402..cc3576b 100644 --- a/src/main/ipc/settings-handler.ts +++ b/src/main/ipc/settings-handler.ts @@ -1,16 +1,3 @@ -/** - * Settings IPC Handler - * - * Provides IPC handlers for settings management: - * - 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' @@ -19,156 +6,129 @@ import { MySqlService } from '../services/database/mysql' import { SqlServerService } from '../services/database/sql-server' import { createLogger } from '../services/logger' import type { UserType, ConnectionTestResult, SaveSettingsResult } from '../types/settings.types' +import { IPC_CHANNELS } from '../../shared/ipc-channels' +import { ValidationError } from '../types/errors' +import { withErrorHandling, type IpcResult } from './index' const log = createLogger('SettingsHandler') -/** - * Register IPC handlers for settings management - */ +type ErpSettingsPayload = { + erp?: { + username?: string + password?: string + } +} + export function registerSettingsHandlers(): void { const configManager = ConfigManager.getInstance() const sessionManager = SessionManager.getInstance() const erpConfigService = UserErpConfigService.getInstance() - /** - * Get current user type - */ - ipcMain.handle('settings:getUserType', async (): Promise => { - return (sessionManager.getUserType() as UserType) || 'Guest' + ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_USER_TYPE, async (): Promise> => { + return withErrorHandling( + async () => (sessionManager.getUserType() as UserType) || 'Guest', + 'settings:getUserType' + ) }) - /** - * Get ERP credentials for current user - */ - ipcMain.handle('settings:getSettings', async (): Promise => { - try { - // Get ERP credentials from database for current user - const userErpConfig = await erpConfigService.getCurrentUserErpConfig() - - return { - erp: { - username: userErpConfig?.username || '', - password: userErpConfig?.password || '' - } - } - } catch (error) { - log.error('Failed to get ERP credentials', { error }) - return { erp: { username: '', password: '' } } - } - }) - - /** - * Save ERP credentials for current user - */ ipcMain.handle( - 'settings:saveSettings', - async (_event, settings: any): Promise => { - try { - log.info('Saving ERP credentials') - - if (settings.erp) { - // Update ERP credentials in database for current user - const currentUser = sessionManager.getUserInfo() - if (!currentUser) { - return { success: false, error: '未找到当前用户' } + IPC_CHANNELS.SETTINGS_GET_SETTINGS, + async (): Promise> => { + return withErrorHandling(async () => { + const userErpConfig = await erpConfigService.getCurrentUserErpConfig() + return { + erp: { + username: userErpConfig?.username || '', + password: userErpConfig?.password || '' } - - // 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') } - - return { success: true } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Error saving ERP credentials', { error: message }) - return { success: false, error: `保存配置失败:${message}` } - } + }, 'settings:getSettings') } ) - /** - * Reset to default settings (Admin only) - */ - ipcMain.handle('settings:resetDefaults', async (): Promise => { - try { - const userType = sessionManager.getUserType() - if (userType !== 'Admin') { - log.warn('Non-admin user attempted to reset defaults', { userType }) - return { success: false, error: '只有管理员可以恢复默认设置' } - } + ipcMain.handle( + IPC_CHANNELS.SETTINGS_SAVE_SETTINGS, + async (_event, settings: ErpSettingsPayload): Promise> => { + return withErrorHandling(async () => { + if (settings.erp) { + const currentUser = sessionManager.getUserInfo() + if (!currentUser) { + throw new ValidationError('未找到当前用户', 'VAL_INVALID_INPUT') + } + + await erpConfigService.updateCurrentUserErpConfig({ + username: settings.erp.username || '', + password: settings.erp.password || '' + }) + } - log.info('Resetting settings to defaults') - const success = await configManager.resetToDefaults() - if (success) { - log.info('Settings reset to defaults successfully') return { success: true } - } else { - log.warn('Failed to reset settings to defaults') - return { success: false, error: '恢复默认设置失败' } - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Error resetting settings', { error: message }) - return { success: false, error: `恢复默认设置失败:${message}` } + }, 'settings:saveSettings') } - }) + ) - /** - * Test database connection - */ - ipcMain.handle('settings:testDbConnection', async (): Promise => { - try { - log.info('Testing database connection') - const config = configManager.getConfig() - const dbType = config.database.activeType + ipcMain.handle( + IPC_CHANNELS.SETTINGS_RESET_DEFAULTS, + async (): Promise> => { + return withErrorHandling(async () => { + const userType = sessionManager.getUserType() + if (userType !== 'Admin') { + throw new ValidationError('只有管理员可以恢复默认设置', 'VAL_INVALID_INPUT') + } - if (dbType === 'mysql') { - // Test MySQL connection - const dbConfig = config.database.mysql - if (!dbConfig.host || !dbConfig.database || !dbConfig.username) { - log.warn('MySQL connection test failed - missing configuration') - return { - success: false, - message: '请先配置 MySQL 主机、数据库名和用户名' + const success = await configManager.resetToDefaults() + if (!success) { + throw new ValidationError('恢复默认设置失败', 'VAL_INVALID_INPUT') + } + + return { success: true } + }, 'settings:resetDefaults') + } + ) + + ipcMain.handle( + IPC_CHANNELS.SETTINGS_TEST_DB_CONNECTION, + async (): Promise> => { + return withErrorHandling(async () => { + log.info('Testing database connection') + const config = configManager.getConfig() + const dbType = config.database.activeType + + if (dbType === 'mysql') { + const dbConfig = config.database.mysql + if (!dbConfig.host || !dbConfig.database || !dbConfig.username) { + return { + success: false, + message: '请先配置 MySQL 主机、数据库名和用户名' + } + } + + const mysqlService = new MySqlService({ + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database + }) + + try { + await mysqlService.connect() + await mysqlService.disconnect() + return { + success: true, + message: 'MySQL 数据库连接测试成功!' + } + } catch (error) { + const message = error instanceof Error ? error.message : '连接失败' + return { + success: false, + message: `MySQL 数据库连接测试失败:${message}` + } } } - const mysqlService = new MySqlService({ - host: dbConfig.host, - port: dbConfig.port, - user: dbConfig.username, - password: dbConfig.password, - database: dbConfig.database - }) - - try { - await mysqlService.connect() - await mysqlService.disconnect() - log.info('MySQL connection test successful') - return { - success: true, - message: 'MySQL 数据库连接测试成功!' - } - } catch (connError) { - const errorMessage = connError instanceof Error ? connError.message : '连接失败' - log.error('MySQL connection failed', { error: errorMessage }) - return { - success: false, - message: `MySQL 数据库连接测试失败:${errorMessage}` - } - } - } 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 { success: false, message: '请先配置 SQL Server 服务器、数据库名和用户名' @@ -189,27 +149,19 @@ export function registerSettingsHandlers(): void { try { await sqlServerService.connect() await sqlServerService.disconnect() - log.info('SQL Server connection test successful') return { success: true, message: 'SQL Server 数据库连接测试成功!' } - } catch (connError) { - const errorMessage = connError instanceof Error ? connError.message : '连接失败' - log.error('SQL Server connection failed', { error: errorMessage }) + } catch (error) { + const message = error instanceof Error ? error.message : '连接失败' return { success: false, - message: `SQL Server 数据库连接测试失败:${errorMessage}` + message: `SQL Server 数据库连接测试失败:${message}` } } - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Database connection test error', { error: message }) - return { - success: false, - message: `数据库连接测试失败:${message}` - } + }, 'settings:testDbConnection') } - }) + ) } + diff --git a/src/main/ipc/user-erp-config-handler.ts b/src/main/ipc/user-erp-config-handler.ts index b647e32..f14b1dc 100644 --- a/src/main/ipc/user-erp-config-handler.ts +++ b/src/main/ipc/user-erp-config-handler.ts @@ -1,32 +1,20 @@ -/** - * IPC handlers for User ERP Configuration - * - * Provides APIs for the renderer process to: - * - Get current user's ERP credentials - * - Update current user's ERP credentials - * - Test ERP connection with provided credentials - */ - import { ipcMain } from 'electron' -import { UserErpConfigService, type ErpCredentials } from '../services/user/user-erp-config-service' +import { UserErpConfigService } 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' +import { SessionManager } from '../services/user/session-manager' +import { IPC_CHANNELS } from '../../shared/ipc-channels' +import { ValidationError } from '../types/errors' +import { withErrorHandling, type IpcResult } from './index' const log = createLogger('UserErpConfigHandler') -/** - * ERP Credentials request (username and password only, URL is from config.yaml) - */ export interface ErpCredentialsRequest { username: string password: string } -/** - * ERP Configuration response (includes URL from config.yaml) - */ export interface ErpConfigResponse { success: boolean config?: { @@ -37,127 +25,80 @@ export interface ErpConfigResponse { error?: string } -/** - * Connection test result - */ export interface ConnectionTestResult { success: boolean message?: string } -/** - * Register IPC handlers for user ERP configuration - */ export function registerUserErpConfigHandlers(): void { const erpConfigService = UserErpConfigService.getInstance() + const sessionManager = SessionManager.getInstance() - /** - * Get current user's ERP credentials - */ - ipcMain.handle('user-erp-config:getCurrent', async (): Promise => { - try { - log.info('Fetching current user ERP credentials') - const credentials = await erpConfigService.getCurrentUserErpConfig() - - if (!credentials) { - return { - success: false, - 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: erpUrl, - username: credentials.username, - password: credentials.password - } - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Get current user ERP credentials failed', { error: message }) - return { - success: false, - error: `获取 ERP 配置失败:${message}` - } - } - }) - - /** - * Update current user's ERP credentials - */ ipcMain.handle( - 'user-erp-config:update', - async (_event, credentials: ErpCredentialsRequest): Promise => { - try { - log.info('Updating current user ERP credentials', { - username: credentials.username - }) + IPC_CHANNELS.USER_ERP_CONFIG_GET_CURRENT, + async (): Promise> => { + return withErrorHandling(async () => { + log.info('Fetching current user ERP credentials') + const credentials = await erpConfigService.getCurrentUserErpConfig() - const success = await erpConfigService.updateCurrentUserErpConfig(credentials) - - if (success) { - 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: { - url: erpUrl, - username: credentials.username, - password: credentials.password - } - } - } else { - log.error('Failed to update ERP credentials') - return { - success: false, - error: '更新 ERP 配置失败' - } + if (!credentials) { + throw new ValidationError('未找到 ERP 配置。请先配置 ERP 账号和密码。', 'VAL_INVALID_INPUT') } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Update ERP credentials failed', { error: message }) + + const configManager = ConfigManager.getInstance() + const globalConfig = configManager.getConfig() + return { - success: false, - error: `更新 ERP 配置失败:${message}` + success: true, + config: { + url: globalConfig.erp.url, + username: credentials.username, + password: credentials.password + } } - } + }, 'user-erp-config:getCurrent') } ) - /** - * Test ERP connection with provided credentials - */ ipcMain.handle( - 'user-erp-config:testConnection', - async (_event, credentials: ErpCredentialsRequest): Promise => { - try { - log.info('Testing ERP connection', { username: credentials.username }) - - if (!credentials.username || !credentials.password) { - return { - success: false, - message: 'ERP 配置不完整,请确保用户名和密码都已填写' - } + IPC_CHANNELS.USER_ERP_CONFIG_UPDATE, + async (_event, credentials: ErpCredentialsRequest): Promise> => { + return withErrorHandling(async () => { + const updated = await erpConfigService.updateCurrentUserErpConfig(credentials) + if (!updated) { + throw new ValidationError('更新 ERP 配置失败', 'VAL_INVALID_INPUT') } - // 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: globalConfig.erp.url, + username: credentials.username, + password: credentials.password + } + } + }, 'user-erp-config:update') + } + ) + + ipcMain.handle( + IPC_CHANNELS.USER_ERP_CONFIG_TEST_CONNECTION, + async (_event, credentials: ErpCredentialsRequest): Promise> => { + return withErrorHandling(async () => { + if (!credentials.username || !credentials.password) { + throw new ValidationError( + 'ERP 配置不完整,请确保用户名和密码都已填写', + 'VAL_MISSING_REQUIRED' + ) + } + + const configManager = ConfigManager.getInstance() + const globalConfig = configManager.getConfig() const authService = new ErpAuthService({ - url: erpUrl, + url: globalConfig.erp.url, username: credentials.username, password: credentials.password, headless: true @@ -165,49 +106,37 @@ export function registerUserErpConfigHandlers(): void { try { await authService.login() - await authService.close() - - log.info('ERP connection test successful') return { success: true, message: 'ERP 连接测试成功' } - } catch (error) { + } finally { await authService.close().catch(() => {}) - throw error } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('ERP connection test failed', { error: message }) - return { - success: false, - message: `ERP 连接测试失败:${message}` - } - } + }, 'user-erp-config:testConnection') } ) - /** - * Get all users' ERP configurations (admin only) - */ ipcMain.handle( - 'user-erp-config:getAll', + IPC_CHANNELS.USER_ERP_CONFIG_GET_ALL, async (): Promise< - Array<{ - username: string - erpUrl: string - erpUsername: string - }> + IpcResult< + Array<{ + username: string + erpUrl: string + erpUsername: string + }> + > > => { - try { - log.info('Fetching all users ERP config') + return withErrorHandling(async () => { + if (!sessionManager.isAdmin()) { + throw new ValidationError('只有管理员可以查看全部用户 ERP 配置', 'VAL_INVALID_INPUT') + } + const configs = await erpConfigService.getAllUsersErpConfig() - log.info('Retrieved ERP configs for all users', { count: configs.length }) return configs - } catch (error) { - log.error('Get all users ERP config failed', { error }) - return [] - } + }, 'user-erp-config:getAll') } ) } + diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts index df40b85..556716b 100644 --- a/src/main/ipc/validation-handler.ts +++ b/src/main/ipc/validation-handler.ts @@ -23,6 +23,7 @@ import type { ValidationResult, MaterialRecordSummary } from '../types/validation.types' +import { IPC_CHANNELS } from '../../shared/ipc-channels' const log = createLogger('ValidationHandler') @@ -30,28 +31,28 @@ const log = createLogger('ValidationHandler') * Shared state for Production IDs from extractor page * This is a simple in-memory store for sharing Production IDs between pages */ -const sharedProductionIds = new Set() +const sharedProductionIdsBySender = new Map>() /** * Set shared Production IDs */ -export function setSharedProductionIds(ids: string[]): void { - sharedProductionIds.clear() - ids.forEach((id) => sharedProductionIds.add(id)) +export function setSharedProductionIds(senderId: number, ids: string[]): void { + sharedProductionIdsBySender.set(senderId, new Set(ids)) } /** * Get shared Production IDs */ -export function getSharedProductionIds(): string[] { - return [...sharedProductionIds] +export function getSharedProductionIds(senderId: number): string[] { + const senderSet = sharedProductionIdsBySender.get(senderId) + return senderSet ? [...senderSet] : [] } /** * Clear shared Production IDs */ -export function clearSharedProductionIds(): void { - sharedProductionIds.clear() +export function clearSharedProductionIds(senderId: number): void { + sharedProductionIdsBySender.delete(senderId) } /** @@ -233,8 +234,8 @@ export function registerValidationHandlers(): void { * Run material validation from database */ ipcMain.handle( - 'validation:validate', - async (_event, request: ValidationRequest): Promise => { + IPC_CHANNELS.VALIDATION_VALIDATE, + async (event, request: ValidationRequest): Promise => { let dbService: MySqlService | SqlServerService | null = null try { @@ -273,7 +274,7 @@ export function registerValidationHandlers(): void { if (request.mode === 'database_filtered') { if (request.useSharedProductionIds) { // Use shared Production IDs from extractor page - const sharedIds = getSharedProductionIds() + const sharedIds = getSharedProductionIds(event.sender.id) log.info(`Using ${sharedIds.length} shared Production IDs`) if (sharedIds.length === 0) { @@ -445,7 +446,7 @@ export function registerValidationHandlers(): void { * Upsert batch materials to MaterialsToBeDeleted */ ipcMain.handle( - 'materials:upsertBatch', + IPC_CHANNELS.MATERIALS_UPSERT_BATCH, async (_event, request: MaterialUpsertBatchRequest): Promise => { try { const dao = new MaterialsToBeDeletedDAO() @@ -472,7 +473,7 @@ export function registerValidationHandlers(): void { * Delete materials by material codes */ ipcMain.handle( - 'materials:delete', + IPC_CHANNELS.MATERIALS_DELETE, async (_event, request: MaterialDeleteRequest): Promise => { try { const dao = new MaterialsToBeDeletedDAO() @@ -496,7 +497,7 @@ export function registerValidationHandlers(): void { /** * Get unique manager names */ - ipcMain.handle('materials:getManagers', async (_event): Promise<{ managers: string[] }> => { + ipcMain.handle(IPC_CHANNELS.MATERIALS_GET_MANAGERS, async (_event): Promise<{ managers: string[] }> => { try { const dao = new MaterialsToBeDeletedDAO() const managers = await dao.getManagers() @@ -513,7 +514,7 @@ export function registerValidationHandlers(): void { * Update manager for a single material */ ipcMain.handle( - 'materials:updateManager', + IPC_CHANNELS.MATERIALS_UPDATE_MANAGER, async ( _event, request: { materialCode: string; managerName: string } @@ -537,7 +538,7 @@ export function registerValidationHandlers(): void { * Get materials by manager */ ipcMain.handle( - 'materials:getByManager', + IPC_CHANNELS.MATERIALS_GET_BY_MANAGER, async (_event, managerName: string): Promise<{ materials: MaterialRecordSummary[] }> => { let dbService: MySqlService | SqlServerService | null = null @@ -616,7 +617,7 @@ export function registerValidationHandlers(): void { * Get all material records */ ipcMain.handle( - 'materials:getAll', + IPC_CHANNELS.MATERIALS_GET_ALL, async (_event): Promise<{ materials: MaterialRecordSummary[] }> => { let dbService: MySqlService | SqlServerService | null = null @@ -691,7 +692,7 @@ export function registerValidationHandlers(): void { /** * Get statistics */ - ipcMain.handle('materials:getStatistics', async (_event): Promise<{ stats: any }> => { + ipcMain.handle(IPC_CHANNELS.MATERIALS_GET_STATISTICS, async (_event): Promise<{ stats: any }> => { try { const dao = new MaterialsToBeDeletedDAO() const stats = await dao.getStatistics() @@ -708,10 +709,10 @@ export function registerValidationHandlers(): void { * Set shared Production IDs from extractor page */ ipcMain.handle( - 'validation:setSharedProductionIds', - async (_event, productionIds: string[]): Promise => { + IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS, + async (event, productionIds: string[]): Promise => { log.info(`Received ${productionIds.length} shared Production IDs`) - setSharedProductionIds(productionIds) + setSharedProductionIds(event.sender.id, productionIds) } ) @@ -719,9 +720,9 @@ export function registerValidationHandlers(): void { * Get shared Production IDs */ ipcMain.handle( - 'validation:getSharedProductionIds', - async (): Promise<{ productionIds: string[] }> => { - return { productionIds: getSharedProductionIds() } + IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS, + async (event): Promise<{ productionIds: string[] }> => { + return { productionIds: getSharedProductionIds(event.sender.id) } } ) @@ -730,7 +731,7 @@ export function registerValidationHandlers(): void { * Filters materials by current user (admin sees all, regular users see only their own) */ ipcMain.handle( - 'validation:getCleanerData', + IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA, async ( _event ): Promise<{ @@ -766,7 +767,7 @@ export function registerValidationHandlers(): void { dbService = await getValidationDatabaseService() // 1. Get order numbers from shared Production IDs - const sharedIds = getSharedProductionIds() + const sharedIds = getSharedProductionIds(_event.sender.id) let orderNumbers: string[] = [] if (sharedIds.length > 0) { diff --git a/src/main/types/ipc-api.types.ts b/src/main/types/ipc-api.types.ts index 011ebe2..2dafe1b 100644 --- a/src/main/types/ipc-api.types.ts +++ b/src/main/types/ipc-api.types.ts @@ -11,6 +11,7 @@ import type { ExportResultItem, ExportResultResponse } from './cleaner.types' +import type { IpcResult } from '../ipc' /** * MySQL connection configuration @@ -60,11 +61,11 @@ export interface SqlServerQueryResult { * File operation APIs */ export interface FileAPI { - readFile: (filePath: string) => Promise - writeFile: (filePath: string, content: string) => Promise - fileExists: (filePath: string) => Promise - listFiles: (dirPath: string) => Promise - openPath: (filePath: string) => Promise + readFile: (filePath: string) => Promise> + writeFile: (filePath: string, content: string) => Promise> + fileExists: (filePath: string) => Promise> + listFiles: (dirPath: string) => Promise> + openPath: (filePath: string) => Promise> } /** @@ -77,7 +78,7 @@ export interface ExtractorAPI { */ runExtractor: ( input: ExtractorInput - ) => Promise<{ success: boolean; data?: ExtractorResult; error?: string }> + ) => Promise> /** * Subscribe to progress updates * @param callback - Callback function receiving progress data @@ -102,13 +103,13 @@ export interface CleanerAPI { */ runCleaner: ( input: CleanerInput - ) => Promise<{ success: boolean; data?: CleanerResult; error?: string }> + ) => Promise> /** * Export validation results to Excel * @param items - Validation result items to export */ - exportResults: (items: ExportResultItem[]) => Promise + exportResults: (items: ExportResultItem[]) => Promise> /** * Subscribe to progress updates @@ -126,45 +127,48 @@ export interface DatabaseAPI { * Connect to MySQL database * @param config - MySQL connection config */ - connectMySql: (config: MySqlConfig) => Promise + connectMySql: (config: MySqlConfig) => Promise> /** * Disconnect from MySQL database */ - disconnectMySql: () => Promise + disconnectMySql: () => Promise> /** * Check if MySQL is connected */ - isMySqlConnected: () => Promise + isMySqlConnected: () => Promise> /** * Execute MySQL query * @param sql - SQL query * @param params - Query parameters */ - queryMySql: (sql: string, params?: unknown[]) => Promise + queryMySql: (sql: string, params?: unknown[]) => Promise> /** * Connect to SQL Server database * @param config - SQL Server connection config */ - connectSqlServer: (config: SqlServerConfig) => Promise + connectSqlServer: (config: SqlServerConfig) => Promise> /** * Disconnect from SQL Server database */ - disconnectSqlServer: () => Promise + disconnectSqlServer: () => Promise> /** * Check if SQL Server is connected */ - isSqlServerConnected: () => Promise + isSqlServerConnected: () => Promise> /** * Execute SQL Server query * @param sql - SQL query * @param params - Query parameters */ - querySqlServer: (sql: string, params?: Record) => Promise + querySqlServer: ( + sql: string, + params?: Record + ) => Promise> } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 371b715..099c148 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -15,277 +15,105 @@ import type { MaterialTypeBatchRequest } from '../main/types/validation.types' import type { - SettingsData, UserType, ConnectionTestResult, SaveSettingsResult } from '../main/types/settings.types' +import type { IpcResult } from '../main/ipc' -/** - * Order number resolver API - */ export interface ResolverAPI { - /** - * Resolve productionIDs and 生产订单号 to production order numbers - * @param input - Resolver input with list of inputs - */ - resolve: (input: ResolverInput) => Promise - /** - * Validate input format only (without database lookup) - * @param inputs - List of inputs to validate - */ - validateFormat: (inputs: string[]) => Promise<{ - success: boolean - results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }> - error?: string - }> + resolve: (input: ResolverInput) => Promise> + validateFormat: (inputs: string[]) => Promise< + IpcResult> + > } -/** - * Authentication API - */ export interface AuthAPI { - /** - * Get computer name - */ - getComputerName: () => Promise - /** - * Silent login by computer name - */ - silentLogin: () => Promise - /** - * Login with username and password - * @param request - Login request with username and password - */ - login: (request: LoginRequest) => Promise - /** - * Logout - */ - logout: () => Promise - /** - * Get current user - */ - getCurrentUser: () => Promise - /** - * Get all users (for admin user selection) - */ - getAllUsers: () => Promise - /** - * Switch user (admin only) - * @param userInfo - User info to switch to - */ - switchUser: (userInfo: UserInfo) => Promise - /** - * Check if current user is admin - */ - isAdmin: () => Promise + getComputerName: () => Promise> + silentLogin: () => Promise> + login: (request: LoginRequest) => Promise> + logout: () => Promise> + getCurrentUser: () => Promise> + getAllUsers: () => Promise> + switchUser: (userInfo: UserInfo) => Promise> + isAdmin: () => Promise> } -/** - * Validation API - */ export interface ValidationAPI { - /** - * Run material validation - * @param request - Validation request - */ - validate: (request: ValidationRequest) => Promise - /** - * Set shared Production IDs from extractor page - * @param productionIds - List of Production IDs - */ - setSharedProductionIds: (productionIds: string[]) => Promise - /** - * Get shared Production IDs - */ - getSharedProductionIds: () => Promise<{ productionIds: string[] }> - /** - * Get cleaner data (order numbers from shared Production IDs + material codes from MaterialsToBeDeleted) - * Filters materials by current user (admin sees all, regular users see only their own) - */ - getCleanerData: () => Promise<{ - success: boolean - orderNumbers?: string[] - materialCodes?: string[] - error?: string - }> + validate: (request: ValidationRequest) => Promise> + setSharedProductionIds: (productionIds: string[]) => Promise> + getSharedProductionIds: () => Promise> + getCleanerData: () => Promise< + IpcResult<{ + orderNumbers: string[] + materialCodes: string[] + }> + > } -/** - * Materials API - */ export interface MaterialsAPI { - /** - * Upsert batch materials to MaterialsToBeDeleted - * @param materials - List of materials with materialCode and managerName - */ - upsertBatch: (materials: { materialCode: string; managerName: string }[]) => Promise<{ - success: boolean - stats?: { total: number; success: number; failed: number } - error?: string - }> - /** - * Delete materials by material codes - * @param materialCodes - List of material codes to delete - */ - delete: (materialCodes: string[]) => Promise<{ - success: boolean - count?: number - error?: string - }> - /** - * Get unique manager names - */ - getManagers: () => Promise<{ managers: string[] }> - /** - * Get materials by manager - * @param managerName - Manager name - */ - getByManager: (managerName: string) => Promise<{ materials: unknown[] }> - /** - * Get all material records - */ - getAll: () => Promise<{ materials: unknown[] }> - /** - * Get statistics - */ - getStatistics: () => Promise<{ stats: unknown }> - /** - * Update manager for a single material - * @param materialCode - Material code - * @param managerName - New manager name - */ + upsertBatch: (materials: { materialCode: string; managerName: string }[]) => Promise< + IpcResult<{ stats: { total: number; success: number; failed: number } }> + > + delete: (materialCodes: string[]) => Promise> + getManagers: () => Promise> + getByManager: (managerName: string) => Promise> + getAll: () => Promise> + getStatistics: () => Promise> updateManager: ( materialCode: string, managerName: string - ) => Promise<{ success: boolean; error?: string }> + ) => Promise> } -/** - * Settings API - */ export interface SettingsAPI { - /** - * Get current user type - */ - getUserType: () => Promise - /** - * Get settings (filtered by user type) - */ - getSettings: () => Promise - /** - * Save settings - * @param settings - Settings data to save - */ - saveSettings: (settings: SettingsData) => Promise - /** - * Reset to defaults (Admin only) - */ - resetDefaults: () => Promise - /** - * Test ERP connection - */ - testErpConnection: () => Promise - /** - * Test database connection - */ - testDbConnection: () => Promise + getUserType: () => Promise> + getSettings: () => Promise> + saveSettings: (settings: { erp?: { username?: string; password?: string } }) => Promise> + resetDefaults: () => Promise> + testDbConnection: () => Promise> } -/** - * Material Type API - */ export interface MaterialTypeAPI { - /** - * Get all material type records - */ - getAll: () => Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> - /** - * Get material types by manager - * @param managerName - Manager name - */ - getByManager: ( - managerName: string - ) => Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> - /** - * Get list of managers - */ - getManagers: () => Promise<{ success: boolean; data?: string[]; error?: string }> - /** - * Upsert (insert or update) a material type record - */ - upsert: ( - materialName: string, - managerName: string - ) => Promise<{ success: boolean; error?: string }> - /** - * Delete a material type record - */ - delete: ( - materialName: string, - managerName: string - ) => Promise<{ success: boolean; error?: string }> - /** - * Batch operation for material types - */ - upsertBatch: (request: MaterialTypeBatchRequest) => Promise<{ - success: boolean - stats?: { total: number; success: number; failed: number } - error?: string - }> + getAll: () => Promise> + getByManager: (managerName: string) => Promise> + getManagers: () => Promise> + upsert: (materialName: string, managerName: string) => Promise> + delete: (materialName: string, managerName: string) => Promise> + upsertBatch: (request: MaterialTypeBatchRequest) => Promise< + IpcResult<{ stats: { total: number; success: number; failed: number } }> + > } -/** - * User ERP Configuration API - */ export interface UserErpConfigAPI { - /** - * Get current user's ERP configuration - */ - getCurrent: () => Promise<{ - success: boolean - config?: { url: string; username: string; password: string } - error?: string - }> - /** - * Update current user's ERP configuration - */ - update: (config: { url: string; username: string; password: string }) => Promise<{ - success: boolean - config?: { url: string; username: string; password: string } - error?: string - }> - /** - * Test ERP connection with provided credentials - */ - testConnection: (config: { url: string; username: string; password: string }) => Promise<{ - success: boolean - message?: string - }> - /** - * Get all users' ERP configurations (admin only) - */ - getAll: () => Promise> + getCurrent: () => Promise< + IpcResult<{ + config: { url: string; username: string; password: string } + }> + > + update: (config: { url: string; username: string; password: string }) => Promise< + IpcResult<{ + config: { url: string; username: string; password: string } + }> + > + testConnection: (config: { url: string; username: string; password: string }) => Promise< + IpcResult<{ message: string }> + > + getAll: () => Promise>> +} + +export interface ProcessAPI { + versions: { + electron: string + chrome: string + node: string + } } declare global { interface Window { electron: { - ipcRenderer: { - send: (channel: string, ...args: unknown[]) => void - on: (channel: string, func: (...args: unknown[]) => void) => void - once: (channel: string, func: (...args: unknown[]) => void) => void - removeListener: (channel: string, func: (...args: unknown[]) => void) => void - removeAllListeners: (channel: string) => void - invoke: (channel: string, ...args: unknown[]) => Promise - } - process: { - versions: { - electron: string - chrome: string - node: string - } - } + process: ProcessAPI file: FileAPI extractor: ExtractorAPI cleaner: CleanerAPI diff --git a/src/preload/index.ts b/src/preload/index.ts index 27c576a..038352e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,5 +1,4 @@ import { contextBridge, ipcRenderer } from 'electron' -import { electronAPI } from '@electron-toolkit/preload' import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types' import type { ExtractorInput, ExtractionProgress } from '../main/types/extractor.types' import type { CleanerInput, CleanerProgress, ExportResultItem } from '../main/types/cleaner.types' @@ -11,158 +10,202 @@ import type { MaterialTypeRecord, MaterialTypeBatchRequest } from '../main/types/validation.types' -import type { SettingsData } from '../main/types/settings.types' +import type { IpcResult } from '../main/ipc' +import { IPC_CHANNELS } from '../shared/ipc-channels' + +type ErpSettingsPayload = { + erp?: { + username?: string + password?: string + } +} + +const processApi = { + versions: { + electron: process.versions.electron, + chrome: process.versions.chrome, + node: process.versions.node + } +} + +function invokeIpc(channel: string, ...args: unknown[]): Promise> { + return ipcRenderer.invoke(channel, ...args).then((result: unknown) => { + if (result && typeof result === 'object' && 'success' in (result as Record)) { + const typed = result as Record + const hasIpcShape = 'data' in typed || 'code' in typed || 'error' in typed + + if (hasIpcShape) { + return typed as unknown as IpcResult + } + + if (typed.success === true) { + return { success: true, data: result as T } + } + + return { + success: false, + error: typeof typed.error === 'string' ? typed.error : 'IPC operation failed' + } + } + + return { success: true, data: result as T } + }) +} -// Custom APIs for renderer const api = { - // File operations + process: processApi, + file: { - readFile: (filePath: string) => ipcRenderer.invoke('file:read', filePath), + readFile: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_READ, filePath), writeFile: (filePath: string, content: string) => - ipcRenderer.invoke('file:write', filePath, content), - fileExists: (filePath: string) => ipcRenderer.invoke('file:exists', filePath), - listFiles: (dirPath: string) => ipcRenderer.invoke('file:list', dirPath), - openPath: (filePath: string) => ipcRenderer.invoke('file:openPath', filePath) + invokeIpc(IPC_CHANNELS.FILE_WRITE, filePath, content), + fileExists: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_EXISTS, filePath), + listFiles: (dirPath: string) => invokeIpc(IPC_CHANNELS.FILE_LIST, dirPath), + openPath: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_OPEN_PATH, filePath) }, - // Extractor service extractor: { - runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input), + runExtractor: (input: ExtractorInput): Promise => + invokeIpc(IPC_CHANNELS.EXTRACTOR_RUN, input), onProgress: (callback: (data: ExtractionProgress) => void) => { const subscription = (_event: Electron.IpcRendererEvent, data: ExtractionProgress) => callback(data) - ipcRenderer.on('extractor:progress', subscription) - return () => ipcRenderer.removeListener('extractor:progress', subscription) + ipcRenderer.on(IPC_CHANNELS.EXTRACTOR_PROGRESS, subscription) + return () => ipcRenderer.removeListener(IPC_CHANNELS.EXTRACTOR_PROGRESS, subscription) }, onLog: (callback: (data: { level: string; message: string }) => void) => { const subscription = ( _event: Electron.IpcRendererEvent, data: { level: string; message: string } ) => callback(data) - ipcRenderer.on('extractor:log', subscription) - return () => ipcRenderer.removeListener('extractor:log', subscription) + ipcRenderer.on(IPC_CHANNELS.EXTRACTOR_LOG, subscription) + return () => ipcRenderer.removeListener(IPC_CHANNELS.EXTRACTOR_LOG, subscription) } }, - // Cleaner service cleaner: { - runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input), - exportResults: (items: ExportResultItem[]) => - ipcRenderer.invoke('cleaner:exportResults', items), + runCleaner: (input: CleanerInput): Promise => + invokeIpc(IPC_CHANNELS.CLEANER_RUN, input), + exportResults: (items: ExportResultItem[]): Promise => + invokeIpc(IPC_CHANNELS.CLEANER_EXPORT_RESULTS, items), onProgress: (callback: (data: CleanerProgress) => void) => { const subscription = (_event: Electron.IpcRendererEvent, data: CleanerProgress) => callback(data) - ipcRenderer.on('cleaner:progress', subscription) - return () => ipcRenderer.removeListener('cleaner:progress', subscription) + ipcRenderer.on(IPC_CHANNELS.CLEANER_PROGRESS, subscription) + return () => ipcRenderer.removeListener(IPC_CHANNELS.CLEANER_PROGRESS, subscription) } }, - // Order number resolver resolver: { - resolve: (input: ResolverInput) => ipcRenderer.invoke('resolver:resolve', input), - validateFormat: (inputs: string[]) => ipcRenderer.invoke('resolver:validateFormat', inputs) + resolve: (input: ResolverInput): Promise => + invokeIpc(IPC_CHANNELS.RESOLVER_RESOLVE, input), + validateFormat: (inputs: string[]): Promise => + invokeIpc(IPC_CHANNELS.RESOLVER_VALIDATE_FORMAT, inputs) }, - // Authentication service auth: { - getComputerName: () => ipcRenderer.invoke('auth:getComputerName'), - silentLogin: () => ipcRenderer.invoke('auth:silentLogin'), - login: (request: LoginRequest) => ipcRenderer.invoke('auth:login', request), - logout: () => ipcRenderer.invoke('auth:logout'), - getCurrentUser: () => ipcRenderer.invoke('auth:getCurrentUser'), - getAllUsers: () => ipcRenderer.invoke('auth:getAllUsers'), - switchUser: (userInfo: UserInfo) => ipcRenderer.invoke('auth:switchUser', userInfo), - isAdmin: () => ipcRenderer.invoke('auth:isAdmin') + getComputerName: (): Promise => + invokeIpc(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME), + silentLogin: (): Promise => invokeIpc(IPC_CHANNELS.AUTH_SILENT_LOGIN), + login: (request: LoginRequest): Promise => + invokeIpc(IPC_CHANNELS.AUTH_LOGIN, request), + logout: (): Promise => invokeIpc(IPC_CHANNELS.AUTH_LOGOUT), + getCurrentUser: (): Promise => invokeIpc(IPC_CHANNELS.AUTH_GET_CURRENT_USER), + getAllUsers: (): Promise => invokeIpc(IPC_CHANNELS.AUTH_GET_ALL_USERS), + switchUser: (userInfo: UserInfo): Promise => + invokeIpc(IPC_CHANNELS.AUTH_SWITCH_USER, userInfo), + isAdmin: (): Promise => invokeIpc(IPC_CHANNELS.AUTH_IS_ADMIN) }, - // Database service database: { - connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config), - disconnectMySql: () => ipcRenderer.invoke('database:mysql:disconnect'), - isMySqlConnected: () => ipcRenderer.invoke('database:mysql:isConnected'), - queryMySql: (sql: string, params?: any[]) => - ipcRenderer.invoke('database:mysql:query', sql, params), - - // SQL Server - connectSqlServer: (config: SqlServerConfig) => - ipcRenderer.invoke('database:sqlserver:connect', config), - disconnectSqlServer: () => ipcRenderer.invoke('database:sqlserver:disconnect'), - isSqlServerConnected: () => ipcRenderer.invoke('database:sqlserver:isConnected'), - querySqlServer: (sql: string, params?: Record) => - ipcRenderer.invoke('database:sqlserver:query', sql, params) + connectMySql: (config: MySqlConfig): Promise => + invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_CONNECT, config), + disconnectMySql: (): Promise => invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT), + isMySqlConnected: (): Promise => + invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_IS_CONNECTED), + queryMySql: (sql: string, params?: any[]): Promise => + invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_QUERY, sql, params), + connectSqlServer: (config: SqlServerConfig): Promise => + invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_CONNECT, config), + disconnectSqlServer: (): Promise => + invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT), + isSqlServerConnected: (): Promise => + invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_IS_CONNECTED), + querySqlServer: (sql: string, params?: Record): Promise => + invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_QUERY, sql, params) }, - // Validation service validation: { - validate: (request: ValidationRequest) => ipcRenderer.invoke('validation:validate', request), - setSharedProductionIds: (productionIds: string[]) => - ipcRenderer.invoke('validation:setSharedProductionIds', productionIds), - getSharedProductionIds: () => ipcRenderer.invoke('validation:getSharedProductionIds'), - getCleanerData: () => ipcRenderer.invoke('validation:getCleanerData') + validate: (request: ValidationRequest): Promise => + invokeIpc(IPC_CHANNELS.VALIDATION_VALIDATE, request), + setSharedProductionIds: (productionIds: string[]): Promise => + invokeIpc(IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS, productionIds), + getSharedProductionIds: (): Promise => + invokeIpc(IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS), + getCleanerData: (): Promise => + invokeIpc(IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA) }, - // Materials service materials: { - upsertBatch: (materials: { materialCode: string; managerName: string }[]) => - ipcRenderer.invoke('materials:upsertBatch', { materials }), - delete: (materialCodes: string[]) => ipcRenderer.invoke('materials:delete', { materialCodes }), - getManagers: () => ipcRenderer.invoke('materials:getManagers'), - getByManager: (managerName: string) => - ipcRenderer.invoke('materials:getByManager', managerName), - getAll: () => ipcRenderer.invoke('materials:getAll'), - getStatistics: () => ipcRenderer.invoke('materials:getStatistics'), - updateManager: (materialCode: string, managerName: string) => - ipcRenderer.invoke('materials:updateManager', { materialCode, managerName }) + upsertBatch: (materials: { materialCode: string; managerName: string }[]): Promise => + invokeIpc(IPC_CHANNELS.MATERIALS_UPSERT_BATCH, { materials }), + delete: (materialCodes: string[]): Promise => + invokeIpc(IPC_CHANNELS.MATERIALS_DELETE, { materialCodes }), + getManagers: (): Promise => invokeIpc(IPC_CHANNELS.MATERIALS_GET_MANAGERS), + getByManager: (managerName: string): Promise => + invokeIpc(IPC_CHANNELS.MATERIALS_GET_BY_MANAGER, managerName), + getAll: (): Promise => invokeIpc(IPC_CHANNELS.MATERIALS_GET_ALL), + getStatistics: (): Promise => invokeIpc(IPC_CHANNELS.MATERIALS_GET_STATISTICS), + updateManager: (materialCode: string, managerName: string): Promise => + invokeIpc(IPC_CHANNELS.MATERIALS_UPDATE_MANAGER, { materialCode, managerName }) }, - // Settings service settings: { - getUserType: () => ipcRenderer.invoke('settings:getUserType'), - getSettings: () => ipcRenderer.invoke('settings:getSettings'), - saveSettings: (settings: SettingsData) => ipcRenderer.invoke('settings:saveSettings', settings), - resetDefaults: () => ipcRenderer.invoke('settings:resetDefaults'), - testErpConnection: () => ipcRenderer.invoke('settings:testErpConnection'), - testDbConnection: () => ipcRenderer.invoke('settings:testDbConnection') + getUserType: (): Promise => invokeIpc(IPC_CHANNELS.SETTINGS_GET_USER_TYPE), + getSettings: (): Promise => invokeIpc(IPC_CHANNELS.SETTINGS_GET_SETTINGS), + saveSettings: (settings: ErpSettingsPayload): Promise => + invokeIpc(IPC_CHANNELS.SETTINGS_SAVE_SETTINGS, settings), + resetDefaults: (): Promise => invokeIpc(IPC_CHANNELS.SETTINGS_RESET_DEFAULTS), + testDbConnection: (): Promise => + invokeIpc(IPC_CHANNELS.SETTINGS_TEST_DB_CONNECTION) }, - // Material Type service materialType: { - getAll: () => ipcRenderer.invoke('materialType:getAll'), - getByManager: (managerName: string) => - ipcRenderer.invoke('materialType:getByManager', managerName), - getManagers: () => ipcRenderer.invoke('materialType:getManagers'), - upsert: (materialName: string, managerName: string) => - ipcRenderer.invoke('materialType:upsert', { materialName, managerName }), - delete: (materialName: string, managerName: string) => - ipcRenderer.invoke('materialType:delete', { materialName, managerName }), - upsertBatch: (request: MaterialTypeBatchRequest) => - ipcRenderer.invoke('materialType:upsertBatch', request) + getAll: (): Promise> => + invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_ALL), + getByManager: (managerName: string): Promise> => + invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_BY_MANAGER, managerName), + getManagers: (): Promise> => + invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_MANAGERS), + upsert: (materialName: string, managerName: string): Promise => + invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_UPSERT, { materialName, managerName }), + delete: (materialName: string, managerName: string): Promise => + invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_DELETE, { materialName, managerName }), + upsertBatch: (request: MaterialTypeBatchRequest): Promise => + invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_UPSERT_BATCH, request) }, - // User ERP Configuration service userErpConfig: { - getCurrent: () => ipcRenderer.invoke('user-erp-config:getCurrent'), - update: (config: { url: string; username: string; password: string }) => - ipcRenderer.invoke('user-erp-config:update', config), - testConnection: (config: { url: string; username: string; password: string }) => - ipcRenderer.invoke('user-erp-config:testConnection', config), - getAll: () => ipcRenderer.invoke('user-erp-config:getAll') + getCurrent: (): Promise => invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_GET_CURRENT), + update: (config: { url: string; username: string; password: string }): Promise => + invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_UPDATE, config), + testConnection: (config: { url: string; username: string; password: string }): Promise => + invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_TEST_CONNECTION, config), + getAll: (): Promise => invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_GET_ALL) } } as const -// Use `contextBridge` APIs to expose Electron APIs to -// renderer only if context isolation is enabled, otherwise -// just add to the DOM global. if (process.contextIsolated) { try { - contextBridge.exposeInMainWorld('electron', { ...electronAPI, ...api }) + contextBridge.exposeInMainWorld('electron', api) contextBridge.exposeInMainWorld('api', api) } catch (error) { console.error(error) } } else { // @ts-ignore (define in dts) - window.electron = { ...electronAPI, ...api } + window.electron = api // @ts-ignore (define in dts) window.api = api } + diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index ceeb89b..5892d52 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -61,16 +61,18 @@ function App(): React.JSX.Element { try { // Get computer name console.log('Getting computer name...') - const name = await window.electron.auth.getComputerName() + const computerNameResult = await window.electron.auth.getComputerName() + const name = computerNameResult.success && computerNameResult.data ? computerNameResult.data : '' console.log('Computer name:', name) setComputerName(name) // Try silent login console.log('Trying silent login...') - const result = await window.electron.auth.silentLogin() + const silentLoginResult = await window.electron.auth.silentLogin() + const result = silentLoginResult.data console.log('Silent login result:', result) - if (result.success && result.userInfo) { + if (silentLoginResult.success && result?.success && result.userInfo) { console.log('Silent login success:', result.userInfo) setCurrentUser({ username: result.userInfo.username, @@ -81,8 +83,8 @@ function App(): React.JSX.Element { if (result.requiresUserSelection) { console.log('Admin user needs to select user') // Load all users for selection - const users = await window.electron.auth.getAllUsers() - setAllUsers(users) + const usersResult = await window.electron.auth.getAllUsers() + setAllUsers(usersResult.success && usersResult.data ? usersResult.data : []) setShowUserSelection(true) } else { console.log('Setting authenticated to true') @@ -107,19 +109,20 @@ function App(): React.JSX.Element { const handleLogin = async (username: string, password: string): Promise => { try { const result = await window.electron.auth.login({ username, password }) + const loginData = result.success ? result.data : undefined - if (result.success && result.userInfo) { + if (result.success && loginData?.userInfo) { setCurrentUser({ - username: result.userInfo.username, - userType: result.userInfo.userType + username: loginData.userInfo.username, + userType: loginData.userInfo.userType }) // Check if admin needs user selection - if (result.userInfo.userType === 'Admin') { + if (loginData.userInfo.userType === 'Admin') { setShowLoginDialog(false) // Load all users for selection - const users = await window.electron.auth.getAllUsers() - setAllUsers(users) + const usersResult = await window.electron.auth.getAllUsers() + setAllUsers(usersResult.success && usersResult.data ? usersResult.data : []) setShowUserSelection(true) } else { setIsAuthenticated(true) @@ -144,10 +147,11 @@ function App(): React.JSX.Element { const handleUserSelect = async (user: SelectedUserInfo) => { try { const result = await window.electron.auth.switchUser(user) - if (result.success) { + const switchData = result.success ? result.data : undefined + if (result.success && switchData) { setCurrentUser({ - username: result.userInfo?.username || user.username, - userType: result.userInfo?.userType || user.userType + username: switchData.userInfo?.username || user.username, + userType: switchData.userInfo?.userType || user.userType }) setIsAuthenticated(true) setShowUserSelection(false) diff --git a/src/renderer/src/components/MaterialTypeManagementDialog.tsx b/src/renderer/src/components/MaterialTypeManagementDialog.tsx index a247280..be7b009 100644 --- a/src/renderer/src/components/MaterialTypeManagementDialog.tsx +++ b/src/renderer/src/components/MaterialTypeManagementDialog.tsx @@ -252,10 +252,11 @@ export const MaterialTypeManagementDialog: React.FC void } -/** - * Hook for authentication operations - */ export function useAuth(): UseAuthReturn { const [state, setState] = useState({ loading: false, @@ -51,156 +40,94 @@ export function useAuth(): UseAuthReturn { const login = useCallback(async (credentials: LoginCredentials): Promise => { setState((prev) => ({ ...prev, loading: true, error: null })) + const result = await window.electron.auth.login(credentials) - try { - const result = (await window.electron.ipcRenderer.invoke('auth:login', credentials)) as { - success: boolean - userInfo?: UserInfo - error?: string - } - - if (result.success && result.userInfo) { - setState({ - loading: false, - user: result.userInfo, - error: null, - isAuthenticated: true - }) - return true - } else { - setState((prev) => ({ - ...prev, - loading: false, - error: result.error || 'Login failed' - })) - return false - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - setState((prev) => ({ ...prev, loading: false, error: message })) + if (!result.success || !result.data?.userInfo) { + setState((prev) => ({ ...prev, loading: false, error: result.error ?? 'Login failed' })) return false } + + setState({ + loading: false, + user: result.data.userInfo, + error: null, + isAuthenticated: true + }) + return true }, []) - const silentLogin = useCallback(async (): Promise<{ - success: boolean - requiresUserSelection?: boolean - }> => { + const silentLogin = useCallback(async (): Promise<{ success: boolean; requiresUserSelection?: boolean }> => { setState((prev) => ({ ...prev, loading: true, error: null })) + const result = await window.electron.auth.silentLogin() - try { - const result = (await window.electron.ipcRenderer.invoke('auth:silentLogin')) as { - success: boolean - userInfo?: UserInfo - error?: string - requiresUserSelection?: boolean - } - - if (result.success && result.userInfo) { - setState({ - loading: false, - user: result.userInfo, - error: null, - isAuthenticated: true - }) - return { success: true, requiresUserSelection: result.requiresUserSelection } - } else { - setState((prev) => ({ - ...prev, - loading: false, - error: result.error || null - })) - return { success: false } - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - setState((prev) => ({ ...prev, loading: false, error: message })) + if (!result.success || !result.data?.userInfo) { + setState((prev) => ({ ...prev, loading: false, error: result.error || null })) return { success: false } } + + setState({ + loading: false, + user: result.data.userInfo, + error: null, + isAuthenticated: true + }) + return { success: true, requiresUserSelection: result.data.requiresUserSelection } }, []) const logout = useCallback(async (): Promise => { - try { - await window.electron.ipcRenderer.invoke('auth:logout') - setState({ - loading: false, - user: null, - error: null, - isAuthenticated: false - }) - } catch (error) { - console.error('Logout error:', error) - } + await window.electron.auth.logout() + setState({ + loading: false, + user: null, + error: null, + isAuthenticated: false + }) }, []) const getCurrentUser = useCallback(async (): Promise => { - try { - const result = (await window.electron.ipcRenderer.invoke('auth:getCurrentUser')) as { - isAuthenticated: boolean - userInfo?: UserInfo - } - if (result.isAuthenticated && result.userInfo) { - const userInfo = result.userInfo - setState((prev) => ({ - ...prev, - user: userInfo, - isAuthenticated: true - })) - return userInfo - } - return null - } catch { + const result = await window.electron.auth.getCurrentUser() + if (!result.success || !result.data?.isAuthenticated || !result.data.userInfo) { return null } + + setState((prev) => ({ + ...prev, + user: result.data!.userInfo!, + isAuthenticated: true + })) + return result.data.userInfo }, []) const getAllUsers = useCallback(async (): Promise => { - try { - return (await window.electron.ipcRenderer.invoke('auth:getAllUsers')) as UserInfo[] - } catch { - return [] - } + const result = await window.electron.auth.getAllUsers() + return result.success && result.data ? result.data : [] }, []) const switchUser = useCallback(async (userInfo: UserInfo): Promise => { setState((prev) => ({ ...prev, loading: true, error: null })) + const result = await window.electron.auth.switchUser(userInfo) - try { - const result = (await window.electron.ipcRenderer.invoke('auth:switchUser', userInfo)) as { - success: boolean - userInfo?: UserInfo - error?: string - } - - if (result.success && result.userInfo) { - setState({ - loading: false, - user: result.userInfo, - error: null, - isAuthenticated: true - }) - return true - } else { - setState((prev) => ({ - ...prev, - loading: false, - error: result.error || 'Switch user failed' - })) - return false - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - setState((prev) => ({ ...prev, loading: false, error: message })) + if (!result.success || !result.data?.userInfo) { + setState((prev) => ({ + ...prev, + loading: false, + error: result.error || 'Switch user failed' + })) return false } + + setState({ + loading: false, + user: result.data.userInfo, + error: null, + isAuthenticated: true + }) + return true }, []) const isAdmin = useCallback(async (): Promise => { - try { - return (await window.electron.ipcRenderer.invoke('auth:isAdmin')) as boolean - } catch { - return false - } + const result = await window.electron.auth.isAdmin() + return result.success && Boolean(result.data) }, []) const reset = useCallback(() => { diff --git a/src/renderer/src/hooks/useCleaner.ts b/src/renderer/src/hooks/useCleaner.ts index 57ef792..5d40ff2 100644 --- a/src/renderer/src/hooks/useCleaner.ts +++ b/src/renderer/src/hooks/useCleaner.ts @@ -85,8 +85,10 @@ export function useCleaner() { useEffect(() => { const initializePage = async () => { try { - const admin = await window.electron.auth.isAdmin() - const user = await window.electron.auth.getCurrentUser() + const adminResult = await window.electron.auth.isAdmin() + const userResult = await window.electron.auth.getCurrentUser() + const admin = adminResult.success && Boolean(adminResult.data) + const user = userResult.success ? userResult.data : undefined setIsAdmin(admin) if (user && user.userInfo) { setCurrentUsername(user.userInfo.username) @@ -98,13 +100,16 @@ export function useCleaner() { // Load managers if (admin) { const resp = await window.electron.materials.getManagers() - setManagers(resp.managers) - setSelectedManagers(new Set(resp.managers)) + const managersPayload = resp.success ? (resp.data as { managers: string[] } | undefined) : undefined + const managerList = managersPayload?.managers ?? [] + setManagers(managerList) + setSelectedManagers(new Set(managerList)) } // Get shared Production IDs const result = await window.electron.validation.getSharedProductionIds() - setSharedProductionIdsCount(result.productionIds.length) + const idsPayload = result.success ? (result.data as { productionIds?: string[] } | undefined) : undefined + setSharedProductionIdsCount(idsPayload?.productionIds?.length ?? 0) } catch (err) { console.error('Initialization failed:', err) } @@ -159,21 +164,28 @@ export function useCleaner() { mode: valMode === 'full' ? 'database_full' : 'database_filtered', useSharedProductionIds: valMode === 'filtered' }) + const validationData = response.success ? (response.data as any) : null - if (response.success && response.results) { - setValidationResults(response.results) - const markedCodes = new Set( - response.results.filter((r) => r.isMarkedForDeletion).map((r) => r.materialCode) + if (response.success && validationData?.success && validationData.results) { + setValidationResults(validationData.results) + const markedCodes = new Set( + validationData.results + .filter((r: ValidationResult) => r.isMarkedForDeletion) + .map((r: ValidationResult) => r.materialCode) ) setSelectedItems(markedCodes) if (isAdmin) { - const uniqueManagers = new Set(response.results.map((r) => r.managerName).filter(Boolean)) - setManagers([...uniqueManagers]) + const uniqueManagers = new Set( + validationData.results + .map((r: ValidationResult) => r.managerName) + .filter((name: string) => Boolean(name)) + ) + setManagers(Array.from(uniqueManagers)) setSelectedManagers(uniqueManagers) } } else { - alert(response.error || '校验失败') + alert(response.error || validationData?.error || '校验失败') } } catch (err) { alert(err instanceof Error ? err.message : '校验过程中发生未知错误') @@ -285,14 +297,16 @@ export function useCleaner() { if (materialsToUpsert.length > 0) { const res = await window.electron.materials.upsertBatch(materialsToUpsert) + const payload = res.success ? (res.data as { stats?: { success?: number } } | undefined) : undefined if (!res.success) throw new Error(res.error || '写入物料失败') - msgParts.push(`写入/更新成功:${res.stats?.success || 0} 条`) + msgParts.push(`写入/更新成功:${payload?.stats?.success || 0} 条`) } if (materialsToDelete.length > 0) { const res = await window.electron.materials.delete(materialsToDelete) + const payload = res.success ? (res.data as { count?: number } | undefined) : undefined if (!res.success) throw new Error(res.error || '删除物料失败') - msgParts.push(`删除成功:${res.count || 0} 条`) + msgParts.push(`删除成功:${payload?.count || 0} 条`) } alert(`操作完成!\n\n${msgParts.join('\n')}`) @@ -300,7 +314,8 @@ export function useCleaner() { // Reload managers if admin if (isAdmin) { const resp = await window.electron.materials.getManagers() - setManagers(resp.managers) + const payload = resp.success ? (resp.data as { managers?: string[] } | undefined) : undefined + setManagers(payload?.managers ?? []) } } catch (err) { alert(err instanceof Error ? err.message : '操作失败') @@ -331,12 +346,13 @@ export function useCleaner() { try { const cleanerDataResult = await window.electron.validation.getCleanerData() - if (!cleanerDataResult.success) { + const cleanerData = cleanerDataResult.success ? (cleanerDataResult.data as any) : null + if (!cleanerDataResult.success || cleanerData?.success === false) { throw new Error(cleanerDataResult.error || '获取清理数据失败') } - const orderNumberList = cleanerDataResult.orderNumbers || [] - const materialCodeList = cleanerDataResult.materialCodes || [] + const orderNumberList = cleanerData?.orderNumbers || [] + const materialCodeList = cleanerData?.materialCodes || [] if (orderNumberList.length === 0) throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。') @@ -349,13 +365,14 @@ export function useCleaner() { dryRun, headless }) + const cleanerRunData = response.success ? (response.data as any) : null - if (response.success && response.data) { + if (response.success && cleanerRunData) { setReportData({ - ordersProcessed: response.data.ordersProcessed, - materialsDeleted: response.data.materialsDeleted, - materialsSkipped: response.data.materialsSkipped, - errors: response.data.errors + ordersProcessed: cleanerRunData.ordersProcessed, + materialsDeleted: cleanerRunData.materialsDeleted, + materialsSkipped: cleanerRunData.materialsSkipped, + errors: cleanerRunData.errors }) } else { throw new Error(response.error || '清理失败') @@ -390,11 +407,12 @@ export function useCleaner() { })) const response = await window.electron.cleaner.exportResults(exportItems) + const exportData = response.success ? (response.data as any) : null - if (response.success) { - alert(`导出成功!\n文件已保存到:${response.filePath}`) + if (response.success && exportData?.success !== false) { + alert(`导出成功!\n文件已保存到:${exportData?.filePath ?? ''}`) } else { - throw new Error(response.error || '导出失败') + throw new Error(response.error || exportData?.error || '导出失败') } } catch (err) { alert(err instanceof Error ? err.message : '导出过程中发生错误') diff --git a/src/renderer/src/hooks/useValidation.ts b/src/renderer/src/hooks/useValidation.ts index 42f0ea2..242324f 100644 --- a/src/renderer/src/hooks/useValidation.ts +++ b/src/renderer/src/hooks/useValidation.ts @@ -1,13 +1,5 @@ -/** - * IPC Hook for Validation operations - * - * Provides a React-friendly interface for validation IPC calls - * with loading state, error handling, and data management. - */ - import { useState, useCallback } from 'react' -// Types based on validation types interface ValidationRequest { mode: 'database_full' | 'database_filtered' productionIdFile?: string @@ -52,9 +44,6 @@ interface UseValidationReturn extends UseValidationState { reset: () => void } -/** - * Hook for validation operations - */ export function useValidation(): UseValidationReturn { const [state, setState] = useState({ loading: false, @@ -63,82 +52,62 @@ export function useValidation(): UseValidationReturn { error: null }) - const validate = useCallback( - async (request: ValidationRequest): Promise => { - setState((prev) => ({ ...prev, loading: true, error: null })) + const validate = useCallback(async (request: ValidationRequest): Promise => { + setState((prev) => ({ ...prev, loading: true, error: null })) + const response = await window.electron.validation.validate(request) + if (!response.success || !response.data) { + setState((prev) => ({ ...prev, loading: false, error: response.error || 'Validation failed' })) + return response.success ? null : { success: false, error: response.error } + } - try { - const result = (await window.electron.ipcRenderer.invoke( - 'validation:validate', - request - )) as ValidationResponse + const result = response.data as ValidationResponse + if (!result.success) { + setState((prev) => ({ ...prev, loading: false, error: result.error || 'Validation failed' })) + return result + } - if (result.success) { - const results = result.results || null - const stats = result.stats || null - setState({ - loading: false, - data: results, - stats: stats, - error: null - }) - return result - } else { - setState((prev) => ({ - ...prev, - loading: false, - error: result.error || 'Validation failed' - })) - return result - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - setState((prev) => ({ ...prev, loading: false, error: message })) - return null - } - }, - [] - ) + setState({ + loading: false, + data: result.results || null, + stats: result.stats || null, + error: null + }) + return result + }, []) const setSharedProductionIds = useCallback(async (ids: string[]): Promise => { - try { - await window.electron.ipcRenderer.invoke('validation:setSharedProductionIds', ids) - } catch (error) { - console.error('Failed to set shared production IDs:', error) - } + await window.electron.validation.setSharedProductionIds(ids) }, []) const getSharedProductionIds = useCallback(async (): Promise => { - try { - const result = (await window.electron.ipcRenderer.invoke( - 'validation:getSharedProductionIds' - )) as { productionIds?: string[] } - return result?.productionIds || [] - } catch { + const result = await window.electron.validation.getSharedProductionIds() + if (!result.success || !result.data) { return [] } + const payload = result.data as { productionIds?: string[] } + return payload.productionIds || [] }, []) - const getCleanerData = useCallback(async (): Promise<{ - orderNumbers: string[] - materialCodes: string[] - } | null> => { - try { - const result = (await window.electron.ipcRenderer.invoke('validation:getCleanerData')) as { - success: boolean - orderNumbers?: string[] - materialCodes?: string[] - } - if (result.success) { - return { - orderNumbers: result.orderNumbers || [], - materialCodes: result.materialCodes || [] - } - } + const getCleanerData = useCallback(async (): Promise<{ orderNumbers: string[]; materialCodes: string[] } | null> => { + const result = await window.electron.validation.getCleanerData() + if (!result.success || !result.data) { return null - } catch { + } + + const payload = result.data as { + success?: boolean + orderNumbers?: string[] + materialCodes?: string[] + } + + if (payload.success === false) { return null } + + return { + orderNumbers: payload.orderNumbers || [], + materialCodes: payload.materialCodes || [] + } }, []) const reset = useCallback(() => { diff --git a/src/renderer/src/pages/SettingsPage.tsx b/src/renderer/src/pages/SettingsPage.tsx index 716e0cc..aeb3f48 100644 --- a/src/renderer/src/pages/SettingsPage.tsx +++ b/src/renderer/src/pages/SettingsPage.tsx @@ -26,14 +26,17 @@ const SettingsPage: React.FC = () => { try { setIsLoading(true) // ERP credentials are loaded from database (current user's config) - const config = await window.electron.settings.getSettings() + const response = await window.electron.settings.getSettings() + const config = response.success ? (response.data as { erp?: ErpCredentials } | undefined) : undefined // Extract ERP credentials from the config - if (config && (config as any).erp) { + if (config?.erp) { setCredentials({ - username: (config as any).erp.username || '', - password: (config as any).erp.password || '' + username: config.erp.username || '', + password: config.erp.password || '' }) + } else if (!response.success) { + showMessage('error', response.error || '加载 ERP 配置失败') } setIsModified(false) } catch (error) { @@ -56,13 +59,14 @@ const SettingsPage: React.FC = () => { username: credentials.username, password: credentials.password } - } as any) + }) + const saveData = result.success ? (result.data as { success?: boolean; error?: string } | undefined) : undefined - if (result.success) { + if (result.success && saveData?.success !== false) { setIsModified(false) showMessage('success', 'ERP 账号密码保存成功') } else { - showMessage('error', result.error || '保存失败') + showMessage('error', result.error || saveData?.error || '保存失败') } } catch (error) { showMessage('error', '保存配置时发生错误') diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts index 7de76d9..ba08ae2 100644 --- a/src/shared/ipc-channels.ts +++ b/src/shared/ipc-channels.ts @@ -9,17 +9,77 @@ export const IPC_CHANNELS = { FILE_WRITE: 'file:write', FILE_EXISTS: 'file:exists', FILE_LIST: 'file:list', + FILE_OPEN_PATH: 'file:openPath', // Extractor service EXTRACTOR_RUN: 'extractor:run', + EXTRACTOR_PROGRESS: 'extractor:progress', + EXTRACTOR_LOG: 'extractor:log', // Cleaner service CLEANER_RUN: 'cleaner:run', CLEANER_EXPORT_RESULTS: 'cleaner:exportResults', + CLEANER_PROGRESS: 'cleaner:progress', // Database service - MySQL DATABASE_MYSQL_CONNECT: 'database:mysql:connect', DATABASE_MYSQL_DISCONNECT: 'database:mysql:disconnect', DATABASE_MYSQL_IS_CONNECTED: 'database:mysql:isConnected', - DATABASE_MYSQL_QUERY: 'database:mysql:query' + DATABASE_MYSQL_QUERY: 'database:mysql:query', + + // Database service - SQL Server + DATABASE_SQLSERVER_CONNECT: 'database:sqlserver:connect', + DATABASE_SQLSERVER_DISCONNECT: 'database:sqlserver:disconnect', + DATABASE_SQLSERVER_IS_CONNECTED: 'database:sqlserver:isConnected', + DATABASE_SQLSERVER_QUERY: 'database:sqlserver:query', + + // Resolver + RESOLVER_RESOLVE: 'resolver:resolve', + RESOLVER_VALIDATE_FORMAT: 'resolver:validateFormat', + + // Auth + AUTH_GET_COMPUTER_NAME: 'auth:getComputerName', + AUTH_SILENT_LOGIN: 'auth:silentLogin', + AUTH_LOGIN: 'auth:login', + AUTH_LOGOUT: 'auth:logout', + AUTH_GET_CURRENT_USER: 'auth:getCurrentUser', + AUTH_GET_ALL_USERS: 'auth:getAllUsers', + AUTH_SWITCH_USER: 'auth:switchUser', + AUTH_IS_ADMIN: 'auth:isAdmin', + + // Validation + VALIDATION_VALIDATE: 'validation:validate', + VALIDATION_SET_SHARED_PRODUCTION_IDS: 'validation:setSharedProductionIds', + VALIDATION_GET_SHARED_PRODUCTION_IDS: 'validation:getSharedProductionIds', + VALIDATION_GET_CLEANER_DATA: 'validation:getCleanerData', + + // Materials + MATERIALS_UPSERT_BATCH: 'materials:upsertBatch', + MATERIALS_DELETE: 'materials:delete', + MATERIALS_GET_MANAGERS: 'materials:getManagers', + MATERIALS_GET_BY_MANAGER: 'materials:getByManager', + MATERIALS_GET_ALL: 'materials:getAll', + MATERIALS_GET_STATISTICS: 'materials:getStatistics', + MATERIALS_UPDATE_MANAGER: 'materials:updateManager', + + // Settings + SETTINGS_GET_USER_TYPE: 'settings:getUserType', + SETTINGS_GET_SETTINGS: 'settings:getSettings', + SETTINGS_SAVE_SETTINGS: 'settings:saveSettings', + SETTINGS_RESET_DEFAULTS: 'settings:resetDefaults', + SETTINGS_TEST_DB_CONNECTION: 'settings:testDbConnection', + + // Material type + MATERIAL_TYPE_GET_ALL: 'materialType:getAll', + MATERIAL_TYPE_GET_BY_MANAGER: 'materialType:getByManager', + MATERIAL_TYPE_GET_MANAGERS: 'materialType:getManagers', + MATERIAL_TYPE_UPSERT: 'materialType:upsert', + MATERIAL_TYPE_DELETE: 'materialType:delete', + MATERIAL_TYPE_UPSERT_BATCH: 'materialType:upsertBatch', + + // User ERP config + USER_ERP_CONFIG_GET_CURRENT: 'user-erp-config:getCurrent', + USER_ERP_CONFIG_UPDATE: 'user-erp-config:update', + USER_ERP_CONFIG_TEST_CONNECTION: 'user-erp-config:testConnection', + USER_ERP_CONFIG_GET_ALL: 'user-erp-config:getAll' } as const diff --git a/tests/unit/file-ipc-paths.test.ts b/tests/unit/file-ipc-paths.test.ts new file mode 100644 index 0000000..f5bf847 --- /dev/null +++ b/tests/unit/file-ipc-paths.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' +import path from 'path' +import { isPathWithinAllowedRoots } from '../../src/main/ipc/file-handler' + +describe('File IPC Path Guard', () => { + it('accepts path under allowed root', () => { + const root = path.resolve('C:/safe/root') + const file = path.join(root, 'nested', 'file.txt') + expect(isPathWithinAllowedRoots(file, [root])).toBe(true) + }) + + it('rejects path outside allowed roots', () => { + const root = path.resolve('C:/safe/root') + const outside = path.resolve('C:/other/location/file.txt') + expect(isPathWithinAllowedRoots(outside, [root])).toBe(false) + }) +}) diff --git a/tests/unit/ipc-index.test.ts b/tests/unit/ipc-index.test.ts new file mode 100644 index 0000000..636e19a --- /dev/null +++ b/tests/unit/ipc-index.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest' +import { withErrorHandling } from '../../src/main/ipc' +import { ValidationError } from '../../src/main/types/errors' + +describe('IPC Result Wrapper', () => { + it('wraps successful values into IpcResult.data', async () => { + const result = await withErrorHandling(async () => ({ value: 42 }), 'test:success') + expect(result.success).toBe(true) + expect(result.data).toEqual({ value: 42 }) + }) + + it('wraps exceptions into IpcResult.error/code', async () => { + const result = await withErrorHandling(async () => { + throw new ValidationError('Invalid input', 'VAL_INVALID_INPUT') + }, 'test:failure') + + expect(result.success).toBe(false) + expect(result.error).toBe('Invalid input') + expect(result.code).toBe('VAL_INVALID_INPUT') + }) +}) diff --git a/tests/unit/preload-surface.test.ts b/tests/unit/preload-surface.test.ts new file mode 100644 index 0000000..77547e5 --- /dev/null +++ b/tests/unit/preload-surface.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest' +import fs from 'fs' +import path from 'path' + +describe('Preload API Surface', () => { + it('does not expose ipcRenderer in window typings', () => { + const dtsPath = path.resolve(process.cwd(), 'src/preload/index.d.ts') + const content = fs.readFileSync(dtsPath, 'utf-8') + expect(content.includes('ipcRenderer:')).toBe(false) + }) +})