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

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

View File

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

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

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 path from 'path'
import { createLogger } from '../services/logger'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
import { ValidationError } from '../types/errors'
import { withErrorHandling, type IpcResult } from './index'
const log = createLogger('FileHandler')
function getAllowedRoots(): string[] {
return [path.resolve(app.getAppPath()), path.resolve(app.getPath('userData'))]
}
export function isPathWithinAllowedRoots(inputPath: string, roots: string[]): boolean {
const normalized = path.resolve(inputPath)
return roots.some((root) => {
const rel = path.relative(root, normalized)
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
})
}
function normalizeAndValidatePath(inputPath: string): string {
const normalized = path.resolve(inputPath)
const isAllowed = isPathWithinAllowedRoots(normalized, getAllowedRoots())
if (!isAllowed) {
throw new ValidationError('Path is outside allowed roots', 'VAL_INVALID_INPUT')
}
return normalized
}
export function registerFileHandlers(): void {
ipcMain.handle('file:read', async (_event, filePath: string): Promise<string> => {
try {
log.debug('Reading file', { filePath })
return await fs.readFile(filePath, 'utf-8')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to read file'
log.error('Failed to read file', { filePath, error: message })
throw new Error(message)
}
ipcMain.handle(IPC_CHANNELS.FILE_READ, async (_event, filePath: string): Promise<IpcResult<string>> => {
return withErrorHandling(async () => {
const safePath = normalizeAndValidatePath(filePath)
log.debug('Reading file', { filePath: safePath })
return await fs.readFile(safePath, 'utf-8')
}, 'file:read')
})
ipcMain.handle('file:write', async (_event, filePath: string, content: string): Promise<void> => {
try {
log.debug('Writing file', { filePath })
const dir = path.dirname(filePath)
await fs.mkdir(dir, { recursive: true })
await fs.writeFile(filePath, content, 'utf-8')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to write file'
log.error('Failed to write file', { filePath, error: message })
throw new Error(message)
ipcMain.handle(
IPC_CHANNELS.FILE_WRITE,
async (_event, filePath: string, content: string): Promise<IpcResult<void>> => {
return withErrorHandling(async () => {
const safePath = normalizeAndValidatePath(filePath)
log.debug('Writing file', { filePath: safePath })
const dir = path.dirname(safePath)
await fs.mkdir(dir, { recursive: true })
await fs.writeFile(safePath, content, 'utf-8')
}, 'file:write')
}
)
ipcMain.handle(IPC_CHANNELS.FILE_EXISTS, async (_event, filePath: string): Promise<IpcResult<boolean>> => {
return withErrorHandling(async () => {
const safePath = normalizeAndValidatePath(filePath)
try {
await fs.access(safePath)
return true
} catch {
return false
}
}, 'file:exists')
})
ipcMain.handle('file:exists', async (_event, filePath: string): Promise<boolean> => {
try {
await fs.access(filePath)
return true
} catch {
return false
}
})
ipcMain.handle('file:list', async (_event, dirPath: string): Promise<string[]> => {
try {
log.debug('Listing directory', { dirPath })
const entries = await fs.readdir(dirPath, { withFileTypes: true })
ipcMain.handle(IPC_CHANNELS.FILE_LIST, async (_event, dirPath: string): Promise<IpcResult<string[]>> => {
return withErrorHandling(async () => {
const safePath = normalizeAndValidatePath(dirPath)
log.debug('Listing directory', { dirPath: safePath })
const entries = await fs.readdir(safePath, { withFileTypes: true })
return entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.sort()
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to list directory'
log.error('Failed to list directory', { dirPath, error: message })
throw new Error(message)
}
}, 'file:list')
})
ipcMain.handle('file:openPath', async (_event, filePath: string): Promise<void> => {
try {
log.debug('Opening path in explorer', { filePath })
await shell.openPath(filePath)
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to open path'
log.error('Failed to open path', { filePath, error: message })
throw new Error(message)
}
ipcMain.handle(IPC_CHANNELS.FILE_OPEN_PATH, async (_event, filePath: string): Promise<IpcResult<void>> => {
return withErrorHandling(async () => {
const safePath = normalizeAndValidatePath(filePath)
log.debug('Opening path in explorer', { filePath: safePath })
await fs.access(safePath)
await shell.openPath(safePath)
}, 'file:openPath')
})
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -252,10 +252,11 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
toUpdate,
toDelete
})
const payload = result.success ? (result.data as { stats?: { success?: number; failed?: number } } | undefined) : undefined
if (result.success) {
alert(
`保存完成!\n成功${result.stats?.success || 0}\n失败${result.stats?.failed || 0}`
`保存完成!\n成功${payload?.stats?.success || 0}\n失败${payload?.stats?.failed || 0}`
)
await loadData()
} 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'
// Types based on the user types
interface UserInfo {
id: number
username: string
@@ -38,9 +30,6 @@ interface UseAuthReturn extends UseAuthState {
reset: () => void
}
/**
* Hook for authentication operations
*/
export function useAuth(): UseAuthReturn {
const [state, setState] = useState<UseAuthState>({
loading: false,
@@ -51,156 +40,94 @@ export function useAuth(): UseAuthReturn {
const login = useCallback(async (credentials: LoginCredentials): Promise<boolean> => {
setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.login(credentials)
try {
const result = (await window.electron.ipcRenderer.invoke('auth:login', credentials)) as {
success: boolean
userInfo?: UserInfo
error?: string
}
if (result.success && result.userInfo) {
setState({
loading: false,
user: result.userInfo,
error: null,
isAuthenticated: true
})
return true
} else {
setState((prev) => ({
...prev,
loading: false,
error: result.error || 'Login failed'
}))
return false
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setState((prev) => ({ ...prev, loading: false, error: message }))
if (!result.success || !result.data?.userInfo) {
setState((prev) => ({ ...prev, loading: false, error: result.error ?? 'Login failed' }))
return false
}
setState({
loading: false,
user: result.data.userInfo,
error: null,
isAuthenticated: true
})
return true
}, [])
const silentLogin = useCallback(async (): Promise<{
success: boolean
requiresUserSelection?: boolean
}> => {
const silentLogin = useCallback(async (): Promise<{ success: boolean; requiresUserSelection?: boolean }> => {
setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.silentLogin()
try {
const result = (await window.electron.ipcRenderer.invoke('auth:silentLogin')) as {
success: boolean
userInfo?: UserInfo
error?: string
requiresUserSelection?: boolean
}
if (result.success && result.userInfo) {
setState({
loading: false,
user: result.userInfo,
error: null,
isAuthenticated: true
})
return { success: true, requiresUserSelection: result.requiresUserSelection }
} else {
setState((prev) => ({
...prev,
loading: false,
error: result.error || null
}))
return { success: false }
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setState((prev) => ({ ...prev, loading: false, error: message }))
if (!result.success || !result.data?.userInfo) {
setState((prev) => ({ ...prev, loading: false, error: result.error || null }))
return { success: false }
}
setState({
loading: false,
user: result.data.userInfo,
error: null,
isAuthenticated: true
})
return { success: true, requiresUserSelection: result.data.requiresUserSelection }
}, [])
const logout = useCallback(async (): Promise<void> => {
try {
await window.electron.ipcRenderer.invoke('auth:logout')
setState({
loading: false,
user: null,
error: null,
isAuthenticated: false
})
} catch (error) {
console.error('Logout error:', error)
}
await window.electron.auth.logout()
setState({
loading: false,
user: null,
error: null,
isAuthenticated: false
})
}, [])
const getCurrentUser = useCallback(async (): Promise<UserInfo | null> => {
try {
const result = (await window.electron.ipcRenderer.invoke('auth:getCurrentUser')) as {
isAuthenticated: boolean
userInfo?: UserInfo
}
if (result.isAuthenticated && result.userInfo) {
const userInfo = result.userInfo
setState((prev) => ({
...prev,
user: userInfo,
isAuthenticated: true
}))
return userInfo
}
return null
} catch {
const result = await window.electron.auth.getCurrentUser()
if (!result.success || !result.data?.isAuthenticated || !result.data.userInfo) {
return null
}
setState((prev) => ({
...prev,
user: result.data!.userInfo!,
isAuthenticated: true
}))
return result.data.userInfo
}, [])
const getAllUsers = useCallback(async (): Promise<UserInfo[]> => {
try {
return (await window.electron.ipcRenderer.invoke('auth:getAllUsers')) as UserInfo[]
} catch {
return []
}
const result = await window.electron.auth.getAllUsers()
return result.success && result.data ? result.data : []
}, [])
const switchUser = useCallback(async (userInfo: UserInfo): Promise<boolean> => {
setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.switchUser(userInfo)
try {
const result = (await window.electron.ipcRenderer.invoke('auth:switchUser', userInfo)) as {
success: boolean
userInfo?: UserInfo
error?: string
}
if (result.success && result.userInfo) {
setState({
loading: false,
user: result.userInfo,
error: null,
isAuthenticated: true
})
return true
} else {
setState((prev) => ({
...prev,
loading: false,
error: result.error || 'Switch user failed'
}))
return false
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setState((prev) => ({ ...prev, loading: false, error: message }))
if (!result.success || !result.data?.userInfo) {
setState((prev) => ({
...prev,
loading: false,
error: result.error || 'Switch user failed'
}))
return false
}
setState({
loading: false,
user: result.data.userInfo,
error: null,
isAuthenticated: true
})
return true
}, [])
const isAdmin = useCallback(async (): Promise<boolean> => {
try {
return (await window.electron.ipcRenderer.invoke('auth:isAdmin')) as boolean
} catch {
return false
}
const result = await window.electron.auth.isAdmin()
return result.success && Boolean(result.data)
}, [])
const reset = useCallback(() => {

View File

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

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

View File

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

View File

@@ -9,17 +9,77 @@ export const IPC_CHANNELS = {
FILE_WRITE: 'file:write',
FILE_EXISTS: 'file:exists',
FILE_LIST: 'file:list',
FILE_OPEN_PATH: 'file:openPath',
// Extractor service
EXTRACTOR_RUN: 'extractor:run',
EXTRACTOR_PROGRESS: 'extractor:progress',
EXTRACTOR_LOG: 'extractor:log',
// Cleaner service
CLEANER_RUN: 'cleaner:run',
CLEANER_EXPORT_RESULTS: 'cleaner:exportResults',
CLEANER_PROGRESS: 'cleaner:progress',
// Database service - MySQL
DATABASE_MYSQL_CONNECT: 'database:mysql:connect',
DATABASE_MYSQL_DISCONNECT: 'database:mysql:disconnect',
DATABASE_MYSQL_IS_CONNECTED: 'database:mysql:isConnected',
DATABASE_MYSQL_QUERY: 'database:mysql:query'
DATABASE_MYSQL_QUERY: 'database:mysql:query',
// Database service - SQL Server
DATABASE_SQLSERVER_CONNECT: 'database:sqlserver:connect',
DATABASE_SQLSERVER_DISCONNECT: 'database:sqlserver:disconnect',
DATABASE_SQLSERVER_IS_CONNECTED: 'database:sqlserver:isConnected',
DATABASE_SQLSERVER_QUERY: 'database:sqlserver:query',
// Resolver
RESOLVER_RESOLVE: 'resolver:resolve',
RESOLVER_VALIDATE_FORMAT: 'resolver:validateFormat',
// Auth
AUTH_GET_COMPUTER_NAME: 'auth:getComputerName',
AUTH_SILENT_LOGIN: 'auth:silentLogin',
AUTH_LOGIN: 'auth:login',
AUTH_LOGOUT: 'auth:logout',
AUTH_GET_CURRENT_USER: 'auth:getCurrentUser',
AUTH_GET_ALL_USERS: 'auth:getAllUsers',
AUTH_SWITCH_USER: 'auth:switchUser',
AUTH_IS_ADMIN: 'auth:isAdmin',
// Validation
VALIDATION_VALIDATE: 'validation:validate',
VALIDATION_SET_SHARED_PRODUCTION_IDS: 'validation:setSharedProductionIds',
VALIDATION_GET_SHARED_PRODUCTION_IDS: 'validation:getSharedProductionIds',
VALIDATION_GET_CLEANER_DATA: 'validation:getCleanerData',
// Materials
MATERIALS_UPSERT_BATCH: 'materials:upsertBatch',
MATERIALS_DELETE: 'materials:delete',
MATERIALS_GET_MANAGERS: 'materials:getManagers',
MATERIALS_GET_BY_MANAGER: 'materials:getByManager',
MATERIALS_GET_ALL: 'materials:getAll',
MATERIALS_GET_STATISTICS: 'materials:getStatistics',
MATERIALS_UPDATE_MANAGER: 'materials:updateManager',
// Settings
SETTINGS_GET_USER_TYPE: 'settings:getUserType',
SETTINGS_GET_SETTINGS: 'settings:getSettings',
SETTINGS_SAVE_SETTINGS: 'settings:saveSettings',
SETTINGS_RESET_DEFAULTS: 'settings:resetDefaults',
SETTINGS_TEST_DB_CONNECTION: 'settings:testDbConnection',
// Material type
MATERIAL_TYPE_GET_ALL: 'materialType:getAll',
MATERIAL_TYPE_GET_BY_MANAGER: 'materialType:getByManager',
MATERIAL_TYPE_GET_MANAGERS: 'materialType:getManagers',
MATERIAL_TYPE_UPSERT: 'materialType:upsert',
MATERIAL_TYPE_DELETE: 'materialType:delete',
MATERIAL_TYPE_UPSERT_BATCH: 'materialType:upsertBatch',
// User ERP config
USER_ERP_CONFIG_GET_CURRENT: 'user-erp-config:getCurrent',
USER_ERP_CONFIG_UPDATE: 'user-erp-config:update',
USER_ERP_CONFIG_TEST_CONNECTION: 'user-erp-config:testConnection',
USER_ERP_CONFIG_GET_ALL: 'user-erp-config:getAll'
} as const

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