Files
BIPMaterialManager/src/main/ipc/extractor-handler.ts
test 6df16898da feat(logging): Wave 2 - integrate logging throughout application
This commit integrates the logging infrastructure across the entire application:

IPC Layer:
- Add logger-handler.ts with centralized IPC logging channels
- Integrate audit logging into auth, cleaner, extractor handlers
- Add structured logging for IPC operations and data flow

Service Layer:
- Add logger integration to ERP services (extractor, cleaner)
- Integrate logging into excel-parser and user DAO
- Add operation tracking and error logging

Renderer Layer:
- Add useLogger hook for component-level logging
- Update App.tsx with session and user activity logging
- Enable frontend audit trail for critical actions

Testing:
- Add comprehensive IPC logging integration tests
- Enhance unit test coverage for logger and audit-logger
- Add end-to-end logging flow validation

Types:
- Update preload type definitions for logging APIs

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 15:07:03 +08:00

260 lines
9.2 KiB
TypeScript

import { ipcMain, type WebContents } from 'electron'
import { ErpAuthService } from '../services/erp/erp-auth'
import { ExtractorService } from '../services/erp/extractor'
import { OrderNumberResolver } from '../services/erp/order-resolver'
import { create, type IDatabaseService } from '../services/database'
import { createLogger } from '../services/logger'
import { logAudit } from '../services/logger/audit-logger'
import { SessionManager } from '../services/user/session-manager'
import { withErrorHandling, type IpcResult } from './index'
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
import type { ExtractorInput, ExtractorResult, ExtractionProgress } from '../types/extractor.types'
import { UserErpConfigService } from '../services/user/user-erp-config-service'
import { ConfigManager } from '../services/config/config-manager'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
const log = createLogger('ExtractorHandler')
function sendProgress(
sender: WebContents,
message: string,
progress: number,
extra?: Partial<ExtractionProgress>
): void {
try {
const progressData = { message, progress, ...extra }
sender.send(IPC_CHANNELS.EXTRACTOR_PROGRESS, progressData)
} catch (error) {
log.warn('Failed to send progress event', { error })
}
}
function sendLog(sender: WebContents, level: string, message: string): void {
try {
sender.send(IPC_CHANNELS.EXTRACTOR_LOG, { level, message })
} catch (error) {
log.warn('Failed to send log event', { error })
}
}
/**
* Get ERP configuration for current user
* URL is from config.yaml (fixed infrastructure)
* Username and password are from user's database config
*/
async function getErpConfig(): Promise<{
url: string
username: string
password: string
}> {
// Get ERP URL from config.yaml (fixed for all users)
const configManager = ConfigManager.getInstance()
const globalConfig = configManager.getConfig()
const erpUrl = globalConfig.erp.url
// Get username and password from user's database config
const erpConfigService = UserErpConfigService.getInstance()
const userConfig = await erpConfigService.getCurrentUserErpConfig()
if (!userConfig || !userConfig.username || !userConfig.password) {
throw new ValidationError(
'ERP 配置不完整。请在设置中配置 ERP 用户名和密码',
'VAL_MISSING_REQUIRED'
)
}
return {
url: erpUrl,
username: userConfig.username,
password: userConfig.password
}
}
/**
* Register IPC handlers for extractor service
*/
export function registerExtractorHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.EXTRACTOR_RUN,
async (event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => {
const sender = event.sender
return withErrorHandling(async () => {
let authService: ErpAuthService | null = null
let dbService: IDatabaseService | null = null
try {
// Get ERP configuration from database for current user
log.info('Fetching ERP configuration from database...')
const erpConfig = await getErpConfig()
log.info('ERP config retrieved', {
url: erpConfig.url ? 'configured' : 'EMPTY',
username: erpConfig.username ? 'configured' : 'EMPTY'
})
// Create database service using factory
log.info('Connecting to database for order resolution...')
sendProgress(sender, '连接数据库...', 3.33, {
phase: 'login',
subProgress: { step: '连接数据库', current: 1, total: 3 }
})
sendLog(sender, 'system', '正在连接数据库...')
try {
dbService = await create()
} catch (error) {
throw new DatabaseQueryError(
'数据库连接失败',
'DB_CONNECTION_FAILED',
error instanceof Error ? error : undefined
)
}
// Resolve order numbers (convert productionIDs to 生产订单号)
sendProgress(sender, '解析订单号...', 6.67, {
phase: 'login',
subProgress: { step: '解析订单号', current: 2, total: 3 }
})
sendLog(sender, 'info', '正在解析订单号...')
const resolver = new OrderNumberResolver(dbService)
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 })
sendLog(sender, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`)
// Create auth service and login
authService = new ErpAuthService({
url: erpConfig.url,
username: erpConfig.username,
password: erpConfig.password,
headless: true
})
sendProgress(sender, '登录 ERP 系统...', 9.99, {
phase: 'login',
subProgress: { step: '登录 ERP 系统', current: 3, total: 3 }
})
sendLog(sender, 'system', '正在登录 ERP 系统...')
log.info('Logging in to ERP...')
try {
await authService.login()
} catch (error) {
const errorMsg = error instanceof Error ? error.message : '未知错误'
sendLog(sender, 'error', `ERP 登录失败:${errorMsg}`)
throw new ErpConnectionError(
'ERP 登录失败',
'ERP_LOGIN_FAILED',
error instanceof Error ? error : undefined
)
}
log.info('Login successful')
sendLog(sender, 'success', 'ERP 登录成功')
// 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,
onProgress: (message, progress, extra) => {
sendProgress(sender, message, progress, extra)
sendLog(sender, 'info', message)
},
onLog: (level, message) => {
sendLog(sender, level, message)
}
}
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
})
// Log detailed error information if any errors occurred
if (result.errors.length > 0) {
log.warn('Extraction errors occurred', { errors: result.errors })
result.errors.forEach((err, index) => {
log.error(`Error ${index + 1}/${result.errors.length}: ${err}`)
})
}
// Audit log: EXTRACT (non-blocking)
const os = await import('os')
const currentUser = SessionManager.getInstance().getUserInfo()
if (currentUser) {
const status: 'success' | 'failure' | 'partial' =
result.errors.length > 0 && result.recordCount > 0
? 'partial'
: result.errors.length > 0
? 'failure'
: 'success'
logAudit('EXTRACT', String(currentUser.id), {
username: currentUser.username,
computerName: os.hostname(),
resource: 'MATERIAL_PLAN',
status,
metadata: {
orderCount: validOrderNumbers.length,
recordCount: result.recordCount,
errorCount: result.errors.length
}
}).catch((err) => log.warn('Failed to write audit log', { err }))
}
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 database
if (dbService) {
try {
await dbService.disconnect()
log.debug('Database disconnected')
} catch (closeError) {
log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
}
}
}, 'extractor:run')
}
)
}