refactor(ipc): harden channels and unify IPC contracts

This commit is contained in:
test
2026-03-08 12:35:00 +08:00
parent 616e2b31a5
commit 8f3e5273e2
25 changed files with 991 additions and 1263 deletions

View File

@@ -24,7 +24,7 @@ function createWindow(): void {
...(process.platform === 'linux' ? { icon } : {}), ...(process.platform === 'linux' ? { icon } : {}),
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),
sandbox: false sandbox: true
} }
}) })

View File

@@ -14,6 +14,9 @@ import { ipcMain } from 'electron'
import { SessionManager } from '../services/user/session-manager' import { SessionManager } from '../services/user/session-manager'
import { createLogger } from '../services/logger' import { createLogger } from '../services/logger'
import type { UserInfo } from '../types/user.types' 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') const log = createLogger('AuthHandler')
@@ -70,16 +73,18 @@ export function registerAuthHandlers(): void {
/** /**
* Get computer name * Get computer name
*/ */
ipcMain.handle('auth:getComputerName', async (): Promise<string> => { ipcMain.handle(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME, async (): Promise<IpcResult<string>> => {
const os = await import('os') return withErrorHandling(async () => {
return os.hostname() const os = await import('os')
return os.hostname()
}, 'auth:getComputerName')
}) })
/** /**
* Silent login by computer name * Silent login by computer name
*/ */
ipcMain.handle('auth:silentLogin', async (): Promise<SilentLoginResponse> => { ipcMain.handle(IPC_CHANNELS.AUTH_SILENT_LOGIN, async (): Promise<IpcResult<SilentLoginResponse>> => {
try { return withErrorHandling(async () => {
log.info('Attempting silent login') log.info('Attempting silent login')
const success = await sessionManager.loginByComputerName() const success = await sessionManager.loginByComputerName()
const userInfo = sessionManager.getUserInfo() const userInfo = sessionManager.getUserInfo()
@@ -101,34 +106,22 @@ export function registerAuthHandlers(): void {
} }
} }
log.warn('Silent login failed - no matching user') throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT')
return { }, 'auth:silentLogin')
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}`
}
}
}) })
/** /**
* Login with username and password * Login with username and password
*/ */
ipcMain.handle('auth:login', async (_event, request: LoginRequest): Promise<LoginResponse> => { ipcMain.handle(
try { IPC_CHANNELS.AUTH_LOGIN,
async (_event, request: LoginRequest): Promise<IpcResult<LoginResponse>> => {
return withErrorHandling(async () => {
const { username, password } = request const { username, password } = request
if (!username || !password) { if (!username || !password) {
log.warn('Login attempt with missing credentials') log.warn('Login attempt with missing credentials')
return { throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED')
success: false,
error: '请输入用户名和密码'
}
} }
log.info('Login attempt', { username }) log.info('Login attempt', { username })
@@ -144,57 +137,53 @@ export function registerAuthHandlers(): void {
} }
log.warn('Login failed - invalid credentials', { username }) log.warn('Login failed - invalid credentials', { username })
return { throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT')
success: false, }, 'auth:login')
error: '用户名或密码错误'
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Login error', { error: message })
return {
success: false,
error: `登录失败:${message}`
}
} }
}) )
/** /**
* Logout * Logout
*/ */
ipcMain.handle('auth:logout', async (): Promise<void> => { ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async (): Promise<IpcResult<void>> => {
const userInfo = sessionManager.getUserInfo() return withErrorHandling(async () => {
log.info('User logout', { username: userInfo?.username }) const userInfo = sessionManager.getUserInfo()
sessionManager.logout() log.info('User logout', { username: userInfo?.username })
sessionManager.logout()
}, 'auth:logout')
}) })
/** /**
* Get current user * Get current user
*/ */
ipcMain.handle('auth:getCurrentUser', async (): Promise<CurrentUserResponse> => { ipcMain.handle(IPC_CHANNELS.AUTH_GET_CURRENT_USER, async (): Promise<IpcResult<CurrentUserResponse>> => {
const isAuthenticated = sessionManager.isAuthenticated() return withErrorHandling(async () => {
const userInfo = sessionManager.getUserInfo() const isAuthenticated = sessionManager.isAuthenticated()
const userInfo = sessionManager.getUserInfo()
return { return {
isAuthenticated, isAuthenticated,
userInfo: userInfo ?? undefined userInfo: userInfo ?? undefined
} }
}, 'auth:getCurrentUser')
}) })
/** /**
* Get all users (for admin user selection) * Get all users (for admin user selection)
*/ */
ipcMain.handle('auth:getAllUsers', async (): Promise<UserInfo[]> => { ipcMain.handle(IPC_CHANNELS.AUTH_GET_ALL_USERS, async (): Promise<IpcResult<UserInfo[]>> => {
log.debug('Fetching all users for admin selection') return withErrorHandling(async () => {
return await sessionManager.getAllUsers() log.debug('Fetching all users for admin selection')
return await sessionManager.getAllUsers()
}, 'auth:getAllUsers')
}) })
/** /**
* Switch user (admin only) * Switch user (admin only)
*/ */
ipcMain.handle( ipcMain.handle(
'auth:switchUser', IPC_CHANNELS.AUTH_SWITCH_USER,
async (_event, userInfo: UserInfo): Promise<UserSelectionResponse> => { async (_event, userInfo: UserInfo): Promise<IpcResult<UserSelectionResponse>> => {
try { return withErrorHandling(async () => {
log.info('User switch attempt', { targetUser: userInfo.username }) log.info('User switch attempt', { targetUser: userInfo.username })
const success = sessionManager.switchUser(userInfo) const success = sessionManager.switchUser(userInfo)
@@ -208,25 +197,16 @@ export function registerAuthHandlers(): void {
} }
log.warn('User switch failed') log.warn('User switch failed')
return { throw new ValidationError('用户切换失败', 'VAL_INVALID_INPUT')
success: false, }, 'auth:switchUser')
error: '用户切换失败'
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('User switch error', { error: message })
return {
success: false,
error: `用户切换失败:${message}`
}
}
} }
) )
/** /**
* Check if current user is admin * Check if current user is admin
*/ */
ipcMain.handle('auth:isAdmin', async (): Promise<boolean> => { ipcMain.handle(IPC_CHANNELS.AUTH_IS_ADMIN, async (): Promise<IpcResult<boolean>> => {
return sessionManager.isAdmin() return withErrorHandling(async () => sessionManager.isAdmin(), 'auth:isAdmin')
}) })
} }

View File

@@ -1,4 +1,4 @@
import { ipcMain, webContents } from 'electron' import { ipcMain, type WebContents } from 'electron'
import { ErpAuthService } from '../services/erp/erp-auth' import { ErpAuthService } from '../services/erp/erp-auth'
import { CleanerService } from '../services/erp/cleaner' import { CleanerService } from '../services/erp/cleaner'
import { OrderNumberResolver } from '../services/erp/order-resolver' import { OrderNumberResolver } from '../services/erp/order-resolver'
@@ -19,11 +19,12 @@ import type {
ExportResultResponse ExportResultResponse
} from '../types/cleaner.types' } from '../types/cleaner.types'
import { UserErpConfigService } from '../services/user/user-erp-config-service' import { UserErpConfigService } from '../services/user/user-erp-config-service'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
const log = createLogger('CleanerHandler') const log = createLogger('CleanerHandler')
function sendProgress( function sendProgress(
windowId: number, sender: WebContents,
message: string, message: string,
progress: number, progress: number,
extra?: Partial<CleanerProgress> extra?: Partial<CleanerProgress>
@@ -39,9 +40,7 @@ function sendProgress(
currentOrderNumber: extra?.currentOrderNumber, currentOrderNumber: extra?.currentOrderNumber,
phase: extra?.phase ?? 'processing' phase: extra?.phase ?? 'processing'
} }
webContents.getAllWebContents().forEach((wc) => { sender.send(IPC_CHANNELS.CLEANER_PROGRESS, progressData)
wc.send('cleaner:progress', progressData)
})
} catch (error) { } catch (error) {
log.warn('Failed to send progress event', { error }) log.warn('Failed to send progress event', { error })
} }
@@ -116,9 +115,9 @@ async function getErpConfig(): Promise<{
export function registerCleanerHandlers(): void { export function registerCleanerHandlers(): void {
ipcMain.handle( ipcMain.handle(
'cleaner:run', IPC_CHANNELS.CLEANER_RUN,
async (event, input: CleanerInput): Promise<IpcResult<CleanerResult>> => { async (event, input: CleanerInput): Promise<IpcResult<CleanerResult>> => {
const windowId = event.sender.id const sender = event.sender
const startTime = Date.now() const startTime = Date.now()
return withErrorHandling(async () => { return withErrorHandling(async () => {
@@ -192,7 +191,7 @@ export function registerCleanerHandlers(): void {
// Send login complete progress // Send login complete progress
const totalOrders = validOrderNumbers.length const totalOrders = validOrderNumbers.length
const loginProgress = (1 / (1 + totalOrders)) * 100 const loginProgress = (1 / (1 + totalOrders)) * 100
sendProgress(windowId, 'ERP 登录成功', loginProgress, { sendProgress(sender, 'ERP 登录成功', loginProgress, {
phase: 'login', phase: 'login',
currentOrderIndex: 0, currentOrderIndex: 0,
totalOrders, totalOrders,
@@ -206,7 +205,7 @@ export function registerCleanerHandlers(): void {
...input, ...input,
orderNumbers: validOrderNumbers, orderNumbers: validOrderNumbers,
onProgress: (message, progress, extra) => { 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 // Send completion progress
sendProgress(windowId, '清理完成', 100, { sendProgress(sender, '清理完成', 100, {
phase: 'complete', phase: 'complete',
currentOrderIndex: totalOrders, currentOrderIndex: totalOrders,
totalOrders, totalOrders,
@@ -280,30 +279,16 @@ export function registerCleanerHandlers(): void {
) )
ipcMain.handle( ipcMain.handle(
'cleaner:exportResults', IPC_CHANNELS.CLEANER_EXPORT_RESULTS,
async (_event, items: ExportResultItem[]): Promise<ExportResultResponse> => { async (_event, items: ExportResultItem[]): Promise<IpcResult<ExportResultResponse>> => {
try { return withErrorHandling(async () => {
log.info('Exporting validation results', { count: items.length }) log.info('Exporting validation results', { count: items.length })
if (!items || items.length === 0) { if (!items || items.length === 0) {
return { throw new ValidationError('没有数据可导出', 'VAL_INVALID_INPUT')
success: false,
error: '没有数据可导出'
}
} }
const exporter = new ResultExporter() const exporter = new ResultExporter()
const result = await exporter.exportValidationResults(items) return await exporter.exportValidationResults(items)
}, 'cleaner:exportResults')
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
}
}
} }
) )
} }

View File

@@ -2,13 +2,15 @@ import { ipcMain } from 'electron'
import { MySqlService } from '../services/database/mysql' import { MySqlService } from '../services/database/mysql'
import { SqlServerService } from '../services/database/sql-server' import { SqlServerService } from '../services/database/sql-server'
import { createLogger } from '../services/logger' import { createLogger } from '../services/logger'
import { DatabaseQueryError, ValidationError } from '../types/errors' import { ValidationError } from '../types/errors'
import type { import type {
MySqlConfig, MySqlConfig,
MySqlQueryResult, MySqlQueryResult,
SqlServerConfig, SqlServerConfig,
SqlServerQueryResult SqlServerQueryResult
} from '../types/ipc-api.types' } from '../types/ipc-api.types'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
import { withErrorHandling, type IpcResult } from './index'
const log = createLogger('DatabaseHandler') const log = createLogger('DatabaseHandler')
@@ -17,6 +19,34 @@ const mysqlServices = new Map<string, MySqlService>()
// Store SQL Server service instances per window/connection // Store SQL Server service instances per window/connection
const sqlServerServices = new Map<string, SqlServerService>() 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 * Get or create MySQL service for a connection ID
@@ -65,29 +95,25 @@ function deleteSqlServerService(connectionId: string): void {
*/ */
export function registerDatabaseHandlers(): void { export function registerDatabaseHandlers(): void {
// Connect to MySQL // Connect to MySQL
ipcMain.handle('database:mysql:connect', async (event, config: MySqlConfig): Promise<void> => { ipcMain.handle(
try { IPC_CHANNELS.DATABASE_MYSQL_CONNECT,
async (event, config: MySqlConfig): Promise<IpcResult<void>> => {
return withErrorHandling(async () => {
// Use window ID as connection identifier // Use window ID as connection identifier
const windowId = (event.sender as { id: number }).id.toString() 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 }) log.info('Connecting to MySQL', { windowId })
const service = new MySqlService(config) const service = new MySqlService(config)
await service.connect() await service.connect()
setMySqlService(windowId, service) setMySqlService(windowId, service)
log.info('MySQL connected', { windowId }) log.info('MySQL connected', { windowId })
} catch (error) { }, 'database:mysql:connect')
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
)
} }
}) )
// Disconnect from MySQL // Disconnect from MySQL
ipcMain.handle('database:mysql:disconnect', async (event): Promise<void> => { ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT, async (event): Promise<IpcResult<void>> => {
try { return withErrorHandling(async () => {
const windowId = (event.sender as { id: number }).id.toString() const windowId = (event.sender as { id: number }).id.toString()
const service = getMySqlService(windowId) const service = getMySqlService(windowId)
if (service) { if (service) {
@@ -95,29 +121,23 @@ export function registerDatabaseHandlers(): void {
deleteMySqlService(windowId) deleteMySqlService(windowId)
log.info('MySQL disconnected', { windowId }) log.info('MySQL disconnected', { windowId })
} }
} catch (error) { }, 'database:mysql:disconnect')
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
)
}
}) })
// Check if MySQL is connected // Check if MySQL is connected
ipcMain.handle('database:mysql:isConnected', async (event): Promise<boolean> => { ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_IS_CONNECTED, async (event): Promise<IpcResult<boolean>> => {
const windowId = (event.sender as { id: number }).id.toString() return withErrorHandling(async () => {
const service = getMySqlService(windowId) const windowId = (event.sender as { id: number }).id.toString()
return service ? service.isConnected() : false const service = getMySqlService(windowId)
return service ? service.isConnected() : false
}, 'database:mysql:isConnected')
}) })
// Execute MySQL query // Execute MySQL query
ipcMain.handle( ipcMain.handle(
'database:mysql:query', IPC_CHANNELS.DATABASE_MYSQL_QUERY,
async (event, sql: string, params?: unknown[]): Promise<MySqlQueryResult> => { async (event, sql: string, params?: unknown[]): Promise<IpcResult<MySqlQueryResult>> => {
try { return withErrorHandling(async () => {
const windowId = (event.sender as { id: number }).id.toString() const windowId = (event.sender as { id: number }).id.toString()
const service = getMySqlService(windowId) const service = getMySqlService(windowId)
@@ -130,44 +150,29 @@ export function registerDatabaseHandlers(): void {
log.debug('Executing MySQL query', { windowId, sql: sql.substring(0, 100) }) log.debug('Executing MySQL query', { windowId, sql: sql.substring(0, 100) })
return await service.query(sql, params) return await service.query(sql, params)
} catch (error) { }, 'database:mysql:query')
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
)
}
} }
) )
// Connect to SQL Server // Connect to SQL Server
ipcMain.handle( ipcMain.handle(
'database:sqlserver:connect', IPC_CHANNELS.DATABASE_SQLSERVER_CONNECT,
async (event, config: SqlServerConfig): Promise<void> => { async (event, config: SqlServerConfig): Promise<IpcResult<void>> => {
try { return withErrorHandling(async () => {
const windowId = (event.sender as { id: number }).id.toString() 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 }) log.info('Connecting to SQL Server', { windowId })
const service = new SqlServerService(config) const service = new SqlServerService(config)
await service.connect() await service.connect()
setSqlServerService(windowId, service) setSqlServerService(windowId, service)
log.info('SQL Server connected', { windowId }) log.info('SQL Server connected', { windowId })
} catch (error) { }, 'database:sqlserver:connect')
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
)
}
} }
) )
// Disconnect from SQL Server // Disconnect from SQL Server
ipcMain.handle('database:sqlserver:disconnect', async (event): Promise<void> => { ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT, async (event): Promise<IpcResult<void>> => {
try { return withErrorHandling(async () => {
const windowId = (event.sender as { id: number }).id.toString() const windowId = (event.sender as { id: number }).id.toString()
const service = getSqlServerService(windowId) const service = getSqlServerService(windowId)
if (service) { if (service) {
@@ -175,34 +180,27 @@ export function registerDatabaseHandlers(): void {
deleteSqlServerService(windowId) deleteSqlServerService(windowId)
log.info('SQL Server disconnected', { windowId }) log.info('SQL Server disconnected', { windowId })
} }
} catch (error) { }, 'database:sqlserver:disconnect')
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
)
}
}) })
// Check if SQL Server is connected // Check if SQL Server is connected
ipcMain.handle('database:sqlserver:isConnected', async (event): Promise<boolean> => { ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_IS_CONNECTED, async (event): Promise<IpcResult<boolean>> => {
const windowId = (event.sender as { id: number }).id.toString() return withErrorHandling(async () => {
const service = getSqlServerService(windowId) const windowId = (event.sender as { id: number }).id.toString()
return service ? service.isConnected() : false const service = getSqlServerService(windowId)
return service ? service.isConnected() : false
}, 'database:sqlserver:isConnected')
}) })
// Execute SQL Server query // Execute SQL Server query
ipcMain.handle( ipcMain.handle(
'database:sqlserver:query', IPC_CHANNELS.DATABASE_SQLSERVER_QUERY,
async ( async (
event, event,
sqlString: string, sqlString: string,
params?: Record<string, unknown> params?: Record<string, unknown>
): Promise<SqlServerQueryResult> => { ): Promise<IpcResult<SqlServerQueryResult>> => {
try { return withErrorHandling(async () => {
const windowId = (event.sender as { id: number }).id.toString() const windowId = (event.sender as { id: number }).id.toString()
const service = getSqlServerService(windowId) const service = getSqlServerService(windowId)
@@ -226,15 +224,7 @@ export function registerDatabaseHandlers(): void {
} else { } else {
return await service.query(sqlString) return await service.query(sqlString)
} }
} catch (error) { }, 'database:sqlserver:query')
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
)
}
} }
) )
} }

View File

@@ -1,4 +1,4 @@
import { ipcMain, webContents } from 'electron' import { ipcMain, type WebContents } from 'electron'
import { ErpAuthService } from '../services/erp/erp-auth' import { ErpAuthService } from '../services/erp/erp-auth'
import { ExtractorService } from '../services/erp/extractor' import { ExtractorService } from '../services/erp/extractor'
import { OrderNumberResolver } from '../services/erp/order-resolver' 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 type { ExtractorInput, ExtractorResult, ExtractionProgress } from '../types/extractor.types'
import { UserErpConfigService } from '../services/user/user-erp-config-service' import { UserErpConfigService } from '../services/user/user-erp-config-service'
import { ConfigManager } from '../services/config/config-manager' import { ConfigManager } from '../services/config/config-manager'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
const log = createLogger('ExtractorHandler') const log = createLogger('ExtractorHandler')
function sendProgress( function sendProgress(
windowId: number, sender: WebContents,
message: string, message: string,
progress: number, progress: number,
extra?: Partial<ExtractionProgress> extra?: Partial<ExtractionProgress>
): void { ): void {
try { try {
const progressData = { message, progress, ...extra } const progressData = { message, progress, ...extra }
webContents.getAllWebContents().forEach((wc) => { sender.send(IPC_CHANNELS.EXTRACTOR_PROGRESS, progressData)
wc.send('extractor:progress', progressData)
})
} catch (error) { } catch (error) {
log.warn('Failed to send progress event', { 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 { try {
webContents.getAllWebContents().forEach((wc) => { sender.send(IPC_CHANNELS.EXTRACTOR_LOG, { level, message })
wc.send('extractor:log', { level, message })
})
} catch (error) { } catch (error) {
log.warn('Failed to send log event', { error }) log.warn('Failed to send log event', { error })
} }
@@ -76,14 +73,13 @@ async function getErpConfig(): Promise<{
*/ */
export function registerExtractorHandlers(): void { export function registerExtractorHandlers(): void {
ipcMain.handle( ipcMain.handle(
'extractor:run', IPC_CHANNELS.EXTRACTOR_RUN,
async (event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => { async (event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => {
const windowId = event.sender.id const sender = event.sender
return withErrorHandling(async () => { return withErrorHandling(async () => {
let authService: ErpAuthService | null = null let authService: ErpAuthService | null = null
let dbService: IDatabaseService | null = null let dbService: IDatabaseService | null = null
let erpConfigService: UserErpConfigService | null = null
try { try {
// Get ERP configuration from database for current user // Get ERP configuration from database for current user
@@ -97,11 +93,11 @@ export function registerExtractorHandlers(): void {
// Create database service using factory // Create database service using factory
log.info('Connecting to database for order resolution...') log.info('Connecting to database for order resolution...')
sendProgress(windowId, '连接数据库...', 3.33, { sendProgress(sender, '连接数据库...', 3.33, {
phase: 'login', phase: 'login',
subProgress: { step: '连接数据库', current: 1, total: 3 } subProgress: { step: '连接数据库', current: 1, total: 3 }
}) })
sendLog(windowId, 'system', '正在连接数据库...') sendLog(sender, 'system', '正在连接数据库...')
try { try {
dbService = await create() dbService = await create()
@@ -114,11 +110,11 @@ export function registerExtractorHandlers(): void {
} }
// Resolve order numbers (convert productionIDs to 生产订单号) // Resolve order numbers (convert productionIDs to 生产订单号)
sendProgress(windowId, '解析订单号...', 6.67, { sendProgress(sender, '解析订单号...', 6.67, {
phase: 'login', phase: 'login',
subProgress: { step: '解析订单号', current: 2, total: 3 } subProgress: { step: '解析订单号', current: 2, total: 3 }
}) })
sendLog(windowId, 'info', '正在解析订单号...') sendLog(sender, 'info', '正在解析订单号...')
const resolver = new OrderNumberResolver(dbService) const resolver = new OrderNumberResolver(dbService)
const mappings = await resolver.resolve(input.orderNumbers) const mappings = await resolver.resolve(input.orderNumbers)
@@ -139,7 +135,7 @@ export function registerExtractorHandlers(): void {
} }
log.info('Resolved order numbers', { count: validOrderNumbers.length }) log.info('Resolved order numbers', { count: validOrderNumbers.length })
sendLog(windowId, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`) sendLog(sender, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`)
// Create auth service and login // Create auth service and login
authService = new ErpAuthService({ authService = new ErpAuthService({
@@ -149,18 +145,18 @@ export function registerExtractorHandlers(): void {
headless: true headless: true
}) })
sendProgress(windowId, '登录 ERP 系统...', 9.99, { sendProgress(sender, '登录 ERP 系统...', 9.99, {
phase: 'login', phase: 'login',
subProgress: { step: '登录 ERP 系统', current: 3, total: 3 } subProgress: { step: '登录 ERP 系统', current: 3, total: 3 }
}) })
sendLog(windowId, 'system', '正在登录 ERP 系统...') sendLog(sender, 'system', '正在登录 ERP 系统...')
log.info('Logging in to ERP...') log.info('Logging in to ERP...')
try { try {
await authService.login() await authService.login()
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : '未知错误' const errorMsg = error instanceof Error ? error.message : '未知错误'
sendLog(windowId, 'error', `ERP 登录失败:${errorMsg}`) sendLog(sender, 'error', `ERP 登录失败:${errorMsg}`)
throw new ErpConnectionError( throw new ErpConnectionError(
'ERP 登录失败', 'ERP 登录失败',
'ERP_LOGIN_FAILED', 'ERP_LOGIN_FAILED',
@@ -168,7 +164,7 @@ export function registerExtractorHandlers(): void {
) )
} }
log.info('Login successful') log.info('Login successful')
sendLog(windowId, 'success', 'ERP 登录成功') sendLog(sender, 'success', 'ERP 登录成功')
// Create extractor service and run extraction with resolved order numbers // Create extractor service and run extraction with resolved order numbers
const extractor = new ExtractorService(authService) const extractor = new ExtractorService(authService)
@@ -178,11 +174,11 @@ export function registerExtractorHandlers(): void {
...input, ...input,
orderNumbers: validOrderNumbers, orderNumbers: validOrderNumbers,
onProgress: (message, progress, extra) => { onProgress: (message, progress, extra) => {
sendProgress(windowId, message, progress, extra) sendProgress(sender, message, progress, extra)
sendLog(windowId, 'info', message) sendLog(sender, 'info', message)
}, },
onLog: (level, message) => { onLog: (level, message) => {
sendLog(windowId, level, message) sendLog(sender, level, message)
} }
} }

View File

@@ -1,67 +1,87 @@
import { ipcMain, shell } from 'electron' import { app, ipcMain, shell } from 'electron'
import * as fs from 'fs/promises' import * as fs from 'fs/promises'
import * as path from 'path' import * as path from 'path'
import { createLogger } from '../services/logger' 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') 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 { export function registerFileHandlers(): void {
ipcMain.handle('file:read', async (_event, filePath: string): Promise<string> => { ipcMain.handle(IPC_CHANNELS.FILE_READ, async (_event, filePath: string): Promise<IpcResult<string>> => {
try { return withErrorHandling(async () => {
log.debug('Reading file', { filePath }) const safePath = normalizeAndValidatePath(filePath)
return await fs.readFile(filePath, 'utf-8') log.debug('Reading file', { filePath: safePath })
} catch (error) { return await fs.readFile(safePath, 'utf-8')
const message = error instanceof Error ? error.message : 'Failed to read file' }, 'file:read')
log.error('Failed to read file', { filePath, error: message })
throw new Error(message)
}
}) })
ipcMain.handle('file:write', async (_event, filePath: string, content: string): Promise<void> => { ipcMain.handle(
try { IPC_CHANNELS.FILE_WRITE,
log.debug('Writing file', { filePath }) async (_event, filePath: string, content: string): Promise<IpcResult<void>> => {
const dir = path.dirname(filePath) return withErrorHandling(async () => {
await fs.mkdir(dir, { recursive: true }) const safePath = normalizeAndValidatePath(filePath)
await fs.writeFile(filePath, content, 'utf-8') log.debug('Writing file', { filePath: safePath })
} catch (error) { const dir = path.dirname(safePath)
const message = error instanceof Error ? error.message : 'Failed to write file' await fs.mkdir(dir, { recursive: true })
log.error('Failed to write file', { filePath, error: message }) await fs.writeFile(safePath, content, 'utf-8')
throw new Error(message) }, '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> => { ipcMain.handle(IPC_CHANNELS.FILE_LIST, async (_event, dirPath: string): Promise<IpcResult<string[]>> => {
try { return withErrorHandling(async () => {
await fs.access(filePath) const safePath = normalizeAndValidatePath(dirPath)
return true log.debug('Listing directory', { dirPath: safePath })
} catch { const entries = await fs.readdir(safePath, { withFileTypes: true })
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 })
return entries return entries
.filter((entry) => entry.isFile()) .filter((entry) => entry.isFile())
.map((entry) => entry.name) .map((entry) => entry.name)
.sort() .sort()
} catch (error) { }, 'file:list')
const message = error instanceof Error ? error.message : 'Failed to list directory'
log.error('Failed to list directory', { dirPath, error: message })
throw new Error(message)
}
}) })
ipcMain.handle('file:openPath', async (_event, filePath: string): Promise<void> => { ipcMain.handle(IPC_CHANNELS.FILE_OPEN_PATH, async (_event, filePath: string): Promise<IpcResult<void>> => {
try { return withErrorHandling(async () => {
log.debug('Opening path in explorer', { filePath }) const safePath = normalizeAndValidatePath(filePath)
await shell.openPath(filePath) log.debug('Opening path in explorer', { filePath: safePath })
} catch (error) { await fs.access(safePath)
const message = error instanceof Error ? error.message : 'Failed to open path' await shell.openPath(safePath)
log.error('Failed to open path', { filePath, error: message }) }, 'file:openPath')
throw new Error(message)
}
}) })
} }

View File

@@ -28,6 +28,14 @@ export interface IpcResult<T = unknown> {
code?: string 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 * Higher-order function to wrap IPC handlers with consistent error handling
* @param handler - The async handler function to wrap * @param handler - The async handler function to wrap
@@ -39,9 +47,9 @@ export function withErrorHandling<T>(
context: string context: string
): Promise<IpcResult<T>> { ): Promise<IpcResult<T>> {
return handler() return handler()
.then((data) => { .then((data): IpcResult<T> => {
log.debug(`[${context}] Handler completed successfully`) log.debug(`[${context}] Handler completed successfully`)
return { success: true, data } return ok(data)
}) })
.catch((error: unknown) => { .catch((error: unknown) => {
const message = getErrorMessage(error) const message = getErrorMessage(error)
@@ -58,7 +66,7 @@ export function withErrorHandling<T>(
log.debug(`[${context}] Stack trace:`, { stack: error.stack }) log.debug(`[${context}] Stack trace:`, { stack: error.stack })
} }
return { success: false, error: message, code } return fail<T>(message, code)
}) })
} }

View File

@@ -17,6 +17,9 @@ import {
type MaterialTypeBatchRequest type MaterialTypeBatchRequest
} from '../services/database/materials-type-to-be-deleted-dao' } from '../services/database/materials-type-to-be-deleted-dao'
import { createLogger } from '../services/logger' 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') const log = createLogger('MaterialTypeHandler')
@@ -30,16 +33,12 @@ export function registerMaterialTypeHandlers(): void {
* Get all material type records * Get all material type records
*/ */
ipcMain.handle( ipcMain.handle(
'materialType:getAll', IPC_CHANNELS.MATERIAL_TYPE_GET_ALL,
async (): Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> => { async (): Promise<IpcResult<MaterialTypeRecord[]>> => {
try { return withErrorHandling(async () => {
const records = await dao.getAllMaterials() const records = await dao.getAllMaterials()
return { success: true, data: records } return records
} catch (error) { }, 'materialType:getAll')
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Get all material types error', { error: message })
return { success: false, error: message }
}
} }
) )
@@ -47,19 +46,15 @@ export function registerMaterialTypeHandlers(): void {
* Get material types by manager * Get material types by manager
*/ */
ipcMain.handle( ipcMain.handle(
'materialType:getByManager', IPC_CHANNELS.MATERIAL_TYPE_GET_BY_MANAGER,
async ( async (
_event, _event,
managerName: string managerName: string
): Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> => { ): Promise<IpcResult<MaterialTypeRecord[]>> => {
try { return withErrorHandling(async () => {
const records = await dao.getMaterialsByManager(managerName) const records = await dao.getMaterialsByManager(managerName)
return { success: true, data: records } return records
} catch (error) { }, 'materialType:getByManager')
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Get material types by manager error', { error: message })
return { success: false, error: message }
}
} }
) )
@@ -67,16 +62,12 @@ export function registerMaterialTypeHandlers(): void {
* Get list of managers * Get list of managers
*/ */
ipcMain.handle( ipcMain.handle(
'materialType:getManagers', IPC_CHANNELS.MATERIAL_TYPE_GET_MANAGERS,
async (): Promise<{ success: boolean; data?: string[]; error?: string }> => { async (): Promise<IpcResult<string[]>> => {
try { return withErrorHandling(async () => {
const managers = await dao.getManagers() const managers = await dao.getManagers()
return { success: true, data: managers } return managers
} catch (error) { }, 'materialType:getManagers')
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Get managers error', { error: message })
return { success: false, error: message }
}
} }
) )
@@ -84,22 +75,18 @@ export function registerMaterialTypeHandlers(): void {
* Upsert (insert or update) a material type record * Upsert (insert or update) a material type record
*/ */
ipcMain.handle( ipcMain.handle(
'materialType:upsert', IPC_CHANNELS.MATERIAL_TYPE_UPSERT,
async ( async (
_event, _event,
{ materialName, managerName }: { materialName: string; managerName: string } { materialName, managerName }: { materialName: string; managerName: string }
): Promise<{ success: boolean; error?: string }> => { ): Promise<IpcResult<{ updated: boolean }>> => {
try { return withErrorHandling(async () => {
const result = await dao.upsertMaterial(materialName, managerName) const result = await dao.upsertMaterial(materialName, managerName)
if (!result) { 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 } return { updated: true }
} catch (error) { }, 'materialType:upsert')
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Upsert material type error', { error: message })
return { success: false, error: message }
}
} }
) )
@@ -107,22 +94,18 @@ export function registerMaterialTypeHandlers(): void {
* Delete a material type record * Delete a material type record
*/ */
ipcMain.handle( ipcMain.handle(
'materialType:delete', IPC_CHANNELS.MATERIAL_TYPE_DELETE,
async ( async (
_event, _event,
{ materialName, managerName }: { materialName: string; managerName: string } { materialName, managerName }: { materialName: string; managerName: string }
): Promise<{ success: boolean; error?: string }> => { ): Promise<IpcResult<{ deleted: boolean }>> => {
try { return withErrorHandling(async () => {
const result = await dao.deleteMaterial(materialName, managerName) const result = await dao.deleteMaterial(materialName, managerName)
if (!result) { 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 } return { deleted: true }
} catch (error) { }, 'materialType:delete')
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Delete material type error', { error: message })
return { success: false, error: message }
}
} }
) )
@@ -130,25 +113,18 @@ export function registerMaterialTypeHandlers(): void {
* Batch operation for material types (insert, update, delete) * Batch operation for material types (insert, update, delete)
*/ */
ipcMain.handle( ipcMain.handle(
'materialType:upsertBatch', IPC_CHANNELS.MATERIAL_TYPE_UPSERT_BATCH,
async ( async (
_event, _event,
request: MaterialTypeBatchRequest request: MaterialTypeBatchRequest
): Promise<{ ): Promise<IpcResult<{ stats: { total: number; success: number; failed: number } }>> => {
success: boolean return withErrorHandling(async () => {
stats?: { total: number; success: number; failed: number }
error?: string
}> => {
try {
const stats = await dao.upsertBatch(request) const stats = await dao.upsertBatch(request)
return { success: true, stats } return { stats }
} catch (error) { }, 'materialType:upsertBatch')
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Batch upsert material types error', { error: message })
return { success: false, error: message }
}
} }
) )
log.info('Material type handlers registered') log.info('Material type handlers registered')
} }

View File

@@ -11,6 +11,8 @@ import { create, type IDatabaseService } from '../services/database'
import { OrderNumberResolver } from '../services/erp/order-resolver' import { OrderNumberResolver } from '../services/erp/order-resolver'
import { createLogger } from '../services/logger' import { createLogger } from '../services/logger'
import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver' 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') const log = createLogger('ResolverHandler')
@@ -49,11 +51,11 @@ export function registerResolverHandlers(): void {
* Converts productionIDs and 生产订单号 to production order numbers * Converts productionIDs and 生产订单号 to production order numbers
*/ */
ipcMain.handle( ipcMain.handle(
'resolver:resolve', IPC_CHANNELS.RESOLVER_RESOLVE,
async (_event, input: ResolverInput): Promise<ResolverResponse> => { async (_event, input: ResolverInput): Promise<IpcResult<ResolverResponse>> => {
let dbService: IDatabaseService | null = null let dbService: IDatabaseService | null = null
try { return withErrorHandling(async () => {
// Create database service using factory // Create database service using factory
log.info('Connecting to database for resolution', { inputCount: input.inputs.length }) log.info('Connecting to database for resolution', { inputCount: input.inputs.length })
dbService = await create() dbService = await create()
@@ -80,14 +82,7 @@ export function registerResolverHandlers(): void {
warnings, warnings,
stats stats
} }
} catch (error) { }, 'resolver:resolve').finally(async () => {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Resolution failed', { error: message })
return {
success: false,
error: `解析失败:${message}`
}
} finally {
// Clean up database connection // Clean up database connection
if (dbService) { if (dbService) {
try { try {
@@ -99,7 +94,7 @@ export function registerResolverHandlers(): void {
}) })
} }
} }
} })
} }
) )
@@ -107,16 +102,12 @@ export function registerResolverHandlers(): void {
* Validate input format only (without database lookup) * Validate input format only (without database lookup)
*/ */
ipcMain.handle( ipcMain.handle(
'resolver:validateFormat', IPC_CHANNELS.RESOLVER_VALIDATE_FORMAT,
async ( async (
_event, _event,
inputs: string[] inputs: string[]
): Promise<{ ): Promise<IpcResult<Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>>> => {
success: boolean return withErrorHandling(async () => {
results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>
error?: string
}> => {
try {
// Create a mock resolver without database connection // Create a mock resolver without database connection
const resolver = new OrderNumberResolver({ const resolver = new OrderNumberResolver({
isConnected: () => false, isConnected: () => false,
@@ -130,15 +121,8 @@ export function registerResolverHandlers(): void {
log.debug('Format validation completed', { inputCount: inputs.length }) log.debug('Format validation completed', { inputCount: inputs.length })
return { success: true, results } return results
} catch (error) { }, 'resolver:validateFormat')
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Format validation failed', { error: message })
return {
success: false,
error: `验证失败:${message}`
}
}
} }
) )
} }

View File

@@ -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 { ipcMain } from 'electron'
import { ConfigManager } from '../services/config/config-manager' import { ConfigManager } from '../services/config/config-manager'
import { SessionManager } from '../services/user/session-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 { SqlServerService } from '../services/database/sql-server'
import { createLogger } from '../services/logger' import { createLogger } from '../services/logger'
import type { UserType, ConnectionTestResult, SaveSettingsResult } from '../types/settings.types' 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') const log = createLogger('SettingsHandler')
/** type ErpSettingsPayload = {
* Register IPC handlers for settings management erp?: {
*/ username?: string
password?: string
}
}
export function registerSettingsHandlers(): void { export function registerSettingsHandlers(): void {
const configManager = ConfigManager.getInstance() const configManager = ConfigManager.getInstance()
const sessionManager = SessionManager.getInstance() const sessionManager = SessionManager.getInstance()
const erpConfigService = UserErpConfigService.getInstance() const erpConfigService = UserErpConfigService.getInstance()
/** ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_USER_TYPE, async (): Promise<IpcResult<UserType>> => {
* Get current user type return withErrorHandling(
*/ async () => (sessionManager.getUserType() as UserType) || 'Guest',
ipcMain.handle('settings:getUserType', async (): Promise<UserType> => { 'settings:getUserType'
return (sessionManager.getUserType() as UserType) || 'Guest' )
}) })
/**
* 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( ipcMain.handle(
'settings:saveSettings', IPC_CHANNELS.SETTINGS_GET_SETTINGS,
async (_event, settings: any): Promise<SaveSettingsResult> => { async (): Promise<IpcResult<{ erp: { username: string; password: string } }>> => {
try { return withErrorHandling(async () => {
log.info('Saving ERP credentials') const userErpConfig = await erpConfigService.getCurrentUserErpConfig()
return {
if (settings.erp) { erp: {
// Update ERP credentials in database for current user username: userErpConfig?.username || '',
const currentUser = sessionManager.getUserInfo() password: userErpConfig?.password || ''
if (!currentUser) {
return { success: false, error: '未找到当前用户' }
} }
// 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')
} }
}, 'settings:getSettings')
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}` }
}
} }
) )
/** ipcMain.handle(
* Reset to default settings (Admin only) IPC_CHANNELS.SETTINGS_SAVE_SETTINGS,
*/ async (_event, settings: ErpSettingsPayload): Promise<IpcResult<SaveSettingsResult>> => {
ipcMain.handle('settings:resetDefaults', async (): Promise<SaveSettingsResult> => { return withErrorHandling(async () => {
try { if (settings.erp) {
const userType = sessionManager.getUserType() const currentUser = sessionManager.getUserInfo()
if (userType !== 'Admin') { if (!currentUser) {
log.warn('Non-admin user attempted to reset defaults', { userType }) throw new ValidationError('未找到当前用户', 'VAL_INVALID_INPUT')
return { success: false, error: '只有管理员可以恢复默认设置' } }
}
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 } return { success: true }
} else { }, 'settings:saveSettings')
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}` }
} }
}) )
/** ipcMain.handle(
* Test database connection IPC_CHANNELS.SETTINGS_RESET_DEFAULTS,
*/ async (): Promise<IpcResult<SaveSettingsResult>> => {
ipcMain.handle('settings:testDbConnection', async (): Promise<ConnectionTestResult> => { return withErrorHandling(async () => {
try { const userType = sessionManager.getUserType()
log.info('Testing database connection') if (userType !== 'Admin') {
const config = configManager.getConfig() throw new ValidationError('只有管理员可以恢复默认设置', 'VAL_INVALID_INPUT')
const dbType = config.database.activeType }
if (dbType === 'mysql') { const success = await configManager.resetToDefaults()
// Test MySQL connection if (!success) {
const dbConfig = config.database.mysql throw new ValidationError('恢复默认设置失败', 'VAL_INVALID_INPUT')
if (!dbConfig.host || !dbConfig.database || !dbConfig.username) { }
log.warn('MySQL connection test failed - missing configuration')
return { return { success: true }
success: false, }, 'settings:resetDefaults')
message: '请先配置 MySQL 主机、数据库名和用户名' }
)
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 const dbConfig = config.database.sqlserver
if (!dbConfig.server || !dbConfig.database || !dbConfig.username) { if (!dbConfig.server || !dbConfig.database || !dbConfig.username) {
log.warn('SQL Server connection test failed - missing configuration')
return { return {
success: false, success: false,
message: '请先配置 SQL Server 服务器、数据库名和用户名' message: '请先配置 SQL Server 服务器、数据库名和用户名'
@@ -189,27 +149,19 @@ export function registerSettingsHandlers(): void {
try { try {
await sqlServerService.connect() await sqlServerService.connect()
await sqlServerService.disconnect() await sqlServerService.disconnect()
log.info('SQL Server connection test successful')
return { return {
success: true, success: true,
message: 'SQL Server 数据库连接测试成功!' message: 'SQL Server 数据库连接测试成功!'
} }
} catch (connError) { } catch (error) {
const errorMessage = connError instanceof Error ? connError.message : '连接失败' const message = error instanceof Error ? error.message : '连接失败'
log.error('SQL Server connection failed', { error: errorMessage })
return { return {
success: false, success: false,
message: `SQL Server 数据库连接测试失败:${errorMessage}` message: `SQL Server 数据库连接测试失败:${message}`
} }
} }
} }, 'settings:testDbConnection')
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Database connection test error', { error: message })
return {
success: false,
message: `数据库连接测试失败:${message}`
}
} }
}) )
} }

View File

@@ -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 { 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 { ErpAuthService } from '../services/erp/erp-auth'
import { ConfigManager } from '../services/config/config-manager' import { ConfigManager } from '../services/config/config-manager'
import { createLogger } from '../services/logger' 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') const log = createLogger('UserErpConfigHandler')
/**
* ERP Credentials request (username and password only, URL is from config.yaml)
*/
export interface ErpCredentialsRequest { export interface ErpCredentialsRequest {
username: string username: string
password: string password: string
} }
/**
* ERP Configuration response (includes URL from config.yaml)
*/
export interface ErpConfigResponse { export interface ErpConfigResponse {
success: boolean success: boolean
config?: { config?: {
@@ -37,127 +25,80 @@ export interface ErpConfigResponse {
error?: string error?: string
} }
/**
* Connection test result
*/
export interface ConnectionTestResult { export interface ConnectionTestResult {
success: boolean success: boolean
message?: string message?: string
} }
/**
* Register IPC handlers for user ERP configuration
*/
export function registerUserErpConfigHandlers(): void { export function registerUserErpConfigHandlers(): void {
const erpConfigService = UserErpConfigService.getInstance() 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( ipcMain.handle(
'user-erp-config:update', IPC_CHANNELS.USER_ERP_CONFIG_GET_CURRENT,
async (_event, credentials: ErpCredentialsRequest): Promise<ErpConfigResponse> => { async (): Promise<IpcResult<ErpConfigResponse>> => {
try { return withErrorHandling(async () => {
log.info('Updating current user ERP credentials', { log.info('Fetching current user ERP credentials')
username: credentials.username const credentials = await erpConfigService.getCurrentUserErpConfig()
})
const success = await erpConfigService.updateCurrentUserErpConfig(credentials) if (!credentials) {
throw new ValidationError('未找到 ERP 配置。请先配置 ERP 账号和密码。', 'VAL_INVALID_INPUT')
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 配置失败'
}
} }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error' const configManager = ConfigManager.getInstance()
log.error('Update ERP credentials failed', { error: message }) const globalConfig = configManager.getConfig()
return { return {
success: false, success: true,
error: `更新 ERP 配置失败:${message}` config: {
url: globalConfig.erp.url,
username: credentials.username,
password: credentials.password
}
} }
} }, 'user-erp-config:getCurrent')
} }
) )
/**
* Test ERP connection with provided credentials
*/
ipcMain.handle( ipcMain.handle(
'user-erp-config:testConnection', IPC_CHANNELS.USER_ERP_CONFIG_UPDATE,
async (_event, credentials: ErpCredentialsRequest): Promise<ConnectionTestResult> => { async (_event, credentials: ErpCredentialsRequest): Promise<IpcResult<ErpConfigResponse>> => {
try { return withErrorHandling(async () => {
log.info('Testing ERP connection', { username: credentials.username }) const updated = await erpConfigService.updateCurrentUserErpConfig(credentials)
if (!updated) {
if (!credentials.username || !credentials.password) { throw new ValidationError('更新 ERP 配置失败', 'VAL_INVALID_INPUT')
return {
success: false,
message: 'ERP 配置不完整,请确保用户名和密码都已填写'
}
} }
// Get ERP URL from config.yaml (fixed for all users)
const configManager = ConfigManager.getInstance() const configManager = ConfigManager.getInstance()
const globalConfig = configManager.getConfig() 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({ const authService = new ErpAuthService({
url: erpUrl, url: globalConfig.erp.url,
username: credentials.username, username: credentials.username,
password: credentials.password, password: credentials.password,
headless: true headless: true
@@ -165,49 +106,37 @@ export function registerUserErpConfigHandlers(): void {
try { try {
await authService.login() await authService.login()
await authService.close()
log.info('ERP connection test successful')
return { return {
success: true, success: true,
message: 'ERP 连接测试成功' message: 'ERP 连接测试成功'
} }
} catch (error) { } finally {
await authService.close().catch(() => {}) await authService.close().catch(() => {})
throw error
} }
} catch (error) { }, 'user-erp-config:testConnection')
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('ERP connection test failed', { error: message })
return {
success: false,
message: `ERP 连接测试失败:${message}`
}
}
} }
) )
/**
* Get all users' ERP configurations (admin only)
*/
ipcMain.handle( ipcMain.handle(
'user-erp-config:getAll', IPC_CHANNELS.USER_ERP_CONFIG_GET_ALL,
async (): Promise< async (): Promise<
Array<{ IpcResult<
username: string Array<{
erpUrl: string username: string
erpUsername: string erpUrl: string
}> erpUsername: string
}>
>
> => { > => {
try { return withErrorHandling(async () => {
log.info('Fetching all users ERP config') if (!sessionManager.isAdmin()) {
throw new ValidationError('只有管理员可以查看全部用户 ERP 配置', 'VAL_INVALID_INPUT')
}
const configs = await erpConfigService.getAllUsersErpConfig() const configs = await erpConfigService.getAllUsersErpConfig()
log.info('Retrieved ERP configs for all users', { count: configs.length })
return configs return configs
} catch (error) { }, 'user-erp-config:getAll')
log.error('Get all users ERP config failed', { error })
return []
}
} }
) )
} }

View File

@@ -23,6 +23,7 @@ import type {
ValidationResult, ValidationResult,
MaterialRecordSummary MaterialRecordSummary
} from '../types/validation.types' } from '../types/validation.types'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
const log = createLogger('ValidationHandler') const log = createLogger('ValidationHandler')
@@ -30,28 +31,28 @@ const log = createLogger('ValidationHandler')
* Shared state for Production IDs from extractor page * Shared state for Production IDs from extractor page
* This is a simple in-memory store for sharing Production IDs between pages * 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 * Set shared Production IDs
*/ */
export function setSharedProductionIds(ids: string[]): void { export function setSharedProductionIds(senderId: number, ids: string[]): void {
sharedProductionIds.clear() sharedProductionIdsBySender.set(senderId, new Set(ids))
ids.forEach((id) => sharedProductionIds.add(id))
} }
/** /**
* Get shared Production IDs * Get shared Production IDs
*/ */
export function getSharedProductionIds(): string[] { export function getSharedProductionIds(senderId: number): string[] {
return [...sharedProductionIds] const senderSet = sharedProductionIdsBySender.get(senderId)
return senderSet ? [...senderSet] : []
} }
/** /**
* Clear shared Production IDs * Clear shared Production IDs
*/ */
export function clearSharedProductionIds(): void { export function clearSharedProductionIds(senderId: number): void {
sharedProductionIds.clear() sharedProductionIdsBySender.delete(senderId)
} }
/** /**
@@ -233,8 +234,8 @@ export function registerValidationHandlers(): void {
* Run material validation from database * Run material validation from database
*/ */
ipcMain.handle( ipcMain.handle(
'validation:validate', IPC_CHANNELS.VALIDATION_VALIDATE,
async (_event, request: ValidationRequest): Promise<ValidationResponse> => { async (event, request: ValidationRequest): Promise<ValidationResponse> => {
let dbService: MySqlService | SqlServerService | null = null let dbService: MySqlService | SqlServerService | null = null
try { try {
@@ -273,7 +274,7 @@ export function registerValidationHandlers(): void {
if (request.mode === 'database_filtered') { if (request.mode === 'database_filtered') {
if (request.useSharedProductionIds) { if (request.useSharedProductionIds) {
// Use shared Production IDs from extractor page // Use shared Production IDs from extractor page
const sharedIds = getSharedProductionIds() const sharedIds = getSharedProductionIds(event.sender.id)
log.info(`Using ${sharedIds.length} shared Production IDs`) log.info(`Using ${sharedIds.length} shared Production IDs`)
if (sharedIds.length === 0) { if (sharedIds.length === 0) {
@@ -445,7 +446,7 @@ export function registerValidationHandlers(): void {
* Upsert batch materials to MaterialsToBeDeleted * Upsert batch materials to MaterialsToBeDeleted
*/ */
ipcMain.handle( ipcMain.handle(
'materials:upsertBatch', IPC_CHANNELS.MATERIALS_UPSERT_BATCH,
async (_event, request: MaterialUpsertBatchRequest): Promise<MaterialOperationResponse> => { async (_event, request: MaterialUpsertBatchRequest): Promise<MaterialOperationResponse> => {
try { try {
const dao = new MaterialsToBeDeletedDAO() const dao = new MaterialsToBeDeletedDAO()
@@ -472,7 +473,7 @@ export function registerValidationHandlers(): void {
* Delete materials by material codes * Delete materials by material codes
*/ */
ipcMain.handle( ipcMain.handle(
'materials:delete', IPC_CHANNELS.MATERIALS_DELETE,
async (_event, request: MaterialDeleteRequest): Promise<MaterialOperationResponse> => { async (_event, request: MaterialDeleteRequest): Promise<MaterialOperationResponse> => {
try { try {
const dao = new MaterialsToBeDeletedDAO() const dao = new MaterialsToBeDeletedDAO()
@@ -496,7 +497,7 @@ export function registerValidationHandlers(): void {
/** /**
* Get unique manager names * 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 { try {
const dao = new MaterialsToBeDeletedDAO() const dao = new MaterialsToBeDeletedDAO()
const managers = await dao.getManagers() const managers = await dao.getManagers()
@@ -513,7 +514,7 @@ export function registerValidationHandlers(): void {
* Update manager for a single material * Update manager for a single material
*/ */
ipcMain.handle( ipcMain.handle(
'materials:updateManager', IPC_CHANNELS.MATERIALS_UPDATE_MANAGER,
async ( async (
_event, _event,
request: { materialCode: string; managerName: string } request: { materialCode: string; managerName: string }
@@ -537,7 +538,7 @@ export function registerValidationHandlers(): void {
* Get materials by manager * Get materials by manager
*/ */
ipcMain.handle( ipcMain.handle(
'materials:getByManager', IPC_CHANNELS.MATERIALS_GET_BY_MANAGER,
async (_event, managerName: string): Promise<{ materials: MaterialRecordSummary[] }> => { async (_event, managerName: string): Promise<{ materials: MaterialRecordSummary[] }> => {
let dbService: MySqlService | SqlServerService | null = null let dbService: MySqlService | SqlServerService | null = null
@@ -616,7 +617,7 @@ export function registerValidationHandlers(): void {
* Get all material records * Get all material records
*/ */
ipcMain.handle( ipcMain.handle(
'materials:getAll', IPC_CHANNELS.MATERIALS_GET_ALL,
async (_event): Promise<{ materials: MaterialRecordSummary[] }> => { async (_event): Promise<{ materials: MaterialRecordSummary[] }> => {
let dbService: MySqlService | SqlServerService | null = null let dbService: MySqlService | SqlServerService | null = null
@@ -691,7 +692,7 @@ export function registerValidationHandlers(): void {
/** /**
* Get statistics * Get statistics
*/ */
ipcMain.handle('materials:getStatistics', async (_event): Promise<{ stats: any }> => { ipcMain.handle(IPC_CHANNELS.MATERIALS_GET_STATISTICS, async (_event): Promise<{ stats: any }> => {
try { try {
const dao = new MaterialsToBeDeletedDAO() const dao = new MaterialsToBeDeletedDAO()
const stats = await dao.getStatistics() const stats = await dao.getStatistics()
@@ -708,10 +709,10 @@ export function registerValidationHandlers(): void {
* Set shared Production IDs from extractor page * Set shared Production IDs from extractor page
*/ */
ipcMain.handle( ipcMain.handle(
'validation:setSharedProductionIds', IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS,
async (_event, productionIds: string[]): Promise<void> => { async (event, productionIds: string[]): Promise<void> => {
log.info(`Received ${productionIds.length} shared Production IDs`) 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 * Get shared Production IDs
*/ */
ipcMain.handle( ipcMain.handle(
'validation:getSharedProductionIds', IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS,
async (): Promise<{ productionIds: string[] }> => { async (event): Promise<{ productionIds: string[] }> => {
return { productionIds: getSharedProductionIds() } 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) * Filters materials by current user (admin sees all, regular users see only their own)
*/ */
ipcMain.handle( ipcMain.handle(
'validation:getCleanerData', IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA,
async ( async (
_event _event
): Promise<{ ): Promise<{
@@ -766,7 +767,7 @@ export function registerValidationHandlers(): void {
dbService = await getValidationDatabaseService() dbService = await getValidationDatabaseService()
// 1. Get order numbers from shared Production IDs // 1. Get order numbers from shared Production IDs
const sharedIds = getSharedProductionIds() const sharedIds = getSharedProductionIds(_event.sender.id)
let orderNumbers: string[] = [] let orderNumbers: string[] = []
if (sharedIds.length > 0) { if (sharedIds.length > 0) {

View File

@@ -11,6 +11,7 @@ import type {
ExportResultItem, ExportResultItem,
ExportResultResponse ExportResultResponse
} from './cleaner.types' } from './cleaner.types'
import type { IpcResult } from '../ipc'
/** /**
* MySQL connection configuration * MySQL connection configuration
@@ -60,11 +61,11 @@ export interface SqlServerQueryResult {
* File operation APIs * File operation APIs
*/ */
export interface FileAPI { export interface FileAPI {
readFile: (filePath: string) => Promise<string> readFile: (filePath: string) => Promise<IpcResult<string>>
writeFile: (filePath: string, content: string) => Promise<void> writeFile: (filePath: string, content: string) => Promise<IpcResult<void>>
fileExists: (filePath: string) => Promise<boolean> fileExists: (filePath: string) => Promise<IpcResult<boolean>>
listFiles: (dirPath: string) => Promise<string[]> listFiles: (dirPath: string) => Promise<IpcResult<string[]>>
openPath: (filePath: string) => Promise<void> openPath: (filePath: string) => Promise<IpcResult<void>>
} }
/** /**
@@ -77,7 +78,7 @@ export interface ExtractorAPI {
*/ */
runExtractor: ( runExtractor: (
input: ExtractorInput input: ExtractorInput
) => Promise<{ success: boolean; data?: ExtractorResult; error?: string }> ) => Promise<IpcResult<ExtractorResult>>
/** /**
* Subscribe to progress updates * Subscribe to progress updates
* @param callback - Callback function receiving progress data * @param callback - Callback function receiving progress data
@@ -102,13 +103,13 @@ export interface CleanerAPI {
*/ */
runCleaner: ( runCleaner: (
input: CleanerInput input: CleanerInput
) => Promise<{ success: boolean; data?: CleanerResult; error?: string }> ) => Promise<IpcResult<CleanerResult>>
/** /**
* Export validation results to Excel * Export validation results to Excel
* @param items - Validation result items to export * @param items - Validation result items to export
*/ */
exportResults: (items: ExportResultItem[]) => Promise<ExportResultResponse> exportResults: (items: ExportResultItem[]) => Promise<IpcResult<ExportResultResponse>>
/** /**
* Subscribe to progress updates * Subscribe to progress updates
@@ -126,45 +127,48 @@ export interface DatabaseAPI {
* Connect to MySQL database * Connect to MySQL database
* @param config - MySQL connection config * @param config - MySQL connection config
*/ */
connectMySql: (config: MySqlConfig) => Promise<void> connectMySql: (config: MySqlConfig) => Promise<IpcResult<void>>
/** /**
* Disconnect from MySQL database * Disconnect from MySQL database
*/ */
disconnectMySql: () => Promise<void> disconnectMySql: () => Promise<IpcResult<void>>
/** /**
* Check if MySQL is connected * Check if MySQL is connected
*/ */
isMySqlConnected: () => Promise<boolean> isMySqlConnected: () => Promise<IpcResult<boolean>>
/** /**
* Execute MySQL query * Execute MySQL query
* @param sql - SQL query * @param sql - SQL query
* @param params - Query parameters * @param params - Query parameters
*/ */
queryMySql: (sql: string, params?: unknown[]) => Promise<MySqlQueryResult> queryMySql: (sql: string, params?: unknown[]) => Promise<IpcResult<MySqlQueryResult>>
/** /**
* Connect to SQL Server database * Connect to SQL Server database
* @param config - SQL Server connection config * @param config - SQL Server connection config
*/ */
connectSqlServer: (config: SqlServerConfig) => Promise<void> connectSqlServer: (config: SqlServerConfig) => Promise<IpcResult<void>>
/** /**
* Disconnect from SQL Server database * Disconnect from SQL Server database
*/ */
disconnectSqlServer: () => Promise<void> disconnectSqlServer: () => Promise<IpcResult<void>>
/** /**
* Check if SQL Server is connected * Check if SQL Server is connected
*/ */
isSqlServerConnected: () => Promise<boolean> isSqlServerConnected: () => Promise<IpcResult<boolean>>
/** /**
* Execute SQL Server query * Execute SQL Server query
* @param sql - SQL query * @param sql - SQL query
* @param params - Query parameters * @param params - Query parameters
*/ */
querySqlServer: (sql: string, params?: Record<string, unknown>) => Promise<SqlServerQueryResult> querySqlServer: (
sql: string,
params?: Record<string, unknown>
) => Promise<IpcResult<SqlServerQueryResult>>
} }

306
src/preload/index.d.ts vendored
View File

@@ -15,277 +15,105 @@ import type {
MaterialTypeBatchRequest MaterialTypeBatchRequest
} from '../main/types/validation.types' } from '../main/types/validation.types'
import type { import type {
SettingsData,
UserType, UserType,
ConnectionTestResult, ConnectionTestResult,
SaveSettingsResult SaveSettingsResult
} from '../main/types/settings.types' } from '../main/types/settings.types'
import type { IpcResult } from '../main/ipc'
/**
* Order number resolver API
*/
export interface ResolverAPI { export interface ResolverAPI {
/** resolve: (input: ResolverInput) => Promise<IpcResult<ResolverResponse>>
* Resolve productionIDs and 生产订单号 to production order numbers validateFormat: (inputs: string[]) => Promise<
* @param input - Resolver input with list of inputs IpcResult<Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>>
*/ >
resolve: (input: ResolverInput) => Promise<ResolverResponse>
/**
* 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
}>
} }
/**
* Authentication API
*/
export interface AuthAPI { export interface AuthAPI {
/** getComputerName: () => Promise<IpcResult<string>>
* Get computer name silentLogin: () => Promise<IpcResult<SilentLoginResponse>>
*/ login: (request: LoginRequest) => Promise<IpcResult<LoginResponse>>
getComputerName: () => Promise<string> logout: () => Promise<IpcResult<void>>
/** getCurrentUser: () => Promise<IpcResult<CurrentUserResponse>>
* Silent login by computer name getAllUsers: () => Promise<IpcResult<UserInfo[]>>
*/ switchUser: (userInfo: UserInfo) => Promise<IpcResult<UserSelectionResponse>>
silentLogin: () => Promise<SilentLoginResponse> isAdmin: () => Promise<IpcResult<boolean>>
/**
* Login with username and password
* @param request - Login request with username and password
*/
login: (request: LoginRequest) => Promise<LoginResponse>
/**
* Logout
*/
logout: () => Promise<void>
/**
* Get current user
*/
getCurrentUser: () => Promise<CurrentUserResponse>
/**
* Get all users (for admin user selection)
*/
getAllUsers: () => Promise<UserInfo[]>
/**
* Switch user (admin only)
* @param userInfo - User info to switch to
*/
switchUser: (userInfo: UserInfo) => Promise<UserSelectionResponse>
/**
* Check if current user is admin
*/
isAdmin: () => Promise<boolean>
} }
/**
* Validation API
*/
export interface ValidationAPI { export interface ValidationAPI {
/** validate: (request: ValidationRequest) => Promise<IpcResult<ValidationResponse>>
* Run material validation setSharedProductionIds: (productionIds: string[]) => Promise<IpcResult<void>>
* @param request - Validation request getSharedProductionIds: () => Promise<IpcResult<{ productionIds: string[] }>>
*/ getCleanerData: () => Promise<
validate: (request: ValidationRequest) => Promise<ValidationResponse> IpcResult<{
/** orderNumbers: string[]
* Set shared Production IDs from extractor page materialCodes: string[]
* @param productionIds - List of Production IDs }>
*/ >
setSharedProductionIds: (productionIds: string[]) => Promise<void>
/**
* 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
}>
} }
/**
* Materials API
*/
export interface MaterialsAPI { export interface MaterialsAPI {
/** upsertBatch: (materials: { materialCode: string; managerName: string }[]) => Promise<
* Upsert batch materials to MaterialsToBeDeleted IpcResult<{ stats: { total: number; success: number; failed: number } }>
* @param materials - List of materials with materialCode and managerName >
*/ delete: (materialCodes: string[]) => Promise<IpcResult<{ count: number }>>
upsertBatch: (materials: { materialCode: string; managerName: string }[]) => Promise<{ getManagers: () => Promise<IpcResult<{ managers: string[] }>>
success: boolean getByManager: (managerName: string) => Promise<IpcResult<{ materials: unknown[] }>>
stats?: { total: number; success: number; failed: number } getAll: () => Promise<IpcResult<{ materials: unknown[] }>>
error?: string getStatistics: () => Promise<IpcResult<{ stats: unknown }>>
}>
/**
* 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
*/
updateManager: ( updateManager: (
materialCode: string, materialCode: string,
managerName: string managerName: string
) => Promise<{ success: boolean; error?: string }> ) => Promise<IpcResult<{ updated: boolean }>>
} }
/**
* Settings API
*/
export interface SettingsAPI { export interface SettingsAPI {
/** getUserType: () => Promise<IpcResult<UserType>>
* Get current user type getSettings: () => Promise<IpcResult<{ erp: { username: string; password: string } }>>
*/ saveSettings: (settings: { erp?: { username?: string; password?: string } }) => Promise<IpcResult<SaveSettingsResult>>
getUserType: () => Promise<UserType> resetDefaults: () => Promise<IpcResult<SaveSettingsResult>>
/** testDbConnection: () => Promise<IpcResult<ConnectionTestResult>>
* Get settings (filtered by user type)
*/
getSettings: () => Promise<SettingsData>
/**
* Save settings
* @param settings - Settings data to save
*/
saveSettings: (settings: SettingsData) => Promise<SaveSettingsResult>
/**
* Reset to defaults (Admin only)
*/
resetDefaults: () => Promise<SaveSettingsResult>
/**
* Test ERP connection
*/
testErpConnection: () => Promise<ConnectionTestResult>
/**
* Test database connection
*/
testDbConnection: () => Promise<ConnectionTestResult>
} }
/**
* Material Type API
*/
export interface MaterialTypeAPI { export interface MaterialTypeAPI {
/** getAll: () => Promise<IpcResult<MaterialTypeRecord[]>>
* Get all material type records getByManager: (managerName: string) => Promise<IpcResult<MaterialTypeRecord[]>>
*/ getManagers: () => Promise<IpcResult<string[]>>
getAll: () => Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> upsert: (materialName: string, managerName: string) => Promise<IpcResult<{ updated: boolean }>>
/** delete: (materialName: string, managerName: string) => Promise<IpcResult<{ deleted: boolean }>>
* Get material types by manager upsertBatch: (request: MaterialTypeBatchRequest) => Promise<
* @param managerName - Manager name IpcResult<{ stats: { total: number; success: number; failed: number } }>
*/ >
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
}>
} }
/**
* User ERP Configuration API
*/
export interface UserErpConfigAPI { export interface UserErpConfigAPI {
/** getCurrent: () => Promise<
* Get current user's ERP configuration IpcResult<{
*/ config: { url: string; username: string; password: string }
getCurrent: () => Promise<{ }>
success: boolean >
config?: { url: string; username: string; password: string } update: (config: { url: string; username: string; password: string }) => Promise<
error?: string IpcResult<{
}> config: { url: string; username: string; password: string }
/** }>
* Update current user's ERP configuration >
*/ testConnection: (config: { url: string; username: string; password: string }) => Promise<
update: (config: { url: string; username: string; password: string }) => Promise<{ IpcResult<{ message: string }>
success: boolean >
config?: { url: string; username: string; password: string } getAll: () => Promise<IpcResult<Array<{ username: string; erpUrl: string; erpUsername: string }>>>
error?: string }
}>
/** export interface ProcessAPI {
* Test ERP connection with provided credentials versions: {
*/ electron: string
testConnection: (config: { url: string; username: string; password: string }) => Promise<{ chrome: string
success: boolean node: string
message?: string }
}>
/**
* Get all users' ERP configurations (admin only)
*/
getAll: () => Promise<Array<{ username: string; erpUrl: string; erpUsername: string }>>
} }
declare global { declare global {
interface Window { interface Window {
electron: { electron: {
ipcRenderer: { process: ProcessAPI
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<unknown>
}
process: {
versions: {
electron: string
chrome: string
node: string
}
}
file: FileAPI file: FileAPI
extractor: ExtractorAPI extractor: ExtractorAPI
cleaner: CleanerAPI cleaner: CleanerAPI

View File

@@ -1,5 +1,4 @@
import { contextBridge, ipcRenderer } from 'electron' import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types' import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types'
import type { ExtractorInput, ExtractionProgress } from '../main/types/extractor.types' import type { ExtractorInput, ExtractionProgress } from '../main/types/extractor.types'
import type { CleanerInput, CleanerProgress, ExportResultItem } from '../main/types/cleaner.types' import type { CleanerInput, CleanerProgress, ExportResultItem } from '../main/types/cleaner.types'
@@ -11,158 +10,202 @@ import type {
MaterialTypeRecord, MaterialTypeRecord,
MaterialTypeBatchRequest MaterialTypeBatchRequest
} from '../main/types/validation.types' } 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<T = unknown>(channel: string, ...args: unknown[]): Promise<IpcResult<T>> {
return ipcRenderer.invoke(channel, ...args).then((result: unknown) => {
if (result && typeof result === 'object' && 'success' in (result as Record<string, unknown>)) {
const typed = result as Record<string, unknown>
const hasIpcShape = 'data' in typed || 'code' in typed || 'error' in typed
if (hasIpcShape) {
return typed as unknown as IpcResult<T>
}
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 = { const api = {
// File operations process: processApi,
file: { file: {
readFile: (filePath: string) => ipcRenderer.invoke('file:read', filePath), readFile: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_READ, filePath),
writeFile: (filePath: string, content: string) => writeFile: (filePath: string, content: string) =>
ipcRenderer.invoke('file:write', filePath, content), invokeIpc(IPC_CHANNELS.FILE_WRITE, filePath, content),
fileExists: (filePath: string) => ipcRenderer.invoke('file:exists', filePath), fileExists: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_EXISTS, filePath),
listFiles: (dirPath: string) => ipcRenderer.invoke('file:list', dirPath), listFiles: (dirPath: string) => invokeIpc(IPC_CHANNELS.FILE_LIST, dirPath),
openPath: (filePath: string) => ipcRenderer.invoke('file:openPath', filePath) openPath: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_OPEN_PATH, filePath)
}, },
// Extractor service
extractor: { extractor: {
runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input), runExtractor: (input: ExtractorInput): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.EXTRACTOR_RUN, input),
onProgress: (callback: (data: ExtractionProgress) => void) => { onProgress: (callback: (data: ExtractionProgress) => void) => {
const subscription = (_event: Electron.IpcRendererEvent, data: ExtractionProgress) => const subscription = (_event: Electron.IpcRendererEvent, data: ExtractionProgress) =>
callback(data) callback(data)
ipcRenderer.on('extractor:progress', subscription) ipcRenderer.on(IPC_CHANNELS.EXTRACTOR_PROGRESS, subscription)
return () => ipcRenderer.removeListener('extractor:progress', subscription) return () => ipcRenderer.removeListener(IPC_CHANNELS.EXTRACTOR_PROGRESS, subscription)
}, },
onLog: (callback: (data: { level: string; message: string }) => void) => { onLog: (callback: (data: { level: string; message: string }) => void) => {
const subscription = ( const subscription = (
_event: Electron.IpcRendererEvent, _event: Electron.IpcRendererEvent,
data: { level: string; message: string } data: { level: string; message: string }
) => callback(data) ) => callback(data)
ipcRenderer.on('extractor:log', subscription) ipcRenderer.on(IPC_CHANNELS.EXTRACTOR_LOG, subscription)
return () => ipcRenderer.removeListener('extractor:log', subscription) return () => ipcRenderer.removeListener(IPC_CHANNELS.EXTRACTOR_LOG, subscription)
} }
}, },
// Cleaner service
cleaner: { cleaner: {
runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input), runCleaner: (input: CleanerInput): Promise<IpcResult> =>
exportResults: (items: ExportResultItem[]) => invokeIpc(IPC_CHANNELS.CLEANER_RUN, input),
ipcRenderer.invoke('cleaner:exportResults', items), exportResults: (items: ExportResultItem[]): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.CLEANER_EXPORT_RESULTS, items),
onProgress: (callback: (data: CleanerProgress) => void) => { onProgress: (callback: (data: CleanerProgress) => void) => {
const subscription = (_event: Electron.IpcRendererEvent, data: CleanerProgress) => const subscription = (_event: Electron.IpcRendererEvent, data: CleanerProgress) =>
callback(data) callback(data)
ipcRenderer.on('cleaner:progress', subscription) ipcRenderer.on(IPC_CHANNELS.CLEANER_PROGRESS, subscription)
return () => ipcRenderer.removeListener('cleaner:progress', subscription) return () => ipcRenderer.removeListener(IPC_CHANNELS.CLEANER_PROGRESS, subscription)
} }
}, },
// Order number resolver
resolver: { resolver: {
resolve: (input: ResolverInput) => ipcRenderer.invoke('resolver:resolve', input), resolve: (input: ResolverInput): Promise<IpcResult> =>
validateFormat: (inputs: string[]) => ipcRenderer.invoke('resolver:validateFormat', inputs) invokeIpc(IPC_CHANNELS.RESOLVER_RESOLVE, input),
validateFormat: (inputs: string[]): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.RESOLVER_VALIDATE_FORMAT, inputs)
}, },
// Authentication service
auth: { auth: {
getComputerName: () => ipcRenderer.invoke('auth:getComputerName'), getComputerName: (): Promise<IpcResult> =>
silentLogin: () => ipcRenderer.invoke('auth:silentLogin'), invokeIpc(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME),
login: (request: LoginRequest) => ipcRenderer.invoke('auth:login', request), silentLogin: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_SILENT_LOGIN),
logout: () => ipcRenderer.invoke('auth:logout'), login: (request: LoginRequest): Promise<IpcResult> =>
getCurrentUser: () => ipcRenderer.invoke('auth:getCurrentUser'), invokeIpc(IPC_CHANNELS.AUTH_LOGIN, request),
getAllUsers: () => ipcRenderer.invoke('auth:getAllUsers'), logout: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_LOGOUT),
switchUser: (userInfo: UserInfo) => ipcRenderer.invoke('auth:switchUser', userInfo), getCurrentUser: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_GET_CURRENT_USER),
isAdmin: () => ipcRenderer.invoke('auth:isAdmin') getAllUsers: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_GET_ALL_USERS),
switchUser: (userInfo: UserInfo): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.AUTH_SWITCH_USER, userInfo),
isAdmin: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_IS_ADMIN)
}, },
// Database service
database: { database: {
connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config), connectMySql: (config: MySqlConfig): Promise<IpcResult> =>
disconnectMySql: () => ipcRenderer.invoke('database:mysql:disconnect'), invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_CONNECT, config),
isMySqlConnected: () => ipcRenderer.invoke('database:mysql:isConnected'), disconnectMySql: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT),
queryMySql: (sql: string, params?: any[]) => isMySqlConnected: (): Promise<IpcResult> =>
ipcRenderer.invoke('database:mysql:query', sql, params), invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_IS_CONNECTED),
queryMySql: (sql: string, params?: any[]): Promise<IpcResult> =>
// SQL Server invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_QUERY, sql, params),
connectSqlServer: (config: SqlServerConfig) => connectSqlServer: (config: SqlServerConfig): Promise<IpcResult> =>
ipcRenderer.invoke('database:sqlserver:connect', config), invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_CONNECT, config),
disconnectSqlServer: () => ipcRenderer.invoke('database:sqlserver:disconnect'), disconnectSqlServer: (): Promise<IpcResult> =>
isSqlServerConnected: () => ipcRenderer.invoke('database:sqlserver:isConnected'), invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT),
querySqlServer: (sql: string, params?: Record<string, unknown>) => isSqlServerConnected: (): Promise<IpcResult> =>
ipcRenderer.invoke('database:sqlserver:query', sql, params) invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_IS_CONNECTED),
querySqlServer: (sql: string, params?: Record<string, unknown>): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_QUERY, sql, params)
}, },
// Validation service
validation: { validation: {
validate: (request: ValidationRequest) => ipcRenderer.invoke('validation:validate', request), validate: (request: ValidationRequest): Promise<IpcResult> =>
setSharedProductionIds: (productionIds: string[]) => invokeIpc(IPC_CHANNELS.VALIDATION_VALIDATE, request),
ipcRenderer.invoke('validation:setSharedProductionIds', productionIds), setSharedProductionIds: (productionIds: string[]): Promise<IpcResult> =>
getSharedProductionIds: () => ipcRenderer.invoke('validation:getSharedProductionIds'), invokeIpc(IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS, productionIds),
getCleanerData: () => ipcRenderer.invoke('validation:getCleanerData') getSharedProductionIds: (): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS),
getCleanerData: (): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA)
}, },
// Materials service
materials: { materials: {
upsertBatch: (materials: { materialCode: string; managerName: string }[]) => upsertBatch: (materials: { materialCode: string; managerName: string }[]): Promise<IpcResult> =>
ipcRenderer.invoke('materials:upsertBatch', { materials }), invokeIpc(IPC_CHANNELS.MATERIALS_UPSERT_BATCH, { materials }),
delete: (materialCodes: string[]) => ipcRenderer.invoke('materials:delete', { materialCodes }), delete: (materialCodes: string[]): Promise<IpcResult> =>
getManagers: () => ipcRenderer.invoke('materials:getManagers'), invokeIpc(IPC_CHANNELS.MATERIALS_DELETE, { materialCodes }),
getByManager: (managerName: string) => getManagers: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.MATERIALS_GET_MANAGERS),
ipcRenderer.invoke('materials:getByManager', managerName), getByManager: (managerName: string): Promise<IpcResult> =>
getAll: () => ipcRenderer.invoke('materials:getAll'), invokeIpc(IPC_CHANNELS.MATERIALS_GET_BY_MANAGER, managerName),
getStatistics: () => ipcRenderer.invoke('materials:getStatistics'), getAll: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.MATERIALS_GET_ALL),
updateManager: (materialCode: string, managerName: string) => getStatistics: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.MATERIALS_GET_STATISTICS),
ipcRenderer.invoke('materials:updateManager', { materialCode, managerName }) updateManager: (materialCode: string, managerName: string): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.MATERIALS_UPDATE_MANAGER, { materialCode, managerName })
}, },
// Settings service
settings: { settings: {
getUserType: () => ipcRenderer.invoke('settings:getUserType'), getUserType: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.SETTINGS_GET_USER_TYPE),
getSettings: () => ipcRenderer.invoke('settings:getSettings'), getSettings: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.SETTINGS_GET_SETTINGS),
saveSettings: (settings: SettingsData) => ipcRenderer.invoke('settings:saveSettings', settings), saveSettings: (settings: ErpSettingsPayload): Promise<IpcResult> =>
resetDefaults: () => ipcRenderer.invoke('settings:resetDefaults'), invokeIpc(IPC_CHANNELS.SETTINGS_SAVE_SETTINGS, settings),
testErpConnection: () => ipcRenderer.invoke('settings:testErpConnection'), resetDefaults: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.SETTINGS_RESET_DEFAULTS),
testDbConnection: () => ipcRenderer.invoke('settings:testDbConnection') testDbConnection: (): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.SETTINGS_TEST_DB_CONNECTION)
}, },
// Material Type service
materialType: { materialType: {
getAll: () => ipcRenderer.invoke('materialType:getAll'), getAll: (): Promise<IpcResult<MaterialTypeRecord[]>> =>
getByManager: (managerName: string) => invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_ALL),
ipcRenderer.invoke('materialType:getByManager', managerName), getByManager: (managerName: string): Promise<IpcResult<MaterialTypeRecord[]>> =>
getManagers: () => ipcRenderer.invoke('materialType:getManagers'), invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_BY_MANAGER, managerName),
upsert: (materialName: string, managerName: string) => getManagers: (): Promise<IpcResult<string[]>> =>
ipcRenderer.invoke('materialType:upsert', { materialName, managerName }), invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_MANAGERS),
delete: (materialName: string, managerName: string) => upsert: (materialName: string, managerName: string): Promise<IpcResult> =>
ipcRenderer.invoke('materialType:delete', { materialName, managerName }), invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_UPSERT, { materialName, managerName }),
upsertBatch: (request: MaterialTypeBatchRequest) => delete: (materialName: string, managerName: string): Promise<IpcResult> =>
ipcRenderer.invoke('materialType:upsertBatch', request) invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_DELETE, { materialName, managerName }),
upsertBatch: (request: MaterialTypeBatchRequest): Promise<IpcResult> =>
invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_UPSERT_BATCH, request)
}, },
// User ERP Configuration service
userErpConfig: { userErpConfig: {
getCurrent: () => ipcRenderer.invoke('user-erp-config:getCurrent'), getCurrent: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_GET_CURRENT),
update: (config: { url: string; username: string; password: string }) => update: (config: { url: string; username: string; password: string }): Promise<IpcResult> =>
ipcRenderer.invoke('user-erp-config:update', config), invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_UPDATE, config),
testConnection: (config: { url: string; username: string; password: string }) => testConnection: (config: { url: string; username: string; password: string }): Promise<IpcResult> =>
ipcRenderer.invoke('user-erp-config:testConnection', config), invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_TEST_CONNECTION, config),
getAll: () => ipcRenderer.invoke('user-erp-config:getAll') getAll: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_GET_ALL)
} }
} as const } 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) { if (process.contextIsolated) {
try { try {
contextBridge.exposeInMainWorld('electron', { ...electronAPI, ...api }) contextBridge.exposeInMainWorld('electron', api)
contextBridge.exposeInMainWorld('api', api) contextBridge.exposeInMainWorld('api', api)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} else { } else {
// @ts-ignore (define in dts) // @ts-ignore (define in dts)
window.electron = { ...electronAPI, ...api } window.electron = api
// @ts-ignore (define in dts) // @ts-ignore (define in dts)
window.api = api window.api = api
} }

View File

@@ -61,16 +61,18 @@ function App(): React.JSX.Element {
try { try {
// Get computer name // Get computer name
console.log('Getting 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) console.log('Computer name:', name)
setComputerName(name) setComputerName(name)
// Try silent login // Try silent login
console.log('Trying 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) 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) console.log('Silent login success:', result.userInfo)
setCurrentUser({ setCurrentUser({
username: result.userInfo.username, username: result.userInfo.username,
@@ -81,8 +83,8 @@ function App(): React.JSX.Element {
if (result.requiresUserSelection) { if (result.requiresUserSelection) {
console.log('Admin user needs to select user') console.log('Admin user needs to select user')
// Load all users for selection // Load all users for selection
const users = await window.electron.auth.getAllUsers() const usersResult = await window.electron.auth.getAllUsers()
setAllUsers(users) setAllUsers(usersResult.success && usersResult.data ? usersResult.data : [])
setShowUserSelection(true) setShowUserSelection(true)
} else { } else {
console.log('Setting authenticated to true') console.log('Setting authenticated to true')
@@ -107,19 +109,20 @@ function App(): React.JSX.Element {
const handleLogin = async (username: string, password: string): Promise<boolean> => { const handleLogin = async (username: string, password: string): Promise<boolean> => {
try { try {
const result = await window.electron.auth.login({ username, password }) 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({ setCurrentUser({
username: result.userInfo.username, username: loginData.userInfo.username,
userType: result.userInfo.userType userType: loginData.userInfo.userType
}) })
// Check if admin needs user selection // Check if admin needs user selection
if (result.userInfo.userType === 'Admin') { if (loginData.userInfo.userType === 'Admin') {
setShowLoginDialog(false) setShowLoginDialog(false)
// Load all users for selection // Load all users for selection
const users = await window.electron.auth.getAllUsers() const usersResult = await window.electron.auth.getAllUsers()
setAllUsers(users) setAllUsers(usersResult.success && usersResult.data ? usersResult.data : [])
setShowUserSelection(true) setShowUserSelection(true)
} else { } else {
setIsAuthenticated(true) setIsAuthenticated(true)
@@ -144,10 +147,11 @@ function App(): React.JSX.Element {
const handleUserSelect = async (user: SelectedUserInfo) => { const handleUserSelect = async (user: SelectedUserInfo) => {
try { try {
const result = await window.electron.auth.switchUser(user) const result = await window.electron.auth.switchUser(user)
if (result.success) { const switchData = result.success ? result.data : undefined
if (result.success && switchData) {
setCurrentUser({ setCurrentUser({
username: result.userInfo?.username || user.username, username: switchData.userInfo?.username || user.username,
userType: result.userInfo?.userType || user.userType userType: switchData.userInfo?.userType || user.userType
}) })
setIsAuthenticated(true) setIsAuthenticated(true)
setShowUserSelection(false) setShowUserSelection(false)

View File

@@ -252,10 +252,11 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
toUpdate, toUpdate,
toDelete toDelete
}) })
const payload = result.success ? (result.data as { stats?: { success?: number; failed?: number } } | undefined) : undefined
if (result.success) { if (result.success) {
alert( alert(
`保存完成!\n成功${result.stats?.success || 0}\n失败${result.stats?.failed || 0}` `保存完成!\n成功${payload?.stats?.success || 0}\n失败${payload?.stats?.failed || 0}`
) )
await loadData() await loadData()
} else { } else {

View File

@@ -1,13 +1,5 @@
/**
* IPC Hook for Authentication operations
*
* Provides a React-friendly interface for auth IPC calls
* with loading state, error handling, and user data management.
*/
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
// Types based on the user types
interface UserInfo { interface UserInfo {
id: number id: number
username: string username: string
@@ -38,9 +30,6 @@ interface UseAuthReturn extends UseAuthState {
reset: () => void reset: () => void
} }
/**
* Hook for authentication operations
*/
export function useAuth(): UseAuthReturn { export function useAuth(): UseAuthReturn {
const [state, setState] = useState<UseAuthState>({ const [state, setState] = useState<UseAuthState>({
loading: false, loading: false,
@@ -51,156 +40,94 @@ export function useAuth(): UseAuthReturn {
const login = useCallback(async (credentials: LoginCredentials): Promise<boolean> => { const login = useCallback(async (credentials: LoginCredentials): Promise<boolean> => {
setState((prev) => ({ ...prev, loading: true, error: null })) setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.login(credentials)
try { if (!result.success || !result.data?.userInfo) {
const result = (await window.electron.ipcRenderer.invoke('auth:login', credentials)) as { setState((prev) => ({ ...prev, loading: false, error: result.error ?? 'Login failed' }))
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 }))
return false return false
} }
setState({
loading: false,
user: result.data.userInfo,
error: null,
isAuthenticated: true
})
return true
}, []) }, [])
const silentLogin = useCallback(async (): Promise<{ const silentLogin = useCallback(async (): Promise<{ success: boolean; requiresUserSelection?: boolean }> => {
success: boolean
requiresUserSelection?: boolean
}> => {
setState((prev) => ({ ...prev, loading: true, error: null })) setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.silentLogin()
try { if (!result.success || !result.data?.userInfo) {
const result = (await window.electron.ipcRenderer.invoke('auth:silentLogin')) as { setState((prev) => ({ ...prev, loading: false, error: result.error || null }))
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 }))
return { success: false } 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<void> => { const logout = useCallback(async (): Promise<void> => {
try { await window.electron.auth.logout()
await window.electron.ipcRenderer.invoke('auth:logout') setState({
setState({ loading: false,
loading: false, user: null,
user: null, error: null,
error: null, isAuthenticated: false
isAuthenticated: false })
})
} catch (error) {
console.error('Logout error:', error)
}
}, []) }, [])
const getCurrentUser = useCallback(async (): Promise<UserInfo | null> => { const getCurrentUser = useCallback(async (): Promise<UserInfo | null> => {
try { const result = await window.electron.auth.getCurrentUser()
const result = (await window.electron.ipcRenderer.invoke('auth:getCurrentUser')) as { if (!result.success || !result.data?.isAuthenticated || !result.data.userInfo) {
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 {
return null return null
} }
setState((prev) => ({
...prev,
user: result.data!.userInfo!,
isAuthenticated: true
}))
return result.data.userInfo
}, []) }, [])
const getAllUsers = useCallback(async (): Promise<UserInfo[]> => { const getAllUsers = useCallback(async (): Promise<UserInfo[]> => {
try { const result = await window.electron.auth.getAllUsers()
return (await window.electron.ipcRenderer.invoke('auth:getAllUsers')) as UserInfo[] return result.success && result.data ? result.data : []
} catch {
return []
}
}, []) }, [])
const switchUser = useCallback(async (userInfo: UserInfo): Promise<boolean> => { const switchUser = useCallback(async (userInfo: UserInfo): Promise<boolean> => {
setState((prev) => ({ ...prev, loading: true, error: null })) setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.switchUser(userInfo)
try { if (!result.success || !result.data?.userInfo) {
const result = (await window.electron.ipcRenderer.invoke('auth:switchUser', userInfo)) as { setState((prev) => ({
success: boolean ...prev,
userInfo?: UserInfo loading: false,
error?: string error: result.error || 'Switch user failed'
} }))
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 }))
return false return false
} }
setState({
loading: false,
user: result.data.userInfo,
error: null,
isAuthenticated: true
})
return true
}, []) }, [])
const isAdmin = useCallback(async (): Promise<boolean> => { const isAdmin = useCallback(async (): Promise<boolean> => {
try { const result = await window.electron.auth.isAdmin()
return (await window.electron.ipcRenderer.invoke('auth:isAdmin')) as boolean return result.success && Boolean(result.data)
} catch {
return false
}
}, []) }, [])
const reset = useCallback(() => { const reset = useCallback(() => {

View File

@@ -85,8 +85,10 @@ export function useCleaner() {
useEffect(() => { useEffect(() => {
const initializePage = async () => { const initializePage = async () => {
try { try {
const admin = await window.electron.auth.isAdmin() const adminResult = await window.electron.auth.isAdmin()
const user = await window.electron.auth.getCurrentUser() const userResult = await window.electron.auth.getCurrentUser()
const admin = adminResult.success && Boolean(adminResult.data)
const user = userResult.success ? userResult.data : undefined
setIsAdmin(admin) setIsAdmin(admin)
if (user && user.userInfo) { if (user && user.userInfo) {
setCurrentUsername(user.userInfo.username) setCurrentUsername(user.userInfo.username)
@@ -98,13 +100,16 @@ export function useCleaner() {
// Load managers // Load managers
if (admin) { if (admin) {
const resp = await window.electron.materials.getManagers() const resp = await window.electron.materials.getManagers()
setManagers(resp.managers) const managersPayload = resp.success ? (resp.data as { managers: string[] } | undefined) : undefined
setSelectedManagers(new Set(resp.managers)) const managerList = managersPayload?.managers ?? []
setManagers(managerList)
setSelectedManagers(new Set(managerList))
} }
// Get shared Production IDs // Get shared Production IDs
const result = await window.electron.validation.getSharedProductionIds() 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) { } catch (err) {
console.error('Initialization failed:', err) console.error('Initialization failed:', err)
} }
@@ -159,21 +164,28 @@ export function useCleaner() {
mode: valMode === 'full' ? 'database_full' : 'database_filtered', mode: valMode === 'full' ? 'database_full' : 'database_filtered',
useSharedProductionIds: valMode === 'filtered' useSharedProductionIds: valMode === 'filtered'
}) })
const validationData = response.success ? (response.data as any) : null
if (response.success && response.results) { if (response.success && validationData?.success && validationData.results) {
setValidationResults(response.results) setValidationResults(validationData.results)
const markedCodes = new Set( const markedCodes = new Set<string>(
response.results.filter((r) => r.isMarkedForDeletion).map((r) => r.materialCode) validationData.results
.filter((r: ValidationResult) => r.isMarkedForDeletion)
.map((r: ValidationResult) => r.materialCode)
) )
setSelectedItems(markedCodes) setSelectedItems(markedCodes)
if (isAdmin) { if (isAdmin) {
const uniqueManagers = new Set(response.results.map((r) => r.managerName).filter(Boolean)) const uniqueManagers = new Set<string>(
setManagers([...uniqueManagers]) validationData.results
.map((r: ValidationResult) => r.managerName)
.filter((name: string) => Boolean(name))
)
setManagers(Array.from(uniqueManagers))
setSelectedManagers(uniqueManagers) setSelectedManagers(uniqueManagers)
} }
} else { } else {
alert(response.error || '校验失败') alert(response.error || validationData?.error || '校验失败')
} }
} catch (err) { } catch (err) {
alert(err instanceof Error ? err.message : '校验过程中发生未知错误') alert(err instanceof Error ? err.message : '校验过程中发生未知错误')
@@ -285,14 +297,16 @@ export function useCleaner() {
if (materialsToUpsert.length > 0) { if (materialsToUpsert.length > 0) {
const res = await window.electron.materials.upsertBatch(materialsToUpsert) 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 || '写入物料失败') if (!res.success) throw new Error(res.error || '写入物料失败')
msgParts.push(`写入/更新成功:${res.stats?.success || 0}`) msgParts.push(`写入/更新成功:${payload?.stats?.success || 0}`)
} }
if (materialsToDelete.length > 0) { if (materialsToDelete.length > 0) {
const res = await window.electron.materials.delete(materialsToDelete) 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 || '删除物料失败') if (!res.success) throw new Error(res.error || '删除物料失败')
msgParts.push(`删除成功:${res.count || 0}`) msgParts.push(`删除成功:${payload?.count || 0}`)
} }
alert(`操作完成!\n\n${msgParts.join('\n')}`) alert(`操作完成!\n\n${msgParts.join('\n')}`)
@@ -300,7 +314,8 @@ export function useCleaner() {
// Reload managers if admin // Reload managers if admin
if (isAdmin) { if (isAdmin) {
const resp = await window.electron.materials.getManagers() 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) { } catch (err) {
alert(err instanceof Error ? err.message : '操作失败') alert(err instanceof Error ? err.message : '操作失败')
@@ -331,12 +346,13 @@ export function useCleaner() {
try { try {
const cleanerDataResult = await window.electron.validation.getCleanerData() 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 || '获取清理数据失败') throw new Error(cleanerDataResult.error || '获取清理数据失败')
} }
const orderNumberList = cleanerDataResult.orderNumbers || [] const orderNumberList = cleanerData?.orderNumbers || []
const materialCodeList = cleanerDataResult.materialCodes || [] const materialCodeList = cleanerData?.materialCodes || []
if (orderNumberList.length === 0) if (orderNumberList.length === 0)
throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。') throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。')
@@ -349,13 +365,14 @@ export function useCleaner() {
dryRun, dryRun,
headless headless
}) })
const cleanerRunData = response.success ? (response.data as any) : null
if (response.success && response.data) { if (response.success && cleanerRunData) {
setReportData({ setReportData({
ordersProcessed: response.data.ordersProcessed, ordersProcessed: cleanerRunData.ordersProcessed,
materialsDeleted: response.data.materialsDeleted, materialsDeleted: cleanerRunData.materialsDeleted,
materialsSkipped: response.data.materialsSkipped, materialsSkipped: cleanerRunData.materialsSkipped,
errors: response.data.errors errors: cleanerRunData.errors
}) })
} else { } else {
throw new Error(response.error || '清理失败') throw new Error(response.error || '清理失败')
@@ -390,11 +407,12 @@ export function useCleaner() {
})) }))
const response = await window.electron.cleaner.exportResults(exportItems) const response = await window.electron.cleaner.exportResults(exportItems)
const exportData = response.success ? (response.data as any) : null
if (response.success) { if (response.success && exportData?.success !== false) {
alert(`导出成功!\n文件已保存到${response.filePath}`) alert(`导出成功!\n文件已保存到${exportData?.filePath ?? ''}`)
} else { } else {
throw new Error(response.error || '导出失败') throw new Error(response.error || exportData?.error || '导出失败')
} }
} catch (err) { } catch (err) {
alert(err instanceof Error ? err.message : '导出过程中发生错误') alert(err instanceof Error ? err.message : '导出过程中发生错误')

View File

@@ -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' import { useState, useCallback } from 'react'
// Types based on validation types
interface ValidationRequest { interface ValidationRequest {
mode: 'database_full' | 'database_filtered' mode: 'database_full' | 'database_filtered'
productionIdFile?: string productionIdFile?: string
@@ -52,9 +44,6 @@ interface UseValidationReturn extends UseValidationState {
reset: () => void reset: () => void
} }
/**
* Hook for validation operations
*/
export function useValidation(): UseValidationReturn { export function useValidation(): UseValidationReturn {
const [state, setState] = useState<UseValidationState>({ const [state, setState] = useState<UseValidationState>({
loading: false, loading: false,
@@ -63,82 +52,62 @@ export function useValidation(): UseValidationReturn {
error: null error: null
}) })
const validate = useCallback( const validate = useCallback(async (request: ValidationRequest): Promise<ValidationResponse | null> => {
async (request: ValidationRequest): Promise<ValidationResponse | null> => { setState((prev) => ({ ...prev, loading: true, error: null }))
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 = response.data as ValidationResponse
const result = (await window.electron.ipcRenderer.invoke( if (!result.success) {
'validation:validate', setState((prev) => ({ ...prev, loading: false, error: result.error || 'Validation failed' }))
request return result
)) as ValidationResponse }
if (result.success) { setState({
const results = result.results || null loading: false,
const stats = result.stats || null data: result.results || null,
setState({ stats: result.stats || null,
loading: false, error: null
data: results, })
stats: stats, return result
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
}
},
[]
)
const setSharedProductionIds = useCallback(async (ids: string[]): Promise<void> => { const setSharedProductionIds = useCallback(async (ids: string[]): Promise<void> => {
try { await window.electron.validation.setSharedProductionIds(ids)
await window.electron.ipcRenderer.invoke('validation:setSharedProductionIds', ids)
} catch (error) {
console.error('Failed to set shared production IDs:', error)
}
}, []) }, [])
const getSharedProductionIds = useCallback(async (): Promise<string[]> => { const getSharedProductionIds = useCallback(async (): Promise<string[]> => {
try { const result = await window.electron.validation.getSharedProductionIds()
const result = (await window.electron.ipcRenderer.invoke( if (!result.success || !result.data) {
'validation:getSharedProductionIds'
)) as { productionIds?: string[] }
return result?.productionIds || []
} catch {
return [] return []
} }
const payload = result.data as { productionIds?: string[] }
return payload.productionIds || []
}, []) }, [])
const getCleanerData = useCallback(async (): Promise<{ const getCleanerData = useCallback(async (): Promise<{ orderNumbers: string[]; materialCodes: string[] } | null> => {
orderNumbers: string[] const result = await window.electron.validation.getCleanerData()
materialCodes: string[] if (!result.success || !result.data) {
} | 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 || []
}
}
return null return null
} catch { }
const payload = result.data as {
success?: boolean
orderNumbers?: string[]
materialCodes?: string[]
}
if (payload.success === false) {
return null return null
} }
return {
orderNumbers: payload.orderNumbers || [],
materialCodes: payload.materialCodes || []
}
}, []) }, [])
const reset = useCallback(() => { const reset = useCallback(() => {

View File

@@ -26,14 +26,17 @@ const SettingsPage: React.FC = () => {
try { try {
setIsLoading(true) setIsLoading(true)
// ERP credentials are loaded from database (current user's config) // 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 // Extract ERP credentials from the config
if (config && (config as any).erp) { if (config?.erp) {
setCredentials({ setCredentials({
username: (config as any).erp.username || '', username: config.erp.username || '',
password: (config as any).erp.password || '' password: config.erp.password || ''
}) })
} else if (!response.success) {
showMessage('error', response.error || '加载 ERP 配置失败')
} }
setIsModified(false) setIsModified(false)
} catch (error) { } catch (error) {
@@ -56,13 +59,14 @@ const SettingsPage: React.FC = () => {
username: credentials.username, username: credentials.username,
password: credentials.password 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) setIsModified(false)
showMessage('success', 'ERP 账号密码保存成功') showMessage('success', 'ERP 账号密码保存成功')
} else { } else {
showMessage('error', result.error || '保存失败') showMessage('error', result.error || saveData?.error || '保存失败')
} }
} catch (error) { } catch (error) {
showMessage('error', '保存配置时发生错误') showMessage('error', '保存配置时发生错误')

View File

@@ -9,17 +9,77 @@ export const IPC_CHANNELS = {
FILE_WRITE: 'file:write', FILE_WRITE: 'file:write',
FILE_EXISTS: 'file:exists', FILE_EXISTS: 'file:exists',
FILE_LIST: 'file:list', FILE_LIST: 'file:list',
FILE_OPEN_PATH: 'file:openPath',
// Extractor service // Extractor service
EXTRACTOR_RUN: 'extractor:run', EXTRACTOR_RUN: 'extractor:run',
EXTRACTOR_PROGRESS: 'extractor:progress',
EXTRACTOR_LOG: 'extractor:log',
// Cleaner service // Cleaner service
CLEANER_RUN: 'cleaner:run', CLEANER_RUN: 'cleaner:run',
CLEANER_EXPORT_RESULTS: 'cleaner:exportResults', CLEANER_EXPORT_RESULTS: 'cleaner:exportResults',
CLEANER_PROGRESS: 'cleaner:progress',
// Database service - MySQL // Database service - MySQL
DATABASE_MYSQL_CONNECT: 'database:mysql:connect', DATABASE_MYSQL_CONNECT: 'database:mysql:connect',
DATABASE_MYSQL_DISCONNECT: 'database:mysql:disconnect', DATABASE_MYSQL_DISCONNECT: 'database:mysql:disconnect',
DATABASE_MYSQL_IS_CONNECTED: 'database:mysql:isConnected', 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 } as const

View File

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

View File

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

View File

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