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>
This commit is contained in:
test
2026-03-08 15:07:03 +08:00
parent 5e6898fb40
commit 6df16898da
20 changed files with 1653 additions and 305 deletions

View File

@@ -13,6 +13,7 @@
import { ipcMain } from 'electron'
import { SessionManager } from '../services/user/session-manager'
import { createLogger } from '../services/logger'
import { logAudit } from '../services/logger/audit-logger'
import type { UserInfo } from '../types/user.types'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
import { ValidationError } from '../types/errors'
@@ -83,32 +84,45 @@ export function registerAuthHandlers(): void {
/**
* Silent login by computer name
*/
ipcMain.handle(IPC_CHANNELS.AUTH_SILENT_LOGIN, async (): Promise<IpcResult<SilentLoginResponse>> => {
return withErrorHandling(async () => {
log.info('Attempting silent login')
const success = await sessionManager.loginByComputerName()
const userInfo = sessionManager.getUserInfo()
ipcMain.handle(
IPC_CHANNELS.AUTH_SILENT_LOGIN,
async (): Promise<IpcResult<SilentLoginResponse>> => {
return withErrorHandling(async () => {
log.info('Attempting silent login')
const success = await sessionManager.loginByComputerName()
const userInfo = sessionManager.getUserInfo()
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'
log.info('Silent login successful', {
username: userInfo.username,
userType: userInfo.userType,
requiresUserSelection
})
log.info('Silent login successful', {
username: userInfo.username,
userType: userInfo.userType,
requiresUserSelection
})
return {
success: true,
userInfo,
requiresUserSelection
// Audit log: LOGIN success (non-blocking)
const os = await import('os')
logAudit('LOGIN', String(userInfo.id), {
username: userInfo.username,
computerName: os.hostname(),
resource: 'ERP_SYSTEM',
status: 'success',
metadata: { loginType: 'silent', userType: userInfo.userType }
}).catch((err) => log.warn('Failed to write audit log', { err }))
return {
success: true,
userInfo,
requiresUserSelection
}
}
}
throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT')
}, 'auth:silentLogin')
})
throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT')
}, 'auth:silentLogin')
}
)
/**
* Login with username and password
@@ -117,27 +131,48 @@ export function registerAuthHandlers(): void {
IPC_CHANNELS.AUTH_LOGIN,
async (_event, request: LoginRequest): Promise<IpcResult<LoginResponse>> => {
return withErrorHandling(async () => {
const { username, password } = request
const { username, password } = request
if (!username || !password) {
log.warn('Login attempt with missing credentials')
throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED')
}
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
if (!username || !password) {
log.warn('Login attempt with missing credentials')
throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED')
}
}
log.warn('Login failed - invalid credentials', { username })
throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT')
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 })
// Audit log: LOGIN success (non-blocking)
const os = await import('os')
logAudit('LOGIN', String(userInfo.id), {
username: userInfo.username,
computerName: os.hostname(),
resource: 'ERP_SYSTEM',
status: 'success',
metadata: { loginType: 'credentials', userType: userInfo.userType }
}).catch((err) => log.warn('Failed to write audit log', { err }))
return {
success: true,
userInfo
}
}
// Audit log: LOGIN failure (non-blocking)
const os = await import('os')
logAudit('LOGIN', '0', {
username,
computerName: os.hostname(),
resource: 'ERP_SYSTEM',
status: 'failure',
metadata: { loginType: 'credentials', reason: 'invalid_credentials' }
}).catch((err) => log.warn('Failed to write audit log', { err }))
log.warn('Login failed - invalid credentials', { username })
throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT')
}, 'auth:login')
}
)
@@ -149,6 +184,19 @@ export function registerAuthHandlers(): void {
return withErrorHandling(async () => {
const userInfo = sessionManager.getUserInfo()
log.info('User logout', { username: userInfo?.username })
// Audit log: LOGOUT (non-blocking)
if (userInfo) {
const os = await import('os')
logAudit('LOGOUT', String(userInfo.id), {
username: userInfo.username,
computerName: os.hostname(),
resource: 'ERP_SYSTEM',
status: 'success',
metadata: { userType: userInfo.userType }
}).catch((err) => log.warn('Failed to write audit log', { err }))
}
sessionManager.logout()
}, 'auth:logout')
})
@@ -156,16 +204,19 @@ export function registerAuthHandlers(): void {
/**
* Get current user
*/
ipcMain.handle(IPC_CHANNELS.AUTH_GET_CURRENT_USER, async (): Promise<IpcResult<CurrentUserResponse>> => {
return withErrorHandling(async () => {
const isAuthenticated = sessionManager.isAuthenticated()
const userInfo = sessionManager.getUserInfo()
return {
isAuthenticated,
userInfo: userInfo ?? undefined
}
}, 'auth:getCurrentUser')
})
ipcMain.handle(
IPC_CHANNELS.AUTH_GET_CURRENT_USER,
async (): Promise<IpcResult<CurrentUserResponse>> => {
return withErrorHandling(async () => {
const isAuthenticated = sessionManager.isAuthenticated()
const userInfo = sessionManager.getUserInfo()
return {
isAuthenticated,
userInfo: userInfo ?? undefined
}
}, 'auth:getCurrentUser')
}
)
/**
* Get all users (for admin user selection)
@@ -209,4 +260,3 @@ export function registerAuthHandlers(): void {
return withErrorHandling(async () => sessionManager.isAdmin(), 'auth:isAdmin')
})
}

View File

@@ -9,6 +9,7 @@ import { ResultExporter } from '../services/excel/result-exporter'
import { CleanerReportGenerator } from '../services/report/cleaner-report-generator'
import { SessionManager } from '../services/user/session-manager'
import { createLogger } from '../services/logger'
import { logAudit } from '../services/logger/audit-logger'
import { withErrorHandling, type IpcResult } from './index'
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
import type {
@@ -230,6 +231,31 @@ export function registerCleanerHandlers(): void {
errorCount: result.errors.length
})
// Audit log: CLEAN (non-blocking)
const os = await import('os')
const currentUser = SessionManager.getInstance().getUserInfo()
if (currentUser) {
const status: 'success' | 'failure' | 'partial' =
result.errors.length > 0 && result.materialsDeleted > 0
? 'partial'
: result.errors.length > 0
? 'failure'
: 'success'
logAudit('CLEAN', String(currentUser.id), {
username: currentUser.username,
computerName: os.hostname(),
resource: 'MATERIAL_PLAN',
status,
metadata: {
orderCount: validOrderNumbers.length,
dryRun: input.dryRun ?? false,
materialsDeleted: result.materialsDeleted,
materialsSkipped: result.materialsSkipped,
errorCount: result.errors.length
}
}).catch((err) => log.warn('Failed to write audit log', { err }))
}
// Generate report (silent, user unaware)
try {
const endTime = Date.now()

View File

@@ -4,6 +4,8 @@ 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'
@@ -202,6 +204,29 @@ export function registerExtractorHandlers(): void {
})
}
// 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

View File

@@ -13,6 +13,7 @@ import { registerValidationHandlers } from './validation-handler'
import { registerSettingsHandlers } from './settings-handler'
import { registerMaterialTypeHandlers } from './material-type-handler'
import { registerUserErpConfigHandlers } from './user-erp-config-handler'
import { registerLoggerHandlers } from './logger-handler'
import { createLogger } from '../services/logger'
import { getErrorMessage, getErrorCode, isBaseError } from '../types/errors'
@@ -85,5 +86,6 @@ export function registerIpcHandlers(): void {
registerSettingsHandlers()
registerMaterialTypeHandlers()
registerUserErpConfigHandlers()
registerLoggerHandlers()
log.info('All IPC handlers registered')
}

View File

@@ -0,0 +1,229 @@
/**
* IPC Logger Handler with Batching
* Receives logs from renderer process and forwards to Winston
*
* Features:
* - 100ms debounce for batch processing
* - Maximum 50 messages per batch
* - Circuit breaker: discards new logs when buffer > 500
* - Error-level logs bypass circuit breaker
*/
import { ipcMain } from 'electron'
import { createLogger } from '../services/logger'
import { IPC_CHANNELS, type LogLevel } from '../../shared/ipc-channels'
const log = createLogger('LoggerHandler')
/**
* Log entry from renderer process
*/
interface LogEntry {
level: LogLevel
message: string
context?: Record<string, unknown>
timestamp: number
}
/**
* Batch processing configuration
*/
const BATCH_CONFIG = {
DEBOUNCE_MS: 100,
MAX_BATCH_SIZE: 50,
CIRCUIT_BREAKER_THRESHOLD: 500
} as const
/**
* Logger handler state
*/
class LoggerHandlerState {
private buffer: LogEntry[] = []
private debounceTimer: NodeJS.Timeout | null = null
private discardedCount = 0
/**
* Add log entry to buffer
* @param entry - Log entry to buffer
* @returns true if entry was buffered, false if discarded
*/
addEntry(entry: LogEntry): boolean {
// Error-level logs always bypass circuit breaker
if (entry.level === 'error') {
this.buffer.push(entry)
this.flushIfNeeded()
return true
}
// Circuit breaker: discard non-error logs when buffer is too large
if (this.buffer.length >= BATCH_CONFIG.CIRCUIT_BREAKER_THRESHOLD) {
this.discardedCount++
// Log warning about discarded logs periodically (every 100 discarded)
if (this.discardedCount % 100 === 0) {
log.warn('Circuit breaker active: discarded logs', {
discardedCount: this.discardedCount,
bufferSize: this.buffer.length
})
}
return false
}
this.buffer.push(entry)
this.flushIfNeeded()
return true
}
/**
* Flush buffer if it reaches max batch size
*/
private flushIfNeeded(): void {
if (this.buffer.length >= BATCH_CONFIG.MAX_BATCH_SIZE) {
this.flush()
} else if (!this.debounceTimer) {
// Start debounce timer if not already running
this.debounceTimer = setTimeout(() => {
this.flush()
}, BATCH_CONFIG.DEBOUNCE_MS)
}
}
/**
* Flush all buffered logs to Winston
*/
flush(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer)
this.debounceTimer = null
}
if (this.buffer.length === 0) {
return
}
// Create a copy of the buffer and clear it
const batch = [...this.buffer]
this.buffer = []
// Process batch asynchronously (non-blocking)
setImmediate(() => {
this.processBatch(batch)
})
}
/**
* Process a batch of log entries
* @param batch - Array of log entries to process
*/
private processBatch(batch: LogEntry[]): void {
try {
for (const entry of batch) {
this.forwardToWinston(entry)
}
} catch (error) {
// If batch processing fails, log the error but don't rethrow
// This ensures logging failures don't crash the app
log.error('Failed to process log batch', {
error: error instanceof Error ? error.message : String(error),
batchSize: batch.length
})
}
}
/**
* Forward a single log entry to Winston logger
* @param entry - Log entry to forward
*/
private forwardToWinston(entry: LogEntry): void {
const context = (entry.context?.component as string) || 'renderer'
const childLogger = log.child({
source: 'renderer',
component: context
})
const message = entry.context?.message
? `[${entry.context.message}] ${entry.message}`
: entry.message
switch (entry.level) {
case 'debug':
childLogger.debug(message, entry.context)
break
case 'warn':
childLogger.warn(message, entry.context)
break
case 'error':
childLogger.error(message, entry.context)
break
case 'info':
default:
childLogger.info(message, entry.context)
break
}
}
/**
* Get current buffer size (for testing/debugging)
*/
getBufferSize(): number {
return this.buffer.length
}
/**
* Get discarded log count (for testing/debugging)
*/
getDiscardedCount(): number {
return this.discardedCount
}
/**
* Reset state (for testing)
*/
reset(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer)
this.debounceTimer = null
}
this.buffer = []
this.discardedCount = 0
}
}
// Singleton state instance
const state = new LoggerHandlerState()
/**
* Register IPC handlers for logger
*/
export function registerLoggerHandlers(): void {
// Use ipcMain.on with send() - fire-and-forget, non-blocking
ipcMain.on(IPC_CHANNELS.LOGGER_FORWARD, (_event, entry: LogEntry) => {
// Validate entry
if (!entry || typeof entry.level !== 'string' || typeof entry.message !== 'string') {
log.warn('Received invalid log entry', { entry })
return
}
// Add to buffer for batch processing
const buffered = state.addEntry(entry)
if (!buffered && process.env.NODE_ENV !== 'production') {
// In development, log when entries are discarded
log.debug('Log entry discarded due to circuit breaker', {
level: entry.level,
message: entry.message
})
}
})
log.info('Logger IPC handler registered', {
channel: IPC_CHANNELS.LOGGER_FORWARD,
debounceMs: BATCH_CONFIG.DEBOUNCE_MS,
maxBatchSize: BATCH_CONFIG.MAX_BATCH_SIZE,
circuitBreakerThreshold: BATCH_CONFIG.CIRCUIT_BREAKER_THRESHOLD
})
}
// Export for testing
export { state }

View File

@@ -5,6 +5,7 @@ import { UserErpConfigService } from '../services/user/user-erp-config-service'
import { MySqlService } from '../services/database/mysql'
import { SqlServerService } from '../services/database/sql-server'
import { createLogger } from '../services/logger'
import { logAudit } from '../services/logger/audit-logger'
import type { UserType, ConnectionTestResult, SaveSettingsResult } from '../types/settings.types'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
import { ValidationError } from '../types/errors'
@@ -60,6 +61,16 @@ export function registerSettingsHandlers(): void {
username: settings.erp.username || '',
password: settings.erp.password || ''
})
// Audit log: SETTINGS_CHANGE (non-blocking)
const os = await import('os')
logAudit('SETTINGS_CHANGE', String(currentUser.id), {
username: currentUser.username,
computerName: os.hostname(),
resource: 'ERP_CONFIG',
status: 'success',
metadata: { changeType: 'erp_credentials', usernameChanged: !!settings.erp.username }
}).catch((err) => log.warn('Failed to write audit log', { err }))
}
return { success: true }
@@ -164,4 +175,3 @@ export function registerSettingsHandlers(): void {
}
)
}

View File

@@ -3,6 +3,9 @@ import { ErpAuthService } from './erp-auth'
import type { CleanerInput, CleanerResult, OrderCleanDetail } from '../../types/cleaner.types'
import type { ErpSession } from '../../types/erp.types'
import type { FrameLocator, Locator, Page } from 'playwright'
import { createLogger } from '../logger'
const log = createLogger('CleanerService')
/**
* Cleaner Service Options
@@ -103,6 +106,13 @@ export class CleanerService {
}
const totalOrders = input.orderNumbers.length
const dryRun = input.dryRun ?? this.dryRun
log.info('Starting cleaner', {
totalOrders,
materialCount: input.materialCodes.length,
dryRun
})
// Create delete set for O(1) lookup
const deleteSet = new Set(input.materialCodes)
@@ -121,6 +131,7 @@ export class CleanerService {
const orderNumber = input.orderNumbers[i]
try {
log.debug('Processing order', { orderNumber, index: i + 1, total: totalOrders })
const detail = await this.processOrder({
workFrame,
popupPage,
@@ -138,6 +149,7 @@ export class CleanerService {
result.materialsSkipped += detail.materialsSkipped
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Order processing failed', { orderNumber, error: message })
result.errors.push(`Order ${orderNumber}: ${message}`)
// Add error detail
@@ -153,8 +165,15 @@ export class CleanerService {
// Close popup page
await popupPage.close()
log.info('Cleaner completed', {
ordersProcessed: result.ordersProcessed,
materialsDeleted: result.materialsDeleted,
materialsSkipped: result.materialsSkipped,
errorCount: result.errors.length
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Cleaner failed', { error: message })
result.errors.push(`Clean failed: ${message}`)
}

View File

@@ -6,6 +6,9 @@ import type {
ExtractorCoreResult,
ExtractionProgress
} from '../../types/extractor.types'
import { createLogger } from '../logger'
const log = createLogger('ExtractorCore')
/**
* ExtractorCore - Handles all web page operations for data extraction

View File

@@ -11,6 +11,9 @@ import type {
ExtractionProgress
} from '../../types/extractor.types'
import { DataImportService } from '../database/data-importer'
import { createLogger } from '../logger'
const log = createLogger('ExtractorService')
/**
* ERP Data Extractor Service
@@ -127,7 +130,7 @@ export class ExtractorService {
return { mergedFile: null, recordCount: 0 }
}
console.log(`[Extractor] Starting merge of ${filePaths.length} files`)
log.info('Starting merge', { fileCount: filePaths.length })
const parser = new ExcelParser({ verbose: true })
// Collect all orders with full order info and materials
@@ -137,17 +140,17 @@ export class ExtractorService {
// Parse each downloaded file and collect orders
for (const filePath of filePaths) {
try {
console.log(`[Extractor] Parsing file: ${filePath}`)
log.debug('Parsing file', { filePath })
await parser.parse(filePath)
// After parse(), the parser stores orders internally as lastOrders
// After parse(), the parser store orders internally as lastOrders
const orders = (parser as any).lastOrders
console.log(`[Extractor] Parsed ${orders?.length || 0} orders from ${filePath}`)
log.debug('File parsed', { filePath, orderCount: orders?.length || 0 })
if (orders && Array.isArray(orders)) {
allOrders.push(...orders)
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
console.error(`[Extractor] Failed to parse file ${filePath}:`, errorMsg)
log.error('Failed to parse file', { filePath, error: errorMsg })
}
}
@@ -157,10 +160,10 @@ export class ExtractorService {
recordCount += order.materials.length
}
console.log(`[Extractor] Total orders: ${allOrders.length}, total records: ${recordCount}`)
log.info('Merge summary', { orderCount: allOrders.length, recordCount })
if (recordCount === 0) {
console.warn('[Extractor] No records found in any of the downloaded files')
log.warn('No records found in any downloaded files')
return { mergedFile: null, recordCount: 0 }
}
@@ -174,17 +177,16 @@ export class ExtractorService {
// Save with error handling
try {
console.log(`[Extractor] Saving merged file to: ${outputPath}`)
log.info('Saving merged file', { outputPath })
await this.saveMergedOrders(allOrders, outputPath)
console.log(`[Extractor] Successfully saved merged file with ${recordCount} records`)
log.info('Merged file saved successfully', { recordCount })
return { mergedFile: outputPath, recordCount }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
const errorStack = error instanceof Error ? error.stack : ''
console.error(`[Extractor] Failed to save merged file: ${errorMsg}`)
console.error(`[Extractor] Error stack: ${errorStack}`)
log.error('Failed to save merged file', { error: errorMsg, stack: errorStack })
// Return parsed record count and error info even if save fails
return { mergedFile: null, recordCount, error: `保存合并文件失败: ${errorMsg}` }
return { mergedFile: null, recordCount, error: `保存合并文件失败${errorMsg}` }
}
}
@@ -196,11 +198,11 @@ export class ExtractorService {
orders: Array<{ orderInfo: any; materials: any[] }>,
outputPath: string
): Promise<void> {
console.log(`[Extractor] Loading ExcelJS...`)
log.debug('Loading ExcelJS')
const ExcelJSModule = await import('exceljs')
// Handle both ESM and CommonJS module formats
const ExcelJS = ExcelJSModule.default || ExcelJSModule
console.log(`[Extractor] ExcelJS loaded, creating workbook...`)
log.debug('ExcelJS loaded, creating workbook')
const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet('Data')
@@ -239,7 +241,7 @@ export class ExtractorService {
{ header: '打印日期', key: 'printDate', width: 20 }
]
console.log(`[Extractor] Adding ${orders.length} orders to worksheet...`)
log.debug('Adding orders to worksheet', { orderCount: orders.length })
// Add data rows - merge orderInfo with each material
for (const order of orders) {
const { orderInfo, materials } = order
@@ -283,9 +285,9 @@ export class ExtractorService {
}
}
console.log(`[Extractor] Writing file to ${outputPath}...`)
log.debug('Writing file', { outputPath })
await workbook.xlsx.writeFile(outputPath)
console.log(`[Extractor] File saved successfully: ${outputPath}`)
log.debug('File saved successfully', { outputPath })
}
/**
@@ -296,10 +298,10 @@ export class ExtractorService {
for (const filePath of filePaths) {
try {
await fs.unlink(filePath)
console.log(`Deleted temporary file: ${filePath}`)
log.debug('Deleted temporary file', { filePath })
} catch (error) {
// Log error but don't fail the main process
console.error(`Failed to delete temporary file ${filePath}:`, error)
log.error('Failed to delete temporary file', { filePath, error })
}
}
}
@@ -310,14 +312,14 @@ export class ExtractorService {
* @returns Import result with statistics
*/
private async importToDatabase(filePath: string): Promise<ImportResult> {
console.log(`[Extractor] Starting database import from: ${filePath}`)
log.info('Starting database import', { filePath })
const importService = new DataImportService()
try {
const result = await importService.importFromExcel(filePath, 1000)
console.log(`[Extractor] Import completed`, {
log.info('Import completed', {
success: result.success,
recordsRead: result.recordsRead,
recordsDeleted: result.recordsDeleted,
@@ -327,7 +329,7 @@ export class ExtractorService {
return result
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
console.error(`[Extractor] Import failed: ${errorMsg}`)
log.error('Import failed', { error: errorMsg })
return {
success: false,
@@ -350,7 +352,7 @@ export class ExtractorService {
filePath: string,
onLog?: (level: LogLevel, message: string) => void
): Promise<ImportResult> {
console.log(`[Extractor] Starting database import from: ${filePath}`)
log.info('Starting database import', { filePath })
onLog?.('info', `开始导入数据到数据库...`)
const importService = new DataImportService()
@@ -358,7 +360,7 @@ export class ExtractorService {
try {
const result = await importService.importFromExcel(filePath, 1000)
console.log(`[Extractor] Import completed`, {
log.info('Import completed', {
success: result.success,
recordsRead: result.recordsRead,
recordsDeleted: result.recordsDeleted,
@@ -377,7 +379,7 @@ export class ExtractorService {
return result
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
console.error(`[Extractor] Import failed: ${errorMsg}`)
log.error('Import failed', { error: errorMsg })
onLog?.('error', `导入失败:${errorMsg}`)
return {

View File

@@ -1,6 +1,9 @@
import ExcelJS from 'exceljs'
import type { DiscreteMaterialPlan, ExcelParseOptions, OrderHeader } from '../../types/excel.types'
import path from 'path'
import { createLogger } from '../logger'
const log = createLogger('ExcelParser')
/**
* Excel Parser Service
@@ -56,17 +59,11 @@ export class ExcelParser {
this.verbose = options.verbose || false
}
private log(...args: any[]): void {
if (this.verbose) {
console.log('[ExcelParser]', ...args)
}
}
/**
* Parse Excel file and extract material plans
*/
async parse(filePath: string, options: ExcelParseOptions = {}): Promise<DiscreteMaterialPlan[]> {
this.log('Parsing Excel file:', filePath)
log.debug('Parsing Excel file:', filePath)
const workbook = new ExcelJS.Workbook()
await workbook.xlsx.readFile(filePath)
@@ -84,7 +81,7 @@ export class ExcelParser {
allRows.push(row.values as any[])
})
this.log(`Total rows read: ${allRows.length} (worksheet has ${worksheet.rowCount} rows)`)
log.debug(`Total rows read: ${allRows.length} (worksheet has ${worksheet.rowCount} rows)`)
// Parse orders from rows
const orders = this.parseOrders(allRows)
@@ -98,7 +95,7 @@ export class ExcelParser {
// Skip empty orders if option is set
if (options.skipEmptyOrders && materials.length === 0) {
this.log('Skipping empty order:', orderInfo.productionOrder)
log.debug('Skipping empty order:', orderInfo.productionOrder)
continue
}
@@ -125,7 +122,7 @@ export class ExcelParser {
}
}
this.log(`Parsed ${plans.length} material plans from ${orders.length} orders`)
log.debug(`Parsed ${plans.length} material plans from ${orders.length} orders`)
return plans
}
@@ -143,7 +140,7 @@ export class ExcelParser {
throw new Error('No parsed data available. Call parse() first.')
}
this.log('Saving parsed data to Excel:', outputPath)
log.debug('Saving parsed data to Excel:', outputPath)
const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet('Data')
@@ -226,7 +223,7 @@ export class ExcelParser {
// Save workbook
await workbook.xlsx.writeFile(outputPath)
this.log(
log.debug(
`Excel file saved: ${outputPath} (${orders.length} orders, ${worksheet.rowCount - 1} data rows)`
)
}
@@ -253,7 +250,7 @@ export class ExcelParser {
}
// Debug: check productionOrder extraction
this.log(`Order ${orders.length + 1}: productionOrder="${orderInfo.productionOrder}"`)
log.debug(`Order ${orders.length + 1}: productionOrder="${orderInfo.productionOrder}"`)
// Find table header row dynamically (look for "序号" in index 1)
// Note: worksheet.eachRow() skips empty rows, so we can't use fixed offsets
@@ -263,7 +260,7 @@ export class ExcelParser {
}
if (tableRow >= allRows.length || !allRows[tableRow]) {
this.log(' ⚠️ Table header not found, skipping this order')
log.debug(' ⚠️ Table header not found, skipping this order')
i++
continue
}
@@ -280,7 +277,7 @@ export class ExcelParser {
if (isEmptyRow) {
// No data, find footer info
this.log('Order has no material data')
log.debug('Order has no material data')
const materials: any[] = []
const footerInfo: OrderHeader = {}
let dataRow = nextRow + 1
@@ -306,7 +303,7 @@ export class ExcelParser {
})
} else {
// Has data, extract materials
this.log('Order has material data')
log.debug('Order has material data')
const materials: any[] = []
const footerInfo: OrderHeader = {}
let dataRow = tableRow + 1
@@ -332,7 +329,7 @@ export class ExcelParser {
if (dataRow + 1 < allRows.length && allRows[dataRow + 1]) {
this.parseHeaderRow(allRows[dataRow + 1], footerInfo)
}
this.log(' Found footer row, stopping material parsing')
log.debug(' Found footer row, stopping material parsing')
break
}
@@ -340,7 +337,7 @@ export class ExcelParser {
// Next row is footer, parse current row as material first
const material = this.parseMaterialRowInternal(allRows[dataRow], dataRow + 1)
if (material) {
this.log(' Parsed material:', material.materialCode)
log.debug(' Parsed material:', material.materialCode)
materials.push(material)
}
@@ -349,17 +346,17 @@ export class ExcelParser {
if (dataRow + 2 < allRows.length && allRows[dataRow + 2]) {
this.parseHeaderRow(allRows[dataRow + 2], footerInfo)
}
this.log(' Found footer in next row, stopping material parsing')
log.debug(' Found footer in next row, stopping material parsing')
break
}
// Extract material data
const material = this.parseMaterialRowInternal(allRows[dataRow], dataRow + 1)
if (material) {
//this.log(' Parsed material:', material.materialCode)
//log.debug(' Parsed material:', material.materialCode)
materials.push(material)
} else {
this.log(' Skipped material row at', dataRow + 1)
log.debug(' Skipped material row at', dataRow + 1)
}
dataRow++
@@ -371,7 +368,7 @@ export class ExcelParser {
})
}
} else {
this.log(
log.debug(
` ⚠️ Table header check failed at row ${tableRow + 1}, value="${allRows[tableRow] ? allRows[tableRow][1] : 'null'}"`
)
}
@@ -383,7 +380,7 @@ export class ExcelParser {
}
}
this.log(`parseOrders: Returning ${orders.length} orders`)
log.debug(`parseOrders: Returning ${orders.length} orders`)
return orders
}

View File

@@ -13,6 +13,9 @@ import { SqlServerService } from '../database/sql-server'
import { ConfigManager } from '../config/config-manager'
import sql from 'mssql'
import type { UserInfo } from '../../types/user.types'
import { createLogger } from '../logger'
const log = createLogger('BipUsersDao')
/**
* Database configuration for BIPUsers table
@@ -160,7 +163,7 @@ export class BIPUsersDAO {
return null
}
} catch (error) {
console.error('[BIPUsersDAO] Authenticate error:', error)
log.error('Authenticate error:', error)
return null
}
}
@@ -215,7 +218,7 @@ export class BIPUsersDAO {
return null
}
} catch (error) {
console.error('[BIPUsersDAO] Authenticate by computer name error:', error)
log.error('Authenticate by computer name error:', error)
return null
}
}
@@ -247,7 +250,7 @@ export class BIPUsersDAO {
createTime: row.CreateTime as Date | undefined
}))
} catch (error) {
console.error('[BIPUsersDAO] Get all users error:', error)
log.error('Get all users error:', error)
return []
}
}
@@ -331,7 +334,7 @@ export class BIPUsersDAO {
return true
}
} catch (error) {
console.error('[BIPUsersDAO] Create user error:', error)
log.error('Create user error:', error)
return false
}
}
@@ -370,7 +373,7 @@ export class BIPUsersDAO {
return true
}
} catch (error) {
console.error('[BIPUsersDAO] Update user type error:', error)
log.error('Update user type error:', error)
return false
}
}
@@ -409,7 +412,7 @@ export class BIPUsersDAO {
return true
}
} catch (error) {
console.error('[BIPUsersDAO] Update password error:', error)
log.error('Update password error:', error)
return false
}
}
@@ -444,7 +447,7 @@ export class BIPUsersDAO {
return true
}
} catch (error) {
console.error('[BIPUsersDAO] Delete user error:', error)
log.error('Delete user error:', error)
return false
}
}
@@ -481,7 +484,7 @@ export class BIPUsersDAO {
return result.rows.length > 0 && (result.rows[0].count as number) > 0
}
} catch (error) {
console.error('[BIPUsersDAO] User exists error:', error)
log.error('User exists error:', error)
return false
}
}
@@ -538,7 +541,7 @@ export class BIPUsersDAO {
return null
}
} catch (error) {
console.error('[BIPUsersDAO] Get user ERP credentials error:', error)
log.error('Get user ERP credentials error:', error)
return null
}
}
@@ -586,7 +589,7 @@ export class BIPUsersDAO {
return true
}
} catch (error) {
console.error('[BIPUsersDAO] Update user ERP credentials error:', error)
log.error('Update user ERP credentials error:', error)
return false
}
}
@@ -624,7 +627,7 @@ export class BIPUsersDAO {
erpUsername: (row[cols.ERP_USERNAME] as string) || ''
}))
} catch (error) {
console.error('[BIPUsersDAO] Get all users ERP config error:', error)
log.error('Get all users ERP config error:', error)
return []
}
}