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