feat: add TypeORM, logger, schemas, hooks and stores

- Add TypeORM integration with data-source, entities and repositories
- Add logger service for structured logging
- Add Zod validation schemas for auth, cleaner and extractor
- Add custom React hooks (useAuth, useCleaner, useExtractor, useValidation)
- Add Zustand stores (useAppStore, useUserStore)
- Add UI components (Button, Modal, Toast)
- Add error types and ErpBrowserManager
- Refactor IPC handlers and services
- Add unit tests for new modules

Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-03-02 23:10:15 +08:00
parent 982fb8fde6
commit a05c8a9037
57 changed files with 5150 additions and 974 deletions

View File

@@ -12,8 +12,11 @@
import { ipcMain } from 'electron'
import { SessionManager } from '../services/user/session-manager'
import { createLogger } from '../services/logger'
import type { UserInfo } from '../types/user.types'
const log = createLogger('AuthHandler')
/**
* Login request
*/
@@ -75,110 +78,115 @@ export function registerAuthHandlers(): void {
/**
* Silent login by computer name
*/
ipcMain.handle(
'auth:silentLogin',
async (): Promise<SilentLoginResponse> => {
try {
const success = await sessionManager.loginByComputerName()
const userInfo = sessionManager.getUserInfo()
ipcMain.handle('auth:silentLogin', async (): Promise<SilentLoginResponse> => {
try {
log.info('Attempting silent login')
const success = await sessionManager.loginByComputerName()
const userInfo = sessionManager.getUserInfo()
if (success && userInfo) {
// Check if admin needs user selection
const requiresUserSelection = userInfo.userType === 'Admin'
if (success && userInfo) {
// Check if admin needs user selection
const requiresUserSelection = userInfo.userType === 'Admin'
return {
success: true,
userInfo,
requiresUserSelection
}
}
log.info('Silent login successful', {
username: userInfo.username,
userType: userInfo.userType,
requiresUserSelection
})
return {
success: false,
requiresUserSelection: false
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return {
success: false,
error: `无感登录失败:${message}`
success: true,
userInfo,
requiresUserSelection
}
}
log.warn('Silent login failed - no matching user')
return {
success: false,
requiresUserSelection: false
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Silent login error', { error: message })
return {
success: false,
error: `无感登录失败:${message}`
}
}
)
})
/**
* Login with username and password
*/
ipcMain.handle(
'auth:login',
async (_event, request: LoginRequest): Promise<LoginResponse> => {
try {
const { username, password } = request
if (!username || !password) {
return {
success: false,
error: '请输入用户名和密码'
}
}
const success = await sessionManager.login(username, password)
const userInfo = sessionManager.getUserInfo()
if (success && userInfo) {
return {
success: true,
userInfo
}
}
ipcMain.handle('auth:login', async (_event, request: LoginRequest): Promise<LoginResponse> => {
try {
const { username, password } = request
if (!username || !password) {
log.warn('Login attempt with missing credentials')
return {
success: false,
error: '用户名密码错误'
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return {
success: false,
error: `登录失败:${message}`
error: '请输入用户名密码'
}
}
log.info('Login attempt', { username })
const success = await sessionManager.login(username, password)
const userInfo = sessionManager.getUserInfo()
if (success && userInfo) {
log.info('Login successful', { username, userType: userInfo.userType })
return {
success: true,
userInfo
}
}
log.warn('Login failed - invalid credentials', { username })
return {
success: false,
error: '用户名或密码错误'
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Login error', { error: message })
return {
success: false,
error: `登录失败:${message}`
}
}
)
})
/**
* Logout
*/
ipcMain.handle('auth:logout', async (): Promise<void> => {
const userInfo = sessionManager.getUserInfo()
log.info('User logout', { username: userInfo?.username })
sessionManager.logout()
})
/**
* Get current user
*/
ipcMain.handle(
'auth:getCurrentUser',
async (): Promise<CurrentUserResponse> => {
const isAuthenticated = sessionManager.isAuthenticated()
const userInfo = sessionManager.getUserInfo()
ipcMain.handle('auth:getCurrentUser', async (): Promise<CurrentUserResponse> => {
const isAuthenticated = sessionManager.isAuthenticated()
const userInfo = sessionManager.getUserInfo()
return {
isAuthenticated,
userInfo: userInfo ?? undefined
}
return {
isAuthenticated,
userInfo: userInfo ?? undefined
}
)
})
/**
* Get all users (for admin user selection)
*/
ipcMain.handle(
'auth:getAllUsers',
async (): Promise<UserInfo[]> => {
return await sessionManager.getAllUsers()
}
)
ipcMain.handle('auth:getAllUsers', async (): Promise<UserInfo[]> => {
log.debug('Fetching all users for admin selection')
return await sessionManager.getAllUsers()
})
/**
* Switch user (admin only)
@@ -187,22 +195,26 @@ export function registerAuthHandlers(): void {
'auth:switchUser',
async (_event, userInfo: UserInfo): Promise<UserSelectionResponse> => {
try {
log.info('User switch attempt', { targetUser: userInfo.username })
const success = sessionManager.switchUser(userInfo)
if (success) {
const newUser = sessionManager.getUserInfo()
log.info('User switch successful', { newUsername: newUser?.username })
return {
success: true,
userInfo: newUser ?? undefined
}
}
log.warn('User switch failed')
return {
success: false,
error: '用户切换失败'
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('User switch error', { error: message })
return {
success: false,
error: `用户切换失败:${message}`
@@ -214,10 +226,7 @@ export function registerAuthHandlers(): void {
/**
* Check if current user is admin
*/
ipcMain.handle(
'auth:isAdmin',
async (): Promise<boolean> => {
return sessionManager.isAdmin()
}
)
ipcMain.handle('auth:isAdmin', async (): Promise<boolean> => {
return sessionManager.isAdmin()
})
}

View File

@@ -3,125 +3,153 @@ import { ErpAuthService } from '../services/erp/erp-auth'
import { CleanerService } from '../services/erp/cleaner'
import { OrderNumberResolver } from '../services/erp/order-resolver'
import { MySqlService } from '../services/database/mysql'
import { createLogger } from '../services/logger'
import { withErrorHandling, type IpcResult } from './index'
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
import type { CleanerInput, CleanerResult } from '../types/cleaner.types'
const log = createLogger('CleanerHandler')
/**
* Register IPC handlers for cleaner service
*/
export function registerCleanerHandlers(): void {
ipcMain.handle(
'cleaner:run',
async (
_event,
input: CleanerInput
): Promise<{ success: boolean; data?: CleanerResult; error?: string }> => {
let authService: ErpAuthService | null = null
let mysqlService: MySqlService | null = null
async (_event, input: CleanerInput): Promise<IpcResult<CleanerResult>> => {
return withErrorHandling(async () => {
let authService: ErpAuthService | null = null
let mysqlService: MySqlService | null = null
try {
// Check environment variables
const erpUrl = process.env.ERP_URL || ''
const erpUsername = process.env.ERP_USERNAME || ''
const erpPassword = process.env.ERP_PASSWORD || ''
try {
// Check environment variables
const erpUrl = process.env.ERP_URL || ''
const erpUsername = process.env.ERP_USERNAME || ''
const erpPassword = process.env.ERP_PASSWORD || ''
console.log('[Cleaner] Config:', {
url: erpUrl ? '***' : 'EMPTY',
username: erpUsername ? '***' : 'EMPTY'
})
log.info('Config check', {
url: erpUrl ? 'configured' : 'EMPTY',
username: erpUsername ? 'configured' : 'EMPTY'
})
if (!erpUrl || !erpUsername || !erpPassword) {
throw new Error(
'ERP 配置不完整。请检查 .env 文件中的 ERP_URL, ERP_USERNAME, ERP_PASSWORD'
)
}
if (!erpUrl || !erpUsername || !erpPassword) {
throw new ValidationError(
'ERP 配置不完整。请检查 .env 文件中的 ERP_URL, ERP_USERNAME, ERP_PASSWORD',
'VAL_MISSING_REQUIRED'
)
}
// Resolve order numbers (convert productionIDs to 生产订单号)
const mysqlConfig = {
host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
user: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || ''
}
// Resolve order numbers (convert productionIDs to 生产订单号)
const mysqlConfig = {
host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
user: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || ''
}
console.log('[Cleaner] Connecting to MySQL for order resolution...')
mysqlService = new MySqlService(mysqlConfig)
await mysqlService.connect()
log.info('Connecting to MySQL for order resolution...')
mysqlService = new MySqlService(mysqlConfig)
const resolver = new OrderNumberResolver(mysqlService)
const mappings = await resolver.resolve(input.orderNumbers)
// Get valid order numbers and warnings
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
const warnings = resolver.getWarnings(mappings)
if (warnings.length > 0) {
console.warn('[Cleaner] Resolution warnings:', warnings)
}
if (validOrderNumbers.length === 0) {
throw new Error('没有有效的生产订单号可处理。请检查输入的格式或数据库连接。')
}
console.log('[Cleaner] Resolved order numbers:', validOrderNumbers)
// Create auth service and login
authService = new ErpAuthService({
url: erpUrl,
username: erpUsername,
password: erpPassword,
headless: true
})
console.log('[Cleaner] Logging in to ERP...')
await authService.login()
console.log('[Cleaner] Login successful')
// Create cleaner service and run cleaning with resolved order numbers
const cleaner = new CleanerService(authService)
const modifiedInput: CleanerInput = {
...input,
orderNumbers: validOrderNumbers,
onProgress: input.onProgress
}
console.log('[Cleaner] Starting cleaning:', modifiedInput)
const result = await cleaner.clean(modifiedInput)
console.log('[Cleaner] Cleaning completed:', result)
// Add warnings to result errors if any
if (warnings.length > 0) {
result.errors = [...warnings, ...result.errors]
}
return { success: true, data: result }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
console.error('[Cleaner] Error:', message)
console.error('[Cleaner] Stack:', error instanceof Error ? error.stack : 'N/A')
return { success: false, error: `清理失败:${message}` }
} finally {
// Clean up: close browser
if (authService) {
try {
await authService.close()
console.log('[Cleaner] Browser closed')
} catch (closeError) {
console.warn('[Cleaner] Error closing browser:', closeError)
await mysqlService.connect()
} catch (error) {
throw new DatabaseQueryError(
'MySQL 连接失败',
'DB_CONNECTION_FAILED',
error instanceof Error ? error : undefined
)
}
const resolver = new OrderNumberResolver(mysqlService)
const mappings = await resolver.resolve(input.orderNumbers)
// Get valid order numbers and warnings
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
const warnings = resolver.getWarnings(mappings)
if (warnings.length > 0) {
log.warn('Resolution warnings', { warnings })
}
if (validOrderNumbers.length === 0) {
throw new ValidationError(
'没有有效的生产订单号可处理。请检查输入的格式或数据库连接。',
'VAL_INVALID_INPUT'
)
}
log.info('Resolved order numbers', { count: validOrderNumbers.length })
// Create auth service and login
authService = new ErpAuthService({
url: erpUrl,
username: erpUsername,
password: erpPassword,
headless: true
})
log.info('Logging in to ERP...')
try {
await authService.login()
} catch (error) {
throw new ErpConnectionError(
'ERP 登录失败',
'ERP_LOGIN_FAILED',
error instanceof Error ? error : undefined
)
}
log.info('Login successful')
// Create cleaner service and run cleaning with resolved order numbers
const cleaner = new CleanerService(authService)
const modifiedInput: CleanerInput = {
...input,
orderNumbers: validOrderNumbers,
onProgress: input.onProgress
}
log.info('Starting cleaning', { orderCount: validOrderNumbers.length })
const result = await cleaner.clean(modifiedInput)
// Add warnings to result errors if any
if (warnings.length > 0) {
result.errors = [...warnings, ...result.errors]
}
log.info('Cleaning completed', {
processedCount: result.ordersProcessed,
errorCount: result.errors.length
})
return result
} finally {
// Clean up: close browser
if (authService) {
try {
await authService.close()
log.debug('Browser closed')
} catch (closeError) {
log.warn('Error closing browser', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
// Clean up: disconnect MySQL
if (mysqlService) {
try {
await mysqlService.disconnect()
log.debug('MySQL disconnected')
} catch (closeError) {
log.warn('Error disconnecting MySQL', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
}
// Clean up: disconnect MySQL
if (mysqlService) {
try {
await mysqlService.disconnect()
console.log('[Cleaner] MySQL disconnected')
} catch (closeError) {
console.warn('[Cleaner] Error disconnecting MySQL:', closeError)
}
}
}
}, 'cleaner:run')
}
)
}

View File

@@ -1,6 +1,8 @@
import { ipcMain } from 'electron'
import { MySqlService } from '../services/database/mysql'
import { SqlServerService } from '../services/database/sql-server'
import { createLogger } from '../services/logger'
import { DatabaseQueryError, ValidationError } from '../types/errors'
import type {
MySqlConfig,
MySqlQueryResult,
@@ -8,6 +10,8 @@ import type {
SqlServerQueryResult
} from '../types/ipc-api.types'
const log = createLogger('DatabaseHandler')
// Store MySQL service instances per window/connection
const mysqlServices = new Map<string, MySqlService>()
@@ -64,34 +68,47 @@ export function registerDatabaseHandlers(): void {
ipcMain.handle('database:mysql:connect', async (event, config: MySqlConfig): Promise<void> => {
try {
// Use window ID as connection identifier
const windowId = (event.sender as any).id.toString()
const windowId = (event.sender as { id: number }).id.toString()
log.info('Connecting to MySQL', { windowId })
const service = new MySqlService(config)
await service.connect()
setMySqlService(windowId, service)
log.info('MySQL connected', { windowId })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to connect to MySQL'
throw new Error(message)
log.error('MySQL connection failed', { error: message })
throw new DatabaseQueryError(
message,
'DB_CONNECTION_FAILED',
error instanceof Error ? error : undefined
)
}
})
// Disconnect from MySQL
ipcMain.handle('database:mysql:disconnect', async (event): Promise<void> => {
try {
const windowId = (event.sender as any).id.toString()
const windowId = (event.sender as { id: number }).id.toString()
const service = getMySqlService(windowId)
if (service) {
await service.disconnect()
deleteMySqlService(windowId)
log.info('MySQL disconnected', { windowId })
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to disconnect from MySQL'
throw new Error(message)
log.error('MySQL disconnect failed', { error: message })
throw new DatabaseQueryError(
message,
'DB_CONNECTION_FAILED',
error instanceof Error ? error : undefined
)
}
})
// Check if MySQL is connected
ipcMain.handle('database:mysql:isConnected', async (event): Promise<boolean> => {
const windowId = (event.sender as any).id.toString()
const windowId = (event.sender as { id: number }).id.toString()
const service = getMySqlService(windowId)
return service ? service.isConnected() : false
})
@@ -99,19 +116,28 @@ export function registerDatabaseHandlers(): void {
// Execute MySQL query
ipcMain.handle(
'database:mysql:query',
async (event, sql: string, params?: any[]): Promise<MySqlQueryResult> => {
async (event, sql: string, params?: unknown[]): Promise<MySqlQueryResult> => {
try {
const windowId = (event.sender as any).id.toString()
const windowId = (event.sender as { id: number }).id.toString()
const service = getMySqlService(windowId)
if (!service) {
throw new Error('Not connected to MySQL. Call connect() first.')
throw new ValidationError(
'Not connected to MySQL. Call connect() first.',
'VAL_INVALID_INPUT'
)
}
log.debug('Executing MySQL query', { windowId, sql: sql.substring(0, 100) })
return await service.query(sql, params)
} catch (error) {
const message = error instanceof Error ? error.message : 'MySQL query failed'
throw new Error(message)
log.error('MySQL query failed', { error: message })
throw new DatabaseQueryError(
message,
'DB_QUERY_FAILED',
error instanceof Error ? error : undefined
)
}
}
)
@@ -121,13 +147,20 @@ export function registerDatabaseHandlers(): void {
'database:sqlserver:connect',
async (event, config: SqlServerConfig): Promise<void> => {
try {
const windowId = (event.sender as any).id.toString()
const windowId = (event.sender as { id: number }).id.toString()
log.info('Connecting to SQL Server', { windowId })
const service = new SqlServerService(config)
await service.connect()
setSqlServerService(windowId, service)
log.info('SQL Server connected', { windowId })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to connect to SQL Server'
throw new Error(message)
log.error('SQL Server connection failed', { error: message })
throw new DatabaseQueryError(
message,
'DB_CONNECTION_FAILED',
error instanceof Error ? error : undefined
)
}
}
)
@@ -135,22 +168,28 @@ export function registerDatabaseHandlers(): void {
// Disconnect from SQL Server
ipcMain.handle('database:sqlserver:disconnect', async (event): Promise<void> => {
try {
const windowId = (event.sender as any).id.toString()
const windowId = (event.sender as { id: number }).id.toString()
const service = getSqlServerService(windowId)
if (service) {
await service.disconnect()
deleteSqlServerService(windowId)
log.info('SQL Server disconnected', { windowId })
}
} catch (error) {
const message =
error instanceof Error ? error.message : 'Failed to disconnect from SQL Server'
throw new Error(message)
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
ipcMain.handle('database:sqlserver:isConnected', async (event): Promise<boolean> => {
const windowId = (event.sender as any).id.toString()
const windowId = (event.sender as { id: number }).id.toString()
const service = getSqlServerService(windowId)
return service ? service.isConnected() : false
})
@@ -164,17 +203,26 @@ export function registerDatabaseHandlers(): void {
params?: Record<string, unknown>
): Promise<SqlServerQueryResult> => {
try {
const windowId = (event.sender as any).id.toString()
const windowId = (event.sender as { id: number }).id.toString()
const service = getSqlServerService(windowId)
if (!service) {
throw new Error('Not connected to SQL Server. Call connect() first.')
throw new ValidationError(
'Not connected to SQL Server. Call connect() first.',
'VAL_INVALID_INPUT'
)
}
log.debug('Executing SQL Server query', { windowId, sql: sqlString.substring(0, 100) })
return await service.query(sqlString, params)
} catch (error) {
const message = error instanceof Error ? error.message : 'SQL Server query failed'
throw new Error(message)
log.error('SQL Server query failed', { error: message })
throw new DatabaseQueryError(
message,
'DB_QUERY_FAILED',
error instanceof Error ? error : undefined
)
}
}
)

View File

@@ -3,125 +3,152 @@ import { ErpAuthService } from '../services/erp/erp-auth'
import { ExtractorService } from '../services/erp/extractor'
import { OrderNumberResolver } from '../services/erp/order-resolver'
import { MySqlService } from '../services/database/mysql'
import { createLogger } from '../services/logger'
import { withErrorHandling, type IpcResult } from './index'
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
import type { ExtractorInput, ExtractorResult } from '../types/extractor.types'
const log = createLogger('ExtractorHandler')
/**
* Register IPC handlers for extractor service
*/
export function registerExtractorHandlers(): void {
ipcMain.handle(
'extractor:run',
async (
_event,
input: ExtractorInput
): Promise<{ success: boolean; data?: ExtractorResult; error?: string }> => {
let authService: ErpAuthService | null = null
let mysqlService: MySqlService | null = null
async (_event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => {
return withErrorHandling(async () => {
let authService: ErpAuthService | null = null
let mysqlService: MySqlService | null = null
try {
// Check environment variables
const erpUrl = process.env.ERP_URL || ''
const erpUsername = process.env.ERP_USERNAME || ''
const erpPassword = process.env.ERP_PASSWORD || ''
try {
// Check environment variables
const erpUrl = process.env.ERP_URL || ''
const erpUsername = process.env.ERP_USERNAME || ''
const erpPassword = process.env.ERP_PASSWORD || ''
console.log('[Extractor] Config:', {
url: erpUrl ? '***' : 'EMPTY',
username: erpUsername ? '***' : 'EMPTY'
})
log.info('Config check', {
url: erpUrl ? 'configured' : 'EMPTY',
username: erpUsername ? 'configured' : 'EMPTY'
})
if (!erpUrl || !erpUsername || !erpPassword) {
throw new Error(
'ERP 配置不完整。请检查 .env 文件中的 ERP_URL, ERP_USERNAME, ERP_PASSWORD'
)
}
if (!erpUrl || !erpUsername || !erpPassword) {
throw new ValidationError(
'ERP 配置不完整。请检查 .env 文件中的 ERP_URL, ERP_USERNAME, ERP_PASSWORD',
'VAL_MISSING_REQUIRED'
)
}
// Resolve order numbers (convert productionIDs to 生产订单号)
const mysqlConfig = {
host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
user: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || ''
}
// Resolve order numbers (convert productionIDs to 生产订单号)
const mysqlConfig = {
host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
user: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || ''
}
console.log('[Extractor] Connecting to MySQL for order resolution...')
mysqlService = new MySqlService(mysqlConfig)
await mysqlService.connect()
log.info('Connecting to MySQL for order resolution...')
mysqlService = new MySqlService(mysqlConfig)
const resolver = new OrderNumberResolver(mysqlService)
const mappings = await resolver.resolve(input.orderNumbers)
// Get valid order numbers and warnings
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
const warnings = resolver.getWarnings(mappings)
if (warnings.length > 0) {
console.warn('[Extractor] Resolution warnings:', warnings)
}
if (validOrderNumbers.length === 0) {
throw new Error('没有有效的生产订单号可处理。请检查输入的格式或数据库连接。')
}
console.log('[Extractor] Resolved order numbers:', validOrderNumbers)
// Create auth service and login
authService = new ErpAuthService({
url: erpUrl,
username: erpUsername,
password: erpPassword,
headless: true
})
console.log('[Extractor] Logging in to ERP...')
await authService.login()
console.log('[Extractor] Login successful')
// Create extractor service and run extraction with resolved order numbers
const extractor = new ExtractorService(authService)
console.log('[Extractor] Starting extraction for orders:', validOrderNumbers)
const modifiedInput: ExtractorInput = {
...input,
orderNumbers: validOrderNumbers
}
const result = await extractor.extract(modifiedInput)
// Add warnings to result errors if any
if (warnings.length > 0) {
result.errors = [...warnings, ...result.errors]
}
console.log('[Extractor] Extraction completed:', result)
return { success: true, data: result }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
console.error('[Extractor] Error:', message)
console.error('[Extractor] Stack:', error instanceof Error ? error.stack : 'N/A')
return { success: false, error: `提取失败:${message}` }
} finally {
// Clean up: close browser
if (authService) {
try {
await authService.close()
console.log('[Extractor] Browser closed')
} catch (closeError) {
console.warn('[Extractor] Error closing browser:', closeError)
await mysqlService.connect()
} catch (error) {
throw new DatabaseQueryError(
'MySQL 连接失败',
'DB_CONNECTION_FAILED',
error instanceof Error ? error : undefined
)
}
const resolver = new OrderNumberResolver(mysqlService)
const mappings = await resolver.resolve(input.orderNumbers)
// Get valid order numbers and warnings
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
const warnings = resolver.getWarnings(mappings)
if (warnings.length > 0) {
log.warn('Resolution warnings', { warnings })
}
if (validOrderNumbers.length === 0) {
throw new ValidationError(
'没有有效的生产订单号可处理。请检查输入的格式或数据库连接。',
'VAL_INVALID_INPUT'
)
}
log.info('Resolved order numbers', { count: validOrderNumbers.length })
// Create auth service and login
authService = new ErpAuthService({
url: erpUrl,
username: erpUsername,
password: erpPassword,
headless: true
})
log.info('Logging in to ERP...')
try {
await authService.login()
} catch (error) {
throw new ErpConnectionError(
'ERP 登录失败',
'ERP_LOGIN_FAILED',
error instanceof Error ? error : undefined
)
}
log.info('Login successful')
// Create extractor service and run extraction with resolved order numbers
const extractor = new ExtractorService(authService)
log.info('Starting extraction', { orderCount: validOrderNumbers.length })
const modifiedInput: ExtractorInput = {
...input,
orderNumbers: validOrderNumbers
}
const result = await extractor.extract(modifiedInput)
// Add warnings to result errors if any
if (warnings.length > 0) {
result.errors = [...warnings, ...result.errors]
}
log.info('Extraction completed', {
rowCount: result.recordCount,
errorCount: result.errors.length
})
return result
} finally {
// Clean up: close browser
if (authService) {
try {
await authService.close()
log.debug('Browser closed')
} catch (closeError) {
log.warn('Error closing browser', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
// Clean up: disconnect MySQL
if (mysqlService) {
try {
await mysqlService.disconnect()
log.debug('MySQL disconnected')
} catch (closeError) {
log.warn('Error disconnecting MySQL', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
}
// Clean up: disconnect MySQL
if (mysqlService) {
try {
await mysqlService.disconnect()
console.log('[Extractor] MySQL disconnected')
} catch (closeError) {
console.warn('[Extractor] Error disconnecting MySQL:', closeError)
}
}
}
}, 'extractor:run')
}
)
}

View File

@@ -1,6 +1,9 @@
import { ipcMain } from 'electron'
import * as fs from 'fs/promises'
import * as path from 'path'
import { createLogger } from '../services/logger'
const log = createLogger('FileHandler')
/**
* Register IPC handlers for file operations
@@ -9,9 +12,11 @@ export function registerFileHandlers(): void {
// Read file content
ipcMain.handle('file:read', async (_event, filePath: string): Promise<string> => {
try {
log.debug('Reading file', { filePath })
return await fs.readFile(filePath, 'utf-8')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to read file'
log.error('Failed to read file', { filePath, error: message })
throw new Error(message)
}
})
@@ -19,12 +24,14 @@ export function registerFileHandlers(): void {
// Write content to file
ipcMain.handle('file:write', async (_event, filePath: string, content: string): Promise<void> => {
try {
log.debug('Writing file', { filePath })
// Ensure directory exists
const dir = path.dirname(filePath)
await fs.mkdir(dir, { recursive: true })
await fs.writeFile(filePath, content, 'utf-8')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to write file'
log.error('Failed to write file', { filePath, error: message })
throw new Error(message)
}
})
@@ -42,6 +49,7 @@ export function registerFileHandlers(): void {
// List files in directory
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
.filter((entry) => entry.isFile())
@@ -49,6 +57,7 @@ export function registerFileHandlers(): void {
.sort()
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to list directory'
log.error('Failed to list directory', { dirPath, error: message })
throw new Error(message)
}
})

View File

@@ -11,11 +11,60 @@ import { registerResolverHandlers } from './resolver-handler'
import { registerAuthHandlers } from './auth-handler'
import { registerValidationHandlers } from './validation-handler'
import { registerSettingsHandlers } from './settings-handler'
import { createLogger } from '../services/logger'
import { getErrorMessage, getErrorCode, isBaseError } from '../types/errors'
const log = createLogger('IPC')
/**
* Standard result type for all IPC handlers
*/
export interface IpcResult<T = unknown> {
success: boolean
data?: T
error?: string
code?: string
}
/**
* Higher-order function to wrap IPC handlers with consistent error handling
* @param handler - The async handler function to wrap
* @param context - The context name for logging
* @returns A wrapped handler that returns IpcResult
*/
export function withErrorHandling<T>(
handler: () => Promise<T>,
context: string
): Promise<IpcResult<T>> {
return handler()
.then((data) => {
log.debug(`[${context}] Handler completed successfully`)
return { success: true, data }
})
.catch((error: unknown) => {
const message = getErrorMessage(error)
const code = getErrorCode(error)
if (isBaseError(error)) {
log.error(`[${context}] ${error.name}: ${message}`, { code, cause: error.cause?.message })
} else {
log.error(`[${context}] Error: ${message}`, { code })
}
// Include stack trace in development
if (process.env.NODE_ENV !== 'production' && error instanceof Error) {
log.debug(`[${context}] Stack trace:`, { stack: error.stack })
}
return { success: false, error: message, code }
})
}
/**
* Register all IPC handlers
*/
export function registerIpcHandlers(): void {
log.info('Registering IPC handlers...')
registerFileHandlers()
registerExtractorHandlers()
registerCleanerHandlers()
@@ -24,4 +73,5 @@ export function registerIpcHandlers(): void {
registerAuthHandlers()
registerValidationHandlers()
registerSettingsHandlers()
log.info('All IPC handlers registered')
}

View File

@@ -9,8 +9,12 @@
import { ipcMain } from 'electron'
import { MySqlService } from '../services/database/mysql'
import { OrderNumberResolver } from '../services/erp/order-resolver'
import { createLogger } from '../services/logger'
import { DatabaseQueryError } from '../types/errors'
import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver'
const log = createLogger('ResolverHandler')
/**
* Resolver input from renderer
*/
@@ -69,6 +73,7 @@ export function registerResolverHandlers(): void {
}
// Create MySQL service
log.info('Connecting to MySQL for resolution', { inputCount: input.inputs.length })
mysqlService = new MySqlService(mysqlConfig)
await mysqlService.connect()
@@ -81,6 +86,12 @@ export function registerResolverHandlers(): void {
const warnings = resolver.getWarnings(mappings)
const stats = resolver.getStats(mappings)
log.info('Resolution completed', {
inputCount: input.inputs.length,
validCount: validOrderNumbers.length,
warningCount: warnings.length
})
return {
success: true,
mappings,
@@ -90,6 +101,7 @@ export function registerResolverHandlers(): void {
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Resolution failed', { error: message })
return {
success: false,
error: `解析失败:${message}`
@@ -99,8 +111,11 @@ export function registerResolverHandlers(): void {
if (mysqlService) {
try {
await mysqlService.disconnect()
log.debug('MySQL disconnected')
} catch (closeError) {
console.warn('[Resolver] Error disconnecting MySQL:', closeError)
log.warn('Error disconnecting MySQL', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
}
@@ -112,7 +127,10 @@ export function registerResolverHandlers(): void {
*/
ipcMain.handle(
'resolver:validateFormat',
async (_event, inputs: string[]): Promise<{
async (
_event,
inputs: string[]
): Promise<{
success: boolean
results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>
error?: string
@@ -122,14 +140,17 @@ export function registerResolverHandlers(): void {
isConnected: () => false
} as MySqlService)
const results = inputs.map(input => ({
const results = inputs.map((input) => ({
input,
type: resolver.recognizeType(input)
}))
log.debug('Format validation completed', { inputCount: inputs.length })
return { success: true, results }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Format validation failed', { error: message })
return {
success: false,
error: `验证失败:${message}`

View File

@@ -14,6 +14,7 @@ import { SessionManager } from '../services/user/session-manager'
import { ErpAuthService } from '../services/erp/erp-auth'
import { MySqlService } from '../services/database/mysql'
import { SqlServerService } from '../services/database/sql-server'
import { createLogger } from '../services/logger'
import type {
SettingsData,
UserType,
@@ -21,14 +22,13 @@ import type {
SaveSettingsResult
} from '../types/settings.types'
const log = createLogger('SettingsHandler')
/**
* Filter settings by user type
* Admin users get all settings, User users get limited settings
*/
function filterSettingsByUserType(
settings: SettingsData,
userType: UserType
): SettingsData {
function filterSettingsByUserType(settings: SettingsData, userType: UserType): SettingsData {
if (userType === 'Admin') {
return settings // Return all settings for Admin
}
@@ -72,6 +72,7 @@ export function registerSettingsHandlers(): void {
*/
ipcMain.handle('settings:getSettings', async (): Promise<SettingsData> => {
const userType = (sessionManager.getUserType() as UserType) || 'Guest'
log.debug('Getting settings', { userType })
const settings = configManager.getAllSettings()
return filterSettingsByUserType(settings, userType)
})
@@ -83,14 +84,18 @@ export function registerSettingsHandlers(): void {
'settings:saveSettings',
async (_event, settings: SettingsData): Promise<SaveSettingsResult> => {
try {
log.info('Saving settings')
const success = await configManager.saveAllSettings(settings)
if (success) {
log.info('Settings saved successfully')
return { success: true }
} else {
log.warn('Failed to save settings')
return { success: false, error: '保存设置失败' }
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Error saving settings', { error: message })
return { success: false, error: `保存设置失败:${message}` }
}
}
@@ -99,157 +104,166 @@ export function registerSettingsHandlers(): void {
/**
* Reset to default settings (Admin only)
*/
ipcMain.handle(
'settings:resetDefaults',
async (): Promise<SaveSettingsResult> => {
try {
const userType = sessionManager.getUserType()
if (userType !== 'Admin') {
return { success: false, error: '只有管理员可以恢复默认设置' }
}
configManager.resetToDefaults()
const success = await configManager.save()
if (success) {
return { success: true }
} else {
return { success: false, error: '恢复默认设置失败' }
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return { success: false, error: `恢复默认设置失败:${message}` }
ipcMain.handle('settings:resetDefaults', async (): Promise<SaveSettingsResult> => {
try {
const userType = sessionManager.getUserType()
if (userType !== 'Admin') {
log.warn('Non-admin user attempted to reset defaults', { userType })
return { success: false, error: '只有管理员可以恢复默认设置' }
}
log.info('Resetting settings to defaults')
configManager.resetToDefaults()
const success = await configManager.save()
if (success) {
log.info('Settings reset to defaults successfully')
return { success: true }
} else {
log.warn('Failed to reset settings to defaults')
return { success: false, error: '恢复默认设置失败' }
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Error resetting settings', { error: message })
return { success: false, error: `恢复默认设置失败:${message}` }
}
)
})
/**
* Test ERP connection
*/
ipcMain.handle(
'settings:testErpConnection',
async (): Promise<ConnectionTestResult> => {
try {
const settings = configManager.getAllSettings()
const erpConfig = settings.erp
ipcMain.handle('settings:testErpConnection', async (): Promise<ConnectionTestResult> => {
try {
log.info('Testing ERP connection')
const settings = configManager.getAllSettings()
const erpConfig = settings.erp
if (!erpConfig.url || !erpConfig.username || !erpConfig.password) {
return {
success: false,
message: '请先配置 ERP URL、用户名和密码'
}
}
// Create ERP auth service and try to login
const erpAuthService = new ErpAuthService(erpConfig)
try {
await erpAuthService.login()
// Login successful, close browser
await erpAuthService.close()
return {
success: true,
message: 'ERP 连接测试成功!'
}
} catch (loginError) {
const errorMessage = loginError instanceof Error ? loginError.message : '登录失败'
return {
success: false,
message: `ERP 连接测试失败:${errorMessage}`
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
if (!erpConfig.url || !erpConfig.username || !erpConfig.password) {
log.warn('ERP connection test failed - missing configuration')
return {
success: false,
message: `ERP 连接测试失败:${message}`
message: '请先配置 ERP URL、用户名和密码'
}
}
// Create ERP auth service and try to login
const erpAuthService = new ErpAuthService(erpConfig)
try {
await erpAuthService.login()
// Login successful, close browser
await erpAuthService.close()
log.info('ERP connection test successful')
return {
success: true,
message: 'ERP 连接测试成功!'
}
} catch (loginError) {
const errorMessage = loginError instanceof Error ? loginError.message : '登录失败'
log.error('ERP login failed', { error: errorMessage })
return {
success: false,
message: `ERP 连接测试失败:${errorMessage}`
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('ERP connection test error', { error: message })
return {
success: false,
message: `ERP 连接测试失败:${message}`
}
}
)
})
/**
* Test database connection
*/
ipcMain.handle(
'settings:testDbConnection',
async (): Promise<ConnectionTestResult> => {
try {
const settings = configManager.getAllSettings()
const dbConfig = settings.database
ipcMain.handle('settings:testDbConnection', async (): Promise<ConnectionTestResult> => {
try {
log.info('Testing database connection')
const settings = configManager.getAllSettings()
const dbConfig = settings.database
if (dbConfig.dbType === 'mysql') {
// Test MySQL connection
if (!dbConfig.mysqlHost || !dbConfig.database || !dbConfig.username) {
return {
success: false,
message: '请先配置 MySQL 主机、数据库名和用户名'
}
}
const mysqlService = new MySqlService({
host: dbConfig.mysqlHost,
port: dbConfig.mysqlPort,
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database
})
try {
await mysqlService.connect()
await mysqlService.disconnect()
return {
success: true,
message: 'MySQL 数据库连接测试成功!'
}
} catch (connError) {
const errorMessage = connError instanceof Error ? connError.message : '连接失败'
return {
success: false,
message: `MySQL 数据库连接测试失败:${errorMessage}`
}
}
} else {
// Test SQL Server connection
if (!dbConfig.server || !dbConfig.database || !dbConfig.username) {
return {
success: false,
message: '请先配置 SQL Server 服务器、数据库名和用户名'
}
}
const sqlServerService = new SqlServerService({
server: dbConfig.server,
port: 1433, // Default SQL Server port
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
options: {
trustServerCertificate: true
}
})
try {
await sqlServerService.connect()
await sqlServerService.disconnect()
return {
success: true,
message: 'SQL Server 数据库连接测试成功!'
}
} catch (connError) {
const errorMessage = connError instanceof Error ? connError.message : '连接失败'
return {
success: false,
message: `SQL Server 数据库连接测试失败:${errorMessage}`
}
if (dbConfig.dbType === 'mysql') {
// Test MySQL connection
if (!dbConfig.mysqlHost || !dbConfig.database || !dbConfig.username) {
log.warn('MySQL connection test failed - missing configuration')
return {
success: false,
message: '请先配置 MySQL 主机、数据库名和用户名'
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return {
success: false,
message: `数据库连接测试失败:${message}`
const mysqlService = new MySqlService({
host: dbConfig.mysqlHost,
port: dbConfig.mysqlPort,
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
if (!dbConfig.server || !dbConfig.database || !dbConfig.username) {
log.warn('SQL Server connection test failed - missing configuration')
return {
success: false,
message: '请先配置 SQL Server 服务器、数据库名和用户名'
}
}
const sqlServerService = new SqlServerService({
server: dbConfig.server,
port: 1433, // Default SQL Server port
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
options: {
trustServerCertificate: true
}
})
try {
await sqlServerService.connect()
await sqlServerService.disconnect()
log.info('SQL Server connection test successful')
return {
success: true,
message: 'SQL Server 数据库连接测试成功!'
}
} catch (connError) {
const errorMessage = connError instanceof Error ? connError.message : '连接失败'
log.error('SQL Server connection failed', { error: errorMessage })
return {
success: false,
message: `SQL Server 数据库连接测试失败:${errorMessage}`
}
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Database connection test error', { error: message })
return {
success: false,
message: `数据库连接测试失败:${message}`
}
}
)
})
}

View File

@@ -12,6 +12,7 @@ import { MySqlService } from '../services/database/mysql'
import { SqlServerService } from '../services/database/sql-server'
import { MaterialsToBeDeletedDAO } from '../services/database/materials-to-be-deleted-dao'
import { DiscreteMaterialPlanDAO } from '../services/database/discrete-material-plan-dao'
import { createLogger } from '../services/logger'
import type {
ValidationRequest,
ValidationResponse,
@@ -22,6 +23,8 @@ import type {
MaterialRecordSummary
} from '../types/validation.types'
const log = createLogger('ValidationHandler')
/**
* Shared state for Production IDs from extractor page
* This is a simple in-memory store for sharing Production IDs between pages
@@ -32,7 +35,7 @@ const sharedProductionIds = new Set<string>()
* Set shared Production IDs
*/
export function setSharedProductionIds(ids: string[]): void {
ids.forEach(id => sharedProductionIds.add(id))
ids.forEach((id) => sharedProductionIds.add(id))
}
/**
@@ -110,11 +113,11 @@ function getTableName(mysqlTableName: string): string {
*/
function readProductionIds(filePath: string): string[] {
const fs = require('fs')
const content = fs.readFileSync(filePath, 'utf-8')
const content = fs.readFileSync(filePath, 'utf-8') as string
return content
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.map((line: string) => line.trim())
.filter((line: string) => line.length > 0)
}
/**
@@ -173,10 +176,11 @@ async function getSourceNumbersFromInputs(
FROM ${contractTableName}
WHERE 总排号 IN (${placeholders})
`
const contractResult = await (dbService as SqlServerService).queryWithParams(contractSql, params)
const dbOrderNumbers = contractResult.rows.map(
row => row. as string
const contractResult = await (dbService as SqlServerService).queryWithParams(
contractSql,
params
)
const dbOrderNumbers = contractResult.rows.map((row) => row. as string)
orderNumbers.push(...dbOrderNumbers)
} else {
const placeholders = productionIds.map(() => '?').join(',')
@@ -186,9 +190,7 @@ async function getSourceNumbersFromInputs(
WHERE 总排号 IN (${placeholders})
`
const contractResult = await (dbService as MySqlService).query(contractSql, productionIds)
const dbOrderNumbers = contractResult.rows.map(
row => row. as string
)
const dbOrderNumbers = contractResult.rows.map((row) => row. as string)
orderNumbers.push(...dbOrderNumbers)
}
}
@@ -208,14 +210,11 @@ export function registerValidationHandlers(): void {
*/
ipcMain.handle(
'validation:validate',
async (
_event,
request: ValidationRequest
): Promise<ValidationResponse> => {
async (_event, request: ValidationRequest): Promise<ValidationResponse> => {
let dbService: MySqlService | SqlServerService | null = null
try {
console.log('[Validation] Starting validation:', request)
log.info('Starting validation', { mode: request.mode })
// Connect to database
dbService = await getValidationDatabaseService()
@@ -229,7 +228,7 @@ export function registerValidationHandlers(): void {
if (request.useSharedProductionIds) {
// Use shared Production IDs from extractor page
const sharedIds = getSharedProductionIds()
console.log(`[Validation] Using ${sharedIds.length} shared Production IDs`)
log.info(`Using ${sharedIds.length} shared Production IDs`)
if (sharedIds.length === 0) {
return {
@@ -244,19 +243,13 @@ export function registerValidationHandlers(): void {
}
sourceNumbers = await getSourceNumbersFromInputs(sharedIds, dbService)
console.log(
`[Validation] Got ${sourceNumbers.length} source numbers from shared Production IDs`
)
log.info(`Got ${sourceNumbers.length} source numbers from shared Production IDs`)
} else if (request.productionIdFile) {
// Read from file
const inputs = readProductionIds(request.productionIdFile)
console.log(
`[Validation] Read ${inputs.length} inputs from file`
)
log.info(`Read ${inputs.length} inputs from file`)
sourceNumbers = await getSourceNumbersFromInputs(inputs, dbService)
console.log(
`[Validation] Got ${sourceNumbers.length} source numbers`
)
log.info(`Got ${sourceNumbers.length} source numbers`)
}
}
@@ -270,9 +263,7 @@ export function registerValidationHandlers(): void {
materialRecords = await materialDao.queryAllDistinctByMaterialCode()
} else if (sourceNumbers && sourceNumbers.length > 0) {
// Filtered query by source numbers
materialRecords = await materialDao.queryBySourceNumbersDistinct(
sourceNumbers
)
materialRecords = await materialDao.queryBySourceNumbersDistinct(sourceNumbers)
}
if (materialRecords.length === 0) {
@@ -298,7 +289,7 @@ export function registerValidationHandlers(): void {
? await (dbService as SqlServerService).query(typeKeywordSql)
: await (dbService as MySqlService).query(typeKeywordSql)
const typeKeywords = typeKeywordResult.rows.map(row => ({
const typeKeywords = typeKeywordResult.rows.map((row) => ({
materialName: row.MaterialName as string,
managerName: row.ManagerName as string
}))
@@ -316,10 +307,7 @@ export function registerValidationHandlers(): void {
const markedCodesDict = new Map<string, string>()
for (const row of markedResult.rows) {
markedCodesDict.set(
row.MaterialCode as string,
row.ManagerName as string
)
markedCodesDict.set(row.MaterialCode as string, row.ManagerName as string)
}
// Match materials
@@ -338,10 +326,7 @@ export function registerValidationHandlers(): void {
// Priority 2: Match with MaterialsTypeToBeDeleted (MaterialName contains)
if (!managerName) {
for (const typeKeyword of typeKeywords) {
if (
typeKeyword.materialName &&
materialName.includes(typeKeyword.materialName)
) {
if (typeKeyword.materialName && materialName.includes(typeKeyword.materialName)) {
matchedTypeKeyword = typeKeyword.materialName
managerName = typeKeyword.managerName
break
@@ -360,8 +345,8 @@ export function registerValidationHandlers(): void {
})
}
const markedCount = results.filter(r => r.isMarkedForDeletion).length
const matchedCount = results.filter(r => r.managerName).length
const markedCount = results.filter((r) => r.isMarkedForDeletion).length
const matchedCount = results.filter((r) => r.managerName).length
return {
success: true,
@@ -374,7 +359,9 @@ export function registerValidationHandlers(): void {
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
console.error('[Validation] Validation error:', error)
log.error('Validation error', {
error: error instanceof Error ? error.message : String(error)
})
return {
success: false,
error: `Validation failed: ${message}`
@@ -384,7 +371,9 @@ export function registerValidationHandlers(): void {
try {
await dbService.disconnect()
} catch (closeError) {
console.warn('[Validation] Error disconnecting database:', closeError)
log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
}
@@ -398,10 +387,7 @@ export function registerValidationHandlers(): void {
*/
ipcMain.handle(
'materials:upsertBatch',
async (
_event,
request: MaterialUpsertBatchRequest
): Promise<MaterialOperationResponse> => {
async (_event, request: MaterialUpsertBatchRequest): Promise<MaterialOperationResponse> => {
try {
const dao = new MaterialsToBeDeletedDAO()
const stats = await dao.upsertBatch(request.materials)
@@ -412,7 +398,9 @@ export function registerValidationHandlers(): void {
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
console.error('[Materials] Upsert batch error:', error)
log.error('Upsert batch error', {
error: error instanceof Error ? error.message : String(error)
})
return {
success: false,
error: `Upsert failed: ${message}`
@@ -426,10 +414,7 @@ export function registerValidationHandlers(): void {
*/
ipcMain.handle(
'materials:delete',
async (
_event,
request: MaterialDeleteRequest
): Promise<MaterialOperationResponse> => {
async (_event, request: MaterialDeleteRequest): Promise<MaterialOperationResponse> => {
try {
const dao = new MaterialsToBeDeletedDAO()
const count = await dao.deleteByMaterialCodes(request.materialCodes)
@@ -440,7 +425,7 @@ export function registerValidationHandlers(): void {
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
console.error('[Materials] Delete error:', error)
log.error('Delete error', { error: error instanceof Error ? error.message : String(error) })
return {
success: false,
error: `Delete failed: ${message}`
@@ -452,29 +437,25 @@ export function registerValidationHandlers(): void {
/**
* Get unique manager names
*/
ipcMain.handle(
'materials:getManagers',
async (_event): Promise<{ managers: string[] }> => {
try {
const dao = new MaterialsToBeDeletedDAO()
const managers = await dao.getManagers()
return { managers }
} catch (error) {
console.error('[Materials] Get managers error:', error)
return { managers: [] }
}
ipcMain.handle('materials:getManagers', async (_event): Promise<{ managers: string[] }> => {
try {
const dao = new MaterialsToBeDeletedDAO()
const managers = await dao.getManagers()
return { managers }
} catch (error) {
log.error('Get managers error', {
error: error instanceof Error ? error.message : String(error)
})
return { managers: [] }
}
)
})
/**
* Get materials by manager
*/
ipcMain.handle(
'materials:getByManager',
async (
_event,
managerName: string
): Promise<{ materials: MaterialRecordSummary[] }> => {
async (_event, managerName: string): Promise<{ materials: MaterialRecordSummary[] }> => {
let dbService: MySqlService | SqlServerService | null = null
try {
@@ -512,25 +493,16 @@ export function registerValidationHandlers(): void {
WHERE MaterialCode = ?
LIMIT 1
`
detailResult = await (dbService as MySqlService).query(detailSql, [
mat.materialCode
])
detailResult = await (dbService as MySqlService).query(detailSql, [mat.materialCode])
}
enrichedMaterials.push({
materialCode: mat.materialCode,
materialName:
detailResult.rows.length > 0
? (detailResult.rows[0].MaterialName as string)
: '',
detailResult.rows.length > 0 ? (detailResult.rows[0].MaterialName as string) : '',
specification:
detailResult.rows.length > 0
? (detailResult.rows[0].Specification as string)
: '',
model:
detailResult.rows.length > 0
? (detailResult.rows[0].Model as string)
: '',
detailResult.rows.length > 0 ? (detailResult.rows[0].Specification as string) : '',
model: detailResult.rows.length > 0 ? (detailResult.rows[0].Model as string) : '',
managerName: mat.managerName,
isMarked: markedCodes.has(mat.materialCode)
})
@@ -538,14 +510,18 @@ export function registerValidationHandlers(): void {
return { materials: enrichedMaterials }
} catch (error) {
console.error('[Materials] Get by manager error:', error)
log.error('Get by manager error', {
error: error instanceof Error ? error.message : String(error)
})
return { materials: [] }
} finally {
if (dbService) {
try {
await dbService.disconnect()
} catch (closeError) {
console.warn('[Materials] Error disconnecting database:', closeError)
log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
}
@@ -592,25 +568,16 @@ export function registerValidationHandlers(): void {
WHERE MaterialCode = ?
LIMIT 1
`
detailResult = await (dbService as MySqlService).query(detailSql, [
mat.materialCode
])
detailResult = await (dbService as MySqlService).query(detailSql, [mat.materialCode])
}
enrichedMaterials.push({
materialCode: mat.materialCode,
materialName:
detailResult.rows.length > 0
? (detailResult.rows[0].MaterialName as string)
: '',
detailResult.rows.length > 0 ? (detailResult.rows[0].MaterialName as string) : '',
specification:
detailResult.rows.length > 0
? (detailResult.rows[0].Specification as string)
: '',
model:
detailResult.rows.length > 0
? (detailResult.rows[0].Model as string)
: '',
detailResult.rows.length > 0 ? (detailResult.rows[0].Specification as string) : '',
model: detailResult.rows.length > 0 ? (detailResult.rows[0].Model as string) : '',
managerName: mat.managerName,
isMarked: markedCodes.has(mat.materialCode)
})
@@ -618,14 +585,18 @@ export function registerValidationHandlers(): void {
return { materials: enrichedMaterials }
} catch (error) {
console.error('[Materials] Get all error:', error)
log.error('Get all error', {
error: error instanceof Error ? error.message : String(error)
})
return { materials: [] }
} finally {
if (dbService) {
try {
await dbService.disconnect()
} catch (closeError) {
console.warn('[Materials] Error disconnecting database:', closeError)
log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
}
@@ -635,19 +606,18 @@ export function registerValidationHandlers(): void {
/**
* Get statistics
*/
ipcMain.handle(
'materials:getStatistics',
async (_event): Promise<{ stats: any }> => {
try {
const dao = new MaterialsToBeDeletedDAO()
const stats = await dao.getStatistics()
return { stats }
} catch (error) {
console.error('[Materials] Get statistics error:', error)
return { stats: null }
}
ipcMain.handle('materials:getStatistics', async (_event): Promise<{ stats: any }> => {
try {
const dao = new MaterialsToBeDeletedDAO()
const stats = await dao.getStatistics()
return { stats }
} catch (error) {
log.error('Get statistics error', {
error: error instanceof Error ? error.message : String(error)
})
return { stats: null }
}
)
})
/**
* Set shared Production IDs from extractor page
@@ -655,7 +625,7 @@ export function registerValidationHandlers(): void {
ipcMain.handle(
'validation:setSharedProductionIds',
async (_event, productionIds: string[]): Promise<void> => {
console.log(`[Validation] Received ${productionIds.length} shared Production IDs`)
log.info(`Received ${productionIds.length} shared Production IDs`)
setSharedProductionIds(productionIds)
}
)
@@ -676,14 +646,18 @@ export function registerValidationHandlers(): void {
*/
ipcMain.handle(
'validation:getCleanerData',
async (_event): Promise<{
async (
_event
): Promise<{
success: boolean
orderNumbers?: string[]
materialCodes?: string[]
error?: string
}> => {
let dbService: MySqlService | SqlServerService | null = null
const sessionManager = (await import('../services/user/session-manager')).SessionManager.getInstance()
const sessionManager = (
await import('../services/user/session-manager')
).SessionManager.getInstance()
try {
// Get current user
@@ -700,7 +674,7 @@ export function registerValidationHandlers(): void {
const dbType = process.env.DB_TYPE?.toLowerCase()
const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql'
console.log(`[CleanerData] User: ${username}, isAdmin: ${isAdmin}`)
log.info(`User: ${username}, isAdmin: ${isAdmin}`)
// Connect to database
dbService = await getValidationDatabaseService()
@@ -710,9 +684,9 @@ export function registerValidationHandlers(): void {
let orderNumbers: string[] = []
if (sharedIds.length > 0) {
console.log(`[CleanerData] Using ${sharedIds.length} shared Production IDs`)
log.info(`Using ${sharedIds.length} shared Production IDs`)
orderNumbers = await getSourceNumbersFromInputs(sharedIds, dbService)
console.log(`[CleanerData] Got ${orderNumbers.length} order numbers`)
log.info(`Got ${orderNumbers.length} order numbers`)
}
// 2. Get material codes from MaterialsToBeDeleted table
@@ -730,10 +704,8 @@ export function registerValidationHandlers(): void {
? await (dbService as SqlServerService).query(allCodesSql)
: await (dbService as MySqlService).query(allCodesSql)
materialCodes = result.rows
.map(row => row.MaterialCode as string)
.filter(Boolean)
console.log(`[CleanerData] Admin user: got ${materialCodes.length} materials`)
materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean)
log.info(`Admin user: got ${materialCodes.length} materials`)
} else {
// Regular users only see their own materials
if (isSqlServer) {
@@ -746,9 +718,7 @@ export function registerValidationHandlers(): void {
const result = await (dbService as SqlServerService).queryWithParams(userMaterialsSql, {
username: { value: username, type: sql.NVarChar }
})
materialCodes = result.rows
.map(row => row.MaterialCode as string)
.filter(Boolean)
materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean)
} else {
const userMaterialsSql = `
SELECT MaterialCode
@@ -756,11 +726,9 @@ export function registerValidationHandlers(): void {
WHERE ManagerName = ? AND MaterialCode IS NOT NULL
`
const result = await (dbService as MySqlService).query(userMaterialsSql, [username])
materialCodes = result.rows
.map(row => row.MaterialCode as string)
.filter(Boolean)
materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean)
}
console.log(`[CleanerData] Regular user: got ${materialCodes.length} materials`)
log.info(`Regular user: got ${materialCodes.length} materials`)
}
return {
@@ -770,7 +738,9 @@ export function registerValidationHandlers(): void {
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
console.error('[CleanerData] Error:', error)
log.error('CleanerData error', {
error: error instanceof Error ? error.message : String(error)
})
return {
success: false,
error: `获取清理数据失败:${message}`
@@ -780,7 +750,9 @@ export function registerValidationHandlers(): void {
try {
await dbService.disconnect()
} catch (closeError) {
console.warn('[CleanerData] Error disconnecting database:', closeError)
log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
}

View File

@@ -0,0 +1,45 @@
/**
* Zod schemas for Authentication module validation
*/
import { z } from 'zod'
/**
* Schema for login request validation
*/
export const LoginRequestSchema = z.object({
username: z.string().min(1, 'Username is required'),
password: z.string().min(1, 'Password is required')
})
export type LoginRequestZod = z.infer<typeof LoginRequestSchema>
/**
* Schema for user info validation
*/
export const UserInfoSchema = z.object({
id: z.number().int().positive(),
username: z.string().min(1),
userType: z.enum(['Admin', 'User', 'Guest']),
computerName: z.string().optional()
})
export type UserInfoZod = z.infer<typeof UserInfoSchema>
/**
* Validate login request
*/
export function validateLoginRequest(input: unknown): {
success: boolean
data?: LoginRequestZod
error?: string
} {
const result = LoginRequestSchema.safeParse(input)
if (result.success) {
return { success: true, data: result.data }
}
return {
success: false,
error: result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ')
}
}

View File

@@ -0,0 +1,47 @@
/**
* Zod schemas for Cleaner module validation
*/
import { z } from 'zod'
/**
* Schema for cleaner input validation
*/
export const CleanerInputSchema = z.object({
orderNumbers: z
.array(z.string().min(1, 'Order number cannot be empty'))
.min(1, 'At least one order number is required'),
materialCodes: z.array(z.string().min(1, 'Material code cannot be empty')),
dryRun: z.boolean()
// Note: onProgress is a function, not validated via Zod
})
export type CleanerInputZod = z.infer<typeof CleanerInputSchema>
/**
* Schema for cleaner result validation
*/
export const CleanerResultSchema = z.object({
processedCount: z.number().int().nonnegative(),
errors: z.array(z.string())
})
export type CleanerResultZod = z.infer<typeof CleanerResultSchema>
/**
* Validate cleaner input
*/
export function validateCleanerInput(input: unknown): {
success: boolean
data?: CleanerInputZod
error?: string
} {
const result = CleanerInputSchema.safeParse(input)
if (result.success) {
return { success: true, data: result.data }
}
return {
success: false,
error: result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ')
}
}

View File

@@ -0,0 +1,46 @@
/**
* Zod schemas for Extractor module validation
*/
import { z } from 'zod'
/**
* Schema for extractor input validation
*/
export const ExtractorInputSchema = z.object({
orderNumbers: z
.array(z.string().min(1, 'Order number cannot be empty'))
.min(1, 'At least one order number is required'),
batchSize: z.number().int().positive().optional().default(10)
// Note: onProgress is a function, not validated via Zod
})
export type ExtractorInputZod = z.infer<typeof ExtractorInputSchema>
/**
* Schema for extractor result validation
*/
export const ExtractorResultSchema = z.object({
data: z.array(z.record(z.string(), z.unknown())),
errors: z.array(z.string())
})
export type ExtractorResultZod = z.infer<typeof ExtractorResultSchema>
/**
* Validate extractor input
*/
export function validateExtractorInput(input: unknown): {
success: boolean
data?: ExtractorInputZod
error?: string
} {
const result = ExtractorInputSchema.safeParse(input)
if (result.success) {
return { success: true, data: result.data }
}
return {
success: false,
error: result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ')
}
}

View File

@@ -186,11 +186,21 @@ export class ConfigManager {
lines.push('# ERP 系统配置')
lines.push('# ===========================')
lines.push(`ERP_URL=${this.configCache.get('erp.url') || DEFAULT_SETTINGS.erp.url}`)
lines.push(`ERP_USERNAME=${this.configCache.get('erp.username') || DEFAULT_SETTINGS.erp.username}`)
lines.push(`ERP_PASSWORD=${this.configCache.get('erp.password') || DEFAULT_SETTINGS.erp.password}`)
lines.push(`ERP_HEADLESS=${this.configCache.get('erp.headless') || DEFAULT_SETTINGS.erp.headless}`)
lines.push(`ERP_IGNORE_HTTPS_ERRORS=${this.configCache.get('erp.ignoreHttpsErrors') || DEFAULT_SETTINGS.erp.ignoreHttpsErrors}`)
lines.push(`ERP_AUTO_CLOSE_BROWSER=${this.configCache.get('erp.autoCloseBrowser') || DEFAULT_SETTINGS.erp.autoCloseBrowser}`)
lines.push(
`ERP_USERNAME=${this.configCache.get('erp.username') || DEFAULT_SETTINGS.erp.username}`
)
lines.push(
`ERP_PASSWORD=${this.configCache.get('erp.password') || DEFAULT_SETTINGS.erp.password}`
)
lines.push(
`ERP_HEADLESS=${this.configCache.get('erp.headless') || DEFAULT_SETTINGS.erp.headless}`
)
lines.push(
`ERP_IGNORE_HTTPS_ERRORS=${this.configCache.get('erp.ignoreHttpsErrors') || DEFAULT_SETTINGS.erp.ignoreHttpsErrors}`
)
lines.push(
`ERP_AUTO_CLOSE_BROWSER=${this.configCache.get('erp.autoCloseBrowser') || DEFAULT_SETTINGS.erp.autoCloseBrowser}`
)
lines.push('')
// Database Configuration - SQL Server
@@ -210,12 +220,24 @@ export class ConfigManager {
lines.push('# ===========================')
lines.push('# 数据库配置 - MySQL (切换时使用)')
lines.push('# ===========================')
lines.push(`DB_TYPE=${this.configCache.get('database.dbType') || DEFAULT_SETTINGS.database.dbType}`)
lines.push(`DB_NAME=${this.configCache.get('database.database') || DEFAULT_SETTINGS.database.database}`)
lines.push(`DB_USERNAME=${this.configCache.get('database.username') || DEFAULT_SETTINGS.database.username}`)
lines.push(`DB_PASSWORD=${this.configCache.get('database.password') || DEFAULT_SETTINGS.database.password}`)
lines.push(`DB_MYSQL_HOST=${this.configCache.get('database.mysqlHost') || DEFAULT_SETTINGS.database.mysqlHost}`)
lines.push(`DB_MYSQL_PORT=${this.configCache.get('database.mysqlPort') || DEFAULT_SETTINGS.database.mysqlPort}`)
lines.push(
`DB_TYPE=${this.configCache.get('database.dbType') || DEFAULT_SETTINGS.database.dbType}`
)
lines.push(
`DB_NAME=${this.configCache.get('database.database') || DEFAULT_SETTINGS.database.database}`
)
lines.push(
`DB_USERNAME=${this.configCache.get('database.username') || DEFAULT_SETTINGS.database.username}`
)
lines.push(
`DB_PASSWORD=${this.configCache.get('database.password') || DEFAULT_SETTINGS.database.password}`
)
lines.push(
`DB_MYSQL_HOST=${this.configCache.get('database.mysqlHost') || DEFAULT_SETTINGS.database.mysqlHost}`
)
lines.push(
`DB_MYSQL_PORT=${this.configCache.get('database.mysqlPort') || DEFAULT_SETTINGS.database.mysqlPort}`
)
lines.push(`DB_MYSQL_CHARSET=utf8mb4`)
lines.push('')
@@ -233,49 +255,85 @@ export class ConfigManager {
lines.push('# ===========================')
lines.push('# 路径配置')
lines.push('# ===========================')
lines.push(`PATH_DATA_DIR=${this.configCache.get('paths.dataDir') || DEFAULT_SETTINGS.paths.dataDir}`)
lines.push(
`PATH_DATA_DIR=${this.configCache.get('paths.dataDir') || DEFAULT_SETTINGS.paths.dataDir}`
)
lines.push(`PATH_PRODUCTION_ID_FILE=ProductionID.txt`)
lines.push(`PATH_DEFAULT_OUTPUT=${this.configCache.get('paths.defaultOutput') || DEFAULT_SETTINGS.paths.defaultOutput}`)
lines.push(`PATH_VALIDATION_OUTPUT=${this.configCache.get('paths.validationOutput') || DEFAULT_SETTINGS.paths.validationOutput}`)
lines.push(
`PATH_DEFAULT_OUTPUT=${this.configCache.get('paths.defaultOutput') || DEFAULT_SETTINGS.paths.defaultOutput}`
)
lines.push(
`PATH_VALIDATION_OUTPUT=${this.configCache.get('paths.validationOutput') || DEFAULT_SETTINGS.paths.validationOutput}`
)
lines.push('')
// Data Extraction Configuration
lines.push('# ===========================')
lines.push('# 数据提取配置')
lines.push('# ===========================')
lines.push(`EXTRACTION_BATCH_SIZE=${this.configCache.get('extraction.batchSize') || DEFAULT_SETTINGS.extraction.batchSize}`)
lines.push(`EXTRACTION_VERBOSE=${this.configCache.get('extraction.verbose') || DEFAULT_SETTINGS.extraction.verbose}`)
lines.push(`EXTRACTION_AUTO_CONVERT=${this.configCache.get('extraction.autoConvert') || DEFAULT_SETTINGS.extraction.autoConvert}`)
lines.push(`EXTRACTION_MERGE_BATCHES=${this.configCache.get('extraction.mergeBatches') || DEFAULT_SETTINGS.extraction.mergeBatches}`)
lines.push(`EXTRACTION_ENABLE_DB_PERSISTENCE=${this.configCache.get('extraction.enableDbPersistence') || DEFAULT_SETTINGS.extraction.enableDbPersistence}`)
lines.push(
`EXTRACTION_BATCH_SIZE=${this.configCache.get('extraction.batchSize') || DEFAULT_SETTINGS.extraction.batchSize}`
)
lines.push(
`EXTRACTION_VERBOSE=${this.configCache.get('extraction.verbose') || DEFAULT_SETTINGS.extraction.verbose}`
)
lines.push(
`EXTRACTION_AUTO_CONVERT=${this.configCache.get('extraction.autoConvert') || DEFAULT_SETTINGS.extraction.autoConvert}`
)
lines.push(
`EXTRACTION_MERGE_BATCHES=${this.configCache.get('extraction.mergeBatches') || DEFAULT_SETTINGS.extraction.mergeBatches}`
)
lines.push(
`EXTRACTION_ENABLE_DB_PERSISTENCE=${this.configCache.get('extraction.enableDbPersistence') || DEFAULT_SETTINGS.extraction.enableDbPersistence}`
)
lines.push('')
// Validation Configuration
lines.push('# ===========================')
lines.push('# 校验配置')
lines.push('# ===========================')
lines.push(`VALIDATION_DATA_SOURCE=${this.configCache.get('validation.dataSource') || DEFAULT_SETTINGS.validation.dataSource}`)
lines.push(`VALIDATION_USE_DATABASE=${this.configCache.get('validation.useDatabase') || true}`)
lines.push(`VALIDATION_BATCH_SIZE=${this.configCache.get('validation.batchSize') || DEFAULT_SETTINGS.validation.batchSize}`)
lines.push(`VALIDATION_ENABLE_CRUD=${this.configCache.get('validation.enableCrud') || DEFAULT_SETTINGS.validation.enableCrud}`)
lines.push(`VALIDATION_DEFAULT_MANAGER=${this.configCache.get('validation.defaultManager') || DEFAULT_SETTINGS.validation.defaultManager}`)
lines.push(`VALIDATION_MATCH_MODE=${this.configCache.get('validation.matchMode') || DEFAULT_SETTINGS.validation.matchMode}`)
lines.push(
`VALIDATION_DATA_SOURCE=${this.configCache.get('validation.dataSource') || DEFAULT_SETTINGS.validation.dataSource}`
)
lines.push(
`VALIDATION_USE_DATABASE=${this.configCache.get('validation.useDatabase') || true}`
)
lines.push(
`VALIDATION_BATCH_SIZE=${this.configCache.get('validation.batchSize') || DEFAULT_SETTINGS.validation.batchSize}`
)
lines.push(
`VALIDATION_ENABLE_CRUD=${this.configCache.get('validation.enableCrud') || DEFAULT_SETTINGS.validation.enableCrud}`
)
lines.push(
`VALIDATION_DEFAULT_MANAGER=${this.configCache.get('validation.defaultManager') || DEFAULT_SETTINGS.validation.defaultManager}`
)
lines.push(
`VALIDATION_MATCH_MODE=${this.configCache.get('validation.matchMode') || DEFAULT_SETTINGS.validation.matchMode}`
)
lines.push('')
// UI Configuration
lines.push('# ===========================')
lines.push('# UI 配置')
lines.push('# ===========================')
lines.push(`UI_FONT_FAMILY=${this.configCache.get('ui.fontFamily') || DEFAULT_SETTINGS.ui.fontFamily}`)
lines.push(`UI_FONT_SIZE=${this.configCache.get('ui.fontSize') || DEFAULT_SETTINGS.ui.fontSize}`)
lines.push(`UI_PRODUCTION_ID_INPUT_WIDTH=${this.configCache.get('ui.productionIdInputWidth') || DEFAULT_SETTINGS.ui.productionIdInputWidth}`)
lines.push(
`UI_FONT_FAMILY=${this.configCache.get('ui.fontFamily') || DEFAULT_SETTINGS.ui.fontFamily}`
)
lines.push(
`UI_FONT_SIZE=${this.configCache.get('ui.fontSize') || DEFAULT_SETTINGS.ui.fontSize}`
)
lines.push(
`UI_PRODUCTION_ID_INPUT_WIDTH=${this.configCache.get('ui.productionIdInputWidth') || DEFAULT_SETTINGS.ui.productionIdInputWidth}`
)
lines.push('')
// Execution Configuration
lines.push('# ===========================')
lines.push('# 执行配置')
lines.push('# ===========================')
lines.push(`EXECUTION_DRYRUN=${this.configCache.get('execution.dryRun') || DEFAULT_SETTINGS.execution.dryRun}`)
lines.push(
`EXECUTION_DRYRUN=${this.configCache.get('execution.dryRun') || DEFAULT_SETTINGS.execution.dryRun}`
)
const content = lines.join('\n')
fs.writeFileSync(this.envPath, content, 'utf-8')
@@ -296,11 +354,19 @@ export class ConfigManager {
username: this.get('ERP_USERNAME', DEFAULT_SETTINGS.erp.username),
password: this.get('ERP_PASSWORD', DEFAULT_SETTINGS.erp.password),
headless: this.getBoolean('ERP_HEADLESS', DEFAULT_SETTINGS.erp.headless),
ignoreHttpsErrors: this.getBoolean('ERP_IGNORE_HTTPS_ERRORS', DEFAULT_SETTINGS.erp.ignoreHttpsErrors),
autoCloseBrowser: this.getBoolean('ERP_AUTO_CLOSE_BROWSER', DEFAULT_SETTINGS.erp.autoCloseBrowser)
ignoreHttpsErrors: this.getBoolean(
'ERP_IGNORE_HTTPS_ERRORS',
DEFAULT_SETTINGS.erp.ignoreHttpsErrors
),
autoCloseBrowser: this.getBoolean(
'ERP_AUTO_CLOSE_BROWSER',
DEFAULT_SETTINGS.erp.autoCloseBrowser
)
},
database: {
dbType: (this.get('DB_TYPE', DEFAULT_SETTINGS.database.dbType) as DatabaseType) || DEFAULT_SETTINGS.database.dbType,
dbType:
(this.get('DB_TYPE', DEFAULT_SETTINGS.database.dbType) as DatabaseType) ||
DEFAULT_SETTINGS.database.dbType,
server: this.get('DB_SERVER', DEFAULT_SETTINGS.database.server),
mysqlHost: this.get('DB_MYSQL_HOST', DEFAULT_SETTINGS.database.mysqlHost),
mysqlPort: this.getNumber('DB_MYSQL_PORT', DEFAULT_SETTINGS.database.mysqlPort),
@@ -311,26 +377,53 @@ export class ConfigManager {
paths: {
dataDir: this.get('PATH_DATA_DIR', DEFAULT_SETTINGS.paths.dataDir),
defaultOutput: this.get('PATH_DEFAULT_OUTPUT', DEFAULT_SETTINGS.paths.defaultOutput),
validationOutput: this.get('PATH_VALIDATION_OUTPUT', DEFAULT_SETTINGS.paths.validationOutput)
validationOutput: this.get(
'PATH_VALIDATION_OUTPUT',
DEFAULT_SETTINGS.paths.validationOutput
)
},
extraction: {
batchSize: this.getNumber('EXTRACTION_BATCH_SIZE', DEFAULT_SETTINGS.extraction.batchSize),
verbose: this.getBoolean('EXTRACTION_VERBOSE', DEFAULT_SETTINGS.extraction.verbose),
autoConvert: this.getBoolean('EXTRACTION_AUTO_CONVERT', DEFAULT_SETTINGS.extraction.autoConvert),
mergeBatches: this.getBoolean('EXTRACTION_MERGE_BATCHES', DEFAULT_SETTINGS.extraction.mergeBatches),
enableDbPersistence: this.getBoolean('EXTRACTION_ENABLE_DB_PERSISTENCE', DEFAULT_SETTINGS.extraction.enableDbPersistence)
autoConvert: this.getBoolean(
'EXTRACTION_AUTO_CONVERT',
DEFAULT_SETTINGS.extraction.autoConvert
),
mergeBatches: this.getBoolean(
'EXTRACTION_MERGE_BATCHES',
DEFAULT_SETTINGS.extraction.mergeBatches
),
enableDbPersistence: this.getBoolean(
'EXTRACTION_ENABLE_DB_PERSISTENCE',
DEFAULT_SETTINGS.extraction.enableDbPersistence
)
},
validation: {
dataSource: (this.get('VALIDATION_DATA_SOURCE', DEFAULT_SETTINGS.validation.dataSource) as ValidationDataSource) || DEFAULT_SETTINGS.validation.dataSource,
dataSource:
(this.get(
'VALIDATION_DATA_SOURCE',
DEFAULT_SETTINGS.validation.dataSource
) as ValidationDataSource) || DEFAULT_SETTINGS.validation.dataSource,
batchSize: this.getNumber('VALIDATION_BATCH_SIZE', DEFAULT_SETTINGS.validation.batchSize),
matchMode: (this.get('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode) as MatchMode) || DEFAULT_SETTINGS.validation.matchMode,
enableCrud: this.getBoolean('VALIDATION_ENABLE_CRUD', DEFAULT_SETTINGS.validation.enableCrud),
defaultManager: this.get('VALIDATION_DEFAULT_MANAGER', DEFAULT_SETTINGS.validation.defaultManager)
matchMode:
(this.get('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode) as MatchMode) ||
DEFAULT_SETTINGS.validation.matchMode,
enableCrud: this.getBoolean(
'VALIDATION_ENABLE_CRUD',
DEFAULT_SETTINGS.validation.enableCrud
),
defaultManager: this.get(
'VALIDATION_DEFAULT_MANAGER',
DEFAULT_SETTINGS.validation.defaultManager
)
},
ui: {
fontFamily: this.get('UI_FONT_FAMILY', DEFAULT_SETTINGS.ui.fontFamily),
fontSize: this.getNumber('UI_FONT_SIZE', DEFAULT_SETTINGS.ui.fontSize),
productionIdInputWidth: this.getNumber('UI_PRODUCTION_ID_INPUT_WIDTH', DEFAULT_SETTINGS.ui.productionIdInputWidth)
productionIdInputWidth: this.getNumber(
'UI_PRODUCTION_ID_INPUT_WIDTH',
DEFAULT_SETTINGS.ui.productionIdInputWidth
)
},
execution: {
dryRun: this.getBoolean('EXECUTION_DRYRUN', DEFAULT_SETTINGS.execution.dryRun)

View File

@@ -0,0 +1,97 @@
/**
* TypeORM Data Source Configuration
*
* Provides a centralized database connection for TypeORM entities.
* Supports both MySQL and SQL Server based on DB_TYPE environment variable.
*/
import 'reflect-metadata'
import { DataSource, DataSourceOptions } from 'typeorm'
/**
* Get database type from environment
*/
function getDatabaseType(): 'mysql' | 'mssql' {
const dbType = process.env.DB_TYPE?.toLowerCase()
if (dbType === 'sqlserver' || dbType === 'mssql') {
return 'mssql'
}
return 'mysql'
}
/**
* Build DataSourceOptions based on database type
*/
function buildDataSourceOptions(): DataSourceOptions {
const type = getDatabaseType()
const commonOptions: Partial<DataSourceOptions> = {
entities: [__dirname + '/entities/*.{ts,js}'],
synchronize: false, // Never auto-sync in production
logging: process.env.NODE_ENV !== 'production'
}
if (type === 'mssql') {
return {
type: 'mssql',
host: process.env.DB_SERVER || 'localhost',
port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10),
username: process.env.DB_USERNAME || 'sa',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || '',
options: {
encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes',
trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes'
},
...commonOptions
} as DataSourceOptions
}
return {
type: 'mysql',
host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || '',
...commonOptions
} as DataSourceOptions
}
/**
* TypeORM DataSource singleton
*/
let dataSource: DataSource | null = null
/**
* Get or create the DataSource
*/
export function getDataSource(): DataSource {
if (!dataSource) {
dataSource = new DataSource(buildDataSourceOptions())
}
return dataSource
}
/**
* Initialize the DataSource
*/
export async function initializeDataSource(): Promise<DataSource> {
const ds = getDataSource()
if (!ds.isInitialized) {
await ds.initialize()
}
return ds
}
/**
* Destroy the DataSource
*/
export async function destroyDataSource(): Promise<void> {
if (dataSource && dataSource.isInitialized) {
await dataSource.destroy()
dataSource = null
}
}
export default getDataSource

View File

@@ -11,6 +11,9 @@
import { MySqlService } from './mysql'
import { SqlServerService } from './sql-server'
import sql from 'mssql'
import { createLogger } from '../logger'
const log = createLogger('DiscreteMaterialPlanDAO')
/**
* Material plan record interface
@@ -171,13 +174,16 @@ export class DiscreteMaterialPlanDAO {
const sqlString = `SELECT * FROM ${tableName}`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query all error:', error)
log.error('Query all error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -215,13 +221,16 @@ export class DiscreteMaterialPlanDAO {
WHERE rn = 1
`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query all distinct by material code error:', error)
log.error('Query all distinct by material code error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -279,7 +288,9 @@ export class DiscreteMaterialPlanDAO {
return allResults
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query by source numbers error:', error)
log.error('Query by source numbers error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -371,7 +382,9 @@ export class DiscreteMaterialPlanDAO {
return allResults
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query by source numbers distinct error:', error)
log.error('Query by source numbers distinct error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -410,7 +423,9 @@ export class DiscreteMaterialPlanDAO {
return result.rows
}
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query by source number error:', error)
log.error('Query by source number error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -451,7 +466,9 @@ export class DiscreteMaterialPlanDAO {
return result.rows
}
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query by plan number error:', error)
log.error('Query by plan number error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -499,7 +516,9 @@ export class DiscreteMaterialPlanDAO {
return result.rows
}
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query by plan numbers error:', error)
log.error('Query by plan numbers error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -517,13 +536,16 @@ export class DiscreteMaterialPlanDAO {
const sqlString = `SELECT COUNT(*) as count FROM ${tableName}`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.length > 0 ? (result.rows[0].count as number) : 0
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Count all error:', error)
log.error('Count all error', {
error: error instanceof Error ? error.message : String(error)
})
return 0
}
}
@@ -562,7 +584,9 @@ export class DiscreteMaterialPlanDAO {
return result.rows.length > 0 ? (result.rows[0].count as number) : 0
}
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Count by plan number error:', error)
log.error('Count by plan number error', {
error: error instanceof Error ? error.message : String(error)
})
return 0
}
}
@@ -594,9 +618,7 @@ export class DiscreteMaterialPlanDAO {
`
const result = await (dbService as SqlServerService).queryWithParams(sqlString, params)
return result.rows
.map(row => row.MaterialName as string)
.filter(Boolean)
return result.rows.map((row) => row.MaterialName as string).filter(Boolean)
} else {
const placeholders = sourceNumbers.map(() => '?').join(',')
@@ -608,9 +630,7 @@ export class DiscreteMaterialPlanDAO {
`
const result = await (dbService as MySqlService).query(sqlString, sourceNumbers)
return result.rows
.map(row => row.MaterialName as string)
.filter(Boolean)
return result.rows.map((row) => row.MaterialName as string).filter(Boolean)
}
} else {
const sqlString = `
@@ -619,16 +639,17 @@ export class DiscreteMaterialPlanDAO {
WHERE MaterialName IS NOT NULL
`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows
.map(row => row.MaterialName as string)
.filter(Boolean)
return result.rows.map((row) => row.MaterialName as string).filter(Boolean)
}
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Get unique material names error:', error)
log.error('Get unique material names error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -652,13 +673,16 @@ export class DiscreteMaterialPlanDAO {
FROM ${tableName}
`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.length > 0 ? result.rows[0] : {}
} catch (error) {
console.error('[DiscreteMaterialPlanDAO] Get statistics error:', error)
log.error('Get statistics error', {
error: error instanceof Error ? error.message : String(error)
})
return {}
}
}

View File

@@ -0,0 +1,143 @@
/**
* TypeORM Entity for DiscreteMaterialPlanData table
*/
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'
@Entity('DiscreteMaterialPlanData')
export class DiscreteMaterialPlan {
@PrimaryGeneratedColumn()
id!: number
@Column({ name: 'Factory', type: 'nvarchar', length: 100, nullable: true })
factory!: string | null
@Column({ name: 'MaterialStatus', type: 'nvarchar', length: 50, nullable: true })
materialStatus!: string | null
@Index()
@Column({ name: 'PlanNumber', type: 'nvarchar', length: 100, nullable: true })
planNumber!: string | null
@Index()
@Column({ name: 'SourceNumber', type: 'nvarchar', length: 100, nullable: true })
sourceNumber!: string | null
@Column({ name: 'MaterialType', type: 'nvarchar', length: 100, nullable: true })
materialType!: string | null
@Column({ name: 'ProductCode', type: 'nvarchar', length: 100, nullable: true })
productCode!: string | null
@Column({ name: 'ProductName', type: 'nvarchar', length: 255, nullable: true })
productName!: string | null
@Column({ name: 'ProductUnit', type: 'nvarchar', length: 50, nullable: true })
productUnit!: string | null
@Column({ name: 'ProductPlanQuantity', type: 'decimal', precision: 18, scale: 4, nullable: true })
productPlanQuantity!: number | null
@Column({ name: 'UseDepartment', type: 'nvarchar', length: 100, nullable: true })
useDepartment!: string | null
@Column({ name: 'Remark', type: 'nvarchar', length: 500, nullable: true })
remark!: string | null
@Column({ name: 'Creator', type: 'nvarchar', length: 100, nullable: true })
creator!: string | null
@Column({ name: 'CreateDate', type: 'datetime', nullable: true })
createDate!: Date | null
@Column({ name: 'Approver', type: 'nvarchar', length: 100, nullable: true })
approver!: string | null
@Column({ name: 'ApproveDate', type: 'datetime', nullable: true })
approveDate!: Date | null
@Column({ name: 'SequenceNumber', type: 'int', nullable: true })
sequenceNumber!: number | null
@Index()
@Column({ name: 'MaterialCode', type: 'nvarchar', length: 100, nullable: true })
materialCode!: string | null
@Column({ name: 'MaterialName', type: 'nvarchar', length: 255, nullable: true })
materialName!: string | null
@Column({ name: 'Specification', type: 'nvarchar', length: 255, nullable: true })
specification!: string | null
@Column({ name: 'Model', type: 'nvarchar', length: 255, nullable: true })
model!: string | null
@Column({ name: 'DrawingNumber', type: 'nvarchar', length: 100, nullable: true })
drawingNumber!: string | null
@Column({ name: 'MaterialQuality', type: 'nvarchar', length: 100, nullable: true })
materialQuality!: string | null
@Column({ name: 'PlanQuantity', type: 'decimal', precision: 18, scale: 4, nullable: true })
planQuantity!: number | null
@Column({ name: 'Unit', type: 'nvarchar', length: 50, nullable: true })
unit!: string | null
@Column({ name: 'RequiredDate', type: 'datetime', nullable: true })
requiredDate!: Date | null
@Column({ name: 'Warehouse', type: 'nvarchar', length: 100, nullable: true })
warehouse!: string | null
@Column({ name: 'UnitUsage', type: 'decimal', precision: 18, scale: 6, nullable: true })
unitUsage!: number | null
@Column({
name: 'CumulativeOutputQuantity',
type: 'decimal',
precision: 18,
scale: 4,
nullable: true
})
cumulativeOutputQuantity!: number | null
@Column({ name: 'BOMVersion', type: 'nvarchar', length: 50, nullable: true })
bomVersion!: string | null
}
/**
* Material plan record interface for type-safe operations
*/
export interface MaterialPlanRecordData {
id?: number
factory?: string | null
materialStatus?: string | null
planNumber?: string | null
sourceNumber?: string | null
materialType?: string | null
productCode?: string | null
productName?: string | null
productUnit?: string | null
productPlanQuantity?: number | null
useDepartment?: string | null
remark?: string | null
creator?: string | null
createDate?: Date | null
approver?: string | null
approveDate?: Date | null
sequenceNumber?: number | null
materialCode?: string | null
materialName?: string | null
specification?: string | null
model?: string | null
drawingNumber?: string | null
materialQuality?: string | null
planQuantity?: number | null
unit?: string | null
requiredDate?: Date | null
warehouse?: string | null
unitUsage?: number | null
cumulativeOutputQuantity?: number | null
bomVersion?: string | null
}

View File

@@ -0,0 +1,27 @@
/**
* TypeORM Entity for MaterialsToBeDeleted table
*/
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'
@Entity('MaterialsToBeDeleted')
export class MaterialsToBeDeleted {
@PrimaryGeneratedColumn()
id!: number
@Index({ unique: true })
@Column({ name: 'MaterialCode', type: 'nvarchar', length: 255, nullable: false })
materialCode!: string
@Column({ name: 'ManagerName', type: 'nvarchar', length: 255, nullable: true })
managerName!: string | null
}
/**
* Material record interface for type-safe operations
*/
export interface MaterialRecordData {
id?: number
materialCode: string
managerName: string | null
}

View File

@@ -11,6 +11,9 @@
import { MySqlService } from './mysql'
import { SqlServerService } from './sql-server'
import sql from 'mssql'
import { createLogger } from '../logger'
const log = createLogger('MaterialsToBeDeletedDAO')
/**
* Material record interface
@@ -132,7 +135,7 @@ export class MaterialsToBeDeletedDAO {
*/
async upsertMaterial(materialCode: string, managerName: string): Promise<boolean> {
if (!materialCode || !materialCode.trim()) {
console.error('[MaterialsToBeDeletedDAO] MaterialCode cannot be empty')
log.error('MaterialCode cannot be empty')
return false
}
@@ -167,7 +170,9 @@ export class MaterialsToBeDeletedDAO {
return true
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Upsert material error:', error)
log.error('Upsert material error', {
error: error instanceof Error ? error.message : String(error)
})
return false
}
}
@@ -177,7 +182,9 @@ export class MaterialsToBeDeletedDAO {
* @param materials - List of materials with materialCode and managerName
* @returns Statistics object
*/
async upsertBatch(materials: { materialCode: string; managerName: string }[]): Promise<UpsertStats> {
async upsertBatch(
materials: { materialCode: string; managerName: string }[]
): Promise<UpsertStats> {
if (!materials || materials.length === 0) {
return { total: 0, success: 0, failed: 0 }
}
@@ -227,12 +234,17 @@ export class MaterialsToBeDeletedDAO {
stats.success++
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Error upserting material:', materialCode, error)
log.error('Error upserting material', {
materialCode,
error: error instanceof Error ? error.message : String(error)
})
stats.failed++
}
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Batch upsert error:', error)
log.error('Batch upsert error', {
error: error instanceof Error ? error.message : String(error)
})
stats.failed = stats.total - stats.success
}
@@ -256,13 +268,16 @@ export class MaterialsToBeDeletedDAO {
WHERE MaterialCode IS NOT NULL
`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return new Set(result.rows.map(row => row.MaterialCode as string).filter(Boolean))
return new Set(result.rows.map((row) => row.MaterialCode as string).filter(Boolean))
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get all material codes error:', error)
log.error('Get all material codes error', {
error: error instanceof Error ? error.message : String(error)
})
return new Set()
}
}
@@ -283,17 +298,20 @@ export class MaterialsToBeDeletedDAO {
ORDER BY ManagerName, MaterialCode
`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.map(row => ({
return result.rows.map((row) => ({
id: row.ID as number,
materialCode: row.MaterialCode as string,
managerName: row.ManagerName as string
}))
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get all records error:', error)
log.error('Get all records error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -320,7 +338,7 @@ export class MaterialsToBeDeletedDAO {
managerName: { value: managerName, type: sql.NVarChar }
})
return result.rows.map(row => ({
return result.rows.map((row) => ({
id: row.ID as number,
materialCode: row.MaterialCode as string,
managerName: row.ManagerName as string
@@ -335,14 +353,16 @@ export class MaterialsToBeDeletedDAO {
const result = await (dbService as MySqlService).query(sqlString, [managerName])
return result.rows.map(row => ({
return result.rows.map((row) => ({
id: row.ID as number,
materialCode: row.MaterialCode as string,
managerName: row.ManagerName as string
}))
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get materials by manager error:', error)
log.error('Get materials by manager error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -363,13 +383,16 @@ export class MaterialsToBeDeletedDAO {
ORDER BY ManagerName
`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.map(row => row.ManagerName as string).filter(Boolean)
return result.rows.map((row) => row.ManagerName as string).filter(Boolean)
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get managers error:', error)
log.error('Get managers error', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
@@ -427,7 +450,9 @@ export class MaterialsToBeDeletedDAO {
}
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get record by material code error:', error)
log.error('Get record by material code error', {
error: error instanceof Error ? error.message : String(error)
})
return null
}
}
@@ -467,7 +492,9 @@ export class MaterialsToBeDeletedDAO {
return result.rowCount > 0
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Delete by material code error:', error)
log.error('Delete by material code error', {
error: error instanceof Error ? error.message : String(error)
})
return false
}
}
@@ -504,7 +531,9 @@ export class MaterialsToBeDeletedDAO {
return result.rowCount
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Delete by manager error:', error)
log.error('Delete by manager error', {
error: error instanceof Error ? error.message : String(error)
})
return 0
}
}
@@ -520,13 +549,16 @@ export class MaterialsToBeDeletedDAO {
const sqlString = `DELETE FROM ${tableName}`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rowCount
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Delete all materials error:', error)
log.error('Delete all materials error', {
error: error instanceof Error ? error.message : String(error)
})
return 0
}
}
@@ -579,7 +611,9 @@ export class MaterialsToBeDeletedDAO {
}
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Delete by material codes error:', error)
log.error('Delete by material codes error', {
error: error instanceof Error ? error.message : String(error)
})
}
return totalDeleted
@@ -622,7 +656,9 @@ export class MaterialsToBeDeletedDAO {
return result.rows.length > 0 && (result.rows[0].count as number) > 0
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Material exists error:', error)
log.error('Material exists error', {
error: error instanceof Error ? error.message : String(error)
})
return false
}
}
@@ -638,13 +674,16 @@ export class MaterialsToBeDeletedDAO {
const sqlString = `SELECT COUNT(*) as count FROM ${tableName}`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.length > 0 ? (result.rows[0].count as number) : 0
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Count all error:', error)
log.error('Count all error', {
error: error instanceof Error ? error.message : String(error)
})
return 0
}
}
@@ -683,7 +722,9 @@ export class MaterialsToBeDeletedDAO {
return result.rows.length > 0 ? (result.rows[0].count as number) : 0
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Count by manager error:', error)
log.error('Count by manager error', {
error: error instanceof Error ? error.message : String(error)
})
return 0
}
}
@@ -706,9 +747,10 @@ export class MaterialsToBeDeletedDAO {
WHERE MaterialCode IS NOT NULL
`
const statsResult = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(statsSql)
: await (dbService as MySqlService).query(statsSql)
const statsResult =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(statsSql)
: await (dbService as MySqlService).query(statsSql)
const stats = statsResult.rows[0] || {}
@@ -721,11 +763,12 @@ export class MaterialsToBeDeletedDAO {
ORDER BY count DESC
`
const managerResult = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(managerSql)
: await (dbService as MySqlService).query(managerSql)
const managerResult =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(managerSql)
: await (dbService as MySqlService).query(managerSql)
const materialsPerManager = managerResult.rows.map(row => ({
const materialsPerManager = managerResult.rows.map((row) => ({
[row.ManagerName as string]: row.count as number
}))
@@ -735,7 +778,9 @@ export class MaterialsToBeDeletedDAO {
materialsPerManager
}
} catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get statistics error:', error)
log.error('Get statistics error', {
error: error instanceof Error ? error.message : String(error)
})
return {
totalMaterials: 0,
uniqueManagers: 0,

View File

@@ -0,0 +1,281 @@
/**
* Repository for DiscreteMaterialPlan entity
*
* Provides type-safe database operations for discrete material plan data.
*/
import { DataSource, Repository, In } from 'typeorm'
import { DiscreteMaterialPlan, MaterialPlanRecordData } from '../entities/DiscreteMaterialPlan'
import { getDataSource } from '../data-source'
import { createLogger } from '../../logger'
const log = createLogger('DiscreteMaterialPlanRepository')
/**
* DiscreteMaterialPlan Repository class
*/
export class DiscreteMaterialPlanRepository {
private repository: Repository<DiscreteMaterialPlan> | null = null
private dataSource: DataSource | null = null
/**
* Get the repository instance
*/
private async getRepository(): Promise<Repository<DiscreteMaterialPlan>> {
if (!this.repository) {
this.dataSource = getDataSource()
if (!this.dataSource.isInitialized) {
await this.dataSource.initialize()
}
this.repository = this.dataSource.getRepository(DiscreteMaterialPlan)
}
return this.repository
}
/**
* Query all records
*/
async queryAll(): Promise<DiscreteMaterialPlan[]> {
try {
const repo = await this.getRepository()
return await repo.find()
} catch (error) {
log.error('Query all failed', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Query all records with deduplication by MaterialCode
*/
async queryAllDistinctByMaterialCode(): Promise<DiscreteMaterialPlan[]> {
try {
const repo = await this.getRepository()
const query = `
WITH RankedRecords AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY MaterialCode
ORDER BY CreateDate ASC, SequenceNumber ASC
) AS rn
FROM DiscreteMaterialPlanData
WHERE MaterialCode IS NOT NULL
)
SELECT * FROM RankedRecords WHERE rn = 1
`
return await repo.query(query)
} catch (error) {
log.error('Query all distinct by material code failed', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Query by source numbers (production order numbers)
*/
async queryBySourceNumbers(sourceNumbers: string[]): Promise<DiscreteMaterialPlan[]> {
if (!sourceNumbers.length) return []
try {
const repo = await this.getRepository()
const batchSize = 2000
const allResults: DiscreteMaterialPlan[] = []
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
const batch = sourceNumbers.slice(i, i + batchSize)
const results = await repo.find({
where: { sourceNumber: In(batch) }
})
allResults.push(...results)
}
return allResults
} catch (error) {
log.error('Query by source numbers failed', {
count: sourceNumbers.length,
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Query by source numbers with deduplication by MaterialCode
*/
async queryBySourceNumbersDistinct(sourceNumbers: string[]): Promise<DiscreteMaterialPlan[]> {
if (!sourceNumbers.length) return []
try {
const repo = await this.getRepository()
const batchSize = 2000
const allResults: DiscreteMaterialPlan[] = []
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
const batch = sourceNumbers.slice(i, i + batchSize)
const query = `
WITH RankedRecords AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY MaterialCode
ORDER BY CreateDate ASC, SequenceNumber ASC
) AS rn
FROM DiscreteMaterialPlanData
WHERE SourceNumber IN (?) AND MaterialCode IS NOT NULL
)
SELECT * FROM RankedRecords WHERE rn = 1
`
const results = await repo.query(query, [batch])
allResults.push(...results)
}
return allResults
} catch (error) {
log.error('Query by source numbers distinct failed', {
count: sourceNumbers.length,
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Query by single source number
*/
async queryBySourceNumber(sourceNumber: string): Promise<DiscreteMaterialPlan[]> {
try {
const repo = await this.getRepository()
return await repo.find({ where: { sourceNumber } })
} catch (error) {
log.error('Query by source number failed', {
sourceNumber,
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Query by plan number
*/
async queryByPlanNumber(planNumber: string): Promise<DiscreteMaterialPlan[]> {
try {
const repo = await this.getRepository()
return await repo.find({ where: { planNumber } })
} catch (error) {
log.error('Query by plan number failed', {
planNumber,
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Query by multiple plan numbers
*/
async queryByPlanNumbers(planNumbers: string[]): Promise<DiscreteMaterialPlan[]> {
if (!planNumbers.length) return []
try {
const repo = await this.getRepository()
return await repo.find({
where: { planNumber: In(planNumbers) }
})
} catch (error) {
log.error('Query by plan numbers failed', {
count: planNumbers.length,
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Count all records
*/
async countAll(): Promise<number> {
try {
const repo = await this.getRepository()
return await repo.count()
} catch {
return 0
}
}
/**
* Get unique material names
*/
async getUniqueMaterialNames(sourceNumbers?: string[]): Promise<string[]> {
try {
const repo = await this.getRepository()
let query = repo
.createQueryBuilder('m')
.select('DISTINCT m.materialName', 'materialName')
.where('m.materialName IS NOT NULL')
if (sourceNumbers && sourceNumbers.length > 0) {
query = query.andWhere('m.sourceNumber IN (:...sourceNumbers)', { sourceNumbers })
}
const result = await query.orderBy('m.materialName', 'ASC').getRawMany()
return result.map((r) => r.materialName).filter(Boolean)
} catch (error) {
log.error('Get unique material names failed', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Get statistics
*/
async getStatistics(): Promise<{
totalRecords: number
uniquePlans: number
uniqueOrders: number
earliestRecord: Date | null
latestRecord: Date | null
}> {
try {
const repo = await this.getRepository()
const result = await repo
.createQueryBuilder('m')
.select('COUNT(*)', 'totalRecords')
.addSelect('COUNT(DISTINCT m.planNumber)', 'uniquePlans')
.addSelect('COUNT(DISTINCT m.sourceNumber)', 'uniqueOrders')
.addSelect('MIN(m.createDate)', 'earliestRecord')
.addSelect('MAX(m.createDate)', 'latestRecord')
.getRawOne()
return {
totalRecords: parseInt(result?.totalRecords || '0', 10),
uniquePlans: parseInt(result?.uniquePlans || '0', 10),
uniqueOrders: parseInt(result?.uniqueOrders || '0', 10),
earliestRecord: result?.earliestRecord || null,
latestRecord: result?.latestRecord || null
}
} catch (error) {
log.error('Get statistics failed', {
error: error instanceof Error ? error.message : String(error)
})
return {
totalRecords: 0,
uniquePlans: 0,
uniqueOrders: 0,
earliestRecord: null,
latestRecord: null
}
}
}
}

View File

@@ -0,0 +1,266 @@
/**
* Repository for MaterialsToBeDeleted entity
*
* Provides type-safe database operations for materials to be deleted.
*/
import { DataSource, Repository, In } from 'typeorm'
import { MaterialsToBeDeleted, MaterialRecordData } from '../entities/MaterialsToBeDeleted'
import { getDataSource } from '../data-source'
import { createLogger } from '../../logger'
const log = createLogger('MaterialsToBeDeletedRepository')
/**
* Upsert statistics
*/
export interface UpsertStats {
total: number
success: number
failed: number
}
/**
* MaterialsToBeDeleted Repository class
*/
export class MaterialsToBeDeletedRepository {
private repository: Repository<MaterialsToBeDeleted> | null = null
private dataSource: DataSource | null = null
/**
* Get the repository instance
*/
private async getRepository(): Promise<Repository<MaterialsToBeDeleted>> {
if (!this.repository) {
this.dataSource = getDataSource()
if (!this.dataSource.isInitialized) {
await this.dataSource.initialize()
}
this.repository = this.dataSource.getRepository(MaterialsToBeDeleted)
}
return this.repository
}
/**
* Insert or update a single material record
*/
async upsert(materialCode: string, managerName: string | null): Promise<boolean> {
try {
const repo = await this.getRepository()
// Use upsert pattern
let entity = await repo.findOne({ where: { materialCode } })
if (entity) {
entity.managerName = managerName
} else {
entity = repo.create({ materialCode, managerName })
}
await repo.save(entity)
log.debug('Upserted material', { materialCode })
return true
} catch (error) {
log.error('Upsert material failed', {
materialCode,
error: error instanceof Error ? error.message : String(error)
})
return false
}
}
/**
* Insert or update multiple material records in batch
*/
async upsertBatch(materials: MaterialRecordData[]): Promise<UpsertStats> {
const stats: UpsertStats = {
total: materials.length,
success: 0,
failed: 0
}
try {
const repo = await this.getRepository()
for (const material of materials) {
if (!material.materialCode?.trim()) {
stats.failed++
continue
}
try {
let entity = await repo.findOne({ where: { materialCode: material.materialCode } })
if (entity) {
entity.managerName = material.managerName
} else {
entity = repo.create({
materialCode: material.materialCode,
managerName: material.managerName
})
}
await repo.save(entity)
stats.success++
} catch {
stats.failed++
}
}
log.info('Batch upsert completed', stats)
return stats
} catch (error) {
log.error('Batch upsert failed', {
error: error instanceof Error ? error.message : String(error)
})
stats.failed = stats.total - stats.success
return stats
}
}
/**
* Get all material codes as a set
*/
async getAllMaterialCodes(): Promise<Set<string>> {
try {
const repo = await this.getRepository()
const records = await repo.find({
select: ['materialCode'],
where: { materialCode: In([]) } // This will be overridden
})
// Use query builder for better performance
const result = await repo
.createQueryBuilder('m')
.select('m.materialCode')
.where('m.materialCode IS NOT NULL')
.getMany()
return new Set(result.map((r) => r.materialCode).filter(Boolean))
} catch (error) {
log.error('Get all material codes failed', {
error: error instanceof Error ? error.message : String(error)
})
return new Set()
}
}
/**
* Get all records
*/
async getAllRecords(): Promise<MaterialsToBeDeleted[]> {
try {
const repo = await this.getRepository()
return await repo.find({
order: { managerName: 'ASC', materialCode: 'ASC' }
})
} catch (error) {
log.error('Get all records failed', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Get materials by manager name
*/
async getByManager(managerName: string): Promise<MaterialsToBeDeleted[]> {
try {
const repo = await this.getRepository()
return await repo.find({
where: { managerName },
order: { materialCode: 'ASC' }
})
} catch (error) {
log.error('Get by manager failed', {
managerName,
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Get unique manager names
*/
async getManagers(): Promise<string[]> {
try {
const repo = await this.getRepository()
const result = await repo
.createQueryBuilder('m')
.select('DISTINCT m.managerName', 'managerName')
.where('m.managerName IS NOT NULL')
.orderBy('m.managerName', 'ASC')
.getRawMany()
return result.map((r) => r.managerName).filter(Boolean)
} catch (error) {
log.error('Get managers failed', {
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
/**
* Delete by material code
*/
async deleteByMaterialCode(materialCode: string): Promise<boolean> {
try {
const repo = await this.getRepository()
const result = await repo.delete({ materialCode })
return (result.affected ?? 0) > 0
} catch (error) {
log.error('Delete by material code failed', {
materialCode,
error: error instanceof Error ? error.message : String(error)
})
return false
}
}
/**
* Delete multiple materials by codes
*/
async deleteByMaterialCodes(materialCodes: string[]): Promise<number> {
if (!materialCodes.length) return 0
try {
const repo = await this.getRepository()
const result = await repo.delete({ materialCode: In(materialCodes) })
return result.affected ?? 0
} catch (error) {
log.error('Delete by material codes failed', {
count: materialCodes.length,
error: error instanceof Error ? error.message : String(error)
})
return 0
}
}
/**
* Check if a material exists
*/
async exists(materialCode: string): Promise<boolean> {
try {
const repo = await this.getRepository()
const count = await repo.count({ where: { materialCode } })
return count > 0
} catch {
return false
}
}
/**
* Count all records
*/
async countAll(): Promise<number> {
try {
const repo = await this.getRepository()
return await repo.count()
} catch {
return 0
}
}
}

View File

@@ -0,0 +1,208 @@
/**
* ERP Browser Manager
*
* Manages browser lifecycle for ERP automation.
* Separates browser management from authentication logic.
*/
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright'
import { createLogger } from '../logger'
const log = createLogger('ErpBrowserManager')
/**
* Browser configuration options
*/
export interface BrowserConfig {
headless?: boolean
slowMo?: number
viewport?: { width: number; height: number }
ignoreHTTPSErrors?: boolean
acceptDownloads?: boolean
}
/**
* Default browser configuration
*/
const DEFAULT_CONFIG: Required<BrowserConfig> = {
headless: false,
slowMo: 100,
viewport: { width: 1920, height: 1080 },
ignoreHTTPSErrors: true,
acceptDownloads: true
}
/**
* Browser session containing all browser-related objects
*/
export interface BrowserSession {
browser: Browser
context: BrowserContext
page: Page
}
/**
* ErpBrowserManager class
* Manages browser lifecycle independently from ERP authentication
*/
export class ErpBrowserManager {
private config: Required<BrowserConfig>
private session: BrowserSession | null = null
constructor(config?: BrowserConfig) {
this.config = { ...DEFAULT_CONFIG, ...config }
}
/**
* Launch a new browser instance
*/
async launch(): Promise<Browser> {
if (this.session?.browser?.isConnected()) {
log.debug('Browser already running, returning existing instance')
return this.session.browser
}
log.info('Launching browser', { headless: this.config.headless })
const browser = await chromium.launch({
headless: this.config.headless,
slowMo: this.config.slowMo,
args: [
'--ignore-certificate-errors',
'--ignore-ssl-errors',
'--ignore-certificate-errors-spki-list',
'--disable-web-security'
]
})
log.info('Browser launched successfully')
return browser
}
/**
* Create a new browser context
*/
async createContext(browser?: Browser): Promise<BrowserContext> {
const browserInstance = browser || (await this.launch())
log.debug('Creating browser context')
const context = await browserInstance.newContext({
acceptDownloads: this.config.acceptDownloads,
viewport: this.config.viewport,
ignoreHTTPSErrors: this.config.ignoreHTTPSErrors,
javaScriptEnabled: true
})
log.debug('Browser context created')
return context
}
/**
* Create a new page in the context
*/
async createPage(context?: BrowserContext): Promise<Page> {
let contextInstance: BrowserContext
if (context) {
contextInstance = context
} else if (this.session?.context) {
contextInstance = this.session.context
} else {
const browser = await this.launch()
contextInstance = await this.createContext(browser)
}
log.debug('Creating new page')
const page = await contextInstance.newPage()
log.debug('Page created')
return page
}
/**
* Initialize a complete browser session
* This creates browser, context, and page in one call
*/
async initialize(): Promise<BrowserSession> {
if (this.session) {
log.debug('Returning existing browser session')
return this.session
}
const browser = await this.launch()
const context = await this.createContext(browser)
const page = await this.createPage(context)
this.session = { browser, context, page }
log.info('Browser session initialized')
return this.session
}
/**
* Get the current session
*/
getSession(): BrowserSession | null {
return this.session
}
/**
* Check if browser is running
*/
isRunning(): boolean {
return this.session?.browser?.isConnected() ?? false
}
/**
* Close the browser and cleanup
*/
async close(): Promise<void> {
if (!this.session) {
log.debug('No browser session to close')
return
}
log.info('Closing browser session')
try {
if (this.session.context) {
await this.session.context.close()
}
} catch (error) {
log.warn('Error closing context', {
error: error instanceof Error ? error.message : String(error)
})
}
try {
if (this.session.browser) {
await this.session.browser.close()
}
} catch (error) {
log.warn('Error closing browser', {
error: error instanceof Error ? error.message : String(error)
})
}
this.session = null
log.info('Browser session closed')
}
/**
* Navigate to a URL
*/
async navigate(url: string, options?: { timeout?: number }): Promise<void> {
const page = this.session?.page
if (!page) {
throw new Error('No page available. Call initialize() first.')
}
log.info('Navigating to URL', { url })
await page.goto(url, { timeout: options?.timeout ?? 30000 })
await page.waitForLoadState('domcontentloaded', { timeout: options?.timeout ?? 10000 })
log.debug('Page loaded')
}
}
export default ErpBrowserManager

View File

@@ -1,5 +1,8 @@
import { chromium, type BrowserContext, type Page } from 'playwright'
import type { ErpConfig, ErpSession } from '../../types/erp.types'
import { createLogger } from '../logger'
const log = createLogger('ErpAuthService')
/**
* ERP Authentication Service
@@ -91,7 +94,7 @@ export class ErpAuthService {
try {
await page.waitForLoadState('domcontentloaded', { timeout: 10000 })
} catch (e) {
console.log('Page load state check timed out, continuing...')
log.warn('Page load state check timed out, continuing')
}
// Handle force login confirmation dialog if present (Python: get_by_role("button", name="确定"))
@@ -99,15 +102,15 @@ export class ErpAuthService {
const confirmBtn = mainFrame.getByRole('button', { name: '确定' })
const count = await confirmBtn.count()
if (count > 0) {
console.log('Force login detected, clicking confirm button')
log.info('Force login detected, clicking confirm button')
await confirmBtn.first().click()
await page.waitForTimeout(2000)
} else {
console.log('Normal login, no confirmation dialog')
log.debug('Normal login, no confirmation dialog')
}
} catch (e) {
} catch {
// No force login dialog, continue
console.log('Normal login, no confirmation dialog')
log.debug('Normal login, no confirmation dialog')
}
// Create session with mainFrame (Python returns main_frame as part of login result)

View File

@@ -156,7 +156,7 @@ export class OrderNumberResolver {
// Update mappings with database results
for (const mapping of mappings) {
if (mapping.inputType === 'productionId') {
const dbResult = productionIdMappings.find(m => m.input === mapping.input)
const dbResult = productionIdMappings.find((m) => m.input === mapping.input)
if (dbResult) {
mapping.orderNumber = dbResult.orderNumber
mapping.isValid = dbResult.isValid
@@ -265,7 +265,7 @@ export class OrderNumberResolver {
const message = error instanceof Error ? error.message : '未知数据库错误'
// Return all as failed with error
return productionIds.map(pid => ({
return productionIds.map((pid) => ({
input: pid,
productionId: pid,
isValid: false,
@@ -294,7 +294,7 @@ export class OrderNumberResolver {
`
const result = await this.mysqlService.query(query, orderNumbers)
return result.rows.map(row => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string)
return result.rows.map((row) => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string)
} catch (error) {
console.warn('[OrderResolver] Failed to verify order numbers:', error)
return orderNumbers // Skip verification on error
@@ -345,9 +345,7 @@ export class OrderNumberResolver {
* @returns List of valid production order numbers
*/
getValidOrderNumbers(mappings: OrderMapping[]): string[] {
return mappings
.filter(m => m.isValid && m.orderNumber)
.map(m => m.orderNumber!)
return mappings.filter((m) => m.isValid && m.orderNumber).map((m) => m.orderNumber!)
}
/**
@@ -356,8 +354,6 @@ export class OrderNumberResolver {
* @returns List of warning messages
*/
getWarnings(mappings: OrderMapping[]): string[] {
return mappings
.filter(m => !m.isValid && m.error)
.map(m => m.error!)
return mappings.filter((m) => !m.isValid && m.error).map((m) => m.error!)
}
}

View File

@@ -0,0 +1,97 @@
/**
* Unified logging system using Winston
* Console + File transports with daily rotation
*/
import winston from 'winston'
import DailyRotateFile from 'winston-daily-rotate-file'
import path from 'path'
import { app } from 'electron'
import fs from 'fs'
// Get log directory - use app.getPath('logs') in production, or local logs dir in development
function getLogDir(): string {
if (app && app.isReady()) {
return app.getPath('logs')
}
// Fallback for development or before app is ready
const devLogDir = path.join(process.cwd(), 'logs')
if (!fs.existsSync(devLogDir)) {
fs.mkdirSync(devLogDir, { recursive: true })
}
return devLogDir
}
// Custom format for console output
const consoleFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, context, ...meta }) => {
const contextStr = context ? `[${context}]` : ''
const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : ''
return `${timestamp} [${level}]${contextStr} ${message}${metaStr}`
})
)
// Custom format for file output
const fileFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.json()
)
// Daily rotate file transport configuration
const createFileTransport = (level?: string): DailyRotateFile => {
return new DailyRotateFile({
filename: path.join(getLogDir(), 'app-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
level,
format: fileFormat
})
}
// Create the logger instance
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
defaultMeta: { service: 'erpauto' },
transports: [
// Console transport - always enabled
new winston.transports.Console({
format: consoleFormat
}),
// File transport for all levels
createFileTransport()
]
})
// Add error-specific file transport in production
if (process.env.NODE_ENV === 'production') {
logger.add(
new DailyRotateFile({
filename: path.join(getLogDir(), 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
level: 'error',
format: fileFormat
})
)
}
/**
* Create a child logger with a specific context
* @param context - The context/module name for the logger
* @returns A child logger instance
*/
export function createLogger(context: string): winston.Logger {
return logger.child({ context })
}
// Export the main logger for direct use
export default logger
// Export log level types for convenience
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose'

View File

@@ -229,11 +229,12 @@ export class BIPUsersDAO {
ORDER BY UserName
`
const result = this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.map(row => ({
return result.rows.map((row) => ({
id: row.ID as number,
username: row.UserName as string,
userType: row.UserType as 'Admin' | 'User' | 'Guest',

169
src/main/types/errors.ts Normal file
View File

@@ -0,0 +1,169 @@
/**
* Custom error types for the application
* Provides structured error handling with codes and context
*/
/**
* Base error class for all application errors
*/
export abstract class BaseError extends Error {
public readonly code: string
public readonly cause?: Error
constructor(name: string, message: string, code: string, cause?: Error) {
super(message)
this.name = name
this.code = code
this.cause = cause
// Maintains proper stack trace for where error was thrown (only in V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
/**
* Get a JSON representation of the error for logging/serialization
*/
toJSON(): Record<string, unknown> {
return {
name: this.name,
message: this.message,
code: this.code,
cause: this.cause?.message
}
}
}
/**
* Error codes for ERP connection errors
*/
export const ERP_ERROR_CODES = {
CONNECTION_FAILED: 'ERP_CONNECTION_FAILED',
LOGIN_FAILED: 'ERP_LOGIN_FAILED',
TIMEOUT: 'ERP_TIMEOUT',
NAVIGATION_ERROR: 'ERP_NAVIGATION_ERROR',
ELEMENT_NOT_FOUND: 'ERP_ELEMENT_NOT_FOUND',
SESSION_EXPIRED: 'ERP_SESSION_EXPIRED',
BROWSER_CRASH: 'ERP_BROWSER_CRASH'
} as const
/**
* Error thrown when ERP connection, login, or browser automation fails
*/
export class ErpConnectionError extends BaseError {
constructor(
message: string,
code: (typeof ERP_ERROR_CODES)[keyof typeof ERP_ERROR_CODES] = ERP_ERROR_CODES.CONNECTION_FAILED,
cause?: Error
) {
super('ErpConnectionError', message, code, cause)
}
}
/**
* Error codes for database errors
*/
export const DATABASE_ERROR_CODES = {
CONNECTION_FAILED: 'DB_CONNECTION_FAILED',
QUERY_FAILED: 'DB_QUERY_FAILED',
TIMEOUT: 'DB_TIMEOUT',
INVALID_PARAMS: 'DB_INVALID_PARAMS',
RECORD_NOT_FOUND: 'DB_RECORD_NOT_FOUND',
TRANSACTION_FAILED: 'DB_TRANSACTION_FAILED'
} as const
/**
* Error thrown when database operations fail
*/
export class DatabaseQueryError extends BaseError {
constructor(
message: string,
code: (typeof DATABASE_ERROR_CODES)[keyof typeof DATABASE_ERROR_CODES] = DATABASE_ERROR_CODES.QUERY_FAILED,
cause?: Error
) {
super('DatabaseQueryError', message, code, cause)
}
}
/**
* Error codes for validation errors
*/
export const VALIDATION_ERROR_CODES = {
INVALID_INPUT: 'VAL_INVALID_INPUT',
MISSING_REQUIRED: 'VAL_MISSING_REQUIRED',
INVALID_FORMAT: 'VAL_INVALID_FORMAT',
OUT_OF_RANGE: 'VAL_OUT_OF_RANGE',
INVALID_TYPE: 'VAL_INVALID_TYPE'
} as const
/**
* Error thrown when input validation fails
*/
export class ValidationError extends BaseError {
constructor(
message: string,
code: (typeof VALIDATION_ERROR_CODES)[keyof typeof VALIDATION_ERROR_CODES] = VALIDATION_ERROR_CODES.INVALID_INPUT,
cause?: Error
) {
super('ValidationError', message, code, cause)
}
}
/**
* Type guard to check if an error is a BaseError
*/
export function isBaseError(error: unknown): error is BaseError {
return error instanceof BaseError
}
/**
* Type guard to check if an error is an ErpConnectionError
*/
export function isErpConnectionError(error: unknown): error is ErpConnectionError {
return error instanceof ErpConnectionError
}
/**
* Type guard to check if an error is a DatabaseQueryError
*/
export function isDatabaseQueryError(error: unknown): error is DatabaseQueryError {
return error instanceof DatabaseQueryError
}
/**
* Type guard to check if an error is a ValidationError
*/
export function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError
}
/**
* Get a user-friendly error message from any error type
* In production, generic errors are sanitized to avoid leaking sensitive info
*/
export function getErrorMessage(error: unknown): string {
if (isBaseError(error)) {
// BaseError messages are developer-controlled and safe
return error.message
}
if (error instanceof Error) {
// In production, return a generic message to avoid leaking sensitive info
// (e.g., database connection strings, file paths, server names)
if (process.env.NODE_ENV === 'production') {
return 'An unexpected error occurred'
}
return error.message
}
return 'An unknown error occurred'
}
/**
* Get error code from any error type
*/
export function getErrorCode(error: unknown): string {
if (isBaseError(error)) {
return error.code
}
return 'UNKNOWN_ERROR'
}

View File

@@ -22,7 +22,11 @@ export type MatchMode = 'substring' | 'exact'
/**
* Validation data source options
*/
export type ValidationDataSource = 'database_full' | 'database_filtered' | 'excel_existing' | 'excel_full'
export type ValidationDataSource =
| 'database_full'
| 'database_filtered'
| 'excel_existing'
| 'excel_full'
/**
* ERP configuration