refactor(ipc): harden channels and unify IPC contracts
This commit is contained in:
@@ -24,7 +24,7 @@ function createWindow(): void {
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
sandbox: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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<string> => {
|
||||
const os = await import('os')
|
||||
return os.hostname()
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME, async (): Promise<IpcResult<string>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const os = await import('os')
|
||||
return os.hostname()
|
||||
}, 'auth:getComputerName')
|
||||
})
|
||||
|
||||
/**
|
||||
* Silent login by computer name
|
||||
*/
|
||||
ipcMain.handle('auth:silentLogin', async (): Promise<SilentLoginResponse> => {
|
||||
try {
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_SILENT_LOGIN, async (): Promise<IpcResult<SilentLoginResponse>> => {
|
||||
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<LoginResponse> => {
|
||||
try {
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.AUTH_LOGIN,
|
||||
async (_event, request: LoginRequest): Promise<IpcResult<LoginResponse>> => {
|
||||
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<void> => {
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
log.info('User logout', { username: userInfo?.username })
|
||||
sessionManager.logout()
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async (): Promise<IpcResult<void>> => {
|
||||
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<CurrentUserResponse> => {
|
||||
const isAuthenticated = sessionManager.isAuthenticated()
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
userInfo: userInfo ?? undefined
|
||||
}
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_GET_CURRENT_USER, async (): Promise<IpcResult<CurrentUserResponse>> => {
|
||||
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<UserInfo[]> => {
|
||||
log.debug('Fetching all users for admin selection')
|
||||
return await sessionManager.getAllUsers()
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_GET_ALL_USERS, async (): Promise<IpcResult<UserInfo[]>> => {
|
||||
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<UserSelectionResponse> => {
|
||||
try {
|
||||
IPC_CHANNELS.AUTH_SWITCH_USER,
|
||||
async (_event, userInfo: UserInfo): Promise<IpcResult<UserSelectionResponse>> => {
|
||||
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<boolean> => {
|
||||
return sessionManager.isAdmin()
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_IS_ADMIN, async (): Promise<IpcResult<boolean>> => {
|
||||
return withErrorHandling(async () => sessionManager.isAdmin(), 'auth:isAdmin')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<CleanerProgress>
|
||||
@@ -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<IpcResult<CleanerResult>> => {
|
||||
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<ExportResultResponse> => {
|
||||
try {
|
||||
IPC_CHANNELS.CLEANER_EXPORT_RESULTS,
|
||||
async (_event, items: ExportResultItem[]): Promise<IpcResult<ExportResultResponse>> => {
|
||||
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')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<string, MySqlService>()
|
||||
|
||||
// Store SQL Server service instances per window/connection
|
||||
const sqlServerServices = new Map<string, SqlServerService>()
|
||||
const cleanupBoundWindows = new Set<string>()
|
||||
|
||||
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<void> => {
|
||||
try {
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.DATABASE_MYSQL_CONNECT,
|
||||
async (event, config: MySqlConfig): Promise<IpcResult<void>> => {
|
||||
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<void> => {
|
||||
try {
|
||||
ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT, async (event): Promise<IpcResult<void>> => {
|
||||
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<boolean> => {
|
||||
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<IpcResult<boolean>> => {
|
||||
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<MySqlQueryResult> => {
|
||||
try {
|
||||
IPC_CHANNELS.DATABASE_MYSQL_QUERY,
|
||||
async (event, sql: string, params?: unknown[]): Promise<IpcResult<MySqlQueryResult>> => {
|
||||
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<void> => {
|
||||
try {
|
||||
IPC_CHANNELS.DATABASE_SQLSERVER_CONNECT,
|
||||
async (event, config: SqlServerConfig): Promise<IpcResult<void>> => {
|
||||
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<void> => {
|
||||
try {
|
||||
ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT, async (event): Promise<IpcResult<void>> => {
|
||||
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<boolean> => {
|
||||
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<IpcResult<boolean>> => {
|
||||
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<string, unknown>
|
||||
): Promise<SqlServerQueryResult> => {
|
||||
try {
|
||||
): Promise<IpcResult<SqlServerQueryResult>> => {
|
||||
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')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<ExtractionProgress>
|
||||
): 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<IpcResult<ExtractorResult>> => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> => {
|
||||
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<IpcResult<string>> => {
|
||||
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<void> => {
|
||||
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<IpcResult<void>> => {
|
||||
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<IpcResult<boolean>> => {
|
||||
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<boolean> => {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('file:list', async (_event, dirPath: string): Promise<string[]> => {
|
||||
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<IpcResult<string[]>> => {
|
||||
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<void> => {
|
||||
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<IpcResult<void>> => {
|
||||
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')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ export interface IpcResult<T = unknown> {
|
||||
code?: string
|
||||
}
|
||||
|
||||
export function ok<T>(data: T): IpcResult<T> {
|
||||
return { success: true, data }
|
||||
}
|
||||
|
||||
export function fail<T = unknown>(error: string, code?: string): IpcResult<T> {
|
||||
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<T>(
|
||||
context: string
|
||||
): Promise<IpcResult<T>> {
|
||||
return handler()
|
||||
.then((data) => {
|
||||
.then((data): IpcResult<T> => {
|
||||
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<T>(
|
||||
log.debug(`[${context}] Stack trace:`, { stack: error.stack })
|
||||
}
|
||||
|
||||
return { success: false, error: message, code }
|
||||
return fail<T>(message, code)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IpcResult<MaterialTypeRecord[]>> => {
|
||||
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<IpcResult<MaterialTypeRecord[]>> => {
|
||||
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<IpcResult<string[]>> => {
|
||||
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<IpcResult<{ updated: boolean }>> => {
|
||||
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<IpcResult<{ deleted: boolean }>> => {
|
||||
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<IpcResult<{ stats: { total: number; success: number; failed: number } }>> => {
|
||||
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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ResolverResponse> => {
|
||||
IPC_CHANNELS.RESOLVER_RESOLVE,
|
||||
async (_event, input: ResolverInput): Promise<IpcResult<ResolverResponse>> => {
|
||||
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<IpcResult<Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>>> => {
|
||||
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')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<UserType> => {
|
||||
return (sessionManager.getUserType() as UserType) || 'Guest'
|
||||
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_USER_TYPE, async (): Promise<IpcResult<UserType>> => {
|
||||
return withErrorHandling(
|
||||
async () => (sessionManager.getUserType() as UserType) || 'Guest',
|
||||
'settings:getUserType'
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get ERP credentials for current user
|
||||
*/
|
||||
ipcMain.handle('settings:getSettings', async (): Promise<any> => {
|
||||
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<SaveSettingsResult> => {
|
||||
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<IpcResult<{ erp: { username: string; password: string } }>> => {
|
||||
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<SaveSettingsResult> => {
|
||||
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<IpcResult<SaveSettingsResult>> => {
|
||||
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<ConnectionTestResult> => {
|
||||
try {
|
||||
log.info('Testing database connection')
|
||||
const config = configManager.getConfig()
|
||||
const dbType = config.database.activeType
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.SETTINGS_RESET_DEFAULTS,
|
||||
async (): Promise<IpcResult<SaveSettingsResult>> => {
|
||||
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<IpcResult<ConnectionTestResult>> => {
|
||||
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')
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ErpConfigResponse> => {
|
||||
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<ErpConfigResponse> => {
|
||||
try {
|
||||
log.info('Updating current user ERP credentials', {
|
||||
username: credentials.username
|
||||
})
|
||||
IPC_CHANNELS.USER_ERP_CONFIG_GET_CURRENT,
|
||||
async (): Promise<IpcResult<ErpConfigResponse>> => {
|
||||
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<ConnectionTestResult> => {
|
||||
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<IpcResult<ErpConfigResponse>> => {
|
||||
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<IpcResult<ConnectionTestResult>> => {
|
||||
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')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string>()
|
||||
const sharedProductionIdsBySender = new Map<number, Set<string>>()
|
||||
|
||||
/**
|
||||
* 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<ValidationResponse> => {
|
||||
IPC_CHANNELS.VALIDATION_VALIDATE,
|
||||
async (event, request: ValidationRequest): Promise<ValidationResponse> => {
|
||||
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<MaterialOperationResponse> => {
|
||||
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<MaterialOperationResponse> => {
|
||||
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<void> => {
|
||||
IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS,
|
||||
async (event, productionIds: string[]): Promise<void> => {
|
||||
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) {
|
||||
|
||||
@@ -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<string>
|
||||
writeFile: (filePath: string, content: string) => Promise<void>
|
||||
fileExists: (filePath: string) => Promise<boolean>
|
||||
listFiles: (dirPath: string) => Promise<string[]>
|
||||
openPath: (filePath: string) => Promise<void>
|
||||
readFile: (filePath: string) => Promise<IpcResult<string>>
|
||||
writeFile: (filePath: string, content: string) => Promise<IpcResult<void>>
|
||||
fileExists: (filePath: string) => Promise<IpcResult<boolean>>
|
||||
listFiles: (dirPath: string) => Promise<IpcResult<string[]>>
|
||||
openPath: (filePath: string) => Promise<IpcResult<void>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +78,7 @@ export interface ExtractorAPI {
|
||||
*/
|
||||
runExtractor: (
|
||||
input: ExtractorInput
|
||||
) => Promise<{ success: boolean; data?: ExtractorResult; error?: string }>
|
||||
) => Promise<IpcResult<ExtractorResult>>
|
||||
/**
|
||||
* 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<IpcResult<CleanerResult>>
|
||||
|
||||
/**
|
||||
* Export validation results to Excel
|
||||
* @param items - Validation result items to export
|
||||
*/
|
||||
exportResults: (items: ExportResultItem[]) => Promise<ExportResultResponse>
|
||||
exportResults: (items: ExportResultItem[]) => Promise<IpcResult<ExportResultResponse>>
|
||||
|
||||
/**
|
||||
* Subscribe to progress updates
|
||||
@@ -126,45 +127,48 @@ export interface DatabaseAPI {
|
||||
* Connect to MySQL database
|
||||
* @param config - MySQL connection config
|
||||
*/
|
||||
connectMySql: (config: MySqlConfig) => Promise<void>
|
||||
connectMySql: (config: MySqlConfig) => Promise<IpcResult<void>>
|
||||
|
||||
/**
|
||||
* Disconnect from MySQL database
|
||||
*/
|
||||
disconnectMySql: () => Promise<void>
|
||||
disconnectMySql: () => Promise<IpcResult<void>>
|
||||
|
||||
/**
|
||||
* Check if MySQL is connected
|
||||
*/
|
||||
isMySqlConnected: () => Promise<boolean>
|
||||
isMySqlConnected: () => Promise<IpcResult<boolean>>
|
||||
|
||||
/**
|
||||
* Execute MySQL query
|
||||
* @param sql - SQL query
|
||||
* @param params - Query parameters
|
||||
*/
|
||||
queryMySql: (sql: string, params?: unknown[]) => Promise<MySqlQueryResult>
|
||||
queryMySql: (sql: string, params?: unknown[]) => Promise<IpcResult<MySqlQueryResult>>
|
||||
|
||||
/**
|
||||
* Connect to SQL Server database
|
||||
* @param config - SQL Server connection config
|
||||
*/
|
||||
connectSqlServer: (config: SqlServerConfig) => Promise<void>
|
||||
connectSqlServer: (config: SqlServerConfig) => Promise<IpcResult<void>>
|
||||
|
||||
/**
|
||||
* Disconnect from SQL Server database
|
||||
*/
|
||||
disconnectSqlServer: () => Promise<void>
|
||||
disconnectSqlServer: () => Promise<IpcResult<void>>
|
||||
|
||||
/**
|
||||
* Check if SQL Server is connected
|
||||
*/
|
||||
isSqlServerConnected: () => Promise<boolean>
|
||||
isSqlServerConnected: () => Promise<IpcResult<boolean>>
|
||||
|
||||
/**
|
||||
* Execute SQL Server query
|
||||
* @param sql - SQL query
|
||||
* @param params - Query parameters
|
||||
*/
|
||||
querySqlServer: (sql: string, params?: Record<string, unknown>) => Promise<SqlServerQueryResult>
|
||||
querySqlServer: (
|
||||
sql: string,
|
||||
params?: Record<string, unknown>
|
||||
) => Promise<IpcResult<SqlServerQueryResult>>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user